@polpo-ai/chat 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/dist/index.js ADDED
@@ -0,0 +1,737 @@
1
+ import {
2
+ ToolCallChip,
3
+ ToolCallShell
4
+ } from "./chunk-CCSIMOXD.js";
5
+ import {
6
+ useDocumentDrag,
7
+ useSubmitHandler
8
+ } from "./chunk-LTLIBITC.js";
9
+
10
+ // src/components/chat.tsx
11
+ import { forwardRef as forwardRef2 } from "react";
12
+
13
+ // src/components/chat-provider.tsx
14
+ import {
15
+ createContext,
16
+ useContext
17
+ } from "react";
18
+ import { useChat } from "@polpo-ai/react";
19
+ import { useFiles } from "@polpo-ai/react";
20
+ import { jsx } from "react/jsx-runtime";
21
+ var ChatContext = createContext(null);
22
+ function ChatProvider({
23
+ sessionId,
24
+ agent,
25
+ onSessionCreated,
26
+ onUpdate,
27
+ children
28
+ }) {
29
+ const chat = useChat({
30
+ sessionId,
31
+ agent,
32
+ onSessionCreated,
33
+ onUpdate
34
+ });
35
+ const { uploadFile, isUploading } = useFiles();
36
+ const value = {
37
+ ...chat,
38
+ uploadFile,
39
+ isUploading
40
+ };
41
+ return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children });
42
+ }
43
+ function useChatContext() {
44
+ const ctx = useContext(ChatContext);
45
+ if (!ctx) {
46
+ throw new Error("useChatContext must be used within a <ChatProvider>");
47
+ }
48
+ return ctx;
49
+ }
50
+
51
+ // src/components/chat-messages.tsx
52
+ import {
53
+ forwardRef,
54
+ useCallback,
55
+ useEffect,
56
+ useImperativeHandle,
57
+ useRef,
58
+ useState
59
+ } from "react";
60
+ import { Virtuoso } from "react-virtuoso";
61
+
62
+ // src/components/chat-skeleton.tsx
63
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
64
+ function Bone({ width, height = 14 }) {
65
+ return /* @__PURE__ */ jsx2(
66
+ "div",
67
+ {
68
+ className: "bg-p-line animate-pulse",
69
+ style: { width, height, borderRadius: height > 20 ? 12 : 6 }
70
+ }
71
+ );
72
+ }
73
+ function MessageSkeleton({ lines = 3 }) {
74
+ const widths = ["85%", "70%", "55%", "90%", "40%"];
75
+ return /* @__PURE__ */ jsx2("div", { className: "w-full px-6 pt-4 pb-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-3xl mx-auto", children: [
76
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
77
+ /* @__PURE__ */ jsx2("div", { className: "size-6 rounded-md bg-p-line animate-pulse shrink-0" }),
78
+ /* @__PURE__ */ jsx2(Bone, { width: "80px", height: 13 })
79
+ ] }),
80
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-2", children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx2(Bone, { width: widths[i % widths.length] }, i)) })
81
+ ] }) });
82
+ }
83
+ function UserMessageSkeleton() {
84
+ return /* @__PURE__ */ jsx2("div", { className: "w-full px-6 py-3", children: /* @__PURE__ */ jsx2("div", { className: "max-w-3xl mx-auto flex justify-end", children: /* @__PURE__ */ jsx2("div", { className: "w-[45%] min-w-[120px] h-[42px] rounded-[18px_18px_4px_18px] bg-p-line animate-pulse" }) }) });
85
+ }
86
+ function ChatSkeleton({ count = 3 }) {
87
+ return /* @__PURE__ */ jsx2("div", { className: "py-2", children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs("div", { children: [
88
+ /* @__PURE__ */ jsx2(UserMessageSkeleton, {}),
89
+ /* @__PURE__ */ jsx2(MessageSkeleton, { lines: i === 0 ? 2 : i === 1 ? 4 : 3 })
90
+ ] }, i)) });
91
+ }
92
+
93
+ // src/components/chat-scroll-button.tsx
94
+ import { ArrowDown } from "lucide-react";
95
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
96
+ function ChatScrollButton({
97
+ isAtBottom,
98
+ showNewMessage,
99
+ onClick,
100
+ className
101
+ }) {
102
+ if (isAtBottom) return null;
103
+ return /* @__PURE__ */ jsxs2(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ "aria-label": "Scroll to bottom",
108
+ onClick,
109
+ className: `absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-[var(--polpo-border)] bg-[var(--polpo-bg,#fff)] shadow-md transition-colors hover:bg-[var(--polpo-bg-hover,#f5f5f5)] ${className ?? ""}`,
110
+ children: [
111
+ /* @__PURE__ */ jsx3(ArrowDown, { className: "h-4 w-4 text-[var(--polpo-fg,#333)]" }),
112
+ showNewMessage && /* @__PURE__ */ jsxs2("span", { className: "absolute -top-1 -right-1 flex h-3 w-3", children: [
113
+ /* @__PURE__ */ jsx3("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--polpo-accent,#3b82f6)] opacity-75" }),
114
+ /* @__PURE__ */ jsx3("span", { className: "relative inline-flex h-3 w-3 rounded-full bg-[var(--polpo-accent,#3b82f6)]" })
115
+ ] })
116
+ ]
117
+ }
118
+ );
119
+ }
120
+
121
+ // src/lib/get-text-content.ts
122
+ function getTextContent(content) {
123
+ if (typeof content === "string") return content;
124
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("");
125
+ }
126
+
127
+ // src/components/chat-messages.tsx
128
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
129
+ function DefaultMessageItem({ msg }) {
130
+ const text = getTextContent(msg.content);
131
+ return /* @__PURE__ */ jsx4("div", { className: "w-full px-6 py-3", children: /* @__PURE__ */ jsxs3("div", { className: "max-w-3xl mx-auto", children: [
132
+ /* @__PURE__ */ jsx4("p", { className: "text-xs font-medium opacity-50 mb-1", children: msg.role === "user" ? "You" : "Assistant" }),
133
+ /* @__PURE__ */ jsx4("p", { className: "whitespace-pre-wrap", children: text })
134
+ ] }) });
135
+ }
136
+ function VirtuosoFooter() {
137
+ return /* @__PURE__ */ jsx4("div", { className: "h-px", "aria-hidden": "true" });
138
+ }
139
+ var ChatMessages = forwardRef(
140
+ function ChatMessages2({ renderItem, className, skeletonCount = 3 }, ref) {
141
+ const { messages, isStreaming, status } = useChatContext();
142
+ const virtuosoRef = useRef(null);
143
+ const [isAtBottom, setIsAtBottom] = useState(true);
144
+ const [showNewMessage, setShowNewMessage] = useState(false);
145
+ const prevMessageCountRef = useRef(0);
146
+ const hasInitialScrollRef = useRef(false);
147
+ const scrollToBottom = useCallback(
148
+ (behavior = "smooth") => {
149
+ virtuosoRef.current?.scrollToIndex({
150
+ index: "LAST",
151
+ align: "end",
152
+ behavior
153
+ });
154
+ setShowNewMessage(false);
155
+ },
156
+ []
157
+ );
158
+ useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
159
+ const handleAtBottomStateChange = useCallback((atBottom) => {
160
+ setIsAtBottom(atBottom);
161
+ if (atBottom) setShowNewMessage(false);
162
+ }, []);
163
+ useEffect(() => {
164
+ if (messages.length > 0 && !hasInitialScrollRef.current) {
165
+ hasInitialScrollRef.current = true;
166
+ requestAnimationFrame(
167
+ () => virtuosoRef.current?.scrollToIndex({
168
+ index: "LAST",
169
+ align: "end",
170
+ behavior: "auto"
171
+ })
172
+ );
173
+ }
174
+ }, [messages.length]);
175
+ useEffect(() => {
176
+ hasInitialScrollRef.current = false;
177
+ }, [status]);
178
+ useEffect(() => {
179
+ const cur = messages.length;
180
+ const prev = prevMessageCountRef.current;
181
+ if (cur > prev && !isAtBottom && prev > 0) {
182
+ setShowNewMessage(true);
183
+ }
184
+ prevMessageCountRef.current = cur;
185
+ }, [messages.length, isAtBottom]);
186
+ const itemContent = useCallback(
187
+ (index, msg) => {
188
+ const isLast = index === messages.length - 1;
189
+ if (renderItem) {
190
+ return renderItem(msg, index, isLast, isStreaming);
191
+ }
192
+ return /* @__PURE__ */ jsx4(DefaultMessageItem, { msg });
193
+ },
194
+ [messages.length, isStreaming, renderItem]
195
+ );
196
+ if (status === "loading") {
197
+ return /* @__PURE__ */ jsx4("div", { className: `flex-1 overflow-hidden ${className ?? ""}`, children: /* @__PURE__ */ jsx4(ChatSkeleton, { count: skeletonCount }) });
198
+ }
199
+ return /* @__PURE__ */ jsxs3("div", { className: `relative flex-1 min-h-0 ${className ?? ""}`, children: [
200
+ /* @__PURE__ */ jsx4(
201
+ Virtuoso,
202
+ {
203
+ ref: virtuosoRef,
204
+ data: messages,
205
+ followOutput: "auto",
206
+ atBottomStateChange: handleAtBottomStateChange,
207
+ atBottomThreshold: 100,
208
+ defaultItemHeight: 120,
209
+ overscan: 500,
210
+ increaseViewportBy: { top: 300, bottom: 300 },
211
+ skipAnimationFrameInResizeObserver: true,
212
+ itemContent,
213
+ className: "h-full",
214
+ components: { Footer: VirtuosoFooter }
215
+ }
216
+ ),
217
+ /* @__PURE__ */ jsx4(
218
+ ChatScrollButton,
219
+ {
220
+ isAtBottom,
221
+ showNewMessage,
222
+ onClick: () => scrollToBottom()
223
+ }
224
+ )
225
+ ] });
226
+ }
227
+ );
228
+
229
+ // src/components/chat-message.tsx
230
+ import {
231
+ memo,
232
+ useState as useState2,
233
+ useCallback as useCallback2
234
+ } from "react";
235
+ import { Copy, Check, FileCode } from "lucide-react";
236
+ import { Streamdown } from "streamdown";
237
+
238
+ // src/lib/relative-time.ts
239
+ function relativeTime(iso) {
240
+ const now = Date.now();
241
+ const then = new Date(iso).getTime();
242
+ const diff = Math.floor((now - then) / 1e3);
243
+ if (diff < 10) return "Just now";
244
+ if (diff < 60) return `${diff}s ago`;
245
+ const mins = Math.floor(diff / 60);
246
+ if (mins < 60) return mins === 1 ? "A minute ago" : `${mins}m ago`;
247
+ const hours = Math.floor(mins / 60);
248
+ if (hours < 24) return hours === 1 ? "An hour ago" : `${hours}h ago`;
249
+ return new Date(iso).toLocaleString(void 0, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
250
+ }
251
+
252
+ // src/components/chat-typing.tsx
253
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
254
+ var dotBase = "inline-block h-1.5 w-1.5 rounded-full bg-current opacity-40 animate-[typing-dot_1.4s_ease-in-out_infinite]";
255
+ function ChatTyping({ className }) {
256
+ return /* @__PURE__ */ jsxs4(
257
+ "span",
258
+ {
259
+ role: "status",
260
+ "aria-label": "Typing",
261
+ className: `inline-flex items-center gap-1 ${className ?? ""}`,
262
+ children: [
263
+ /* @__PURE__ */ jsx5("span", { className: dotBase, style: { animationDelay: "0ms" } }),
264
+ /* @__PURE__ */ jsx5("span", { className: dotBase, style: { animationDelay: "200ms" } }),
265
+ /* @__PURE__ */ jsx5("span", { className: dotBase, style: { animationDelay: "400ms" } })
266
+ ]
267
+ }
268
+ );
269
+ }
270
+
271
+ // src/components/chat-message.tsx
272
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
273
+ function CopyButton({ text }) {
274
+ const [copied, setCopied] = useState2(false);
275
+ const handleCopy = useCallback2(() => {
276
+ navigator.clipboard.writeText(text);
277
+ setCopied(true);
278
+ setTimeout(() => setCopied(false), 1500);
279
+ }, [text]);
280
+ return /* @__PURE__ */ jsx6(
281
+ "button",
282
+ {
283
+ type: "button",
284
+ onClick: handleCopy,
285
+ "aria-label": "Copy message",
286
+ className: "inline-flex items-center justify-center rounded-md p-1 text-[var(--ink-3)] hover:text-[var(--ink)] hover:bg-[var(--warm)] transition-colors",
287
+ children: copied ? /* @__PURE__ */ jsx6(Check, { className: "size-3.5" }) : /* @__PURE__ */ jsx6(Copy, { className: "size-3.5" })
288
+ }
289
+ );
290
+ }
291
+ function ContentParts({
292
+ parts,
293
+ align
294
+ }) {
295
+ const nonText = parts.filter((p) => p.type !== "text");
296
+ if (nonText.length === 0) return null;
297
+ return /* @__PURE__ */ jsx6(
298
+ "div",
299
+ {
300
+ className: `flex flex-wrap gap-1.5 mb-1 ${align === "end" ? "justify-end" : ""}`,
301
+ children: nonText.map((part, i) => {
302
+ if (part.type === "image_url") {
303
+ return /* @__PURE__ */ jsx6(
304
+ "a",
305
+ {
306
+ href: part.image_url.url,
307
+ target: "_blank",
308
+ rel: "noopener noreferrer",
309
+ className: "rounded-lg overflow-hidden max-w-[200px]",
310
+ children: /* @__PURE__ */ jsx6(
311
+ "img",
312
+ {
313
+ src: part.image_url.url,
314
+ alt: "",
315
+ className: "w-full h-auto block"
316
+ }
317
+ )
318
+ },
319
+ i
320
+ );
321
+ }
322
+ if (part.type === "file") {
323
+ const fileId = part.file_id;
324
+ const fn = fileId.split("/").pop() || fileId;
325
+ return /* @__PURE__ */ jsxs5(
326
+ "a",
327
+ {
328
+ href: `/api/polpo/files/read?path=${encodeURIComponent(fileId)}`,
329
+ target: "_blank",
330
+ rel: "noopener noreferrer",
331
+ className: "flex items-center gap-1.5 bg-[var(--warm)] border border-[var(--line)] rounded-lg px-2.5 py-1.5 text-xs text-[var(--ink)] hover:border-[var(--ink-3)] transition-colors",
332
+ children: [
333
+ /* @__PURE__ */ jsx6(FileCode, { size: 13 }),
334
+ /* @__PURE__ */ jsx6("span", { className: "truncate max-w-[120px]", children: fn })
335
+ ]
336
+ },
337
+ i
338
+ );
339
+ }
340
+ return null;
341
+ })
342
+ }
343
+ );
344
+ }
345
+ var ChatUserMessage = memo(
346
+ function ChatUserMessage2({
347
+ msg,
348
+ isLast,
349
+ isStreaming
350
+ }) {
351
+ const text = getTextContent(msg.content);
352
+ return /* @__PURE__ */ jsx6("div", { className: "w-full px-6 py-3", children: /* @__PURE__ */ jsx6("div", { className: "max-w-3xl mx-auto", children: /* @__PURE__ */ jsxs5("div", { className: "group flex w-full flex-col gap-2 ml-auto justify-end", children: [
353
+ Array.isArray(msg.content) && /* @__PURE__ */ jsx6(ContentParts, { parts: msg.content, align: "end" }),
354
+ /* @__PURE__ */ jsx6("div", { className: "w-fit max-w-[80%] ml-auto rounded-[18px_18px_4px_18px] bg-[var(--warm)] px-4 py-3", children: text ? /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words text-[var(--ink)]", children: text }) : null }),
355
+ text && (!isLast || !isStreaming) && /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-1.5 h-6", children: [
356
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-[var(--ink-3)] opacity-0 group-hover:opacity-100 transition-opacity", children: msg.ts ? relativeTime(msg.ts) : "" }),
357
+ /* @__PURE__ */ jsx6("div", { className: "opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsx6(CopyButton, { text }) })
358
+ ] })
359
+ ] }) }) });
360
+ },
361
+ (prev, next) => prev.isLast === next.isLast && prev.isStreaming === next.isStreaming && prev.msg.id === next.msg.id && prev.msg.content === next.msg.content
362
+ );
363
+ var ChatAssistantMessage = memo(
364
+ function ChatAssistantMessage2({
365
+ msg,
366
+ isLast,
367
+ isStreaming,
368
+ avatar,
369
+ agentName,
370
+ streamdownComponents: components
371
+ }) {
372
+ const text = getTextContent(msg.content);
373
+ const filteredToolCalls = msg.toolCalls?.filter(
374
+ (tc) => tc.name !== "ask_user_question"
375
+ );
376
+ return /* @__PURE__ */ jsx6("div", { className: "w-full px-6 pt-4 pb-6", children: /* @__PURE__ */ jsx6("div", { className: "max-w-3xl mx-auto", children: /* @__PURE__ */ jsxs5("div", { className: "group flex w-full flex-col gap-2", children: [
377
+ (avatar || agentName) && /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 mb-1", children: [
378
+ avatar,
379
+ agentName && /* @__PURE__ */ jsx6("span", { className: "font-display text-[13px] font-semibold text-[var(--ink)]", children: agentName })
380
+ ] }),
381
+ filteredToolCalls && filteredToolCalls.length > 0 && /* @__PURE__ */ jsx6("div", { className: "flex flex-col gap-1 mb-1", children: filteredToolCalls.map((tc) => /* @__PURE__ */ jsx6(ToolCallChip, { tool: tc }, tc.id)) }),
382
+ Array.isArray(msg.content) && /* @__PURE__ */ jsx6(ContentParts, { parts: msg.content, align: "start" }),
383
+ /* @__PURE__ */ jsx6("div", { className: "w-full text-[var(--ink)]", children: text ? components ? /* @__PURE__ */ jsx6(
384
+ Streamdown,
385
+ {
386
+ className: "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
387
+ components,
388
+ children: text
389
+ }
390
+ ) : /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words", children: text }) : !filteredToolCalls?.length && /* @__PURE__ */ jsx6(ChatTyping, { className: "pt-1" }) }),
391
+ text && (!isLast || !isStreaming) && /* @__PURE__ */ jsx6("div", { className: "h-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsx6(CopyButton, { text }) })
392
+ ] }) }) });
393
+ },
394
+ (prev, next) => prev.avatar === next.avatar && prev.agentName === next.agentName && prev.isLast === next.isLast && prev.isStreaming === next.isStreaming && prev.streamdownComponents === next.streamdownComponents && prev.msg.id === next.msg.id && prev.msg.content === next.msg.content && prev.msg.toolCalls?.length === next.msg.toolCalls?.length && JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) === JSON.stringify(next.msg.toolCalls?.map((t) => t.state))
395
+ );
396
+ var ChatMessage = memo(
397
+ function ChatMessage2({
398
+ msg,
399
+ isLast,
400
+ isStreaming,
401
+ avatar,
402
+ agentName,
403
+ streamdownComponents: streamdownComponents2
404
+ }) {
405
+ if (msg.role === "user") {
406
+ return /* @__PURE__ */ jsx6(ChatUserMessage, { msg, isLast, isStreaming });
407
+ }
408
+ return /* @__PURE__ */ jsx6(
409
+ ChatAssistantMessage,
410
+ {
411
+ msg,
412
+ isLast,
413
+ isStreaming,
414
+ avatar,
415
+ agentName,
416
+ streamdownComponents: streamdownComponents2
417
+ }
418
+ );
419
+ },
420
+ (prev, next) => prev.avatar === next.avatar && prev.agentName === next.agentName && prev.isLast === next.isLast && prev.isStreaming === next.isStreaming && prev.streamdownComponents === next.streamdownComponents && prev.msg.id === next.msg.id && prev.msg.content === next.msg.content && prev.msg.role === next.msg.role && prev.msg.toolCalls?.length === next.msg.toolCalls?.length && JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) === JSON.stringify(next.msg.toolCalls?.map((t) => t.state))
421
+ );
422
+
423
+ // src/components/chat-input.tsx
424
+ import {
425
+ useState as useState3,
426
+ useRef as useRef2,
427
+ useCallback as useCallback3,
428
+ useEffect as useEffect2
429
+ } from "react";
430
+ import { ArrowUp, Square, Plus, X, Upload } from "lucide-react";
431
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
432
+ function ChatInput({
433
+ placeholder = "Type a message\u2026",
434
+ hint,
435
+ allowAttachments = true,
436
+ className,
437
+ renderSubmit
438
+ }) {
439
+ const { sendMessage, isStreaming, abort, uploadFile } = useChatContext();
440
+ const handleSubmit = useSubmitHandler(sendMessage, uploadFile);
441
+ const dragging = useDocumentDrag();
442
+ const [text, setText] = useState3("");
443
+ const [files, setFiles] = useState3([]);
444
+ const [isSending, setIsSending] = useState3(false);
445
+ const textareaRef = useRef2(null);
446
+ const fileInputRef = useRef2(null);
447
+ useEffect2(() => {
448
+ const el = textareaRef.current;
449
+ if (!el) return;
450
+ el.style.height = "auto";
451
+ el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
452
+ }, [text]);
453
+ const addFiles = useCallback3((incoming) => {
454
+ const arr = Array.from(incoming);
455
+ setFiles((prev) => {
456
+ const existing = new Set(prev.map((f) => f.file.name));
457
+ const fresh = arr.filter((f) => !existing.has(f.name));
458
+ return [
459
+ ...prev,
460
+ ...fresh.map((file) => ({
461
+ id: Math.random().toString(36).slice(2),
462
+ file,
463
+ url: URL.createObjectURL(file)
464
+ }))
465
+ ];
466
+ });
467
+ }, []);
468
+ const removeFile = useCallback3((id) => {
469
+ setFiles((prev) => {
470
+ const f = prev.find((p) => p.id === id);
471
+ if (f) URL.revokeObjectURL(f.url);
472
+ return prev.filter((p) => p.id !== id);
473
+ });
474
+ }, []);
475
+ useEffect2(
476
+ () => () => {
477
+ files.forEach((f) => URL.revokeObjectURL(f.url));
478
+ },
479
+ // eslint-disable-next-line react-hooks/exhaustive-deps
480
+ []
481
+ );
482
+ const submit = useCallback3(async () => {
483
+ if (isSending) return;
484
+ const trimmed = text.trim();
485
+ if (!trimmed && files.length === 0) return;
486
+ setIsSending(true);
487
+ try {
488
+ await handleSubmit({
489
+ text: trimmed,
490
+ files: files.map((f) => ({ url: f.url, filename: f.file.name }))
491
+ });
492
+ setText("");
493
+ setFiles([]);
494
+ } finally {
495
+ setIsSending(false);
496
+ }
497
+ setTimeout(() => textareaRef.current?.focus(), 0);
498
+ }, [text, files, handleSubmit, isSending]);
499
+ const onKeyDown = useCallback3(
500
+ (e) => {
501
+ if (e.key === "Enter" && !e.shiftKey) {
502
+ e.preventDefault();
503
+ submit();
504
+ }
505
+ },
506
+ [submit]
507
+ );
508
+ const onFileChange = useCallback3(
509
+ (e) => {
510
+ if (e.target.files) addFiles(e.target.files);
511
+ e.target.value = "";
512
+ },
513
+ [addFiles]
514
+ );
515
+ const onDrop = useCallback3(
516
+ (e) => {
517
+ e.preventDefault();
518
+ if (e.dataTransfer.files.length > 0) {
519
+ addFiles(e.dataTransfer.files);
520
+ }
521
+ },
522
+ [addFiles]
523
+ );
524
+ return /* @__PURE__ */ jsx7("div", { className: `shrink-0 ${className || ""}`, children: /* @__PURE__ */ jsx7("div", { className: "w-full px-6 py-3", children: /* @__PURE__ */ jsxs6("div", { className: "max-w-3xl mx-auto relative", children: [
525
+ allowAttachments && dragging && /* @__PURE__ */ jsxs6("div", { className: "absolute inset-0 z-10 bg-blue-50 border-2 border-dashed border-blue-400 rounded-2xl flex items-center justify-center gap-2 text-blue-600 text-sm font-medium pointer-events-none", children: [
526
+ /* @__PURE__ */ jsx7(Upload, { className: "size-4" }),
527
+ " Drop files to attach"
528
+ ] }),
529
+ /* @__PURE__ */ jsxs6(
530
+ "div",
531
+ {
532
+ className: "rounded-2xl border border-gray-200 shadow-sm focus-within:border-blue-400 focus-within:shadow-md transition-all bg-white",
533
+ onDrop: allowAttachments ? onDrop : void 0,
534
+ onDragOver: allowAttachments ? (e) => e.preventDefault() : void 0,
535
+ children: [
536
+ files.length > 0 && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2 px-4 pt-3", children: files.map((f) => {
537
+ const isImage = f.file.type.startsWith("image/");
538
+ return /* @__PURE__ */ jsxs6(
539
+ "div",
540
+ {
541
+ className: "relative flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs",
542
+ children: [
543
+ isImage ? /* @__PURE__ */ jsx7("img", { src: f.url, alt: f.file.name, className: "size-8 rounded object-cover" }) : /* @__PURE__ */ jsx7("div", { className: "size-8 rounded bg-gray-200 flex items-center justify-center text-gray-500 text-[10px] font-mono uppercase", children: f.file.name.split(".").pop()?.slice(0, 4) }),
544
+ /* @__PURE__ */ jsx7("span", { className: "truncate max-w-[120px]", children: f.file.name }),
545
+ /* @__PURE__ */ jsx7(
546
+ "button",
547
+ {
548
+ type: "button",
549
+ onClick: () => removeFile(f.id),
550
+ className: "size-4 rounded-full bg-gray-300/50 flex items-center justify-center hover:bg-red-500 hover:text-white transition-colors",
551
+ children: /* @__PURE__ */ jsx7(X, { className: "size-2.5" })
552
+ }
553
+ )
554
+ ]
555
+ },
556
+ f.id
557
+ );
558
+ }) }),
559
+ /* @__PURE__ */ jsx7(
560
+ "textarea",
561
+ {
562
+ ref: textareaRef,
563
+ value: text,
564
+ onChange: (e) => setText(e.target.value),
565
+ onKeyDown,
566
+ placeholder,
567
+ rows: 1,
568
+ className: "w-full resize-none bg-transparent px-5 pt-4 pb-2 text-sm outline-none placeholder:text-gray-400"
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between px-3 pb-3", children: [
572
+ allowAttachments ? /* @__PURE__ */ jsx7(
573
+ "button",
574
+ {
575
+ type: "button",
576
+ onClick: () => fileInputRef.current?.click(),
577
+ className: "flex items-center justify-center size-8 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors",
578
+ "aria-label": "Attach file",
579
+ children: /* @__PURE__ */ jsx7(Plus, { className: "size-4" })
580
+ }
581
+ ) : /* @__PURE__ */ jsx7("div", {}),
582
+ renderSubmit ? renderSubmit({ isStreaming, onStop: abort }) : /* @__PURE__ */ jsx7(
583
+ "button",
584
+ {
585
+ type: "button",
586
+ onClick: isStreaming ? abort : submit,
587
+ disabled: isSending && !isStreaming,
588
+ className: "flex items-center justify-center size-8 rounded-lg bg-gray-900 text-white hover:bg-gray-700 disabled:opacity-40 transition-colors",
589
+ "aria-label": isStreaming ? "Stop" : "Send",
590
+ children: isStreaming ? /* @__PURE__ */ jsx7(Square, { className: "size-3.5" }) : /* @__PURE__ */ jsx7(ArrowUp, { className: "size-4" })
591
+ }
592
+ )
593
+ ] })
594
+ ]
595
+ }
596
+ ),
597
+ allowAttachments && /* @__PURE__ */ jsx7(
598
+ "input",
599
+ {
600
+ ref: fileInputRef,
601
+ type: "file",
602
+ multiple: true,
603
+ onChange: onFileChange,
604
+ className: "hidden"
605
+ }
606
+ ),
607
+ hint && /* @__PURE__ */ jsx7("p", { className: "text-center text-xs text-gray-400 mt-2", children: hint })
608
+ ] }) }) });
609
+ }
610
+
611
+ // src/components/chat.tsx
612
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
613
+ var Chat = forwardRef2(function Chat2({
614
+ sessionId,
615
+ agent,
616
+ onSessionCreated,
617
+ onUpdate,
618
+ renderMessage,
619
+ avatar,
620
+ agentName,
621
+ streamdownComponents: streamdownComponents2,
622
+ skeletonCount,
623
+ inputPlaceholder,
624
+ inputHint,
625
+ allowAttachments,
626
+ children,
627
+ className
628
+ }, ref) {
629
+ const hasChildren = children !== void 0 && children !== null;
630
+ const defaultRender = (msg, _index, isLast, isStreaming) => /* @__PURE__ */ jsx8(
631
+ ChatMessage,
632
+ {
633
+ msg,
634
+ isLast,
635
+ isStreaming,
636
+ avatar,
637
+ agentName,
638
+ streamdownComponents: streamdownComponents2
639
+ }
640
+ );
641
+ return /* @__PURE__ */ jsx8(
642
+ ChatProvider,
643
+ {
644
+ sessionId,
645
+ agent,
646
+ onSessionCreated,
647
+ onUpdate,
648
+ children: /* @__PURE__ */ jsxs7("div", { className: `flex flex-col min-w-0 min-h-0 ${className || ""}`, children: [
649
+ /* @__PURE__ */ jsx8(
650
+ ChatMessages,
651
+ {
652
+ ref,
653
+ renderItem: renderMessage || defaultRender,
654
+ skeletonCount,
655
+ className: "flex-1"
656
+ }
657
+ ),
658
+ hasChildren ? children : /* @__PURE__ */ jsx8(
659
+ ChatInput,
660
+ {
661
+ placeholder: inputPlaceholder,
662
+ hint: inputHint,
663
+ allowAttachments
664
+ }
665
+ )
666
+ ] })
667
+ }
668
+ );
669
+ });
670
+
671
+ // src/components/streamdown-code.tsx
672
+ import { jsx as jsx9 } from "react/jsx-runtime";
673
+ function FallbackCodeBlock({ code, language }) {
674
+ return /* @__PURE__ */ jsx9(
675
+ "pre",
676
+ {
677
+ "data-language": language,
678
+ style: {
679
+ margin: "0.75rem 0",
680
+ padding: "0.875rem 1rem",
681
+ borderRadius: "10px",
682
+ overflowX: "auto",
683
+ border: "1px solid var(--line, #e5e7eb)",
684
+ background: "var(--bg, #f9fafb)",
685
+ color: "var(--ink, inherit)",
686
+ fontSize: "0.75rem",
687
+ lineHeight: "1.625"
688
+ },
689
+ children: /* @__PURE__ */ jsx9("code", { className: `language-${language}`, children: code })
690
+ }
691
+ );
692
+ }
693
+ function createStreamdownComponents(CodeBlockComponent) {
694
+ const Block = CodeBlockComponent ?? FallbackCodeBlock;
695
+ function StreamdownCode(props) {
696
+ const {
697
+ children,
698
+ className,
699
+ node: _,
700
+ "data-block": dataBlock,
701
+ ...rest
702
+ } = props;
703
+ if (dataBlock !== void 0) {
704
+ const match = /language-(\w+)/.exec(className || "");
705
+ const lang = match?.[1] || "text";
706
+ const code = String(children).replace(/\n$/, "");
707
+ return /* @__PURE__ */ jsx9(Block, { code, language: lang });
708
+ }
709
+ return /* @__PURE__ */ jsx9("code", { className, ...rest, children });
710
+ }
711
+ return { code: StreamdownCode };
712
+ }
713
+ var streamdownComponents = createStreamdownComponents();
714
+ export {
715
+ Chat,
716
+ ChatAssistantMessage,
717
+ ChatInput,
718
+ ChatMessage,
719
+ ChatMessages,
720
+ ChatProvider,
721
+ ChatScrollButton,
722
+ ChatSkeleton,
723
+ ChatTyping,
724
+ ChatUserMessage,
725
+ MessageSkeleton,
726
+ ToolCallChip,
727
+ ToolCallShell,
728
+ UserMessageSkeleton,
729
+ createStreamdownComponents,
730
+ getTextContent,
731
+ relativeTime,
732
+ streamdownComponents,
733
+ useChatContext,
734
+ useDocumentDrag,
735
+ useSubmitHandler
736
+ };
737
+ //# sourceMappingURL=index.js.map