Andrew commited on
Commit
50c8eb0
·
1 Parent(s): a0e1d3c

feat(ui): update NavConversationItem to show tree visualization

Browse files
src/lib/components/NavConversationItem.svelte CHANGED
@@ -1,14 +1,21 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
 
4
 
5
  import CarbonCheckmark from "~icons/carbon/checkmark";
6
  import CarbonTrashCan from "~icons/carbon/trash-can";
7
  import CarbonClose from "~icons/carbon/close";
8
  import CarbonEdit from "~icons/carbon/edit";
9
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
 
10
 
11
  import EditConversationModal from "$lib/components/EditConversationModal.svelte";
 
 
 
 
 
12
 
13
  interface Props {
14
  conv: ConvSidebar;
@@ -21,8 +28,95 @@
21
 
22
  let confirmDelete = $state(false);
23
  let renameOpen = $state(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </script>
25
 
 
26
  <a
27
  data-sveltekit-noscroll
28
  onmouseleave={() => {
@@ -102,6 +196,15 @@
102
  {/if}
103
  </a>
104
 
 
 
 
 
 
 
 
 
 
105
  <!-- Edit title modal -->
106
  {#if renameOpen}
107
  <EditConversationModal
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
4
+ import { goto } from "$app/navigation";
5
 
6
  import CarbonCheckmark from "~icons/carbon/checkmark";
7
  import CarbonTrashCan from "~icons/carbon/trash-can";
8
  import CarbonClose from "~icons/carbon/close";
9
  import CarbonEdit from "~icons/carbon/edit";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
+ import { type Message, MessageRole } from "$lib/types/Message";
12
 
13
  import EditConversationModal from "$lib/components/EditConversationModal.svelte";
14
+ import ConversationTreeGraph from "$lib/components/ConversationTreeGraph.svelte";
15
+ import { conversationTree } from "$lib/stores/conversationTree";
16
+ import type { TreeLayoutNode } from "$lib/utils/tree/layout";
17
+ import { buildTreeWithPositions } from "$lib/utils/tree/layout";
18
+ import { onDestroy } from "svelte";
19
 
20
  interface Props {
21
  conv: ConvSidebar;
 
28
 
29
  let confirmDelete = $state(false);
30
  let renameOpen = $state(false);
31
+
32
+ // Check if this is the active conversation with tree data
33
+ let isActiveWithTree = $derived(
34
+ conv.id === page.params.id && $conversationTree.conversationId === conv.id
35
+ );
36
+
37
+ let treeData = $state<{ nodes: TreeLayoutNode[]; width: number; height: number }>({
38
+ nodes: [],
39
+ width: 0,
40
+ height: 0
41
+ });
42
+
43
+ let treeUpdateTimeout: ReturnType<typeof setTimeout>;
44
+
45
+ // Update tree data when conversation or messages change
46
+ // Only update after messages are complete (have content)
47
+ // DEBOUNCED to prevent layout thrashing during streaming
48
+ $effect(() => {
49
+ if (isActiveWithTree && $conversationTree.messages.length > 0) {
50
+ clearTimeout(treeUpdateTimeout);
51
+ treeUpdateTimeout = setTimeout(() => {
52
+ // Filter to only messages with content (streaming complete)
53
+ // For assistant messages with personas, check personaResponses
54
+ const completeMessages = $conversationTree.messages.filter(m => {
55
+ // Exclude system messages from tree visualization
56
+ if (m.from === MessageRole.System) {
57
+ return false;
58
+ }
59
+
60
+ if (m.from === MessageRole.Assistant && m.personaResponses && m.personaResponses.length > 0) {
61
+ // Check if at least one persona has content
62
+ return m.personaResponses.some(pr => pr.content && pr.content.trim().length > 0);
63
+ }
64
+ // For user messages, check main content field
65
+ return m.content && m.content.trim().length > 0;
66
+ });
67
+
68
+ if (completeMessages.length > 0) {
69
+ buildTreeWithPositions(
70
+ completeMessages,
71
+ $conversationTree.activeMessageId || undefined,
72
+ new Set($conversationTree.activePath)
73
+ ).then((data) => {
74
+ treeData = data;
75
+ // Set CSS variable for sidebar width (tree width + padding)
76
+ // Allow expansion up to 800px for very wide trees
77
+ const sidebarWidth = Math.max(290, Math.min(800, data.width + 60)); // +60 for padding
78
+ document.documentElement.style.setProperty('--sidebar-width', `${sidebarWidth}px`);
79
+ }).catch((err) => {
80
+ console.error('Error building tree:', err);
81
+ });
82
+ }
83
+ }, 300); // 300ms debounce
84
+ } else {
85
+ treeData = { nodes: [], width: 0, height: 0 };
86
+ // Reset to default width
87
+ document.documentElement.style.setProperty('--sidebar-width', '290px');
88
+ }
89
+ });
90
+
91
+ onDestroy(() => {
92
+ if (treeUpdateTimeout) clearTimeout(treeUpdateTimeout);
93
+ });
94
+
95
+ function handleTreeNodeClick(messageId: string) {
96
+ const clickedMessage = $conversationTree.messages.find(m => m.id === messageId);
97
+ if (!clickedMessage) {
98
+ console.error('Clicked message not found:', messageId);
99
+ goto(`${base}/conversation/${conv.id}?msgId=${messageId}&scrollTo=true`);
100
+ return;
101
+ }
102
+
103
+ const currentBranchState = $conversationTree.branchedFrom;
104
+
105
+ if (currentBranchState) {
106
+ const currentActivePath = new Set($conversationTree.activePath);
107
+
108
+ if (currentActivePath.has(messageId)) {
109
+ // Message is in active branch; preserve state
110
+ goto(`${base}/conversation/${conv.id}?msgId=${messageId}&scrollTo=true&keepBranch=true`);
111
+ return;
112
+ }
113
+ }
114
+
115
+ goto(`${base}/conversation/${conv.id}?msgId=${messageId}&scrollTo=true`);
116
+ }
117
  </script>
118
 
119
+ <div class="flex flex-col">
120
  <a
121
  data-sveltekit-noscroll
122
  onmouseleave={() => {
 
196
  {/if}
197
  </a>
198
 
199
+ <!-- Tree view (only shown for active conversation) -->
200
+ {#if isActiveWithTree && treeData.nodes.length > 0}
201
+ <ConversationTreeGraph
202
+ {treeData}
203
+ onNodeClick={handleTreeNodeClick}
204
+ />
205
+ {/if}
206
+ </div>
207
+
208
  <!-- Edit title modal -->
209
  {#if renameOpen}
210
  <EditConversationModal