@mordn/chat-widget 0.6.2 → 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
@@ -720,6 +720,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
720
720
  const [loading, setLoading] = useState2(false);
721
721
  const debounceRef = useRef2(null);
722
722
  const requestIdRef = useRef2(0);
723
+ const sessionValidatedRef = useRef2(false);
723
724
  const pluginsByTrigger = useMemo2(() => {
724
725
  const map = /* @__PURE__ */ new Map();
725
726
  for (const p of plugins ?? []) {
@@ -736,9 +737,14 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
736
737
  return value.slice(active.triggerIndex + 1, cursor);
737
738
  }, [active, value, textareaRef]);
738
739
  useEffect2(() => {
739
- if (!active) return;
740
- const ta = textareaRef.current;
741
- 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;
742
748
  if (value[active.triggerIndex] !== active.plugin.trigger) {
743
749
  setActive(null);
744
750
  return;
@@ -746,7 +752,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
746
752
  if (/\s/.test(query)) {
747
753
  setActive(null);
748
754
  }
749
- }, [active, value, query, textareaRef]);
755
+ }, [active, value, query]);
750
756
  useEffect2(() => {
751
757
  if (!active) {
752
758
  setItems([]);
@@ -754,13 +760,20 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
754
760
  return;
755
761
  }
756
762
  if (debounceRef.current) clearTimeout(debounceRef.current);
757
- setLoading(true);
758
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);
759
772
  debounceRef.current = setTimeout(async () => {
760
773
  try {
761
- const result = await active.plugin.fetch(query);
774
+ const items2 = await result;
762
775
  if (reqId !== requestIdRef.current) return;
763
- setItems(result);
776
+ setItems(items2);
764
777
  setHighlight(0);
765
778
  } catch (err) {
766
779
  console.error("[input-plugin] fetch failed:", err);
@@ -841,14 +854,8 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
841
854
  },
842
855
  [active, items, highlight, selectItem, close, pluginsByTrigger, value]
843
856
  );
844
- useEffect2(() => {
845
- if (!active) return;
846
- if (value[active.triggerIndex] !== active.plugin.trigger) {
847
- close();
848
- }
849
- }, [value, active, close]);
850
- const popover = active && (loading || items.length > 0 || active.plugin.emptyText) ? /* @__PURE__ */ jsx10(
851
- PluginPopover,
857
+ const panel = active ? /* @__PURE__ */ jsx10(
858
+ PluginPanel,
852
859
  {
853
860
  plugin: active.plugin,
854
861
  items,
@@ -858,43 +865,122 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
858
865
  onSelect: selectItem
859
866
  }
860
867
  ) : null;
861
- return { onKeyDown, popover, isOpen: !!active };
868
+ return { onKeyDown, panel, isOpen: !!active };
862
869
  }
863
- 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]);
864
885
  return /* @__PURE__ */ jsxs6(
865
886
  "div",
866
887
  {
867
888
  role: "listbox",
868
- className: cn(
869
- "absolute bottom-full left-0 right-0 mb-2 z-30",
870
- "rounded-md border border-border bg-popover text-popover-foreground shadow-md",
871
- "max-h-64 overflow-y-auto"
872
- ),
889
+ className: "rounded-t-xl bg-[hsl(var(--chat-background))] overflow-hidden mx-auto",
873
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
+ },
874
900
  children: [
875
- 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 }),
876
- loading && items.length === 0 && /* @__PURE__ */ jsx10("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "Loading\u2026" }),
877
- !loading && items.length === 0 && /* @__PURE__ */ jsx10("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: plugin.emptyText ?? "No results" }),
878
- items.map((item, idx) => /* @__PURE__ */ jsxs6(
879
- "button",
901
+ plugin.heading && /* @__PURE__ */ jsx10(
902
+ "div",
880
903
  {
881
- type: "button",
882
- role: "option",
883
- "aria-selected": idx === highlight,
884
- onMouseEnter: () => onHover(idx),
885
- onClick: () => onSelect(item),
886
- className: cn(
887
- "w-full text-left px-3 py-2 text-sm transition-colors",
888
- "flex flex-col gap-0.5",
889
- idx === highlight ? "bg-accent text-accent-foreground" : "hover:bg-muted"
890
- ),
891
- children: [
892
- /* @__PURE__ */ jsx10("span", { className: "font-medium leading-tight", children: item.label }),
893
- item.sublabel && /* @__PURE__ */ jsx10("span", { className: "text-[11px] text-muted-foreground leading-tight", children: item.sublabel })
894
- ]
895
- },
896
- item.id
897
- ))
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
+ )
898
984
  ]
899
985
  }
900
986
  );
@@ -2352,21 +2438,19 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2352
2438
  }
2353
2439
  }
2354
2440
  ),
2441
+ inputPlugins.panel,
2355
2442
  /* @__PURE__ */ jsxs13(PromptInput, { onSubmit: handleSubmit, globalDrop: true, multiple: true, accept: "image/*", children: [
2356
2443
  /* @__PURE__ */ jsxs13(PromptInputBody, { children: [
2357
2444
  /* @__PURE__ */ jsx21(PromptInputAttachments, { children: (attachment) => /* @__PURE__ */ jsx21(PromptInputAttachment, { data: attachment }) }),
2358
- /* @__PURE__ */ jsxs13("div", { className: "relative", children: [
2359
- /* @__PURE__ */ jsx21(
2360
- PromptInputTextarea,
2361
- {
2362
- ref: inputRef,
2363
- onChange: (e) => setInput(e.target.value),
2364
- onKeyDown: inputPlugins.onKeyDown,
2365
- value: input
2366
- }
2367
- ),
2368
- inputPlugins.popover
2369
- ] })
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
+ )
2370
2454
  ] }),
2371
2455
  /* @__PURE__ */ jsxs13(PromptInputToolbar, { children: [
2372
2456
  /* @__PURE__ */ jsx21(PromptInputTools, { children: config?.features?.fileUpload === true && /* @__PURE__ */ jsx21(AttachButton, {}) }),