@springbrand/message-panel 0.1.2 → 0.1.3-alpha.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/demo/camel-chat-showcase.tsx +1265 -0
- package/demo/fixtures.ts +292 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +6 -16
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1423 -0
- package/src/camel/camel-model-select.tsx +88 -0
- package/src/camel/camel-plan-bar.tsx +115 -0
- package/src/camel/camel-prompt-input.tsx +547 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +316 -0
- package/src/camel/turn-summary-bar.tsx +81 -0
- package/src/chat-summary-panel.tsx +229 -0
- package/src/composer/chat-composer.tsx +216 -0
- package/src/composer/composer-attachments.tsx +138 -0
- package/src/composer/composer-trigger-popover.tsx +236 -0
- package/src/composer/composer.tsx +425 -0
- package/src/composer/drop-controller.ts +80 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +138 -0
- package/src/conversation.tsx +76 -0
- package/src/internal/cn.ts +3 -0
- package/src/message-panel.tsx +368 -0
- package/src/message.tsx +254 -0
- package/src/parts/index.tsx +1149 -0
- package/src/parts/kind.ts +67 -0
- package/src/parts/plan.ts +63 -0
- package/src/parts/primitives.tsx +439 -0
- package/src/registry.ts +44 -0
- package/src/status.tsx +132 -0
- package/dist/demo/camel-chat-showcase.d.ts +0 -1
- package/dist/demo/camel-chat-showcase.js +0 -1007
- package/dist/demo/fixtures.d.ts +0 -32
- package/dist/demo/fixtures.js +0 -250
- package/dist/demo/index.js +0 -4
- package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
- package/dist/demo/message-panel-chat-showcase.js +0 -114
- package/dist/demo/message-panel-gallery.d.ts +0 -1
- package/dist/demo/message-panel-gallery.js +0 -130
- package/dist/src/adapters/ai-sdk.d.ts +0 -4
- package/dist/src/adapters/ai-sdk.js +0 -25
- package/dist/src/camel/camel-chat-messages.d.ts +0 -44
- package/dist/src/camel/camel-chat-messages.js +0 -421
- package/dist/src/camel/camel-model-select.d.ts +0 -13
- package/dist/src/camel/camel-model-select.js +0 -7
- package/dist/src/camel/camel-plan-bar.d.ts +0 -5
- package/dist/src/camel/camel-plan-bar.js +0 -24
- package/dist/src/camel/camel-prompt-input.d.ts +0 -32
- package/dist/src/camel/camel-prompt-input.js +0 -182
- package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
- package/dist/src/camel/camel-tool-presentation.js +0 -784
- package/dist/src/camel/camel-turn.d.ts +0 -57
- package/dist/src/camel/camel-turn.js +0 -203
- package/dist/src/camel/index.js +0 -7
- package/dist/src/camel/turn-summary-bar.d.ts +0 -8
- package/dist/src/camel/turn-summary-bar.js +0 -12
- package/dist/src/chat-summary-panel.d.ts +0 -22
- package/dist/src/chat-summary-panel.js +0 -26
- package/dist/src/composer/chat-composer.d.ts +0 -4
- package/dist/src/composer/chat-composer.js +0 -90
- package/dist/src/composer/composer-attachments.d.ts +0 -13
- package/dist/src/composer/composer-attachments.js +0 -47
- package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
- package/dist/src/composer/composer-trigger-popover.js +0 -29
- package/dist/src/composer/composer.d.ts +0 -15
- package/dist/src/composer/composer.js +0 -180
- package/dist/src/composer/drop-controller.d.ts +0 -8
- package/dist/src/composer/drop-controller.js +0 -69
- package/dist/src/composer/index.js +0 -6
- package/dist/src/composer/types.d.ts +0 -62
- package/dist/src/composer/types.js +0 -1
- package/dist/src/contracts.d.ts +0 -94
- package/dist/src/contracts.js +0 -1
- package/dist/src/conversation.d.ts +0 -13
- package/dist/src/conversation.js +0 -18
- package/dist/src/index.js +0 -12
- package/dist/src/internal/cn.d.ts +0 -1
- package/dist/src/internal/cn.js +0 -3
- package/dist/src/message-panel.d.ts +0 -4
- package/dist/src/message-panel.js +0 -144
- package/dist/src/message.d.ts +0 -36
- package/dist/src/message.js +0 -83
- package/dist/src/parts/index.d.ts +0 -187
- package/dist/src/parts/index.js +0 -332
- package/dist/src/parts/kind.d.ts +0 -3
- package/dist/src/parts/kind.js +0 -69
- package/dist/src/parts/plan.d.ts +0 -10
- package/dist/src/parts/plan.js +0 -40
- package/dist/src/parts/primitives.d.ts +0 -70
- package/dist/src/parts/primitives.js +0 -153
- package/dist/src/registry.d.ts +0 -4
- package/dist/src/registry.js +0 -31
- package/dist/src/status.d.ts +0 -26
- package/dist/src/status.js +0 -74
- /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
- /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
- /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
- /package/{dist/src/index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RichPartKind } from "../contracts";
|
|
2
|
+
|
|
3
|
+
function objectOf(value: unknown): Record<string, unknown> {
|
|
4
|
+
return value != null && typeof value === "object"
|
|
5
|
+
? (value as Record<string, unknown>)
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toolNameOfPart(part: unknown): string | null {
|
|
10
|
+
const record = objectOf(part);
|
|
11
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
12
|
+
if (type === "toolCall") {
|
|
13
|
+
return typeof record.name === "string" ? record.name : "tool";
|
|
14
|
+
}
|
|
15
|
+
if (type === "dynamic-tool") {
|
|
16
|
+
return typeof record.toolName === "string" ? record.toolName : "dynamic";
|
|
17
|
+
}
|
|
18
|
+
return type.startsWith("tool-") ? type.slice(5) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolvePartKind(part: unknown): RichPartKind | null {
|
|
22
|
+
const record = objectOf(part);
|
|
23
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
24
|
+
const toolName = toolNameOfPart(part);
|
|
25
|
+
if (type === "thinking") return "reasoning";
|
|
26
|
+
if (type === "tool") return "tool";
|
|
27
|
+
if (toolName === "update_plan") return "plan";
|
|
28
|
+
if (toolName === "ask_user") return "ask_user";
|
|
29
|
+
if (toolName === "suggest_followups") return "suggestions";
|
|
30
|
+
if (toolName === "schedule") return "schedule";
|
|
31
|
+
if (toolName === "parallel") return "parallel";
|
|
32
|
+
if (
|
|
33
|
+
toolName === "subagents" ||
|
|
34
|
+
toolName === "run_agent" ||
|
|
35
|
+
toolName === "run_temporary_agent" ||
|
|
36
|
+
toolName === "fanout" ||
|
|
37
|
+
toolName === "extract" ||
|
|
38
|
+
toolName === "dispatch_background"
|
|
39
|
+
) {
|
|
40
|
+
return "subagents";
|
|
41
|
+
}
|
|
42
|
+
if (toolName) return "tool";
|
|
43
|
+
const normalized = type.startsWith("data-")
|
|
44
|
+
? type.slice(5).replaceAll("-", "_")
|
|
45
|
+
: type;
|
|
46
|
+
return [
|
|
47
|
+
"text",
|
|
48
|
+
"reasoning",
|
|
49
|
+
"error",
|
|
50
|
+
"image",
|
|
51
|
+
"chart",
|
|
52
|
+
"agent_card",
|
|
53
|
+
"plan",
|
|
54
|
+
"schedule",
|
|
55
|
+
"parallel",
|
|
56
|
+
"subagents",
|
|
57
|
+
"confirmation",
|
|
58
|
+
"file",
|
|
59
|
+
"dashboard",
|
|
60
|
+
"ask_user",
|
|
61
|
+
"suggestions",
|
|
62
|
+
].includes(normalized)
|
|
63
|
+
? (normalized as RichPartKind)
|
|
64
|
+
: type === "reasoning-file"
|
|
65
|
+
? "file"
|
|
66
|
+
: null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolvePartKind } from "./kind";
|
|
2
|
+
|
|
3
|
+
type UnknownRecord = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export type PlanStepStatus = "pending" | "in_progress" | "done";
|
|
6
|
+
|
|
7
|
+
export interface PlanStep {
|
|
8
|
+
text: string;
|
|
9
|
+
status?: PlanStepStatus;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function recordOf(value: unknown): UnknownRecord {
|
|
13
|
+
return value != null && typeof value === "object"
|
|
14
|
+
? (value as UnknownRecord)
|
|
15
|
+
: {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function planStepsOfPart(part: unknown): PlanStep[] {
|
|
19
|
+
const record = recordOf(part);
|
|
20
|
+
const data = recordOf(
|
|
21
|
+
record.data ?? record.input ?? record.arguments ?? record,
|
|
22
|
+
);
|
|
23
|
+
if (!Array.isArray(data.steps)) return [];
|
|
24
|
+
|
|
25
|
+
return data.steps.flatMap((step) => {
|
|
26
|
+
const value = recordOf(step);
|
|
27
|
+
const text = typeof value.text === "string" ? value.text.trim() : "";
|
|
28
|
+
if (!text) return [];
|
|
29
|
+
const status =
|
|
30
|
+
value.status === "in_progress" || value.status === "done"
|
|
31
|
+
? value.status
|
|
32
|
+
: "pending";
|
|
33
|
+
return [{ text, status }];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function projectLatestPlan(
|
|
38
|
+
messages: readonly {
|
|
39
|
+
role: string;
|
|
40
|
+
parts: readonly unknown[];
|
|
41
|
+
}[],
|
|
42
|
+
): PlanStep[] | null {
|
|
43
|
+
for (
|
|
44
|
+
let messageIndex = messages.length - 1;
|
|
45
|
+
messageIndex >= 0;
|
|
46
|
+
messageIndex -= 1
|
|
47
|
+
) {
|
|
48
|
+
const message = messages[messageIndex];
|
|
49
|
+
if (message.role !== "assistant") continue;
|
|
50
|
+
for (
|
|
51
|
+
let partIndex = message.parts.length - 1;
|
|
52
|
+
partIndex >= 0;
|
|
53
|
+
partIndex -= 1
|
|
54
|
+
) {
|
|
55
|
+
const part = message.parts[partIndex];
|
|
56
|
+
if (resolvePartKind(part) !== "plan") continue;
|
|
57
|
+
const steps = planStepsOfPart(part);
|
|
58
|
+
if (steps.length > 0) return steps;
|
|
59
|
+
if (recordOf(part).state !== "input-streaming") return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BookOpenIcon,
|
|
3
|
+
BrainIcon,
|
|
4
|
+
CheckCircle2Icon,
|
|
5
|
+
ChevronDownIcon,
|
|
6
|
+
ChevronRightIcon,
|
|
7
|
+
CircleIcon,
|
|
8
|
+
ClockIcon,
|
|
9
|
+
WrenchIcon,
|
|
10
|
+
XCircleIcon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import {
|
|
13
|
+
createContext,
|
|
14
|
+
forwardRef,
|
|
15
|
+
useContext,
|
|
16
|
+
useMemo,
|
|
17
|
+
useState,
|
|
18
|
+
type ButtonHTMLAttributes,
|
|
19
|
+
type DetailsHTMLAttributes,
|
|
20
|
+
type HTMLAttributes,
|
|
21
|
+
type ReactNode,
|
|
22
|
+
} from "react";
|
|
23
|
+
import type { AIToolPart } from "../contracts";
|
|
24
|
+
import { cn } from "../internal/cn";
|
|
25
|
+
import { MessageResponse } from "../message";
|
|
26
|
+
|
|
27
|
+
interface ReasoningContextValue {
|
|
28
|
+
open: boolean;
|
|
29
|
+
toggle: () => void;
|
|
30
|
+
isStreaming: boolean;
|
|
31
|
+
duration?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
|
35
|
+
|
|
36
|
+
export interface ReasoningProps extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
isStreaming?: boolean;
|
|
38
|
+
open?: boolean;
|
|
39
|
+
defaultOpen?: boolean;
|
|
40
|
+
onOpenChange?: (open: boolean) => void;
|
|
41
|
+
duration?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Reasoning({
|
|
45
|
+
isStreaming = false,
|
|
46
|
+
open,
|
|
47
|
+
defaultOpen,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
duration,
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: ReasoningProps) {
|
|
54
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? isStreaming);
|
|
55
|
+
const resolvedOpen = open ?? internalOpen;
|
|
56
|
+
const toggle = () => {
|
|
57
|
+
const next = !resolvedOpen;
|
|
58
|
+
if (open === undefined) setInternalOpen(next);
|
|
59
|
+
onOpenChange?.(next);
|
|
60
|
+
};
|
|
61
|
+
const value = useMemo(
|
|
62
|
+
() => ({ open: resolvedOpen, toggle, isStreaming, duration }),
|
|
63
|
+
[resolvedOpen, isStreaming, duration],
|
|
64
|
+
);
|
|
65
|
+
return (
|
|
66
|
+
<ReasoningContext.Provider value={value}>
|
|
67
|
+
<div className={cn("sb-reasoning", className)} {...props}>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
</ReasoningContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function ReasoningTrigger({
|
|
75
|
+
children,
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
79
|
+
const context = useContext(ReasoningContext);
|
|
80
|
+
if (!context) throw new Error("ReasoningTrigger must be used within Reasoning");
|
|
81
|
+
const label = context.isStreaming
|
|
82
|
+
? "Thinking…"
|
|
83
|
+
: context.duration
|
|
84
|
+
? `Thought for ${context.duration}s`
|
|
85
|
+
: "Reasoning";
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={cn("sb-reasoning__trigger", className)}
|
|
90
|
+
onClick={context.toggle}
|
|
91
|
+
aria-expanded={context.open}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{children ?? (
|
|
95
|
+
<>
|
|
96
|
+
<BrainIcon size={16} />
|
|
97
|
+
<span>{label}</span>
|
|
98
|
+
<ChevronDownIcon size={16} aria-hidden="true" />
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
</button>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ReasoningContent({
|
|
106
|
+
children,
|
|
107
|
+
className,
|
|
108
|
+
...props
|
|
109
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, "children"> & { children: string }) {
|
|
110
|
+
const context = useContext(ReasoningContext);
|
|
111
|
+
if (!context) throw new Error("ReasoningContent must be used within Reasoning");
|
|
112
|
+
if (!context.open) return null;
|
|
113
|
+
return (
|
|
114
|
+
<div className={cn("sb-reasoning__content", className)} {...props}>
|
|
115
|
+
<MessageResponse streaming={context.isStreaming}>{children}</MessageResponse>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type ToolPart = AIToolPart;
|
|
121
|
+
export type ToolState = ToolPart["state"];
|
|
122
|
+
|
|
123
|
+
const toolStatus: Record<
|
|
124
|
+
ToolState,
|
|
125
|
+
{ label: string; icon: ReactNode; tone: string }
|
|
126
|
+
> = {
|
|
127
|
+
"approval-requested": {
|
|
128
|
+
label: "Awaiting approval",
|
|
129
|
+
icon: <ClockIcon size={14} />,
|
|
130
|
+
tone: "warning",
|
|
131
|
+
},
|
|
132
|
+
"approval-responded": {
|
|
133
|
+
label: "Responded",
|
|
134
|
+
icon: <CheckCircle2Icon size={14} />,
|
|
135
|
+
tone: "info",
|
|
136
|
+
},
|
|
137
|
+
"input-available": {
|
|
138
|
+
label: "Running",
|
|
139
|
+
icon: <ClockIcon size={14} />,
|
|
140
|
+
tone: "running",
|
|
141
|
+
},
|
|
142
|
+
"input-streaming": {
|
|
143
|
+
label: "Pending",
|
|
144
|
+
icon: <CircleIcon size={14} />,
|
|
145
|
+
tone: "running",
|
|
146
|
+
},
|
|
147
|
+
"output-available": {
|
|
148
|
+
label: "Completed",
|
|
149
|
+
icon: <CheckCircle2Icon size={14} />,
|
|
150
|
+
tone: "success",
|
|
151
|
+
},
|
|
152
|
+
"output-denied": {
|
|
153
|
+
label: "Denied",
|
|
154
|
+
icon: <XCircleIcon size={14} />,
|
|
155
|
+
tone: "warning",
|
|
156
|
+
},
|
|
157
|
+
"output-error": {
|
|
158
|
+
label: "Error",
|
|
159
|
+
icon: <XCircleIcon size={14} />,
|
|
160
|
+
tone: "error",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export function getStatusBadge(status: ToolState) {
|
|
165
|
+
const value = toolStatus[status];
|
|
166
|
+
return (
|
|
167
|
+
<span className="sb-tool-line__status" data-tone={value.tone}>
|
|
168
|
+
{value.icon}
|
|
169
|
+
{value.label}
|
|
170
|
+
</span>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const Tool = forwardRef<
|
|
175
|
+
HTMLDetailsElement,
|
|
176
|
+
DetailsHTMLAttributes<HTMLDetailsElement>
|
|
177
|
+
>(({ className, ...props }, ref) => (
|
|
178
|
+
<details ref={ref} className={cn("sb-tool-line", className)} {...props} />
|
|
179
|
+
));
|
|
180
|
+
Tool.displayName = "Tool";
|
|
181
|
+
|
|
182
|
+
type ToolHeaderProps = HTMLAttributes<HTMLElement> & {
|
|
183
|
+
title?: string;
|
|
184
|
+
type: ToolPart["type"];
|
|
185
|
+
state: ToolState;
|
|
186
|
+
toolName?: string;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export function ToolHeader({
|
|
190
|
+
className,
|
|
191
|
+
title,
|
|
192
|
+
type,
|
|
193
|
+
state,
|
|
194
|
+
toolName,
|
|
195
|
+
...props
|
|
196
|
+
}: ToolHeaderProps) {
|
|
197
|
+
const derived =
|
|
198
|
+
type === "dynamic-tool"
|
|
199
|
+
? toolName ?? "tool"
|
|
200
|
+
: type.split("-").slice(1).join("-");
|
|
201
|
+
return (
|
|
202
|
+
<summary className={cn("sb-tool-line__trigger", className)} {...props}>
|
|
203
|
+
<WrenchIcon size={16} />
|
|
204
|
+
<span>{title ?? derived}</span>
|
|
205
|
+
{state !== "output-available" && getStatusBadge(state)}
|
|
206
|
+
<ChevronRightIcon
|
|
207
|
+
size={16}
|
|
208
|
+
className="sb-tool-line__chevron"
|
|
209
|
+
aria-hidden="true"
|
|
210
|
+
/>
|
|
211
|
+
</summary>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const ToolContent = forwardRef<
|
|
216
|
+
HTMLDivElement,
|
|
217
|
+
HTMLAttributes<HTMLDivElement>
|
|
218
|
+
>(({ className, ...props }, ref) => (
|
|
219
|
+
<div ref={ref} className={cn("sb-tool-line__details", className)} {...props} />
|
|
220
|
+
));
|
|
221
|
+
ToolContent.displayName = "ToolContent";
|
|
222
|
+
|
|
223
|
+
export function ToolInput({
|
|
224
|
+
input,
|
|
225
|
+
className,
|
|
226
|
+
...props
|
|
227
|
+
}: HTMLAttributes<HTMLPreElement> & { input: unknown }) {
|
|
228
|
+
return (
|
|
229
|
+
<pre className={cn("sb-terminal", className)} {...props}>
|
|
230
|
+
{JSON.stringify(input, null, 2)}
|
|
231
|
+
</pre>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function ToolOutput({
|
|
236
|
+
output,
|
|
237
|
+
errorText,
|
|
238
|
+
className,
|
|
239
|
+
...props
|
|
240
|
+
}: HTMLAttributes<HTMLPreElement> & {
|
|
241
|
+
output: unknown;
|
|
242
|
+
errorText?: string;
|
|
243
|
+
}) {
|
|
244
|
+
if (output == null && !errorText) return null;
|
|
245
|
+
const content =
|
|
246
|
+
typeof output === "string"
|
|
247
|
+
? output
|
|
248
|
+
: output == null
|
|
249
|
+
? ""
|
|
250
|
+
: JSON.stringify(output, null, 2);
|
|
251
|
+
return (
|
|
252
|
+
<pre
|
|
253
|
+
className={cn("sb-terminal", className)}
|
|
254
|
+
data-error={errorText ? "true" : undefined}
|
|
255
|
+
{...props}
|
|
256
|
+
>
|
|
257
|
+
{errorText || content}
|
|
258
|
+
</pre>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface ConfirmationContextValue {
|
|
263
|
+
state: ToolState | "pending" | "confirmed" | "cancelled";
|
|
264
|
+
approval?: { id: string; approved?: boolean };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const ConfirmationContext = createContext<ConfirmationContextValue | null>(null);
|
|
268
|
+
|
|
269
|
+
export interface ConfirmationProps extends HTMLAttributes<HTMLDivElement> {
|
|
270
|
+
state: ToolState | "pending" | "confirmed" | "cancelled";
|
|
271
|
+
approval?: { id: string; approved?: boolean };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function Confirmation({
|
|
275
|
+
state,
|
|
276
|
+
approval,
|
|
277
|
+
className,
|
|
278
|
+
...props
|
|
279
|
+
}: ConfirmationProps) {
|
|
280
|
+
const value = useMemo(() => ({ state, approval }), [state, approval]);
|
|
281
|
+
return (
|
|
282
|
+
<ConfirmationContext.Provider value={value}>
|
|
283
|
+
<div className={cn("sb-confirmation", className)} {...props} />
|
|
284
|
+
</ConfirmationContext.Provider>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function useConfirmation() {
|
|
289
|
+
const context = useContext(ConfirmationContext);
|
|
290
|
+
if (!context) throw new Error("Confirmation child must be used within Confirmation");
|
|
291
|
+
return context;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const ConfirmationTitle = ({
|
|
295
|
+
className,
|
|
296
|
+
...props
|
|
297
|
+
}: HTMLAttributes<HTMLDivElement>) => (
|
|
298
|
+
<div className={cn("sb-confirmation__title", className)} {...props} />
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
export function ConfirmationRequest({ children }: { children?: ReactNode }) {
|
|
302
|
+
const { state } = useConfirmation();
|
|
303
|
+
return state === "approval-requested" || state === "pending" ? children : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function ConfirmationAccepted({ children }: { children?: ReactNode }) {
|
|
307
|
+
const { state, approval } = useConfirmation();
|
|
308
|
+
return state === "confirmed" || approval?.approved === true ? children : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function ConfirmationRejected({ children }: { children?: ReactNode }) {
|
|
312
|
+
const { state, approval } = useConfirmation();
|
|
313
|
+
return state === "cancelled" ||
|
|
314
|
+
state === "output-denied" ||
|
|
315
|
+
approval?.approved === false
|
|
316
|
+
? children
|
|
317
|
+
: null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function ConfirmationActions({
|
|
321
|
+
className,
|
|
322
|
+
...props
|
|
323
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
324
|
+
const { state } = useConfirmation();
|
|
325
|
+
if (state !== "approval-requested" && state !== "pending") return null;
|
|
326
|
+
return (
|
|
327
|
+
<div
|
|
328
|
+
className={cn("sb-confirmation__actions", className)}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export const ConfirmationAction = forwardRef<
|
|
335
|
+
HTMLButtonElement,
|
|
336
|
+
ButtonHTMLAttributes<HTMLButtonElement>
|
|
337
|
+
>(({ className, ...props }, ref) => (
|
|
338
|
+
<button
|
|
339
|
+
ref={ref}
|
|
340
|
+
type="button"
|
|
341
|
+
className={cn("sb-button", className)}
|
|
342
|
+
{...props}
|
|
343
|
+
/>
|
|
344
|
+
));
|
|
345
|
+
ConfirmationAction.displayName = "ConfirmationAction";
|
|
346
|
+
|
|
347
|
+
export const Plan = forwardRef<
|
|
348
|
+
HTMLDivElement,
|
|
349
|
+
HTMLAttributes<HTMLDivElement> & {
|
|
350
|
+
isStreaming?: boolean;
|
|
351
|
+
}
|
|
352
|
+
>(({ className, isStreaming, ...props }, ref) => (
|
|
353
|
+
<div
|
|
354
|
+
ref={ref}
|
|
355
|
+
className={cn("sb-plan", className)}
|
|
356
|
+
data-streaming={isStreaming ? "true" : undefined}
|
|
357
|
+
{...props}
|
|
358
|
+
/>
|
|
359
|
+
));
|
|
360
|
+
Plan.displayName = "Plan";
|
|
361
|
+
|
|
362
|
+
export const PlanHeader = ({
|
|
363
|
+
className,
|
|
364
|
+
...props
|
|
365
|
+
}: HTMLAttributes<HTMLDivElement>) => (
|
|
366
|
+
<div className={cn("sb-plan__header", className)} {...props} />
|
|
367
|
+
);
|
|
368
|
+
export const PlanTitle = ({
|
|
369
|
+
className,
|
|
370
|
+
...props
|
|
371
|
+
}: HTMLAttributes<HTMLHeadingElement>) => (
|
|
372
|
+
<h3 className={cn("sb-plan__title", className)} {...props} />
|
|
373
|
+
);
|
|
374
|
+
export const PlanDescription = ({
|
|
375
|
+
className,
|
|
376
|
+
...props
|
|
377
|
+
}: HTMLAttributes<HTMLParagraphElement>) => (
|
|
378
|
+
<p className={cn("sb-plan__description", className)} {...props} />
|
|
379
|
+
);
|
|
380
|
+
export const PlanContent = ({
|
|
381
|
+
className,
|
|
382
|
+
...props
|
|
383
|
+
}: HTMLAttributes<HTMLDivElement>) => (
|
|
384
|
+
<div className={cn("sb-plan__content", className)} {...props} />
|
|
385
|
+
);
|
|
386
|
+
export const PlanFooter = ({
|
|
387
|
+
className,
|
|
388
|
+
...props
|
|
389
|
+
}: HTMLAttributes<HTMLDivElement>) => (
|
|
390
|
+
<div className={cn("sb-plan__footer", className)} {...props} />
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
export const Sources = forwardRef<
|
|
394
|
+
HTMLDetailsElement,
|
|
395
|
+
DetailsHTMLAttributes<HTMLDetailsElement>
|
|
396
|
+
>(({ className, ...props }, ref) => (
|
|
397
|
+
<details ref={ref} className={cn("sb-sources", className)} {...props} />
|
|
398
|
+
));
|
|
399
|
+
Sources.displayName = "Sources";
|
|
400
|
+
|
|
401
|
+
export function SourcesTrigger({
|
|
402
|
+
count,
|
|
403
|
+
children,
|
|
404
|
+
className,
|
|
405
|
+
...props
|
|
406
|
+
}: HTMLAttributes<HTMLElement> & { count: number }) {
|
|
407
|
+
return (
|
|
408
|
+
<summary className={cn("sb-sources__trigger", className)} {...props}>
|
|
409
|
+
{children ?? (
|
|
410
|
+
<>
|
|
411
|
+
<span>来源 {count} 个</span>
|
|
412
|
+
<ChevronDownIcon size={15} />
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
415
|
+
</summary>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
export const SourcesContent = ({
|
|
419
|
+
className,
|
|
420
|
+
...props
|
|
421
|
+
}: HTMLAttributes<HTMLDivElement>) => (
|
|
422
|
+
<div className={cn("sb-sources__content", className)} {...props} />
|
|
423
|
+
);
|
|
424
|
+
export function Source({
|
|
425
|
+
title,
|
|
426
|
+
children,
|
|
427
|
+
...props
|
|
428
|
+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { title?: string }) {
|
|
429
|
+
return (
|
|
430
|
+
<a target="_blank" rel="noreferrer" {...props}>
|
|
431
|
+
{children ?? (
|
|
432
|
+
<>
|
|
433
|
+
<BookOpenIcon size={14} />
|
|
434
|
+
<span>{title}</span>
|
|
435
|
+
</>
|
|
436
|
+
)}
|
|
437
|
+
</a>
|
|
438
|
+
);
|
|
439
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PartRendererComponent,
|
|
3
|
+
PartRendererDefinition,
|
|
4
|
+
PartRendererRegistry,
|
|
5
|
+
} from "./contracts";
|
|
6
|
+
import { DefaultPartRenderer } from "./parts";
|
|
7
|
+
|
|
8
|
+
export function createPartRendererRegistry(
|
|
9
|
+
definitions: readonly PartRendererDefinition[],
|
|
10
|
+
): PartRendererRegistry {
|
|
11
|
+
const stable = [...definitions];
|
|
12
|
+
return {
|
|
13
|
+
names: stable.map((definition) => definition.name),
|
|
14
|
+
resolve(part: unknown): PartRendererComponent | undefined {
|
|
15
|
+
return stable.find((definition) => definition.match(part))?.component;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergePartRendererRegistries(
|
|
21
|
+
...registries: Array<PartRendererRegistry | undefined>
|
|
22
|
+
): PartRendererRegistry {
|
|
23
|
+
const resolved = registries.filter(
|
|
24
|
+
(registry): registry is PartRendererRegistry => registry !== undefined,
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
names: resolved.flatMap((registry) => registry.names),
|
|
28
|
+
resolve(part: unknown) {
|
|
29
|
+
for (const registry of resolved) {
|
|
30
|
+
const component = registry.resolve(part);
|
|
31
|
+
if (component) return component;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const defaultPartRendererRegistry = createPartRendererRegistry([
|
|
39
|
+
{
|
|
40
|
+
name: "default",
|
|
41
|
+
match: () => true,
|
|
42
|
+
component: DefaultPartRenderer,
|
|
43
|
+
},
|
|
44
|
+
]);
|