enzostvs HF Staff commited on
Commit
09fc27e
Β·
1 Parent(s): 2317272

fix with claude the auto fit view

Browse files
src/lib/components/chat/User.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { Send, X } from '@lucide/svelte';
3
  import {
4
  Handle,
5
  useEdges,
@@ -38,6 +38,7 @@
38
  let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
39
  let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
40
  let loading = $state.raw<boolean>(false);
 
41
  let messages = $state.raw<ChatMessage[]>(
42
  (nodeData.current?.data?.messages as ChatMessage[]) ?? []
43
  );
@@ -77,6 +78,7 @@
77
  signinModalState.open = true;
78
  return;
79
  }
 
80
  const newNodes: Node[] = [];
81
  const newEdges: Edge[] = [];
82
  messages = [...messages, { role: 'user', content: prompt }];
@@ -134,7 +136,10 @@
134
  Authorization: `Bearer ${authState.token ?? ''}`
135
  }
136
  });
137
- if (!response.ok) throw new Error(response.statusText);
 
 
 
138
  if (!response.body) throw new Error('No response body');
139
 
140
  let content = '';
@@ -145,6 +150,10 @@
145
  while (true) {
146
  const { done, value } = await reader.read();
147
  if (done) {
 
 
 
 
148
  const newNodeId = `user-${crypto.randomUUID()}`;
149
  const newNode: Node = {
150
  id: newNodeId,
@@ -179,7 +188,14 @@
179
  updateNodeData(node.id, { ...node.data, content, loading: false }, { replace: true });
180
  }
181
  } catch (error) {
182
- console.error(error);
 
 
 
 
 
 
 
183
  } finally {
184
  loading = false;
185
  }
@@ -246,6 +262,24 @@
246
  {/if}
247
  </div>
248
  </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  {#if lastMessage}
250
  <Message message={lastMessage} />
251
  {:else}
 
1
  <script lang="ts">
2
+ import { Send, X, TriangleAlert } from '@lucide/svelte';
3
  import {
4
  Handle,
5
  useEdges,
 
38
  let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
39
  let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
40
  let loading = $state.raw<boolean>(false);
41
+ let errorMessage = $state.raw<string>('');
42
  let messages = $state.raw<ChatMessage[]>(
43
  (nodeData.current?.data?.messages as ChatMessage[]) ?? []
44
  );
 
78
  signinModalState.open = true;
79
  return;
80
  }
81
+ errorMessage = '';
82
  const newNodes: Node[] = [];
83
  const newEdges: Edge[] = [];
84
  messages = [...messages, { role: 'user', content: prompt }];
 
136
  Authorization: `Bearer ${authState.token ?? ''}`
137
  }
138
  });
139
+ if (!response.ok) {
140
+ const errorBody = await response.text().catch(() => response.statusText);
141
+ throw new Error(errorBody || response.statusText);
142
+ }
143
  if (!response.body) throw new Error('No response body');
144
 
145
  let content = '';
 
150
  while (true) {
151
  const { done, value } = await reader.read();
152
  if (done) {
153
+ if (content.includes('__ERROR__')) {
154
+ const errorMessage = content.split('__ERROR__').pop() ?? 'Unknown error';
155
+ throw new Error(errorMessage);
156
+ }
157
  const newNodeId = `user-${crypto.randomUUID()}`;
158
  const newNode: Node = {
159
  id: newNodeId,
 
188
  updateNodeData(node.id, { ...node.data, content, loading: false }, { replace: true });
189
  }
190
  } catch (error) {
191
+ const msg = error instanceof Error ? error.message : 'An unknown error occurred';
192
+ // Remove the failed assistant node and its edge
193
+ updateNodes((currentNodes) => currentNodes.filter((n) => n.id !== node.id));
194
+ updateEdges((currentEdges) => currentEdges.filter((e) => e.target !== node.id));
195
+ // Revert messages so the user can re-send
196
+ messages = messages.filter((m, i) => i < messages.length - 1 || m.role !== 'user');
197
+ updateNodeData(id, { ...nodeData.current?.data, messages }, { replace: true });
198
+ errorMessage = msg;
199
  } finally {
200
  loading = false;
201
  }
 
262
  {/if}
263
  </div>
264
  </header>
265
+ {#if errorMessage}
266
+ <div
267
+ class="mb-2.5 flex items-center justify-between gap-3 rounded-lg bg-rose-500/10 p-3 text-sm text-rose-600"
268
+ >
269
+ <div class="flex items-center gap-1">
270
+ <TriangleAlert class="size-4 shrink-0" />
271
+ <p class="flex-1 text-sm text-red-600">{errorMessage}</p>
272
+ </div>
273
+ <Button
274
+ variant="destructive"
275
+ size="icon-3xs"
276
+ class="!shadow-none! rounded-full!"
277
+ onclick={() => (errorMessage = '')}
278
+ >
279
+ <X class="size-3" />
280
+ </Button>
281
+ </div>
282
+ {/if}
283
  {#if lastMessage}
284
  <Message message={lastMessage} />
285
  {:else}
src/lib/components/flow/FitViewOnResize.svelte CHANGED
@@ -8,7 +8,6 @@
8
 
9
  let { initialNodes }: { initialNodes: Node[] } = $props();
10
 
11
- // Fallback dimensions (used before nodes are measured by xyflow)
12
  const DEFAULT_WIDTH = breakpointsState.isMobile ? 300 : 600;
13
  const DEFAULT_HEIGHT = 200;
14
  const H_SPACING = 60;
@@ -20,17 +19,23 @@
20
 
21
  let lastLayoutKey = $state<string | null>(null);
22
 
23
- /** Get the actual measured height of a node, or fallback */
 
24
  function getMeasuredHeight(node: Node): number {
25
  return node.measured?.height ?? DEFAULT_HEIGHT;
26
  }
27
 
28
- /** Get the actual measured width of a node, or fallback */
29
  function getMeasuredWidth(node: Node): number {
30
  return node.measured?.width ?? DEFAULT_WIDTH;
31
  }
32
 
33
- // Layout key: changes when nodes/edges are added/removed, OR when measured dimensions change
 
 
 
 
 
 
34
  const layoutKey = $derived(
35
  (() => {
36
  const nodes = nodesStore.current;
@@ -45,7 +50,6 @@
45
  .map((e) => `${e.source}-${e.target}`)
46
  .sort()
47
  .join(',');
48
- // Include measured dimensions so layout re-runs when nodes get measured
49
  const dims = ns
50
  .map((n) => `${n.id}:${n.measured?.width ?? 0}x${n.measured?.height ?? 0}`)
51
  .sort()
@@ -67,8 +71,10 @@
67
  runLayout(ns, es);
68
  });
69
 
 
 
70
  function handleWindowResize() {
71
- if (!viewState.draggable) return;
72
  fitView({
73
  maxZoom: 1,
74
  minZoom: breakpointsState.isMobile ? 1 : 0.8,
@@ -77,139 +83,180 @@
77
  });
78
  }
79
 
80
- onMount(() => {
81
- window.addEventListener('resize', handleWindowResize);
82
- });
83
-
84
- onDestroy(() => {
85
- window.removeEventListener('resize', handleWindowResize);
86
- });
87
-
88
- /** Whether a child should be placed to the right of its parent (same row) */
89
- function isSideChild(nodeId: string, nodeMap: Map<string, Node>): boolean {
90
- const node = nodeMap.get(nodeId);
91
- return node?.type === 'user-follow-up';
92
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- /**
95
- * Custom tree layout:
96
- * - Root nodes (no incoming edge) on the same horizontal row
97
- * - Siblings (children sharing the same source) on the same horizontal row
98
- * - user-follow-up nodes placed to the RIGHT of their source (same row)
99
- * - Parent centered above its below-children
100
- * - Works recursively for any depth
101
- */
102
  function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
 
 
103
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
104
 
105
- // Build parent->children mapping from edges
106
- const childrenBySource = new Map<string, string[]>();
107
  const hasParent = new Set<string>();
 
108
  for (const e of edges) {
109
  hasParent.add(e.target);
110
- const list = childrenBySource.get(e.source) ?? [];
111
  list.push(e.target);
112
- childrenBySource.set(e.source, list);
113
  }
114
 
115
- // Root nodes: no incoming edge
116
- const rootNodes = nodes.filter((n) => !hasParent.has(n.id));
 
 
 
117
 
118
- // 1) Compute the horizontal space each subtree needs (ignoring side children)
119
- const subtreeWidths = new Map<string, number>();
120
 
121
- function computeSubtreeWidth(nodeId: string): number {
122
- if (subtreeWidths.has(nodeId)) return subtreeWidths.get(nodeId)!;
123
  const node = nodeMap.get(nodeId);
124
- const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
125
- const children = childrenBySource.get(nodeId) ?? [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- // Side children are independent β€” they don't affect the main tree width
128
- const belowChildIds = children.filter((id) => !isSideChild(id, nodeMap));
 
129
 
130
- if (belowChildIds.length === 0) {
131
- subtreeWidths.set(nodeId, nodeWidth);
132
- return nodeWidth;
 
 
133
  }
134
 
135
- const belowChildrenWidth =
136
- belowChildIds.reduce((sum, cid) => sum + computeSubtreeWidth(cid), 0) +
137
- (belowChildIds.length - 1) * H_SPACING;
138
 
139
- const width = Math.max(nodeWidth, belowChildrenWidth);
140
- subtreeWidths.set(nodeId, width);
141
- return width;
142
  }
143
 
144
- for (const root of rootNodes) {
145
- computeSubtreeWidth(root.id);
146
- }
147
- // Also compute widths for side-child subtrees (they need it for their own below-children)
148
- for (const node of nodes) {
149
- if (!subtreeWidths.has(node.id)) {
150
- computeSubtreeWidth(node.id);
151
- }
152
  }
153
 
154
- // 2) Place nodes top-down
 
155
  const positions = new Map<string, { x: number; y: number }>();
156
 
157
- function placeNode(nodeId: string, allocatedX: number, y: number) {
158
  const node = nodeMap.get(nodeId);
159
- const nodeWidth = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
160
- const nodeHeight = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
161
- const stWidth = subtreeWidths.get(nodeId) ?? nodeWidth;
162
- const children = childrenBySource.get(nodeId) ?? [];
163
-
164
- const sideChildIds = children.filter((id) => isSideChild(id, nodeMap));
165
- const belowChildIds = children.filter((id) => !isSideChild(id, nodeMap));
166
-
167
- // Center node within its allocated subtree space
168
- const x = allocatedX + (stWidth - nodeWidth) / 2;
169
- positions.set(nodeId, { x, y });
170
-
171
- // Place below-children under this node
172
- if (belowChildIds.length > 0) {
173
- const childY = y + nodeHeight + V_SPACING;
174
- let childX = allocatedX;
175
- for (const childId of belowChildIds) {
176
- const childStWidth = subtreeWidths.get(childId) ?? DEFAULT_WIDTH;
177
- placeNode(childId, childX, childY);
178
- childX += childStWidth + H_SPACING;
 
 
 
 
 
 
 
 
 
179
  }
180
  }
181
 
182
- // Place side-children to the right of this node, same Y (independent of main tree)
183
- let sideX = x + nodeWidth + H_SPACING;
184
- for (const sideId of sideChildIds) {
185
- placeNode(sideId, sideX, y);
186
- const sideNode = nodeMap.get(sideId);
187
- const sideWidth = sideNode ? getMeasuredWidth(sideNode) : DEFAULT_WIDTH;
188
- sideX += sideWidth + H_SPACING;
189
  }
190
  }
191
 
192
- // Place root nodes side by side
193
  let rootX = 0;
194
- for (const root of rootNodes) {
195
- placeNode(root.id, rootX, 0);
196
- rootX += (subtreeWidths.get(root.id) ?? DEFAULT_WIDTH) + H_SPACING;
197
  }
198
 
199
- // Orphan nodes (safety net)
200
  for (const node of nodes) {
201
  if (positions.has(node.id)) continue;
202
- const allY = Array.from(positions.values()).map((p) => p.y);
203
- const maxY = allY.length > 0 ? Math.max(...allY) : 0;
204
  positions.set(node.id, { x: 0, y: maxY + DEFAULT_HEIGHT + V_SPACING });
205
  }
206
 
207
- return nodes.map((node) => ({
208
- ...node,
209
- position: positions.get(node.id) ?? { x: 0, y: 0 }
210
  }));
211
  }
212
 
 
 
213
  function runLayout(nodes: Node[], edges: Edge[]) {
214
  const result = computeLayout(nodes, edges);
215
 
@@ -217,16 +264,16 @@
217
  if (current.length === 0) {
218
  nodesStore.set(result);
219
  } else {
220
- const positionMap = new Map(result.map((n) => [n.id, n.position]));
221
  nodesStore.update((prev) =>
222
  prev.map((n) => {
223
- const pos = positionMap.get(n.id);
224
  return pos ? { ...n, position: pos } : n;
225
  })
226
  );
227
  }
228
 
229
- if (!viewState.draggable) {
230
  fitView({
231
  maxZoom: 1,
232
  minZoom: breakpointsState.isMobile ? 1 : 0.7,
 
8
 
9
  let { initialNodes }: { initialNodes: Node[] } = $props();
10
 
 
11
  const DEFAULT_WIDTH = breakpointsState.isMobile ? 300 : 600;
12
  const DEFAULT_HEIGHT = 200;
13
  const H_SPACING = 60;
 
19
 
20
  let lastLayoutKey = $state<string | null>(null);
21
 
22
+ // ─── Helpers ────────────────────────────────────────────────────────
23
+
24
  function getMeasuredHeight(node: Node): number {
25
  return node.measured?.height ?? DEFAULT_HEIGHT;
26
  }
27
 
 
28
  function getMeasuredWidth(node: Node): number {
29
  return node.measured?.width ?? DEFAULT_WIDTH;
30
  }
31
 
32
+ /** Side children are placed to the RIGHT of their parent (same row). */
33
+ function isSideChild(nodeId: string, nodeMap: Map<string, Node>): boolean {
34
+ return nodeMap.get(nodeId)?.type === 'user-follow-up';
35
+ }
36
+
37
+ // ─── Layout key (triggers re-layout on structural / measurement changes) ───
38
+
39
  const layoutKey = $derived(
40
  (() => {
41
  const nodes = nodesStore.current;
 
50
  .map((e) => `${e.source}-${e.target}`)
51
  .sort()
52
  .join(',');
 
53
  const dims = ns
54
  .map((n) => `${n.id}:${n.measured?.width ?? 0}x${n.measured?.height ?? 0}`)
55
  .sort()
 
71
  runLayout(ns, es);
72
  });
73
 
74
+ // ─── Window resize ─────────────────────────────────────────────────
75
+
76
  function handleWindowResize() {
77
+ if (viewState.draggable) return;
78
  fitView({
79
  maxZoom: 1,
80
  minZoom: breakpointsState.isMobile ? 1 : 0.8,
 
83
  });
84
  }
85
 
86
+ onMount(() => window.addEventListener('resize', handleWindowResize));
87
+ onDestroy(() => window.removeEventListener('resize', handleWindowResize));
88
+
89
+ // ─── Layout algorithm ──────────────────────────────────────────────
90
+ //
91
+ // Two-phase block-based tree layout that guarantees zero overlap.
92
+ //
93
+ // Each node's subtree is split into two non-overlapping horizontal zones:
94
+ //
95
+ // Block A (left) β€” the node itself, with its below-children stacked underneath.
96
+ // Block B (right) β€” side-children (user-follow-up) and their full subtrees.
97
+ //
98
+ // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€Block A──────────────┐ β”Œβ”€β”€β”€β”€β”€Block B─────┐
99
+ // β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
100
+ // β”‚ β”‚ Parent β”‚ β”‚ β”‚ β”‚ Side node β”‚ β”‚
101
+ // β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
102
+ // β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
103
+ // β”‚ β”‚ Child A β”‚ β”‚ Child B β”‚ β”‚ β”‚ β”‚ Side sub β”‚ β”‚
104
+ // β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
105
+ // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
106
+ //
107
+ // Phase 1 (bottom-up): compute the bounding-box extent of every subtree.
108
+ // Phase 2 (top-down): assign positions, centering parents above children.
109
+ //
110
+
111
+ type Extent = {
112
+ /** Total bounding-box width of the entire subtree. */
113
+ width: number;
114
+ /** Total bounding-box height of the entire subtree. */
115
+ height: number;
116
+ /** Width of Block A only (node + below-children column). */
117
+ blockAWidth: number;
118
+ };
119
 
 
 
 
 
 
 
 
 
120
  function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
121
+ if (nodes.length === 0) return [];
122
+
123
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
124
 
125
+ // Build parent β†’ children adjacency from edges
126
+ const childrenOf = new Map<string, string[]>();
127
  const hasParent = new Set<string>();
128
+
129
  for (const e of edges) {
130
  hasParent.add(e.target);
131
+ const list = childrenOf.get(e.source) ?? [];
132
  list.push(e.target);
133
+ childrenOf.set(e.source, list);
134
  }
135
 
136
+ const roots = nodes.filter((n) => !hasParent.has(n.id));
137
+
138
+ // ── Phase 1: compute extents bottom-up ──────────────────────────
139
+
140
+ const extents = new Map<string, Extent>();
141
 
142
+ function computeExtent(nodeId: string): Extent {
143
+ if (extents.has(nodeId)) return extents.get(nodeId)!;
144
 
 
 
145
  const node = nodeMap.get(nodeId);
146
+ const nodeW = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
147
+ const nodeH = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
148
+
149
+ const children = childrenOf.get(nodeId) ?? [];
150
+ const sideIds = children.filter((id) => isSideChild(id, nodeMap));
151
+ const belowIds = children.filter((id) => !isSideChild(id, nodeMap));
152
+
153
+ // Block A β€” node width vs. combined below-children width
154
+ let belowTotalW = 0;
155
+ let belowMaxH = 0;
156
+
157
+ for (let i = 0; i < belowIds.length; i++) {
158
+ const ext = computeExtent(belowIds[i]);
159
+ if (i > 0) belowTotalW += H_SPACING;
160
+ belowTotalW += ext.width;
161
+ belowMaxH = Math.max(belowMaxH, ext.height);
162
+ }
163
+
164
+ const blockAW = Math.max(nodeW, belowTotalW);
165
+ const blockAH = belowIds.length > 0 ? nodeH + V_SPACING + belowMaxH : nodeH;
166
 
167
+ // Block B β€” side-children and their subtrees
168
+ let blockBW = 0;
169
+ let blockBH = 0;
170
 
171
+ for (let i = 0; i < sideIds.length; i++) {
172
+ const ext = computeExtent(sideIds[i]);
173
+ if (i > 0) blockBW += H_SPACING;
174
+ blockBW += ext.width;
175
+ blockBH = Math.max(blockBH, ext.height);
176
  }
177
 
178
+ const totalW = sideIds.length > 0 ? blockAW + H_SPACING + blockBW : blockAW;
179
+ const totalH = Math.max(blockAH, blockBH);
 
180
 
181
+ const extent: Extent = { width: totalW, height: totalH, blockAWidth: blockAW };
182
+ extents.set(nodeId, extent);
183
+ return extent;
184
  }
185
 
186
+ // Compute all extents (roots first, then any orphans)
187
+ for (const r of roots) computeExtent(r.id);
188
+ for (const n of nodes) {
189
+ if (!extents.has(n.id)) computeExtent(n.id);
 
 
 
 
190
  }
191
 
192
+ // ── Phase 2: assign positions top-down ──────────────────────────
193
+
194
  const positions = new Map<string, { x: number; y: number }>();
195
 
196
+ function placeSubtree(nodeId: string, allocX: number, y: number) {
197
  const node = nodeMap.get(nodeId);
198
+ const nodeW = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
199
+ const nodeH = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;
200
+
201
+ const children = childrenOf.get(nodeId) ?? [];
202
+ const sideIds = children.filter((id) => isSideChild(id, nodeMap));
203
+ const belowIds = children.filter((id) => !isSideChild(id, nodeMap));
204
+
205
+ const { blockAWidth } = extents.get(nodeId)!;
206
+
207
+ // Center the node horizontally within Block A
208
+ positions.set(nodeId, {
209
+ x: allocX + (blockAWidth - nodeW) / 2,
210
+ y
211
+ });
212
+
213
+ // Place below-children, centered within Block A
214
+ if (belowIds.length > 0) {
215
+ let belowTotalW = 0;
216
+ for (let i = 0; i < belowIds.length; i++) {
217
+ if (i > 0) belowTotalW += H_SPACING;
218
+ belowTotalW += extents.get(belowIds[i])!.width;
219
+ }
220
+
221
+ const childY = y + nodeH + V_SPACING;
222
+ let childX = allocX + (blockAWidth - belowTotalW) / 2;
223
+
224
+ for (const cid of belowIds) {
225
+ placeSubtree(cid, childX, childY);
226
+ childX += extents.get(cid)!.width + H_SPACING;
227
  }
228
  }
229
 
230
+ // Place side-children to the right of Block A (non-overlapping zone)
231
+ let sideX = allocX + blockAWidth + H_SPACING;
232
+ for (const sid of sideIds) {
233
+ placeSubtree(sid, sideX, y);
234
+ sideX += extents.get(sid)!.width + H_SPACING;
 
 
235
  }
236
  }
237
 
238
+ // Lay out root nodes side by side
239
  let rootX = 0;
240
+ for (const r of roots) {
241
+ placeSubtree(r.id, rootX, 0);
242
+ rootX += extents.get(r.id)!.width + H_SPACING;
243
  }
244
 
245
+ // Safety net for any disconnected / orphan nodes
246
  for (const node of nodes) {
247
  if (positions.has(node.id)) continue;
248
+ const maxY = Math.max(0, ...Array.from(positions.values()).map((p) => p.y));
 
249
  positions.set(node.id, { x: 0, y: maxY + DEFAULT_HEIGHT + V_SPACING });
250
  }
251
 
252
+ return nodes.map((n) => ({
253
+ ...n,
254
+ position: positions.get(n.id) ?? { x: 0, y: 0 }
255
  }));
256
  }
257
 
258
+ // ─── Apply layout to the store ─────────────────────────────────────
259
+
260
  function runLayout(nodes: Node[], edges: Edge[]) {
261
  const result = computeLayout(nodes, edges);
262
 
 
264
  if (current.length === 0) {
265
  nodesStore.set(result);
266
  } else {
267
+ const posMap = new Map(result.map((n) => [n.id, n.position]));
268
  nodesStore.update((prev) =>
269
  prev.map((n) => {
270
+ const pos = posMap.get(n.id);
271
  return pos ? { ...n, position: pos } : n;
272
  })
273
  );
274
  }
275
 
276
+ if (viewState.draggable) {
277
  fitView({
278
  maxZoom: 1,
279
  minZoom: breakpointsState.isMobile ? 1 : 0.7,
src/routes/+page.svelte CHANGED
@@ -58,7 +58,6 @@
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
 
58
  // }
59
  }}
60
  />
 
61
  <div class="h-screen w-screen overflow-hidden">
62
  <SvelteFlow
63
  bind:nodes
src/routes/api/+server.ts CHANGED
@@ -33,14 +33,13 @@ export async function POST({ request }: RequestEvent) {
33
  try {
34
  for await (const chunk of stream) {
35
  const content = chunk.choices?.[0]?.delta?.content ?? '';
36
- console.log(chunk);
37
- // const usage = {};
38
  if (content) {
39
  controller.enqueue(encoder.encode(content));
40
  }
41
  }
42
  } catch (err) {
43
- controller.error(err);
 
44
  } finally {
45
  controller.close();
46
  }
 
33
  try {
34
  for await (const chunk of stream) {
35
  const content = chunk.choices?.[0]?.delta?.content ?? '';
 
 
36
  if (content) {
37
  controller.enqueue(encoder.encode(content));
38
  }
39
  }
40
  } catch (err) {
41
+ const message = err instanceof Error ? err.message : 'An unknown error occurred';
42
+ controller.enqueue(encoder.encode(`\n\n__ERROR__${message}`));
43
  } finally {
44
  controller.close();
45
  }