@melony/react 0.1.23 → 0.1.24
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.cjs +202 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +203 -70
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -584,11 +584,22 @@ function Composer({
|
|
|
584
584
|
className,
|
|
585
585
|
options = [],
|
|
586
586
|
autoFocus = false,
|
|
587
|
-
defaultSelectedIds = []
|
|
587
|
+
defaultSelectedIds = [],
|
|
588
|
+
fileAttachments,
|
|
589
|
+
// Legacy props for backward compatibility
|
|
590
|
+
accept: legacyAccept,
|
|
591
|
+
maxFiles: legacyMaxFiles,
|
|
592
|
+
maxFileSize: legacyMaxFileSize
|
|
588
593
|
}) {
|
|
594
|
+
const enabled = fileAttachments?.enabled !== false;
|
|
595
|
+
const accept = fileAttachments?.accept ?? legacyAccept;
|
|
596
|
+
const maxFiles = fileAttachments?.maxFiles ?? legacyMaxFiles ?? 10;
|
|
597
|
+
const maxFileSize = fileAttachments?.maxFileSize ?? legacyMaxFileSize ?? 10 * 1024 * 1024;
|
|
589
598
|
const [selectedOptions, setSelectedOptions] = React10__namespace.default.useState(
|
|
590
599
|
() => new Set(defaultSelectedIds)
|
|
591
600
|
);
|
|
601
|
+
const [attachedFiles, setAttachedFiles] = React10__namespace.default.useState([]);
|
|
602
|
+
const fileInputRef = React10__namespace.default.useRef(null);
|
|
592
603
|
const toggleOption = (id, groupOptions, type = "multiple") => {
|
|
593
604
|
const next = new Set(selectedOptions);
|
|
594
605
|
if (type === "single") {
|
|
@@ -608,7 +619,34 @@ function Composer({
|
|
|
608
619
|
}
|
|
609
620
|
setSelectedOptions(next);
|
|
610
621
|
};
|
|
611
|
-
const
|
|
622
|
+
const handleFileSelect = (e) => {
|
|
623
|
+
const files = Array.from(e.target.files || []);
|
|
624
|
+
const validFiles = files.filter((file) => {
|
|
625
|
+
if (file.size > maxFileSize) {
|
|
626
|
+
console.warn(`File ${file.name} exceeds maximum size of ${maxFileSize} bytes`);
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
return true;
|
|
630
|
+
});
|
|
631
|
+
const remainingSlots = maxFiles - attachedFiles.length;
|
|
632
|
+
const filesToAdd = validFiles.slice(0, remainingSlots);
|
|
633
|
+
if (filesToAdd.length < validFiles.length) {
|
|
634
|
+
console.warn(`Only ${filesToAdd.length} files can be added (max: ${maxFiles})`);
|
|
635
|
+
}
|
|
636
|
+
setAttachedFiles((prev) => [...prev, ...filesToAdd]);
|
|
637
|
+
if (fileInputRef.current) {
|
|
638
|
+
fileInputRef.current.value = "";
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const handleRemoveFile = (index) => {
|
|
642
|
+
setAttachedFiles((prev) => prev.filter((_, i) => i !== index));
|
|
643
|
+
};
|
|
644
|
+
const formatFileSize = (bytes) => {
|
|
645
|
+
if (bytes < 1024) return bytes + " B";
|
|
646
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
647
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
648
|
+
};
|
|
649
|
+
const handleInternalSubmit = async () => {
|
|
612
650
|
const state = {};
|
|
613
651
|
options.forEach((group) => {
|
|
614
652
|
const selectedInGroup = group.options.filter(
|
|
@@ -625,85 +663,178 @@ function Composer({
|
|
|
625
663
|
}
|
|
626
664
|
}
|
|
627
665
|
});
|
|
666
|
+
if (attachedFiles.length > 0) {
|
|
667
|
+
const filePromises = attachedFiles.map((file) => {
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
const reader = new FileReader();
|
|
670
|
+
reader.onload = () => {
|
|
671
|
+
try {
|
|
672
|
+
const base64 = reader.result;
|
|
673
|
+
if (!base64) {
|
|
674
|
+
reject(new Error("FileReader returned empty result"));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const base64Data = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
678
|
+
resolve({
|
|
679
|
+
name: file.name,
|
|
680
|
+
type: file.type,
|
|
681
|
+
size: file.size,
|
|
682
|
+
data: base64Data
|
|
683
|
+
});
|
|
684
|
+
} catch (error) {
|
|
685
|
+
reject(error);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
reader.onerror = (error) => {
|
|
689
|
+
reject(new Error(`Failed to read file ${file.name}: ${error}`));
|
|
690
|
+
};
|
|
691
|
+
reader.onabort = () => {
|
|
692
|
+
reject(new Error(`File read aborted for ${file.name}`));
|
|
693
|
+
};
|
|
694
|
+
reader.readAsDataURL(file);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
try {
|
|
698
|
+
const convertedFiles = await Promise.all(filePromises);
|
|
699
|
+
if (convertedFiles.length > 0) {
|
|
700
|
+
state.files = convertedFiles;
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error("Failed to convert files to base64:", error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
628
706
|
onSubmit(state);
|
|
707
|
+
setAttachedFiles([]);
|
|
629
708
|
};
|
|
630
709
|
const handleKeyDown = (e) => {
|
|
631
710
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
632
711
|
e.preventDefault();
|
|
633
|
-
handleInternalSubmit();
|
|
712
|
+
handleInternalSubmit().catch(console.error);
|
|
634
713
|
}
|
|
635
714
|
};
|
|
636
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
637
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
638
|
-
|
|
715
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative flex flex-col w-full", className), children: [
|
|
716
|
+
enabled && attachedFiles.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex flex-wrap gap-2", children: attachedFiles.map((file, index) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
717
|
+
"div",
|
|
639
718
|
{
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
className: "min-h-[44px] max-h-[200px] border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 px-3 py-2 text-[15px] resize-none",
|
|
645
|
-
autoFocus
|
|
646
|
-
}
|
|
647
|
-
),
|
|
648
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center px-1", children: [
|
|
649
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: options.map((group) => {
|
|
650
|
-
const selectedInGroup = group.options.filter(
|
|
651
|
-
(o) => selectedOptions.has(o.id)
|
|
652
|
-
);
|
|
653
|
-
const label = selectedInGroup.length === 0 ? group.label : selectedInGroup.length === 1 ? selectedInGroup[0].label : `${group.label} (${selectedInGroup.length})`;
|
|
654
|
-
const isSingle = group.type === "single";
|
|
655
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
719
|
+
className: "flex items-center gap-2 px-3 py-1.5 bg-muted rounded-lg text-sm",
|
|
720
|
+
children: [
|
|
721
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate max-w-[200px]", title: file.name, children: file.name }),
|
|
722
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground text-xs", children: formatFileSize(file.size) }),
|
|
656
723
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
657
|
-
|
|
724
|
+
"button",
|
|
658
725
|
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
className: cn(
|
|
665
|
-
selectedInGroup.length > 0 ? "text-foreground bg-muted/50" : "text-muted-foreground"
|
|
666
|
-
),
|
|
667
|
-
children: [
|
|
668
|
-
label,
|
|
669
|
-
/* @__PURE__ */ jsxRuntime.jsx(ICONS.IconChevronDown, { className: "h-3 w-3 opacity-50" })
|
|
670
|
-
]
|
|
671
|
-
}
|
|
672
|
-
)
|
|
726
|
+
type: "button",
|
|
727
|
+
onClick: () => handleRemoveFile(index),
|
|
728
|
+
className: "ml-1 hover:bg-muted-foreground/20 rounded p-0.5 transition-colors",
|
|
729
|
+
"aria-label": "Remove file",
|
|
730
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ICONS.IconX, { className: "h-3.5 w-3.5" })
|
|
673
731
|
}
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
{
|
|
681
|
-
checked: selectedOptions.has(option.id),
|
|
682
|
-
onCheckedChange: () => toggleOption(
|
|
683
|
-
option.id,
|
|
684
|
-
group.options,
|
|
685
|
-
isSingle ? "single" : "multiple"
|
|
686
|
-
),
|
|
687
|
-
onSelect: (e) => e.preventDefault(),
|
|
688
|
-
children: option.label
|
|
689
|
-
},
|
|
690
|
-
option.id
|
|
691
|
-
))
|
|
692
|
-
] }) })
|
|
693
|
-
] }, group.id);
|
|
694
|
-
}) }),
|
|
732
|
+
)
|
|
733
|
+
]
|
|
734
|
+
},
|
|
735
|
+
index
|
|
736
|
+
)) }),
|
|
737
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-col w-full border-input border-[1.5px] rounded-3xl bg-background shadow-sm focus-within:border-ring transition-all p-2", children: [
|
|
695
738
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
696
|
-
|
|
739
|
+
Textarea,
|
|
697
740
|
{
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
741
|
+
value,
|
|
742
|
+
onChange: (e) => onChange(e.target.value),
|
|
743
|
+
onKeyDown: handleKeyDown,
|
|
744
|
+
placeholder,
|
|
745
|
+
className: "min-h-[44px] max-h-[200px] border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 px-3 py-2 text-[15px] resize-none",
|
|
746
|
+
autoFocus
|
|
703
747
|
}
|
|
704
|
-
)
|
|
748
|
+
),
|
|
749
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center px-1", children: [
|
|
750
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
751
|
+
enabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
752
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
753
|
+
"input",
|
|
754
|
+
{
|
|
755
|
+
ref: fileInputRef,
|
|
756
|
+
type: "file",
|
|
757
|
+
multiple: true,
|
|
758
|
+
accept,
|
|
759
|
+
onChange: handleFileSelect,
|
|
760
|
+
className: "hidden",
|
|
761
|
+
disabled: isLoading || attachedFiles.length >= maxFiles
|
|
762
|
+
}
|
|
763
|
+
),
|
|
764
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
765
|
+
Button,
|
|
766
|
+
{
|
|
767
|
+
type: "button",
|
|
768
|
+
variant: "ghost",
|
|
769
|
+
size: "sm",
|
|
770
|
+
onClick: () => fileInputRef.current?.click(),
|
|
771
|
+
disabled: isLoading || attachedFiles.length >= maxFiles,
|
|
772
|
+
className: "text-muted-foreground",
|
|
773
|
+
title: attachedFiles.length >= maxFiles ? `Maximum ${maxFiles} files allowed` : "Attach file",
|
|
774
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ICONS.IconPaperclip, { className: "h-4 w-4" })
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
] }),
|
|
778
|
+
options.map((group) => {
|
|
779
|
+
const selectedInGroup = group.options.filter(
|
|
780
|
+
(o) => selectedOptions.has(o.id)
|
|
781
|
+
);
|
|
782
|
+
const label = selectedInGroup.length === 0 ? group.label : selectedInGroup.length === 1 ? selectedInGroup[0].label : `${group.label} (${selectedInGroup.length})`;
|
|
783
|
+
const isSingle = group.type === "single";
|
|
784
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
785
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
786
|
+
DropdownMenuTrigger,
|
|
787
|
+
{
|
|
788
|
+
render: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
789
|
+
Button,
|
|
790
|
+
{
|
|
791
|
+
variant: "ghost",
|
|
792
|
+
size: "sm",
|
|
793
|
+
className: cn(
|
|
794
|
+
selectedInGroup.length > 0 ? "text-foreground bg-muted/50" : "text-muted-foreground"
|
|
795
|
+
),
|
|
796
|
+
children: [
|
|
797
|
+
label,
|
|
798
|
+
/* @__PURE__ */ jsxRuntime.jsx(ICONS.IconChevronDown, { className: "h-3 w-3 opacity-50" })
|
|
799
|
+
]
|
|
800
|
+
}
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
),
|
|
804
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", className: "w-56", children: /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuGroup, { children: [
|
|
805
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuLabel, { children: group.label }),
|
|
806
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuSeparator, {}),
|
|
807
|
+
group.options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
808
|
+
DropdownMenuCheckboxItem,
|
|
809
|
+
{
|
|
810
|
+
checked: selectedOptions.has(option.id),
|
|
811
|
+
onCheckedChange: () => toggleOption(
|
|
812
|
+
option.id,
|
|
813
|
+
group.options,
|
|
814
|
+
isSingle ? "single" : "multiple"
|
|
815
|
+
),
|
|
816
|
+
onSelect: (e) => e.preventDefault(),
|
|
817
|
+
children: option.label
|
|
818
|
+
},
|
|
819
|
+
option.id
|
|
820
|
+
))
|
|
821
|
+
] }) })
|
|
822
|
+
] }, group.id);
|
|
823
|
+
})
|
|
824
|
+
] }),
|
|
825
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
826
|
+
Button,
|
|
827
|
+
{
|
|
828
|
+
type: "submit",
|
|
829
|
+
disabled: !value.trim() && attachedFiles.length === 0 && !isLoading || isLoading,
|
|
830
|
+
size: "icon-lg",
|
|
831
|
+
onClick: () => handleInternalSubmit().catch(console.error),
|
|
832
|
+
children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(ICONS.IconLoader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(ICONS.IconArrowUp, { className: "h-5 w-5" })
|
|
833
|
+
}
|
|
834
|
+
)
|
|
835
|
+
] })
|
|
705
836
|
] })
|
|
706
|
-
] })
|
|
837
|
+
] });
|
|
707
838
|
}
|
|
708
839
|
function Card({
|
|
709
840
|
className,
|
|
@@ -2377,13 +2508,14 @@ function Thread({
|
|
|
2377
2508
|
}, [messages]);
|
|
2378
2509
|
const handleSubmit = async (state, overrideInput) => {
|
|
2379
2510
|
const text = (overrideInput ?? input).trim();
|
|
2380
|
-
|
|
2511
|
+
const hasFiles = state?.files && Array.isArray(state.files) && state.files.length > 0;
|
|
2512
|
+
if (!text && !hasFiles || isLoading) return;
|
|
2381
2513
|
if (!overrideInput) setInput("");
|
|
2382
2514
|
await sendEvent(
|
|
2383
2515
|
{
|
|
2384
2516
|
type: "text",
|
|
2385
2517
|
role: "user",
|
|
2386
|
-
data: { content: text }
|
|
2518
|
+
data: { content: text || "" }
|
|
2387
2519
|
},
|
|
2388
2520
|
{ state: { ...state, threadId: activeThreadId ?? void 0 } }
|
|
2389
2521
|
);
|
|
@@ -2441,7 +2573,8 @@ function Thread({
|
|
|
2441
2573
|
isLoading,
|
|
2442
2574
|
options,
|
|
2443
2575
|
autoFocus,
|
|
2444
|
-
defaultSelectedIds: allDefaultSelectedIds
|
|
2576
|
+
defaultSelectedIds: allDefaultSelectedIds,
|
|
2577
|
+
fileAttachments: config?.fileAttachments
|
|
2445
2578
|
}
|
|
2446
2579
|
) }) })
|
|
2447
2580
|
]
|