@springbrand/message-panel 0.1.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/index.ts +4 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +44 -0
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1420 -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 +540 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +315 -0
- package/src/camel/index.ts +7 -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/index.ts +6 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +139 -0
- package/src/conversation.tsx +76 -0
- package/src/index.ts +12 -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/src/styles/camel-chat.css +36 -0
- package/src/styles/index.css +2186 -0
- package/src/styles/tokens.css +31 -0
package/src/message.tsx
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { cjk } from "@streamdown/cjk";
|
|
2
|
+
import { code } from "@streamdown/code";
|
|
3
|
+
import { math } from "@streamdown/math";
|
|
4
|
+
import { mermaid } from "@streamdown/mermaid";
|
|
5
|
+
import {
|
|
6
|
+
CheckIcon,
|
|
7
|
+
CopyIcon,
|
|
8
|
+
GitForkIcon,
|
|
9
|
+
Loader2Icon,
|
|
10
|
+
RefreshCcwIcon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import {
|
|
13
|
+
forwardRef,
|
|
14
|
+
memo,
|
|
15
|
+
useCallback,
|
|
16
|
+
useEffect,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
type ButtonHTMLAttributes,
|
|
20
|
+
type HTMLAttributes,
|
|
21
|
+
} from "react";
|
|
22
|
+
import {
|
|
23
|
+
defaultRehypePlugins,
|
|
24
|
+
Streamdown,
|
|
25
|
+
type StreamdownProps,
|
|
26
|
+
} from "streamdown";
|
|
27
|
+
import { cn } from "./internal/cn";
|
|
28
|
+
import type { MessageRole } from "./contracts";
|
|
29
|
+
|
|
30
|
+
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
|
31
|
+
from: MessageRole;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Message = forwardRef<HTMLDivElement, MessageProps>(
|
|
35
|
+
({ className, from, ...props }, ref) => (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"sb-message",
|
|
40
|
+
from === "user" ? "sb-message--user" : "sb-message--assistant",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
data-role={from}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
Message.displayName = "Message";
|
|
49
|
+
|
|
50
|
+
export const MessageContent = forwardRef<
|
|
51
|
+
HTMLDivElement,
|
|
52
|
+
HTMLAttributes<HTMLDivElement>
|
|
53
|
+
>(({ className, ...props }, ref) => (
|
|
54
|
+
<div ref={ref} className={cn("sb-message__content", className)} {...props} />
|
|
55
|
+
));
|
|
56
|
+
MessageContent.displayName = "MessageContent";
|
|
57
|
+
|
|
58
|
+
export const MessageActions = forwardRef<
|
|
59
|
+
HTMLDivElement,
|
|
60
|
+
HTMLAttributes<HTMLDivElement>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<div ref={ref} className={cn("sb-message__actions", className)} {...props} />
|
|
63
|
+
));
|
|
64
|
+
MessageActions.displayName = "MessageActions";
|
|
65
|
+
|
|
66
|
+
export type MessageActionProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
67
|
+
tooltip?: string;
|
|
68
|
+
label?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const MessageAction = forwardRef<HTMLButtonElement, MessageActionProps>(
|
|
72
|
+
({ className, tooltip, label, children, ...props }, ref) => (
|
|
73
|
+
<button
|
|
74
|
+
ref={ref}
|
|
75
|
+
type="button"
|
|
76
|
+
className={cn("sb-message__action", className)}
|
|
77
|
+
title={tooltip}
|
|
78
|
+
aria-label={label ?? tooltip}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</button>
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
MessageAction.displayName = "MessageAction";
|
|
86
|
+
|
|
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
|
+
export type MessageResponseProps = StreamdownProps & {
|
|
96
|
+
streaming?: boolean;
|
|
97
|
+
resolveUrl?: MessageUrlResolver;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type MessageUrlResolver = (url: string) => string | null | undefined;
|
|
101
|
+
|
|
102
|
+
type UrlNode = {
|
|
103
|
+
type?: string;
|
|
104
|
+
properties?: Record<string, unknown>;
|
|
105
|
+
children?: UrlNode[];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const streamdownRehypePlugins = Object.values(defaultRehypePlugins) as NonNullable<
|
|
109
|
+
StreamdownProps["rehypePlugins"]
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
function resolveUrlsPlugin(resolveUrl: MessageUrlResolver) {
|
|
113
|
+
return () => (tree: UrlNode) => {
|
|
114
|
+
const visit = (node: UrlNode): void => {
|
|
115
|
+
if (node.type === "element" && node.properties) {
|
|
116
|
+
const url = node.properties.href;
|
|
117
|
+
if (typeof url === "string") {
|
|
118
|
+
const resolved = resolveUrl(url);
|
|
119
|
+
if (resolved === null) delete node.properties.href;
|
|
120
|
+
else if (resolved !== undefined) node.properties.href = resolved;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
node.children?.forEach(visit);
|
|
124
|
+
};
|
|
125
|
+
visit(tree);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const MessageResponse = memo(
|
|
130
|
+
({
|
|
131
|
+
className,
|
|
132
|
+
streaming = false,
|
|
133
|
+
resolveUrl,
|
|
134
|
+
rehypePlugins,
|
|
135
|
+
...props
|
|
136
|
+
}: MessageResponseProps) => (
|
|
137
|
+
<Streamdown
|
|
138
|
+
className={cn("sb-message-response", className)}
|
|
139
|
+
plugins={streamdownPlugins}
|
|
140
|
+
rehypePlugins={
|
|
141
|
+
resolveUrl
|
|
142
|
+
? [resolveUrlsPlugin(resolveUrl), ...(rehypePlugins ?? streamdownRehypePlugins)]
|
|
143
|
+
: rehypePlugins
|
|
144
|
+
}
|
|
145
|
+
{...(streaming
|
|
146
|
+
? {
|
|
147
|
+
mode: "streaming" as const,
|
|
148
|
+
isAnimating: true,
|
|
149
|
+
animated: streamAnimation,
|
|
150
|
+
}
|
|
151
|
+
: { mode: "static" as const })}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
),
|
|
155
|
+
(previous, next) =>
|
|
156
|
+
previous.children === next.children &&
|
|
157
|
+
previous.streaming === next.streaming &&
|
|
158
|
+
previous.resolveUrl === next.resolveUrl,
|
|
159
|
+
);
|
|
160
|
+
MessageResponse.displayName = "MessageResponse";
|
|
161
|
+
|
|
162
|
+
export function AssistantMessageActions({
|
|
163
|
+
text,
|
|
164
|
+
messageId,
|
|
165
|
+
canRetry,
|
|
166
|
+
onCopy,
|
|
167
|
+
onRetry,
|
|
168
|
+
onFork,
|
|
169
|
+
}: {
|
|
170
|
+
text: string;
|
|
171
|
+
messageId: string;
|
|
172
|
+
canRetry: boolean;
|
|
173
|
+
onCopy?: (text: string) => void | Promise<void>;
|
|
174
|
+
onRetry?: (messageId: string) => void | Promise<void>;
|
|
175
|
+
onFork?: (messageId: string) => void | Promise<void>;
|
|
176
|
+
}) {
|
|
177
|
+
const [copied, setCopied] = useState(false);
|
|
178
|
+
const [forking, setForking] = useState(false);
|
|
179
|
+
const timer = useRef<number | null>(null);
|
|
180
|
+
|
|
181
|
+
useEffect(
|
|
182
|
+
() => () => {
|
|
183
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
184
|
+
},
|
|
185
|
+
[],
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const copy = useCallback(async () => {
|
|
189
|
+
if (onCopy) await onCopy(text);
|
|
190
|
+
else if (typeof navigator !== "undefined") await navigator.clipboard.writeText(text);
|
|
191
|
+
setCopied(true);
|
|
192
|
+
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
193
|
+
timer.current = window.setTimeout(() => setCopied(false), 1500);
|
|
194
|
+
}, [onCopy, text]);
|
|
195
|
+
|
|
196
|
+
if (!text && (!canRetry || !onRetry) && !onFork) return null;
|
|
197
|
+
return (
|
|
198
|
+
<MessageActions>
|
|
199
|
+
{text && (
|
|
200
|
+
<MessageAction
|
|
201
|
+
tooltip={copied ? "已复制" : "复制"}
|
|
202
|
+
label="复制"
|
|
203
|
+
onClick={() => void copy()}
|
|
204
|
+
>
|
|
205
|
+
{copied ? <CheckIcon size={15} /> : <CopyIcon size={15} />}
|
|
206
|
+
</MessageAction>
|
|
207
|
+
)}
|
|
208
|
+
{canRetry && onRetry && (
|
|
209
|
+
<MessageAction
|
|
210
|
+
tooltip="重试"
|
|
211
|
+
label="重试"
|
|
212
|
+
onClick={() => void onRetry(messageId)}
|
|
213
|
+
>
|
|
214
|
+
<RefreshCcwIcon size={15} />
|
|
215
|
+
</MessageAction>
|
|
216
|
+
)}
|
|
217
|
+
{onFork && (
|
|
218
|
+
<MessageAction
|
|
219
|
+
tooltip={forking ? "正在创建新会话…" : "从这里创建新会话"}
|
|
220
|
+
label={forking ? "正在创建新会话" : "从这里创建新会话"}
|
|
221
|
+
disabled={forking}
|
|
222
|
+
onClick={() => {
|
|
223
|
+
setForking(true);
|
|
224
|
+
void Promise.resolve(onFork(messageId)).finally(() =>
|
|
225
|
+
setForking(false),
|
|
226
|
+
);
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{forking ? (
|
|
230
|
+
<Loader2Icon size={15} className="sb-spin" />
|
|
231
|
+
) : (
|
|
232
|
+
<GitForkIcon size={15} />
|
|
233
|
+
)}
|
|
234
|
+
</MessageAction>
|
|
235
|
+
)}
|
|
236
|
+
</MessageActions>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function BrandMark({ className }: { className?: string }) {
|
|
241
|
+
return (
|
|
242
|
+
<span className={cn("sb-brand-mark", className)} aria-hidden="true">
|
|
243
|
+
<svg width="17" height="17" viewBox="0 0 28 28" fill="none">
|
|
244
|
+
<path
|
|
245
|
+
d="M14 14 C 11.5 10.5, 6.5 10.5, 6.5 14 C 6.5 17.5, 11.5 17.5, 14 14 C 16.5 10.5, 21.5 10.5, 21.5 14 C 21.5 17.5, 16.5 17.5, 14 14 Z"
|
|
246
|
+
stroke="currentColor"
|
|
247
|
+
strokeWidth="1.8"
|
|
248
|
+
strokeLinecap="round"
|
|
249
|
+
className="sb-brand-mark__loop"
|
|
250
|
+
/>
|
|
251
|
+
</svg>
|
|
252
|
+
</span>
|
|
253
|
+
);
|
|
254
|
+
}
|