Overview.svelte 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <script lang="ts">
  2. import { getContext, createEventDispatcher, onDestroy } from 'svelte';
  3. import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
  4. const dispatch = createEventDispatcher();
  5. const i18n = getContext('i18n');
  6. import { onMount, tick } from 'svelte';
  7. import { writable } from 'svelte/store';
  8. import { models, showOverview, theme, user } from '$lib/stores';
  9. import '@xyflow/svelte/dist/style.css';
  10. import CustomNode from './Overview/Node.svelte';
  11. import Flow from './Overview/Flow.svelte';
  12. import XMark from '../icons/XMark.svelte';
  13. import ArrowLeft from '../icons/ArrowLeft.svelte';
  14. const { width, height } = useStore();
  15. const { fitView, getViewport } = useSvelteFlow();
  16. const nodesInitialized = useNodesInitialized();
  17. export let history;
  18. let selectedMessageId = null;
  19. const nodes = writable([]);
  20. const edges = writable([]);
  21. let layoutDirection = 'vertical';
  22. const nodeTypes = {
  23. custom: CustomNode
  24. };
  25. $: if (history) {
  26. drawFlow(layoutDirection);
  27. }
  28. $: if (history && history.currentId) {
  29. focusNode();
  30. }
  31. const focusNode = async () => {
  32. if (selectedMessageId === null) {
  33. await fitView({ nodes: [{ id: history.currentId }] });
  34. } else {
  35. await fitView({ nodes: [{ id: selectedMessageId }] });
  36. }
  37. selectedMessageId = null;
  38. };
  39. const drawFlow = async (direction) => {
  40. const nodeList = [];
  41. const edgeList = [];
  42. const levelOffset = direction === 'vertical' ? 150 : 300;
  43. const siblingOffset = direction === 'vertical' ? 250 : 150;
  44. // Map to keep track of node positions at each level
  45. let positionMap = new Map();
  46. // Helper function to truncate labels
  47. function createLabel(content) {
  48. const maxLength = 100;
  49. return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
  50. }
  51. // Create nodes and map children to ensure alignment in width
  52. let layerWidths = {}; // Track widths of each layer
  53. Object.keys(history.messages).forEach((id) => {
  54. const message = history.messages[id];
  55. const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
  56. if (!layerWidths[level]) layerWidths[level] = 0;
  57. positionMap.set(id, {
  58. id: message.id,
  59. level,
  60. position: layerWidths[level]++
  61. });
  62. });
  63. // Adjust positions based on siblings count to centralize vertical spacing
  64. Object.keys(history.messages).forEach((id) => {
  65. const pos = positionMap.get(id);
  66. const x = direction === 'vertical' ? pos.position * siblingOffset : pos.level * levelOffset;
  67. const y = direction === 'vertical' ? pos.level * levelOffset : pos.position * siblingOffset;
  68. nodeList.push({
  69. id: pos.id,
  70. type: 'custom',
  71. data: {
  72. user: $user,
  73. message: history.messages[id],
  74. model: $models.find((model) => model.id === history.messages[id].model)
  75. },
  76. position: { x, y }
  77. });
  78. // Create edges
  79. const parentId = history.messages[id].parentId;
  80. if (parentId) {
  81. edgeList.push({
  82. id: parentId + '-' + pos.id,
  83. source: parentId,
  84. target: pos.id,
  85. selectable: false,
  86. class: ' dark:fill-gray-300 fill-gray-300',
  87. type: 'smoothstep',
  88. animated: history.currentId === id || recurseCheckChild(id, history.currentId)
  89. });
  90. }
  91. });
  92. await edges.set([...edgeList]);
  93. await nodes.set([...nodeList]);
  94. };
  95. const recurseCheckChild = (nodeId, currentId) => {
  96. const node = history.messages[nodeId];
  97. return (
  98. node.childrenIds &&
  99. node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
  100. );
  101. };
  102. const setLayoutDirection = (direction) => {
  103. layoutDirection = direction;
  104. drawFlow(layoutDirection);
  105. };
  106. onMount(() => {
  107. drawFlow(layoutDirection);
  108. nodesInitialized.subscribe(async (initialized) => {
  109. if (initialized) {
  110. await tick();
  111. const res = await fitView({ nodes: [{ id: history.currentId }] });
  112. }
  113. });
  114. width.subscribe((value) => {
  115. if (value) {
  116. // fitView();
  117. fitView({ nodes: [{ id: history.currentId }] });
  118. }
  119. });
  120. height.subscribe((value) => {
  121. if (value) {
  122. // fitView();
  123. fitView({ nodes: [{ id: history.currentId }] });
  124. }
  125. });
  126. });
  127. onDestroy(() => {
  128. console.log('Overview destroyed');
  129. nodes.set([]);
  130. edges.set([]);
  131. });
  132. </script>
  133. <div class="w-full h-full relative">
  134. <div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
  135. <div class="flex items-center gap-2.5">
  136. <button
  137. class="self-center p-0.5"
  138. on:click={() => {
  139. showOverview.set(false);
  140. }}
  141. >
  142. <ArrowLeft className="size-3.5" />
  143. </button>
  144. <div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
  145. </div>
  146. <button
  147. class="self-center p-0.5"
  148. on:click={() => {
  149. dispatch('close');
  150. showOverview.set(false);
  151. }}
  152. >
  153. <XMark className="size-3.5" />
  154. </button>
  155. </div>
  156. {#if $nodes.length > 0}
  157. <Flow
  158. {nodes}
  159. {nodeTypes}
  160. {edges}
  161. {setLayoutDirection}
  162. on:nodeclick={(e) => {
  163. console.log(e.detail.node.data);
  164. dispatch('nodeclick', e.detail);
  165. selectedMessageId = e.detail.node.data.message.id;
  166. fitView({ nodes: [{ id: selectedMessageId }] });
  167. }}
  168. />
  169. {/if}
  170. </div>