| | <script lang="ts"> |
| | import { getContext, createEventDispatcher, onDestroy } from 'svelte'; |
| | import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte'; |
| | |
| | const dispatch = createEventDispatcher(); |
| | const i18n = getContext('i18n'); |
| | |
| | import { onMount, tick } from 'svelte'; |
| | |
| | import { writable } from 'svelte/store'; |
| | import { models, showOverview, theme, user } from '$lib/stores'; |
| | |
| | import '@xyflow/svelte/dist/style.css'; |
| | |
| | import CustomNode from './Overview/Node.svelte'; |
| | import Flow from './Overview/Flow.svelte'; |
| | import XMark from '../icons/XMark.svelte'; |
| | import ArrowLeft from '../icons/ArrowLeft.svelte'; |
| | |
| | const { width, height } = useStore(); |
| | |
| | const { fitView, getViewport } = useSvelteFlow(); |
| | const nodesInitialized = useNodesInitialized(); |
| | |
| | export let history; |
| | |
| | let selectedMessageId = null; |
| | |
| | const nodes = writable([]); |
| | const edges = writable([]); |
| | |
| | const nodeTypes = { |
| | custom: CustomNode |
| | }; |
| | |
| | $: if (history) { |
| | drawFlow(); |
| | } |
| | |
| | $: if (history && history.currentId) { |
| | focusNode(); |
| | } |
| | |
| | const focusNode = async () => { |
| | if (selectedMessageId === null) { |
| | await fitView({ nodes: [{ id: history.currentId }] }); |
| | } else { |
| | await fitView({ nodes: [{ id: selectedMessageId }] }); |
| | } |
| | |
| | selectedMessageId = null; |
| | }; |
| | |
| | const drawFlow = async () => { |
| | const nodeList = []; |
| | const edgeList = []; |
| | const levelOffset = 150; |
| | const siblingOffset = 250; |
| | |
| | |
| | let positionMap = new Map(); |
| | |
| | |
| | function createLabel(content) { |
| | const maxLength = 100; |
| | return content.length > maxLength ? content.substr(0, maxLength) + '...' : content; |
| | } |
| | |
| | |
| | let layerWidths = {}; |
| | |
| | Object.keys(history.messages).forEach((id) => { |
| | const message = history.messages[id]; |
| | const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0; |
| | if (!layerWidths[level]) layerWidths[level] = 0; |
| | |
| | positionMap.set(id, { |
| | id: message.id, |
| | level, |
| | position: layerWidths[level]++ |
| | }); |
| | }); |
| | |
| | |
| | Object.keys(history.messages).forEach((id) => { |
| | const pos = positionMap.get(id); |
| | const xOffset = pos.position * siblingOffset; |
| | const y = pos.level * levelOffset; |
| | const x = xOffset; |
| | |
| | nodeList.push({ |
| | id: pos.id, |
| | type: 'custom', |
| | data: { |
| | user: $user, |
| | message: history.messages[id], |
| | model: $models.find((model) => model.id === history.messages[id].model) |
| | }, |
| | position: { x, y } |
| | }); |
| | |
| | |
| | const parentId = history.messages[id].parentId; |
| | if (parentId) { |
| | edgeList.push({ |
| | id: parentId + '-' + pos.id, |
| | source: parentId, |
| | target: pos.id, |
| | selectable: false, |
| | class: ' dark:fill-gray-300 fill-gray-300', |
| | type: 'smoothstep', |
| | animated: history.currentId === id || recurseCheckChild(id, history.currentId) |
| | }); |
| | } |
| | }); |
| | |
| | await edges.set([...edgeList]); |
| | await nodes.set([...nodeList]); |
| | }; |
| | |
| | const recurseCheckChild = (nodeId, currentId) => { |
| | const node = history.messages[nodeId]; |
| | return ( |
| | node.childrenIds && |
| | node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId)) |
| | ); |
| | }; |
| | |
| | onMount(() => { |
| | drawFlow(); |
| | |
| | nodesInitialized.subscribe(async (initialized) => { |
| | if (initialized) { |
| | await tick(); |
| | const res = await fitView({ nodes: [{ id: history.currentId }] }); |
| | } |
| | }); |
| | |
| | width.subscribe((value) => { |
| | if (value) { |
| | |
| | fitView({ nodes: [{ id: history.currentId }] }); |
| | } |
| | }); |
| | |
| | height.subscribe((value) => { |
| | if (value) { |
| | |
| | fitView({ nodes: [{ id: history.currentId }] }); |
| | } |
| | }); |
| | }); |
| | |
| | onDestroy(() => { |
| | console.log('Overview destroyed'); |
| | |
| | nodes.set([]); |
| | edges.set([]); |
| | }); |
| | </script> |
| |
|
| | <div class="w-full h-full relative"> |
| | <div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3.5"> |
| | <div class="flex items-center gap-2.5"> |
| | <button |
| | class="self-center p-0.5" |
| | on:click={() => { |
| | showOverview.set(false); |
| | }} |
| | > |
| | <ArrowLeft className="size-3.5" /> |
| | </button> |
| | <div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div> |
| | </div> |
| | <button |
| | class="self-center p-0.5" |
| | on:click={() => { |
| | dispatch('close'); |
| | showOverview.set(false); |
| | }} |
| | > |
| | <XMark className="size-3.5" /> |
| | </button> |
| | </div> |
| |
|
| | {#if $nodes.length > 0} |
| | <Flow |
| | {nodes} |
| | {nodeTypes} |
| | {edges} |
| | on:nodeclick={(e) => { |
| | console.log(e.detail.node.data); |
| | dispatch('nodeclick', e.detail); |
| | selectedMessageId = e.detail.node.data.message.id; |
| | fitView({ nodes: [{ id: selectedMessageId }] }); |
| | }} |
| | /> |
| | {/if} |
| | </div> |
| |
|