@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.js CHANGED
@@ -220,11 +220,13 @@ var import_lucide_react3 = require("lucide-react");
220
220
  var import_jsx_runtime6 = require("react/jsx-runtime");
221
221
 
222
222
  // src/ui/textarea.tsx
223
+ var React = __toESM(require("react"));
223
224
  var import_jsx_runtime7 = require("react/jsx-runtime");
224
- function Textarea({ className, ...props }) {
225
+ var Textarea = React.forwardRef(({ className, ...props }, ref) => {
225
226
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
226
227
  "textarea",
227
228
  {
229
+ ref,
228
230
  "data-slot": "textarea",
229
231
  className: cn(
230
232
  "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",
@@ -233,7 +235,8 @@ function Textarea({ className, ...props }) {
233
235
  ...props
234
236
  }
235
237
  );
236
- }
238
+ });
239
+ Textarea.displayName = "Textarea";
237
240
 
238
241
  // src/components/prompt-input.tsx
239
242
  var import_lucide_react4 = require("lucide-react");
@@ -747,6 +750,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
747
750
  const [loading, setLoading] = (0, import_react3.useState)(false);
748
751
  const debounceRef = (0, import_react3.useRef)(null);
749
752
  const requestIdRef = (0, import_react3.useRef)(0);
753
+ const sessionValidatedRef = (0, import_react3.useRef)(false);
750
754
  const pluginsByTrigger = (0, import_react3.useMemo)(() => {
751
755
  const map = /* @__PURE__ */ new Map();
752
756
  for (const p of plugins ?? []) {
@@ -763,9 +767,14 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
763
767
  return value.slice(active.triggerIndex + 1, cursor);
764
768
  }, [active, value, textareaRef]);
765
769
  (0, import_react3.useEffect)(() => {
766
- if (!active) return;
767
- const ta = textareaRef.current;
768
- 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;
769
778
  if (value[active.triggerIndex] !== active.plugin.trigger) {
770
779
  setActive(null);
771
780
  return;
@@ -773,7 +782,7 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
773
782
  if (/\s/.test(query)) {
774
783
  setActive(null);
775
784
  }
776
- }, [active, value, query, textareaRef]);
785
+ }, [active, value, query]);
777
786
  (0, import_react3.useEffect)(() => {
778
787
  if (!active) {
779
788
  setItems([]);
@@ -781,13 +790,20 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
781
790
  return;
782
791
  }
783
792
  if (debounceRef.current) clearTimeout(debounceRef.current);
784
- setLoading(true);
785
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);
786
802
  debounceRef.current = setTimeout(async () => {
787
803
  try {
788
- const result = await active.plugin.fetch(query);
804
+ const items2 = await result;
789
805
  if (reqId !== requestIdRef.current) return;
790
- setItems(result);
806
+ setItems(items2);
791
807
  setHighlight(0);
792
808
  } catch (err) {
793
809
  console.error("[input-plugin] fetch failed:", err);
@@ -868,14 +884,8 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
868
884
  },
869
885
  [active, items, highlight, selectItem, close, pluginsByTrigger, value]
870
886
  );
871
- (0, import_react3.useEffect)(() => {
872
- if (!active) return;
873
- if (value[active.triggerIndex] !== active.plugin.trigger) {
874
- close();
875
- }
876
- }, [value, active, close]);
877
- const popover = active && (loading || items.length > 0 || active.plugin.emptyText) ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
878
- PluginPopover,
887
+ const panel = active ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
888
+ PluginPanel,
879
889
  {
880
890
  plugin: active.plugin,
881
891
  items,
@@ -885,43 +895,122 @@ function useInputPlugins({ textareaRef, value, setValue, plugins }) {
885
895
  onSelect: selectItem
886
896
  }
887
897
  ) : null;
888
- return { onKeyDown, popover, isOpen: !!active };
898
+ return { onKeyDown, panel, isOpen: !!active };
889
899
  }
890
- 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]);
891
915
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
892
916
  "div",
893
917
  {
894
918
  role: "listbox",
895
- className: cn(
896
- "absolute bottom-full left-0 right-0 mb-2 z-30",
897
- "rounded-md border border-border bg-popover text-popover-foreground shadow-md",
898
- "max-h-64 overflow-y-auto"
899
- ),
919
+ className: "rounded-t-xl bg-[hsl(var(--chat-background))] overflow-hidden mx-auto",
900
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
+ },
901
930
  children: [
902
- 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 }),
903
- loading && items.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "Loading\u2026" }),
904
- !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" }),
905
- items.map((item, idx) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
906
- "button",
931
+ plugin.heading && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
932
+ "div",
907
933
  {
908
- type: "button",
909
- role: "option",
910
- "aria-selected": idx === highlight,
911
- onMouseEnter: () => onHover(idx),
912
- onClick: () => onSelect(item),
913
- className: cn(
914
- "w-full text-left px-3 py-2 text-sm transition-colors",
915
- "flex flex-col gap-0.5",
916
- idx === highlight ? "bg-accent text-accent-foreground" : "hover:bg-muted"
917
- ),
918
- children: [
919
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "font-medium leading-tight", children: item.label }),
920
- item.sublabel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-[11px] text-muted-foreground leading-tight", children: item.sublabel })
921
- ]
922
- },
923
- item.id
924
- ))
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
+ )
925
1014
  ]
926
1015
  }
927
1016
  );
@@ -2369,21 +2458,19 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2369
2458
  }
2370
2459
  }
2371
2460
  ),
2461
+ inputPlugins.panel,
2372
2462
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInput, { onSubmit: handleSubmit, globalDrop: true, multiple: true, accept: "image/*", children: [
2373
2463
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInputBody, { children: [
2374
2464
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputAttachments, { children: (attachment) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputAttachment, { data: attachment }) }),
2375
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "relative", children: [
2376
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2377
- PromptInputTextarea,
2378
- {
2379
- ref: inputRef,
2380
- onChange: (e) => setInput(e.target.value),
2381
- onKeyDown: inputPlugins.onKeyDown,
2382
- value: input
2383
- }
2384
- ),
2385
- inputPlugins.popover
2386
- ] })
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
+ )
2387
2474
  ] }),
2388
2475
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(PromptInputToolbar, { children: [
2389
2476
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PromptInputTools, { children: config?.features?.fileUpload === true && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AttachButton, {}) }),
@@ -2936,9 +3023,9 @@ function useChatTheme() {
2936
3023
  }
2937
3024
 
2938
3025
  // src/ui/input.tsx
2939
- var React2 = __toESM(require("react"));
3026
+ var React3 = __toESM(require("react"));
2940
3027
  var import_jsx_runtime23 = require("react/jsx-runtime");
2941
- var Input = React2.forwardRef(
3028
+ var Input = React3.forwardRef(
2942
3029
  ({ className, type, ...props }, ref) => {
2943
3030
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2944
3031
  "input",