@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.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
 
@@ -65,10 +65,7 @@ interface MelonyContextValue extends ClientState {
65
65
  }) => Promise<void>;
66
66
  reset: (events?: Event[]) => void;
67
67
  client: MelonyClient;
68
- config?: {
69
- starterPrompts: any[];
70
- options: any[];
71
- };
68
+ config?: Config;
72
69
  }
73
70
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
74
71
  interface MelonyClientProviderProps {
@@ -156,8 +153,17 @@ interface ComposerProps {
156
153
  options?: ComposerOptionGroup[];
157
154
  autoFocus?: boolean;
158
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;
159
165
  }
160
- 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;
161
167
 
162
168
  interface ChatHeaderProps {
163
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
 
@@ -65,10 +65,7 @@ interface MelonyContextValue extends ClientState {
65
65
  }) => Promise<void>;
66
66
  reset: (events?: Event[]) => void;
67
67
  client: MelonyClient;
68
- config?: {
69
- starterPrompts: any[];
70
- options: any[];
71
- };
68
+ config?: Config;
72
69
  }
73
70
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
74
71
  interface MelonyClientProviderProps {
@@ -156,8 +153,17 @@ interface ComposerProps {
156
153
  options?: ComposerOptionGroup[];
157
154
  autoFocus?: boolean;
158
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;
159
165
  }
160
- 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;
161
167
 
162
168
  interface ChatHeaderProps {
163
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,
@@ -2356,13 +2487,14 @@ function Thread({
2356
2487
  }, [messages]);
2357
2488
  const handleSubmit = async (state, overrideInput) => {
2358
2489
  const text = (overrideInput ?? input).trim();
2359
- if (!text || isLoading) return;
2490
+ const hasFiles = state?.files && Array.isArray(state.files) && state.files.length > 0;
2491
+ if (!text && !hasFiles || isLoading) return;
2360
2492
  if (!overrideInput) setInput("");
2361
2493
  await sendEvent(
2362
2494
  {
2363
2495
  type: "text",
2364
2496
  role: "user",
2365
- data: { content: text }
2497
+ data: { content: text || "" }
2366
2498
  },
2367
2499
  { state: { ...state, threadId: activeThreadId ?? void 0 } }
2368
2500
  );
@@ -2420,7 +2552,8 @@ function Thread({
2420
2552
  isLoading,
2421
2553
  options,
2422
2554
  autoFocus,
2423
- defaultSelectedIds: allDefaultSelectedIds
2555
+ defaultSelectedIds: allDefaultSelectedIds,
2556
+ fileAttachments: config?.fileAttachments
2424
2557
  }
2425
2558
  ) }) })
2426
2559
  ]