@frankieai/react 0.2.8 → 0.2.9

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.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  CardHeader: () => CardHeader,
32
32
  CardTitle: () => CardTitle,
33
33
  ChatPanel: () => ChatPanel,
34
+ Choices: () => Choices,
34
35
  Composer: () => Composer,
35
36
  FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
36
37
  FrankieApiError: () => FrankieApiError,
@@ -51,6 +52,7 @@ __export(index_exports, {
51
52
  applyEvent: () => applyEvent,
52
53
  buttonVariants: () => buttonVariants,
53
54
  cn: () => cn,
55
+ extractChoices: () => extractChoices,
54
56
  newAssistantMessage: () => newAssistantMessage,
55
57
  newUserMessage: () => newUserMessage,
56
58
  readEventStream: () => readEventStream,
@@ -463,6 +465,41 @@ function useFrankieChatState() {
463
465
  setMessages(next);
464
466
  const abort = new AbortController();
465
467
  abortRef.current = abort;
468
+ let bufferedText = "";
469
+ let pendingThinking = "";
470
+ let frame = null;
471
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
472
+ const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
473
+ const revealTick = () => {
474
+ frame = null;
475
+ let addText = "";
476
+ if (bufferedText) {
477
+ const step = Math.max(3, Math.ceil(bufferedText.length / 6));
478
+ addText = bufferedText.slice(0, step);
479
+ bufferedText = bufferedText.slice(step);
480
+ }
481
+ const addThinking = pendingThinking;
482
+ pendingThinking = "";
483
+ if (addText || addThinking) {
484
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
485
+ }
486
+ if (bufferedText) scheduleReveal();
487
+ };
488
+ const scheduleReveal = () => {
489
+ if (frame === null) frame = raf(revealTick);
490
+ };
491
+ const flushDeltas = () => {
492
+ if (frame !== null) {
493
+ cancelRaf(frame);
494
+ frame = null;
495
+ }
496
+ if (!bufferedText && !pendingThinking) return;
497
+ const addText = bufferedText;
498
+ const addThinking = pendingThinking;
499
+ bufferedText = "";
500
+ pendingThinking = "";
501
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
502
+ };
466
503
  try {
467
504
  let receivedText = false;
468
505
  let receivedToolActivity = false;
@@ -484,11 +521,23 @@ function useFrankieChatState() {
484
521
  return;
485
522
  }
486
523
  if (event.type === "ping" || event.type === "session") return;
487
- if (event.type === "text_delta") receivedText = true;
524
+ if (event.type === "text_delta") {
525
+ receivedText = true;
526
+ bufferedText += event.text;
527
+ scheduleReveal();
528
+ return;
529
+ }
530
+ if (event.type === "thinking_delta") {
531
+ pendingThinking += event.text;
532
+ scheduleReveal();
533
+ return;
534
+ }
488
535
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
536
+ flushDeltas();
489
537
  patchAssistant(assistantId, (message) => applyEvent(message, event));
490
538
  }
491
539
  );
540
+ flushDeltas();
492
541
  if (!receivedText) {
493
542
  patchAssistant(assistantId, (message) => ({
494
543
  ...message,
@@ -497,6 +546,7 @@ function useFrankieChatState() {
497
546
  }));
498
547
  }
499
548
  } catch (caught) {
549
+ flushDeltas();
500
550
  if (abort.signal.aborted) {
501
551
  patchAssistant(assistantId, (message) => ({
502
552
  ...message,
@@ -509,6 +559,7 @@ function useFrankieChatState() {
509
559
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
510
560
  }
511
561
  } finally {
562
+ flushDeltas();
512
563
  patchAssistant(
513
564
  assistantId,
514
565
  (message) => message.status === "streaming" ? { ...message, status: "done" } : message
@@ -611,8 +662,45 @@ function useFrankieTheme() {
611
662
  }
612
663
 
613
664
  // src/components/chat/FrankieWidget.tsx
614
- var import_lucide_react6 = require("lucide-react");
615
- var import_react9 = require("react");
665
+ var import_lucide_react7 = require("lucide-react");
666
+ var import_react10 = require("react");
667
+
668
+ // src/lib/styles-ready.ts
669
+ var import_react3 = require("react");
670
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
671
+ var SENTINEL = "--frankie-styles-ready";
672
+ function useStylesReady(ref, timeoutMs = 600) {
673
+ const [ready, setReady] = (0, import_react3.useState)(false);
674
+ useIsomorphicLayoutEffect(() => {
675
+ if (ready) return;
676
+ const node = ref.current;
677
+ if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
678
+ setReady(true);
679
+ return;
680
+ }
681
+ const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
682
+ if (applied()) {
683
+ setReady(true);
684
+ return;
685
+ }
686
+ const deadline = Date.now() + timeoutMs;
687
+ let frame = 0;
688
+ const poll = () => {
689
+ if (applied() || Date.now() >= deadline) {
690
+ setReady(true);
691
+ return;
692
+ }
693
+ frame = window.requestAnimationFrame(poll);
694
+ };
695
+ if (typeof window.requestAnimationFrame !== "function") {
696
+ setReady(true);
697
+ return;
698
+ }
699
+ frame = window.requestAnimationFrame(poll);
700
+ return () => window.cancelAnimationFrame(frame);
701
+ }, [ref, ready, timeoutMs]);
702
+ return ready;
703
+ }
616
704
 
617
705
  // src/lib/theme.ts
618
706
  function themeToCssVars(theme) {
@@ -672,8 +760,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
672
760
  }
673
761
 
674
762
  // src/components/chat/ChatPanel.tsx
675
- var import_lucide_react5 = require("lucide-react");
676
- var import_react8 = require("react");
763
+ var import_lucide_react6 = require("lucide-react");
764
+ var import_react9 = require("react");
677
765
 
678
766
  // src/components/ui/avatar.tsx
679
767
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -723,9 +811,9 @@ function CardFooter({ className, ...props }) {
723
811
  }
724
812
 
725
813
  // src/components/ui/scroll-area.tsx
726
- var import_react3 = require("react");
814
+ var import_react4 = require("react");
727
815
  var import_jsx_runtime5 = require("react/jsx-runtime");
728
- var ScrollArea = (0, import_react3.forwardRef)(
816
+ var ScrollArea = (0, import_react4.forwardRef)(
729
817
  function ScrollArea2({ className, ...props }, ref) {
730
818
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref, className: cn("frankie-scroll overflow-y-auto overscroll-contain", className), ...props });
731
819
  }
@@ -779,12 +867,12 @@ function AgentContext() {
779
867
 
780
868
  // src/components/chat/Composer.tsx
781
869
  var import_lucide_react2 = require("lucide-react");
782
- var import_react5 = require("react");
870
+ var import_react6 = require("react");
783
871
 
784
872
  // src/components/ui/textarea.tsx
785
- var import_react4 = require("react");
873
+ var import_react5 = require("react");
786
874
  var import_jsx_runtime7 = require("react/jsx-runtime");
787
- var Textarea = (0, import_react4.forwardRef)(
875
+ var Textarea = (0, import_react5.forwardRef)(
788
876
  function Textarea2({ className, ...props }, ref) {
789
877
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
790
878
  "textarea",
@@ -803,21 +891,28 @@ var Textarea = (0, import_react4.forwardRef)(
803
891
  // src/components/chat/Composer.tsx
804
892
  var import_jsx_runtime8 = require("react/jsx-runtime");
805
893
  var MAX_ROWS_PX = 140;
806
- function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
807
- const [value, setValue] = (0, import_react5.useState)("");
808
- const [files, setFiles] = (0, import_react5.useState)([]);
809
- const [fileError, setFileError] = (0, import_react5.useState)(void 0);
810
- const textareaRef = (0, import_react5.useRef)(null);
811
- const fileInputRef = (0, import_react5.useRef)(null);
894
+ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
895
+ const [value, setValue] = (0, import_react6.useState)("");
896
+ const [files, setFiles] = (0, import_react6.useState)([]);
897
+ const [fileError, setFileError] = (0, import_react6.useState)(void 0);
898
+ const textareaRef = (0, import_react6.useRef)(null);
899
+ const fileInputRef = (0, import_react6.useRef)(null);
900
+ (0, import_react6.useEffect)(() => {
901
+ if (!focusHandleRef) return;
902
+ focusHandleRef.current = () => textareaRef.current?.focus();
903
+ return () => {
904
+ focusHandleRef.current = null;
905
+ };
906
+ }, [focusHandleRef]);
812
907
  const canAttach = Boolean(attachments?.enabled);
813
908
  const models = modelPicker?.models ?? [];
814
- (0, import_react5.useEffect)(() => {
909
+ (0, import_react6.useEffect)(() => {
815
910
  const node = textareaRef.current;
816
911
  if (!node) return;
817
912
  node.style.height = "auto";
818
913
  node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
819
914
  }, [value]);
820
- const addFiles = (0, import_react5.useCallback)(
915
+ const addFiles = (0, import_react6.useCallback)(
821
916
  async (list) => {
822
917
  if (!list || !attachments) return;
823
918
  setFileError(void 0);
@@ -850,7 +945,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
850
945
  [attachments, files.length]
851
946
  );
852
947
  const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
853
- const submit = (0, import_react5.useCallback)(() => {
948
+ const submit = (0, import_react6.useCallback)(() => {
854
949
  const text = value.trim();
855
950
  if (!text && files.length === 0 || busy || disabled) return;
856
951
  setValue("");
@@ -980,15 +1075,77 @@ function readAsBase64(file) {
980
1075
  }
981
1076
 
982
1077
  // src/components/chat/MessageBubble.tsx
983
- var import_lucide_react4 = require("lucide-react");
984
- var import_react7 = require("react");
1078
+ var import_lucide_react5 = require("lucide-react");
1079
+ var import_react8 = require("react");
985
1080
 
986
- // src/components/chat/Markdown.tsx
987
- var import_react6 = require("react");
1081
+ // src/lib/choices.ts
1082
+ var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
1083
+ var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
1084
+ function extractChoices(text) {
1085
+ const closed = CLOSED.exec(text);
1086
+ if (closed) {
1087
+ const options = parseOptionLines(closed[1] ?? "");
1088
+ const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
1089
+ return { body, options: options.length ? options : null };
1090
+ }
1091
+ const open = OPEN.exec(text);
1092
+ if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
1093
+ return { body: text, options: null };
1094
+ }
1095
+ function parseOptionLines(block) {
1096
+ return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
1097
+ }
1098
+
1099
+ // src/components/chat/Choices.tsx
1100
+ var import_lucide_react3 = require("lucide-react");
988
1101
  var import_jsx_runtime9 = require("react/jsx-runtime");
989
- function Markdown({ text, className }) {
990
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
1102
+ function Choices({
1103
+ options,
1104
+ onPick,
1105
+ onSomethingElse,
1106
+ disabled
1107
+ }) {
1108
+ if (options.length === 0) return null;
1109
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
1110
+ options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1111
+ "button",
1112
+ {
1113
+ type: "button",
1114
+ className: "frankie-choice",
1115
+ disabled,
1116
+ onClick: () => onPick(option),
1117
+ children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: option }),
1120
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
1121
+ ]
1122
+ },
1123
+ `${index}-${option}`
1124
+ )),
1125
+ onSomethingElse ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1126
+ "button",
1127
+ {
1128
+ type: "button",
1129
+ className: "frankie-choice frankie-choice-other",
1130
+ disabled,
1131
+ onClick: onSomethingElse,
1132
+ children: [
1133
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.PencilLine, { className: "size-3.5" }) }),
1134
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
1135
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
1136
+ ]
1137
+ }
1138
+ ) : null
1139
+ ] });
991
1140
  }
1141
+
1142
+ // src/components/chat/Markdown.tsx
1143
+ var import_react7 = require("react");
1144
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1145
+ var Markdown = (0, import_react7.memo)(function Markdown2({ text, className }) {
1146
+ const blocks = (0, import_react7.useMemo)(() => renderBlocks(text), [text]);
1147
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn("frankie-prose", className), children: blocks });
1148
+ });
992
1149
  function renderBlocks(text) {
993
1150
  const lines = text.split("\n");
994
1151
  const blocks = [];
@@ -997,13 +1154,13 @@ function renderBlocks(text) {
997
1154
  let code = null;
998
1155
  const flushParagraph = () => {
999
1156
  if (paragraph.length === 0) return;
1000
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1157
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1001
1158
  paragraph = [];
1002
1159
  };
1003
1160
  const flushList = () => {
1004
1161
  if (!list) return;
1005
- const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: renderInline(item) }, index));
1006
- blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: items }, `l${blocks.length}`));
1162
+ const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderInline(item) }, index));
1163
+ blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { children: items }, `l${blocks.length}`));
1007
1164
  list = null;
1008
1165
  };
1009
1166
  for (const line of lines) {
@@ -1011,7 +1168,7 @@ function renderBlocks(text) {
1011
1168
  if (fence) {
1012
1169
  if (code) {
1013
1170
  blocks.push(
1014
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1171
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1015
1172
  );
1016
1173
  code = null;
1017
1174
  } else {
@@ -1042,11 +1199,13 @@ function renderBlocks(text) {
1042
1199
  flushList();
1043
1200
  continue;
1044
1201
  }
1045
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
1202
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
1046
1203
  if (heading) {
1047
1204
  flushParagraph();
1048
1205
  flushList();
1049
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1206
+ const level = (heading[1] ?? "#").length;
1207
+ const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
1208
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
1050
1209
  continue;
1051
1210
  }
1052
1211
  flushList();
@@ -1054,7 +1213,7 @@ function renderBlocks(text) {
1054
1213
  }
1055
1214
  if (code) {
1056
1215
  blocks.push(
1057
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1216
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1058
1217
  );
1059
1218
  }
1060
1219
  flushParagraph();
@@ -1069,28 +1228,28 @@ function renderInline(text) {
1069
1228
  const token = match[0];
1070
1229
  const start = match.index;
1071
1230
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
1072
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
1231
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react7.Fragment, { children: renderToken(token) }, start));
1073
1232
  lastIndex = start + token.length;
1074
1233
  }
1075
1234
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1076
1235
  return nodes;
1077
1236
  }
1078
1237
  function renderToken(token) {
1079
- if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: token.slice(2, -2) });
1080
- if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: token.slice(1, -1) });
1238
+ if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: token.slice(2, -2) });
1239
+ if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: token.slice(1, -1) });
1081
1240
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1082
1241
  const href = link ? link[2] ?? "" : token;
1083
1242
  const label = link ? link[1] ?? "" : token;
1084
1243
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
1085
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1244
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1086
1245
  }
1087
1246
 
1088
1247
  // src/components/chat/ToolActivityList.tsx
1089
- var import_lucide_react3 = require("lucide-react");
1248
+ var import_lucide_react4 = require("lucide-react");
1090
1249
 
1091
1250
  // src/components/ui/badge.tsx
1092
1251
  var import_class_variance_authority2 = require("class-variance-authority");
1093
- var import_jsx_runtime10 = require("react/jsx-runtime");
1252
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1094
1253
  var badgeVariants = (0, import_class_variance_authority2.cva)(
1095
1254
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1096
1255
  {
@@ -1110,44 +1269,44 @@ function Badge({
1110
1269
  variant,
1111
1270
  ...props
1112
1271
  }) {
1113
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1272
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1114
1273
  }
1115
1274
 
1116
1275
  // src/components/chat/ToolActivityList.tsx
1117
- var import_jsx_runtime11 = require("react/jsx-runtime");
1276
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1118
1277
  function ToolActivityList({ tools, renderTool }) {
1119
1278
  if (tools.length === 0) return null;
1120
1279
  const running = tools.some((tool) => tool.status === "running");
1121
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1122
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1123
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1124
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: running ? "Working" : "Activity" }),
1125
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-[11px]", children: [
1280
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1283
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: running ? "Working" : "Activity" }),
1284
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[11px]", children: [
1126
1285
  "\xB7 ",
1127
1286
  tools.length,
1128
1287
  " tool call",
1129
1288
  tools.length === 1 ? "" : "s"
1130
1289
  ] })
1131
1290
  ] }),
1132
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1291
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1133
1292
  ] });
1134
1293
  }
1135
1294
  function ToolActivityRow({ tool }) {
1136
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1137
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolIcon, { status: tool.status }),
1138
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: tool.label })
1295
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolIcon, { status: tool.status }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: tool.label })
1139
1298
  ] });
1140
1299
  }
1141
1300
  function ToolIcon({ status }) {
1142
1301
  switch (status) {
1143
1302
  case "running":
1144
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1303
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1145
1304
  case "done":
1146
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3", "aria-hidden": "true" });
1305
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3", "aria-hidden": "true" });
1147
1306
  case "denied":
1148
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1307
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1149
1308
  default:
1150
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.X, { className: "size-3", "aria-hidden": "true" });
1309
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.X, { className: "size-3", "aria-hidden": "true" });
1151
1310
  }
1152
1311
  }
1153
1312
  function badgeVariant(status) {
@@ -1158,47 +1317,54 @@ function badgeVariant(status) {
1158
1317
  }
1159
1318
 
1160
1319
  // src/components/chat/MessageBubble.tsx
1161
- var import_jsx_runtime12 = require("react/jsx-runtime");
1162
- function MessageBubble({
1320
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1321
+ var MessageBubble = (0, import_react8.memo)(function MessageBubble2({
1163
1322
  message,
1164
1323
  assistantName,
1165
1324
  avatarUrl,
1166
1325
  showToolActivity,
1167
- renderTool
1326
+ renderTool,
1327
+ onChoose,
1328
+ onRequestInput,
1329
+ active,
1330
+ busy
1168
1331
  }) {
1169
1332
  if (message.role === "user") {
1170
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1171
- message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1334
+ message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1172
1335
  "span",
1173
1336
  {
1174
1337
  className: "inline-flex max-w-[12rem] items-center gap-1 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
1175
1338
  children: [
1176
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1177
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: file.name })
1339
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: file.name })
1178
1341
  ]
1179
1342
  },
1180
1343
  `${file.name}-${index}`
1181
1344
  )) }) : null,
1182
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1183
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1184
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text })
1345
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1346
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1347
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: message.text })
1185
1348
  ] }) : null
1186
1349
  ] });
1187
1350
  }
1188
1351
  const empty = message.text.length === 0 && message.tools.length === 0;
1189
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1190
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1191
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1192
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text }) : null
1352
+ const { body, options } = extractChoices(message.text);
1353
+ const showChoices = Boolean(active && onChoose && options && message.status === "done");
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1355
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1356
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1357
+ body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: body }) : null
1193
1358
  ] }),
1194
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1195
- message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Markdown, { text: message.text, className: "text-sm" }),
1196
- showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1359
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1360
+ message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }),
1361
+ showChoices ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1362
+ showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1197
1363
  ] })
1198
1364
  ] });
1199
- }
1365
+ });
1200
1366
  function MessageCopyButton({ text }) {
1201
- const [copied, setCopied] = (0, import_react7.useState)(false);
1367
+ const [copied, setCopied] = (0, import_react8.useState)(false);
1202
1368
  const copy = async () => {
1203
1369
  try {
1204
1370
  await navigator.clipboard.writeText(text);
@@ -1207,12 +1373,12 @@ function MessageCopyButton({ text }) {
1207
1373
  } catch {
1208
1374
  }
1209
1375
  };
1210
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1211
1377
  }
1212
1378
  function TypingDots() {
1213
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1214
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1215
- [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1379
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1380
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1381
+ [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1216
1382
  "span",
1217
1383
  {
1218
1384
  className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
@@ -1224,14 +1390,14 @@ function TypingDots() {
1224
1390
  }
1225
1391
 
1226
1392
  // src/components/chat/SuggestedPrompts.tsx
1227
- var import_jsx_runtime13 = require("react/jsx-runtime");
1393
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1228
1394
  function SuggestedPrompts({
1229
1395
  prompts,
1230
1396
  onPick,
1231
1397
  disabled
1232
1398
  }) {
1233
1399
  if (prompts.length === 0) return null;
1234
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1235
1401
  Button,
1236
1402
  {
1237
1403
  type: "button",
@@ -1246,7 +1412,7 @@ function SuggestedPrompts({
1246
1412
  }
1247
1413
 
1248
1414
  // src/components/chat/ChatPanel.tsx
1249
- var import_jsx_runtime14 = require("react/jsx-runtime");
1415
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1250
1416
  function ChatPanel({
1251
1417
  className,
1252
1418
  header,
@@ -1262,12 +1428,14 @@ function ChatPanel({
1262
1428
  const { config, configError, loading } = useFrankie();
1263
1429
  const theme = useFrankieTheme();
1264
1430
  const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
1265
- const scrollRef = (0, import_react8.useRef)(null);
1266
- const atBottomRef = (0, import_react8.useRef)(true);
1431
+ const scrollRef = (0, import_react9.useRef)(null);
1432
+ const atBottomRef = (0, import_react9.useRef)(true);
1267
1433
  const features = config?.features;
1268
1434
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1269
- const [model, setModel] = (0, import_react8.useState)(void 0);
1270
- (0, import_react8.useEffect)(() => {
1435
+ const [model, setModel] = (0, import_react9.useState)(void 0);
1436
+ const composerFocus = (0, import_react9.useRef)(null);
1437
+ const focusComposer = () => composerFocus.current?.();
1438
+ (0, import_react9.useEffect)(() => {
1271
1439
  const node = scrollRef.current;
1272
1440
  if (!node) return;
1273
1441
  const onScroll = () => {
@@ -1276,14 +1444,14 @@ function ChatPanel({
1276
1444
  node.addEventListener("scroll", onScroll, { passive: true });
1277
1445
  return () => node.removeEventListener("scroll", onScroll);
1278
1446
  }, []);
1279
- (0, import_react8.useEffect)(() => {
1447
+ (0, import_react9.useEffect)(() => {
1280
1448
  const node = scrollRef.current;
1281
1449
  if (!node || !atBottomRef.current) return;
1282
1450
  node.scrollTop = node.scrollHeight;
1283
1451
  }, [messages]);
1284
1452
  const name = config?.branding.name ?? "Assistant";
1285
1453
  const mode = resolveMode(theme?.mode);
1286
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1454
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1287
1455
  Card,
1288
1456
  {
1289
1457
  className: cn(
@@ -1296,44 +1464,49 @@ function ChatPanel({
1296
1464
  style: themeToCssVars(theme),
1297
1465
  "data-frankie-mode": mode,
1298
1466
  children: [
1299
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardHeader, { children: [
1300
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1301
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "min-w-0 flex-1", children: [
1302
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardTitle, { children: name }),
1303
- config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardDescription, { children: config.branding.tagline }) : null
1467
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardHeader, { children: [
1468
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1469
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "min-w-0 flex-1", children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardTitle, { children: name }),
1471
+ config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardDescription, { children: config.branding.tagline }) : null
1304
1472
  ] }),
1305
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RotateCcw, {}) }),
1306
- onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, {}) }) : null
1473
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.RotateCcw, {}) }),
1474
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, {}) }) : null
1307
1475
  ] }),
1308
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AgentContext, {}),
1309
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1310
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1311
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: configError }),
1312
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1313
- ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-col gap-4", children: [
1314
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1315
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1316
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1317
- (message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1476
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AgentContext, {}),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1478
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: configError }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1481
+ ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
1482
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1484
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1485
+ (message, index) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1318
1486
  MessageBubble,
1319
1487
  {
1320
1488
  message,
1321
1489
  assistantName: name,
1322
1490
  ...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
1323
1491
  showToolActivity: config?.showToolActivity ?? true,
1324
- ...renderTool ? { renderTool } : {}
1492
+ ...renderTool ? { renderTool } : {},
1493
+ onChoose: send,
1494
+ onRequestInput: focusComposer,
1495
+ active: index === messages.length - 1,
1496
+ busy
1325
1497
  },
1326
1498
  message.id
1327
1499
  )
1328
1500
  ) }) }) }),
1329
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1330
- error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1331
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1501
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1502
+ error ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1503
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1332
1504
  Composer,
1333
1505
  {
1334
1506
  onSend: send,
1335
1507
  onStop: stop,
1336
1508
  busy,
1509
+ focusHandleRef: composerFocus,
1337
1510
  disabled: loading || restoring || Boolean(configError),
1338
1511
  ...placeholder ? { placeholder } : {},
1339
1512
  ...autoFocus ? { autoFocus } : {},
@@ -1349,7 +1522,7 @@ function ChatPanel({
1349
1522
  }
1350
1523
 
1351
1524
  // src/components/chat/FrankieWidget.tsx
1352
- var import_jsx_runtime15 = require("react/jsx-runtime");
1525
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1353
1526
  function FrankieWidget({
1354
1527
  position = "bottom-right",
1355
1528
  offset = 24,
@@ -1362,13 +1535,15 @@ function FrankieWidget({
1362
1535
  }) {
1363
1536
  const { config } = useFrankie();
1364
1537
  const theme = useFrankieTheme();
1365
- const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react9.useState)(defaultOpen);
1538
+ const rootRef = (0, import_react10.useRef)(null);
1539
+ const stylesReady = useStylesReady(rootRef);
1540
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react10.useState)(defaultOpen);
1366
1541
  const open = controlledOpen ?? uncontrolledOpen;
1367
1542
  const setOpen = (next) => {
1368
1543
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1369
1544
  onOpenChange?.(next);
1370
1545
  };
1371
- (0, import_react9.useEffect)(() => {
1546
+ (0, import_react10.useEffect)(() => {
1372
1547
  if (!open) return;
1373
1548
  const onKeyDown = (event) => {
1374
1549
  if (event.key === "Escape") setOpen(false);
@@ -1379,15 +1554,17 @@ function FrankieWidget({
1379
1554
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1380
1555
  const mode = resolveMode(theme?.mode);
1381
1556
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1382
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1557
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1383
1558
  "div",
1384
1559
  {
1560
+ ref: rootRef,
1385
1561
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1386
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1562
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
1387
1563
  "data-frankie-mode": mode,
1388
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1389
- open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1390
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1564
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1565
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1566
+ open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1567
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1391
1568
  Button,
1392
1569
  {
1393
1570
  size: "icon",
@@ -1396,7 +1573,7 @@ function FrankieWidget({
1396
1573
  "aria-expanded": open,
1397
1574
  "aria-label": open ? "Close the assistant" : label,
1398
1575
  title: label,
1399
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.MessageCircle, { className: "size-5" })
1576
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.MessageCircle, { className: "size-5" })
1400
1577
  }
1401
1578
  ) })
1402
1579
  ] })
@@ -1405,39 +1582,44 @@ function FrankieWidget({
1405
1582
  }
1406
1583
 
1407
1584
  // src/components/chat/FrankieInline.tsx
1408
- var import_jsx_runtime16 = require("react/jsx-runtime");
1585
+ var import_react11 = require("react");
1586
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1409
1587
  function FrankieInline({ className, style, ...panelProps }) {
1410
1588
  const theme = useFrankieTheme();
1411
1589
  const mode = resolveMode(theme?.mode);
1412
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1590
+ const rootRef = (0, import_react11.useRef)(null);
1591
+ const stylesReady = useStylesReady(rootRef);
1592
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1413
1593
  "div",
1414
1594
  {
1595
+ ref: rootRef,
1415
1596
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1416
- style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1597
+ style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
1417
1598
  "data-frankie-mode": mode,
1418
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, fill: true })
1599
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1600
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatPanel, { ...panelProps, fill: true })
1419
1601
  }
1420
1602
  );
1421
1603
  }
1422
1604
 
1423
1605
  // src/components/chat/FrankieChatPage.tsx
1424
- var import_react10 = require("react");
1425
- var import_lucide_react7 = require("lucide-react");
1426
- var import_jsx_runtime17 = require("react/jsx-runtime");
1606
+ var import_react12 = require("react");
1607
+ var import_lucide_react8 = require("lucide-react");
1608
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1427
1609
  function PageEmptyState() {
1428
1610
  const { config } = useFrankie();
1429
1611
  const { send, busy, restoring } = useFrankieChat();
1430
1612
  const name = config?.branding.name ?? "Frankie";
1431
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1432
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1433
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1434
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1435
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { children: "How can I help?" }),
1436
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1613
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1615
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1616
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1617
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h1", { children: "How can I help?" }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1437
1619
  ] }),
1438
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1439
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: prompt }),
1440
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1620
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: prompt }),
1622
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1441
1623
  ] }, prompt)) })
1442
1624
  ] });
1443
1625
  }
@@ -1445,22 +1627,22 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1445
1627
  const { client, config } = useFrankie();
1446
1628
  const { busy, messages } = useFrankieChat();
1447
1629
  const theme = useFrankieTheme();
1448
- const [conversations, setConversations] = (0, import_react10.useState)([]);
1449
- const [historyError, setHistoryError] = (0, import_react10.useState)("");
1450
- const [historyLoading, setHistoryLoading] = (0, import_react10.useState)(false);
1451
- const [drawerOpen, setDrawerOpen] = (0, import_react10.useState)(false);
1452
- const [collapsed, setCollapsed] = (0, import_react10.useState)(() => {
1630
+ const [conversations, setConversations] = (0, import_react12.useState)([]);
1631
+ const [historyError, setHistoryError] = (0, import_react12.useState)("");
1632
+ const [historyLoading, setHistoryLoading] = (0, import_react12.useState)(false);
1633
+ const [drawerOpen, setDrawerOpen] = (0, import_react12.useState)(false);
1634
+ const [collapsed, setCollapsed] = (0, import_react12.useState)(() => {
1453
1635
  try {
1454
1636
  return window.localStorage.getItem("frankie.history-collapsed") === "1";
1455
1637
  } catch {
1456
1638
  return false;
1457
1639
  }
1458
1640
  });
1459
- const [query, setQuery] = (0, import_react10.useState)("");
1460
- const dialog = (0, import_react10.useRef)(null);
1461
- const menu = (0, import_react10.useRef)(null);
1462
- const requestVersion = (0, import_react10.useRef)(0);
1463
- const refresh = (0, import_react10.useCallback)(async () => {
1641
+ const [query, setQuery] = (0, import_react12.useState)("");
1642
+ const dialog = (0, import_react12.useRef)(null);
1643
+ const menu = (0, import_react12.useRef)(null);
1644
+ const requestVersion = (0, import_react12.useRef)(0);
1645
+ const refresh = (0, import_react12.useCallback)(async () => {
1464
1646
  if (!showHistory || !config || config.allowAnonymous) {
1465
1647
  setHistoryError("");
1466
1648
  return;
@@ -1481,19 +1663,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1481
1663
  if (version === requestVersion.current) setHistoryLoading(false);
1482
1664
  }
1483
1665
  }, [client, showHistory, config?.allowAnonymous]);
1484
- (0, import_react10.useEffect)(() => {
1666
+ (0, import_react12.useEffect)(() => {
1485
1667
  if (!busy) void refresh();
1486
1668
  return () => {
1487
1669
  requestVersion.current++;
1488
1670
  };
1489
1671
  }, [refresh, busy]);
1490
- (0, import_react10.useEffect)(() => {
1672
+ (0, import_react12.useEffect)(() => {
1491
1673
  try {
1492
1674
  window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
1493
1675
  } catch {
1494
1676
  }
1495
1677
  }, [collapsed]);
1496
- (0, import_react10.useEffect)(() => {
1678
+ (0, import_react12.useEffect)(() => {
1497
1679
  const node = dialog.current;
1498
1680
  if (drawerOpen) {
1499
1681
  node?.showModal?.();
@@ -1511,79 +1693,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1511
1693
  close();
1512
1694
  }
1513
1695
  };
1514
- const history = /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1515
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-history-heading", children: [
1516
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Conversations" }),
1517
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.X, { size: 18 }) })
1696
+ const history = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1697
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-history-heading", children: [
1698
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Conversations" }),
1699
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.X, { size: 18 }) })
1518
1700
  ] }),
1519
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1701
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1520
1702
  onNew();
1521
1703
  close();
1522
1704
  }, children: [
1523
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 17 }),
1705
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 17 }),
1524
1706
  "New chat"
1525
1707
  ] }),
1526
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "frankie-history-search", children: [
1527
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "Search conversations" }),
1528
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1708
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { className: "frankie-history-search", children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "Search conversations" }),
1710
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1529
1711
  ] }),
1530
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1531
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: item.title }),
1532
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1712
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1713
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: item.title }),
1714
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1533
1715
  ] }, item.id)) }),
1534
- historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { role: "status", children: [
1716
+ historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { role: "status", children: [
1535
1717
  historyError,
1536
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1537
- ] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1538
- navigation ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1718
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1719
+ ] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1720
+ navigation ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1539
1721
  ] });
1540
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1541
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1542
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.ChevronsRight, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.ChevronsLeft, { size: 16 }) }),
1722
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1723
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1724
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsRight, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsLeft, { size: 16 }) }),
1543
1725
  collapsed ? null : history
1544
1726
  ] }) : null,
1545
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-main", children: [
1546
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("header", { className: "frankie-page-header", children: [
1547
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1548
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Menu, { size: 20 }) }) : null,
1549
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1727
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-main", children: [
1728
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("header", { className: "frankie-page-header", children: [
1729
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1730
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Menu, { size: 20 }) }) : null,
1731
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1550
1732
  ] }),
1551
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1552
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1553
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 16 }),
1554
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "New chat" })
1733
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1734
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1735
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 16 }),
1736
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "New chat" })
1555
1737
  ] }),
1556
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("details", { className: "frankie-chat-settings", children: [
1557
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Settings, { size: 19 }) }),
1558
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1559
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Chat settings" }),
1560
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1561
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1562
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.History, { size: 16 }),
1738
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { className: "frankie-chat-settings", children: [
1739
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Settings, { size: 19 }) }),
1740
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1741
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Chat settings" }),
1742
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1743
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1744
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.History, { size: 16 }),
1563
1745
  "Refresh history"
1564
1746
  ] }),
1565
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: "Skills are checked at the start of each message." })
1747
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: "Skills are checked at the start of each message." })
1566
1748
  ] })
1567
1749
  ] })
1568
1750
  ] })
1569
1751
  ] }),
1570
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageEmptyState, {}) }) })
1752
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageEmptyState, {}) }) })
1571
1753
  ] }),
1572
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1754
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1573
1755
  if (event.target === event.currentTarget) close();
1574
- }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { children: history }) }) : null
1756
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("aside", { children: history }) }) : null
1575
1757
  ] });
1576
1758
  }
1577
1759
  function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1578
- const [id, setId] = (0, import_react10.useState)(() => provider.conversationId ?? crypto.randomUUID());
1760
+ const [id, setId] = (0, import_react12.useState)(() => provider.conversationId ?? crypto.randomUUID());
1579
1761
  const onNew = () => setId(crypto.randomUUID());
1580
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1762
+ const pageRef = (0, import_react12.useRef)(null);
1763
+ const stylesReady = useStylesReady(pageRef);
1764
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { ref: pageRef, className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } }, "data-frankie-styles": stylesReady ? "ready" : "pending", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1581
1765
  }
1582
1766
 
1583
1767
  // src/components/ui/separator.tsx
1584
- var import_jsx_runtime18 = require("react/jsx-runtime");
1768
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1585
1769
  function Separator({ className }) {
1586
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1770
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1587
1771
  }
1588
1772
  // Annotate the CommonJS export names for ESM import in node:
1589
1773
  0 && (module.exports = {
@@ -1597,6 +1781,7 @@ function Separator({ className }) {
1597
1781
  CardHeader,
1598
1782
  CardTitle,
1599
1783
  ChatPanel,
1784
+ Choices,
1600
1785
  Composer,
1601
1786
  FRANKIE_CLOUD_URL,
1602
1787
  FrankieApiError,
@@ -1617,6 +1802,7 @@ function Separator({ className }) {
1617
1802
  applyEvent,
1618
1803
  buttonVariants,
1619
1804
  cn,
1805
+ extractChoices,
1620
1806
  newAssistantMessage,
1621
1807
  newUserMessage,
1622
1808
  readEventStream,