| <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> |
|
|