@tangle-network/ui 1.0.0
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { ArrowDown } from "lucide-react";
|
|
9
|
+
import { cn } from "../lib/utils";
|
|
10
|
+
import type { SessionMessage } from "../types/message";
|
|
11
|
+
import type { SessionPart, TextPart, ToolPart } from "../types/parts";
|
|
12
|
+
import type { AgentBranding } from "../types/branding";
|
|
13
|
+
import type { CustomToolRenderer } from "../types/tool-display";
|
|
14
|
+
import type { Run } from "../types/run";
|
|
15
|
+
import { useRunGroups } from "../hooks/use-run-groups";
|
|
16
|
+
import { useRunCollapseState } from "../hooks/use-run-collapse-state";
|
|
17
|
+
import { useAutoScroll } from "../hooks/use-auto-scroll";
|
|
18
|
+
import { MessageList } from "./message-list";
|
|
19
|
+
import {
|
|
20
|
+
AgentTimeline,
|
|
21
|
+
type AgentTimelineItem,
|
|
22
|
+
} from "./agent-timeline";
|
|
23
|
+
import { ChatInput, type PendingFile } from "./chat-input";
|
|
24
|
+
import { InlineThinkingItem } from "../run/inline-thinking-item";
|
|
25
|
+
import { getToolDisplayMetadata } from "../utils/tool-display";
|
|
26
|
+
import {
|
|
27
|
+
OpenUIArtifactRenderer,
|
|
28
|
+
type OpenUIAction,
|
|
29
|
+
type OpenUIComponentNode,
|
|
30
|
+
} from "../openui/openui-artifact-renderer";
|
|
31
|
+
|
|
32
|
+
export interface ChatContainerProps {
|
|
33
|
+
messages: SessionMessage[];
|
|
34
|
+
partMap: Record<string, SessionPart[]>;
|
|
35
|
+
isStreaming: boolean;
|
|
36
|
+
onSend?: (text: string) => void;
|
|
37
|
+
onCancel?: () => void;
|
|
38
|
+
branding?: AgentBranding;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Hide the input area (useful for read-only views). */
|
|
42
|
+
hideInput?: boolean;
|
|
43
|
+
/** Custom renderer for tool details. Return ReactNode to override, null to use default. */
|
|
44
|
+
renderToolDetail?: CustomToolRenderer;
|
|
45
|
+
/** Presentation mode for the session view. */
|
|
46
|
+
presentation?: "runs" | "timeline";
|
|
47
|
+
modelLabel?: string;
|
|
48
|
+
onModelClick?: () => void;
|
|
49
|
+
pendingFiles?: PendingFile[];
|
|
50
|
+
onRemoveFile?: (id: string) => void;
|
|
51
|
+
onAttach?: (files: FileList) => void;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
/** Callback when an OpenUI action button is pressed within inline OpenUI blocks. */
|
|
54
|
+
onOpenUIAction?: (action: OpenUIAction) => void;
|
|
55
|
+
/** Enable rendering OpenUI schemas inline in the chat timeline. Defaults to true. */
|
|
56
|
+
enableOpenUI?: boolean;
|
|
57
|
+
/** Optional actions rendered beside each grouped assistant run. */
|
|
58
|
+
renderRunActions?: (run: Run) => ReactNode;
|
|
59
|
+
/** Optional actions rendered below each user message bubble. */
|
|
60
|
+
renderUserMessageActions?: (message: SessionMessage, parts: SessionPart[]) => ReactNode;
|
|
61
|
+
/** Optional actions rendered beside individual tool items. */
|
|
62
|
+
renderToolActions?: (
|
|
63
|
+
part: ToolPart,
|
|
64
|
+
options: {
|
|
65
|
+
run: Run;
|
|
66
|
+
messageId: string;
|
|
67
|
+
partIndex: number;
|
|
68
|
+
},
|
|
69
|
+
) => ReactNode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const OPENUI_NODE_TYPES = new Set([
|
|
73
|
+
"heading", "text", "badge", "stat", "key_value", "code",
|
|
74
|
+
"markdown", "table", "actions", "separator", "stack", "grid", "card",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
function isOpenUINode(value: unknown): value is OpenUIComponentNode {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === "object" &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
"type" in value &&
|
|
82
|
+
typeof (value as Record<string, unknown>).type === "string" &&
|
|
83
|
+
OPENUI_NODE_TYPES.has((value as Record<string, unknown>).type as string)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractOpenUISchema(output: unknown): OpenUIComponentNode[] | null {
|
|
88
|
+
if (output == null) return null;
|
|
89
|
+
|
|
90
|
+
// Direct node or array of nodes
|
|
91
|
+
if (isOpenUINode(output)) return [output];
|
|
92
|
+
if (Array.isArray(output) && output.length > 0 && output.every(isOpenUINode)) {
|
|
93
|
+
return output as OpenUIComponentNode[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wrapped in { openui: ... } or { schema: ... } or { ui: ... }
|
|
97
|
+
if (typeof output === "object" && !Array.isArray(output)) {
|
|
98
|
+
const obj = output as Record<string, unknown>;
|
|
99
|
+
for (const key of ["openui", "schema", "ui"]) {
|
|
100
|
+
if (obj[key]) {
|
|
101
|
+
const inner = obj[key];
|
|
102
|
+
if (isOpenUINode(inner)) return [inner];
|
|
103
|
+
if (Array.isArray(inner) && inner.length > 0 && inner.every(isOpenUINode)) {
|
|
104
|
+
return inner as OpenUIComponentNode[];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Try to parse string as JSON containing OpenUI
|
|
111
|
+
if (typeof output === "string") {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(output);
|
|
114
|
+
return extractOpenUISchema(parsed);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatUnknown(value: unknown): string | undefined {
|
|
124
|
+
if (value == null) return undefined;
|
|
125
|
+
if (typeof value === "string") return value;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return JSON.stringify(value, null, 2);
|
|
129
|
+
} catch {
|
|
130
|
+
return String(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createdAtFromMessage(message: SessionMessage) {
|
|
135
|
+
return message.time?.created ? new Date(message.time.created) : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function mapToolPartToTimelineType(part: ToolPart) {
|
|
139
|
+
const name = part.tool.toLowerCase().replace(/^tool:/, "");
|
|
140
|
+
|
|
141
|
+
switch (name) {
|
|
142
|
+
case "bash":
|
|
143
|
+
case "shell":
|
|
144
|
+
case "command":
|
|
145
|
+
case "execute":
|
|
146
|
+
return "bash" as const;
|
|
147
|
+
case "write":
|
|
148
|
+
case "write_file":
|
|
149
|
+
case "create_file":
|
|
150
|
+
return "write" as const;
|
|
151
|
+
case "read":
|
|
152
|
+
case "read_file":
|
|
153
|
+
case "cat":
|
|
154
|
+
return "read" as const;
|
|
155
|
+
case "edit":
|
|
156
|
+
case "patch":
|
|
157
|
+
case "sed":
|
|
158
|
+
return "edit" as const;
|
|
159
|
+
case "glob":
|
|
160
|
+
case "find":
|
|
161
|
+
return "glob" as const;
|
|
162
|
+
case "ls":
|
|
163
|
+
return "list" as const;
|
|
164
|
+
case "grep":
|
|
165
|
+
case "search":
|
|
166
|
+
case "rg":
|
|
167
|
+
return "grep" as const;
|
|
168
|
+
case "inspect":
|
|
169
|
+
return "inspect" as const;
|
|
170
|
+
default:
|
|
171
|
+
return "unknown" as const;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildTimelineItems(
|
|
176
|
+
messages: SessionMessage[],
|
|
177
|
+
partMap: Record<string, SessionPart[]>,
|
|
178
|
+
isStreaming: boolean,
|
|
179
|
+
onOpenUIAction?: (action: OpenUIAction) => void,
|
|
180
|
+
enableOpenUI = true,
|
|
181
|
+
): { items: AgentTimelineItem[]; showThinking: boolean } {
|
|
182
|
+
const items: AgentTimelineItem[] = [];
|
|
183
|
+
const lastAssistantMessage = [...messages].reverse().find((message) => message.role === "assistant");
|
|
184
|
+
const toToolCall = (part: ToolPart) => {
|
|
185
|
+
const meta = getToolDisplayMetadata(part as ToolPart);
|
|
186
|
+
const start = part.state.time?.start;
|
|
187
|
+
const end = part.state.time?.end;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
id: part.id,
|
|
191
|
+
type: mapToolPartToTimelineType(part),
|
|
192
|
+
label: meta.description ? `${meta.title}: ${meta.description}` : meta.title,
|
|
193
|
+
status:
|
|
194
|
+
part.state.status === "completed"
|
|
195
|
+
? "success"
|
|
196
|
+
: part.state.status === "error"
|
|
197
|
+
? "error"
|
|
198
|
+
: "running",
|
|
199
|
+
detail: formatUnknown(part.state.input),
|
|
200
|
+
output: formatUnknown(part.state.output),
|
|
201
|
+
duration: start && end ? end - start : undefined,
|
|
202
|
+
} as const;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
for (const message of messages) {
|
|
206
|
+
const parts = partMap[message.id] ?? [];
|
|
207
|
+
|
|
208
|
+
if (message.role === "user") {
|
|
209
|
+
const content = parts
|
|
210
|
+
.filter((part): part is TextPart => part.type === "text")
|
|
211
|
+
.map((part) => part.text)
|
|
212
|
+
.join("\n")
|
|
213
|
+
.trim();
|
|
214
|
+
|
|
215
|
+
if (!content) continue;
|
|
216
|
+
|
|
217
|
+
items.push({
|
|
218
|
+
id: message.id,
|
|
219
|
+
kind: "message",
|
|
220
|
+
role: "user",
|
|
221
|
+
content,
|
|
222
|
+
timestamp: createdAtFromMessage(message),
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const toolBuffer: ToolPart[] = [];
|
|
228
|
+
const flushToolBuffer = (index: number) => {
|
|
229
|
+
if (toolBuffer.length === 0) return;
|
|
230
|
+
|
|
231
|
+
if (toolBuffer.length === 1) {
|
|
232
|
+
items.push({
|
|
233
|
+
id: `${message.id}-tool-${toolBuffer[0].id}`,
|
|
234
|
+
kind: "tool",
|
|
235
|
+
call: toToolCall(toolBuffer[0]),
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
items.push({
|
|
239
|
+
id: `${message.id}-tool-group-${index}`,
|
|
240
|
+
kind: "tool_group",
|
|
241
|
+
title: "Tool activity",
|
|
242
|
+
calls: toolBuffer.map((part) => toToolCall(part)),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Render OpenUI schemas from completed tool outputs inline
|
|
247
|
+
if (enableOpenUI) {
|
|
248
|
+
for (const part of toolBuffer) {
|
|
249
|
+
if (part.state.status !== "completed" || !part.state.output) continue;
|
|
250
|
+
const schema = extractOpenUISchema(part.state.output);
|
|
251
|
+
if (!schema) continue;
|
|
252
|
+
items.push({
|
|
253
|
+
id: `${message.id}-openui-${part.id}`,
|
|
254
|
+
kind: "custom",
|
|
255
|
+
content: (
|
|
256
|
+
<div className="my-2 rounded-[var(--radius-lg)] border border-border bg-card p-4 shadow-[var(--shadow-card)]">
|
|
257
|
+
<OpenUIArtifactRenderer schema={schema} onAction={onOpenUIAction} />
|
|
258
|
+
</div>
|
|
259
|
+
),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
toolBuffer.length = 0;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
parts.forEach((part, index) => {
|
|
268
|
+
const itemId = `${message.id}-${index}`;
|
|
269
|
+
|
|
270
|
+
if (part.type === "tool") {
|
|
271
|
+
toolBuffer.push(part);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
flushToolBuffer(index);
|
|
276
|
+
|
|
277
|
+
if (part.type === "text" && !part.synthetic && part.text.trim()) {
|
|
278
|
+
// Check if the text itself contains an OpenUI JSON block
|
|
279
|
+
if (enableOpenUI) {
|
|
280
|
+
const jsonMatch = part.text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
281
|
+
if (jsonMatch) {
|
|
282
|
+
const schema = extractOpenUISchema(jsonMatch[1]);
|
|
283
|
+
if (schema) {
|
|
284
|
+
// Render the text before the JSON block (if any)
|
|
285
|
+
const beforeJson = part.text.slice(0, part.text.indexOf("```")).trim();
|
|
286
|
+
if (beforeJson) {
|
|
287
|
+
items.push({
|
|
288
|
+
id: `${itemId}-text`,
|
|
289
|
+
kind: "message",
|
|
290
|
+
role: "assistant",
|
|
291
|
+
content: beforeJson,
|
|
292
|
+
timestamp: createdAtFromMessage(message),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
items.push({
|
|
296
|
+
id: `${itemId}-openui`,
|
|
297
|
+
kind: "custom",
|
|
298
|
+
content: (
|
|
299
|
+
<div className="my-2 rounded-[var(--radius-lg)] border border-border bg-card p-4 shadow-[var(--shadow-card)]">
|
|
300
|
+
<OpenUIArtifactRenderer schema={schema} onAction={onOpenUIAction} />
|
|
301
|
+
</div>
|
|
302
|
+
),
|
|
303
|
+
});
|
|
304
|
+
// Render text after the JSON block (if any)
|
|
305
|
+
const afterJson = part.text.slice(part.text.lastIndexOf("```") + 3).trim();
|
|
306
|
+
if (afterJson) {
|
|
307
|
+
items.push({
|
|
308
|
+
id: `${itemId}-after`,
|
|
309
|
+
kind: "message",
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: afterJson,
|
|
312
|
+
timestamp: createdAtFromMessage(message),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
items.push({
|
|
321
|
+
id: itemId,
|
|
322
|
+
kind: "message",
|
|
323
|
+
role: "assistant",
|
|
324
|
+
content: part.text,
|
|
325
|
+
timestamp: createdAtFromMessage(message),
|
|
326
|
+
isStreaming: isStreaming && lastAssistantMessage?.id === message.id && index === parts.length - 1,
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (part.type === "reasoning") {
|
|
332
|
+
items.push({
|
|
333
|
+
id: itemId,
|
|
334
|
+
kind: "custom",
|
|
335
|
+
content: <InlineThinkingItem part={part} defaultOpen={isStreaming && lastAssistantMessage?.id === message.id} />,
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
flushToolBuffer(parts.length);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const showThinking =
|
|
345
|
+
isStreaming &&
|
|
346
|
+
lastAssistantMessage != null &&
|
|
347
|
+
!items.some(
|
|
348
|
+
(item) =>
|
|
349
|
+
item.kind === "message" &&
|
|
350
|
+
item.role === "assistant" &&
|
|
351
|
+
item.id.startsWith(lastAssistantMessage.id),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return { items, showThinking };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Full chat container: message list + auto-scroll + input area.
|
|
359
|
+
* Orchestrates useRunGroups, useRunCollapseState, and useAutoScroll.
|
|
360
|
+
*/
|
|
361
|
+
export const ChatContainer = memo(
|
|
362
|
+
({
|
|
363
|
+
messages,
|
|
364
|
+
partMap,
|
|
365
|
+
isStreaming,
|
|
366
|
+
onSend,
|
|
367
|
+
onCancel,
|
|
368
|
+
branding,
|
|
369
|
+
placeholder = "Type a message...",
|
|
370
|
+
className,
|
|
371
|
+
hideInput = false,
|
|
372
|
+
renderToolDetail,
|
|
373
|
+
presentation = "runs",
|
|
374
|
+
modelLabel,
|
|
375
|
+
onModelClick,
|
|
376
|
+
pendingFiles,
|
|
377
|
+
onRemoveFile,
|
|
378
|
+
onAttach,
|
|
379
|
+
disabled = false,
|
|
380
|
+
onOpenUIAction,
|
|
381
|
+
enableOpenUI = true,
|
|
382
|
+
renderRunActions,
|
|
383
|
+
renderUserMessageActions,
|
|
384
|
+
renderToolActions,
|
|
385
|
+
}: ChatContainerProps) => {
|
|
386
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
387
|
+
|
|
388
|
+
// Group messages into runs
|
|
389
|
+
const groups = useRunGroups({ messages, partMap, isStreaming });
|
|
390
|
+
|
|
391
|
+
// Extract runs for collapse state
|
|
392
|
+
const runs = groups.filter((g) => g.type === "run").map((g) => g.run);
|
|
393
|
+
const { isCollapsed, toggleCollapse } = useRunCollapseState(runs);
|
|
394
|
+
|
|
395
|
+
// Auto-scroll
|
|
396
|
+
const { isAtBottom, scrollToBottom } = useAutoScroll(scrollRef, [
|
|
397
|
+
messages,
|
|
398
|
+
partMap,
|
|
399
|
+
isStreaming,
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
const timeline = useMemo(
|
|
403
|
+
() => buildTimelineItems(messages, partMap, isStreaming, onOpenUIAction, enableOpenUI),
|
|
404
|
+
[messages, partMap, isStreaming, onOpenUIAction, enableOpenUI],
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const handleSend = useCallback(
|
|
408
|
+
(text: string) => {
|
|
409
|
+
onSend?.(text);
|
|
410
|
+
},
|
|
411
|
+
[onSend],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div className={cn("flex h-full flex-col", className)}>
|
|
416
|
+
{/* Message area */}
|
|
417
|
+
<div
|
|
418
|
+
ref={scrollRef}
|
|
419
|
+
className="flex-1 overflow-y-auto [scrollbar-gutter:stable]"
|
|
420
|
+
>
|
|
421
|
+
{messages.length === 0 ? (
|
|
422
|
+
<div className="flex h-full items-center justify-center">
|
|
423
|
+
<div className="max-w-md text-center">
|
|
424
|
+
<div className="text-sm font-medium text-muted-foreground">Start a conversation.</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
) : presentation === "timeline" ? (
|
|
428
|
+
<div className="mx-auto flex min-h-full w-full max-w-3xl flex-col justify-end">
|
|
429
|
+
<AgentTimeline items={timeline.items} isThinking={timeline.showThinking} />
|
|
430
|
+
</div>
|
|
431
|
+
) : (
|
|
432
|
+
<div className="mx-auto flex min-h-full w-full max-w-3xl flex-col justify-end">
|
|
433
|
+
<MessageList
|
|
434
|
+
groups={groups}
|
|
435
|
+
partMap={partMap}
|
|
436
|
+
isCollapsed={isCollapsed}
|
|
437
|
+
onToggleCollapse={toggleCollapse}
|
|
438
|
+
branding={branding}
|
|
439
|
+
renderToolDetail={renderToolDetail}
|
|
440
|
+
renderRunActions={renderRunActions}
|
|
441
|
+
renderUserMessageActions={renderUserMessageActions}
|
|
442
|
+
renderToolActions={renderToolActions}
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{/* Scroll-to-bottom button */}
|
|
449
|
+
{!isAtBottom && (
|
|
450
|
+
<div className="relative z-10 -mt-10 flex justify-center">
|
|
451
|
+
<button
|
|
452
|
+
onClick={scrollToBottom}
|
|
453
|
+
className={cn(
|
|
454
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded-full",
|
|
455
|
+
"border border-border bg-card shadow-[0_6px_16px_rgba(15,23,42,0.08)]",
|
|
456
|
+
"text-xs text-foreground transition-colors hover:bg-accent",
|
|
457
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60",
|
|
458
|
+
)}
|
|
459
|
+
>
|
|
460
|
+
<ArrowDown className="w-3 h-3" />
|
|
461
|
+
Scroll to bottom
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Input area */}
|
|
467
|
+
{!hideInput && onSend && (
|
|
468
|
+
<ChatInput
|
|
469
|
+
onSend={handleSend}
|
|
470
|
+
onCancel={onCancel}
|
|
471
|
+
isStreaming={isStreaming}
|
|
472
|
+
placeholder={placeholder}
|
|
473
|
+
modelLabel={modelLabel}
|
|
474
|
+
onModelClick={onModelClick}
|
|
475
|
+
pendingFiles={pendingFiles}
|
|
476
|
+
onRemoveFile={onRemoveFile}
|
|
477
|
+
onAttach={onAttach}
|
|
478
|
+
disabled={disabled}
|
|
479
|
+
className="shrink-0 border-t border-border bg-background"
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
ChatContainer.displayName = "ChatContainer";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { ChatInput, type PendingFile } from './chat-input'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ChatInput> = {
|
|
5
|
+
title: 'Chat/ChatInput',
|
|
6
|
+
component: ChatInput,
|
|
7
|
+
parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
|
|
8
|
+
decorators: [
|
|
9
|
+
(Story) => (
|
|
10
|
+
<div className="w-[680px]">
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
15
|
+
args: {
|
|
16
|
+
onSend: (msg, files) => console.log('send', msg, files),
|
|
17
|
+
onCancel: () => console.log('cancel'),
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof ChatInput>
|
|
23
|
+
|
|
24
|
+
export const Empty: Story = {
|
|
25
|
+
name: 'Empty — ready',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const WithModelSelector: Story = {
|
|
29
|
+
name: 'With model selector',
|
|
30
|
+
args: {
|
|
31
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
32
|
+
onModelClick: () => console.log('model click'),
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const WithAttachButtons: Story = {
|
|
37
|
+
name: 'With attach buttons',
|
|
38
|
+
args: {
|
|
39
|
+
onAttach: (files) => console.log('attach', files),
|
|
40
|
+
onAttachFolder: (files) => console.log('attach folder', files),
|
|
41
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
42
|
+
onModelClick: () => console.log('model click'),
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Streaming: Story = {
|
|
47
|
+
name: 'Streaming — stop button active',
|
|
48
|
+
args: {
|
|
49
|
+
isStreaming: true,
|
|
50
|
+
onCancel: () => console.log('cancel'),
|
|
51
|
+
onAttach: (files) => console.log('attach', files),
|
|
52
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Disabled: Story = {
|
|
57
|
+
name: 'Disabled',
|
|
58
|
+
args: {
|
|
59
|
+
disabled: true,
|
|
60
|
+
onAttach: (files) => console.log('attach', files),
|
|
61
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sampleFiles: PendingFile[] = [
|
|
66
|
+
{ id: 'f1', name: 'dataset.csv', size: 204800, type: 'file', status: 'ready' },
|
|
67
|
+
{ id: 'f2', name: 'report.pdf', size: 1572864, type: 'file', status: 'uploading' },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
export const WithFilesAttached: Story = {
|
|
71
|
+
name: 'With file attachments',
|
|
72
|
+
args: {
|
|
73
|
+
onAttach: (files) => console.log('attach', files),
|
|
74
|
+
pendingFiles: sampleFiles,
|
|
75
|
+
onRemoveFile: (id) => console.log('remove', id),
|
|
76
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
77
|
+
onModelClick: () => console.log('model click'),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const folderFiles: PendingFile[] = [
|
|
82
|
+
{ id: 'd1', name: 'my-project', size: 0, type: 'folder', fileCount: 42, status: 'ready' },
|
|
83
|
+
{ id: 'f3', name: 'notes.txt', size: 1024, type: 'file', status: 'ready' },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
export const WithFolderAttached: Story = {
|
|
87
|
+
name: 'With folder + file',
|
|
88
|
+
args: {
|
|
89
|
+
onAttach: (files) => console.log('attach', files),
|
|
90
|
+
onAttachFolder: (files) => console.log('attach folder', files),
|
|
91
|
+
pendingFiles: folderFiles,
|
|
92
|
+
onRemoveFile: (id) => console.log('remove', id),
|
|
93
|
+
modelLabel: 'claude-sonnet-4-6',
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const WithErrorFile: Story = {
|
|
98
|
+
name: 'With error attachment',
|
|
99
|
+
args: {
|
|
100
|
+
onAttach: (files) => console.log('attach', files),
|
|
101
|
+
pendingFiles: [
|
|
102
|
+
{ id: 'e1', name: 'too-large.zip', size: 524288000, type: 'file', status: 'error' },
|
|
103
|
+
{ id: 'f4', name: 'config.json', size: 2048, type: 'file', status: 'ready' },
|
|
104
|
+
],
|
|
105
|
+
onRemoveFile: (id) => console.log('remove', id),
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const CustomPlaceholder: Story = {
|
|
110
|
+
name: 'Custom placeholder',
|
|
111
|
+
args: {
|
|
112
|
+
placeholder: 'Describe the data transformation you need…',
|
|
113
|
+
onAttach: (files) => console.log('attach', files),
|
|
114
|
+
modelLabel: 'gpt-4o',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Static drag-over overlay preview */
|
|
119
|
+
export const DragOverPreview: Story = {
|
|
120
|
+
name: 'Drag-over overlay (static preview)',
|
|
121
|
+
render: (args) => (
|
|
122
|
+
<div className="w-[680px] px-4 py-3 relative">
|
|
123
|
+
{/* Drag overlay replica */}
|
|
124
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-[28px] border-2 border-dashed border-blue-400 bg-blue-500/8 backdrop-blur-sm pointer-events-none mx-4 my-3">
|
|
125
|
+
<div className="text-center">
|
|
126
|
+
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500/15">
|
|
127
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
128
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
129
|
+
<polyline points="17 8 12 3 7 8" />
|
|
130
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
131
|
+
</svg>
|
|
132
|
+
</div>
|
|
133
|
+
<p className="text-sm font-semibold text-white">Drop files to add context</p>
|
|
134
|
+
<p className="mt-1 text-xs text-zinc-400">Files will be attached to your next message.</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
{/* Underlying input (blurred) */}
|
|
138
|
+
<ChatInput {...args} onAttach={() => {}} />
|
|
139
|
+
</div>
|
|
140
|
+
),
|
|
141
|
+
args: {},
|
|
142
|
+
}
|