nextworks 0.2.0-alpha.13 → 0.2.0-alpha.15
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.
- package/README.md +3 -1
- package/dist/cli_manifests/blocks_manifest.json +5 -0
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -0
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +2 -0
- package/dist/kits/blocks/app/templates/aiworkflow/PresetThemeVars.tsx +1 -58
- package/dist/kits/blocks/app/templates/aiworkflow/README.md +2 -0
- package/dist/kits/blocks/app/templates/aiworkflow/components/CTA.tsx +9 -9
- package/dist/kits/blocks/app/templates/aiworkflow/components/Contact.tsx +12 -13
- package/dist/kits/blocks/app/templates/aiworkflow/components/FAQ.tsx +22 -19
- package/dist/kits/blocks/app/templates/aiworkflow/components/FeatureMockups.tsx +562 -0
- package/dist/kits/blocks/app/templates/aiworkflow/components/Features.tsx +18 -16
- package/dist/kits/blocks/app/templates/aiworkflow/components/Footer.tsx +13 -9
- package/dist/kits/blocks/app/templates/aiworkflow/components/Hero.tsx +883 -636
- package/dist/kits/blocks/app/templates/aiworkflow/components/Navbar.tsx +14 -15
- package/dist/kits/blocks/app/templates/aiworkflow/components/Pricing.tsx +27 -22
- package/dist/kits/blocks/app/templates/aiworkflow/components/ProcessTimeline.tsx +20 -21
- package/dist/kits/blocks/app/templates/aiworkflow/components/Testimonials.tsx +17 -13
- package/dist/kits/blocks/app/templates/aiworkflow/components/TrustBadges.tsx +15 -12
- package/dist/kits/blocks/app/templates/aiworkflow/themes/animation.tsx +151 -0
- package/dist/kits/blocks/app/templates/aiworkflow/themes/default.tsx +158 -0
- package/dist/kits/blocks/app/templates/aiworkflow/themes/test.tsx +163 -0
- package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +46 -0
- package/dist/kits/blocks/app/templates/gallery/page.tsx +550 -161
- package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +74 -64
- package/dist/kits/blocks/components/sections/Navbar.tsx +2 -0
- package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +16 -13
- package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +283 -162
- package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +65 -53
- package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +20 -17
- package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +208 -127
- package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +95 -0
- package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +714 -161
- package/dist/kits/blocks/components/sections/product-demo/types.ts +69 -0
- package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
- package/dist/kits/blocks/package-deps.json +3 -3
- package/dist/kits/blocks/public/placeholders/aiworkflow/live.svg +92 -0
- package/dist/kits/blocks/public/placeholders/aiworkflow/review.svg +80 -0
- package/dist/kits/blocks/public/placeholders/aiworkflow/task.svg +71 -0
- package/dist/kits/blocks/tsconfig.json +13 -0
- package/dist/utils/file-operations.d.ts.map +1 -1
- package/dist/utils/file-operations.js +6 -1
- package/dist/utils/file-operations.js.map +1 -1
- 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
|
-
|
|
14
|
-
|
|
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
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
118
493
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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>
|