@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.
Files changed (38) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/index.ts +4 -0
  4. package/demo/message-panel-chat-showcase.tsx +250 -0
  5. package/demo/message-panel-gallery.tsx +389 -0
  6. package/package.json +44 -0
  7. package/src/adapters/ai-sdk.ts +32 -0
  8. package/src/camel/camel-chat-messages.tsx +1420 -0
  9. package/src/camel/camel-model-select.tsx +88 -0
  10. package/src/camel/camel-plan-bar.tsx +115 -0
  11. package/src/camel/camel-prompt-input.tsx +540 -0
  12. package/src/camel/camel-tool-presentation.tsx +1270 -0
  13. package/src/camel/camel-turn.ts +315 -0
  14. package/src/camel/index.ts +7 -0
  15. package/src/camel/turn-summary-bar.tsx +81 -0
  16. package/src/chat-summary-panel.tsx +229 -0
  17. package/src/composer/chat-composer.tsx +216 -0
  18. package/src/composer/composer-attachments.tsx +138 -0
  19. package/src/composer/composer-trigger-popover.tsx +236 -0
  20. package/src/composer/composer.tsx +425 -0
  21. package/src/composer/drop-controller.ts +80 -0
  22. package/src/composer/index.ts +6 -0
  23. package/src/composer/types.ts +79 -0
  24. package/src/contracts.ts +139 -0
  25. package/src/conversation.tsx +76 -0
  26. package/src/index.ts +12 -0
  27. package/src/internal/cn.ts +3 -0
  28. package/src/message-panel.tsx +368 -0
  29. package/src/message.tsx +254 -0
  30. package/src/parts/index.tsx +1149 -0
  31. package/src/parts/kind.ts +67 -0
  32. package/src/parts/plan.ts +63 -0
  33. package/src/parts/primitives.tsx +439 -0
  34. package/src/registry.ts +44 -0
  35. package/src/status.tsx +132 -0
  36. package/src/styles/camel-chat.css +36 -0
  37. package/src/styles/index.css +2186 -0
  38. package/src/styles/tokens.css +31 -0
@@ -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
+ }