@wallavi/widget 1.6.0 → 1.6.2

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
@@ -21,17 +21,31 @@ type PickerPart = {
21
21
  }>;
22
22
  selectedValue?: string;
23
23
  };
24
+ type PlanStepState = {
25
+ index: number;
26
+ description: string;
27
+ status: "pending" | "executing" | "success" | "failed";
28
+ error?: string;
29
+ };
30
+ type PlanPart = {
31
+ type: "plan";
32
+ planId: string;
33
+ goal: string;
34
+ steps: PlanStepState[];
35
+ };
24
36
  type MessagePart = {
25
37
  type: "text";
26
38
  text: string;
27
39
  } | {
28
40
  type: "reasoning";
29
41
  text: string;
30
- } | ToolPart | PickerPart;
42
+ } | ToolPart | PickerPart | PlanPart;
31
43
  type Message = {
32
44
  id: string;
33
45
  role: "user" | "assistant";
34
46
  parts: MessagePart[];
47
+ /** Files the user attached to this message — stored for display in the chat thread. */
48
+ attachments?: AttachmentPayload[];
35
49
  };
36
50
  interface PageContext {
37
51
  /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
@@ -101,9 +115,9 @@ interface ChatWidgetConfig {
101
115
  */
102
116
  voiceAutoSend?: boolean;
103
117
  /**
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.
118
+ * Show the paperclip button and enable drag-and-drop / clipboard-paste for file attachments.
119
+ * Supported types: CSV, TXT, PDF (text extracted as context), JPG, PNG, WebP, GIF (stored in Vercel Blob).
120
+ * Default: false.
107
121
  */
108
122
  enableAttachments?: boolean;
109
123
  }
@@ -125,6 +139,8 @@ interface AttachmentPayload {
125
139
  contentType: AttachmentContentType;
126
140
  textContent?: string;
127
141
  base64?: string;
142
+ /** Persistent public URL — set by upload.ts when Vercel Blob is configured. */
143
+ url?: string;
128
144
  rowCount?: number;
129
145
  truncated?: boolean;
130
146
  sizeBytes: number;
package/dist/index.d.ts CHANGED
@@ -21,17 +21,31 @@ type PickerPart = {
21
21
  }>;
22
22
  selectedValue?: string;
23
23
  };
24
+ type PlanStepState = {
25
+ index: number;
26
+ description: string;
27
+ status: "pending" | "executing" | "success" | "failed";
28
+ error?: string;
29
+ };
30
+ type PlanPart = {
31
+ type: "plan";
32
+ planId: string;
33
+ goal: string;
34
+ steps: PlanStepState[];
35
+ };
24
36
  type MessagePart = {
25
37
  type: "text";
26
38
  text: string;
27
39
  } | {
28
40
  type: "reasoning";
29
41
  text: string;
30
- } | ToolPart | PickerPart;
42
+ } | ToolPart | PickerPart | PlanPart;
31
43
  type Message = {
32
44
  id: string;
33
45
  role: "user" | "assistant";
34
46
  parts: MessagePart[];
47
+ /** Files the user attached to this message — stored for display in the chat thread. */
48
+ attachments?: AttachmentPayload[];
35
49
  };
36
50
  interface PageContext {
37
51
  /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
@@ -101,9 +115,9 @@ interface ChatWidgetConfig {
101
115
  */
102
116
  voiceAutoSend?: boolean;
103
117
  /**
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.
118
+ * Show the paperclip button and enable drag-and-drop / clipboard-paste for file attachments.
119
+ * Supported types: CSV, TXT, PDF (text extracted as context), JPG, PNG, WebP, GIF (stored in Vercel Blob).
120
+ * Default: false.
107
121
  */
108
122
  enableAttachments?: boolean;
109
123
  }
@@ -125,6 +139,8 @@ interface AttachmentPayload {
125
139
  contentType: AttachmentContentType;
126
140
  textContent?: string;
127
141
  base64?: string;
142
+ /** Persistent public URL — set by upload.ts when Vercel Blob is configured. */
143
+ url?: string;
128
144
  rowCount?: number;
129
145
  truncated?: boolean;
130
146
  sizeBytes: number;
package/dist/index.js CHANGED
@@ -205,6 +205,30 @@ function useChat({
205
205
  });
206
206
  break;
207
207
  }
208
+ case "plan-created": {
209
+ msg.parts.push({
210
+ type: "plan",
211
+ planId: proto.planId,
212
+ goal: proto.goal,
213
+ steps: proto.steps.map((s) => ({ ...s, status: "pending" }))
214
+ });
215
+ break;
216
+ }
217
+ case "plan-step-update": {
218
+ const pIdx = msg.parts.findIndex(
219
+ (p) => p.type === "plan" && p.planId === proto.planId
220
+ );
221
+ if (pIdx !== -1) {
222
+ const prev2 = msg.parts[pIdx];
223
+ msg.parts[pIdx] = {
224
+ ...prev2,
225
+ steps: prev2.steps.map(
226
+ (s) => s.index === proto.stepIndex ? { ...s, status: proto.status, ...proto.error ? { error: proto.error } : {} } : s
227
+ )
228
+ };
229
+ }
230
+ break;
231
+ }
208
232
  }
209
233
  const copy = [...prev];
210
234
  copy[idx] = msg;
@@ -254,17 +278,22 @@ function useChat({
254
278
  const userInput = (text ?? input).trim();
255
279
  if (!userInput || streaming) return;
256
280
  setInput("");
281
+ const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
282
+ pendingAttachmentsRef.current = [];
257
283
  const userMsgId = newId();
258
284
  setMessages((prev) => [
259
285
  ...prev,
260
- { id: userMsgId, role: "user", parts: [{ type: "text", text: userInput }] }
286
+ {
287
+ id: userMsgId,
288
+ role: "user",
289
+ parts: [{ type: "text", text: userInput }],
290
+ ...attachments ? { attachments } : {}
291
+ }
261
292
  ]);
262
293
  setStreaming(true);
263
294
  const assistantMsgId = newId();
264
295
  streamingMsgIdRef.current = assistantMsgId;
265
296
  setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
266
- const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
267
- pendingAttachmentsRef.current = [];
268
297
  try {
269
298
  await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
270
299
  } catch {
@@ -606,10 +635,166 @@ function ChatHeader({
606
635
  }
607
636
  );
608
637
  }
638
+ var cn2 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
639
+ function formatBytes(bytes) {
640
+ if (bytes < 1024) return `${bytes} B`;
641
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
642
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
643
+ }
644
+ function ChipLeading({ a }) {
645
+ if (a.status === "uploading") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 shrink-0 animate-spin" });
646
+ if (a.status === "error") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 shrink-0" });
647
+ const thumbSrc = a.payload?.url ?? a.payload?.base64;
648
+ if (a.mimeType.startsWith("image/") && thumbSrc) {
649
+ return /* @__PURE__ */ jsxRuntime.jsx(
650
+ "img",
651
+ {
652
+ src: thumbSrc,
653
+ alt: a.name,
654
+ className: "h-6 w-6 rounded object-cover shrink-0 border border-border/40"
655
+ }
656
+ );
657
+ }
658
+ 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" }) });
659
+ if (a.mimeType === "application/pdf") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0 text-red-400" });
660
+ if (a.mimeType.includes("csv") || a.mimeType.includes("spreadsheet") || a.name.endsWith(".csv")) {
661
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileSpreadsheet, { className: "h-3 w-3 shrink-0 text-emerald-500" });
662
+ }
663
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0" });
664
+ }
665
+ function AttachmentChips({ attachments, onRemove }) {
666
+ if (attachments.length === 0) return null;
667
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
668
+ "div",
669
+ {
670
+ className: cn2(
671
+ "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[200px]",
672
+ 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"
673
+ ),
674
+ children: [
675
+ /* @__PURE__ */ jsxRuntime.jsx(ChipLeading, { a }),
676
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
677
+ a.status === "ready" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground/50", children: formatBytes(a.sizeBytes) }),
678
+ /* @__PURE__ */ jsxRuntime.jsx(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ onClick: () => onRemove(a.id),
683
+ className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors ml-0.5",
684
+ title: "Remove",
685
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-2.5 w-2.5" })
686
+ }
687
+ )
688
+ ]
689
+ },
690
+ a.id
691
+ )) });
692
+ }
609
693
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
610
694
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
611
695
  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
696
  var ReactMarkdown = ReactMarkdownLib__default.default;
697
+ function SentAttachments({ attachments, contrastColor }) {
698
+ const images = attachments.filter((a) => a.contentType === "image");
699
+ const files = attachments.filter((a) => a.contentType !== "image");
700
+ const isDark = contrastColor === "#ffffff" || contrastColor === "#fff";
701
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [
702
+ images.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex gap-1.5 flex-wrap", images.length === 1 && ""), children: images.map((img) => {
703
+ const src = img.url ?? img.base64;
704
+ return src ? /* @__PURE__ */ jsxRuntime.jsx(
705
+ "img",
706
+ {
707
+ src,
708
+ alt: img.name,
709
+ className: cn(
710
+ "rounded-xl object-cover border",
711
+ images.length === 1 ? "w-full max-h-48" : "h-20 w-20",
712
+ isDark ? "border-white/20" : "border-black/10"
713
+ )
714
+ },
715
+ img.id
716
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
717
+ "div",
718
+ {
719
+ className: cn(
720
+ "h-20 w-20 rounded-xl flex items-center justify-center text-[10px] font-medium",
721
+ isDark ? "bg-white/10 text-white/60" : "bg-black/10 text-black/40"
722
+ ),
723
+ children: "IMG"
724
+ },
725
+ img.id
726
+ );
727
+ }) }),
728
+ files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
729
+ "div",
730
+ {
731
+ className: cn(
732
+ "flex items-center gap-1 rounded-lg px-2 py-1 text-[11px]",
733
+ isDark ? "bg-white/15 text-white/80" : "bg-black/10 text-black/60"
734
+ ),
735
+ children: [
736
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate max-w-[120px]", children: f.name }),
737
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 shrink-0", children: formatBytes(f.sizeBytes) })
738
+ ]
739
+ },
740
+ f.id
741
+ )) })
742
+ ] });
743
+ }
744
+ function PlanStepIcon({ status }) {
745
+ if (status === "executing") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 animate-spin text-primary" });
746
+ if (status === "success") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3 w-3 text-emerald-500" });
747
+ if (status === "failed") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 text-destructive" });
748
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-3 rounded-full border-2 border-muted-foreground/30" });
749
+ }
750
+ function PlanCard({ part }) {
751
+ const successCount = part.steps.filter((s) => s.status === "success").length;
752
+ const hasExecuting = part.steps.some((s) => s.status === "executing");
753
+ const allDone = successCount === part.steps.length && part.steps.length > 0;
754
+ const anyFailed = part.steps.some((s) => s.status === "failed");
755
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border bg-background overflow-hidden text-xs w-full shadow-sm", children: [
756
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b", children: [
757
+ hasExecuting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-primary/80" }) : allDone ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3.5 w-3.5 shrink-0 text-emerald-500" }) : anyFailed ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3.5 w-3.5 shrink-0 text-destructive" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { className: "h-3.5 w-3.5 shrink-0 text-primary/70" }),
758
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground/80 truncate flex-1 leading-snug", children: part.goal }),
759
+ part.steps.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [
760
+ successCount,
761
+ "/",
762
+ part.steps.length
763
+ ] })
764
+ ] }),
765
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-border/40", children: part.steps.map((step) => /* @__PURE__ */ jsxRuntime.jsxs(
766
+ "div",
767
+ {
768
+ className: cn(
769
+ "flex items-start gap-2.5 px-3 py-2 transition-colors duration-200",
770
+ step.status === "executing" && "bg-primary/5"
771
+ ),
772
+ children: [
773
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(PlanStepIcon, { status: step.status }) }),
774
+ /* @__PURE__ */ jsxRuntime.jsxs(
775
+ "span",
776
+ {
777
+ className: cn(
778
+ "leading-relaxed",
779
+ step.status === "pending" && "text-muted-foreground",
780
+ step.status === "executing" && "text-foreground font-medium",
781
+ step.status === "success" && "text-foreground/60 line-through decoration-foreground/20",
782
+ step.status === "failed" && "text-destructive"
783
+ ),
784
+ children: [
785
+ step.index + 1,
786
+ ". ",
787
+ step.description,
788
+ step.error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-destructive/70 text-[10px] mt-0.5 no-underline", children: step.error })
789
+ ]
790
+ }
791
+ )
792
+ ]
793
+ },
794
+ step.index
795
+ )) })
796
+ ] });
797
+ }
613
798
  function ThinkingDots() {
614
799
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
615
800
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
@@ -798,14 +983,18 @@ function MessageBubble({
798
983
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
799
984
  const toolParts = message.parts.filter((p) => p.type === "tool");
800
985
  const pickerParts = message.parts.filter((p) => p.type === "picker");
986
+ const planParts = message.parts.filter((p) => p.type === "plan");
801
987
  const contrastColor = getContrastColor(userColor);
802
988
  if (isUser) {
803
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
989
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
804
990
  "div",
805
991
  {
806
- className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed",
992
+ className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed flex flex-col gap-2",
807
993
  style: { backgroundColor: userColor, color: contrastColor },
808
- children: textPart?.text
994
+ children: [
995
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(SentAttachments, { attachments: message.attachments, contrastColor }),
996
+ textPart?.text && /* @__PURE__ */ jsxRuntime.jsx("span", { children: textPart.text })
997
+ ]
809
998
  }
810
999
  ) });
811
1000
  }
@@ -815,6 +1004,7 @@ function MessageBubble({
815
1004
  /* @__PURE__ */ jsxRuntime.jsx(Avatar2, { style: { width: 28, height: 28, marginTop: 2, border: "1px solid rgba(0,0,0,0.08)" }, children: profilePicture ? /* @__PURE__ */ jsxRuntime.jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback2, { style: { fontSize: 10, fontWeight: 600, backgroundColor: "var(--primary, #19191c)", color: "var(--primary-foreground, #fff)" }, children: agentName.slice(0, 2).toUpperCase() }) }),
816
1005
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
817
1006
  showThinking && reasoningPart && /* @__PURE__ */ jsxRuntime.jsx(ReasoningBlock, { text: reasoningPart.text }),
1007
+ planParts.map((p) => /* @__PURE__ */ jsxRuntime.jsx(PlanCard, { part: p }, p.planId)),
818
1008
  visibleToolParts.map((t) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallBadge, { part: t }, t.toolCallId)),
819
1009
  pickerParts.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
820
1010
  PickerSelector,
@@ -910,44 +1100,6 @@ function ChatMessages({
910
1100
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: bottomRef })
911
1101
  ] });
912
1102
  }
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
1103
  var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
952
1104
  function ChatInput({
953
1105
  input,
@@ -1034,6 +1186,18 @@ function ChatInput({
1034
1186
  e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
1035
1187
  },
1036
1188
  onKeyDown: handleKeyDown,
1189
+ onPaste: (e) => {
1190
+ if (!hasAttachments || !onAttach) return;
1191
+ const files = Array.from(e.clipboardData?.files ?? []).filter(
1192
+ (f) => f.type.startsWith("image/")
1193
+ );
1194
+ if (files.length > 0) {
1195
+ e.preventDefault();
1196
+ const dt = new DataTransfer();
1197
+ files.forEach((f) => dt.items.add(f));
1198
+ onAttach(dt.files);
1199
+ }
1200
+ },
1037
1201
  disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
1038
1202
  autoFocus: true
1039
1203
  }
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';
@@ -179,6 +179,30 @@ function useChat({
179
179
  });
180
180
  break;
181
181
  }
182
+ case "plan-created": {
183
+ msg.parts.push({
184
+ type: "plan",
185
+ planId: proto.planId,
186
+ goal: proto.goal,
187
+ steps: proto.steps.map((s) => ({ ...s, status: "pending" }))
188
+ });
189
+ break;
190
+ }
191
+ case "plan-step-update": {
192
+ const pIdx = msg.parts.findIndex(
193
+ (p) => p.type === "plan" && p.planId === proto.planId
194
+ );
195
+ if (pIdx !== -1) {
196
+ const prev2 = msg.parts[pIdx];
197
+ msg.parts[pIdx] = {
198
+ ...prev2,
199
+ steps: prev2.steps.map(
200
+ (s) => s.index === proto.stepIndex ? { ...s, status: proto.status, ...proto.error ? { error: proto.error } : {} } : s
201
+ )
202
+ };
203
+ }
204
+ break;
205
+ }
182
206
  }
183
207
  const copy = [...prev];
184
208
  copy[idx] = msg;
@@ -228,17 +252,22 @@ function useChat({
228
252
  const userInput = (text ?? input).trim();
229
253
  if (!userInput || streaming) return;
230
254
  setInput("");
255
+ const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
256
+ pendingAttachmentsRef.current = [];
231
257
  const userMsgId = newId();
232
258
  setMessages((prev) => [
233
259
  ...prev,
234
- { id: userMsgId, role: "user", parts: [{ type: "text", text: userInput }] }
260
+ {
261
+ id: userMsgId,
262
+ role: "user",
263
+ parts: [{ type: "text", text: userInput }],
264
+ ...attachments ? { attachments } : {}
265
+ }
235
266
  ]);
236
267
  setStreaming(true);
237
268
  const assistantMsgId = newId();
238
269
  streamingMsgIdRef.current = assistantMsgId;
239
270
  setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
240
- const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
241
- pendingAttachmentsRef.current = [];
242
271
  try {
243
272
  await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
244
273
  } catch {
@@ -580,10 +609,166 @@ function ChatHeader({
580
609
  }
581
610
  );
582
611
  }
612
+ var cn2 = (...inputs) => twMerge(clsx(inputs));
613
+ function formatBytes(bytes) {
614
+ if (bytes < 1024) return `${bytes} B`;
615
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
616
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
617
+ }
618
+ function ChipLeading({ a }) {
619
+ if (a.status === "uploading") return /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" });
620
+ if (a.status === "error") return /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" });
621
+ const thumbSrc = a.payload?.url ?? a.payload?.base64;
622
+ if (a.mimeType.startsWith("image/") && thumbSrc) {
623
+ return /* @__PURE__ */ jsx(
624
+ "img",
625
+ {
626
+ src: thumbSrc,
627
+ alt: a.name,
628
+ className: "h-6 w-6 rounded object-cover shrink-0 border border-border/40"
629
+ }
630
+ );
631
+ }
632
+ 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" }) });
633
+ if (a.mimeType === "application/pdf") return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0 text-red-400" });
634
+ if (a.mimeType.includes("csv") || a.mimeType.includes("spreadsheet") || a.name.endsWith(".csv")) {
635
+ return /* @__PURE__ */ jsx(FileSpreadsheet, { className: "h-3 w-3 shrink-0 text-emerald-500" });
636
+ }
637
+ return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0" });
638
+ }
639
+ function AttachmentChips({ attachments, onRemove }) {
640
+ if (attachments.length === 0) return null;
641
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxs(
642
+ "div",
643
+ {
644
+ className: cn2(
645
+ "flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[200px]",
646
+ 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"
647
+ ),
648
+ children: [
649
+ /* @__PURE__ */ jsx(ChipLeading, { a }),
650
+ /* @__PURE__ */ jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
651
+ a.status === "ready" && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/50", children: formatBytes(a.sizeBytes) }),
652
+ /* @__PURE__ */ jsx(
653
+ "button",
654
+ {
655
+ type: "button",
656
+ onClick: () => onRemove(a.id),
657
+ className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors ml-0.5",
658
+ title: "Remove",
659
+ children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
660
+ }
661
+ )
662
+ ]
663
+ },
664
+ a.id
665
+ )) });
666
+ }
583
667
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
584
668
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
585
669
  var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
586
670
  var ReactMarkdown = ReactMarkdownLib;
671
+ function SentAttachments({ attachments, contrastColor }) {
672
+ const images = attachments.filter((a) => a.contentType === "image");
673
+ const files = attachments.filter((a) => a.contentType !== "image");
674
+ const isDark = contrastColor === "#ffffff" || contrastColor === "#fff";
675
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [
676
+ images.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex gap-1.5 flex-wrap", images.length === 1 && ""), children: images.map((img) => {
677
+ const src = img.url ?? img.base64;
678
+ return src ? /* @__PURE__ */ jsx(
679
+ "img",
680
+ {
681
+ src,
682
+ alt: img.name,
683
+ className: cn(
684
+ "rounded-xl object-cover border",
685
+ images.length === 1 ? "w-full max-h-48" : "h-20 w-20",
686
+ isDark ? "border-white/20" : "border-black/10"
687
+ )
688
+ },
689
+ img.id
690
+ ) : /* @__PURE__ */ jsx(
691
+ "div",
692
+ {
693
+ className: cn(
694
+ "h-20 w-20 rounded-xl flex items-center justify-center text-[10px] font-medium",
695
+ isDark ? "bg-white/10 text-white/60" : "bg-black/10 text-black/40"
696
+ ),
697
+ children: "IMG"
698
+ },
699
+ img.id
700
+ );
701
+ }) }),
702
+ files.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: files.map((f) => /* @__PURE__ */ jsxs(
703
+ "div",
704
+ {
705
+ className: cn(
706
+ "flex items-center gap-1 rounded-lg px-2 py-1 text-[11px]",
707
+ isDark ? "bg-white/15 text-white/80" : "bg-black/10 text-black/60"
708
+ ),
709
+ children: [
710
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: f.name }),
711
+ /* @__PURE__ */ jsx("span", { className: "opacity-60 shrink-0", children: formatBytes(f.sizeBytes) })
712
+ ]
713
+ },
714
+ f.id
715
+ )) })
716
+ ] });
717
+ }
718
+ function PlanStepIcon({ status }) {
719
+ if (status === "executing") return /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-primary" });
720
+ if (status === "success") return /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3 w-3 text-emerald-500" });
721
+ if (status === "failed") return /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 text-destructive" });
722
+ return /* @__PURE__ */ jsx("div", { className: "h-3 w-3 rounded-full border-2 border-muted-foreground/30" });
723
+ }
724
+ function PlanCard({ part }) {
725
+ const successCount = part.steps.filter((s) => s.status === "success").length;
726
+ const hasExecuting = part.steps.some((s) => s.status === "executing");
727
+ const allDone = successCount === part.steps.length && part.steps.length > 0;
728
+ const anyFailed = part.steps.some((s) => s.status === "failed");
729
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-background overflow-hidden text-xs w-full shadow-sm", children: [
730
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b", children: [
731
+ hasExecuting ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-primary/80" }) : allDone ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 shrink-0 text-emerald-500" }) : anyFailed ? /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 shrink-0 text-destructive" }) : /* @__PURE__ */ jsx(Zap, { className: "h-3.5 w-3.5 shrink-0 text-primary/70" }),
732
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground/80 truncate flex-1 leading-snug", children: part.goal }),
733
+ part.steps.length > 0 && /* @__PURE__ */ jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [
734
+ successCount,
735
+ "/",
736
+ part.steps.length
737
+ ] })
738
+ ] }),
739
+ /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/40", children: part.steps.map((step) => /* @__PURE__ */ jsxs(
740
+ "div",
741
+ {
742
+ className: cn(
743
+ "flex items-start gap-2.5 px-3 py-2 transition-colors duration-200",
744
+ step.status === "executing" && "bg-primary/5"
745
+ ),
746
+ children: [
747
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 shrink-0", children: /* @__PURE__ */ jsx(PlanStepIcon, { status: step.status }) }),
748
+ /* @__PURE__ */ jsxs(
749
+ "span",
750
+ {
751
+ className: cn(
752
+ "leading-relaxed",
753
+ step.status === "pending" && "text-muted-foreground",
754
+ step.status === "executing" && "text-foreground font-medium",
755
+ step.status === "success" && "text-foreground/60 line-through decoration-foreground/20",
756
+ step.status === "failed" && "text-destructive"
757
+ ),
758
+ children: [
759
+ step.index + 1,
760
+ ". ",
761
+ step.description,
762
+ step.error && /* @__PURE__ */ jsx("span", { className: "block text-destructive/70 text-[10px] mt-0.5 no-underline", children: step.error })
763
+ ]
764
+ }
765
+ )
766
+ ]
767
+ },
768
+ step.index
769
+ )) })
770
+ ] });
771
+ }
587
772
  function ThinkingDots() {
588
773
  return /* @__PURE__ */ jsxs(Fragment, { children: [
589
774
  /* @__PURE__ */ jsx("style", { children: `
@@ -772,14 +957,18 @@ function MessageBubble({
772
957
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
773
958
  const toolParts = message.parts.filter((p) => p.type === "tool");
774
959
  const pickerParts = message.parts.filter((p) => p.type === "picker");
960
+ const planParts = message.parts.filter((p) => p.type === "plan");
775
961
  const contrastColor = getContrastColor(userColor);
776
962
  if (isUser) {
777
- return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
963
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
778
964
  "div",
779
965
  {
780
- className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed",
966
+ className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed flex flex-col gap-2",
781
967
  style: { backgroundColor: userColor, color: contrastColor },
782
- children: textPart?.text
968
+ children: [
969
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx(SentAttachments, { attachments: message.attachments, contrastColor }),
970
+ textPart?.text && /* @__PURE__ */ jsx("span", { children: textPart.text })
971
+ ]
783
972
  }
784
973
  ) });
785
974
  }
@@ -789,6 +978,7 @@ function MessageBubble({
789
978
  /* @__PURE__ */ jsx(Avatar2, { style: { width: 28, height: 28, marginTop: 2, border: "1px solid rgba(0,0,0,0.08)" }, children: profilePicture ? /* @__PURE__ */ jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsx(AvatarFallback2, { style: { fontSize: 10, fontWeight: 600, backgroundColor: "var(--primary, #19191c)", color: "var(--primary-foreground, #fff)" }, children: agentName.slice(0, 2).toUpperCase() }) }),
790
979
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
791
980
  showThinking && reasoningPart && /* @__PURE__ */ jsx(ReasoningBlock, { text: reasoningPart.text }),
981
+ planParts.map((p) => /* @__PURE__ */ jsx(PlanCard, { part: p }, p.planId)),
792
982
  visibleToolParts.map((t) => /* @__PURE__ */ jsx(ToolCallBadge, { part: t }, t.toolCallId)),
793
983
  pickerParts.map((p) => /* @__PURE__ */ jsx(
794
984
  PickerSelector,
@@ -884,44 +1074,6 @@ function ChatMessages({
884
1074
  /* @__PURE__ */ jsx("div", { ref: bottomRef })
885
1075
  ] });
886
1076
  }
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
1077
  var cn3 = (...inputs) => twMerge(clsx(inputs));
926
1078
  function ChatInput({
927
1079
  input,
@@ -1008,6 +1160,18 @@ function ChatInput({
1008
1160
  e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
1009
1161
  },
1010
1162
  onKeyDown: handleKeyDown,
1163
+ onPaste: (e) => {
1164
+ if (!hasAttachments || !onAttach) return;
1165
+ const files = Array.from(e.clipboardData?.files ?? []).filter(
1166
+ (f) => f.type.startsWith("image/")
1167
+ );
1168
+ if (files.length > 0) {
1169
+ e.preventDefault();
1170
+ const dt = new DataTransfer();
1171
+ files.forEach((f) => dt.items.add(f));
1172
+ onAttach(dt.files);
1173
+ }
1174
+ },
1011
1175
  disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
1012
1176
  autoFocus: true
1013
1177
  }
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.2",
37
37
  "scripts": {
38
38
  "build": "tsup",
39
39
  "typecheck": "tsc --noEmit"