enzostvs HF Staff commited on
Commit
bb21933
·
1 Parent(s): e24ad39

add new feature

Browse files
src/lib/components/chat/Assistant.svelte CHANGED
@@ -1,6 +1,16 @@
1
  <script lang="ts">
2
- import { Handle, useNodesData, Position, type NodeProps } from '@xyflow/svelte';
3
- import { Star } from '@lucide/svelte';
 
 
 
 
 
 
 
 
 
 
4
 
5
  import type { ChatModel, ChatMessage } from '$lib/helpers/types';
6
  import { Button } from '$lib/components/ui/button';
@@ -11,8 +21,11 @@
11
 
12
  // svelte-ignore state_referenced_locally
13
  const nodeData = useNodesData(id);
 
 
14
 
15
  let selectedModel = $derived((nodeData.current?.data.selectedModel as ChatModel) ?? null);
 
16
  let loading = $derived((nodeData.current?.data.loading as boolean) ?? false);
17
  let message = $derived(
18
  nodeData.current?.data.content
@@ -23,9 +36,80 @@
23
  } as ChatMessage)
24
  : null
25
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </script>
27
 
28
- <article class="w-full rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]">
 
 
29
  <div class="nodrag pointer-events-auto cursor-auto">
30
  <header class="mb-3 flex items-center justify-between">
31
  <div class="flex flex-wrap items-center gap-1">
@@ -49,13 +133,26 @@
49
  </div>
50
  {/if}
51
  {#if message}
52
- <Message {message} provider={selectedModel.provider} />
 
 
53
  {/if}
54
  </div>
 
 
 
 
 
 
 
 
 
 
 
55
  </article>
56
- <Handle type="target" position={Position.Top} class="opacity-0" />
57
- <Handle type="target" position={Position.Left} class="opacity-0" />
58
- <Handle type="target" position={Position.Right} class="opacity-0" />
59
- <Handle type="source" position={Position.Bottom} class="opacity-0" />
60
- <Handle type="source" position={Position.Left} class="opacity-0" />
61
- <Handle type="source" position={Position.Right} class="opacity-0" />
 
1
  <script lang="ts">
2
+ import {
3
+ Handle,
4
+ useNodesData,
5
+ Position,
6
+ type NodeProps,
7
+ useNodes,
8
+ useEdges,
9
+ type Edge,
10
+ type Node
11
+ } from '@xyflow/svelte';
12
+ import { MessageCirclePlus, Star } from '@lucide/svelte';
13
+ import { mode } from 'mode-watcher';
14
 
15
  import type { ChatModel, ChatMessage } from '$lib/helpers/types';
16
  import { Button } from '$lib/components/ui/button';
 
21
 
22
  // svelte-ignore state_referenced_locally
23
  const nodeData = useNodesData(id);
24
+ const { update: updateNodes } = useNodes();
25
+ const { update: updateEdges } = useEdges();
26
 
27
  let selectedModel = $derived((nodeData.current?.data.selectedModel as ChatModel) ?? null);
28
+ let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
29
  let loading = $derived((nodeData.current?.data.loading as boolean) ?? false);
30
  let message = $derived(
31
  nodeData.current?.data.content
 
36
  } as ChatMessage)
37
  : null
38
  );
39
+ let containerRef: HTMLDivElement | null = $state(null);
40
+ let selectedText = $state<string | null>(null);
41
+ let selectedTextPosition = $state<{ y: number } | null>(null);
42
+
43
+ $effect(() => {
44
+ document.addEventListener('selectionchange', handleTextSelectionChange);
45
+ return () => {
46
+ document.removeEventListener('selectionchange', handleTextSelectionChange);
47
+ };
48
+ });
49
+
50
+ function handleTextSelectionChange(e: Event) {
51
+ const selection = document.getSelection();
52
+ const text = selection?.toString();
53
+ if (!text || text.trim() === '') {
54
+ selectedText = null;
55
+ return;
56
+ }
57
+ if (containerRef?.contains(selection?.anchorNode as unknown as HTMLElement)) {
58
+ selectedText = text;
59
+ let rect = selection?.getRangeAt(0).getBoundingClientRect();
60
+ if (rect) {
61
+ selectedTextPosition = {
62
+ y: rect.top - containerRef.getBoundingClientRect().top
63
+ };
64
+ } else {
65
+ selectedTextPosition = {
66
+ y:
67
+ (selection?.anchorNode?.parentElement?.getBoundingClientRect().top ?? 0) -
68
+ containerRef.getBoundingClientRect().top
69
+ };
70
+ }
71
+ } else {
72
+ selectedText = null;
73
+ }
74
+ }
75
+
76
+ function handleAddFollowUpMessage(prompt: string) {
77
+ document.getSelection()?.removeAllRanges();
78
+ const newNodes: Node[] = [];
79
+ const newEdges: Edge[] = [];
80
+
81
+ const newNodeId = `user-follow-up-${crypto.randomUUID()}`;
82
+ const newNode: Node = {
83
+ id: newNodeId,
84
+ type: 'user-follow-up',
85
+ position: {
86
+ x: 0,
87
+ y: 0
88
+ },
89
+ data: {
90
+ role: 'user',
91
+ prompt,
92
+ selectedModels: [selectedModel],
93
+ messages: [...messages, message]
94
+ }
95
+ };
96
+ const newEdge: Edge = {
97
+ id: `edge-${crypto.randomUUID()}`,
98
+ source: id,
99
+ target: newNodeId,
100
+ sourceHandle: 's-right',
101
+ targetHandle: 't-left'
102
+ };
103
+ newNodes.push(newNode);
104
+ newEdges.push(newEdge);
105
+ updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
106
+ updateEdges((currentEdges) => [...currentEdges, ...newEdges]);
107
+ }
108
  </script>
109
 
110
+ <article
111
+ class="relative w-full rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
112
+ >
113
  <div class="nodrag pointer-events-auto cursor-auto">
114
  <header class="mb-3 flex items-center justify-between">
115
  <div class="flex flex-wrap items-center gap-1">
 
133
  </div>
134
  {/if}
135
  {#if message}
136
+ <div bind:this={containerRef}>
137
+ <Message {message} provider={selectedModel.provider} />
138
+ </div>
139
  {/if}
140
  </div>
141
+ <Button
142
+ variant={mode.current === 'dark' ? 'default' : 'outline'}
143
+ size="icon-lg"
144
+ class="absolute top-0 right-0 z-50 translate-x-1/2 {selectedText
145
+ ? 'opacity-100'
146
+ : 'pointer-events-none opacity-0'}"
147
+ style="top: {(selectedTextPosition?.y ?? 0) + 50}px;"
148
+ onclick={() => selectedText && handleAddFollowUpMessage(selectedText)}
149
+ >
150
+ <MessageCirclePlus class="size-5" />
151
+ </Button>
152
  </article>
153
+ <Handle type="target" id="t-top" class="opacity-0" position={Position.Top} />
154
+ <Handle type="target" id="t-left" class="opacity-0" position={Position.Left} />
155
+ <Handle type="target" id="t-right" class="opacity-0" position={Position.Right} />
156
+ <Handle type="source" id="s-bottom" class="opacity-0" position={Position.Bottom} />
157
+ <Handle type="source" id="s-left" class="opacity-0" position={Position.Left} />
158
+ <Handle type="source" id="s-right" class="opacity-0" position={Position.Right} />
src/lib/components/chat/Message.svelte CHANGED
@@ -38,7 +38,7 @@
38
  {:else}
39
  <SvelteMarkdown source={message.content} renderers={renderers as any} />
40
  {#if message.timestamp}
41
- <p class="flex items-center gap-1 text-xs text-muted-foreground/70">
42
  Generated in
43
  {message.timestamp / 1000}s using
44
  <span class="flex items-center gap-1 rounded bg-muted py-0.5 pr-1 pl-0.5">
 
38
  {:else}
39
  <SvelteMarkdown source={message.content} renderers={renderers as any} />
40
  {#if message.timestamp}
41
+ <p class="flex items-center gap-1 text-xs text-muted-foreground/70 select-none">
42
  Generated in
43
  {message.timestamp / 1000}s using
44
  <span class="flex items-center gap-1 rounded bg-muted py-0.5 pr-1 pl-0.5">
src/lib/components/chat/User.svelte CHANGED
@@ -11,7 +11,6 @@
11
  type Node,
12
  useSvelteFlow
13
  } from '@xyflow/svelte';
14
- import { mode } from 'mode-watcher';
15
 
16
  import type { ChatModel, ChatMessage } from '$lib/helpers/types';
17
  import { Button } from '$lib/components/ui/button';
@@ -21,11 +20,11 @@
21
  import SettingsModel from '$lib/components/model/SettingsModel.svelte';
22
  import { MAX_MODELS_PER_NODE, MAX_SUGGESTIONS } from '$lib';
23
  import { SUGGESTIONS_PROMPT } from '$lib/consts';
24
- import { breakpointsState } from '$lib/state/breakpoints.svelte';
25
  import { authState } from '$lib/state/auth.svelte';
26
  import { signinModalState } from '$lib/state/signin-modal.svelte';
 
27
 
28
- let { id, selected }: NodeProps = $props();
29
 
30
  // svelte-ignore state_referenced_locally
31
  const nodeData = useNodesData(id);
@@ -96,7 +95,8 @@
96
  role: 'assistant',
97
  selectedModel: m,
98
  content: '',
99
- loading: true
 
100
  }
101
  };
102
  const newEdge: Edge = {
@@ -169,7 +169,7 @@
169
  const end = Date.now();
170
  updateNodeData(
171
  node.id,
172
- { ...node.data, content, timestamp: end - start, loading: false },
173
  { replace: true }
174
  );
175
  break;
@@ -191,9 +191,18 @@
191
  ? messages[messages.length - 1]
192
  : null
193
  );
 
 
 
 
 
 
 
194
  </script>
195
 
196
- <article class="w-full rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]">
 
 
197
  <div class="nodrag pointer-events-auto cursor-auto">
198
  <header class="mb-3 flex items-center justify-between">
199
  <div class="flex flex-wrap items-center gap-1">
@@ -280,7 +289,7 @@
280
  <div></div>
281
  {/if}
282
  <Button
283
- variant={mode.current === 'dark' ? 'default' : 'default'}
284
  size="icon-sm"
285
  class=""
286
  disabled={!selectedModels.length || !prompt || loading}
@@ -297,9 +306,9 @@
297
  {/if}
298
  </div>
299
  </article>
300
- <Handle type="target" position={Position.Top} class="opacity-0" />
301
- <Handle type="target" position={Position.Left} class="opacity-0" />
302
- <Handle type="target" position={Position.Right} class="opacity-0" />
303
- <Handle type="source" position={Position.Bottom} class="opacity-0" />
304
- <Handle type="source" position={Position.Left} class="opacity-0" />
305
- <Handle type="source" position={Position.Right} class="opacity-0" />
 
11
  type Node,
12
  useSvelteFlow
13
  } from '@xyflow/svelte';
 
14
 
15
  import type { ChatModel, ChatMessage } from '$lib/helpers/types';
16
  import { Button } from '$lib/components/ui/button';
 
20
  import SettingsModel from '$lib/components/model/SettingsModel.svelte';
21
  import { MAX_MODELS_PER_NODE, MAX_SUGGESTIONS } from '$lib';
22
  import { SUGGESTIONS_PROMPT } from '$lib/consts';
 
23
  import { authState } from '$lib/state/auth.svelte';
24
  import { signinModalState } from '$lib/state/signin-modal.svelte';
25
+ import { onMount } from 'svelte';
26
 
27
+ let { id }: NodeProps = $props();
28
 
29
  // svelte-ignore state_referenced_locally
30
  const nodeData = useNodesData(id);
 
95
  role: 'assistant',
96
  selectedModel: m,
97
  content: '',
98
+ loading: true,
99
+ messages
100
  }
101
  };
102
  const newEdge: Edge = {
 
169
  const end = Date.now();
170
  updateNodeData(
171
  node.id,
172
+ { ...node.data, content, timestamp: end - start, loading: false, messages },
173
  { replace: true }
174
  );
175
  break;
 
191
  ? messages[messages.length - 1]
192
  : null
193
  );
194
+
195
+ onMount(() => {
196
+ if (prompt.trim() !== '' && prompt) {
197
+ handleTriggerAction();
198
+ prompt = '';
199
+ }
200
+ });
201
  </script>
202
 
203
+ <article
204
+ class="relative z-10 w-full rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
205
+ >
206
  <div class="nodrag pointer-events-auto cursor-auto">
207
  <header class="mb-3 flex items-center justify-between">
208
  <div class="flex flex-wrap items-center gap-1">
 
289
  <div></div>
290
  {/if}
291
  <Button
292
+ variant="default"
293
  size="icon-sm"
294
  class=""
295
  disabled={!selectedModels.length || !prompt || loading}
 
306
  {/if}
307
  </div>
308
  </article>
309
+ <Handle type="target" id="t-top" position={Position.Top} class="opacity-0" />
310
+ <Handle type="target" id="t-left" position={Position.Left} class="opacity-0" />
311
+ <Handle type="target" id="t-right" position={Position.Right} class="opacity-0" />
312
+ <Handle type="source" id="s-bottom" position={Position.Bottom} class="opacity-0" />
313
+ <Handle type="source" id="s-left" position={Position.Left} class="opacity-0" />
314
+ <Handle type="source" id="s-right" position={Position.Right} class="opacity-0" />
src/lib/components/flow/FitViewOnResize.svelte CHANGED
@@ -125,12 +125,19 @@
125
  if (fitViewTimer) clearTimeout(fitViewTimer);
126
  });
127
 
 
 
 
 
 
 
128
  /**
129
  * Custom tree layout:
130
  * - Root nodes (no incoming edge) on the same horizontal row
131
  * - Siblings (children sharing the same source) on the same horizontal row
132
- * - Parent centered above its children
133
- * - Works recursively for any depth (user -> assistant -> user -> ...)
 
134
  */
135
  function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
136
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
@@ -148,7 +155,7 @@
148
  // Root nodes: no incoming edge
149
  const rootNodes = nodes.filter((n) => !hasParent.has(n.id));
150
 
151
- // 1) Compute the horizontal space each subtree needs
152
  const subtreeWidths = new Map<string, number>();
153
 
154
  function computeSubtreeWidth(nodeId: string): number {
@@ -157,16 +164,19 @@
157
  const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
158
  const children = childrenBySource.get(nodeId) ?? [];
159
 
160
- if (children.length === 0) {
 
 
 
161
  subtreeWidths.set(nodeId, nodeWidth);
162
  return nodeWidth;
163
  }
164
 
165
- const childrenTotalWidth =
166
- children.reduce((sum, childId) => sum + computeSubtreeWidth(childId), 0) +
167
- (children.length - 1) * H_SPACING;
168
 
169
- const width = Math.max(nodeWidth, childrenTotalWidth);
170
  subtreeWidths.set(nodeId, width);
171
  return width;
172
  }
@@ -174,8 +184,14 @@
174
  for (const root of rootNodes) {
175
  computeSubtreeWidth(root.id);
176
  }
 
 
 
 
 
 
177
 
178
- // 2) Place nodes top-down: each node is centered in its allocated subtree width
179
  const positions = new Map<string, { x: number; y: number }>();
180
 
181
  function placeNode(nodeId: string, allocatedX: number, y: number) {
@@ -183,28 +199,39 @@
183
  const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
184
  const nodeHeight = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
185
  const stWidth = subtreeWidths.get(nodeId) ?? nodeWidth;
 
 
 
 
186
 
187
  // Center node within its allocated subtree space
188
  const x = allocatedX + (stWidth - nodeWidth) / 2;
189
  positions.set(nodeId, { x, y });
190
 
191
- const children = childrenBySource.get(nodeId) ?? [];
192
- if (children.length === 0) return;
193
-
194
- const childY = y + nodeHeight + V_SPACING;
195
- let childX = allocatedX;
 
 
 
 
 
196
 
197
- for (const childId of children) {
198
- const childStWidth = subtreeWidths.get(childId) ?? DEFAULT_WIDTH;
199
- placeNode(childId, childX, childY);
200
- childX += childStWidth + H_SPACING;
 
 
 
201
  }
202
  }
203
 
204
  // Place root nodes side by side
205
  let rootX = 0;
206
  for (const root of rootNodes) {
207
- computeSubtreeWidth(root.id);
208
  placeNode(root.id, rootX, 0);
209
  rootX += (subtreeWidths.get(root.id) ?? DEFAULT_WIDTH) + H_SPACING;
210
  }
 
125
  if (fitViewTimer) clearTimeout(fitViewTimer);
126
  });
127
 
128
+ /** Whether a child should be placed to the right of its parent (same row) */
129
+ function isSideChild(nodeId: string, nodeMap: Map<string, Node>): boolean {
130
+ const node = nodeMap.get(nodeId);
131
+ return node?.type === 'user-follow-up';
132
+ }
133
+
134
  /**
135
  * Custom tree layout:
136
  * - Root nodes (no incoming edge) on the same horizontal row
137
  * - Siblings (children sharing the same source) on the same horizontal row
138
+ * - user-follow-up nodes placed to the RIGHT of their source (same row)
139
+ * - Parent centered above its below-children
140
+ * - Works recursively for any depth
141
  */
142
  function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
143
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
 
155
  // Root nodes: no incoming edge
156
  const rootNodes = nodes.filter((n) => !hasParent.has(n.id));
157
 
158
+ // 1) Compute the horizontal space each subtree needs (ignoring side children)
159
  const subtreeWidths = new Map<string, number>();
160
 
161
  function computeSubtreeWidth(nodeId: string): number {
 
164
  const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
165
  const children = childrenBySource.get(nodeId) ?? [];
166
 
167
+ // Side children are independent — they don't affect the main tree width
168
+ const belowChildIds = children.filter((id) => !isSideChild(id, nodeMap));
169
+
170
+ if (belowChildIds.length === 0) {
171
  subtreeWidths.set(nodeId, nodeWidth);
172
  return nodeWidth;
173
  }
174
 
175
+ const belowChildrenWidth =
176
+ belowChildIds.reduce((sum, cid) => sum + computeSubtreeWidth(cid), 0) +
177
+ (belowChildIds.length - 1) * H_SPACING;
178
 
179
+ const width = Math.max(nodeWidth, belowChildrenWidth);
180
  subtreeWidths.set(nodeId, width);
181
  return width;
182
  }
 
184
  for (const root of rootNodes) {
185
  computeSubtreeWidth(root.id);
186
  }
187
+ // Also compute widths for side-child subtrees (they need it for their own below-children)
188
+ for (const node of nodes) {
189
+ if (!subtreeWidths.has(node.id)) {
190
+ computeSubtreeWidth(node.id);
191
+ }
192
+ }
193
 
194
+ // 2) Place nodes top-down
195
  const positions = new Map<string, { x: number; y: number }>();
196
 
197
  function placeNode(nodeId: string, allocatedX: number, y: number) {
 
199
  const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
200
  const nodeHeight = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
201
  const stWidth = subtreeWidths.get(nodeId) ?? nodeWidth;
202
+ const children = childrenBySource.get(nodeId) ?? [];
203
+
204
+ const sideChildIds = children.filter((id) => isSideChild(id, nodeMap));
205
+ const belowChildIds = children.filter((id) => !isSideChild(id, nodeMap));
206
 
207
  // Center node within its allocated subtree space
208
  const x = allocatedX + (stWidth - nodeWidth) / 2;
209
  positions.set(nodeId, { x, y });
210
 
211
+ // Place below-children under this node
212
+ if (belowChildIds.length > 0) {
213
+ const childY = y + nodeHeight + V_SPACING;
214
+ let childX = allocatedX;
215
+ for (const childId of belowChildIds) {
216
+ const childStWidth = subtreeWidths.get(childId) ?? DEFAULT_WIDTH;
217
+ placeNode(childId, childX, childY);
218
+ childX += childStWidth + H_SPACING;
219
+ }
220
+ }
221
 
222
+ // Place side-children to the right of this node, same Y (independent of main tree)
223
+ let sideX = x + nodeWidth + H_SPACING;
224
+ for (const sideId of sideChildIds) {
225
+ placeNode(sideId, sideX, y);
226
+ const sideNode = nodeMap.get(sideId);
227
+ const sideWidth = sideNode ? getMeasuredWidth(sideNode) : DEFAULT_WIDTH;
228
+ sideX += sideWidth + H_SPACING;
229
  }
230
  }
231
 
232
  // Place root nodes side by side
233
  let rootX = 0;
234
  for (const root of rootNodes) {
 
235
  placeNode(root.id, rootX, 0);
236
  rootX += (subtreeWidths.get(root.id) ?? DEFAULT_WIDTH) + H_SPACING;
237
  }
src/lib/helpers/resolve-collisions.ts ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Node } from '@xyflow/svelte';
2
+
3
+ export type CollisionAlgorithmOptions = {
4
+ maxIterations: number;
5
+ overlapThreshold: number;
6
+ margin: number;
7
+ };
8
+
9
+ export type CollisionAlgorithm = (nodes: Node[], options: CollisionAlgorithmOptions) => Node[];
10
+
11
+ type Box = {
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ moved: boolean;
17
+ node: Node;
18
+ };
19
+
20
+ function getBoxesFromNodes(nodes: Node[], margin = 0): Box[] {
21
+ const boxes: Box[] = new Array(nodes.length);
22
+
23
+ for (let i = 0; i < nodes.length; i++) {
24
+ const node = nodes[i];
25
+ boxes[i] = {
26
+ x: node.position.x - margin,
27
+ y: node.position.y - margin,
28
+ width: (node.width ?? node.measured?.width ?? 0) + margin * 2,
29
+ height: (node.height ?? node.measured?.height ?? 0) + margin * 2,
30
+ node,
31
+ moved: false
32
+ };
33
+ }
34
+
35
+ return boxes;
36
+ }
37
+
38
+ export const resolveCollisions: CollisionAlgorithm = (
39
+ nodes,
40
+ { maxIterations = 50, overlapThreshold = 0.5, margin = 0 }
41
+ ) => {
42
+ const boxes = getBoxesFromNodes(nodes, margin);
43
+
44
+ for (let iter = 0; iter <= maxIterations; iter++) {
45
+ let moved = false;
46
+
47
+ for (let i = 0; i < boxes.length; i++) {
48
+ for (let j = i + 1; j < boxes.length; j++) {
49
+ const A = boxes[i];
50
+ const B = boxes[j];
51
+
52
+ // Calculate center positions
53
+ const centerAX = A.x + A.width * 0.5;
54
+ const centerAY = A.y + A.height * 0.5;
55
+ const centerBX = B.x + B.width * 0.5;
56
+ const centerBY = B.y + B.height * 0.5;
57
+
58
+ // Calculate distance between centers
59
+ const dx = centerAX - centerBX;
60
+ const dy = centerAY - centerBY;
61
+
62
+ // Calculate overlap along each axis
63
+ const px = (A.width + B.width) * 0.5 - Math.abs(dx);
64
+ const py = (A.height + B.height) * 0.5 - Math.abs(dy);
65
+
66
+ // Check if there's significant overlap
67
+ if (px > overlapThreshold && py > overlapThreshold) {
68
+ A.moved = B.moved = moved = true;
69
+ // Resolve along the smallest overlap axis
70
+ if (px < py) {
71
+ // Move along x-axis
72
+ const sx = dx > 0 ? 1 : -1;
73
+ const moveAmount = (px / 2) * sx;
74
+ A.x += moveAmount;
75
+ B.x -= moveAmount;
76
+ } else {
77
+ // Move along y-axis
78
+ const sy = dy > 0 ? 1 : -1;
79
+ const moveAmount = (py / 2) * sy;
80
+ A.y += moveAmount;
81
+ B.y -= moveAmount;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ // Early exit if no overlaps were found
87
+ if (!moved) {
88
+ break;
89
+ }
90
+ }
91
+
92
+ const newNodes = boxes.map((box) => {
93
+ if (box.moved) {
94
+ return {
95
+ ...box.node,
96
+ position: {
97
+ x: box.x + margin,
98
+ y: box.y + margin
99
+ }
100
+ };
101
+ }
102
+ return box.node;
103
+ });
104
+
105
+ return newNodes;
106
+ };
src/routes/+page.svelte CHANGED
@@ -23,10 +23,12 @@
23
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
24
  import PanelCanvasActions from '$lib/components/flow/actions/PanelCanvasActions.svelte';
25
  import { viewState } from '$lib/state/view.svelte';
 
26
 
27
  const nodeTypes = {
28
  user: User,
29
- assistant: Assistant
 
30
  };
31
 
32
  function getInitialNodes() {
@@ -37,7 +39,7 @@
37
  position: { x: 0, y: 0 },
38
  data: {
39
  isFirstNode: true,
40
- selectedModels: modelsState.models.slice(0, MAX_DEFAULT_MODELS) as ChatModel[]
41
  }
42
  }
43
  ];
@@ -56,7 +58,7 @@
56
  // }
57
  }}
58
  />
59
-
60
  <div class="h-screen w-screen overflow-hidden">
61
  <SvelteFlow
62
  bind:nodes
@@ -81,6 +83,13 @@
81
  panOnDrag={viewState.draggable}
82
  onbeforedelete={() => Promise.resolve(false)}
83
  defaultEdgeOptions={{ type: 'smoothstep' }}
 
 
 
 
 
 
 
84
  class="bg-background!"
85
  >
86
  <FitViewOnResize {initialNodes} />
 
23
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
24
  import PanelCanvasActions from '$lib/components/flow/actions/PanelCanvasActions.svelte';
25
  import { viewState } from '$lib/state/view.svelte';
26
+ import { resolveCollisions } from '$lib/helpers/resolve-collisions';
27
 
28
  const nodeTypes = {
29
  user: User,
30
+ assistant: Assistant,
31
+ 'user-follow-up': User
32
  };
33
 
34
  function getInitialNodes() {
 
39
  position: { x: 0, y: 0 },
40
  data: {
41
  isFirstNode: true,
42
+ selectedModels: modelsState.models.slice(1, 1 + MAX_DEFAULT_MODELS) as ChatModel[]
43
  }
44
  }
45
  ];
 
58
  // }
59
  }}
60
  />
61
+ <!-- todo: resolve collissions when new nodes are added, maybe do it in the fitviewonresize component -->
62
  <div class="h-screen w-screen overflow-hidden">
63
  <SvelteFlow
64
  bind:nodes
 
83
  panOnDrag={viewState.draggable}
84
  onbeforedelete={() => Promise.resolve(false)}
85
  defaultEdgeOptions={{ type: 'smoothstep' }}
86
+ onnodedragstop={() => {
87
+ nodes = resolveCollisions(nodes, {
88
+ maxIterations: Infinity,
89
+ overlapThreshold: 0.5,
90
+ margin: 15
91
+ });
92
+ }}
93
  class="bg-background!"
94
  >
95
  <FitViewOnResize {initialNodes} />