<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; // Vertical spacing between layers const siblingOffset = 250; // Horizontal spacing between nodes at the same layer // Map to keep track of node positions at each level let positionMap = new Map(); // Helper function to truncate labels function createLabel(content) { const maxLength = 100; return content.length > maxLength ? content.substr(0, maxLength) + '...' : content; } // Create nodes and map children to ensure alignment in width let layerWidths = {}; // Track widths of each layer 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]++ }); }); // Adjust positions based on siblings count to centralize vertical spacing 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 } }); // Create edges 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(); fitView({ nodes: [{ id: history.currentId }] }); } }); height.subscribe((value) => { if (value) { // fitView(); 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>