@melony/react 0.1.22 → 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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { ClientState, MelonyClient } from 'melony/client';
4
- import { Role, Event, UINode } from 'melony';
4
+ import { Role, Event, Config, UINode } from 'melony';
5
5
  import { QueryClient } from '@tanstack/react-query';
6
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
7
7
 
@@ -42,6 +42,7 @@ interface ComposerOptionGroup {
42
42
  label: string;
43
43
  options: ComposerOption[];
44
44
  type?: "single" | "multiple";
45
+ defaultSelectedIds?: string[];
45
46
  }
46
47
  interface AuthService {
47
48
  getMe: () => Promise<User | null>;
@@ -64,10 +65,7 @@ interface MelonyContextValue extends ClientState {
64
65
  }) => Promise<void>;
65
66
  reset: (events?: Event[]) => void;
66
67
  client: MelonyClient;
67
- config?: {
68
- starterPrompts: any[];
69
- options: any[];
70
- };
68
+ config?: Config;
71
69
  }
72
70
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
73
71
  interface MelonyClientProviderProps {
@@ -155,8 +153,17 @@ interface ComposerProps {
155
153
  options?: ComposerOptionGroup[];
156
154
  autoFocus?: boolean;
157
155
  defaultSelectedIds?: string[];
156
+ fileAttachments?: {
157
+ enabled?: boolean;
158
+ accept?: string;
159
+ maxFiles?: number;
160
+ maxFileSize?: number;
161
+ };
162
+ accept?: string;
163
+ maxFiles?: number;
164
+ maxFileSize?: number;
158
165
  }
159
- declare function Composer({ value, onChange, onSubmit, placeholder, isLoading, className, options, autoFocus, defaultSelectedIds, }: ComposerProps): react_jsx_runtime.JSX.Element;
166
+ declare function Composer({ value, onChange, onSubmit, placeholder, isLoading, className, options, autoFocus, defaultSelectedIds, fileAttachments, accept: legacyAccept, maxFiles: legacyMaxFiles, maxFileSize: legacyMaxFileSize, }: ComposerProps): react_jsx_runtime.JSX.Element;
160
167
 
161
168
  interface ChatHeaderProps {
162
169
  /**
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { ClientState, MelonyClient } from 'melony/client';
4
- import { Role, Event, UINode } from 'melony';
4
+ import { Role, Event, Config, UINode } from 'melony';
5
5
  import { QueryClient } from '@tanstack/react-query';
6
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
7
7
 
@@ -42,6 +42,7 @@ interface ComposerOptionGroup {
42
42
  label: string;
43
43
  options: ComposerOption[];
44
44
  type?: "single" | "multiple";
45
+ defaultSelectedIds?: string[];
45
46
  }
46
47
  interface AuthService {
47
48
  getMe: () => Promise<User | null>;
@@ -64,10 +65,7 @@ interface MelonyContextValue extends ClientState {
64
65
  }) => Promise<void>;
65
66
  reset: (events?: Event[]) => void;
66
67
  client: MelonyClient;
67
- config?: {
68
- starterPrompts: any[];
69
- options: any[];
70
- };
68
+ config?: Config;
71
69
  }
72
70
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
73
71
  interface MelonyClientProviderProps {
@@ -155,8 +153,17 @@ interface ComposerProps {
155
153
  options?: ComposerOptionGroup[];
156
154
  autoFocus?: boolean;
157
155
  defaultSelectedIds?: string[];
156
+ fileAttachments?: {
157
+ enabled?: boolean;
158
+ accept?: string;
159
+ maxFiles?: number;
160
+ maxFileSize?: number;
161
+ };
162
+ accept?: string;
163
+ maxFiles?: number;
164
+ maxFileSize?: number;
158
165
  }
159
- declare function Composer({ value, onChange, onSubmit, placeholder, isLoading, className, options, autoFocus, defaultSelectedIds, }: ComposerProps): react_jsx_runtime.JSX.Element;
166
+ declare function Composer({ value, onChange, onSubmit, placeholder, isLoading, className, options, autoFocus, defaultSelectedIds, fileAttachments, accept: legacyAccept, maxFiles: legacyMaxFiles, maxFileSize: legacyMaxFileSize, }: ComposerProps): react_jsx_runtime.JSX.Element;
160
167
 
161
168
  interface ChatHeaderProps {
162
169
  /**
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { twMerge } from 'tailwind-merge';
10
10
  import { Button as Button$1 } from '@base-ui/react/button';
11
11
  import { cva } from 'class-variance-authority';
12
12
  import * as ICONS from '@tabler/icons-react';
13
- import { IconChevronDown, IconLoader2, IconArrowUp, IconPlus, IconMessage, IconTrash, IconHistory, IconX, IconArrowLeft, IconChevronLeft, IconChevronRight, IconUser, IconLogout, IconBrandGoogle, IconDeviceDesktop, IconMoon, IconSun, IconCheck, IconChevronUp, IconSelector } from '@tabler/icons-react';
13
+ import { IconX, IconPaperclip, IconChevronDown, IconLoader2, IconArrowUp, IconPlus, IconMessage, IconTrash, IconHistory, IconArrowLeft, IconChevronLeft, IconChevronRight, IconUser, IconLogout, IconBrandGoogle, IconDeviceDesktop, IconMoon, IconSun, IconCheck, IconChevronUp, IconSelector } from '@tabler/icons-react';
14
14
  import { Menu } from '@base-ui/react/menu';
15
15
  import { Separator as Separator$1 } from '@base-ui/react/separator';
16
16
  import { Dialog as Dialog$1 } from '@base-ui/react/dialog';
@@ -563,11 +563,22 @@ function Composer({
563
563
  className,
564
564
  options = [],
565
565
  autoFocus = false,
566
- defaultSelectedIds = []
566
+ defaultSelectedIds = [],
567
+ fileAttachments,
568
+ // Legacy props for backward compatibility
569
+ accept: legacyAccept,
570
+ maxFiles: legacyMaxFiles,
571
+ maxFileSize: legacyMaxFileSize
567
572
  }) {
573
+ const enabled = fileAttachments?.enabled !== false;
574
+ const accept = fileAttachments?.accept ?? legacyAccept;
575
+ const maxFiles = fileAttachments?.maxFiles ?? legacyMaxFiles ?? 10;
576
+ const maxFileSize = fileAttachments?.maxFileSize ?? legacyMaxFileSize ?? 10 * 1024 * 1024;
568
577
  const [selectedOptions, setSelectedOptions] = React10__default.useState(
569
578
  () => new Set(defaultSelectedIds)
570
579
  );
580
+ const [attachedFiles, setAttachedFiles] = React10__default.useState([]);
581
+ const fileInputRef = React10__default.useRef(null);
571
582
  const toggleOption = (id, groupOptions, type = "multiple") => {
572
583
  const next = new Set(selectedOptions);
573
584
  if (type === "single") {
@@ -587,7 +598,34 @@ function Composer({
587
598
  }
588
599
  setSelectedOptions(next);
589
600
  };
590
- const handleInternalSubmit = () => {
601
+ const handleFileSelect = (e) => {
602
+ const files = Array.from(e.target.files || []);
603
+ const validFiles = files.filter((file) => {
604
+ if (file.size > maxFileSize) {
605
+ console.warn(`File ${file.name} exceeds maximum size of ${maxFileSize} bytes`);
606
+ return false;
607
+ }
608
+ return true;
609
+ });
610
+ const remainingSlots = maxFiles - attachedFiles.length;
611
+ const filesToAdd = validFiles.slice(0, remainingSlots);
612
+ if (filesToAdd.length < validFiles.length) {
613
+ console.warn(`Only ${filesToAdd.length} files can be added (max: ${maxFiles})`);
614
+ }
615
+ setAttachedFiles((prev) => [...prev, ...filesToAdd]);
616
+ if (fileInputRef.current) {
617
+ fileInputRef.current.value = "";
618
+ }
619
+ };
620
+ const handleRemoveFile = (index) => {
621
+ setAttachedFiles((prev) => prev.filter((_, i) => i !== index));
622
+ };
623
+ const formatFileSize = (bytes) => {
624
+ if (bytes < 1024) return bytes + " B";
625
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
626
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
627
+ };
628
+ const handleInternalSubmit = async () => {
591
629
  const state = {};
592
630
  options.forEach((group) => {
593
631
  const selectedInGroup = group.options.filter(
@@ -604,85 +642,178 @@ function Composer({
604
642
  }
605
643
  }
606
644
  });
645
+ if (attachedFiles.length > 0) {
646
+ const filePromises = attachedFiles.map((file) => {
647
+ return new Promise((resolve, reject) => {
648
+ const reader = new FileReader();
649
+ reader.onload = () => {
650
+ try {
651
+ const base64 = reader.result;
652
+ if (!base64) {
653
+ reject(new Error("FileReader returned empty result"));
654
+ return;
655
+ }
656
+ const base64Data = base64.includes(",") ? base64.split(",")[1] : base64;
657
+ resolve({
658
+ name: file.name,
659
+ type: file.type,
660
+ size: file.size,
661
+ data: base64Data
662
+ });
663
+ } catch (error) {
664
+ reject(error);
665
+ }
666
+ };
667
+ reader.onerror = (error) => {
668
+ reject(new Error(`Failed to read file ${file.name}: ${error}`));
669
+ };
670
+ reader.onabort = () => {
671
+ reject(new Error(`File read aborted for ${file.name}`));
672
+ };
673
+ reader.readAsDataURL(file);
674
+ });
675
+ });
676
+ try {
677
+ const convertedFiles = await Promise.all(filePromises);
678
+ if (convertedFiles.length > 0) {
679
+ state.files = convertedFiles;
680
+ }
681
+ } catch (error) {
682
+ console.error("Failed to convert files to base64:", error);
683
+ }
684
+ }
607
685
  onSubmit(state);
686
+ setAttachedFiles([]);
608
687
  };
609
688
  const handleKeyDown = (e) => {
610
689
  if (e.key === "Enter" && !e.shiftKey) {
611
690
  e.preventDefault();
612
- handleInternalSubmit();
691
+ handleInternalSubmit().catch(console.error);
613
692
  }
614
693
  };
615
- return /* @__PURE__ */ jsx("div", { className: cn("relative flex flex-col w-full", className), children: /* @__PURE__ */ 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: [
616
- /* @__PURE__ */ jsx(
617
- Textarea,
694
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative flex flex-col w-full", className), children: [
695
+ enabled && attachedFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: "mb-2 flex flex-wrap gap-2", children: attachedFiles.map((file, index) => /* @__PURE__ */ jsxs(
696
+ "div",
618
697
  {
619
- value,
620
- onChange: (e) => onChange(e.target.value),
621
- onKeyDown: handleKeyDown,
622
- placeholder,
623
- 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",
624
- autoFocus
625
- }
626
- ),
627
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center px-1", children: [
628
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: options.map((group) => {
629
- const selectedInGroup = group.options.filter(
630
- (o) => selectedOptions.has(o.id)
631
- );
632
- const label = selectedInGroup.length === 0 ? group.label : selectedInGroup.length === 1 ? selectedInGroup[0].label : `${group.label} (${selectedInGroup.length})`;
633
- const isSingle = group.type === "single";
634
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
698
+ className: "flex items-center gap-2 px-3 py-1.5 bg-muted rounded-lg text-sm",
699
+ children: [
700
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[200px]", title: file.name, children: file.name }),
701
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: formatFileSize(file.size) }),
635
702
  /* @__PURE__ */ jsx(
636
- DropdownMenuTrigger,
703
+ "button",
637
704
  {
638
- render: /* @__PURE__ */ jsxs(
639
- Button,
640
- {
641
- variant: "ghost",
642
- size: "sm",
643
- className: cn(
644
- selectedInGroup.length > 0 ? "text-foreground bg-muted/50" : "text-muted-foreground"
645
- ),
646
- children: [
647
- label,
648
- /* @__PURE__ */ jsx(IconChevronDown, { className: "h-3 w-3 opacity-50" })
649
- ]
650
- }
651
- )
705
+ type: "button",
706
+ onClick: () => handleRemoveFile(index),
707
+ className: "ml-1 hover:bg-muted-foreground/20 rounded p-0.5 transition-colors",
708
+ "aria-label": "Remove file",
709
+ children: /* @__PURE__ */ jsx(IconX, { className: "h-3.5 w-3.5" })
652
710
  }
653
- ),
654
- /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", className: "w-56", children: /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
655
- /* @__PURE__ */ jsx(DropdownMenuLabel, { children: group.label }),
656
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
657
- group.options.map((option) => /* @__PURE__ */ jsx(
658
- DropdownMenuCheckboxItem,
659
- {
660
- checked: selectedOptions.has(option.id),
661
- onCheckedChange: () => toggleOption(
662
- option.id,
663
- group.options,
664
- isSingle ? "single" : "multiple"
665
- ),
666
- onSelect: (e) => e.preventDefault(),
667
- children: option.label
668
- },
669
- option.id
670
- ))
671
- ] }) })
672
- ] }, group.id);
673
- }) }),
711
+ )
712
+ ]
713
+ },
714
+ index
715
+ )) }),
716
+ /* @__PURE__ */ 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: [
674
717
  /* @__PURE__ */ jsx(
675
- Button,
718
+ Textarea,
676
719
  {
677
- type: "submit",
678
- disabled: !value.trim() && !isLoading || isLoading,
679
- size: "icon-lg",
680
- onClick: handleInternalSubmit,
681
- children: isLoading ? /* @__PURE__ */ jsx(IconLoader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx(IconArrowUp, { className: "h-5 w-5" })
720
+ value,
721
+ onChange: (e) => onChange(e.target.value),
722
+ onKeyDown: handleKeyDown,
723
+ placeholder,
724
+ 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",
725
+ autoFocus
682
726
  }
683
- )
727
+ ),
728
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center px-1", children: [
729
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
730
+ enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
731
+ /* @__PURE__ */ jsx(
732
+ "input",
733
+ {
734
+ ref: fileInputRef,
735
+ type: "file",
736
+ multiple: true,
737
+ accept,
738
+ onChange: handleFileSelect,
739
+ className: "hidden",
740
+ disabled: isLoading || attachedFiles.length >= maxFiles
741
+ }
742
+ ),
743
+ /* @__PURE__ */ jsx(
744
+ Button,
745
+ {
746
+ type: "button",
747
+ variant: "ghost",
748
+ size: "sm",
749
+ onClick: () => fileInputRef.current?.click(),
750
+ disabled: isLoading || attachedFiles.length >= maxFiles,
751
+ className: "text-muted-foreground",
752
+ title: attachedFiles.length >= maxFiles ? `Maximum ${maxFiles} files allowed` : "Attach file",
753
+ children: /* @__PURE__ */ jsx(IconPaperclip, { className: "h-4 w-4" })
754
+ }
755
+ )
756
+ ] }),
757
+ options.map((group) => {
758
+ const selectedInGroup = group.options.filter(
759
+ (o) => selectedOptions.has(o.id)
760
+ );
761
+ const label = selectedInGroup.length === 0 ? group.label : selectedInGroup.length === 1 ? selectedInGroup[0].label : `${group.label} (${selectedInGroup.length})`;
762
+ const isSingle = group.type === "single";
763
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
764
+ /* @__PURE__ */ jsx(
765
+ DropdownMenuTrigger,
766
+ {
767
+ render: /* @__PURE__ */ jsxs(
768
+ Button,
769
+ {
770
+ variant: "ghost",
771
+ size: "sm",
772
+ className: cn(
773
+ selectedInGroup.length > 0 ? "text-foreground bg-muted/50" : "text-muted-foreground"
774
+ ),
775
+ children: [
776
+ label,
777
+ /* @__PURE__ */ jsx(IconChevronDown, { className: "h-3 w-3 opacity-50" })
778
+ ]
779
+ }
780
+ )
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", className: "w-56", children: /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
784
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: group.label }),
785
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
786
+ group.options.map((option) => /* @__PURE__ */ jsx(
787
+ DropdownMenuCheckboxItem,
788
+ {
789
+ checked: selectedOptions.has(option.id),
790
+ onCheckedChange: () => toggleOption(
791
+ option.id,
792
+ group.options,
793
+ isSingle ? "single" : "multiple"
794
+ ),
795
+ onSelect: (e) => e.preventDefault(),
796
+ children: option.label
797
+ },
798
+ option.id
799
+ ))
800
+ ] }) })
801
+ ] }, group.id);
802
+ })
803
+ ] }),
804
+ /* @__PURE__ */ jsx(
805
+ Button,
806
+ {
807
+ type: "submit",
808
+ disabled: !value.trim() && attachedFiles.length === 0 && !isLoading || isLoading,
809
+ size: "icon-lg",
810
+ onClick: () => handleInternalSubmit().catch(console.error),
811
+ children: isLoading ? /* @__PURE__ */ jsx(IconLoader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx(IconArrowUp, { className: "h-5 w-5" })
812
+ }
813
+ )
814
+ ] })
684
815
  ] })
685
- ] }) });
816
+ ] });
686
817
  }
687
818
  function Card({
688
819
  className,
@@ -2341,6 +2472,14 @@ function Thread({
2341
2472
  });
2342
2473
  const starterPrompts = localStarterPrompts ?? config?.starterPrompts;
2343
2474
  const options = localOptions ?? config?.options;
2475
+ const allDefaultSelectedIds = useMemo(() => {
2476
+ const defaultSelectedIdsFromOptions = options?.flatMap(
2477
+ (group) => group.defaultSelectedIds ?? []
2478
+ ) ?? [];
2479
+ return [
2480
+ .../* @__PURE__ */ new Set([...defaultSelectedIdsFromOptions, ...defaultSelectedIds ?? []])
2481
+ ];
2482
+ }, [options, defaultSelectedIds]);
2344
2483
  const [input, setInput] = useState("");
2345
2484
  const messagesEndRef = useRef(null);
2346
2485
  useEffect(() => {
@@ -2348,13 +2487,14 @@ function Thread({
2348
2487
  }, [messages]);
2349
2488
  const handleSubmit = async (state, overrideInput) => {
2350
2489
  const text = (overrideInput ?? input).trim();
2351
- if (!text || isLoading) return;
2490
+ const hasFiles = state?.files && Array.isArray(state.files) && state.files.length > 0;
2491
+ if (!text && !hasFiles || isLoading) return;
2352
2492
  if (!overrideInput) setInput("");
2353
2493
  await sendEvent(
2354
2494
  {
2355
2495
  type: "text",
2356
2496
  role: "user",
2357
- data: { content: text }
2497
+ data: { content: text || "" }
2358
2498
  },
2359
2499
  { state: { ...state, threadId: activeThreadId ?? void 0 } }
2360
2500
  );
@@ -2412,7 +2552,8 @@ function Thread({
2412
2552
  isLoading,
2413
2553
  options,
2414
2554
  autoFocus,
2415
- defaultSelectedIds
2555
+ defaultSelectedIds: allDefaultSelectedIds,
2556
+ fileAttachments: config?.fileAttachments
2416
2557
  }
2417
2558
  ) }) })
2418
2559
  ]