@mordn/chat-widget 0.6.1 → 0.7.0

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.mjs CHANGED
@@ -171,11 +171,13 @@ import { CheckIcon as CheckIcon2, ChevronDownIcon, ChevronUpIcon } from "lucide-
171
171
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
172
172
 
173
173
  // src/ui/textarea.tsx
174
+ import * as React from "react";
174
175
  import { jsx as jsx7 } from "react/jsx-runtime";
175
- function Textarea({ className, ...props }) {
176
+ var Textarea = React.forwardRef(({ className, ...props }, ref) => {
176
177
  return /* @__PURE__ */ jsx7(
177
178
  "textarea",
178
179
  {
180
+ ref,
179
181
  "data-slot": "textarea",
180
182
  className: cn(
181
183
  "border-input placeholder:text-muted-foreground focus-visible:border-ring aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-[14px] transition-colors outline-none disabled:cursor-not-allowed disabled:opacity-50",
@@ -184,7 +186,8 @@ function Textarea({ className, ...props }) {
184
186
  ...props
185
187
  }
186
188
  );
187
- }
189
+ });
190
+ Textarea.displayName = "Textarea";
188
191
 
189
192
  // src/components/prompt-input.tsx
190
193
  import {
@@ -197,7 +200,7 @@ import {
197
200
  XIcon
198
201
  } from "lucide-react";
199
202
  import { nanoid } from "nanoid";
200
- import React, {
203
+ import React2, {
201
204
  Children,
202
205
  createContext,
203
206
  Fragment as Fragment2,
@@ -509,7 +512,7 @@ var PromptInputBody = ({
509
512
  className,
510
513
  ...props
511
514
  }) => /* @__PURE__ */ jsx8("div", { className: cn(className, "flex flex-col"), ...props });
512
- var PromptInputTextarea = React.forwardRef(({
515
+ var PromptInputTextarea = React2.forwardRef(({
513
516
  onChange,
514
517
  onKeyDown: externalOnKeyDown,
515
518
  className,
@@ -717,6 +720,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
717
720
  const [loading, setLoading] = useState2(false);
718
721
  const debounceRef = useRef2(null);
719
722
  const requestIdRef = useRef2(0);
723
+ const sessionValidatedRef = useRef2(false);
720
724
  const pluginsByTrigger = useMemo2(() => {
721
725
  const map = /* @__PURE__ */ new Map();
722
726
  for (const p of plugins ?? []) {
@@ -733,9 +737,14 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
733
737
  return value.slice(active.triggerIndex + 1, cursor);
734
738
  }, [active, value, textareaRef]);
735
739
  useEffect2(() => {
736
- if (!active) return;
737
- const ta = textareaRef.current;
738
- if (!ta) return;
740
+ if (!active) {
741
+ sessionValidatedRef.current = false;
742
+ return;
743
+ }
744
+ if (value[active.triggerIndex] === active.plugin.trigger) {
745
+ sessionValidatedRef.current = true;
746
+ }
747
+ if (!sessionValidatedRef.current) return;
739
748
  if (value[active.triggerIndex] !== active.plugin.trigger) {
740
749
  setActive(null);
741
750
  return;
@@ -743,7 +752,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
743
752
  if (/\s/.test(query)) {
744
753
  setActive(null);
745
754
  }
746
- }, [active, value, query, textareaRef]);
755
+ }, [active, value, query]);
747
756
  useEffect2(() => {
748
757
  if (!active) {
749
758
  setItems([]);
@@ -751,13 +760,20 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
751
760
  return;
752
761
  }
753
762
  if (debounceRef.current) clearTimeout(debounceRef.current);
754
- setLoading(true);
755
763
  const reqId = ++requestIdRef.current;
764
+ const result = active.plugin.fetch(query);
765
+ if (Array.isArray(result)) {
766
+ setItems(result);
767
+ setHighlight(0);
768
+ setLoading(false);
769
+ return;
770
+ }
771
+ setLoading(true);
756
772
  debounceRef.current = setTimeout(async () => {
757
773
  try {
758
- const result = await active.plugin.fetch(query);
774
+ const items2 = await result;
759
775
  if (reqId !== requestIdRef.current) return;
760
- setItems(result);
776
+ setItems(items2);
761
777
  setHighlight(0);
762
778
  } catch (err) {
763
779
  console.error("[input-plugin] fetch failed:", err);
@@ -838,14 +854,8 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
838
854
  },
839
855
  [active, items, highlight, selectItem, close, pluginsByTrigger, value]
840
856
  );
841
- useEffect2(() => {
842
- if (!active) return;
843
- if (value[active.triggerIndex] !== active.plugin.trigger) {
844
- close();
845
- }
846
- }, [value, active, close]);
847
- const popover = active && (loading || items.length > 0 || active.plugin.emptyText) ? /* @__PURE__ */ jsx10(
848
- PluginPopover,
857
+ const panel = active ? /* @__PURE__ */ jsx10(
858
+ PluginPanel,
849
859
  {
850
860
  plugin: active.plugin,
851
861
  items,
@@ -855,43 +865,122 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
855
865
  onSelect: selectItem
856
866
  }
857
867
  ) : null;
858
- return { onKeyDown, popover, isOpen: !!active };
868
+ return { onKeyDown, panel, isOpen: !!active };
859
869
  }
860
- function PluginPopover({ plugin, items, loading, highlight, onHover, onSelect }) {
870
+ function PluginPanel({ plugin, items, loading, highlight, onHover, onSelect }) {
871
+ const viewportRef = useRef2(null);
872
+ const itemRefs = useRef2([]);
873
+ useEffect2(() => {
874
+ const btn = itemRefs.current[highlight];
875
+ const viewport = viewportRef.current;
876
+ if (!btn || !viewport) return;
877
+ const btnRect = btn.getBoundingClientRect();
878
+ const viewRect = viewport.getBoundingClientRect();
879
+ if (btnRect.top < viewRect.top) {
880
+ viewport.scrollTop -= viewRect.top - btnRect.top;
881
+ } else if (btnRect.bottom > viewRect.bottom) {
882
+ viewport.scrollTop += btnRect.bottom - viewRect.bottom;
883
+ }
884
+ }, [highlight]);
861
885
  return /* @__PURE__ */ jsxs6(
862
886
  "div",
863
887
  {
864
888
  role: "listbox",
865
- className: cn(
866
- "absolute bottom-full left-0 right-0 mb-2 z-30",
867
- "rounded-md border border-border bg-popover text-popover-foreground shadow-md",
868
- "max-h-64 overflow-y-auto"
869
- ),
889
+ className: "rounded-t-xl bg-[hsl(var(--chat-background))] overflow-hidden mx-auto",
870
890
  onMouseDown: (e) => e.preventDefault(),
891
+ style: {
892
+ width: "96%",
893
+ borderTop: "1px solid var(--chat-divider)",
894
+ borderLeft: "1px solid var(--chat-divider)",
895
+ borderRight: "1px solid var(--chat-divider)",
896
+ // Pull down 1px so our bottom edge overlaps the form's top
897
+ // border, removing the visible seam between the two surfaces.
898
+ marginBottom: -1
899
+ },
871
900
  children: [
872
- plugin.heading && /* @__PURE__ */ jsx10("div", { className: "px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground border-b border-border", children: plugin.heading }),
873
- loading && items.length === 0 && /* @__PURE__ */ jsx10("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "Loading\u2026" }),
874
- !loading && items.length === 0 && /* @__PURE__ */ jsx10("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: plugin.emptyText ?? "No results" }),
875
- items.map((item, idx) => /* @__PURE__ */ jsxs6(
876
- "button",
901
+ plugin.heading && /* @__PURE__ */ jsx10(
902
+ "div",
877
903
  {
878
- type: "button",
879
- role: "option",
880
- "aria-selected": idx === highlight,
881
- onMouseEnter: () => onHover(idx),
882
- onClick: () => onSelect(item),
883
- className: cn(
884
- "w-full text-left px-3 py-2 text-sm transition-colors",
885
- "flex flex-col gap-0.5",
886
- idx === highlight ? "bg-accent text-accent-foreground" : "hover:bg-muted"
887
- ),
888
- children: [
889
- /* @__PURE__ */ jsx10("span", { className: "font-medium leading-tight", children: item.label }),
890
- item.sublabel && /* @__PURE__ */ jsx10("span", { className: "text-[11px] text-muted-foreground leading-tight", children: item.sublabel })
891
- ]
892
- },
893
- item.id
894
- ))
904
+ className: "px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide",
905
+ style: {
906
+ color: "hsl(var(--chat-text)/0.5)",
907
+ borderBottom: "1px solid var(--chat-divider)"
908
+ },
909
+ children: plugin.heading
910
+ }
911
+ ),
912
+ loading && items.length === 0 && /* @__PURE__ */ jsx10(
913
+ "div",
914
+ {
915
+ className: "px-3 py-2 text-[13px]",
916
+ style: { color: "hsl(var(--chat-text)/0.5)" },
917
+ children: "Loading\u2026"
918
+ }
919
+ ),
920
+ !loading && items.length === 0 && /* @__PURE__ */ jsx10(
921
+ "div",
922
+ {
923
+ className: "px-3 py-2 text-[13px]",
924
+ style: { color: "hsl(var(--chat-text)/0.5)" },
925
+ children: plugin.emptyText ?? "No results"
926
+ }
927
+ ),
928
+ items.length > 0 && /* @__PURE__ */ jsx10(
929
+ "div",
930
+ {
931
+ ref: viewportRef,
932
+ className: "max-h-[200px] overflow-y-auto",
933
+ children: /* @__PURE__ */ jsx10("div", { className: "py-1", children: items.map((item, idx) => /* @__PURE__ */ jsxs6("div", { children: [
934
+ /* @__PURE__ */ jsxs6(
935
+ "button",
936
+ {
937
+ ref: (el) => {
938
+ itemRefs.current[idx] = el;
939
+ },
940
+ type: "button",
941
+ role: "option",
942
+ "aria-selected": idx === highlight,
943
+ onMouseEnter: () => onHover(idx),
944
+ onClick: () => onSelect(item),
945
+ className: cn(
946
+ "w-full text-left px-3 py-2",
947
+ "flex items-center justify-between gap-3",
948
+ "transition-colors duration-150 ease-out",
949
+ "cursor-pointer"
950
+ ),
951
+ style: {
952
+ backgroundColor: idx === highlight ? "hsl(var(--chat-text)/0.06)" : "transparent"
953
+ },
954
+ children: [
955
+ /* @__PURE__ */ jsx10(
956
+ "span",
957
+ {
958
+ className: "text-[13px] truncate",
959
+ style: { color: "hsl(var(--chat-text)/0.85)" },
960
+ children: item.label
961
+ }
962
+ ),
963
+ item.sublabel && /* @__PURE__ */ jsx10(
964
+ "span",
965
+ {
966
+ className: "text-[11px] flex-shrink-0",
967
+ style: { color: "hsl(var(--chat-text)/0.4)" },
968
+ children: item.sublabel
969
+ }
970
+ )
971
+ ]
972
+ }
973
+ ),
974
+ idx < items.length - 1 && /* @__PURE__ */ jsx10(
975
+ "div",
976
+ {
977
+ className: "h-px mx-3",
978
+ style: { backgroundColor: "var(--chat-divider)" }
979
+ }
980
+ )
981
+ ] }, item.id)) })
982
+ }
983
+ )
895
984
  ]
896
985
  }
897
986
  );
@@ -2349,21 +2438,19 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2349
2438
  }
2350
2439
  }
2351
2440
  ),
2441
+ inputPlugins.panel,
2352
2442
  /* @__PURE__ */ jsxs13(PromptInput, { onSubmit: handleSubmit, globalDrop: true, multiple: true, accept: "image/*", children: [
2353
2443
  /* @__PURE__ */ jsxs13(PromptInputBody, { children: [
2354
2444
  /* @__PURE__ */ jsx21(PromptInputAttachments, { children: (attachment) => /* @__PURE__ */ jsx21(PromptInputAttachment, { data: attachment }) }),
2355
- /* @__PURE__ */ jsxs13("div", { className: "relative", children: [
2356
- /* @__PURE__ */ jsx21(
2357
- PromptInputTextarea,
2358
- {
2359
- ref: inputRef,
2360
- onChange: (e) => setInput(e.target.value),
2361
- onKeyDown: inputPlugins.onKeyDown,
2362
- value: input
2363
- }
2364
- ),
2365
- inputPlugins.popover
2366
- ] })
2445
+ /* @__PURE__ */ jsx21(
2446
+ PromptInputTextarea,
2447
+ {
2448
+ ref: inputRef,
2449
+ onChange: (e) => setInput(e.target.value),
2450
+ onKeyDown: inputPlugins.onKeyDown,
2451
+ value: input
2452
+ }
2453
+ )
2367
2454
  ] }),
2368
2455
  /* @__PURE__ */ jsxs13(PromptInputToolbar, { children: [
2369
2456
  /* @__PURE__ */ jsx21(PromptInputTools, { children: config?.features?.fileUpload === true && /* @__PURE__ */ jsx21(AttachButton, {}) }),
@@ -2916,9 +3003,9 @@ function useChatTheme() {
2916
3003
  }
2917
3004
 
2918
3005
  // src/ui/input.tsx
2919
- import * as React2 from "react";
3006
+ import * as React3 from "react";
2920
3007
  import { jsx as jsx23 } from "react/jsx-runtime";
2921
- var Input = React2.forwardRef(
3008
+ var Input = React3.forwardRef(
2922
3009
  ({ className, type, ...props }, ref) => {
2923
3010
  return /* @__PURE__ */ jsx23(
2924
3011
  "input",