nextworks 0.2.0-alpha.13 → 0.2.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +3 -1
  2. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -0
  3. package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +2 -0
  4. package/dist/kits/blocks/app/templates/aiworkflow/PresetThemeVars.tsx +1 -58
  5. package/dist/kits/blocks/app/templates/aiworkflow/README.md +2 -0
  6. package/dist/kits/blocks/app/templates/aiworkflow/components/CTA.tsx +9 -9
  7. package/dist/kits/blocks/app/templates/aiworkflow/components/Contact.tsx +12 -13
  8. package/dist/kits/blocks/app/templates/aiworkflow/components/FAQ.tsx +22 -19
  9. package/dist/kits/blocks/app/templates/aiworkflow/components/FeatureMockups.tsx +562 -0
  10. package/dist/kits/blocks/app/templates/aiworkflow/components/Features.tsx +18 -16
  11. package/dist/kits/blocks/app/templates/aiworkflow/components/Footer.tsx +13 -9
  12. package/dist/kits/blocks/app/templates/aiworkflow/components/Hero.tsx +883 -636
  13. package/dist/kits/blocks/app/templates/aiworkflow/components/Navbar.tsx +14 -15
  14. package/dist/kits/blocks/app/templates/aiworkflow/components/Pricing.tsx +27 -22
  15. package/dist/kits/blocks/app/templates/aiworkflow/components/ProcessTimeline.tsx +20 -21
  16. package/dist/kits/blocks/app/templates/aiworkflow/components/Testimonials.tsx +17 -13
  17. package/dist/kits/blocks/app/templates/aiworkflow/components/TrustBadges.tsx +15 -12
  18. package/dist/kits/blocks/app/templates/aiworkflow/themes/animation.tsx +151 -0
  19. package/dist/kits/blocks/app/templates/aiworkflow/themes/default.tsx +158 -0
  20. package/dist/kits/blocks/app/templates/aiworkflow/themes/test.tsx +163 -0
  21. package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +46 -0
  22. package/dist/kits/blocks/app/templates/gallery/page.tsx +550 -161
  23. package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +74 -64
  24. package/dist/kits/blocks/components/sections/Navbar.tsx +2 -0
  25. package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +16 -13
  26. package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +283 -162
  27. package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +65 -53
  28. package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +20 -17
  29. package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +208 -127
  30. package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +95 -0
  31. package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +714 -161
  32. package/dist/kits/blocks/components/sections/product-demo/types.ts +69 -0
  33. package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
  34. package/dist/kits/blocks/package-deps.json +3 -3
  35. package/dist/kits/blocks/public/placeholders/aiworkflow/live.svg +92 -0
  36. package/dist/kits/blocks/public/placeholders/aiworkflow/review.svg +80 -0
  37. package/dist/kits/blocks/public/placeholders/aiworkflow/task.svg +71 -0
  38. package/dist/kits/blocks/tsconfig.json +13 -0
  39. package/dist/utils/file-operations.d.ts.map +1 -1
  40. package/dist/utils/file-operations.js +6 -1
  41. package/dist/utils/file-operations.js.map +1 -1
  42. package/package.json +1 -1
@@ -1,189 +1,742 @@
1
1
  import React from "react";
2
- import { cn } from "@/lib/utils";
3
2
  import type {
4
- ProductDemoStatusTone,
5
- ProductDemoWorkflowRegion,
6
3
  ProductDemoWorkflowStudioState,
4
+ ProductDemoWorkflowTranscriptEntry,
7
5
  } from "./types";
8
6
 
7
+ function useAnimatedPatchCount(target: number | undefined, visible: boolean) {
8
+ const safeTarget =
9
+ typeof target === "number" ? Math.max(target, 0) : undefined;
10
+ const [displayValue, setDisplayValue] = React.useState(() =>
11
+ typeof safeTarget === "number" ? 1 : 0,
12
+ );
13
+
14
+ React.useEffect(() => {
15
+ if (typeof safeTarget !== "number") {
16
+ return;
17
+ }
18
+
19
+ if (!visible) {
20
+ setDisplayValue(safeTarget);
21
+ return;
22
+ }
23
+
24
+ if (safeTarget <= 2) {
25
+ setDisplayValue(safeTarget);
26
+ return;
27
+ }
28
+
29
+ let frameId = 0;
30
+ const duration = Math.min(900, Math.max(520, safeTarget * 26));
31
+ const start = performance.now();
32
+
33
+ const tick = (now: number) => {
34
+ const elapsed = now - start;
35
+ const progress = Math.min(elapsed / duration, 1);
36
+ const eased = 1 - Math.pow(1 - progress, 3);
37
+ const nextValue = Math.max(1, Math.round(safeTarget * eased));
38
+
39
+ setDisplayValue(nextValue);
40
+
41
+ if (progress < 1) {
42
+ frameId = window.requestAnimationFrame(tick);
43
+ }
44
+ };
45
+
46
+ setDisplayValue(1);
47
+ frameId = window.requestAnimationFrame(tick);
48
+
49
+ return () => window.cancelAnimationFrame(frameId);
50
+ }, [safeTarget, visible]);
51
+
52
+ return typeof safeTarget === "number" ? displayValue : undefined;
53
+ }
54
+
55
+ function PatchCount({
56
+ prefix,
57
+ value,
58
+ visible,
59
+ className,
60
+ }: {
61
+ prefix: "+" | "-";
62
+ value?: number;
63
+ visible: boolean;
64
+ className: string;
65
+ }) {
66
+ const animatedValue = useAnimatedPatchCount(value, visible);
67
+
68
+ if (typeof value !== "number" || typeof animatedValue !== "number") {
69
+ return null;
70
+ }
71
+
72
+ return (
73
+ <span className={className}>
74
+ {prefix}
75
+ {animatedValue}
76
+ </span>
77
+ );
78
+ }
79
+
9
80
  export interface WorkflowStudioPanelProps {
10
81
  state: ProductDemoWorkflowStudioState;
11
82
  }
12
83
 
13
- const STATUS_TONE_CLASSES: Record<ProductDemoStatusTone, string> = {
14
- neutral: "border-border/60 bg-muted/55 text-muted-foreground",
15
- info: "border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-300",
16
- success:
17
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
18
- warning:
19
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
20
- danger: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-300",
84
+ type SessionEntry = ProductDemoWorkflowTranscriptEntry & {
85
+ origin?: "user" | "system";
21
86
  };
22
87
 
23
- function getStatusClass(tone: ProductDemoStatusTone = "neutral") {
24
- return STATUS_TONE_CLASSES[tone];
25
- }
88
+ function normalizeEntry(
89
+ entry: string | ProductDemoWorkflowTranscriptEntry,
90
+ index: number,
91
+ ): ProductDemoWorkflowTranscriptEntry {
92
+ if (typeof entry !== "string") {
93
+ return entry;
94
+ }
95
+
96
+ if (index === 0) {
97
+ return { id: `entry-${index}`, kind: "title", text: entry };
98
+ }
26
99
 
27
- function getRegionState(
28
- region: ProductDemoWorkflowRegion,
29
- activeRegionId: string | undefined,
30
- ) {
31
- const isActive = region.id === activeRegionId || region.active;
32
- const isHighlighted = region.highlighted || isActive;
100
+ if (/^thought/i.test(entry)) {
101
+ return { id: `entry-${index}`, kind: "thought", text: entry };
102
+ }
103
+
104
+ return { id: `entry-${index}`, kind: "activity", text: entry };
105
+ }
33
106
 
34
- return { isActive, isHighlighted };
107
+ function getEntryLabel(kind?: ProductDemoWorkflowTranscriptEntry["kind"]) {
108
+ switch (kind) {
109
+ case "prompt":
110
+ return "Task";
111
+ case "activity":
112
+ return "Action";
113
+ case "thought":
114
+ return "Reasoning";
115
+ case "message":
116
+ return "Update";
117
+ case "file":
118
+ return "Patch";
119
+ default:
120
+ return "Session";
121
+ }
35
122
  }
36
123
 
37
124
  export function WorkflowStudioPanel({ state }: WorkflowStudioPanelProps) {
38
- return (
39
- <div className="flex h-full flex-col gap-4">
40
- <div className="space-y-1.5">
41
- {state.title && (
42
- <h4 className="text-sm font-semibold text-card-foreground">
43
- {state.title}
44
- </h4>
45
- )}
46
- {state.subtitle && (
47
- <p className="text-xs leading-relaxed text-muted-foreground">
48
- {state.subtitle}
49
- </p>
50
- )}
51
- </div>
125
+ const scrollViewportRef = React.useRef<HTMLDivElement | null>(null);
126
+ const scrollbarTrackRef = React.useRef<HTMLDivElement | null>(null);
127
+ const dragStateRef = React.useRef<{
128
+ pointerId: number;
129
+ startY: number;
130
+ startScrollTop: number;
131
+ scrollRatio: number;
132
+ pointerOffsetY: number;
133
+ } | null>(null);
134
+ const activeIndex = state.nodes.findIndex(
135
+ (node) => node.id === state.activeNodeId || node.active,
136
+ );
52
137
 
53
- {state.regions?.length ? (
54
- <div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
55
- {state.regions.map((region) => {
56
- const { isActive, isHighlighted } = getRegionState(
57
- region,
58
- state.activeRegionId,
59
- );
60
-
61
- return (
62
- <div
63
- key={region.id}
64
- className={cn(
65
- "rounded-2xl border border-border/60 bg-background/75 p-3",
66
- isActive && "border-primary/45 bg-primary/8 shadow-sm",
67
- isHighlighted && "ring-1 ring-primary/20",
68
- )}
69
- >
70
- <div className="flex items-start justify-between gap-3">
71
- <div>
72
- <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground">
73
- Region
74
- </div>
75
- <div className="mt-1 text-sm font-semibold text-card-foreground">
76
- {region.label}
77
- </div>
78
- </div>
79
- {region.status && (
80
- <span
81
- className={cn(
82
- "rounded-full border px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em]",
83
- getStatusClass(region.status),
84
- )}
85
- >
86
- {region.status}
87
- </span>
88
- )}
89
- </div>
90
- {region.description && (
91
- <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
92
- {region.description}
93
- </p>
94
- )}
95
- {region.nodeIds?.length ? (
96
- <div className="mt-3 flex flex-wrap gap-1.5">
97
- {region.nodeIds.map((nodeId) => {
98
- const node = state.nodes.find(
99
- (item) => item.id === nodeId,
100
- );
101
-
102
- return (
103
- <span
104
- key={nodeId}
105
- className="rounded-full border border-border/60 bg-background/80 px-2 py-0.5 text-[10px] text-muted-foreground"
106
- >
107
- {node?.label ?? nodeId}
108
- </span>
109
- );
110
- })}
111
- </div>
112
- ) : null}
113
- </div>
114
- );
115
- })}
138
+ const activeNode = activeIndex >= 0 ? state.nodes[activeIndex] : undefined;
139
+ const transcript = (state.transcript ?? []).map(normalizeEntry);
140
+ const composer = state.composer;
141
+ const playbackMs = state.playbackMs ?? 1800;
142
+ const [visibleCount, setVisibleCount] = React.useState(
143
+ state.playbackStep ?? Math.max(1, Math.min(2, transcript.length)),
144
+ );
145
+ const [scrollMetrics, setScrollMetrics] = React.useState({
146
+ scrollTop: 0,
147
+ scrollHeight: 1,
148
+ clientHeight: 1,
149
+ });
150
+ const [composerValue, setComposerValue] = React.useState("");
151
+ const [localItems, setLocalItems] = React.useState<
152
+ Array<SessionEntry & { insertionIndex: number; order: number }>
153
+ >([]);
154
+ const submissionSeqRef = React.useRef(0);
155
+ const pendingTimeoutsRef = React.useRef<number[]>([]);
156
+ const previousPlaybackStepRef = React.useRef<number | undefined>(
157
+ state.playbackStep,
158
+ );
159
+
160
+ React.useEffect(() => {
161
+ setComposerValue("");
162
+ setLocalItems([]);
163
+ submissionSeqRef.current = 0;
164
+
165
+ pendingTimeoutsRef.current.forEach((timeoutId) => {
166
+ window.clearTimeout(timeoutId);
167
+ });
168
+ pendingTimeoutsRef.current = [];
169
+ }, [state.title, state.subtitle, state.activeNodeId, state.window.key]);
170
+
171
+ React.useEffect(() => {
172
+ return () => {
173
+ pendingTimeoutsRef.current.forEach((timeoutId) => {
174
+ window.clearTimeout(timeoutId);
175
+ });
176
+ pendingTimeoutsRef.current = [];
177
+ };
178
+ }, []);
179
+
180
+ const scrollToBottom = React.useCallback(() => {
181
+ const viewport = scrollViewportRef.current;
182
+
183
+ if (!viewport) {
184
+ return;
185
+ }
186
+
187
+ viewport.scrollTop = viewport.scrollHeight;
188
+ }, []);
189
+
190
+ const handleComposerSubmit = React.useCallback(
191
+ (event: React.FormEvent<HTMLFormElement>) => {
192
+ event.preventDefault();
193
+
194
+ const trimmedValue = composerValue.trim();
195
+
196
+ if (!trimmedValue) {
197
+ return;
198
+ }
199
+
200
+ const submissionIndex = submissionSeqRef.current + 1;
201
+ submissionSeqRef.current = submissionIndex;
202
+ const insertionIndex = Math.max(visibleCount, 1);
203
+
204
+ const promptEntry: SessionEntry & {
205
+ insertionIndex: number;
206
+ order: number;
207
+ } = {
208
+ id: `local-prompt-${submissionIndex}`,
209
+ kind: "prompt",
210
+ text: trimmedValue,
211
+ origin: "user",
212
+ insertionIndex,
213
+ order: 0,
214
+ };
215
+ const responseEntry: SessionEntry & {
216
+ insertionIndex: number;
217
+ order: number;
218
+ } = {
219
+ id: `local-response-${submissionIndex}`,
220
+ kind: "message",
221
+ text: "Added to session.",
222
+ origin: "system",
223
+ insertionIndex,
224
+ order: 1,
225
+ };
226
+
227
+ setLocalItems((currentItems) => [...currentItems, promptEntry]);
228
+ setComposerValue("");
229
+ window.requestAnimationFrame(scrollToBottom);
230
+
231
+ const timeoutId = window.setTimeout(() => {
232
+ setLocalItems((currentItems) => [...currentItems, responseEntry]);
233
+ window.requestAnimationFrame(scrollToBottom);
234
+ }, 680);
235
+
236
+ pendingTimeoutsRef.current.push(timeoutId);
237
+ },
238
+ [composerValue, scrollToBottom, visibleCount],
239
+ );
240
+
241
+ React.useEffect(() => {
242
+ if (typeof state.playbackStep === "number") {
243
+ const previousPlaybackStep = previousPlaybackStepRef.current;
244
+ const isPlaybackLoopReset =
245
+ typeof previousPlaybackStep === "number" &&
246
+ previousPlaybackStep > 2 &&
247
+ state.playbackStep <= 2;
248
+
249
+ if (isPlaybackLoopReset) {
250
+ setLocalItems([]);
251
+ pendingTimeoutsRef.current.forEach((timeoutId) => {
252
+ window.clearTimeout(timeoutId);
253
+ });
254
+ pendingTimeoutsRef.current = [];
255
+ }
256
+
257
+ previousPlaybackStepRef.current = state.playbackStep;
258
+ setVisibleCount(
259
+ Math.max(1, Math.min(state.playbackStep, transcript.length)),
260
+ );
261
+ return;
262
+ }
263
+
264
+ setVisibleCount(Math.max(1, Math.min(2, transcript.length)));
265
+
266
+ if (transcript.length <= 2) {
267
+ return;
268
+ }
269
+
270
+ const interval = window.setInterval(() => {
271
+ setVisibleCount((current) => {
272
+ if (current >= transcript.length) {
273
+ return 2;
274
+ }
275
+
276
+ return current + 1;
277
+ });
278
+ }, playbackMs);
279
+
280
+ return () => window.clearInterval(interval);
281
+ }, [playbackMs, transcript.length, state.window.title, state.playbackStep]);
282
+
283
+ React.useEffect(() => {
284
+ const viewport = scrollViewportRef.current;
285
+
286
+ if (!viewport) {
287
+ return;
288
+ }
289
+
290
+ const updateScrollMetrics = () => {
291
+ setScrollMetrics({
292
+ scrollTop: viewport.scrollTop,
293
+ scrollHeight: viewport.scrollHeight,
294
+ clientHeight: viewport.clientHeight,
295
+ });
296
+ };
297
+
298
+ updateScrollMetrics();
299
+
300
+ viewport.addEventListener("scroll", updateScrollMetrics, {
301
+ passive: true,
302
+ });
303
+
304
+ const resizeObserver = new ResizeObserver(() => {
305
+ updateScrollMetrics();
306
+ });
307
+
308
+ resizeObserver.observe(viewport);
309
+
310
+ if (viewport.firstElementChild instanceof HTMLElement) {
311
+ resizeObserver.observe(viewport.firstElementChild);
312
+ }
313
+
314
+ window.addEventListener("resize", updateScrollMetrics);
315
+
316
+ return () => {
317
+ viewport.removeEventListener("scroll", updateScrollMetrics);
318
+ resizeObserver.disconnect();
319
+ window.removeEventListener("resize", updateScrollMetrics);
320
+ };
321
+ }, [visibleCount, transcript.length]);
322
+
323
+ const visibleTranscript = transcript.slice(0, visibleCount);
324
+ const injectedItemsByIndex = React.useMemo(() => {
325
+ const map = new Map<number, Array<SessionEntry & { order: number }>>();
326
+
327
+ localItems.forEach((item) => {
328
+ const itemsAtIndex = map.get(item.insertionIndex) ?? [];
329
+ itemsAtIndex.push(item);
330
+ map.set(item.insertionIndex, itemsAtIndex);
331
+ });
332
+
333
+ for (const itemsAtIndex of map.values()) {
334
+ itemsAtIndex.sort((a, b) => a.order - b.order);
335
+ }
336
+
337
+ return map;
338
+ }, [localItems]);
339
+ const isRunning = visibleCount < transcript.length;
340
+
341
+ const hasOverflow =
342
+ scrollMetrics.scrollHeight > scrollMetrics.clientHeight + 1;
343
+ const thumbHeight = hasOverflow
344
+ ? Math.max(
345
+ 36,
346
+ (scrollMetrics.clientHeight / scrollMetrics.scrollHeight) *
347
+ scrollMetrics.clientHeight,
348
+ )
349
+ : 0;
350
+ const maxThumbOffset = Math.max(scrollMetrics.clientHeight - thumbHeight, 0);
351
+ const maxScrollTop = Math.max(
352
+ scrollMetrics.scrollHeight - scrollMetrics.clientHeight,
353
+ 1,
354
+ );
355
+ const thumbOffset = hasOverflow
356
+ ? (scrollMetrics.scrollTop / maxScrollTop) * maxThumbOffset
357
+ : 0;
358
+
359
+ const updateScrollTopFromPointer = React.useCallback(
360
+ (clientY: number) => {
361
+ const viewport = scrollViewportRef.current;
362
+ const track = scrollbarTrackRef.current;
363
+
364
+ if (!viewport || !track || !hasOverflow) {
365
+ return;
366
+ }
367
+
368
+ const trackRect = track.getBoundingClientRect();
369
+ const nextThumbTop = Math.min(
370
+ Math.max(
371
+ clientY - trackRect.top - dragStateRef.current!.pointerOffsetY,
372
+ 0,
373
+ ),
374
+ maxThumbOffset,
375
+ );
376
+ const nextScrollTop =
377
+ maxThumbOffset > 0 ? (nextThumbTop / maxThumbOffset) * maxScrollTop : 0;
378
+
379
+ viewport.scrollTop = nextScrollTop;
380
+ },
381
+ [hasOverflow, maxScrollTop, maxThumbOffset],
382
+ );
383
+
384
+ const handleScrollbarPointerDown = React.useCallback(
385
+ (event: React.PointerEvent<HTMLDivElement>) => {
386
+ const viewport = scrollViewportRef.current;
387
+ const track = scrollbarTrackRef.current;
388
+
389
+ if (!viewport || !track || !hasOverflow) {
390
+ return;
391
+ }
392
+
393
+ const trackRect = track.getBoundingClientRect();
394
+ const targetElement = event.target as HTMLElement | null;
395
+ const clickedThumb = targetElement?.dataset.scrollbarThumb === "true";
396
+ const pointerOffsetY = clickedThumb
397
+ ? event.clientY - trackRect.top - thumbOffset
398
+ : thumbHeight / 2;
399
+
400
+ dragStateRef.current = {
401
+ pointerId: event.pointerId,
402
+ startY: event.clientY,
403
+ startScrollTop: viewport.scrollTop,
404
+ scrollRatio: maxThumbOffset > 0 ? maxScrollTop / maxThumbOffset : 0,
405
+ pointerOffsetY,
406
+ };
407
+
408
+ if (!clickedThumb) {
409
+ updateScrollTopFromPointer(event.clientY);
410
+ }
411
+
412
+ (event.currentTarget as HTMLDivElement).setPointerCapture(
413
+ event.pointerId,
414
+ );
415
+ event.preventDefault();
416
+ },
417
+ [
418
+ hasOverflow,
419
+ maxScrollTop,
420
+ maxThumbOffset,
421
+ thumbHeight,
422
+ thumbOffset,
423
+ updateScrollTopFromPointer,
424
+ ],
425
+ );
426
+
427
+ const handleScrollbarPointerMove = React.useCallback(
428
+ (event: React.PointerEvent<HTMLDivElement>) => {
429
+ const dragState = dragStateRef.current;
430
+
431
+ if (!dragState || dragState.pointerId !== event.pointerId) {
432
+ return;
433
+ }
434
+
435
+ const viewport = scrollViewportRef.current;
436
+ const track = scrollbarTrackRef.current;
437
+
438
+ if (!viewport || !track || !hasOverflow) {
439
+ return;
440
+ }
441
+
442
+ const deltaY = event.clientY - dragState.startY;
443
+ const nextScrollTop = Math.min(
444
+ Math.max(dragState.startScrollTop + deltaY * dragState.scrollRatio, 0),
445
+ maxScrollTop,
446
+ );
447
+
448
+ viewport.scrollTop = nextScrollTop;
449
+ event.preventDefault();
450
+ },
451
+ [hasOverflow, maxScrollTop],
452
+ );
453
+
454
+ const handleScrollbarPointerUp = React.useCallback(
455
+ (event: React.PointerEvent<HTMLDivElement>) => {
456
+ const dragState = dragStateRef.current;
457
+
458
+ if (dragState?.pointerId === event.pointerId) {
459
+ dragStateRef.current = null;
460
+ }
461
+ },
462
+ [],
463
+ );
464
+
465
+ const renderSessionEntry = (
466
+ entry: SessionEntry,
467
+ index: number,
468
+ isLocalEntry = false,
469
+ ) => {
470
+ if (entry.kind === "title") {
471
+ return (
472
+ <div key={entry.id} className="space-y-2.5">
473
+ <div className="flex items-center justify-between gap-3 text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
474
+ <div className="flex min-w-0 items-center gap-2">
475
+ <span>Session focus</span>
476
+ <span className="h-1 w-1 rounded-full bg-[var(--demo-border-strong)]" />
477
+ <span className="truncate">{entry.text}</span>
478
+ </div>
479
+ {activeNode?.type ? (
480
+ <span className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-2 py-1 text-[8px] tracking-[0.16em] text-[var(--demo-muted-fg)]">
481
+ {activeNode.type}
482
+ </span>
483
+ ) : null}
484
+ </div>
485
+ {activeNode?.description ? (
486
+ <div className="rounded-lg border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-3 py-2.5 text-[12px] leading-relaxed text-[var(--demo-fg)] shadow-none">
487
+ {activeNode.description}
488
+ </div>
489
+ ) : null}
116
490
  </div>
117
- ) : null}
491
+ );
492
+ }
118
493
 
119
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
120
- {state.nodes.map((node) => {
121
- const isActive = node.id === state.activeNodeId || node.active;
494
+ if (entry.kind === "prompt" && isLocalEntry) {
495
+ return (
496
+ <div
497
+ key={entry.id}
498
+ className="space-y-1.5 rounded-lg border border-[var(--demo-border-strong)] bg-[var(--demo-shell-strong-bg)] px-3 py-2.5"
499
+ >
500
+ <div className="flex items-center justify-between gap-3 text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
501
+ <span>Prompt</span>
502
+ <span className="text-[8px] tracking-[0.18em] text-[var(--demo-subtle-fg)]">
503
+ Sent
504
+ </span>
505
+ </div>
506
+ <div className="text-[12px] leading-relaxed text-[var(--demo-fg)]">
507
+ {entry.text}
508
+ </div>
509
+ </div>
510
+ );
511
+ }
122
512
 
123
- return (
124
- <div
125
- key={node.id}
126
- className={cn(
127
- "rounded-2xl border border-border/60 bg-background/80 p-3 shadow-sm",
128
- isActive && "border-primary/45 bg-primary/6 shadow-md",
129
- node.emphasized && "ring-1 ring-primary/30",
130
- )}
131
- >
132
- <div className="flex items-start justify-between gap-3">
133
- <div>
134
- <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
135
- {node.type ?? "step"}
136
- </div>
137
- <div className="mt-1 text-sm font-semibold text-card-foreground">
138
- {node.label}
139
- </div>
140
- </div>
141
- {node.status && (
142
- <span
143
- className={cn(
144
- "rounded-full border px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em]",
145
- getStatusClass(node.status),
146
- )}
147
- >
148
- {node.status}
149
- </span>
150
- )}
151
- </div>
152
- {node.description && (
153
- <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
154
- {node.description}
155
- </p>
156
- )}
157
- {node.metadata && (
158
- <div className="mt-3 text-[11px] text-muted-foreground/90">
159
- {node.metadata}
160
- </div>
161
- )}
513
+ if (entry.kind === "prompt") {
514
+ return (
515
+ <div
516
+ key={entry.id}
517
+ className="space-y-1.5 rounded-lg border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-3 py-2.5"
518
+ >
519
+ <div className="text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
520
+ {getEntryLabel(entry.kind)}
521
+ </div>
522
+ <div className="text-[12px] leading-relaxed text-[var(--demo-fg)]">
523
+ {entry.text}
524
+ </div>
525
+ </div>
526
+ );
527
+ }
528
+
529
+ if (entry.kind === "message") {
530
+ return (
531
+ <div key={entry.id} className="max-w-[92%] space-y-1">
532
+ <div className="text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
533
+ {getEntryLabel(entry.kind)}
534
+ </div>
535
+ <div className="text-[12px] leading-relaxed text-[var(--demo-fg)]">
536
+ {isLocalEntry ? (
537
+ <span className="inline-flex items-center gap-2">
538
+ <span className="h-1.5 w-1.5 rounded-full bg-[var(--demo-accent)]" />
539
+
540
+ <span>{entry.text}</span>
541
+ </span>
542
+ ) : (
543
+ entry.text
544
+ )}
545
+ </div>
546
+ </div>
547
+ );
548
+ }
549
+
550
+ if (entry.kind === "file") {
551
+ const isNewestVisibleFile =
552
+ entry.id ===
553
+ [...visibleTranscript]
554
+ .reverse()
555
+ .find((transcriptEntry) => transcriptEntry.kind === "file")?.id;
556
+
557
+ return (
558
+ <div
559
+ key={entry.id}
560
+ className="space-y-1.5 rounded-md border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-3 py-2"
561
+ >
562
+ <div className="text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
563
+ {getEntryLabel(entry.kind)}
564
+ </div>
565
+ <div className="flex items-center justify-between gap-3 text-[11px] text-[var(--demo-fg)]">
566
+ <span className="truncate font-mono text-[11px] text-[var(--demo-fg)]">
567
+ {entry.path ?? entry.text}
568
+ </span>
569
+ <div className="flex items-center gap-2 font-mono text-[11px] tabular-nums">
570
+ <PatchCount
571
+ prefix="+"
572
+ value={entry.added}
573
+ visible={isNewestVisibleFile}
574
+ className="text-[var(--demo-info)]"
575
+ />
576
+ <PatchCount
577
+ prefix="-"
578
+ value={entry.removed}
579
+ visible={isNewestVisibleFile}
580
+ className="text-[var(--demo-danger)]"
581
+ />
162
582
  </div>
163
- );
164
- })}
583
+ </div>
584
+ </div>
585
+ );
586
+ }
587
+
588
+ if (entry.kind === "thought") {
589
+ return (
590
+ <div key={entry.id} className="space-y-1">
591
+ <div className="text-[9px] uppercase tracking-[0.15em] text-[var(--demo-subtle-fg)]">
592
+ {getEntryLabel(entry.kind)}
593
+ </div>
594
+ <div className="text-[11px] leading-relaxed text-[var(--demo-muted-fg)]">
595
+ {entry.text}
596
+ </div>
597
+ </div>
598
+ );
599
+ }
600
+
601
+ const isLastActivity =
602
+ !isLocalEntry &&
603
+ (index === visibleTranscript.length - 1 ||
604
+ (index < visibleTranscript.length - 1 &&
605
+ visibleTranscript[index + 1]?.kind === "file"));
606
+
607
+ return (
608
+ <div key={entry.id} className="space-y-1.5">
609
+ {/*
610
+ <div className="flex items-center gap-2 text-[9px] uppercase tracking-[0.15em] text-slate-400/90 dark:text-slate-500/90">
611
+ <span>{getEntryLabel(entry.kind)}</span>
612
+ <span className="h-1 w-1 rounded-full bg-[var(--demo-border-strong)]" />
613
+ <span className="truncate">
614
+ {activeNode?.type ?? "Agent"}
615
+ </span>
616
+ </div>
617
+ */}
618
+ <div className="text-[11px] leading-relaxed text-[var(--demo-muted-fg)]">
619
+ {entry.text}
620
+ </div>
621
+ {isLastActivity && activeNode?.metadata ? (
622
+ <div className="pt-1 text-[11px] leading-relaxed text-[var(--demo-muted-fg)]">
623
+ {activeNode.metadata}
624
+ </div>
625
+ ) : null}
626
+ {isRunning && index === visibleTranscript.length - 1 ? (
627
+ <div className="flex items-center gap-2 pt-1 text-[11px] text-[var(--demo-subtle-fg)]">
628
+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--demo-accent)]" />
629
+ Running
630
+ </div>
631
+ ) : null}
165
632
  </div>
633
+ );
634
+ };
166
635
 
167
- {state.branches?.length ? (
168
- <div className="rounded-2xl border border-dashed border-border/60 bg-background/60 p-3">
169
- <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground">
170
- Transitions
636
+ return (
637
+ <div className="flex h-full min-h-0 flex-col overflow-hidden bg-[var(--demo-panel-muted-bg)] text-[var(--demo-fg)] [font-synthesis:none] antialiased">
638
+ <div className="relative min-h-0 flex-1">
639
+ <div
640
+ ref={scrollViewportRef}
641
+ className="min-h-0 h-full overflow-y-auto px-4 py-4 pr-5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
642
+ >
643
+ <div className="flex min-h-full flex-col pr-1.5">
644
+ <div className="space-y-3">
645
+ {visibleTranscript.map((entry, index) => (
646
+ <React.Fragment key={entry.id}>
647
+ {renderSessionEntry(entry, index)}
648
+ {injectedItemsByIndex.has(index + 1)
649
+ ? injectedItemsByIndex
650
+ .get(index + 1)
651
+ ?.map((item) =>
652
+ renderSessionEntry(item, index + 0.5, true),
653
+ )
654
+ : null}
655
+ </React.Fragment>
656
+ ))}
657
+ </div>
658
+
659
+ <div className="flex-1" />
171
660
  </div>
172
- <div className="mt-3 flex flex-wrap gap-2">
173
- {state.branches.map((branch) => (
174
- <div
175
- key={branch.id}
176
- className={cn(
177
- "rounded-full border px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.14em]",
178
- getStatusClass(branch.status),
179
- branch.active &&
180
- "border-primary/45 bg-primary/10 text-primary",
181
- )}
182
- >
183
- {branch.label ?? `${branch.fromNodeId} → ${branch.toNodeId}`}
184
- </div>
185
- ))}
661
+ </div>
662
+
663
+ {hasOverflow ? (
664
+ <div
665
+ ref={scrollbarTrackRef}
666
+ aria-hidden="true"
667
+ onPointerDown={handleScrollbarPointerDown}
668
+ onPointerMove={handleScrollbarPointerMove}
669
+ onPointerUp={handleScrollbarPointerUp}
670
+ onPointerCancel={handleScrollbarPointerUp}
671
+ className="absolute inset-y-3 right-1.5 w-[10px] cursor-pointer overflow-hidden rounded-full bg-[var(--demo-scroll-track)]"
672
+ >
673
+ <div
674
+ data-scrollbar-thumb="true"
675
+ className="absolute inset-x-1 rounded-full bg-[var(--demo-scroll-thumb)] shadow-[inset_0_1px_0_rgba(255,255,255,0.35)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] will-change-transform"
676
+ style={{
677
+ height: `${thumbHeight}px`,
678
+ transform: `translateY(${thumbOffset}px)`,
679
+ }}
680
+ />
186
681
  </div>
682
+ ) : null}
683
+ </div>
684
+
685
+ {composer ? (
686
+ <div className="border-t border-[var(--demo-border)] bg-[var(--demo-panel-subtle-bg)] px-4 py-3">
687
+ <form
688
+ className="rounded-lg border border-[var(--demo-border)] bg-[var(--demo-panel-bg)] px-3 py-3 shadow-none"
689
+ onSubmit={handleComposerSubmit}
690
+ >
691
+ <label className="block text-[11px] text-[var(--demo-subtle-fg)]">
692
+ <span className="sr-only">
693
+ {composer.placeholder ??
694
+ "Ask the agent to inspect, search, or build..."}
695
+ </span>
696
+ <input
697
+ type="text"
698
+ value={composerValue}
699
+ onChange={(event) => setComposerValue(event.target.value)}
700
+ placeholder={
701
+ composer.placeholder ??
702
+ "Ask the agent to inspect, search, or build..."
703
+ }
704
+ className="w-full bg-transparent text-[11px] leading-5 text-[var(--demo-fg)] outline-none placeholder:text-[var(--demo-subtle-fg)]"
705
+ />
706
+ </label>
707
+
708
+ <div className="mt-3 flex items-center gap-2 text-[10px] text-[var(--demo-muted-fg)]">
709
+ <span className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-shell-strong-bg)] px-2.5 py-1 text-[var(--demo-muted-fg)]">
710
+ {composer.modeLabel ?? "Agent"}
711
+ </span>
712
+ <span className="rounded-full border border-[var(--demo-border)] bg-[var(--demo-shell-strong-bg)] px-2.5 py-1 text-[var(--demo-muted-fg)]">
713
+ {composer.modelLabel ?? "Model 2"}
714
+ </span>
715
+ <div className="ml-auto flex items-center gap-2">
716
+ <button
717
+ type="submit"
718
+ disabled={!composerValue.trim()}
719
+ aria-label="Submit prompt"
720
+ className="flex h-6 w-6 items-center justify-center rounded-full border border-[var(--demo-border)] bg-[var(--demo-shell-strong-bg)] text-[var(--demo-muted-fg)] transition hover:border-[var(--demo-border-strong)] hover:bg-[var(--demo-panel-bg)] hover:text-[var(--demo-fg)] disabled:cursor-not-allowed disabled:opacity-40"
721
+ >
722
+ <svg
723
+ aria-hidden="true"
724
+ viewBox="0 0 16 16"
725
+ className="h-3.5 w-3.5"
726
+ fill="none"
727
+ >
728
+ <path
729
+ d="M8 3.25v9.5M8 3.25 4.75 6.5M8 3.25 11.25 6.5"
730
+ stroke="currentColor"
731
+ strokeWidth="1.4"
732
+ strokeLinecap="round"
733
+ strokeLinejoin="round"
734
+ />
735
+ </svg>
736
+ </button>
737
+ </div>
738
+ </div>
739
+ </form>
187
740
  </div>
188
741
  ) : null}
189
742
  </div>