@wallavi/widget 1.6.0 → 1.6.1

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.d.mts CHANGED
@@ -32,6 +32,8 @@ type Message = {
32
32
  id: string;
33
33
  role: "user" | "assistant";
34
34
  parts: MessagePart[];
35
+ /** Files the user attached to this message — stored for display in the chat thread. */
36
+ attachments?: AttachmentPayload[];
35
37
  };
36
38
  interface PageContext {
37
39
  /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
@@ -101,9 +103,9 @@ interface ChatWidgetConfig {
101
103
  */
102
104
  voiceAutoSend?: boolean;
103
105
  /**
104
- * Show the paperclip button to attach files (CSV, TXT, images).
105
- * Text files are extracted and injected as context into the pipeline.
106
- * Images are accepted and stored; persistent URLs require Phase 2 storage config.
106
+ * Show the paperclip button and enable drag-and-drop / clipboard-paste for file attachments.
107
+ * Supported types: CSV, TXT, PDF (text extracted as context), JPG, PNG, WebP, GIF (stored in Vercel Blob).
108
+ * Default: false.
107
109
  */
108
110
  enableAttachments?: boolean;
109
111
  }
@@ -125,6 +127,8 @@ interface AttachmentPayload {
125
127
  contentType: AttachmentContentType;
126
128
  textContent?: string;
127
129
  base64?: string;
130
+ /** Persistent public URL — set by upload.ts when Vercel Blob is configured. */
131
+ url?: string;
128
132
  rowCount?: number;
129
133
  truncated?: boolean;
130
134
  sizeBytes: number;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ type Message = {
32
32
  id: string;
33
33
  role: "user" | "assistant";
34
34
  parts: MessagePart[];
35
+ /** Files the user attached to this message — stored for display in the chat thread. */
36
+ attachments?: AttachmentPayload[];
35
37
  };
36
38
  interface PageContext {
37
39
  /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
@@ -101,9 +103,9 @@ interface ChatWidgetConfig {
101
103
  */
102
104
  voiceAutoSend?: boolean;
103
105
  /**
104
- * Show the paperclip button to attach files (CSV, TXT, images).
105
- * Text files are extracted and injected as context into the pipeline.
106
- * Images are accepted and stored; persistent URLs require Phase 2 storage config.
106
+ * Show the paperclip button and enable drag-and-drop / clipboard-paste for file attachments.
107
+ * Supported types: CSV, TXT, PDF (text extracted as context), JPG, PNG, WebP, GIF (stored in Vercel Blob).
108
+ * Default: false.
107
109
  */
108
110
  enableAttachments?: boolean;
109
111
  }
@@ -125,6 +127,8 @@ interface AttachmentPayload {
125
127
  contentType: AttachmentContentType;
126
128
  textContent?: string;
127
129
  base64?: string;
130
+ /** Persistent public URL — set by upload.ts when Vercel Blob is configured. */
131
+ url?: string;
128
132
  rowCount?: number;
129
133
  truncated?: boolean;
130
134
  sizeBytes: number;
package/dist/index.js CHANGED
@@ -254,17 +254,22 @@ function useChat({
254
254
  const userInput = (text ?? input).trim();
255
255
  if (!userInput || streaming) return;
256
256
  setInput("");
257
+ const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
258
+ pendingAttachmentsRef.current = [];
257
259
  const userMsgId = newId();
258
260
  setMessages((prev) => [
259
261
  ...prev,
260
- { id: userMsgId, role: "user", parts: [{ type: "text", text: userInput }] }
262
+ {
263
+ id: userMsgId,
264
+ role: "user",
265
+ parts: [{ type: "text", text: userInput }],
266
+ ...attachments ? { attachments } : {}
267
+ }
261
268
  ]);
262
269
  setStreaming(true);
263
270
  const assistantMsgId = newId();
264
271
  streamingMsgIdRef.current = assistantMsgId;
265
272
  setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
266
- const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
267
- pendingAttachmentsRef.current = [];
268
273
  try {
269
274
  await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
270
275
  } catch {
@@ -606,10 +611,112 @@ function ChatHeader({
606
611
  }
607
612
  );
608
613
  }
614
+ var cn2 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
615
+ function formatBytes(bytes) {
616
+ if (bytes < 1024) return `${bytes} B`;
617
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
618
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
619
+ }
620
+ function ChipLeading({ a }) {
621
+ if (a.status === "uploading") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 shrink-0 animate-spin" });
622
+ if (a.status === "error") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 shrink-0" });
623
+ const thumbSrc = a.payload?.url ?? a.payload?.base64;
624
+ if (a.mimeType.startsWith("image/") && thumbSrc) {
625
+ return /* @__PURE__ */ jsxRuntime.jsx(
626
+ "img",
627
+ {
628
+ src: thumbSrc,
629
+ alt: a.name,
630
+ className: "h-6 w-6 rounded object-cover shrink-0 border border-border/40"
631
+ }
632
+ );
633
+ }
634
+ if (a.mimeType.startsWith("image/")) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-6 rounded bg-muted-foreground/10 border border-border/30 shrink-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[8px] font-semibold text-muted-foreground/50", children: "IMG" }) });
635
+ if (a.mimeType === "application/pdf") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0 text-red-400" });
636
+ if (a.mimeType.includes("csv") || a.mimeType.includes("spreadsheet") || a.name.endsWith(".csv")) {
637
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileSpreadsheet, { className: "h-3 w-3 shrink-0 text-emerald-500" });
638
+ }
639
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0" });
640
+ }
641
+ function AttachmentChips({ attachments, onRemove }) {
642
+ if (attachments.length === 0) return null;
643
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
644
+ "div",
645
+ {
646
+ className: cn2(
647
+ "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[200px]",
648
+ a.status === "error" ? "border-red-200 bg-red-50 text-red-600 dark:border-red-800 dark:bg-red-950 dark:text-red-400" : "border-border bg-muted/60 text-muted-foreground"
649
+ ),
650
+ children: [
651
+ /* @__PURE__ */ jsxRuntime.jsx(ChipLeading, { a }),
652
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
653
+ a.status === "ready" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground/50", children: formatBytes(a.sizeBytes) }),
654
+ /* @__PURE__ */ jsxRuntime.jsx(
655
+ "button",
656
+ {
657
+ type: "button",
658
+ onClick: () => onRemove(a.id),
659
+ className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors ml-0.5",
660
+ title: "Remove",
661
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-2.5 w-2.5" })
662
+ }
663
+ )
664
+ ]
665
+ },
666
+ a.id
667
+ )) });
668
+ }
609
669
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
610
670
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
611
671
  var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
612
672
  var ReactMarkdown = ReactMarkdownLib__default.default;
673
+ function SentAttachments({ attachments, contrastColor }) {
674
+ const images = attachments.filter((a) => a.contentType === "image");
675
+ const files = attachments.filter((a) => a.contentType !== "image");
676
+ const isDark = contrastColor === "#ffffff" || contrastColor === "#fff";
677
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [
678
+ images.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex gap-1.5 flex-wrap", images.length === 1 && ""), children: images.map((img) => {
679
+ const src = img.url ?? img.base64;
680
+ return src ? /* @__PURE__ */ jsxRuntime.jsx(
681
+ "img",
682
+ {
683
+ src,
684
+ alt: img.name,
685
+ className: cn(
686
+ "rounded-xl object-cover border",
687
+ images.length === 1 ? "w-full max-h-48" : "h-20 w-20",
688
+ isDark ? "border-white/20" : "border-black/10"
689
+ )
690
+ },
691
+ img.id
692
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
693
+ "div",
694
+ {
695
+ className: cn(
696
+ "h-20 w-20 rounded-xl flex items-center justify-center text-[10px] font-medium",
697
+ isDark ? "bg-white/10 text-white/60" : "bg-black/10 text-black/40"
698
+ ),
699
+ children: "IMG"
700
+ },
701
+ img.id
702
+ );
703
+ }) }),
704
+ files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
705
+ "div",
706
+ {
707
+ className: cn(
708
+ "flex items-center gap-1 rounded-lg px-2 py-1 text-[11px]",
709
+ isDark ? "bg-white/15 text-white/80" : "bg-black/10 text-black/60"
710
+ ),
711
+ children: [
712
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate max-w-[120px]", children: f.name }),
713
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 shrink-0", children: formatBytes(f.sizeBytes) })
714
+ ]
715
+ },
716
+ f.id
717
+ )) })
718
+ ] });
719
+ }
613
720
  function ThinkingDots() {
614
721
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
615
722
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
@@ -800,12 +907,15 @@ function MessageBubble({
800
907
  const pickerParts = message.parts.filter((p) => p.type === "picker");
801
908
  const contrastColor = getContrastColor(userColor);
802
909
  if (isUser) {
803
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
910
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
804
911
  "div",
805
912
  {
806
- className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed",
913
+ className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed flex flex-col gap-2",
807
914
  style: { backgroundColor: userColor, color: contrastColor },
808
- children: textPart?.text
915
+ children: [
916
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(SentAttachments, { attachments: message.attachments, contrastColor }),
917
+ textPart?.text && /* @__PURE__ */ jsxRuntime.jsx("span", { children: textPart.text })
918
+ ]
809
919
  }
810
920
  ) });
811
921
  }
@@ -910,44 +1020,6 @@ function ChatMessages({
910
1020
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: bottomRef })
911
1021
  ] });
912
1022
  }
913
- var cn2 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
914
- function formatBytes(bytes) {
915
- if (bytes < 1024) return `${bytes} B`;
916
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
917
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
918
- }
919
- function FileIcon({ mimeType }) {
920
- if (mimeType.startsWith("image/")) return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImageIcon, { className: "h-3 w-3 shrink-0" });
921
- return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0" });
922
- }
923
- function AttachmentChips({ attachments, onRemove }) {
924
- if (attachments.length === 0) return null;
925
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
926
- "div",
927
- {
928
- className: cn2(
929
- "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[180px]",
930
- a.status === "error" ? "border-red-200 bg-red-50 text-red-600 dark:border-red-800 dark:bg-red-950 dark:text-red-400" : "border-border bg-muted/60 text-muted-foreground"
931
- ),
932
- children: [
933
- a.status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }) : a.status === "error" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { mimeType: a.mimeType }),
934
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
935
- a.status === "ready" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground/60", children: formatBytes(a.sizeBytes) }),
936
- /* @__PURE__ */ jsxRuntime.jsx(
937
- "button",
938
- {
939
- type: "button",
940
- onClick: () => onRemove(a.id),
941
- className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors",
942
- title: "Remove attachment",
943
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-2.5 w-2.5" })
944
- }
945
- )
946
- ]
947
- },
948
- a.id
949
- )) });
950
- }
951
1023
  var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
952
1024
  function ChatInput({
953
1025
  input,
@@ -1034,6 +1106,18 @@ function ChatInput({
1034
1106
  e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
1035
1107
  },
1036
1108
  onKeyDown: handleKeyDown,
1109
+ onPaste: (e) => {
1110
+ if (!hasAttachments || !onAttach) return;
1111
+ const files = Array.from(e.clipboardData?.files ?? []).filter(
1112
+ (f) => f.type.startsWith("image/")
1113
+ );
1114
+ if (files.length > 0) {
1115
+ e.preventDefault();
1116
+ const dt = new DataTransfer();
1117
+ files.forEach((f) => dt.items.add(f));
1118
+ onAttach(dt.files);
1119
+ }
1120
+ },
1037
1121
  disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
1038
1122
  autoFocus: true
1039
1123
  }
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
2
- import { UploadCloud, X, RotateCcw, Loader2, Square, Mic, Paperclip, ArrowUp, AlertCircle, Zap, ChevronDown, CheckCircle2, Search, Check, ImageIcon, FileText } from 'lucide-react';
2
+ import { UploadCloud, X, RotateCcw, Loader2, Square, Mic, Paperclip, ArrowUp, Zap, ChevronDown, CheckCircle2, AlertCircle, Search, Check, FileText, FileSpreadsheet } from 'lucide-react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
@@ -228,17 +228,22 @@ function useChat({
228
228
  const userInput = (text ?? input).trim();
229
229
  if (!userInput || streaming) return;
230
230
  setInput("");
231
+ const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
232
+ pendingAttachmentsRef.current = [];
231
233
  const userMsgId = newId();
232
234
  setMessages((prev) => [
233
235
  ...prev,
234
- { id: userMsgId, role: "user", parts: [{ type: "text", text: userInput }] }
236
+ {
237
+ id: userMsgId,
238
+ role: "user",
239
+ parts: [{ type: "text", text: userInput }],
240
+ ...attachments ? { attachments } : {}
241
+ }
235
242
  ]);
236
243
  setStreaming(true);
237
244
  const assistantMsgId = newId();
238
245
  streamingMsgIdRef.current = assistantMsgId;
239
246
  setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
240
- const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
241
- pendingAttachmentsRef.current = [];
242
247
  try {
243
248
  await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
244
249
  } catch {
@@ -580,10 +585,112 @@ function ChatHeader({
580
585
  }
581
586
  );
582
587
  }
588
+ var cn2 = (...inputs) => twMerge(clsx(inputs));
589
+ function formatBytes(bytes) {
590
+ if (bytes < 1024) return `${bytes} B`;
591
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
592
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
593
+ }
594
+ function ChipLeading({ a }) {
595
+ if (a.status === "uploading") return /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" });
596
+ if (a.status === "error") return /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" });
597
+ const thumbSrc = a.payload?.url ?? a.payload?.base64;
598
+ if (a.mimeType.startsWith("image/") && thumbSrc) {
599
+ return /* @__PURE__ */ jsx(
600
+ "img",
601
+ {
602
+ src: thumbSrc,
603
+ alt: a.name,
604
+ className: "h-6 w-6 rounded object-cover shrink-0 border border-border/40"
605
+ }
606
+ );
607
+ }
608
+ if (a.mimeType.startsWith("image/")) return /* @__PURE__ */ jsx("div", { className: "h-6 w-6 rounded bg-muted-foreground/10 border border-border/30 shrink-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-[8px] font-semibold text-muted-foreground/50", children: "IMG" }) });
609
+ if (a.mimeType === "application/pdf") return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0 text-red-400" });
610
+ if (a.mimeType.includes("csv") || a.mimeType.includes("spreadsheet") || a.name.endsWith(".csv")) {
611
+ return /* @__PURE__ */ jsx(FileSpreadsheet, { className: "h-3 w-3 shrink-0 text-emerald-500" });
612
+ }
613
+ return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0" });
614
+ }
615
+ function AttachmentChips({ attachments, onRemove }) {
616
+ if (attachments.length === 0) return null;
617
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxs(
618
+ "div",
619
+ {
620
+ className: cn2(
621
+ "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[200px]",
622
+ a.status === "error" ? "border-red-200 bg-red-50 text-red-600 dark:border-red-800 dark:bg-red-950 dark:text-red-400" : "border-border bg-muted/60 text-muted-foreground"
623
+ ),
624
+ children: [
625
+ /* @__PURE__ */ jsx(ChipLeading, { a }),
626
+ /* @__PURE__ */ jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
627
+ a.status === "ready" && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/50", children: formatBytes(a.sizeBytes) }),
628
+ /* @__PURE__ */ jsx(
629
+ "button",
630
+ {
631
+ type: "button",
632
+ onClick: () => onRemove(a.id),
633
+ className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors ml-0.5",
634
+ title: "Remove",
635
+ children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
636
+ }
637
+ )
638
+ ]
639
+ },
640
+ a.id
641
+ )) });
642
+ }
583
643
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
584
644
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
585
645
  var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
586
646
  var ReactMarkdown = ReactMarkdownLib;
647
+ function SentAttachments({ attachments, contrastColor }) {
648
+ const images = attachments.filter((a) => a.contentType === "image");
649
+ const files = attachments.filter((a) => a.contentType !== "image");
650
+ const isDark = contrastColor === "#ffffff" || contrastColor === "#fff";
651
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [
652
+ images.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex gap-1.5 flex-wrap", images.length === 1 && ""), children: images.map((img) => {
653
+ const src = img.url ?? img.base64;
654
+ return src ? /* @__PURE__ */ jsx(
655
+ "img",
656
+ {
657
+ src,
658
+ alt: img.name,
659
+ className: cn(
660
+ "rounded-xl object-cover border",
661
+ images.length === 1 ? "w-full max-h-48" : "h-20 w-20",
662
+ isDark ? "border-white/20" : "border-black/10"
663
+ )
664
+ },
665
+ img.id
666
+ ) : /* @__PURE__ */ jsx(
667
+ "div",
668
+ {
669
+ className: cn(
670
+ "h-20 w-20 rounded-xl flex items-center justify-center text-[10px] font-medium",
671
+ isDark ? "bg-white/10 text-white/60" : "bg-black/10 text-black/40"
672
+ ),
673
+ children: "IMG"
674
+ },
675
+ img.id
676
+ );
677
+ }) }),
678
+ files.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: files.map((f) => /* @__PURE__ */ jsxs(
679
+ "div",
680
+ {
681
+ className: cn(
682
+ "flex items-center gap-1 rounded-lg px-2 py-1 text-[11px]",
683
+ isDark ? "bg-white/15 text-white/80" : "bg-black/10 text-black/60"
684
+ ),
685
+ children: [
686
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: f.name }),
687
+ /* @__PURE__ */ jsx("span", { className: "opacity-60 shrink-0", children: formatBytes(f.sizeBytes) })
688
+ ]
689
+ },
690
+ f.id
691
+ )) })
692
+ ] });
693
+ }
587
694
  function ThinkingDots() {
588
695
  return /* @__PURE__ */ jsxs(Fragment, { children: [
589
696
  /* @__PURE__ */ jsx("style", { children: `
@@ -774,12 +881,15 @@ function MessageBubble({
774
881
  const pickerParts = message.parts.filter((p) => p.type === "picker");
775
882
  const contrastColor = getContrastColor(userColor);
776
883
  if (isUser) {
777
- return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
884
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
778
885
  "div",
779
886
  {
780
- className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed",
887
+ className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed flex flex-col gap-2",
781
888
  style: { backgroundColor: userColor, color: contrastColor },
782
- children: textPart?.text
889
+ children: [
890
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx(SentAttachments, { attachments: message.attachments, contrastColor }),
891
+ textPart?.text && /* @__PURE__ */ jsx("span", { children: textPart.text })
892
+ ]
783
893
  }
784
894
  ) });
785
895
  }
@@ -884,44 +994,6 @@ function ChatMessages({
884
994
  /* @__PURE__ */ jsx("div", { ref: bottomRef })
885
995
  ] });
886
996
  }
887
- var cn2 = (...inputs) => twMerge(clsx(inputs));
888
- function formatBytes(bytes) {
889
- if (bytes < 1024) return `${bytes} B`;
890
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
891
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
892
- }
893
- function FileIcon({ mimeType }) {
894
- if (mimeType.startsWith("image/")) return /* @__PURE__ */ jsx(ImageIcon, { className: "h-3 w-3 shrink-0" });
895
- return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0" });
896
- }
897
- function AttachmentChips({ attachments, onRemove }) {
898
- if (attachments.length === 0) return null;
899
- return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxs(
900
- "div",
901
- {
902
- className: cn2(
903
- "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[180px]",
904
- a.status === "error" ? "border-red-200 bg-red-50 text-red-600 dark:border-red-800 dark:bg-red-950 dark:text-red-400" : "border-border bg-muted/60 text-muted-foreground"
905
- ),
906
- children: [
907
- a.status === "uploading" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }) : a.status === "error" ? /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(FileIcon, { mimeType: a.mimeType }),
908
- /* @__PURE__ */ jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
909
- a.status === "ready" && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: formatBytes(a.sizeBytes) }),
910
- /* @__PURE__ */ jsx(
911
- "button",
912
- {
913
- type: "button",
914
- onClick: () => onRemove(a.id),
915
- className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors",
916
- title: "Remove attachment",
917
- children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
918
- }
919
- )
920
- ]
921
- },
922
- a.id
923
- )) });
924
- }
925
997
  var cn3 = (...inputs) => twMerge(clsx(inputs));
926
998
  function ChatInput({
927
999
  input,
@@ -1008,6 +1080,18 @@ function ChatInput({
1008
1080
  e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
1009
1081
  },
1010
1082
  onKeyDown: handleKeyDown,
1083
+ onPaste: (e) => {
1084
+ if (!hasAttachments || !onAttach) return;
1085
+ const files = Array.from(e.clipboardData?.files ?? []).filter(
1086
+ (f) => f.type.startsWith("image/")
1087
+ );
1088
+ if (files.length > 0) {
1089
+ e.preventDefault();
1090
+ const dt = new DataTransfer();
1091
+ files.forEach((f) => dt.items.add(f));
1092
+ onAttach(dt.files);
1093
+ }
1094
+ },
1011
1095
  disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
1012
1096
  autoFocus: true
1013
1097
  }
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "private": false,
35
35
  "types": "./dist/index.d.ts",
36
- "version": "1.6.0",
36
+ "version": "1.6.1",
37
37
  "scripts": {
38
38
  "build": "tsup",
39
39
  "typecheck": "tsc --noEmit"