@springbrand/message-panel 0.1.0 → 0.1.2
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/dist/demo/camel-chat-showcase.d.ts +1 -0
- package/dist/demo/camel-chat-showcase.js +1007 -0
- package/dist/demo/fixtures.d.ts +32 -0
- package/dist/demo/fixtures.js +250 -0
- package/dist/demo/index.js +4 -0
- package/dist/demo/message-panel-chat-showcase.d.ts +1 -0
- package/dist/demo/message-panel-chat-showcase.js +114 -0
- package/dist/demo/message-panel-gallery.d.ts +1 -0
- package/dist/demo/message-panel-gallery.js +130 -0
- package/dist/src/adapters/ai-sdk.d.ts +4 -0
- package/dist/src/adapters/ai-sdk.js +25 -0
- package/dist/src/camel/camel-chat-messages.d.ts +44 -0
- package/dist/src/camel/camel-chat-messages.js +421 -0
- package/dist/src/camel/camel-model-select.d.ts +13 -0
- package/dist/src/camel/camel-model-select.js +7 -0
- package/dist/src/camel/camel-plan-bar.d.ts +5 -0
- package/dist/src/camel/camel-plan-bar.js +24 -0
- package/dist/src/camel/camel-prompt-input.d.ts +32 -0
- package/dist/src/camel/camel-prompt-input.js +182 -0
- package/dist/src/camel/camel-tool-presentation.d.ts +21 -0
- package/dist/src/camel/camel-tool-presentation.js +784 -0
- package/dist/src/camel/camel-turn.d.ts +57 -0
- package/dist/src/camel/camel-turn.js +203 -0
- package/dist/src/camel/index.js +7 -0
- package/dist/src/camel/turn-summary-bar.d.ts +8 -0
- package/dist/src/camel/turn-summary-bar.js +12 -0
- package/dist/src/chat-summary-panel.d.ts +22 -0
- package/dist/src/chat-summary-panel.js +26 -0
- package/dist/src/composer/chat-composer.d.ts +4 -0
- package/dist/src/composer/chat-composer.js +90 -0
- package/dist/src/composer/composer-attachments.d.ts +13 -0
- package/dist/src/composer/composer-attachments.js +47 -0
- package/dist/src/composer/composer-trigger-popover.d.ts +31 -0
- package/dist/src/composer/composer-trigger-popover.js +29 -0
- package/dist/src/composer/composer.d.ts +15 -0
- package/dist/src/composer/composer.js +180 -0
- package/dist/src/composer/drop-controller.d.ts +8 -0
- package/dist/src/composer/drop-controller.js +69 -0
- package/dist/src/composer/index.js +6 -0
- package/dist/src/composer/types.d.ts +62 -0
- package/dist/src/composer/types.js +1 -0
- package/dist/src/contracts.d.ts +94 -0
- package/dist/src/contracts.js +1 -0
- package/dist/src/conversation.d.ts +13 -0
- package/dist/src/conversation.js +18 -0
- package/dist/src/index.js +12 -0
- package/dist/src/internal/cn.d.ts +1 -0
- package/dist/src/internal/cn.js +3 -0
- package/dist/src/message-panel.d.ts +4 -0
- package/dist/src/message-panel.js +144 -0
- package/dist/src/message.d.ts +36 -0
- package/dist/src/message.js +83 -0
- package/dist/src/parts/index.d.ts +187 -0
- package/dist/src/parts/index.js +332 -0
- package/dist/src/parts/kind.d.ts +3 -0
- package/dist/src/parts/kind.js +69 -0
- package/dist/src/parts/plan.d.ts +10 -0
- package/dist/src/parts/plan.js +40 -0
- package/dist/src/parts/primitives.d.ts +70 -0
- package/dist/src/parts/primitives.js +153 -0
- package/dist/src/registry.d.ts +4 -0
- package/dist/src/registry.js +31 -0
- package/dist/src/status.d.ts +26 -0
- package/dist/src/status.js +74 -0
- package/package.json +17 -8
- package/demo/camel-chat-showcase.tsx +0 -1265
- package/demo/fixtures.ts +0 -292
- package/demo/message-panel-chat-showcase.tsx +0 -250
- package/demo/message-panel-gallery.tsx +0 -389
- package/src/adapters/ai-sdk.ts +0 -32
- package/src/camel/camel-chat-messages.tsx +0 -1420
- package/src/camel/camel-model-select.tsx +0 -88
- package/src/camel/camel-plan-bar.tsx +0 -115
- package/src/camel/camel-prompt-input.tsx +0 -540
- package/src/camel/camel-tool-presentation.tsx +0 -1270
- package/src/camel/camel-turn.ts +0 -315
- package/src/camel/turn-summary-bar.tsx +0 -81
- package/src/chat-summary-panel.tsx +0 -229
- package/src/composer/chat-composer.tsx +0 -216
- package/src/composer/composer-attachments.tsx +0 -138
- package/src/composer/composer-trigger-popover.tsx +0 -236
- package/src/composer/composer.tsx +0 -425
- package/src/composer/drop-controller.ts +0 -80
- package/src/composer/types.ts +0 -79
- package/src/contracts.ts +0 -139
- package/src/conversation.tsx +0 -76
- package/src/internal/cn.ts +0 -3
- package/src/message-panel.tsx +0 -368
- package/src/message.tsx +0 -254
- package/src/parts/index.tsx +0 -1149
- package/src/parts/kind.ts +0 -67
- package/src/parts/plan.ts +0 -63
- package/src/parts/primitives.tsx +0 -439
- package/src/registry.ts +0 -44
- package/src/status.tsx +0 -132
- /package/{demo/index.ts → dist/demo/index.d.ts} +0 -0
- /package/{src/camel/index.ts → dist/src/camel/index.d.ts} +0 -0
- /package/{src/composer/index.ts → dist/src/composer/index.d.ts} +0 -0
- /package/{src/index.ts → dist/src/index.d.ts} +0 -0
|
@@ -1,1420 +0,0 @@
|
|
|
1
|
-
import type { UIMessage } from "ai";
|
|
2
|
-
import {
|
|
3
|
-
ArrowDownIcon,
|
|
4
|
-
BellIcon,
|
|
5
|
-
CheckIcon,
|
|
6
|
-
ChevronRightIcon,
|
|
7
|
-
CopyIcon,
|
|
8
|
-
GitForkIcon,
|
|
9
|
-
Loader2Icon,
|
|
10
|
-
TriangleAlertIcon,
|
|
11
|
-
} from "lucide-react";
|
|
12
|
-
import {
|
|
13
|
-
useCallback,
|
|
14
|
-
useEffect,
|
|
15
|
-
useMemo,
|
|
16
|
-
useRef,
|
|
17
|
-
useState,
|
|
18
|
-
type ButtonHTMLAttributes,
|
|
19
|
-
type ReactNode,
|
|
20
|
-
} from "react";
|
|
21
|
-
import {
|
|
22
|
-
MessageResponse,
|
|
23
|
-
type MessageUrlResolver,
|
|
24
|
-
} from "../message";
|
|
25
|
-
import {
|
|
26
|
-
ParallelPart,
|
|
27
|
-
SubAgentsPart,
|
|
28
|
-
resolvePartKind,
|
|
29
|
-
toolNameOfPart,
|
|
30
|
-
type ParallelTool,
|
|
31
|
-
type SubAgent,
|
|
32
|
-
} from "../parts";
|
|
33
|
-
import { deriveThinkingActivity, ThinkingIndicator } from "../status";
|
|
34
|
-
import type {
|
|
35
|
-
ToolApprovalDecision,
|
|
36
|
-
ToolApprovalView,
|
|
37
|
-
} from "../contracts";
|
|
38
|
-
import { cn } from "../internal/cn";
|
|
39
|
-
import {
|
|
40
|
-
CamelToolDetails,
|
|
41
|
-
getCamelToolPresentation,
|
|
42
|
-
} from "./camel-tool-presentation";
|
|
43
|
-
import {
|
|
44
|
-
camelCompletedAt,
|
|
45
|
-
camelCreatedAt,
|
|
46
|
-
camelMessageText,
|
|
47
|
-
camelMetadata,
|
|
48
|
-
formatCamelMessageTime,
|
|
49
|
-
projectCamelMessages,
|
|
50
|
-
type CamelAssistantTurn,
|
|
51
|
-
type CamelPartRef,
|
|
52
|
-
type CamelStandaloneMessage,
|
|
53
|
-
} from "./camel-turn";
|
|
54
|
-
import { TurnSummaryBar } from "./turn-summary-bar";
|
|
55
|
-
|
|
56
|
-
type ChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
57
|
-
|
|
58
|
-
export interface CamelPendingApproval {
|
|
59
|
-
id: string;
|
|
60
|
-
action: string;
|
|
61
|
-
summary: string;
|
|
62
|
-
executionLevelLabel: string;
|
|
63
|
-
requiredExecutionLevelLabel: string;
|
|
64
|
-
disabled?: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface CamelChatMessagesProps {
|
|
68
|
-
messages: readonly UIMessage[];
|
|
69
|
-
status: ChatStatus;
|
|
70
|
-
/** Exact tool names promoted from the collapsible trace to final output. */
|
|
71
|
-
finalToolNames?: readonly string[];
|
|
72
|
-
isHydrating?: boolean;
|
|
73
|
-
isRecovering?: boolean;
|
|
74
|
-
isServerStreaming?: boolean;
|
|
75
|
-
hasPendingSteer?: boolean;
|
|
76
|
-
startedWithPending?: boolean;
|
|
77
|
-
error?: unknown;
|
|
78
|
-
onRetry?: () => void;
|
|
79
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
80
|
-
onSuggestion: (text: string) => void;
|
|
81
|
-
onAnswer: (payload: unknown) => void;
|
|
82
|
-
onFork?: (messageId: string) => void | Promise<void>;
|
|
83
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
84
|
-
resolveUrl?: MessageUrlResolver;
|
|
85
|
-
/** Set false when durable approval actions are rendered outside the trace. */
|
|
86
|
-
inlineApprovalActions?: boolean;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const pendingSteerActivity = {
|
|
90
|
-
state: "working" as const,
|
|
91
|
-
label: "Waiting for the current step to finish…",
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type UnknownRecord = Record<string, unknown>;
|
|
95
|
-
|
|
96
|
-
type CamelButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
97
|
-
size?: "sm" | "icon" | "icon-sm";
|
|
98
|
-
variant?: "default" | "outline" | "ghost";
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
function Button({
|
|
102
|
-
className,
|
|
103
|
-
size = "sm",
|
|
104
|
-
variant = "default",
|
|
105
|
-
type = "button",
|
|
106
|
-
...props
|
|
107
|
-
}: CamelButtonProps) {
|
|
108
|
-
return (
|
|
109
|
-
<button
|
|
110
|
-
type={type}
|
|
111
|
-
className={cn(
|
|
112
|
-
"inline-flex shrink-0 items-center justify-center gap-1 rounded-md text-xs font-medium transition-all outline-none disabled:pointer-events-none disabled:opacity-50",
|
|
113
|
-
variant === "default" &&
|
|
114
|
-
"bg-primary text-primary-foreground hover:bg-primary/80",
|
|
115
|
-
variant === "outline" &&
|
|
116
|
-
"border border-border bg-background hover:bg-muted",
|
|
117
|
-
variant === "ghost" && "hover:bg-muted hover:text-foreground",
|
|
118
|
-
size === "sm" && "h-7 px-2",
|
|
119
|
-
size === "icon" && "size-9 rounded-full",
|
|
120
|
-
size === "icon-sm" && "size-8 p-0",
|
|
121
|
-
className,
|
|
122
|
-
)}
|
|
123
|
-
{...props}
|
|
124
|
-
/>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function CamelChatError({
|
|
129
|
-
error,
|
|
130
|
-
onRetry,
|
|
131
|
-
}: {
|
|
132
|
-
error: unknown;
|
|
133
|
-
onRetry?: () => void;
|
|
134
|
-
}) {
|
|
135
|
-
return (
|
|
136
|
-
<div
|
|
137
|
-
className="flex items-start gap-2 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
138
|
-
data-camel-chat-error=""
|
|
139
|
-
role="alert"
|
|
140
|
-
>
|
|
141
|
-
<TriangleAlertIcon className="size-4 shrink-0" />
|
|
142
|
-
<span className="min-w-0 flex-1 break-words">
|
|
143
|
-
{error instanceof Error
|
|
144
|
-
? error.message
|
|
145
|
-
: typeof error === "string"
|
|
146
|
-
? error
|
|
147
|
-
: "Something went wrong"}
|
|
148
|
-
</span>
|
|
149
|
-
{onRetry && (
|
|
150
|
-
<Button
|
|
151
|
-
type="button"
|
|
152
|
-
size="sm"
|
|
153
|
-
variant="outline"
|
|
154
|
-
className="shrink-0"
|
|
155
|
-
onClick={onRetry}
|
|
156
|
-
>
|
|
157
|
-
Retry
|
|
158
|
-
</Button>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function CamelApprovalPrompts({
|
|
165
|
-
approvals,
|
|
166
|
-
error,
|
|
167
|
-
onApprove,
|
|
168
|
-
}: {
|
|
169
|
-
approvals: readonly CamelPendingApproval[];
|
|
170
|
-
error?: string | null;
|
|
171
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
172
|
-
}) {
|
|
173
|
-
if (approvals.length === 0) return null;
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div className="flex flex-col gap-2" data-camel-approval-prompts="">
|
|
177
|
-
{approvals.map((approval) => (
|
|
178
|
-
<section
|
|
179
|
-
key={approval.id}
|
|
180
|
-
aria-label="等待审批"
|
|
181
|
-
className="rounded-xl border border-border bg-background p-3 shadow-sm"
|
|
182
|
-
>
|
|
183
|
-
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
184
|
-
<p className="text-xs font-medium text-muted-foreground">
|
|
185
|
-
需要审批 · {approval.action}
|
|
186
|
-
</p>
|
|
187
|
-
<p className="text-xs text-muted-foreground">
|
|
188
|
-
Agent: {approval.executionLevelLabel} · Tool:{" "}
|
|
189
|
-
{approval.requiredExecutionLevelLabel}
|
|
190
|
-
</p>
|
|
191
|
-
</div>
|
|
192
|
-
<p className="mt-1 text-sm text-foreground">{approval.summary}</p>
|
|
193
|
-
<div className="mt-2 flex flex-wrap justify-end gap-2">
|
|
194
|
-
<Button
|
|
195
|
-
variant="outline"
|
|
196
|
-
disabled={approval.disabled}
|
|
197
|
-
onClick={() => onApprove(approval.id, "deny")}
|
|
198
|
-
>
|
|
199
|
-
拒绝
|
|
200
|
-
</Button>
|
|
201
|
-
<Button
|
|
202
|
-
variant="outline"
|
|
203
|
-
disabled={approval.disabled}
|
|
204
|
-
onClick={() => onApprove(approval.id, "allow_once")}
|
|
205
|
-
>
|
|
206
|
-
仅本次允许
|
|
207
|
-
</Button>
|
|
208
|
-
<Button
|
|
209
|
-
disabled={approval.disabled}
|
|
210
|
-
onClick={() => onApprove(approval.id, "allow_level")}
|
|
211
|
-
>
|
|
212
|
-
允许该档位
|
|
213
|
-
</Button>
|
|
214
|
-
</div>
|
|
215
|
-
</section>
|
|
216
|
-
))}
|
|
217
|
-
{error && (
|
|
218
|
-
<p className="px-1 text-xs text-destructive" role="alert">
|
|
219
|
-
{error}
|
|
220
|
-
</p>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function Collapsible({
|
|
227
|
-
open,
|
|
228
|
-
onOpenChange,
|
|
229
|
-
disabled = false,
|
|
230
|
-
trigger,
|
|
231
|
-
children,
|
|
232
|
-
}: {
|
|
233
|
-
open: boolean;
|
|
234
|
-
onOpenChange: (open: boolean) => void;
|
|
235
|
-
disabled?: boolean;
|
|
236
|
-
trigger: ReactNode;
|
|
237
|
-
children: ReactNode;
|
|
238
|
-
}) {
|
|
239
|
-
return (
|
|
240
|
-
<>
|
|
241
|
-
<div onClick={() => {
|
|
242
|
-
if (!disabled) onOpenChange(!open);
|
|
243
|
-
}}>
|
|
244
|
-
{trigger}
|
|
245
|
-
</div>
|
|
246
|
-
<div
|
|
247
|
-
data-camel-collapsible-content=""
|
|
248
|
-
data-state={open ? "open" : "closed"}
|
|
249
|
-
aria-hidden={!open}
|
|
250
|
-
inert={!open ? true : undefined}
|
|
251
|
-
>
|
|
252
|
-
{open && (
|
|
253
|
-
<div data-camel-collapsible-inner="">
|
|
254
|
-
{children}
|
|
255
|
-
</div>
|
|
256
|
-
)}
|
|
257
|
-
</div>
|
|
258
|
-
</>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function recordOf(value: unknown): UnknownRecord {
|
|
263
|
-
return value != null && typeof value === "object"
|
|
264
|
-
? (value as UnknownRecord)
|
|
265
|
-
: {};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function dataOf(part: unknown): UnknownRecord {
|
|
269
|
-
const record = recordOf(part);
|
|
270
|
-
return recordOf(record.data ?? record.input ?? record);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function toolApprovalOf(part: unknown): ToolApprovalView | undefined {
|
|
274
|
-
const approval = recordOf(recordOf(part).approval);
|
|
275
|
-
return typeof approval.id === "string"
|
|
276
|
-
? {
|
|
277
|
-
id: approval.id,
|
|
278
|
-
...(typeof approval.approved === "boolean"
|
|
279
|
-
? { approved: approval.approved }
|
|
280
|
-
: {}),
|
|
281
|
-
}
|
|
282
|
-
: undefined;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function normalizedPartType(part: unknown): string {
|
|
286
|
-
const record = recordOf(part);
|
|
287
|
-
const rawType = typeof record.type === "string" ? record.type : "unknown";
|
|
288
|
-
return (
|
|
289
|
-
resolvePartKind(part) ??
|
|
290
|
-
(rawType.startsWith("data-")
|
|
291
|
-
? rawType.slice(5).replaceAll("-", "_")
|
|
292
|
-
: rawType)
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function ClipboardAction({ text }: { text: string }) {
|
|
297
|
-
const [copied, setCopied] = useState(false);
|
|
298
|
-
const timerRef = useRef<number | null>(null);
|
|
299
|
-
|
|
300
|
-
useEffect(
|
|
301
|
-
() => () => {
|
|
302
|
-
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
303
|
-
},
|
|
304
|
-
[],
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const copy = useCallback(() => {
|
|
308
|
-
void navigator.clipboard.writeText(text).then(() => {
|
|
309
|
-
setCopied(true);
|
|
310
|
-
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
311
|
-
timerRef.current = window.setTimeout(() => setCopied(false), 1500);
|
|
312
|
-
});
|
|
313
|
-
}, [text]);
|
|
314
|
-
|
|
315
|
-
return (
|
|
316
|
-
<Button
|
|
317
|
-
type="button"
|
|
318
|
-
variant="ghost"
|
|
319
|
-
size="icon-sm"
|
|
320
|
-
className="size-8 text-muted-foreground"
|
|
321
|
-
onClick={copy}
|
|
322
|
-
aria-label={copied ? "Copied!" : "Copy message"}
|
|
323
|
-
title={copied ? "Copied!" : "Copy message"}
|
|
324
|
-
>
|
|
325
|
-
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
|
326
|
-
</Button>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function CamelThinking({
|
|
331
|
-
part,
|
|
332
|
-
isActive,
|
|
333
|
-
}: {
|
|
334
|
-
part: unknown;
|
|
335
|
-
isActive: boolean;
|
|
336
|
-
}) {
|
|
337
|
-
const record = recordOf(part);
|
|
338
|
-
const data = dataOf(part);
|
|
339
|
-
const text = String(record.text ?? data.detail ?? data.text ?? "");
|
|
340
|
-
const streaming = isActive && record.state === "streaming";
|
|
341
|
-
const [open, setOpen] = useState(false);
|
|
342
|
-
|
|
343
|
-
return (
|
|
344
|
-
<div data-part-type="reasoning">
|
|
345
|
-
<Collapsible
|
|
346
|
-
open={open}
|
|
347
|
-
onOpenChange={setOpen}
|
|
348
|
-
trigger={
|
|
349
|
-
<div
|
|
350
|
-
role="button"
|
|
351
|
-
tabIndex={0}
|
|
352
|
-
className={cn(
|
|
353
|
-
"thinking-block group/thinking -mx-2 flex w-full cursor-pointer items-center gap-2 rounded px-2 py-1 text-left text-sm text-muted-foreground",
|
|
354
|
-
"transition-colors duration-150 hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50",
|
|
355
|
-
)}
|
|
356
|
-
onKeyDown={(event) => {
|
|
357
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
358
|
-
event.preventDefault();
|
|
359
|
-
setOpen((current) => !current);
|
|
360
|
-
}
|
|
361
|
-
}}
|
|
362
|
-
>
|
|
363
|
-
<span
|
|
364
|
-
className={cn(
|
|
365
|
-
"size-1.5 shrink-0 rounded-full",
|
|
366
|
-
streaming
|
|
367
|
-
? "animate-pulse bg-blue-500 motion-reduce:animate-none"
|
|
368
|
-
: "bg-green-500",
|
|
369
|
-
)}
|
|
370
|
-
/>
|
|
371
|
-
<span className="min-w-0 flex-1 truncate">
|
|
372
|
-
{streaming ? "Thinking…" : "Thought"}
|
|
373
|
-
</span>
|
|
374
|
-
<ChevronRightIcon
|
|
375
|
-
className={cn(
|
|
376
|
-
"size-4 text-muted-foreground/50 opacity-0 transition-all duration-150 group-hover/thinking:opacity-100",
|
|
377
|
-
open && "rotate-90 opacity-100",
|
|
378
|
-
)}
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
}
|
|
382
|
-
>
|
|
383
|
-
<div className="ml-1 mt-1 border-l border-border/40 pl-4 text-xs text-muted-foreground/80">
|
|
384
|
-
<MessageResponse>{text}</MessageResponse>
|
|
385
|
-
</div>
|
|
386
|
-
</Collapsible>
|
|
387
|
-
</div>
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function CamelToolRow({
|
|
392
|
-
part,
|
|
393
|
-
isActive,
|
|
394
|
-
children,
|
|
395
|
-
onApprove,
|
|
396
|
-
getToolApproval,
|
|
397
|
-
}: {
|
|
398
|
-
part: unknown;
|
|
399
|
-
isActive: boolean;
|
|
400
|
-
children?: ReactNode;
|
|
401
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
402
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
403
|
-
}) {
|
|
404
|
-
const [open, setOpen] = useState(false);
|
|
405
|
-
const approval = getToolApproval?.(part) ?? toolApprovalOf(part);
|
|
406
|
-
const pendingApproval = approval && approval.approved === undefined;
|
|
407
|
-
const presentation = getCamelToolPresentation(part, isActive);
|
|
408
|
-
const hasDetails = children != null || presentation.hasDetails;
|
|
409
|
-
const title = presentation.summary;
|
|
410
|
-
|
|
411
|
-
useEffect(() => {
|
|
412
|
-
if (presentation.isSubAgent && presentation.running) setOpen(true);
|
|
413
|
-
}, [presentation.isSubAgent, presentation.running]);
|
|
414
|
-
|
|
415
|
-
return (
|
|
416
|
-
<div data-part-type={normalizedPartType(part)}>
|
|
417
|
-
<Collapsible
|
|
418
|
-
open={open}
|
|
419
|
-
onOpenChange={setOpen}
|
|
420
|
-
disabled={!hasDetails}
|
|
421
|
-
trigger={
|
|
422
|
-
<div
|
|
423
|
-
role="button"
|
|
424
|
-
tabIndex={0}
|
|
425
|
-
aria-label={title}
|
|
426
|
-
aria-expanded={hasDetails ? open : undefined}
|
|
427
|
-
className={cn(
|
|
428
|
-
"tool-call group/toolcall -mx-2 flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm text-muted-foreground",
|
|
429
|
-
hasDetails && "cursor-pointer hover:bg-muted/30",
|
|
430
|
-
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50",
|
|
431
|
-
)}
|
|
432
|
-
onKeyDown={(event) => {
|
|
433
|
-
if (hasDetails && (event.key === "Enter" || event.key === " ")) {
|
|
434
|
-
event.preventDefault();
|
|
435
|
-
setOpen((current) => !current);
|
|
436
|
-
}
|
|
437
|
-
}}
|
|
438
|
-
>
|
|
439
|
-
<span
|
|
440
|
-
className={cn(
|
|
441
|
-
"size-1.5 shrink-0 rounded-full",
|
|
442
|
-
presentation.failed
|
|
443
|
-
? "bg-red-500"
|
|
444
|
-
: presentation.running || pendingApproval
|
|
445
|
-
? "animate-pulse bg-blue-500 motion-reduce:animate-none"
|
|
446
|
-
: "bg-green-500",
|
|
447
|
-
)}
|
|
448
|
-
/>
|
|
449
|
-
<span className="min-w-0 flex-1 truncate">
|
|
450
|
-
{title}
|
|
451
|
-
</span>
|
|
452
|
-
{hasDetails && (
|
|
453
|
-
<ChevronRightIcon
|
|
454
|
-
className={cn(
|
|
455
|
-
"size-4 text-muted-foreground/50 opacity-0 transition-all duration-150 group-hover/toolcall:opacity-100",
|
|
456
|
-
open && "rotate-90 opacity-100",
|
|
457
|
-
)}
|
|
458
|
-
/>
|
|
459
|
-
)}
|
|
460
|
-
</div>
|
|
461
|
-
}
|
|
462
|
-
>
|
|
463
|
-
{hasDetails ? (
|
|
464
|
-
<div className="group/details ml-1 mt-1 border-l border-border/50 pl-4 text-xs text-muted-foreground/80">
|
|
465
|
-
{children ?? (
|
|
466
|
-
<CamelToolDetails presentation={presentation} />
|
|
467
|
-
)}
|
|
468
|
-
</div>
|
|
469
|
-
) : null}
|
|
470
|
-
</Collapsible>
|
|
471
|
-
{pendingApproval && (
|
|
472
|
-
<div className="ml-1 mt-2 flex flex-wrap items-center gap-2 border-l border-border/50 pl-4">
|
|
473
|
-
<Button
|
|
474
|
-
type="button"
|
|
475
|
-
size="sm"
|
|
476
|
-
variant="outline"
|
|
477
|
-
onClick={() => onApprove(approval.id, "deny")}
|
|
478
|
-
>
|
|
479
|
-
Deny
|
|
480
|
-
</Button>
|
|
481
|
-
<Button
|
|
482
|
-
type="button"
|
|
483
|
-
size="sm"
|
|
484
|
-
variant="outline"
|
|
485
|
-
onClick={() => onApprove(approval.id, "allow_once")}
|
|
486
|
-
>
|
|
487
|
-
Allow once
|
|
488
|
-
</Button>
|
|
489
|
-
<Button
|
|
490
|
-
type="button"
|
|
491
|
-
size="sm"
|
|
492
|
-
onClick={() => onApprove(approval.id, "allow_level")}
|
|
493
|
-
>
|
|
494
|
-
Allow this level
|
|
495
|
-
</Button>
|
|
496
|
-
</div>
|
|
497
|
-
)}
|
|
498
|
-
</div>
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function CamelSuggestions({
|
|
503
|
-
part,
|
|
504
|
-
onSuggestion,
|
|
505
|
-
}: {
|
|
506
|
-
part: unknown;
|
|
507
|
-
onSuggestion: (text: string) => void;
|
|
508
|
-
}) {
|
|
509
|
-
const data = dataOf(part);
|
|
510
|
-
const items = Array.isArray(data.items)
|
|
511
|
-
? data.items.filter((item): item is string => typeof item === "string")
|
|
512
|
-
: [];
|
|
513
|
-
return (
|
|
514
|
-
<div
|
|
515
|
-
className="space-y-2 pt-1"
|
|
516
|
-
data-part-type="suggestions"
|
|
517
|
-
>
|
|
518
|
-
<div className="flex items-center gap-2 text-sm text-emerald-600">
|
|
519
|
-
<CheckIcon className="size-4" />
|
|
520
|
-
<span>Task complete</span>
|
|
521
|
-
</div>
|
|
522
|
-
<div className="flex flex-wrap gap-2">
|
|
523
|
-
{items.map((item) => (
|
|
524
|
-
<Button
|
|
525
|
-
key={item}
|
|
526
|
-
type="button"
|
|
527
|
-
variant="outline"
|
|
528
|
-
size="sm"
|
|
529
|
-
className="rounded-full"
|
|
530
|
-
onClick={() => onSuggestion(item)}
|
|
531
|
-
>
|
|
532
|
-
{item}
|
|
533
|
-
</Button>
|
|
534
|
-
))}
|
|
535
|
-
</div>
|
|
536
|
-
</div>
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function scheduleDuration(seconds: unknown): string {
|
|
541
|
-
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
|
542
|
-
return "";
|
|
543
|
-
}
|
|
544
|
-
for (const [size, unit] of [
|
|
545
|
-
[86_400, "day"],
|
|
546
|
-
[3_600, "hour"],
|
|
547
|
-
[60, "minute"],
|
|
548
|
-
] as const) {
|
|
549
|
-
if (seconds % size === 0) {
|
|
550
|
-
const value = seconds / size;
|
|
551
|
-
return `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function scheduleCadence(data: UnknownRecord): string {
|
|
558
|
-
const trigger = recordOf(data.trigger);
|
|
559
|
-
const kind = trigger.kind;
|
|
560
|
-
let cadence = "";
|
|
561
|
-
if (kind === "once-in") {
|
|
562
|
-
cadence = `In ${scheduleDuration(trigger.seconds)}`;
|
|
563
|
-
} else if (kind === "once-at" && typeof trigger.at === "string") {
|
|
564
|
-
cadence = `Once at ${trigger.at}`;
|
|
565
|
-
} else if (kind === "interval") {
|
|
566
|
-
cadence = `Every ${scheduleDuration(trigger.seconds)}`;
|
|
567
|
-
} else if (kind === "cron" && typeof trigger.cron === "string") {
|
|
568
|
-
cadence = `Cron ${trigger.cron}`;
|
|
569
|
-
}
|
|
570
|
-
if (!cadence) {
|
|
571
|
-
cadence = String(data.cadence ?? data.schedule ?? "Scheduled");
|
|
572
|
-
}
|
|
573
|
-
const timezone =
|
|
574
|
-
typeof data.timezone === "string"
|
|
575
|
-
? data.timezone
|
|
576
|
-
: typeof data.tz === "string"
|
|
577
|
-
? data.tz
|
|
578
|
-
: "";
|
|
579
|
-
return timezone ? `${cadence} · ${timezone}` : cadence;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function CamelSchedule({
|
|
583
|
-
part,
|
|
584
|
-
isActive,
|
|
585
|
-
}: {
|
|
586
|
-
part: unknown;
|
|
587
|
-
isActive: boolean;
|
|
588
|
-
}) {
|
|
589
|
-
const data = dataOf(part);
|
|
590
|
-
const presentation = getCamelToolPresentation(part, isActive);
|
|
591
|
-
const state = presentation.failed
|
|
592
|
-
? "failed"
|
|
593
|
-
: presentation.running
|
|
594
|
-
? "running"
|
|
595
|
-
: "scheduled";
|
|
596
|
-
return (
|
|
597
|
-
<section
|
|
598
|
-
className="rounded-xl border border-border bg-muted/20 p-3"
|
|
599
|
-
data-part-type="schedule"
|
|
600
|
-
data-state={state}
|
|
601
|
-
>
|
|
602
|
-
<div className="flex items-start gap-3">
|
|
603
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
604
|
-
<BellIcon className="size-4" />
|
|
605
|
-
</span>
|
|
606
|
-
<div className="min-w-0 flex-1">
|
|
607
|
-
<strong className="block truncate text-sm text-foreground">
|
|
608
|
-
{String(data.label ?? data.title ?? "Scheduled task")}
|
|
609
|
-
</strong>
|
|
610
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
611
|
-
{scheduleCadence(data)}
|
|
612
|
-
</p>
|
|
613
|
-
</div>
|
|
614
|
-
<span
|
|
615
|
-
className={cn(
|
|
616
|
-
"flex shrink-0 items-center gap-1 text-xs",
|
|
617
|
-
state === "failed"
|
|
618
|
-
? "text-destructive"
|
|
619
|
-
: state === "running"
|
|
620
|
-
? "text-blue-500"
|
|
621
|
-
: "text-emerald-600",
|
|
622
|
-
)}
|
|
623
|
-
>
|
|
624
|
-
{state === "failed" ? (
|
|
625
|
-
<TriangleAlertIcon className="size-3.5" />
|
|
626
|
-
) : state === "running" ? (
|
|
627
|
-
<Loader2Icon className="size-3.5 animate-spin motion-reduce:animate-none" />
|
|
628
|
-
) : (
|
|
629
|
-
<CheckIcon className="size-3.5" />
|
|
630
|
-
)}
|
|
631
|
-
{state === "failed"
|
|
632
|
-
? "Failed"
|
|
633
|
-
: state === "running"
|
|
634
|
-
? "Scheduling…"
|
|
635
|
-
: "Scheduled"}
|
|
636
|
-
</span>
|
|
637
|
-
</div>
|
|
638
|
-
</section>
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function CamelAskUser({
|
|
643
|
-
part,
|
|
644
|
-
submittedAnswer,
|
|
645
|
-
onAnswer,
|
|
646
|
-
}: {
|
|
647
|
-
part: unknown;
|
|
648
|
-
submittedAnswer?: string;
|
|
649
|
-
onAnswer: (payload: unknown) => void;
|
|
650
|
-
}) {
|
|
651
|
-
const record = recordOf(part);
|
|
652
|
-
const data = dataOf(part);
|
|
653
|
-
const output = record.output;
|
|
654
|
-
const questions = Array.isArray(data.questions)
|
|
655
|
-
? data.questions.map(recordOf)
|
|
656
|
-
: typeof data.question === "string"
|
|
657
|
-
? [{
|
|
658
|
-
prompt: data.question,
|
|
659
|
-
kind: data.multiSelect === true ? "multi" : "single",
|
|
660
|
-
options: Array.isArray(data.options)
|
|
661
|
-
? data.options.filter(
|
|
662
|
-
(option): option is string => typeof option === "string",
|
|
663
|
-
)
|
|
664
|
-
: [],
|
|
665
|
-
}]
|
|
666
|
-
: [];
|
|
667
|
-
const [selections, setSelections] = useState<string[][]>(() =>
|
|
668
|
-
questions.map(() => []),
|
|
669
|
-
);
|
|
670
|
-
const [text, setText] = useState("");
|
|
671
|
-
const [submitted, setSubmitted] = useState(false);
|
|
672
|
-
const outputRecord = recordOf(output);
|
|
673
|
-
const externalAnswer =
|
|
674
|
-
submittedAnswer?.trim() ||
|
|
675
|
-
(typeof output === "string"
|
|
676
|
-
? output.trim()
|
|
677
|
-
: typeof outputRecord.text === "string"
|
|
678
|
-
? outputRecord.text.trim()
|
|
679
|
-
: "");
|
|
680
|
-
const externalSelections = new Set(
|
|
681
|
-
externalAnswer
|
|
682
|
-
.split("、")
|
|
683
|
-
.map((value) => value.trim())
|
|
684
|
-
.filter(Boolean),
|
|
685
|
-
);
|
|
686
|
-
const resolvedSubmitted = submitted || Boolean(externalAnswer);
|
|
687
|
-
const valid = questions.every((question, index) => {
|
|
688
|
-
const options = Array.isArray(question.options) ? question.options : [];
|
|
689
|
-
return options.length === 0 || (selections[index]?.length ?? 0) > 0;
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
return (
|
|
693
|
-
<section
|
|
694
|
-
className="space-y-3 rounded-xl border border-border bg-muted/20 p-3"
|
|
695
|
-
data-part-type="ask_user"
|
|
696
|
-
data-state={resolvedSubmitted ? "submitted" : "idle"}
|
|
697
|
-
>
|
|
698
|
-
{questions.map((question, questionIndex) => {
|
|
699
|
-
const prompt = String(question.prompt ?? "Choose an option");
|
|
700
|
-
const options = Array.isArray(question.options)
|
|
701
|
-
? question.options.filter(
|
|
702
|
-
(option): option is string => typeof option === "string",
|
|
703
|
-
)
|
|
704
|
-
: [];
|
|
705
|
-
const multi = question.kind === "multi";
|
|
706
|
-
return (
|
|
707
|
-
<div className="space-y-2" key={`${prompt}:${questionIndex}`}>
|
|
708
|
-
<p className="text-sm text-foreground">{prompt}</p>
|
|
709
|
-
<div className="flex flex-wrap gap-2">
|
|
710
|
-
{options.map((option) => {
|
|
711
|
-
const selected = externalAnswer
|
|
712
|
-
? externalSelections.has(option)
|
|
713
|
-
: selections[questionIndex]?.includes(option);
|
|
714
|
-
return (
|
|
715
|
-
<Button
|
|
716
|
-
key={option}
|
|
717
|
-
type="button"
|
|
718
|
-
size="sm"
|
|
719
|
-
variant={selected ? "default" : "outline"}
|
|
720
|
-
disabled={resolvedSubmitted}
|
|
721
|
-
onClick={() =>
|
|
722
|
-
setSelections((current) =>
|
|
723
|
-
current.map((values, index) => {
|
|
724
|
-
if (index !== questionIndex) return values;
|
|
725
|
-
if (!multi) return [option];
|
|
726
|
-
return values.includes(option)
|
|
727
|
-
? values.filter((value) => value !== option)
|
|
728
|
-
: [...values, option];
|
|
729
|
-
}),
|
|
730
|
-
)
|
|
731
|
-
}
|
|
732
|
-
>
|
|
733
|
-
{option}
|
|
734
|
-
</Button>
|
|
735
|
-
);
|
|
736
|
-
})}
|
|
737
|
-
</div>
|
|
738
|
-
</div>
|
|
739
|
-
);
|
|
740
|
-
})}
|
|
741
|
-
{typeof data.freeTextPlaceholder === "string" && (
|
|
742
|
-
<input
|
|
743
|
-
value={text}
|
|
744
|
-
disabled={resolvedSubmitted}
|
|
745
|
-
onChange={(event) => setText(event.currentTarget.value)}
|
|
746
|
-
placeholder={data.freeTextPlaceholder}
|
|
747
|
-
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring focus:ring-2 focus:ring-ring/30"
|
|
748
|
-
/>
|
|
749
|
-
)}
|
|
750
|
-
<Button
|
|
751
|
-
type="button"
|
|
752
|
-
size="sm"
|
|
753
|
-
disabled={resolvedSubmitted || !valid}
|
|
754
|
-
onClick={() => {
|
|
755
|
-
onAnswer({ selections, text });
|
|
756
|
-
setSubmitted(true);
|
|
757
|
-
}}
|
|
758
|
-
>
|
|
759
|
-
{resolvedSubmitted ? "Submitted" : "Submit"}
|
|
760
|
-
</Button>
|
|
761
|
-
{externalAnswer && (
|
|
762
|
-
<p className="text-xs text-muted-foreground">
|
|
763
|
-
已回答:{externalAnswer}
|
|
764
|
-
</p>
|
|
765
|
-
)}
|
|
766
|
-
</section>
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function CamelRichPart({
|
|
771
|
-
part,
|
|
772
|
-
isActive,
|
|
773
|
-
onApprove,
|
|
774
|
-
onSuggestion,
|
|
775
|
-
onAnswer,
|
|
776
|
-
replyText,
|
|
777
|
-
getToolApproval,
|
|
778
|
-
}: {
|
|
779
|
-
part: unknown;
|
|
780
|
-
isActive: boolean;
|
|
781
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
782
|
-
onSuggestion: (text: string) => void;
|
|
783
|
-
onAnswer: (payload: unknown) => void;
|
|
784
|
-
replyText?: string;
|
|
785
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
786
|
-
}) {
|
|
787
|
-
const kind = normalizedPartType(part);
|
|
788
|
-
const data = dataOf(part);
|
|
789
|
-
|
|
790
|
-
if (kind === "suggestions") {
|
|
791
|
-
return <CamelSuggestions part={part} onSuggestion={onSuggestion} />;
|
|
792
|
-
}
|
|
793
|
-
if (kind === "schedule") {
|
|
794
|
-
return <CamelSchedule part={part} isActive={isActive} />;
|
|
795
|
-
}
|
|
796
|
-
if (kind === "ask_user") {
|
|
797
|
-
return (
|
|
798
|
-
<CamelAskUser
|
|
799
|
-
part={part}
|
|
800
|
-
submittedAnswer={replyText}
|
|
801
|
-
onAnswer={onAnswer}
|
|
802
|
-
/>
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
if (kind === "parallel") {
|
|
806
|
-
const tools = Array.isArray(data.tools)
|
|
807
|
-
? (data.tools as ParallelTool[])
|
|
808
|
-
: [];
|
|
809
|
-
return (
|
|
810
|
-
<ParallelPart
|
|
811
|
-
label={
|
|
812
|
-
typeof data.label === "string" ? data.label : "Parallel tasks"
|
|
813
|
-
}
|
|
814
|
-
tools={tools}
|
|
815
|
-
isActive={isActive}
|
|
816
|
-
/>
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
if (kind === "subagents") {
|
|
820
|
-
const presentation = getCamelToolPresentation(part, isActive);
|
|
821
|
-
const toolName = toolNameOfPart(part);
|
|
822
|
-
const fallbackName =
|
|
823
|
-
toolName === "run_temporary_agent"
|
|
824
|
-
? "Temporary Agent"
|
|
825
|
-
: toolName === "dispatch_background"
|
|
826
|
-
? `${String(data.agentType ?? "Background").replace(/^./, (value) => value.toUpperCase())} Agent`
|
|
827
|
-
: toolName === "fanout"
|
|
828
|
-
? "Fanout Agent"
|
|
829
|
-
: toolName === "extract"
|
|
830
|
-
? "Extract Agent"
|
|
831
|
-
: "Sub-agent";
|
|
832
|
-
const agents = Array.isArray(data.agents)
|
|
833
|
-
? (data.agents as SubAgent[])
|
|
834
|
-
: [
|
|
835
|
-
{
|
|
836
|
-
name: String(
|
|
837
|
-
data.subagentName ??
|
|
838
|
-
data.name ??
|
|
839
|
-
fallbackName,
|
|
840
|
-
),
|
|
841
|
-
summary:
|
|
842
|
-
presentation.errorText ||
|
|
843
|
-
presentation.outputText ||
|
|
844
|
-
String(data.summary ?? ""),
|
|
845
|
-
status: presentation.failed
|
|
846
|
-
? "failed" as const
|
|
847
|
-
: presentation.running
|
|
848
|
-
? "running" as const
|
|
849
|
-
: "done" as const,
|
|
850
|
-
},
|
|
851
|
-
];
|
|
852
|
-
return <SubAgentsPart agents={agents} isActive={isActive} />;
|
|
853
|
-
}
|
|
854
|
-
if (kind === "image") {
|
|
855
|
-
const src = String(data.src ?? data.url ?? "");
|
|
856
|
-
return (
|
|
857
|
-
<figure className="my-1" data-part-type="image">
|
|
858
|
-
<img
|
|
859
|
-
src={src}
|
|
860
|
-
alt={String(data.alt ?? data.filename ?? "Generated image")}
|
|
861
|
-
className="max-h-[28rem] max-w-full rounded-lg border border-border object-contain"
|
|
862
|
-
/>
|
|
863
|
-
</figure>
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
if (kind === "error") {
|
|
867
|
-
return (
|
|
868
|
-
<div
|
|
869
|
-
className="flex items-start gap-2 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
870
|
-
data-part-type="error"
|
|
871
|
-
role="alert"
|
|
872
|
-
>
|
|
873
|
-
<TriangleAlertIcon className="mt-0.5 size-4 shrink-0" />
|
|
874
|
-
<div>
|
|
875
|
-
<div className="font-medium">{String(data.title ?? "Error")}</div>
|
|
876
|
-
<div className="mt-0.5 text-xs opacity-80">
|
|
877
|
-
{String(data.body ?? data.message ?? "")}
|
|
878
|
-
</div>
|
|
879
|
-
</div>
|
|
880
|
-
</div>
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
return (
|
|
885
|
-
<CamelToolRow
|
|
886
|
-
part={part}
|
|
887
|
-
isActive={isActive}
|
|
888
|
-
onApprove={onApprove}
|
|
889
|
-
getToolApproval={getToolApproval}
|
|
890
|
-
/>
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function AssistantPart({
|
|
895
|
-
part,
|
|
896
|
-
isActive,
|
|
897
|
-
onApprove,
|
|
898
|
-
onSuggestion,
|
|
899
|
-
onAnswer,
|
|
900
|
-
replyText,
|
|
901
|
-
getToolApproval,
|
|
902
|
-
resolveUrl,
|
|
903
|
-
}: {
|
|
904
|
-
part: unknown;
|
|
905
|
-
isActive: boolean;
|
|
906
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
907
|
-
onSuggestion: (text: string) => void;
|
|
908
|
-
onAnswer: (payload: unknown) => void;
|
|
909
|
-
replyText?: string;
|
|
910
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
911
|
-
resolveUrl?: MessageUrlResolver;
|
|
912
|
-
}) {
|
|
913
|
-
const record = recordOf(part);
|
|
914
|
-
if (record.type === "step-start") return null;
|
|
915
|
-
if (record.type === "text") {
|
|
916
|
-
const text = String(record.text ?? "").trim();
|
|
917
|
-
if (!text) return null;
|
|
918
|
-
return (
|
|
919
|
-
<div data-part-type="text">
|
|
920
|
-
<MessageResponse streaming={isActive} resolveUrl={resolveUrl}>
|
|
921
|
-
{text}
|
|
922
|
-
</MessageResponse>
|
|
923
|
-
</div>
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
if (record.type === "reasoning") {
|
|
927
|
-
return <CamelThinking part={part} isActive={isActive} />;
|
|
928
|
-
}
|
|
929
|
-
return (
|
|
930
|
-
<CamelRichPart
|
|
931
|
-
part={part}
|
|
932
|
-
isActive={isActive}
|
|
933
|
-
onApprove={onApprove}
|
|
934
|
-
onSuggestion={onSuggestion}
|
|
935
|
-
onAnswer={onAnswer}
|
|
936
|
-
replyText={replyText}
|
|
937
|
-
getToolApproval={getToolApproval}
|
|
938
|
-
/>
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function ForkAction({
|
|
943
|
-
messageId,
|
|
944
|
-
onFork,
|
|
945
|
-
}: {
|
|
946
|
-
messageId: string;
|
|
947
|
-
onFork: (messageId: string) => void | Promise<void>;
|
|
948
|
-
}) {
|
|
949
|
-
const [forking, setForking] = useState(false);
|
|
950
|
-
|
|
951
|
-
return (
|
|
952
|
-
<Button
|
|
953
|
-
type="button"
|
|
954
|
-
variant="ghost"
|
|
955
|
-
size="icon-sm"
|
|
956
|
-
className="size-8 text-muted-foreground"
|
|
957
|
-
disabled={forking}
|
|
958
|
-
onClick={() => {
|
|
959
|
-
setForking(true);
|
|
960
|
-
void Promise.resolve(onFork(messageId)).finally(() => setForking(false));
|
|
961
|
-
}}
|
|
962
|
-
aria-label={forking ? "Starting new thread…" : "New thread from here"}
|
|
963
|
-
title={forking ? "Starting new thread…" : "New thread from here"}
|
|
964
|
-
>
|
|
965
|
-
{forking ? (
|
|
966
|
-
<Loader2Icon className="size-4 animate-spin" />
|
|
967
|
-
) : (
|
|
968
|
-
<GitForkIcon className="size-4" />
|
|
969
|
-
)}
|
|
970
|
-
</Button>
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function UserMessage({ message }: { message: UIMessage }) {
|
|
975
|
-
const text = camelMessageText(message);
|
|
976
|
-
const files = message.parts.filter((part) => part.type === "file");
|
|
977
|
-
const metadata = camelMetadata(message);
|
|
978
|
-
const createdAt = camelCreatedAt(message);
|
|
979
|
-
const messageTime =
|
|
980
|
-
createdAt === undefined ? null : formatCamelMessageTime(createdAt);
|
|
981
|
-
const authorStamp = [metadata.authorDisplayName, messageTime]
|
|
982
|
-
.filter(Boolean)
|
|
983
|
-
.join(" ");
|
|
984
|
-
|
|
985
|
-
if (!text && files.length === 0) return null;
|
|
986
|
-
|
|
987
|
-
return (
|
|
988
|
-
<div
|
|
989
|
-
className="group mb-1 mt-6"
|
|
990
|
-
data-message-id={message.id}
|
|
991
|
-
data-message-role="user"
|
|
992
|
-
>
|
|
993
|
-
<div className="flex flex-col items-end gap-2">
|
|
994
|
-
{files.length > 0 && (
|
|
995
|
-
<div className="flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
996
|
-
{files.map((file, index) =>
|
|
997
|
-
file.mediaType?.startsWith("image/") ? (
|
|
998
|
-
<img
|
|
999
|
-
key={`${file.url}:${index}`}
|
|
1000
|
-
src={file.url}
|
|
1001
|
-
alt={file.filename ?? "Attachment"}
|
|
1002
|
-
className="max-h-52 max-w-64 rounded-xl border border-border object-cover"
|
|
1003
|
-
/>
|
|
1004
|
-
) : (
|
|
1005
|
-
<span
|
|
1006
|
-
key={`${file.url}:${index}`}
|
|
1007
|
-
className="rounded-lg border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground"
|
|
1008
|
-
>
|
|
1009
|
-
{file.filename ?? "Attachment"}
|
|
1010
|
-
</span>
|
|
1011
|
-
),
|
|
1012
|
-
)}
|
|
1013
|
-
</div>
|
|
1014
|
-
)}
|
|
1015
|
-
{text && (
|
|
1016
|
-
<div className="camel-user-message max-w-[85%] rounded-3xl border border-border bg-muted/30 px-4 py-3 text-foreground">
|
|
1017
|
-
<MessageResponse>{text}</MessageResponse>
|
|
1018
|
-
</div>
|
|
1019
|
-
)}
|
|
1020
|
-
{text && (
|
|
1021
|
-
<div
|
|
1022
|
-
className="flex items-center justify-end gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 pointer-coarse:opacity-100"
|
|
1023
|
-
role="group"
|
|
1024
|
-
aria-label="Message actions"
|
|
1025
|
-
>
|
|
1026
|
-
{authorStamp && (
|
|
1027
|
-
<span className="mr-1 text-xs text-muted-foreground">
|
|
1028
|
-
{authorStamp}
|
|
1029
|
-
</span>
|
|
1030
|
-
)}
|
|
1031
|
-
<ClipboardAction text={text} />
|
|
1032
|
-
</div>
|
|
1033
|
-
)}
|
|
1034
|
-
</div>
|
|
1035
|
-
</div>
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
function standaloneText(message: UIMessage): string {
|
|
1040
|
-
const part = message.parts.find((candidate) =>
|
|
1041
|
-
candidate.type.startsWith("data-"),
|
|
1042
|
-
);
|
|
1043
|
-
const data = dataOf(part);
|
|
1044
|
-
return String(data.text ?? data.command ?? data.output ?? "").trim();
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function CompactSummary({ message }: { message: UIMessage }) {
|
|
1048
|
-
const text = standaloneText(message);
|
|
1049
|
-
if (!text) return null;
|
|
1050
|
-
return (
|
|
1051
|
-
<details
|
|
1052
|
-
className="my-3 rounded-xl border border-border bg-muted/20 px-3 py-2 text-sm"
|
|
1053
|
-
data-camel-compact-summary=""
|
|
1054
|
-
data-message-id={message.id}
|
|
1055
|
-
>
|
|
1056
|
-
<summary className="cursor-pointer text-xs font-medium text-muted-foreground">
|
|
1057
|
-
Compacted context
|
|
1058
|
-
</summary>
|
|
1059
|
-
<div className="mt-3 border-t border-border/40 pt-3">
|
|
1060
|
-
<MessageResponse>{text}</MessageResponse>
|
|
1061
|
-
</div>
|
|
1062
|
-
</details>
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function StandaloneMessage({ item }: { item: CamelStandaloneMessage }) {
|
|
1067
|
-
const { message, displayKind } = item;
|
|
1068
|
-
if (displayKind === "user") return <UserMessage message={message} />;
|
|
1069
|
-
if (displayKind === "compact-summary") {
|
|
1070
|
-
return <CompactSummary message={message} />;
|
|
1071
|
-
}
|
|
1072
|
-
const text = standaloneText(message);
|
|
1073
|
-
if (!text) return null;
|
|
1074
|
-
if (displayKind === "slash-command") {
|
|
1075
|
-
return (
|
|
1076
|
-
<div
|
|
1077
|
-
className="mt-4 flex justify-end"
|
|
1078
|
-
data-message-id={message.id}
|
|
1079
|
-
data-message-kind="slash-command"
|
|
1080
|
-
>
|
|
1081
|
-
<span className="font-mono text-sm text-foreground">{text}</span>
|
|
1082
|
-
</div>
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
if (displayKind === "local-command-output") {
|
|
1086
|
-
return (
|
|
1087
|
-
<div
|
|
1088
|
-
className="mt-4 flex justify-start"
|
|
1089
|
-
data-message-id={message.id}
|
|
1090
|
-
data-message-kind="local-command-output"
|
|
1091
|
-
>
|
|
1092
|
-
<span className="text-sm italic text-muted-foreground">{text}</span>
|
|
1093
|
-
</div>
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1096
|
-
return (
|
|
1097
|
-
<div
|
|
1098
|
-
className="my-3 text-sm italic text-muted-foreground"
|
|
1099
|
-
data-message-id={message.id}
|
|
1100
|
-
data-message-kind="notice"
|
|
1101
|
-
>
|
|
1102
|
-
{text}
|
|
1103
|
-
</div>
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function RenderAssistantPart({
|
|
1108
|
-
partRef,
|
|
1109
|
-
isActive,
|
|
1110
|
-
onApprove,
|
|
1111
|
-
onSuggestion,
|
|
1112
|
-
onAnswer,
|
|
1113
|
-
replyText,
|
|
1114
|
-
getToolApproval,
|
|
1115
|
-
resolveUrl,
|
|
1116
|
-
}: {
|
|
1117
|
-
partRef: CamelPartRef;
|
|
1118
|
-
isActive: boolean;
|
|
1119
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
1120
|
-
onSuggestion: (text: string) => void;
|
|
1121
|
-
onAnswer: (payload: unknown) => void;
|
|
1122
|
-
replyText?: string;
|
|
1123
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
1124
|
-
resolveUrl?: MessageUrlResolver;
|
|
1125
|
-
}) {
|
|
1126
|
-
return (
|
|
1127
|
-
<AssistantPart
|
|
1128
|
-
key={`${partRef.messageId}:${partRef.partIndex}:${String(
|
|
1129
|
-
recordOf(partRef.part).type ?? "part",
|
|
1130
|
-
)}`}
|
|
1131
|
-
part={partRef.part}
|
|
1132
|
-
isActive={isActive}
|
|
1133
|
-
onApprove={onApprove}
|
|
1134
|
-
onSuggestion={onSuggestion}
|
|
1135
|
-
onAnswer={onAnswer}
|
|
1136
|
-
replyText={replyText}
|
|
1137
|
-
getToolApproval={getToolApproval}
|
|
1138
|
-
resolveUrl={resolveUrl}
|
|
1139
|
-
/>
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function AssistantTurn({
|
|
1144
|
-
turn,
|
|
1145
|
-
isActive,
|
|
1146
|
-
hasPendingSteer,
|
|
1147
|
-
hasPendingApproval,
|
|
1148
|
-
onFork,
|
|
1149
|
-
onApprove,
|
|
1150
|
-
onSuggestion,
|
|
1151
|
-
onAnswer,
|
|
1152
|
-
getToolApproval,
|
|
1153
|
-
resolveUrl,
|
|
1154
|
-
}: {
|
|
1155
|
-
turn: CamelAssistantTurn;
|
|
1156
|
-
isActive: boolean;
|
|
1157
|
-
hasPendingSteer: boolean;
|
|
1158
|
-
hasPendingApproval: boolean;
|
|
1159
|
-
onFork?: (messageId: string) => void | Promise<void>;
|
|
1160
|
-
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
1161
|
-
onSuggestion: (text: string) => void;
|
|
1162
|
-
onAnswer: (payload: unknown) => void;
|
|
1163
|
-
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
1164
|
-
resolveUrl?: MessageUrlResolver;
|
|
1165
|
-
}) {
|
|
1166
|
-
const renderPart = (partRef: CamelPartRef, active = false) => (
|
|
1167
|
-
<RenderAssistantPart
|
|
1168
|
-
key={`${partRef.messageId}:${partRef.partIndex}`}
|
|
1169
|
-
partRef={partRef}
|
|
1170
|
-
isActive={active}
|
|
1171
|
-
onApprove={onApprove}
|
|
1172
|
-
onSuggestion={onSuggestion}
|
|
1173
|
-
onAnswer={onAnswer}
|
|
1174
|
-
replyText={turn.replyText}
|
|
1175
|
-
getToolApproval={getToolApproval}
|
|
1176
|
-
resolveUrl={resolveUrl}
|
|
1177
|
-
/>
|
|
1178
|
-
);
|
|
1179
|
-
const showWorkInPlace =
|
|
1180
|
-
isActive || hasPendingApproval || turn.terminalState !== undefined;
|
|
1181
|
-
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1182
|
-
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1183
|
-
const messageTime =
|
|
1184
|
-
completedAt === undefined ? null : formatCamelMessageTime(completedAt);
|
|
1185
|
-
const hasActions =
|
|
1186
|
-
!showWorkInPlace && Boolean(messageTime || turn.copyText || onFork);
|
|
1187
|
-
const thinkingActivity = deriveThinkingActivity(
|
|
1188
|
-
turn.traceParts.at(-1)?.part ?? turn.finalParts.at(-1)?.part,
|
|
1189
|
-
);
|
|
1190
|
-
const activeActivity = hasPendingSteer
|
|
1191
|
-
? pendingSteerActivity
|
|
1192
|
-
: thinkingActivity;
|
|
1193
|
-
|
|
1194
|
-
return (
|
|
1195
|
-
<div
|
|
1196
|
-
className="group/turn"
|
|
1197
|
-
data-message-id={turn.actionMessageId}
|
|
1198
|
-
data-message-role="assistant"
|
|
1199
|
-
data-turn-step-count={turn.stepCount}
|
|
1200
|
-
translate={showWorkInPlace ? "no" : undefined}
|
|
1201
|
-
>
|
|
1202
|
-
<div className="flex flex-col gap-1">
|
|
1203
|
-
{showSummary && (
|
|
1204
|
-
<TurnSummaryBar
|
|
1205
|
-
durationMs={turn.durationMs}
|
|
1206
|
-
stepCount={turn.stepCount}
|
|
1207
|
-
showSeparator={
|
|
1208
|
-
turn.finalParts.length > 0 || turn.terminalState !== undefined
|
|
1209
|
-
}
|
|
1210
|
-
>
|
|
1211
|
-
{turn.traceParts.map((partRef) => renderPart(partRef))}
|
|
1212
|
-
</TurnSummaryBar>
|
|
1213
|
-
)}
|
|
1214
|
-
{showWorkInPlace && turn.traceParts.length > 0 && (
|
|
1215
|
-
<div className="max-w-none space-y-1" data-camel-turn-trace="">
|
|
1216
|
-
{turn.traceParts.map((partRef) => renderPart(partRef, isActive))}
|
|
1217
|
-
</div>
|
|
1218
|
-
)}
|
|
1219
|
-
{(turn.finalParts.length > 0 ||
|
|
1220
|
-
turn.terminalState?.kind === "error") && (
|
|
1221
|
-
<div className="max-w-none space-y-4" data-camel-final-answer="">
|
|
1222
|
-
{turn.finalParts.map((partRef) => renderPart(partRef, isActive))}
|
|
1223
|
-
{turn.terminalState?.kind === "error" && (
|
|
1224
|
-
<div
|
|
1225
|
-
className="flex items-start gap-2 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
1226
|
-
data-part-type="error"
|
|
1227
|
-
role="alert"
|
|
1228
|
-
>
|
|
1229
|
-
<TriangleAlertIcon className="mt-0.5 size-4 shrink-0" />
|
|
1230
|
-
<div>
|
|
1231
|
-
<div className="font-medium">
|
|
1232
|
-
{turn.terminalState.title}
|
|
1233
|
-
</div>
|
|
1234
|
-
{turn.terminalState.body && (
|
|
1235
|
-
<div className="mt-0.5 text-xs opacity-80">
|
|
1236
|
-
{turn.terminalState.body}
|
|
1237
|
-
</div>
|
|
1238
|
-
)}
|
|
1239
|
-
</div>
|
|
1240
|
-
</div>
|
|
1241
|
-
)}
|
|
1242
|
-
</div>
|
|
1243
|
-
)}
|
|
1244
|
-
{turn.terminalState?.kind === "interrupted" && (
|
|
1245
|
-
<div
|
|
1246
|
-
className="px-1 py-1 text-xs text-muted-foreground/60"
|
|
1247
|
-
data-camel-turn-status="interrupted"
|
|
1248
|
-
role="status"
|
|
1249
|
-
>
|
|
1250
|
-
{turn.terminalState.title}
|
|
1251
|
-
</div>
|
|
1252
|
-
)}
|
|
1253
|
-
{hasActions && (
|
|
1254
|
-
<div
|
|
1255
|
-
className="flex items-center gap-0.5 opacity-0 transition-opacity group-hover/turn:opacity-100 group-focus-within/turn:opacity-100 pointer-coarse:opacity-100"
|
|
1256
|
-
role="group"
|
|
1257
|
-
aria-label="Message actions"
|
|
1258
|
-
>
|
|
1259
|
-
{messageTime && (
|
|
1260
|
-
<span className="mr-1 text-xs text-muted-foreground">
|
|
1261
|
-
{messageTime}
|
|
1262
|
-
</span>
|
|
1263
|
-
)}
|
|
1264
|
-
{onFork && (
|
|
1265
|
-
<ForkAction messageId={turn.actionMessageId} onFork={onFork} />
|
|
1266
|
-
)}
|
|
1267
|
-
{turn.copyText && <ClipboardAction text={turn.copyText} />}
|
|
1268
|
-
</div>
|
|
1269
|
-
)}
|
|
1270
|
-
{isActive && turn.finalParts.length === 0 && (
|
|
1271
|
-
<ThinkingIndicator {...activeActivity} />
|
|
1272
|
-
)}
|
|
1273
|
-
</div>
|
|
1274
|
-
</div>
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
export function CamelChatMessages({
|
|
1279
|
-
messages,
|
|
1280
|
-
status,
|
|
1281
|
-
finalToolNames,
|
|
1282
|
-
isHydrating = false,
|
|
1283
|
-
isRecovering = false,
|
|
1284
|
-
isServerStreaming = false,
|
|
1285
|
-
hasPendingSteer = false,
|
|
1286
|
-
startedWithPending = false,
|
|
1287
|
-
error,
|
|
1288
|
-
onRetry,
|
|
1289
|
-
onApprove,
|
|
1290
|
-
onSuggestion,
|
|
1291
|
-
onAnswer,
|
|
1292
|
-
onFork,
|
|
1293
|
-
getToolApproval,
|
|
1294
|
-
resolveUrl,
|
|
1295
|
-
inlineApprovalActions = true,
|
|
1296
|
-
}: CamelChatMessagesProps) {
|
|
1297
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
1298
|
-
const endRef = useRef<HTMLDivElement>(null);
|
|
1299
|
-
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
1300
|
-
const isActive = status === "submitted" || status === "streaming";
|
|
1301
|
-
const items = useMemo(
|
|
1302
|
-
() => projectCamelMessages(messages, finalToolNames),
|
|
1303
|
-
[messages, finalToolNames],
|
|
1304
|
-
);
|
|
1305
|
-
const presentedToolApproval = (part: unknown) => {
|
|
1306
|
-
const approval = getToolApproval?.(part) ?? toolApprovalOf(part);
|
|
1307
|
-
return !inlineApprovalActions && approval?.approved === undefined && approval
|
|
1308
|
-
? { ...approval, approved: false }
|
|
1309
|
-
: approval;
|
|
1310
|
-
};
|
|
1311
|
-
const lastItem = items.at(-1);
|
|
1312
|
-
const activeAssistantMessageId =
|
|
1313
|
-
isActive && lastItem?.kind === "assistant"
|
|
1314
|
-
? lastItem.actionMessageId
|
|
1315
|
-
: null;
|
|
1316
|
-
|
|
1317
|
-
const syncScrollButton = useCallback(() => {
|
|
1318
|
-
const element = scrollRef.current;
|
|
1319
|
-
if (!element) return;
|
|
1320
|
-
const distance =
|
|
1321
|
-
element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
1322
|
-
setShowScrollButton(distance > 120);
|
|
1323
|
-
}, []);
|
|
1324
|
-
|
|
1325
|
-
useEffect(() => {
|
|
1326
|
-
const element = scrollRef.current;
|
|
1327
|
-
if (!element || showScrollButton) return;
|
|
1328
|
-
element.scrollTo({ top: element.scrollHeight });
|
|
1329
|
-
}, [messages, status, showScrollButton]);
|
|
1330
|
-
|
|
1331
|
-
return (
|
|
1332
|
-
<div
|
|
1333
|
-
className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
|
|
1334
|
-
data-sb-message-panel=""
|
|
1335
|
-
data-camel-chat-transcript=""
|
|
1336
|
-
data-status={status}
|
|
1337
|
-
aria-busy={isActive || isHydrating}
|
|
1338
|
-
>
|
|
1339
|
-
{(isRecovering || isServerStreaming) && (
|
|
1340
|
-
<div
|
|
1341
|
-
className="absolute inset-x-0 top-0 z-10 flex h-7 items-center justify-center gap-2 bg-background/85 text-xs text-muted-foreground backdrop-blur-sm"
|
|
1342
|
-
role="status"
|
|
1343
|
-
>
|
|
1344
|
-
<span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
1345
|
-
{isRecovering ? "Recovering…" : "Running…"}
|
|
1346
|
-
</div>
|
|
1347
|
-
)}
|
|
1348
|
-
<div
|
|
1349
|
-
ref={scrollRef}
|
|
1350
|
-
onScroll={syncScrollButton}
|
|
1351
|
-
tabIndex={0}
|
|
1352
|
-
role="region"
|
|
1353
|
-
aria-label="Chat messages"
|
|
1354
|
-
className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden"
|
|
1355
|
-
>
|
|
1356
|
-
<div className="mx-auto flex w-full max-w-3xl flex-col px-4 pb-6 pt-2 md:px-6">
|
|
1357
|
-
{messages.length === 0 && !startedWithPending && !isHydrating ? (
|
|
1358
|
-
<div className="flex min-h-56 items-center justify-center text-center font-[family-name:var(--font-display)] text-4xl text-foreground">
|
|
1359
|
-
How can I help?
|
|
1360
|
-
</div>
|
|
1361
|
-
) : null}
|
|
1362
|
-
{items.map((item) =>
|
|
1363
|
-
item.kind === "assistant" ? (
|
|
1364
|
-
<AssistantTurn
|
|
1365
|
-
key={item.key}
|
|
1366
|
-
turn={item}
|
|
1367
|
-
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1368
|
-
hasPendingSteer={hasPendingSteer}
|
|
1369
|
-
hasPendingApproval={item.parts.some((partRef) => {
|
|
1370
|
-
const approval =
|
|
1371
|
-
getToolApproval?.(partRef.part) ??
|
|
1372
|
-
toolApprovalOf(partRef.part);
|
|
1373
|
-
return approval != null && approval.approved === undefined;
|
|
1374
|
-
})}
|
|
1375
|
-
onFork={onFork}
|
|
1376
|
-
onApprove={onApprove}
|
|
1377
|
-
onSuggestion={onSuggestion}
|
|
1378
|
-
onAnswer={onAnswer}
|
|
1379
|
-
getToolApproval={presentedToolApproval}
|
|
1380
|
-
resolveUrl={resolveUrl}
|
|
1381
|
-
/>
|
|
1382
|
-
) : (
|
|
1383
|
-
<StandaloneMessage key={item.key} item={item} />
|
|
1384
|
-
),
|
|
1385
|
-
)}
|
|
1386
|
-
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1387
|
-
{isActive && activeAssistantMessageId === null && (
|
|
1388
|
-
<ThinkingIndicator
|
|
1389
|
-
{...(hasPendingSteer ? pendingSteerActivity : {})}
|
|
1390
|
-
/>
|
|
1391
|
-
)}
|
|
1392
|
-
<div ref={endRef} />
|
|
1393
|
-
</div>
|
|
1394
|
-
</div>
|
|
1395
|
-
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t from-background to-transparent" />
|
|
1396
|
-
<Button
|
|
1397
|
-
type="button"
|
|
1398
|
-
variant="outline"
|
|
1399
|
-
size="icon"
|
|
1400
|
-
className={cn(
|
|
1401
|
-
"absolute bottom-3 left-1/2 z-20 -translate-x-1/2 rounded-full border-border/50 bg-background/80 shadow-md backdrop-blur-sm transition-all duration-200",
|
|
1402
|
-
showScrollButton
|
|
1403
|
-
? "translate-y-0 opacity-100"
|
|
1404
|
-
: "pointer-events-none translate-y-2 opacity-0",
|
|
1405
|
-
)}
|
|
1406
|
-
onClick={() => endRef.current?.scrollIntoView({ behavior: "smooth" })}
|
|
1407
|
-
aria-label="Scroll to bottom"
|
|
1408
|
-
>
|
|
1409
|
-
<ArrowDownIcon className="size-4" />
|
|
1410
|
-
</Button>
|
|
1411
|
-
{isHydrating && (
|
|
1412
|
-
<div className="absolute inset-0 z-30 grid place-items-center bg-background/90">
|
|
1413
|
-
<span className="text-sm text-muted-foreground">
|
|
1414
|
-
Loading conversation…
|
|
1415
|
-
</span>
|
|
1416
|
-
</div>
|
|
1417
|
-
)}
|
|
1418
|
-
</div>
|
|
1419
|
-
);
|
|
1420
|
-
}
|