@signalflare-ai/ui 1.0.0 → 1.1.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.
@@ -2,7 +2,6 @@
2
2
  import { t as cn } from "./cn-YROP2_ox.js";
3
3
  import { t as Tooltip } from "./tooltip-Cb7QW-7H.js";
4
4
  import { t as Button } from "./button-De0267YU.js";
5
- import { t as InputGroup } from "./input-DddtBN-g.js";
6
5
  import { Fragment, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
7
  import { ArrowUpIcon, ArrowsClockwiseIcon, CaretDownIcon, CheckIcon, FileIcon, FileTextIcon, ImageIcon, MicrophoneIcon, PaperclipIcon, PlusIcon, SpinnerGapIcon, SquareIcon, XIcon } from "@phosphor-icons/react";
@@ -442,7 +441,8 @@ var useStore = (source, selector = (s) => s, compare) => useSelector(source, sel
442
441
  */
443
442
  var DEFAULT_REQUEST = {
444
443
  text: "",
445
- files: []
444
+ files: [],
445
+ tags: []
446
446
  };
447
447
  var DEFAULT_DISPLAY = { sendStatus: "idle" };
448
448
  function createPromptInputRequestController(options = {}) {
@@ -494,13 +494,60 @@ function createPromptInputRequestController(options = {}) {
494
494
  };
495
495
  });
496
496
  },
497
+ addTags(tags) {
498
+ request.setState((prev) => {
499
+ const existing = prev.tags ?? [];
500
+ const incoming = tags.filter((tag) => !existing.some((item) => item.id === tag.id));
501
+ return {
502
+ ...prev,
503
+ tags: [...existing, ...incoming]
504
+ };
505
+ });
506
+ },
507
+ insertTag(tag, range) {
508
+ request.setState((prev) => {
509
+ const mention = tag.mention ?? `@${tag.label.replace(/\s+/g, "-").toLowerCase()}`;
510
+ const text = prev.text ?? "";
511
+ const start = range?.start ?? text.length;
512
+ const end = range?.end ?? text.length;
513
+ const prefix = text.slice(0, start);
514
+ const suffix = text.slice(end);
515
+ const needsPrefixSpace = prefix.length > 0 && !/\s$/.test(prefix);
516
+ const needsSuffixSpace = suffix.length > 0 && !/^\s/.test(suffix);
517
+ const nextText = `${prefix}${needsPrefixSpace ? " " : ""}${mention}${needsSuffixSpace ? " " : ""}${suffix}`;
518
+ const nextTag = {
519
+ ...tag,
520
+ mention
521
+ };
522
+ const existing = prev.tags ?? [];
523
+ const nextTags = existing.some((item) => item.id === tag.id) ? existing.map((item) => item.id === tag.id ? nextTag : item) : [...existing, nextTag];
524
+ return {
525
+ ...prev,
526
+ text: nextText,
527
+ tags: nextTags
528
+ };
529
+ });
530
+ },
531
+ removeTag(id) {
532
+ request.setState((prev) => ({
533
+ ...prev,
534
+ tags: (prev.tags ?? []).filter((tag) => tag.id !== id)
535
+ }));
536
+ },
537
+ clearTags() {
538
+ request.setState((prev) => ({
539
+ ...prev,
540
+ tags: []
541
+ }));
542
+ },
497
543
  resetRequest() {
498
544
  request.setState((prev) => {
499
545
  for (const f of prev.files) if (f.url.startsWith("blob:")) URL.revokeObjectURL(f.url);
500
546
  return {
501
547
  ...prev,
502
548
  text: "",
503
- files: []
549
+ files: [],
550
+ tags: []
504
551
  };
505
552
  });
506
553
  },
@@ -698,9 +745,10 @@ function makeAttachmentId() {
698
745
  * </PromptInput>
699
746
  * ```
700
747
  */
701
- var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileSize, onError, onSubmit, children, backLayer, backLayerTitle = "Context", backLayerOpen, onBackLayerOpenChange, autoOpenBackLayerWhen, ...props }) => {
748
+ function PromptInput({ className, accept, multiple, globalDrop, maxFiles, maxFileSize, onError, onSubmit, children, backLayer, backLayerTitle = "Context", backLayerOpen, onBackLayerOpenChange, autoOpenBackLayerWhen, ...props }) {
702
749
  const hasBackLayer = backLayer !== void 0;
703
750
  const controller = useOptionalPromptInputController();
751
+ const requestCtrl = useOptionalPromptInputRequestController();
704
752
  const usingProvider = !!controller;
705
753
  const inputRef = useRef(null);
706
754
  const anchorRef = useRef(null);
@@ -714,6 +762,7 @@ var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileS
714
762
  }, [autoOpenBackLayerWhen, onBackLayerOpenChange]);
715
763
  const [items, setItems] = useState([]);
716
764
  const files = usingProvider ? controller.attachments.files : items;
765
+ const tags = useStore(requestCtrl?.request ?? FALLBACK_REQUEST_STORE, (state) => state.tags ?? []);
717
766
  const openFileDialogLocal = useCallback(() => {
718
767
  inputRef.current?.click();
719
768
  }, []);
@@ -837,7 +886,7 @@ var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileS
837
886
  const handleSubmit = (event) => {
838
887
  event.preventDefault();
839
888
  const form = event.currentTarget;
840
- const text = usingProvider ? controller.textInput.value : new FormData(form).get("message") || "";
889
+ const text = requestCtrl ? requestCtrl.request.state.text : usingProvider ? controller.textInput.value : new FormData(form).get("message") || "";
841
890
  if (!usingProvider) form.reset();
842
891
  const doSubmit = async () => {
843
892
  const convertedFiles = await Promise.all(files.map(async (item) => {
@@ -850,10 +899,14 @@ var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileS
850
899
  try {
851
900
  await onSubmit({
852
901
  text,
853
- files: convertedFiles
902
+ files: convertedFiles,
903
+ tags
854
904
  }, event);
855
- clear();
856
- if (usingProvider) controller.textInput.clear();
905
+ if (requestCtrl) requestCtrl.resetRequest();
906
+ else {
907
+ clear();
908
+ if (usingProvider) controller.textInput.clear();
909
+ }
857
910
  } catch {}
858
911
  };
859
912
  doSubmit().catch(() => {});
@@ -878,11 +931,14 @@ var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileS
878
931
  children: backLayer
879
932
  }),
880
933
  /* @__PURE__ */ jsx("div", {
881
- className: "flex flex-col overflow-hidden rounded-xl bg-sf-base ring ring-sf-line/50 focus-within:ring-sf-ring",
934
+ className: "flex flex-col overflow-visible rounded-xl bg-sf-base ring ring-sf-line/50 focus-within:ring-sf-ring",
882
935
  children
883
936
  })
884
937
  ]
885
- }) : /* @__PURE__ */ jsx(InputGroup, { children });
938
+ }) : /* @__PURE__ */ jsx("div", {
939
+ className: "flex w-full flex-col items-stretch overflow-visible rounded-xl border-0 bg-sf-base shadow-xs ring ring-sf-line focus-within:ring-sf-ring",
940
+ children
941
+ });
886
942
  const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [
887
943
  /* @__PURE__ */ jsx("span", {
888
944
  "aria-hidden": "true",
@@ -909,7 +965,7 @@ var PromptInput = ({ className, accept, multiple, globalDrop, maxFiles, maxFileS
909
965
  value: ctx,
910
966
  children: inner
911
967
  });
912
- };
968
+ }
913
969
  var PromptInputBody = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
914
970
  className: cn("contents", className),
915
971
  ...props
@@ -970,6 +1026,8 @@ var handleTextareaKeyDown = (e) => {
970
1026
  */
971
1027
  var PromptInputTextarea = ({ onChange, className, placeholder = "What would you like to know?", ...props }) => {
972
1028
  const controller = useOptionalPromptInputController();
1029
+ const requestCtrl = useOptionalPromptInputRequestController();
1030
+ const requestText = useStore(requestCtrl?.request ?? FALLBACK_REQUEST_STORE, (state) => typeof state.text === "string" ? state.text : "");
973
1031
  const attachments = usePromptInputAttachments();
974
1032
  const handlePaste = (event) => {
975
1033
  const items = event.clipboardData?.items;
@@ -984,7 +1042,13 @@ var PromptInputTextarea = ({ onChange, className, placeholder = "What would you
984
1042
  attachments.add(pastedFiles);
985
1043
  }
986
1044
  };
987
- const controlledProps = controller ? {
1045
+ const controlledProps = requestCtrl ? {
1046
+ value: requestText,
1047
+ onChange: (e) => {
1048
+ requestCtrl.setRequestField("text", e.currentTarget.value);
1049
+ onChange?.(e);
1050
+ }
1051
+ } : controller ? {
988
1052
  value: controller.textInput.value,
989
1053
  onChange: (e) => {
990
1054
  controller.textInput.setInput(e.currentTarget.value);
@@ -992,7 +1056,7 @@ var PromptInputTextarea = ({ onChange, className, placeholder = "What would you
992
1056
  }
993
1057
  } : { onChange };
994
1058
  return /* @__PURE__ */ jsx("textarea", {
995
- className: cn("field-sizing-content max-h-48 min-h-16 w-full resize-none border-0 bg-transparent px-3 py-2 text-sm text-sf-default outline-none placeholder:text-sf-inactive", className),
1059
+ className: cn("field-sizing-content max-h-48 min-h-16 w-full resize-none border-0 bg-transparent px-3 pt-3 pb-1 text-sm text-sf-default outline-none placeholder:text-sf-inactive", className),
996
1060
  name: "message",
997
1061
  onKeyDown: handleTextareaKeyDown,
998
1062
  onPaste: handlePaste,
@@ -1002,12 +1066,300 @@ var PromptInputTextarea = ({ onChange, className, placeholder = "What would you
1002
1066
  ...controlledProps
1003
1067
  });
1004
1068
  };
1069
+ var getTagMention = (tag) => tag.mention ?? `@${tag.label.replace(/\s+/g, "-").toLowerCase()}`;
1070
+ var findMentionAtCursor = (text, cursor) => {
1071
+ const beforeCursor = text.slice(0, cursor);
1072
+ const match = /(^|\s)@([\w-]*)$/.exec(beforeCursor);
1073
+ if (!match) return null;
1074
+ return {
1075
+ start: cursor - match[0].trimStart().length,
1076
+ end: cursor,
1077
+ query: match[2] ?? ""
1078
+ };
1079
+ };
1080
+ var renderInlineReferenceText = (text, tags) => {
1081
+ if (text.length === 0) return null;
1082
+ const mentions = tags.map((tag) => ({
1083
+ tag,
1084
+ mention: getTagMention(tag)
1085
+ })).filter((item) => item.mention.length > 0).sort((a, b) => b.mention.length - a.mention.length);
1086
+ const parts = [];
1087
+ let index = 0;
1088
+ while (index < text.length) {
1089
+ let match;
1090
+ for (const mention of mentions) if (text.startsWith(mention.mention, index)) {
1091
+ match = mention;
1092
+ break;
1093
+ }
1094
+ if (match) {
1095
+ parts.push(/* @__PURE__ */ jsx("span", {
1096
+ className: "rounded-md bg-sf-tint font-medium text-sf-strong ring-1 ring-sf-line",
1097
+ children: match.mention
1098
+ }, `${match.tag.id}-${index}`));
1099
+ index += match.mention.length;
1100
+ continue;
1101
+ }
1102
+ parts.push(text[index]);
1103
+ index += 1;
1104
+ }
1105
+ return parts;
1106
+ };
1107
+ var getTextareaCursorPosition = (textarea, wrapper) => {
1108
+ if (!wrapper) return {
1109
+ left: 12,
1110
+ top: 0
1111
+ };
1112
+ const computed = window.getComputedStyle(textarea);
1113
+ const mirror = document.createElement("div");
1114
+ const marker = document.createElement("span");
1115
+ const wrapperRect = wrapper.getBoundingClientRect();
1116
+ for (const property of [
1117
+ "borderBottomWidth",
1118
+ "borderLeftWidth",
1119
+ "borderRightWidth",
1120
+ "borderTopWidth",
1121
+ "boxSizing",
1122
+ "fontFamily",
1123
+ "fontSize",
1124
+ "fontStyle",
1125
+ "fontWeight",
1126
+ "letterSpacing",
1127
+ "lineHeight",
1128
+ "paddingBottom",
1129
+ "paddingLeft",
1130
+ "paddingRight",
1131
+ "paddingTop",
1132
+ "textTransform",
1133
+ "width",
1134
+ "wordSpacing"
1135
+ ]) mirror.style[property] = computed[property];
1136
+ mirror.style.position = "absolute";
1137
+ mirror.style.visibility = "hidden";
1138
+ mirror.style.whiteSpace = "pre-wrap";
1139
+ mirror.style.overflowWrap = "break-word";
1140
+ mirror.textContent = textarea.value.slice(0, textarea.selectionStart);
1141
+ marker.textContent = "​";
1142
+ mirror.append(marker);
1143
+ document.body.append(mirror);
1144
+ const markerRect = marker.getBoundingClientRect();
1145
+ const top = markerRect.top - mirror.getBoundingClientRect().top + textarea.offsetTop - 6;
1146
+ const left = Math.min(Math.max(markerRect.left - mirror.getBoundingClientRect().left + textarea.offsetLeft, 12), Math.max(wrapperRect.width - 300, 12));
1147
+ mirror.remove();
1148
+ return {
1149
+ left,
1150
+ top
1151
+ };
1152
+ };
1153
+ var PromptInputReferencePicker = ({ activeIndex, onSelect, options, style }) => /* @__PURE__ */ jsxs("div", {
1154
+ className: "absolute z-50 w-72 -translate-y-full overflow-hidden rounded-lg bg-sf-elevated p-1 shadow-lg ring ring-sf-line",
1155
+ style,
1156
+ children: [/* @__PURE__ */ jsx("div", {
1157
+ className: "px-2 py-1.5 text-xs font-medium text-sf-subtle",
1158
+ children: "References"
1159
+ }), /* @__PURE__ */ jsx("div", {
1160
+ className: "max-h-56 overflow-y-auto",
1161
+ children: options.map((tag, index) => /* @__PURE__ */ jsxs("button", {
1162
+ "aria-selected": index === activeIndex,
1163
+ className: cn("flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm text-sf-default hover:bg-sf-tint aria-selected:bg-sf-tint", index === activeIndex && "bg-sf-tint"),
1164
+ onMouseDown: (event) => {
1165
+ event.preventDefault();
1166
+ onSelect(tag);
1167
+ },
1168
+ role: "option",
1169
+ type: "button",
1170
+ children: [
1171
+ tag.icon && /* @__PURE__ */ jsx("span", {
1172
+ className: "mt-0.5 shrink-0",
1173
+ children: tag.icon
1174
+ }),
1175
+ /* @__PURE__ */ jsxs("span", {
1176
+ className: "min-w-0 flex-1",
1177
+ children: [/* @__PURE__ */ jsx("span", {
1178
+ className: "block truncate font-medium",
1179
+ children: tag.label
1180
+ }), tag.description && /* @__PURE__ */ jsx("span", {
1181
+ className: "block truncate text-xs text-sf-subtle",
1182
+ children: tag.description
1183
+ })]
1184
+ }),
1185
+ /* @__PURE__ */ jsx("span", {
1186
+ className: "shrink-0 text-xs text-sf-subtle",
1187
+ children: getTagMention(tag)
1188
+ })
1189
+ ]
1190
+ }, tag.id))
1191
+ })]
1192
+ });
1193
+ var PromptInputEditor = ({ className, placeholder = "What would you like to know?", onReferenceSearch, references, onChange, onKeyDown, style, ...props }) => {
1194
+ const controller = useOptionalPromptInputController();
1195
+ const requestCtrl = useOptionalPromptInputRequestController();
1196
+ const textareaRef = useRef(null);
1197
+ const wrapperRef = useRef(null);
1198
+ const [mention, setMention] = useState(null);
1199
+ const [options, setOptions] = useState([]);
1200
+ const [activeIndex, setActiveIndex] = useState(0);
1201
+ const [pickerPosition, setPickerPosition] = useState({
1202
+ left: 12,
1203
+ top: 0
1204
+ });
1205
+ const requestText = useStore(requestCtrl?.request ?? FALLBACK_REQUEST_STORE, (state) => typeof state.text === "string" ? state.text : "");
1206
+ const tags = useStore(requestCtrl?.request ?? FALLBACK_REQUEST_STORE, (state) => Array.isArray(state.tags) ? state.tags : []);
1207
+ const attachments = usePromptInputAttachments();
1208
+ const value = requestCtrl ? requestText : controller?.textInput.value ?? "";
1209
+ const updateMention = useCallback((textarea) => {
1210
+ const nextMention = findMentionAtCursor(textarea.value, textarea.selectionStart);
1211
+ if (nextMention) setPickerPosition(getTextareaCursorPosition(textarea, wrapperRef.current));
1212
+ setMention((current) => {
1213
+ if (!(current || nextMention)) return current;
1214
+ if (current && nextMention && current.start === nextMention.start && current.end === nextMention.end && current.query === nextMention.query) return current;
1215
+ return nextMention;
1216
+ });
1217
+ }, []);
1218
+ useEffect(() => {
1219
+ let cancelled = false;
1220
+ if (!mention) {
1221
+ setOptions([]);
1222
+ return;
1223
+ }
1224
+ const load = async () => {
1225
+ const result = onReferenceSearch ? await onReferenceSearch({
1226
+ trigger: "@",
1227
+ query: mention.query
1228
+ }) : (references ?? []).filter((tag) => tag.label.toLowerCase().includes(mention.query.toLowerCase()));
1229
+ if (!cancelled) {
1230
+ setOptions(result);
1231
+ setActiveIndex((index) => index < result.length ? index : 0);
1232
+ }
1233
+ };
1234
+ load().catch(() => {
1235
+ if (!cancelled) setOptions([]);
1236
+ });
1237
+ return () => {
1238
+ cancelled = true;
1239
+ };
1240
+ }, [
1241
+ mention,
1242
+ onReferenceSearch,
1243
+ references
1244
+ ]);
1245
+ const insertReference = useCallback((tag) => {
1246
+ if (!(requestCtrl && mention)) return;
1247
+ const mentionText = getTagMention(tag);
1248
+ requestCtrl.insertTag({
1249
+ ...tag,
1250
+ mention: mentionText
1251
+ }, mention);
1252
+ setMention(null);
1253
+ setOptions([]);
1254
+ requestAnimationFrame(() => {
1255
+ const textarea = textareaRef.current;
1256
+ if (!textarea) return;
1257
+ const cursor = mention.start + mentionText.length + 1;
1258
+ textarea.focus();
1259
+ textarea.setSelectionRange(cursor, cursor);
1260
+ });
1261
+ }, [mention, requestCtrl]);
1262
+ const handlePaste = (event) => {
1263
+ const items = event.clipboardData?.items;
1264
+ if (!items) return;
1265
+ const pastedFiles = [];
1266
+ for (const item of items) if (item.kind === "file") {
1267
+ const file = item.getAsFile();
1268
+ if (file) pastedFiles.push(file);
1269
+ }
1270
+ if (pastedFiles.length > 0) {
1271
+ event.preventDefault();
1272
+ attachments.add(pastedFiles);
1273
+ }
1274
+ };
1275
+ const handleKeyDown = (event) => {
1276
+ if (mention && options.length > 0) {
1277
+ if (event.key === "ArrowDown") {
1278
+ event.preventDefault();
1279
+ setActiveIndex((index) => (index + 1) % options.length);
1280
+ return;
1281
+ }
1282
+ if (event.key === "ArrowUp") {
1283
+ event.preventDefault();
1284
+ setActiveIndex((index) => (index - 1 + options.length) % options.length);
1285
+ return;
1286
+ }
1287
+ if (event.key === "Enter" || event.key === "Tab") {
1288
+ event.preventDefault();
1289
+ insertReference(options[activeIndex]);
1290
+ return;
1291
+ }
1292
+ if (event.key === "Escape") {
1293
+ event.preventDefault();
1294
+ setMention(null);
1295
+ setOptions([]);
1296
+ return;
1297
+ }
1298
+ }
1299
+ handleTextareaKeyDown(event);
1300
+ onKeyDown?.(event);
1301
+ };
1302
+ const handleChange = (event) => {
1303
+ if (requestCtrl) requestCtrl.setRequestField("text", event.currentTarget.value);
1304
+ else if (controller) controller.textInput.setInput(event.currentTarget.value);
1305
+ updateMention(event.currentTarget);
1306
+ onChange?.(event);
1307
+ };
1308
+ const showPicker = mention && options.length > 0;
1309
+ return /* @__PURE__ */ jsxs("div", {
1310
+ className: "relative",
1311
+ ref: wrapperRef,
1312
+ children: [
1313
+ /* @__PURE__ */ jsx("div", {
1314
+ "aria-hidden": "true",
1315
+ className: cn("pointer-events-none field-sizing-content max-h-48 min-h-16 w-full overflow-hidden whitespace-pre-wrap break-words px-3 pt-3 pb-1 text-sm text-sf-default", value.length === 0 && "text-transparent"),
1316
+ children: renderInlineReferenceText(value, tags)
1317
+ }),
1318
+ /* @__PURE__ */ jsx("textarea", {
1319
+ className: cn("absolute inset-0 field-sizing-content max-h-48 min-h-16 w-full resize-none border-0 bg-transparent px-3 pt-3 pb-1 text-sm text-transparent outline-none placeholder:text-sf-inactive", className),
1320
+ name: "message",
1321
+ onChange: handleChange,
1322
+ onClick: (event) => updateMention(event.currentTarget),
1323
+ onKeyDown: handleKeyDown,
1324
+ onKeyUp: (event) => {
1325
+ if ([
1326
+ "ArrowDown",
1327
+ "ArrowUp",
1328
+ "Enter",
1329
+ "Tab",
1330
+ "Escape"
1331
+ ].includes(event.key)) return;
1332
+ updateMention(event.currentTarget);
1333
+ },
1334
+ onPaste: handlePaste,
1335
+ placeholder,
1336
+ ref: textareaRef,
1337
+ rows: 1,
1338
+ style: {
1339
+ caretColor: "var(--text-color-sf-default)",
1340
+ ...style
1341
+ },
1342
+ value,
1343
+ ...props
1344
+ }),
1345
+ showPicker && /* @__PURE__ */ jsx(PromptInputReferencePicker, {
1346
+ activeIndex,
1347
+ onSelect: insertReference,
1348
+ options,
1349
+ style: {
1350
+ left: pickerPosition.left,
1351
+ top: pickerPosition.top
1352
+ }
1353
+ })
1354
+ ]
1355
+ });
1356
+ };
1005
1357
  var PromptInputToolbar = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
1006
- className: cn("flex min-w-0 items-center justify-between gap-1 px-2 py-1.5", className),
1358
+ className: cn("grid grid-cols-[minmax(0,1fr)_auto] items-end gap-2 px-2.5 pt-1 pb-2", className),
1007
1359
  ...props
1008
1360
  });
1009
1361
  var PromptInputTools = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
1010
- className: cn("flex min-w-0 items-center gap-1", className),
1362
+ className: cn("flex min-w-0 flex-wrap items-center gap-1", className),
1011
1363
  ...props
1012
1364
  });
1013
1365
  var PromptInputButton = ({ variant = "ghost", size = "sm", className, ...props }) => /* @__PURE__ */ jsx(Button, {
@@ -1032,7 +1384,7 @@ var PromptInputSubmit = ({ className, variant = "primary", size = "sm", status =
1032
1384
  else Icon = /* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-4" });
1033
1385
  return /* @__PURE__ */ jsx(Button, {
1034
1386
  "aria-label": "Submit",
1035
- className: cn(className),
1387
+ className: cn("size-8 shrink-0 justify-center rounded-lg p-0", className),
1036
1388
  size,
1037
1389
  type: "submit",
1038
1390
  variant,
@@ -1435,17 +1787,107 @@ function PromptInputAttachments({ className, children, ...props }) {
1435
1787
  if (attachments.files.length === 0) return null;
1436
1788
  return /* @__PURE__ */ jsx("div", {
1437
1789
  "aria-live": "polite",
1438
- className: cn("overflow-hidden px-2 transition-[height] duration-200 ease-out", className),
1790
+ className: cn("overflow-hidden px-2.5 transition-[height] duration-200 ease-out", className),
1439
1791
  style: { height: attachments.files.length ? height : 0 },
1440
1792
  ...props,
1441
1793
  children: /* @__PURE__ */ jsx("div", {
1442
- className: "flex flex-wrap gap-2 py-1",
1794
+ className: "flex flex-wrap gap-2 pt-2 pb-1",
1443
1795
  ref: contentRef,
1444
1796
  children: attachments.files.map((file) => /* @__PURE__ */ jsx(Fragment, { children: children(file) }, file.id))
1445
1797
  })
1446
1798
  });
1447
1799
  }
1448
1800
  /**
1801
+ * Single reference chip for prompt tags. Includes a remove button when mounted
1802
+ * inside a PromptInputRequestControllerProvider.
1803
+ */
1804
+ function PromptInputTag({ data, className, ...props }) {
1805
+ const requestCtrl = useOptionalPromptInputRequestController();
1806
+ const handleRemove = useCallback(() => {
1807
+ requestCtrl?.removeTag(data.id);
1808
+ }, [requestCtrl, data.id]);
1809
+ const chip = /* @__PURE__ */ jsxs("div", {
1810
+ className: cn("group inline-flex h-8 items-center gap-2 rounded-md border border-sf-info/30 bg-sf-info-tint px-2 text-sm text-sf-default", className),
1811
+ ...props,
1812
+ children: [
1813
+ data.icon && /* @__PURE__ */ jsx("div", {
1814
+ className: "flex size-4 shrink-0 items-center justify-center",
1815
+ children: data.icon
1816
+ }),
1817
+ /* @__PURE__ */ jsx("span", {
1818
+ className: "max-w-[180px] truncate font-medium",
1819
+ children: data.label
1820
+ }),
1821
+ requestCtrl && /* @__PURE__ */ jsx(Button, {
1822
+ "aria-label": "Remove tag",
1823
+ className: "ml-0.5 size-4 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100",
1824
+ onClick: handleRemove,
1825
+ size: "sm",
1826
+ type: "button",
1827
+ variant: "ghost",
1828
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3" })
1829
+ })
1830
+ ]
1831
+ });
1832
+ if (!data.description) return chip;
1833
+ return /* @__PURE__ */ jsx(Tooltip, {
1834
+ content: data.description,
1835
+ children: chip
1836
+ });
1837
+ }
1838
+ /**
1839
+ * Renders all prompt tags using a render prop. Hidden when no tags.
1840
+ * Animates height as tags are added/removed.
1841
+ */
1842
+ function PromptInputTags({ className, children, ...props }) {
1843
+ const tags = useStore(useOptionalPromptInputRequestController()?.request ?? FALLBACK_REQUEST_STORE, (state) => state.tags ?? []);
1844
+ const [height, setHeight] = useState(0);
1845
+ const contentRef = useRef(null);
1846
+ useLayoutEffect(() => {
1847
+ const el = contentRef.current;
1848
+ if (!el) return;
1849
+ const ro = new ResizeObserver(() => {
1850
+ setHeight(el.getBoundingClientRect().height);
1851
+ });
1852
+ ro.observe(el);
1853
+ setHeight(el.getBoundingClientRect().height);
1854
+ return () => ro.disconnect();
1855
+ }, []);
1856
+ useLayoutEffect(() => {
1857
+ const el = contentRef.current;
1858
+ if (el) setHeight(el.getBoundingClientRect().height);
1859
+ }, [tags.length]);
1860
+ if (tags.length === 0) return null;
1861
+ return /* @__PURE__ */ jsx("div", {
1862
+ "aria-live": "polite",
1863
+ className: cn("overflow-hidden px-2.5 transition-[height] duration-200 ease-out", className),
1864
+ style: { height: tags.length ? height : 0 },
1865
+ ...props,
1866
+ children: /* @__PURE__ */ jsx("div", {
1867
+ className: "flex flex-wrap gap-2 pt-2 pb-1",
1868
+ ref: contentRef,
1869
+ children: tags.map((tag) => /* @__PURE__ */ jsx(Fragment, { children: children(tag) }, tag.id))
1870
+ })
1871
+ });
1872
+ }
1873
+ /**
1874
+ * Toolbar trigger for opening a consumer-owned tag picker.
1875
+ */
1876
+ function PromptInputAddTagButton({ children, onClick, onOpenPicker, ...props }) {
1877
+ return /* @__PURE__ */ jsx(Button, {
1878
+ "aria-label": "Add tag",
1879
+ onClick: (event) => {
1880
+ onOpenPicker?.();
1881
+ onClick?.(event);
1882
+ },
1883
+ size: "sm",
1884
+ type: "button",
1885
+ variant: "ghost",
1886
+ ...props,
1887
+ children: children ?? /* @__PURE__ */ jsx(PlusIcon, { className: "size-4" })
1888
+ });
1889
+ }
1890
+ /**
1449
1891
  * Voice input button. Uses the Web Speech API (Chrome/Edge). Hidden on unsupported browsers.
1450
1892
  * Pulses while listening.
1451
1893
  */
@@ -1535,7 +1977,20 @@ var PromptInputAttachButton = ({ "aria-label": ariaLabel = "Attach file", classN
1535
1977
  })
1536
1978
  });
1537
1979
  };
1980
+ PromptInput.BackLayer = PromptInputBackLayer;
1981
+ PromptInput.Textarea = PromptInputTextarea;
1982
+ PromptInput.Editor = PromptInputEditor;
1983
+ PromptInput.Toolbar = PromptInputToolbar;
1984
+ PromptInput.Tools = PromptInputTools;
1985
+ PromptInput.Submit = PromptInputSubmit;
1986
+ PromptInput.ModeSelector = PromptInputModeSelector;
1987
+ PromptInput.AddTagButton = PromptInputAddTagButton;
1988
+ PromptInput.Tags = PromptInputTags;
1989
+ PromptInput.Tag = PromptInputTag;
1990
+ PromptInput.AttachButton = PromptInputAttachButton;
1991
+ PromptInput.Attachments = PromptInputAttachments;
1992
+ PromptInput.Attachment = PromptInputAttachment;
1538
1993
  //#endregion
1539
- export { useProviderAttachments as A, PromptInputTextarea as C, SF_AI_PROMPT_INPUT_VARIANTS as D, SF_AI_PROMPT_INPUT_DEFAULT_VARIANTS as E, usePromptInputRequestController as F, useRequestField as I, useSetRequestField as L, createPromptInputRequestController as M, useDisplayField as N, usePromptInputAttachments as O, useOptionalPromptInputRequestController as P, PromptInputSubmit as S, PromptInputTools as T, PromptInputModeSelector as _, PromptInputActionMenuItem as a, PromptInputProvider as b, PromptInputAttachment as c, PromptInputBody as d, PromptInputButton as f, PromptInputModeSelect as g, PromptInputModeCycle as h, PromptInputActionMenuContent as i, PromptInputRequestControllerProvider as j, usePromptInputController as k, PromptInputAttachments as l, PromptInputCompactSelect as m, PromptInputActionAddAttachments as n, PromptInputActionMenuTrigger as o, PromptInputCompactCycle as p, PromptInputActionMenu as r, PromptInputAttachButton as s, PromptInput as t, PromptInputBackLayer as u, PromptInputModelCycle as v, PromptInputToolbar as w, PromptInputSpeechButton as x, PromptInputModelSelect as y };
1994
+ export { SF_AI_PROMPT_INPUT_DEFAULT_VARIANTS as A, useRequestField as B, PromptInputSpeechButton as C, PromptInputTextarea as D, PromptInputTags as E, PromptInputRequestControllerProvider as F, createPromptInputRequestController as I, useDisplayField as L, usePromptInputAttachments as M, usePromptInputController as N, PromptInputToolbar as O, useProviderAttachments as P, useOptionalPromptInputRequestController as R, PromptInputProvider as S, PromptInputTag as T, useSetRequestField as V, PromptInputModeCycle as _, PromptInputActionMenuItem as a, PromptInputModelCycle as b, PromptInputAttachButton as c, PromptInputBackLayer as d, PromptInputBody as f, PromptInputEditor as g, PromptInputCompactSelect as h, PromptInputActionMenuContent as i, SF_AI_PROMPT_INPUT_VARIANTS as j, PromptInputTools as k, PromptInputAttachment as l, PromptInputCompactCycle as m, PromptInputActionAddAttachments as n, PromptInputActionMenuTrigger as o, PromptInputButton as p, PromptInputActionMenu as r, PromptInputAddTagButton as s, PromptInput as t, PromptInputAttachments as u, PromptInputModeSelect as v, PromptInputSubmit as w, PromptInputModelSelect as x, PromptInputModeSelector as y, usePromptInputRequestController as z };
1540
1995
 
1541
- //# sourceMappingURL=ai-prompt-input-Dy1LfxPk.js.map
1996
+ //# sourceMappingURL=ai-prompt-input-CuluUzpf.js.map