@springbrand/message-panel 0.1.3-alpha.0 → 0.1.3-alpha.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/cloud-os/README.md +71 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +606 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +640 -0
- package/cloud-os/chat/tool-presentation.ts +542 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +599 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +532 -0
- package/cloud-os/index.ts +103 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +202 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +559 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +470 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +50 -20
- package/src/camel/camel-turn.ts +21 -3
- package/src/message.tsx +0 -8
- package/src/parts/plan.ts +3 -2
|
@@ -71,9 +71,7 @@ export interface CamelChatMessagesProps {
|
|
|
71
71
|
finalToolNames?: readonly string[];
|
|
72
72
|
isHydrating?: boolean;
|
|
73
73
|
isRecovering?: boolean;
|
|
74
|
-
|
|
75
|
-
/** Show the opt-in activity bar for recovering or server-streaming states. */
|
|
76
|
-
showActivityStatus?: boolean;
|
|
74
|
+
recoveryStatusLabel?: string;
|
|
77
75
|
hasPendingSteer?: boolean;
|
|
78
76
|
startedWithPending?: boolean;
|
|
79
77
|
error?: unknown;
|
|
@@ -163,6 +161,23 @@ export function CamelChatError({
|
|
|
163
161
|
);
|
|
164
162
|
}
|
|
165
163
|
|
|
164
|
+
export function CamelChatRecovery({
|
|
165
|
+
label = "模型响应暂时中断,正在重试…",
|
|
166
|
+
}: {
|
|
167
|
+
label?: string;
|
|
168
|
+
}) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className="mt-4 flex items-center gap-2 rounded-lg border border-border/70 bg-muted/40 px-3 py-2 text-sm text-muted-foreground"
|
|
172
|
+
data-camel-chat-recovery=""
|
|
173
|
+
role="status"
|
|
174
|
+
>
|
|
175
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
|
|
176
|
+
<span>{label}</span>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
166
181
|
export function CamelApprovalPrompts({
|
|
167
182
|
approvals,
|
|
168
183
|
error,
|
|
@@ -403,16 +418,21 @@ function CamelToolRow({
|
|
|
403
418
|
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
404
419
|
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
405
420
|
}) {
|
|
406
|
-
const [open, setOpen] = useState(false);
|
|
407
421
|
const approval = getToolApproval?.(part) ?? toolApprovalOf(part);
|
|
408
422
|
const pendingApproval = approval && approval.approved === undefined;
|
|
409
423
|
const presentation = getCamelToolPresentation(part, isActive);
|
|
424
|
+
const autoOpen = presentation.running && (
|
|
425
|
+
presentation.isSubAgent ||
|
|
426
|
+
presentation.kind === "write" ||
|
|
427
|
+
presentation.kind === "edit"
|
|
428
|
+
);
|
|
429
|
+
const [open, setOpen] = useState(autoOpen);
|
|
410
430
|
const hasDetails = children != null || presentation.hasDetails;
|
|
411
431
|
const title = presentation.summary;
|
|
412
432
|
|
|
413
433
|
useEffect(() => {
|
|
414
|
-
if (
|
|
415
|
-
}, [
|
|
434
|
+
if (autoOpen) setOpen(true);
|
|
435
|
+
}, [autoOpen]);
|
|
416
436
|
|
|
417
437
|
return (
|
|
418
438
|
<div data-part-type={normalizedPartType(part)}>
|
|
@@ -1145,6 +1165,7 @@ function RenderAssistantPart({
|
|
|
1145
1165
|
function AssistantTurn({
|
|
1146
1166
|
turn,
|
|
1147
1167
|
isActive,
|
|
1168
|
+
keepTraceExpanded,
|
|
1148
1169
|
hasPendingSteer,
|
|
1149
1170
|
hasPendingApproval,
|
|
1150
1171
|
onFork,
|
|
@@ -1156,6 +1177,7 @@ function AssistantTurn({
|
|
|
1156
1177
|
}: {
|
|
1157
1178
|
turn: CamelAssistantTurn;
|
|
1158
1179
|
isActive: boolean;
|
|
1180
|
+
keepTraceExpanded: boolean;
|
|
1159
1181
|
hasPendingSteer: boolean;
|
|
1160
1182
|
hasPendingApproval: boolean;
|
|
1161
1183
|
onFork?: (messageId: string) => void | Promise<void>;
|
|
@@ -1179,7 +1201,10 @@ function AssistantTurn({
|
|
|
1179
1201
|
/>
|
|
1180
1202
|
);
|
|
1181
1203
|
const showWorkInPlace =
|
|
1182
|
-
isActive ||
|
|
1204
|
+
isActive ||
|
|
1205
|
+
keepTraceExpanded ||
|
|
1206
|
+
hasPendingApproval ||
|
|
1207
|
+
turn.terminalState !== undefined;
|
|
1183
1208
|
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1184
1209
|
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1185
1210
|
const messageTime =
|
|
@@ -1189,6 +1214,11 @@ function AssistantTurn({
|
|
|
1189
1214
|
const thinkingActivity = deriveThinkingActivity(
|
|
1190
1215
|
turn.traceParts.at(-1)?.part ?? turn.finalParts.at(-1)?.part,
|
|
1191
1216
|
);
|
|
1217
|
+
const hasRunningTool = turn.traceParts.some(
|
|
1218
|
+
({ part }) =>
|
|
1219
|
+
toolNameOfPart(part) !== null &&
|
|
1220
|
+
getCamelToolPresentation(part, isActive).running,
|
|
1221
|
+
);
|
|
1192
1222
|
const activeActivity = hasPendingSteer
|
|
1193
1223
|
? pendingSteerActivity
|
|
1194
1224
|
: thinkingActivity;
|
|
@@ -1269,7 +1299,7 @@ function AssistantTurn({
|
|
|
1269
1299
|
{turn.copyText && <ClipboardAction text={turn.copyText} />}
|
|
1270
1300
|
</div>
|
|
1271
1301
|
)}
|
|
1272
|
-
{isActive && turn.finalParts.length === 0 && (
|
|
1302
|
+
{isActive && turn.finalParts.length === 0 && !hasRunningTool && (
|
|
1273
1303
|
<ThinkingIndicator {...activeActivity} />
|
|
1274
1304
|
)}
|
|
1275
1305
|
</div>
|
|
@@ -1283,8 +1313,7 @@ export function CamelChatMessages({
|
|
|
1283
1313
|
finalToolNames,
|
|
1284
1314
|
isHydrating = false,
|
|
1285
1315
|
isRecovering = false,
|
|
1286
|
-
|
|
1287
|
-
showActivityStatus = false,
|
|
1316
|
+
recoveryStatusLabel,
|
|
1288
1317
|
hasPendingSteer = false,
|
|
1289
1318
|
startedWithPending = false,
|
|
1290
1319
|
error,
|
|
@@ -1316,6 +1345,10 @@ export function CamelChatMessages({
|
|
|
1316
1345
|
isActive && lastItem?.kind === "assistant"
|
|
1317
1346
|
? lastItem.actionMessageId
|
|
1318
1347
|
: null;
|
|
1348
|
+
const recoveringAssistantMessageId =
|
|
1349
|
+
isRecovering && lastItem?.kind === "assistant"
|
|
1350
|
+
? lastItem.actionMessageId
|
|
1351
|
+
: null;
|
|
1319
1352
|
|
|
1320
1353
|
const syncScrollButton = useCallback(() => {
|
|
1321
1354
|
const element = scrollRef.current;
|
|
@@ -1329,7 +1362,7 @@ export function CamelChatMessages({
|
|
|
1329
1362
|
const element = scrollRef.current;
|
|
1330
1363
|
if (!element || showScrollButton) return;
|
|
1331
1364
|
element.scrollTo({ top: element.scrollHeight });
|
|
1332
|
-
}, [messages, status, showScrollButton]);
|
|
1365
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
1333
1366
|
|
|
1334
1367
|
return (
|
|
1335
1368
|
<div
|
|
@@ -1339,15 +1372,6 @@ export function CamelChatMessages({
|
|
|
1339
1372
|
data-status={status}
|
|
1340
1373
|
aria-busy={isActive || isHydrating}
|
|
1341
1374
|
>
|
|
1342
|
-
{showActivityStatus && (isRecovering || isServerStreaming) && (
|
|
1343
|
-
<div
|
|
1344
|
-
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"
|
|
1345
|
-
role="status"
|
|
1346
|
-
>
|
|
1347
|
-
<span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
1348
|
-
{isRecovering ? "Recovering…" : "Running…"}
|
|
1349
|
-
</div>
|
|
1350
|
-
)}
|
|
1351
1375
|
<div
|
|
1352
1376
|
ref={scrollRef}
|
|
1353
1377
|
onScroll={syncScrollButton}
|
|
@@ -1368,6 +1392,9 @@ export function CamelChatMessages({
|
|
|
1368
1392
|
key={item.key}
|
|
1369
1393
|
turn={item}
|
|
1370
1394
|
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1395
|
+
keepTraceExpanded={
|
|
1396
|
+
item.actionMessageId === recoveringAssistantMessageId
|
|
1397
|
+
}
|
|
1371
1398
|
hasPendingSteer={hasPendingSteer}
|
|
1372
1399
|
hasPendingApproval={item.parts.some((partRef) => {
|
|
1373
1400
|
const approval =
|
|
@@ -1386,6 +1413,9 @@ export function CamelChatMessages({
|
|
|
1386
1413
|
<StandaloneMessage key={item.key} item={item} />
|
|
1387
1414
|
),
|
|
1388
1415
|
)}
|
|
1416
|
+
{isRecovering && (
|
|
1417
|
+
<CamelChatRecovery label={recoveryStatusLabel} />
|
|
1418
|
+
)}
|
|
1389
1419
|
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1390
1420
|
{isActive && activeAssistantMessageId === null && (
|
|
1391
1421
|
<ThinkingIndicator
|
package/src/camel/camel-turn.ts
CHANGED
|
@@ -169,11 +169,20 @@ function isTraceWork(
|
|
|
169
169
|
if (isVisibleReasoning(part)) return true;
|
|
170
170
|
const kind = resolvePartKind(part);
|
|
171
171
|
if (kind === "plan") return false;
|
|
172
|
-
|
|
173
|
-
if (toolName !== null && finalToolNames.includes(toolName)) return false;
|
|
172
|
+
if (isFinalOutputTool(part, finalToolNames)) return false;
|
|
174
173
|
return TRACE_PART_KINDS.has(kind ?? "");
|
|
175
174
|
}
|
|
176
175
|
|
|
176
|
+
function isFinalOutputTool(
|
|
177
|
+
part: CamelPart,
|
|
178
|
+
finalToolNames: readonly string[],
|
|
179
|
+
): boolean {
|
|
180
|
+
const toolName = toolNameOfPart(part);
|
|
181
|
+
return toolName !== null &&
|
|
182
|
+
(finalToolNames.includes(toolName) ||
|
|
183
|
+
resolvePartKind(part) === "suggestions");
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
function isTraceBoundary(
|
|
178
187
|
part: CamelPart,
|
|
179
188
|
finalToolNames: readonly string[],
|
|
@@ -226,8 +235,17 @@ export function projectCamelAssistantTurn(
|
|
|
226
235
|
partIndex,
|
|
227
236
|
part,
|
|
228
237
|
}));
|
|
229
|
-
let
|
|
238
|
+
let finalOutputBoundary = parts.length;
|
|
230
239
|
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
240
|
+
if (isFinalOutputTool(parts[index].part, finalToolNames)) {
|
|
241
|
+
finalOutputBoundary = index;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let lastTraceBoundary = -1;
|
|
246
|
+
// Provider cleanup steps after final output must not pull the answer back
|
|
247
|
+
// into the collapsible work trace.
|
|
248
|
+
for (let index = finalOutputBoundary - 1; index >= 0; index -= 1) {
|
|
231
249
|
if (isTraceBoundary(parts[index].part, finalToolNames)) {
|
|
232
250
|
lastTraceBoundary = index;
|
|
233
251
|
break;
|
package/src/message.tsx
CHANGED
|
@@ -85,13 +85,6 @@ export const MessageAction = forwardRef<HTMLButtonElement, MessageActionProps>(
|
|
|
85
85
|
MessageAction.displayName = "MessageAction";
|
|
86
86
|
|
|
87
87
|
const streamdownPlugins = { cjk, code, math, mermaid };
|
|
88
|
-
const streamAnimation = {
|
|
89
|
-
animation: "fadeIn",
|
|
90
|
-
sep: "word",
|
|
91
|
-
duration: 260,
|
|
92
|
-
stagger: 14,
|
|
93
|
-
} as const;
|
|
94
|
-
|
|
95
88
|
export type MessageResponseProps = StreamdownProps & {
|
|
96
89
|
streaming?: boolean;
|
|
97
90
|
resolveUrl?: MessageUrlResolver;
|
|
@@ -146,7 +139,6 @@ export const MessageResponse = memo(
|
|
|
146
139
|
? {
|
|
147
140
|
mode: "streaming" as const,
|
|
148
141
|
isAnimating: true,
|
|
149
|
-
animated: streamAnimation,
|
|
150
142
|
}
|
|
151
143
|
: { mode: "static" as const })}
|
|
152
144
|
{...props}
|
package/src/parts/plan.ts
CHANGED
|
@@ -26,8 +26,9 @@ export function planStepsOfPart(part: unknown): PlanStep[] {
|
|
|
26
26
|
const value = recordOf(step);
|
|
27
27
|
const text = typeof value.text === "string" ? value.text.trim() : "";
|
|
28
28
|
if (!text) return [];
|
|
29
|
-
const status =
|
|
30
|
-
|
|
29
|
+
const status = value.status === "completed"
|
|
30
|
+
? "done"
|
|
31
|
+
: value.status === "in_progress" || value.status === "done"
|
|
31
32
|
? value.status
|
|
32
33
|
: "pending";
|
|
33
34
|
return [{ text, status }];
|