@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.js CHANGED
@@ -750,6 +750,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
750
750
  const [loading, setLoading] = (0, import_react3.useState)(false);
751
751
  const debounceRef = (0, import_react3.useRef)(null);
752
752
  const requestIdRef = (0, import_react3.useRef)(0);
753
+ const sessionValidatedRef = (0, import_react3.useRef)(false);
753
754
  const pluginsByTrigger = (0, import_react3.useMemo)(() => {
754
755
  const map = /* @__PURE__ */ new Map();
755
756
  for (const p of plugins ?? []) {
@@ -766,9 +767,14 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
766
767
  return value.slice(active.triggerIndex + 1, cursor);
767
768
  }, [active, value, textareaRef]);
768
769
  (0, import_react3.useEffect)(() => {
769
- if (!active) return;
770
- const ta = textareaRef.current;
771
- if (!ta) return;
770
+ if (!active) {
771
+ sessionValidatedRef.current = false;
772
+ return;
773
+ }
774
+ if (value[active.triggerIndex] === active.plugin.trigger) {
775
+ sessionValidatedRef.current = true;
776
+ }
777
+ if (!sessionValidatedRef.current) return;
772
778
  if (value[active.triggerIndex] !== active.plugin.trigger) {
773
779
  setActive(null);
774
780
  return;
@@ -776,7 +782,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
776
782
  if (/\s/.test(query)) {
777
783
  setActive(null);
778
784
  }
779
- }, [active, value, query, textareaRef]);
785
+ }, [active, value, query]);
780
786
  (0, import_react3.useEffect)(() => {
781
787
  if (!active) {
782
788
  setItems([]);
@@ -784,13 +790,20 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
784
790
  return;
785
791
  }
786
792
  if (debounceRef.current) clearTimeout(debounceRef.current);
787
- setLoading(true);
788
793
  const reqId = ++requestIdRef.current;
794
+ const result = active.plugin.fetch(query);
795
+ if (Array.isArray(result)) {
796
+ setItems(result);
797
+ setHighlight(0);
798
+ setLoading(false);
799
+ return;
800
+ }
801
+ setLoading(true);
789
802
  debounceRef.current = setTimeout(async () => {
790
803
  try {
791
- const result = await active.plugin.fetch(query);
804
+ const items2 = await result;
792
805
  if (reqId !== requestIdRef.current) return;
793
- setItems(result);
806
+ setItems(items2);
794
807
  setHighlight(0);
795
808
  } catch (err) {
796
809
  console.error("[input-plugin] fetch failed:", err);
@@ -871,14 +884,8 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
871
884
  },
872
885
  [active, items, highlight, selectItem, close, pluginsByTrigger, value]
873
886
  );
874
- (0, import_react3.useEffect)(() => {
875
- if (!active) return;
876
- if (value[active.triggerIndex] !== active.plugin.trigger) {
877
- close();
878
- }
879
- }, [value, active, close]);
880
- const popover = active && (loading || items.length > 0 || active.plugin.emptyText) ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
881
- PluginPopover,
887
+ const panel = active ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
888
+ PluginPanel,
882
889
  {
883
890
  plugin: active.plugin,
884
891
  items,
@@ -888,43 +895,122 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
888
895
  onSelect: selectItem
889
896
  }
890
897
  ) : null;
891
- return { onKeyDown, popover, isOpen: !!active };
898
+ return { onKeyDown, panel, isOpen: !!active };
892
899
  }
893
- function PluginPopover({ plugin, items, loading, highlight, onHover, onSelect }) {
900
+ function PluginPanel({ plugin, items, loading, highlight, onHover, onSelect }) {
901
+ const viewportRef = (0, import_react3.useRef)(null);
902
+ const itemRefs = (0, import_react3.useRef)([]);
903
+ (0, import_react3.useEffect)(() => {
904
+ const btn = itemRefs.current[highlight];
905
+ const viewport = viewportRef.current;
906
+ if (!btn || !viewport) return;
907
+ const btnRect = btn.getBoundingClientRect();
908
+ const viewRect = viewport.getBoundingClientRect();
909
+ if (btnRect.top < viewRect.top) {
910
+ viewport.scrollTop -= viewRect.top - btnRect.top;
911
+ } else if (btnRect.bottom > viewRect.bottom) {
912
+ viewport.scrollTop += btnRect.bottom - viewRect.bottom;
913
+ }
914
+ }, [highlight]);
894
915
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
895
916
  "div",
896
917
  {
897
918
  role: "listbox",
898
- className: cn(
899
- "absolute bottom-full left-0 right-0 mb-2 z-30",
900
- "rounded-md border border-border bg-popover text-popover-foreground shadow-md",
901
- "max-h-64 overflow-y-auto"
902
- ),
919
+ className: "rounded-t-xl bg-[hsl(var(--chat-background))] overflow-hidden mx-auto",
903
920
  onMouseDown: (e) => e.preventDefault(),
921
+ style: {
922
+ width: "96%",
923
+ borderTop: "1px solid var(--chat-divider)",
924
+ borderLeft: "1px solid var(--chat-divider)",
925
+ borderRight: "1px solid var(--chat-divider)",
926
+ // Pull down 1px so our bottom edge overlaps the form's top
927
+ // border, removing the visible seam between the two surfaces.
928
+ marginBottom: -1
929
+ },
904
930
  children: [
905
- plugin.heading && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground border-b border-border", children: plugin.heading }),
906
- loading && items.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "Loading\u2026" }),
907
- !loading && items.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: plugin.emptyText ?? "No results" }),
908
- items.map((item, idx) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
909
- "button",
931
+ plugin.heading && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
932
+ "div",
910
933
  {
911
- type: "button",
912
- role: "option",
913
- "aria-selected": idx === highlight,
914
- onMouseEnter: () => onHover(idx),
915
- onClick: () => onSelect(item),
916
- className: cn(
917
- "w-full text-left px-3 py-2 text-sm transition-colors",
918
- "flex flex-col gap-0.5",
919
- idx === highlight ? "bg-accent text-accent-foreground" : "hover:bg-muted"
920
- ),
921
- children: [
922
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "font-medium leading-tight", children: item.label }),
923
- item.sublabel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-[11px] text-muted-foreground leading-tight", children: item.sublabel })
924
- ]
925
- },
926
- item.id
927
- ))
934
+ className: "px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide",
935
+ style: {
936
+ color: "hsl(var(--chat-text)/0.5)",
937
+ borderBottom: "1px solid var(--chat-divider)"
938
+ },
939
+ children: plugin.heading
940
+ }
941
+ ),
942
+ loading && items.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
943
+ "div",
944
+ {
945
+ className: "px-3 py-2 text-[13px]",
946
+ style: { color: "hsl(var(--chat-text)/0.5)" },
947
+ children: "Loading\u2026"
948
+ }
949
+ ),
950
+ !loading && items.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
951
+ "div",
952
+ {
953
+ className: "px-3 py-2 text-[13px]",
954
+ style: { color: "hsl(var(--chat-text)/0.5)" },
955
+ children: plugin.emptyText ?? "No results"
956
+ }
957
+ ),
958
+ items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
959
+ "div",
960
+ {
961
+ ref: viewportRef,
962
+ className: "max-h-[200px] overflow-y-auto",
963
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "py-1", children: items.map((item, idx) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
964
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
965
+ "button",
966
+ {
967
+ ref: (el) => {
968
+ itemRefs.current[idx] = el;
969
+ },
970
+ type: "button",
971
+ role: "option",
972
+ "aria-selected": idx === highlight,
973
+ onMouseEnter: () => onHover(idx),
974
+ onClick: () => onSelect(item),
975
+ className: cn(
976
+ "w-full text-left px-3 py-2",
977
+ "flex items-center justify-between gap-3",
978
+ "transition-colors duration-150 ease-out",
979
+ "cursor-pointer"
980
+ ),
981
+ style: {
982
+ backgroundColor: idx === highlight ? "hsl(var(--chat-text)/0.06)" : "transparent"
983
+ },
984
+ children: [
985
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
986
+ "span",
987
+ {
988
+ className: "text-[13px] truncate",
989
+ style: { color: "hsl(var(--chat-text)/0.85)" },
990
+ children: item.label
991
+ }
992
+ ),
993
+ item.sublabel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
994
+ "span",
995
+ {
996
+ className: "text-[11px] flex-shrink-0",
997
+ style: { color: "hsl(var(--chat-text)/0.4)" },
998
+ children: item.sublabel
999
+ }
1000
+ )
1001
+ ]
1002
+ }
1003
+ ),
1004
+ idx < items.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1005
+ "div",
1006
+ {
1007
+ className: "h-px mx-3",
1008
+ style: { backgroundColor: "var(--chat-divider)" }
1009
+ }
1010
+ )
1011
+ ] }, item.id)) })
1012
+ }
1013
+ )
928
1014
  ]
929
1015
  }
930
1016
  );
@@ -2372,21 +2458,19 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2372
2458
  }
2373
2459
  }
2374
2460
  ),
2461
+ inputPlugins.panel,
2375
2462
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInput, { onSubmit: handleSubmit, globalDrop: true, multiple: true, accept: "image/*", children: [
2376
2463
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInputBody, { children: [
2377
2464
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputAttachments, { children: (attachment) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputAttachment, { data: attachment }) }),
2378
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "relative", children: [
2379
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2380
- PromptInputTextarea,
2381
- {
2382
- ref: inputRef,
2383
- onChange: (e) => setInput(e.target.value),
2384
- onKeyDown: inputPlugins.onKeyDown,
2385
- value: input
2386
- }
2387
- ),
2388
- inputPlugins.popover
2389
- ] })
2465
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2466
+ PromptInputTextarea,
2467
+ {
2468
+ ref: inputRef,
2469
+ onChange: (e) => setInput(e.target.value),
2470
+ onKeyDown: inputPlugins.onKeyDown,
2471
+ value: input
2472
+ }
2473
+ )
2390
2474
  ] }),
2391
2475
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInputToolbar, { children: [
2392
2476
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputTools, { children: config?.features?.fileUpload === true && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AttachButton, {}) }),