@cntyclub/agent-react 0.5.0 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
- import { ChatMessage, ChatMessageBubble, Markdown, Collapsible, CollapsibleTrigger, ChatToolChip, cn, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Button, Group, Spinner, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatMessages, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, ChatTypingIndicator, ChatInput, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
2
- import { ChevronDownIcon, CheckIcon, ShieldAlertIcon, CheckCheckIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Maximize2Icon, XIcon, Trash2Icon, SparklesIcon, CodeIcon } from 'lucide-react';
3
- import * as React3 from 'react';
1
+ import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatMessages, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, ChatTypingIndicator, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
2
+ import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, CheckIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
+ import * as React5 from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/api-client.ts
@@ -25,13 +25,15 @@ var AgentApiClient = class {
25
25
  const base = this.config.apiBaseUrl.replace(/\/$/, "");
26
26
  const separator = path.includes("?") ? "&" : "?";
27
27
  const url = `${base}${path}${separator}client_id=${encodeURIComponent(this.config.clientId)}`;
28
+ const isFormData = typeof FormData !== "undefined" && options.body instanceof FormData;
29
+ const headers = {
30
+ Authorization: `Bearer ${token}`,
31
+ "X-Agent-Client-Id": this.config.clientId
32
+ };
33
+ if (!isFormData) headers["Content-Type"] = "application/json";
28
34
  const response = await fetch(url, {
29
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
30
- headers: {
31
- Authorization: `Bearer ${token}`,
32
- "Content-Type": "application/json",
33
- "X-Agent-Client-Id": this.config.clientId
34
- },
35
+ body: options.body === void 0 ? void 0 : isFormData ? options.body : JSON.stringify(options.body),
36
+ headers,
35
37
  method: options.method ?? "GET"
36
38
  });
37
39
  if (response.status === 204) return void 0;
@@ -49,7 +51,15 @@ var AgentApiClient = class {
49
51
  getConfig() {
50
52
  return this.request("/agent-mode/config/");
51
53
  }
52
- sendMessage(message, conversationId) {
54
+ sendMessage(message, conversationId, files) {
55
+ if (files && files.length > 0) {
56
+ const form = new FormData();
57
+ form.append("client_id", this.config.clientId);
58
+ form.append("message", message);
59
+ if (conversationId) form.append("conversation_id", conversationId);
60
+ for (const file of files) form.append("files", file, file.name);
61
+ return this.request("/agent-mode/chat/", { body: form, method: "POST" });
62
+ }
53
63
  return this.request("/agent-mode/chat/", {
54
64
  body: {
55
65
  client_id: this.config.clientId,
@@ -78,6 +88,176 @@ var AgentApiClient = class {
78
88
  }
79
89
  };
80
90
 
91
+ // src/file-support.ts
92
+ var DEFAULT_ACCEPT = [".txt", ".md", ".markdown", ".csv", ".tsv", ".log", ".docx"];
93
+ var DEFAULT_MAX_SIZE_MB = 5;
94
+ function fileUploadEnabled(config) {
95
+ return config.fileUpload?.enabled !== false;
96
+ }
97
+ function acceptedExtensions(config) {
98
+ const accept = config.fileUpload?.accept;
99
+ return accept && accept.length > 0 ? accept.map((ext) => ext.toLowerCase()) : DEFAULT_ACCEPT;
100
+ }
101
+ function maxSizeMb(config) {
102
+ return config.fileUpload?.maxSizeMb ?? DEFAULT_MAX_SIZE_MB;
103
+ }
104
+ function acceptAttribute(config) {
105
+ return acceptedExtensions(config).join(",");
106
+ }
107
+ function extensionOf(filename) {
108
+ const dot = filename.lastIndexOf(".");
109
+ return dot >= 0 ? filename.slice(dot).toLowerCase() : "";
110
+ }
111
+ function validateFile(config, file) {
112
+ const extensions = acceptedExtensions(config);
113
+ if (!extensions.includes(extensionOf(file.name))) {
114
+ return `\u201C${file.name}\u201D isn\u2019t a supported text file. Allowed: ${extensions.join(", ")}.`;
115
+ }
116
+ const limit = maxSizeMb(config);
117
+ if (file.size > limit * 1024 * 1024) {
118
+ return `\u201C${file.name}\u201D is too large (max ${limit} MB).`;
119
+ }
120
+ return null;
121
+ }
122
+ function formatBytes(bytes) {
123
+ if (!bytes && bytes !== 0) return "";
124
+ if (bytes < 1024) return `${bytes} B`;
125
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
126
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
127
+ }
128
+ function AgentComposer({
129
+ acceptAttribute: acceptAttribute2,
130
+ className,
131
+ disabled,
132
+ files,
133
+ onPickFiles,
134
+ onRemoveFile,
135
+ onSubmit,
136
+ onValueChange,
137
+ placeholder = "Ask anything\u2026",
138
+ value
139
+ }) {
140
+ const textareaRef = React5.useRef(null);
141
+ const inputRef = React5.useRef(null);
142
+ const canSubmit = !disabled && (value.trim().length > 0 || files.length > 0);
143
+ const submit = React5.useCallback(() => {
144
+ if (!canSubmit) return;
145
+ onSubmit(value.trim());
146
+ }, [canSubmit, onSubmit, value]);
147
+ React5.useEffect(() => {
148
+ const textarea = textareaRef.current;
149
+ if (!textarea) return;
150
+ textarea.style.height = "auto";
151
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 144)}px`;
152
+ }, [value]);
153
+ const handlePick = (event) => {
154
+ const picked = Array.from(event.target.files ?? []);
155
+ if (picked.length) onPickFiles?.(picked);
156
+ event.target.value = "";
157
+ };
158
+ return /* @__PURE__ */ jsx(
159
+ "form",
160
+ {
161
+ className: cn("shrink-0 bg-background p-3 pt-1.5", className),
162
+ "data-slot": "agent-composer",
163
+ onSubmit: (event) => {
164
+ event.preventDefault();
165
+ submit();
166
+ },
167
+ children: /* @__PURE__ */ jsxs(
168
+ "div",
169
+ {
170
+ className: cn(
171
+ "flex flex-col gap-1.5 rounded-2xl border border-input bg-popover p-1.5 shadow-xs transition-[border-color,box-shadow] duration-150 focus-within:border-ring/64 focus-within:ring-2 focus-within:ring-ring/24 dark:bg-input/32",
172
+ disabled && "opacity-64"
173
+ ),
174
+ children: [
175
+ files.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pt-1", children: files.map((file, index) => /* @__PURE__ */ jsxs(
176
+ "span",
177
+ {
178
+ className: "flex max-w-52 items-center gap-1.5 rounded-lg border border-border/72 bg-muted/48 py-1 pr-1 pl-2 text-xs",
179
+ children: [
180
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
181
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: file.name }),
182
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatBytes(file.size) }),
183
+ onRemoveFile ? /* @__PURE__ */ jsx(
184
+ "button",
185
+ {
186
+ "aria-label": `Remove ${file.name}`,
187
+ className: "flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground [&_svg]:size-3",
188
+ onClick: () => onRemoveFile(index),
189
+ type: "button",
190
+ children: /* @__PURE__ */ jsx(XIcon, {})
191
+ }
192
+ ) : null
193
+ ]
194
+ },
195
+ `${file.name}-${index}`
196
+ )) }) : null,
197
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1.5", children: [
198
+ onPickFiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
199
+ /* @__PURE__ */ jsx(
200
+ "input",
201
+ {
202
+ accept: acceptAttribute2,
203
+ className: "hidden",
204
+ multiple: true,
205
+ onChange: handlePick,
206
+ ref: inputRef,
207
+ type: "file"
208
+ }
209
+ ),
210
+ /* @__PURE__ */ jsx(
211
+ Button,
212
+ {
213
+ "aria-label": "Attach files",
214
+ className: "shrink-0",
215
+ disabled,
216
+ onClick: () => inputRef.current?.click(),
217
+ size: "icon-sm",
218
+ type: "button",
219
+ variant: "ghost",
220
+ children: /* @__PURE__ */ jsx(PaperclipIcon, {})
221
+ }
222
+ )
223
+ ] }) : null,
224
+ /* @__PURE__ */ jsx(
225
+ "textarea",
226
+ {
227
+ className: "max-h-36 min-h-8 flex-1 resize-none self-center bg-transparent py-1.5 pl-1.5 text-sm outline-none placeholder:text-muted-foreground",
228
+ disabled,
229
+ onChange: (event) => onValueChange(event.target.value),
230
+ onKeyDown: (event) => {
231
+ if (event.key === "Enter" && !event.shiftKey) {
232
+ event.preventDefault();
233
+ submit();
234
+ }
235
+ },
236
+ placeholder,
237
+ ref: textareaRef,
238
+ rows: 1,
239
+ value
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsx(
243
+ Button,
244
+ {
245
+ "aria-label": "Send message",
246
+ className: "shrink-0 rounded-full before:rounded-full",
247
+ disabled: !canSubmit,
248
+ size: "icon-sm",
249
+ type: "submit",
250
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
251
+ }
252
+ )
253
+ ] })
254
+ ]
255
+ }
256
+ )
257
+ }
258
+ );
259
+ }
260
+
81
261
  // src/message-group.ts
82
262
  function groupMessages(messages) {
83
263
  const items = [];
@@ -147,7 +327,21 @@ function groupMessages(messages) {
147
327
  }
148
328
  function MessageItem({ message }) {
149
329
  if (message.role === "user") {
150
- return /* @__PURE__ */ jsx(ChatMessage, { from: "user", children: /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) });
330
+ const attachments = message.attachments ?? [];
331
+ return /* @__PURE__ */ jsxs(ChatMessage, { from: "user", children: [
332
+ attachments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: attachments.map((attachment) => /* @__PURE__ */ jsxs(
333
+ "span",
334
+ {
335
+ className: "flex max-w-52 items-center gap-1.5 rounded-lg border border-border/72 bg-muted/48 px-2 py-1 text-muted-foreground text-xs",
336
+ children: [
337
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3.5 shrink-0" }),
338
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: attachment.filename })
339
+ ]
340
+ },
341
+ attachment.id
342
+ )) }) : null,
343
+ message.content ? /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) : null
344
+ ] });
151
345
  }
152
346
  if (message.role === "assistant") {
153
347
  if (!message.content || !message.content.trim()) return null;
@@ -355,20 +549,146 @@ function resolveToolPresentation(describe, toolName, args) {
355
549
  }
356
550
  return { details, summary, title };
357
551
  }
358
- function StepTables({ blocks, stepKey }) {
359
- const tables = (blocks ?? []).filter((block) => block.type === "table");
360
- if (tables.length === 0) return null;
361
- return /* @__PURE__ */ jsx(Fragment, { children: tables.map((block, index) => {
362
- const { columns, rows } = toDisplayTable(block);
552
+ function DownloadButton({ block, size = "sm" }) {
553
+ if (!block.download_url) {
554
+ return /* @__PURE__ */ jsx(Button, { disabled: true, size, variant: "outline", children: size === "icon-sm" ? /* @__PURE__ */ jsx(DownloadIcon, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
555
+ /* @__PURE__ */ jsx(DownloadIcon, {}),
556
+ " Download"
557
+ ] }) });
558
+ }
559
+ return /* @__PURE__ */ jsxs(
560
+ Button,
561
+ {
562
+ render: (
563
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
564
+ /* @__PURE__ */ jsx("a", { download: block.filename, href: block.download_url, rel: "noopener noreferrer", target: "_blank" })
565
+ ),
566
+ size,
567
+ variant: size === "icon-sm" ? "outline" : "default",
568
+ children: [
569
+ /* @__PURE__ */ jsx(DownloadIcon, {}),
570
+ size === "icon-sm" ? null : "Download"
571
+ ]
572
+ }
573
+ );
574
+ }
575
+ function DocumentViewer({ block, onClose }) {
576
+ React5.useEffect(() => {
577
+ const onKey = (event) => event.key === "Escape" && onClose();
578
+ window.addEventListener("keydown", onKey);
579
+ return () => window.removeEventListener("keydown", onKey);
580
+ }, [onClose]);
581
+ return /* @__PURE__ */ jsxs("div", { "aria-modal": "true", className: "fixed inset-0 z-[60] flex flex-col bg-background", role: "dialog", children: [
582
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-border/72 border-b px-4 py-3", children: [
583
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
584
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
585
+ /* @__PURE__ */ jsx("div", { className: "truncate font-medium text-sm", children: block.title || block.filename }),
586
+ /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs uppercase", children: [
587
+ block.format,
588
+ block.size_bytes ? ` \xB7 ${formatBytes(block.size_bytes)}` : ""
589
+ ] })
590
+ ] }),
591
+ /* @__PURE__ */ jsx(DownloadButton, { block }),
592
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Close preview", onClick: onClose, size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ jsx(XIcon, {}) })
593
+ ] }),
594
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4", children: /* @__PURE__ */ jsxs("pre", { className: "mx-auto max-w-3xl whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-foreground", children: [
595
+ block.preview || "(no preview available \u2014 download to view)",
596
+ block.preview_truncated ? "\n\n\u2026 preview truncated. Download for the full file." : ""
597
+ ] }) })
598
+ ] });
599
+ }
600
+ function DocumentCard({ block }) {
601
+ const [open, setOpen] = React5.useState(false);
602
+ const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
603
+ return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
604
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
605
+ /* @__PURE__ */ jsx("span", { className: "flex size-7 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary [&_svg]:size-4", children: /* @__PURE__ */ jsx(FileTextIcon, {}) }),
606
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
607
+ /* @__PURE__ */ jsx("div", { className: "truncate font-medium text-sm", children: block.title || block.filename }),
608
+ /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs uppercase", children: [
609
+ block.format,
610
+ block.size_bytes ? ` \xB7 ${formatBytes(block.size_bytes)}` : ""
611
+ ] })
612
+ ] }),
613
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Preview", onClick: () => setOpen(true), size: "icon-sm", variant: "ghost", children: /* @__PURE__ */ jsx(Maximize2Icon, {}) }),
614
+ /* @__PURE__ */ jsx(DownloadButton, { block, size: "icon-sm" })
615
+ ] }),
616
+ previewLines ? /* @__PURE__ */ jsx(
617
+ "button",
618
+ {
619
+ className: cn(
620
+ "block w-full cursor-pointer px-3 py-2 text-left transition-colors hover:bg-accent/40"
621
+ ),
622
+ onClick: () => setOpen(true),
623
+ type: "button",
624
+ children: /* @__PURE__ */ jsx("pre", { className: "max-h-28 overflow-hidden whitespace-pre-wrap break-words font-mono text-[11px] leading-snug text-muted-foreground", children: previewLines })
625
+ }
626
+ ) : null,
627
+ open ? /* @__PURE__ */ jsx(DocumentViewer, { block, onClose: () => setOpen(false) }) : null
628
+ ] });
629
+ }
630
+ function TaskChecklist({ tasks }) {
631
+ if (!tasks || tasks.length === 0) return null;
632
+ const done = tasks.filter((task) => task.status === "done").length;
633
+ return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full rounded-xl border border-border bg-card p-3 shadow-xs", children: [
634
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-1.5 font-medium text-sm", children: [
635
+ /* @__PURE__ */ jsx(ListChecksIcon, { className: "size-4 text-muted-foreground" }),
636
+ "Tasks",
637
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-muted-foreground text-xs", children: [
638
+ done,
639
+ "/",
640
+ tasks.length
641
+ ] })
642
+ ] }),
643
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1.5", children: tasks.map((task, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 text-sm", children: [
644
+ task.status === "in_progress" ? /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5", children: /* @__PURE__ */ jsx(Spinner, {}) }) : /* @__PURE__ */ jsx(
645
+ Checkbox,
646
+ {
647
+ "aria-hidden": true,
648
+ checked: task.status === "done",
649
+ className: "pointer-events-none shrink-0",
650
+ readOnly: true,
651
+ tabIndex: -1
652
+ }
653
+ ),
654
+ /* @__PURE__ */ jsx(
655
+ "span",
656
+ {
657
+ className: cn(
658
+ "min-w-0",
659
+ task.status === "done" && "text-muted-foreground line-through",
660
+ task.status === "in_progress" && "font-medium"
661
+ ),
662
+ children: task.title
663
+ }
664
+ )
665
+ ] }, `${index}-${task.title}`)) })
666
+ ] });
667
+ }
668
+ function StepBlocks({ blocks, stepKey }) {
669
+ const visible = (blocks ?? []).filter(
670
+ (block) => block.type === "table" || block.type === "document" || block.type === "tasks"
671
+ );
672
+ if (visible.length === 0) return null;
673
+ return /* @__PURE__ */ jsx(Fragment, { children: visible.map((block, index) => {
674
+ const key = `${stepKey}-block-${index}`;
675
+ if (block.type === "document") {
676
+ return /* @__PURE__ */ jsx(DocumentCard, { block }, key);
677
+ }
678
+ if (block.type === "tasks") {
679
+ return /* @__PURE__ */ jsx(TaskChecklist, { tasks: block.tasks }, key);
680
+ }
681
+ const table = block;
682
+ const { columns, rows } = toDisplayTable(table);
363
683
  return /* @__PURE__ */ jsx("div", { className: "w-full max-w-full", children: /* @__PURE__ */ jsx(
364
684
  DataTablePaged,
365
685
  {
366
686
  columns,
367
687
  pageSize: 8,
368
688
  rows,
369
- totalCount: typeof block.pagination?.count === "number" ? block.pagination.count : block.row_count
689
+ totalCount: typeof table.pagination?.count === "number" ? table.pagination.count : table.row_count
370
690
  }
371
- ) }, `${stepKey}-table-${index}`);
691
+ ) }, key);
372
692
  }) });
373
693
  }
374
694
  function RawJsonDisclosure({ result }) {
@@ -400,7 +720,7 @@ function StepDetail({
400
720
  ] });
401
721
  }
402
722
  function ToolActivityGroup({ describeToolCall, steps }) {
403
- const [open, setOpen] = React3.useState(false);
723
+ const [open, setOpen] = React5.useState(false);
404
724
  if (steps.length === 0) return null;
405
725
  const last = steps[steps.length - 1];
406
726
  const anyRunning = steps.some((step) => step.status === "running");
@@ -430,7 +750,7 @@ function ToolActivityGroup({ describeToolCall, steps }) {
430
750
  ),
431
751
  /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-col gap-1.5", children: steps.map((step) => /* @__PURE__ */ jsx(StepDetail, { describeToolCall, step }, step.callId)) }) })
432
752
  ] }),
433
- steps.map((step) => /* @__PURE__ */ jsx(StepTables, { blocks: step.blocks, stepKey: step.callId }, `${step.callId}-tables`))
753
+ steps.map((step) => /* @__PURE__ */ jsx(StepBlocks, { blocks: step.blocks, stepKey: step.callId }, `${step.callId}-blocks`))
434
754
  ] });
435
755
  }
436
756
  function ToolApprovalCard({
@@ -503,21 +823,73 @@ function AgentPanel({
503
823
  onClose,
504
824
  onToggleAgentMode
505
825
  }) {
506
- const [draft, setDraft] = React3.useState("");
507
- const [view, setView] = React3.useState("chat");
826
+ const [draft, setDraft] = React5.useState("");
827
+ const [view, setView] = React5.useState("chat");
828
+ const [files, setFiles] = React5.useState([]);
829
+ const [uploadError, setUploadError] = React5.useState(null);
830
+ const [dragging, setDragging] = React5.useState(false);
831
+ const dragDepth = React5.useRef(0);
508
832
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
833
+ const uploadEnabled = fileUploadEnabled(config);
509
834
  const suggestions = config.suggestions ?? [
510
835
  "What can you help me with?",
511
836
  "Give me a quick summary of my data."
512
837
  ];
513
- const handleSubmit = React3.useCallback(
838
+ const addFiles = React5.useCallback(
839
+ (incoming) => {
840
+ const accepted = [];
841
+ let rejection = null;
842
+ for (const file of incoming) {
843
+ const error = validateFile(config, file);
844
+ if (error) {
845
+ rejection = error;
846
+ continue;
847
+ }
848
+ accepted.push(file);
849
+ }
850
+ setUploadError(rejection);
851
+ if (accepted.length) setFiles((current) => [...current, ...accepted].slice(0, 10));
852
+ },
853
+ [config]
854
+ );
855
+ const removeFile = React5.useCallback((index) => {
856
+ setFiles((current) => current.filter((_, i) => i !== index));
857
+ }, []);
858
+ const handleSubmit = React5.useCallback(
514
859
  (text) => {
860
+ const staged = files;
515
861
  setDraft("");
516
- void chat.send(text);
862
+ setFiles([]);
863
+ setUploadError(null);
864
+ void chat.send(text, staged);
517
865
  },
518
- [chat]
866
+ [chat, files]
519
867
  );
520
- const openHistory = React3.useCallback(() => {
868
+ const onDragEnter = React5.useCallback(
869
+ (event) => {
870
+ if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
871
+ dragDepth.current += 1;
872
+ setDragging(true);
873
+ },
874
+ [uploadEnabled]
875
+ );
876
+ const onDragLeave = React5.useCallback((event) => {
877
+ if (!event.dataTransfer?.types?.includes("Files")) return;
878
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
879
+ if (dragDepth.current === 0) setDragging(false);
880
+ }, []);
881
+ const onDrop = React5.useCallback(
882
+ (event) => {
883
+ if (!uploadEnabled) return;
884
+ event.preventDefault();
885
+ dragDepth.current = 0;
886
+ setDragging(false);
887
+ const dropped = Array.from(event.dataTransfer?.files ?? []);
888
+ if (dropped.length) addFiles(dropped);
889
+ },
890
+ [addFiles, uploadEnabled]
891
+ );
892
+ const openHistory = React5.useCallback(() => {
521
893
  setView("history");
522
894
  void chat.refreshConversations();
523
895
  }, [chat]);
@@ -526,11 +898,20 @@ function AgentPanel({
526
898
  "div",
527
899
  {
528
900
  className: cn(
529
- "flex min-h-0 flex-col overflow-hidden bg-background text-foreground",
901
+ "relative flex min-h-0 flex-col overflow-hidden bg-background text-foreground",
530
902
  className
531
903
  ),
532
904
  "data-slot": "agent-panel",
905
+ onDragEnter,
906
+ onDragLeave,
907
+ onDragOver: uploadEnabled ? (event) => event.preventDefault() : void 0,
908
+ onDrop,
533
909
  children: [
910
+ dragging && view === "chat" ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 z-40 flex flex-col items-center justify-center gap-2 bg-background/85 backdrop-blur-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 rounded-2xl border-2 border-primary/40 border-dashed px-8 py-6 text-center", children: [
911
+ /* @__PURE__ */ jsx(UploadCloudIcon, { className: "size-8 text-primary" }),
912
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: "Drop text files to attach" }),
913
+ /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-xs", children: acceptAttribute(config).replaceAll(",", " \xB7 ") })
914
+ ] }) }) : null,
534
915
  /* @__PURE__ */ jsxs(ChatHeader, { children: [
535
916
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2.5", children: [
536
917
  /* @__PURE__ */ jsx(ChatHeaderAvatar, {}),
@@ -671,15 +1052,30 @@ function AgentPanel({
671
1052
  )),
672
1053
  busy ? /* @__PURE__ */ jsx(ChatTypingIndicator, {}) : null
673
1054
  ] }) }),
674
- chat.error ? /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-1 flex items-center justify-between gap-2 rounded-lg bg-destructive/10 px-3 py-2 text-destructive text-xs", children: [
675
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: chat.error }),
676
- /* @__PURE__ */ jsx("button", { className: "shrink-0 cursor-pointer font-medium", onClick: chat.clearError, type: "button", children: "Dismiss" })
1055
+ chat.error || uploadError ? /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-1 flex items-center justify-between gap-2 rounded-lg bg-destructive/10 px-3 py-2 text-destructive text-xs", children: [
1056
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: chat.error ?? uploadError }),
1057
+ /* @__PURE__ */ jsx(
1058
+ "button",
1059
+ {
1060
+ className: "shrink-0 cursor-pointer font-medium",
1061
+ onClick: () => {
1062
+ chat.clearError();
1063
+ setUploadError(null);
1064
+ },
1065
+ type: "button",
1066
+ children: "Dismiss"
1067
+ }
1068
+ )
677
1069
  ] }) : null,
678
1070
  /* @__PURE__ */ jsx(
679
- ChatInput,
1071
+ AgentComposer,
680
1072
  {
1073
+ acceptAttribute: acceptAttribute(config),
681
1074
  className: cn(agentMode && "mx-auto w-full max-w-3xl"),
682
1075
  disabled: busy || chat.pendingApprovals.length > 0,
1076
+ files,
1077
+ onPickFiles: uploadEnabled ? addFiles : void 0,
1078
+ onRemoveFile: removeFile,
683
1079
  onSubmit: handleSubmit,
684
1080
  onValueChange: setDraft,
685
1081
  placeholder: chat.pendingApprovals.length > 0 ? "Approve or decline the pending action first\u2026" : `Ask ${agentName}\u2026`,
@@ -692,16 +1088,16 @@ function AgentPanel({
692
1088
  );
693
1089
  }
694
1090
  function useAgentChat(config) {
695
- const client = React3.useMemo(() => new AgentApiClient(config), [config]);
696
- const [agentInfo, setAgentInfo] = React3.useState(null);
697
- const [conversationId, setConversationId] = React3.useState(null);
698
- const [conversations, setConversations] = React3.useState([]);
699
- const [messages, setMessages] = React3.useState([]);
700
- const [pendingApprovals, setPendingApprovals] = React3.useState([]);
701
- const [sending, setSending] = React3.useState(false);
702
- const [resolvingApprovalId, setResolvingApprovalId] = React3.useState(null);
703
- const [error, setError] = React3.useState(null);
704
- React3.useEffect(() => {
1091
+ const client = React5.useMemo(() => new AgentApiClient(config), [config]);
1092
+ const [agentInfo, setAgentInfo] = React5.useState(null);
1093
+ const [conversationId, setConversationId] = React5.useState(null);
1094
+ const [conversations, setConversations] = React5.useState([]);
1095
+ const [messages, setMessages] = React5.useState([]);
1096
+ const [pendingApprovals, setPendingApprovals] = React5.useState([]);
1097
+ const [sending, setSending] = React5.useState(false);
1098
+ const [resolvingApprovalId, setResolvingApprovalId] = React5.useState(null);
1099
+ const [error, setError] = React5.useState(null);
1100
+ React5.useEffect(() => {
705
1101
  let cancelled = false;
706
1102
  client.getConfig().then((info) => {
707
1103
  if (!cancelled) setAgentInfo(info);
@@ -711,11 +1107,11 @@ function useAgentChat(config) {
711
1107
  cancelled = true;
712
1108
  };
713
1109
  }, [client]);
714
- const handleFailure = React3.useCallback((err) => {
1110
+ const handleFailure = React5.useCallback((err) => {
715
1111
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
716
1112
  setError(message);
717
1113
  }, []);
718
- const applyResponse = React3.useCallback(
1114
+ const applyResponse = React5.useCallback(
719
1115
  (response) => {
720
1116
  if (response.status === "error" && !response.messages?.length) {
721
1117
  setError(response.message ?? "The assistant could not answer.");
@@ -736,13 +1132,14 @@ function useAgentChat(config) {
736
1132
  },
737
1133
  []
738
1134
  );
739
- const send = React3.useCallback(
740
- async (text) => {
1135
+ const send = React5.useCallback(
1136
+ async (text, files) => {
741
1137
  if (sending) return;
1138
+ if (!text.trim() && !(files && files.length)) return;
742
1139
  setSending(true);
743
1140
  setError(null);
744
1141
  try {
745
- const response = await client.sendMessage(text, conversationId);
1142
+ const response = await client.sendMessage(text, conversationId, files);
746
1143
  applyResponse(response);
747
1144
  } catch (err) {
748
1145
  handleFailure(err);
@@ -752,7 +1149,7 @@ function useAgentChat(config) {
752
1149
  },
753
1150
  [applyResponse, client, conversationId, handleFailure, sending]
754
1151
  );
755
- const resolveApproval = React3.useCallback(
1152
+ const resolveApproval = React5.useCallback(
756
1153
  async (callId, approve) => {
757
1154
  if (resolvingApprovalId) return;
758
1155
  setResolvingApprovalId(callId);
@@ -768,13 +1165,13 @@ function useAgentChat(config) {
768
1165
  },
769
1166
  [applyResponse, client, handleFailure, resolvingApprovalId]
770
1167
  );
771
- const newChat = React3.useCallback(() => {
1168
+ const newChat = React5.useCallback(() => {
772
1169
  setConversationId(null);
773
1170
  setMessages([]);
774
1171
  setPendingApprovals([]);
775
1172
  setError(null);
776
1173
  }, []);
777
- const refreshConversations = React3.useCallback(async () => {
1174
+ const refreshConversations = React5.useCallback(async () => {
778
1175
  try {
779
1176
  const { conversations: list } = await client.listConversations();
780
1177
  setConversations(list);
@@ -782,7 +1179,7 @@ function useAgentChat(config) {
782
1179
  handleFailure(err);
783
1180
  }
784
1181
  }, [client, handleFailure]);
785
- const openConversation = React3.useCallback(
1182
+ const openConversation = React5.useCallback(
786
1183
  async (id) => {
787
1184
  setError(null);
788
1185
  try {
@@ -796,7 +1193,7 @@ function useAgentChat(config) {
796
1193
  },
797
1194
  [client, handleFailure]
798
1195
  );
799
- const deleteConversation = React3.useCallback(
1196
+ const deleteConversation = React5.useCallback(
800
1197
  async (id) => {
801
1198
  try {
802
1199
  await client.deleteConversation(id);
@@ -808,7 +1205,7 @@ function useAgentChat(config) {
808
1205
  },
809
1206
  [client, conversationId, handleFailure, newChat]
810
1207
  );
811
- const clearError = React3.useCallback(() => setError(null), []);
1208
+ const clearError = React5.useCallback(() => setError(null), []);
812
1209
  return {
813
1210
  agentInfo,
814
1211
  clearError,
@@ -843,11 +1240,11 @@ function readStored(clientId) {
843
1240
  }
844
1241
  }
845
1242
  function useAlwaysApprove(clientId) {
846
- const [approved, setApproved] = React3.useState(() => /* @__PURE__ */ new Set());
847
- React3.useEffect(() => {
1243
+ const [approved, setApproved] = React5.useState(() => /* @__PURE__ */ new Set());
1244
+ React5.useEffect(() => {
848
1245
  setApproved(readStored(clientId));
849
1246
  }, [clientId]);
850
- const persist = React3.useCallback(
1247
+ const persist = React5.useCallback(
851
1248
  (next) => {
852
1249
  setApproved(next);
853
1250
  if (typeof window === "undefined") return;
@@ -858,8 +1255,8 @@ function useAlwaysApprove(clientId) {
858
1255
  },
859
1256
  [clientId]
860
1257
  );
861
- const isAlwaysApproved = React3.useCallback((toolName) => approved.has(toolName), [approved]);
862
- const setAlwaysApprove = React3.useCallback(
1258
+ const isAlwaysApproved = React5.useCallback((toolName) => approved.has(toolName), [approved]);
1259
+ const setAlwaysApprove = React5.useCallback(
863
1260
  (toolName) => {
864
1261
  if (approved.has(toolName)) return;
865
1262
  const next = new Set(approved);
@@ -868,7 +1265,7 @@ function useAlwaysApprove(clientId) {
868
1265
  },
869
1266
  [approved, persist]
870
1267
  );
871
- const clearAlwaysApprove = React3.useCallback(
1268
+ const clearAlwaysApprove = React5.useCallback(
872
1269
  (toolName) => {
873
1270
  if (!approved.has(toolName)) return;
874
1271
  const next = new Set(approved);
@@ -880,16 +1277,16 @@ function useAlwaysApprove(clientId) {
880
1277
  return { clearAlwaysApprove, isAlwaysApproved, setAlwaysApprove };
881
1278
  }
882
1279
  function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
883
- const [uncontrolledOpen, setUncontrolledOpen] = React3.useState(false);
1280
+ const [uncontrolledOpen, setUncontrolledOpen] = React5.useState(false);
884
1281
  const open = controlledOpen ?? uncontrolledOpen;
885
- const setOpen = React3.useCallback(
1282
+ const setOpen = React5.useCallback(
886
1283
  (next) => {
887
1284
  setUncontrolledOpen(next);
888
1285
  onOpenChange?.(next);
889
1286
  },
890
1287
  [onOpenChange]
891
1288
  );
892
- const [agentMode, setAgentMode] = React3.useState(false);
1289
+ const [agentMode, setAgentMode] = React5.useState(false);
893
1290
  const isDesktop = useMediaQuery("(min-width: 640px)", {
894
1291
  defaultSSRValue: true,
895
1292
  ssr: true
@@ -899,14 +1296,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
899
1296
  const alwaysApprove = useAlwaysApprove(config.clientId);
900
1297
  const { pendingApprovals, resolveApproval, resolvingApprovalId } = chat;
901
1298
  const { isAlwaysApproved } = alwaysApprove;
902
- React3.useEffect(() => {
1299
+ React5.useEffect(() => {
903
1300
  if (!open || resolvingApprovalId) return;
904
1301
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
905
1302
  if (target) void resolveApproval(target.id, true);
906
1303
  }, [open, pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
907
- const lastNavigatedMessageId = React3.useRef(null);
1304
+ const lastNavigatedMessageId = React5.useRef(null);
908
1305
  const { messages } = chat;
909
- React3.useEffect(() => {
1306
+ React5.useEffect(() => {
910
1307
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
911
1308
  const toolMessages = messages.filter((message) => message.role === "tool");
912
1309
  const latest = toolMessages[toolMessages.length - 1];
@@ -918,7 +1315,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
918
1315
  const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
919
1316
  if (path) config.navigate(path);
920
1317
  }, [config, fullscreen, messages, open]);
921
- const closePanel = React3.useCallback(() => {
1318
+ const closePanel = React5.useCallback(() => {
922
1319
  setOpen(false);
923
1320
  setAgentMode(false);
924
1321
  }, [setOpen]);
@@ -997,6 +1394,6 @@ function defineAgentConfig(config) {
997
1394
  return config;
998
1395
  }
999
1396
 
1000
- export { AgentApiClient, AgentApiError, AgentPanel, AgentWidget, MessageItem, ToolActivityGroup, ToolApprovalCard, defineAgentConfig, deriveDetails, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove };
1397
+ export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
1001
1398
  //# sourceMappingURL=index.js.map
1002
1399
  //# sourceMappingURL=index.js.map