@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.js CHANGED
@@ -401,6 +401,41 @@ function useFrankieChatState() {
401
401
  setMessages(next);
402
402
  const abort = new AbortController();
403
403
  abortRef.current = abort;
404
+ let bufferedText = "";
405
+ let pendingThinking = "";
406
+ let frame = null;
407
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
408
+ const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
409
+ const revealTick = () => {
410
+ frame = null;
411
+ let addText = "";
412
+ if (bufferedText) {
413
+ const step = Math.max(3, Math.ceil(bufferedText.length / 6));
414
+ addText = bufferedText.slice(0, step);
415
+ bufferedText = bufferedText.slice(step);
416
+ }
417
+ const addThinking = pendingThinking;
418
+ pendingThinking = "";
419
+ if (addText || addThinking) {
420
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
421
+ }
422
+ if (bufferedText) scheduleReveal();
423
+ };
424
+ const scheduleReveal = () => {
425
+ if (frame === null) frame = raf(revealTick);
426
+ };
427
+ const flushDeltas = () => {
428
+ if (frame !== null) {
429
+ cancelRaf(frame);
430
+ frame = null;
431
+ }
432
+ if (!bufferedText && !pendingThinking) return;
433
+ const addText = bufferedText;
434
+ const addThinking = pendingThinking;
435
+ bufferedText = "";
436
+ pendingThinking = "";
437
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
438
+ };
404
439
  try {
405
440
  let receivedText = false;
406
441
  let receivedToolActivity = false;
@@ -422,11 +457,23 @@ function useFrankieChatState() {
422
457
  return;
423
458
  }
424
459
  if (event.type === "ping" || event.type === "session") return;
425
- if (event.type === "text_delta") receivedText = true;
460
+ if (event.type === "text_delta") {
461
+ receivedText = true;
462
+ bufferedText += event.text;
463
+ scheduleReveal();
464
+ return;
465
+ }
466
+ if (event.type === "thinking_delta") {
467
+ pendingThinking += event.text;
468
+ scheduleReveal();
469
+ return;
470
+ }
426
471
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
472
+ flushDeltas();
427
473
  patchAssistant(assistantId, (message) => applyEvent(message, event));
428
474
  }
429
475
  );
476
+ flushDeltas();
430
477
  if (!receivedText) {
431
478
  patchAssistant(assistantId, (message) => ({
432
479
  ...message,
@@ -435,6 +482,7 @@ function useFrankieChatState() {
435
482
  }));
436
483
  }
437
484
  } catch (caught) {
485
+ flushDeltas();
438
486
  if (abort.signal.aborted) {
439
487
  patchAssistant(assistantId, (message) => ({
440
488
  ...message,
@@ -447,6 +495,7 @@ function useFrankieChatState() {
447
495
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
448
496
  }
449
497
  } finally {
498
+ flushDeltas();
450
499
  patchAssistant(
451
500
  assistantId,
452
501
  (message) => message.status === "streaming" ? { ...message, status: "done" } : message
@@ -550,7 +599,44 @@ function useFrankieTheme() {
550
599
 
551
600
  // src/components/chat/FrankieWidget.tsx
552
601
  import { MessageCircle, X as X4 } from "lucide-react";
553
- import { useEffect as useEffect5, useState as useState6 } from "react";
602
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
603
+
604
+ // src/lib/styles-ready.ts
605
+ import { useEffect as useEffect3, useLayoutEffect, useState as useState3 } from "react";
606
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect;
607
+ var SENTINEL = "--frankie-styles-ready";
608
+ function useStylesReady(ref, timeoutMs = 600) {
609
+ const [ready, setReady] = useState3(false);
610
+ useIsomorphicLayoutEffect(() => {
611
+ if (ready) return;
612
+ const node = ref.current;
613
+ if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
614
+ setReady(true);
615
+ return;
616
+ }
617
+ const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
618
+ if (applied()) {
619
+ setReady(true);
620
+ return;
621
+ }
622
+ const deadline = Date.now() + timeoutMs;
623
+ let frame = 0;
624
+ const poll = () => {
625
+ if (applied() || Date.now() >= deadline) {
626
+ setReady(true);
627
+ return;
628
+ }
629
+ frame = window.requestAnimationFrame(poll);
630
+ };
631
+ if (typeof window.requestAnimationFrame !== "function") {
632
+ setReady(true);
633
+ return;
634
+ }
635
+ frame = window.requestAnimationFrame(poll);
636
+ return () => window.cancelAnimationFrame(frame);
637
+ }, [ref, ready, timeoutMs]);
638
+ return ready;
639
+ }
554
640
 
555
641
  // src/lib/theme.ts
556
642
  function themeToCssVars(theme) {
@@ -611,7 +697,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
611
697
 
612
698
  // src/components/chat/ChatPanel.tsx
613
699
  import { RotateCcw, X as X3 } from "lucide-react";
614
- import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
700
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
615
701
 
616
702
  // src/components/ui/avatar.tsx
617
703
  import { jsx as jsx3 } from "react/jsx-runtime";
@@ -717,7 +803,7 @@ function AgentContext() {
717
803
 
718
804
  // src/components/chat/Composer.tsx
719
805
  import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
720
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
806
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
721
807
 
722
808
  // src/components/ui/textarea.tsx
723
809
  import { forwardRef as forwardRef2 } from "react";
@@ -741,15 +827,22 @@ var Textarea = forwardRef2(
741
827
  // src/components/chat/Composer.tsx
742
828
  import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
743
829
  var MAX_ROWS_PX = 140;
744
- function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
745
- const [value, setValue] = useState3("");
746
- const [files, setFiles] = useState3([]);
747
- const [fileError, setFileError] = useState3(void 0);
830
+ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
831
+ const [value, setValue] = useState4("");
832
+ const [files, setFiles] = useState4([]);
833
+ const [fileError, setFileError] = useState4(void 0);
748
834
  const textareaRef = useRef3(null);
749
835
  const fileInputRef = useRef3(null);
836
+ useEffect4(() => {
837
+ if (!focusHandleRef) return;
838
+ focusHandleRef.current = () => textareaRef.current?.focus();
839
+ return () => {
840
+ focusHandleRef.current = null;
841
+ };
842
+ }, [focusHandleRef]);
750
843
  const canAttach = Boolean(attachments?.enabled);
751
844
  const models = modelPicker?.models ?? [];
752
- useEffect3(() => {
845
+ useEffect4(() => {
753
846
  const node = textareaRef.current;
754
847
  if (!node) return;
755
848
  node.style.height = "auto";
@@ -919,14 +1012,76 @@ function readAsBase64(file) {
919
1012
 
920
1013
  // src/components/chat/MessageBubble.tsx
921
1014
  import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
922
- import { useState as useState4 } from "react";
1015
+ import { memo as memo2, useState as useState5 } from "react";
923
1016
 
924
- // src/components/chat/Markdown.tsx
925
- import { Fragment as Fragment2 } from "react";
926
- import { jsx as jsx9 } from "react/jsx-runtime";
927
- function Markdown({ text, className }) {
928
- return /* @__PURE__ */ jsx9("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
1017
+ // src/lib/choices.ts
1018
+ var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
1019
+ var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
1020
+ function extractChoices(text) {
1021
+ const closed = CLOSED.exec(text);
1022
+ if (closed) {
1023
+ const options = parseOptionLines(closed[1] ?? "");
1024
+ const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
1025
+ return { body, options: options.length ? options : null };
1026
+ }
1027
+ const open = OPEN.exec(text);
1028
+ if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
1029
+ return { body: text, options: null };
1030
+ }
1031
+ function parseOptionLines(block) {
1032
+ return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
1033
+ }
1034
+
1035
+ // src/components/chat/Choices.tsx
1036
+ import { ArrowRight, PencilLine } from "lucide-react";
1037
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1038
+ function Choices({
1039
+ options,
1040
+ onPick,
1041
+ onSomethingElse,
1042
+ disabled
1043
+ }) {
1044
+ if (options.length === 0) return null;
1045
+ return /* @__PURE__ */ jsxs3("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
1046
+ options.map((option, index) => /* @__PURE__ */ jsxs3(
1047
+ "button",
1048
+ {
1049
+ type: "button",
1050
+ className: "frankie-choice",
1051
+ disabled,
1052
+ onClick: () => onPick(option),
1053
+ children: [
1054
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
1055
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: option }),
1056
+ /* @__PURE__ */ jsx9(ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
1057
+ ]
1058
+ },
1059
+ `${index}-${option}`
1060
+ )),
1061
+ onSomethingElse ? /* @__PURE__ */ jsxs3(
1062
+ "button",
1063
+ {
1064
+ type: "button",
1065
+ className: "frankie-choice frankie-choice-other",
1066
+ disabled,
1067
+ onClick: onSomethingElse,
1068
+ children: [
1069
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ jsx9(PencilLine, { className: "size-3.5" }) }),
1070
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
1071
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
1072
+ ]
1073
+ }
1074
+ ) : null
1075
+ ] });
929
1076
  }
1077
+
1078
+ // src/components/chat/Markdown.tsx
1079
+ import { Fragment as Fragment2, memo, useMemo as useMemo2 } from "react";
1080
+ import { jsx as jsx10 } from "react/jsx-runtime";
1081
+ var Markdown = memo(function Markdown2({ text, className }) {
1082
+ const blocks = useMemo2(() => renderBlocks(text), [text]);
1083
+ return /* @__PURE__ */ jsx10("div", { className: cn("frankie-prose", className), children: blocks });
1084
+ });
930
1085
  function renderBlocks(text) {
931
1086
  const lines = text.split("\n");
932
1087
  const blocks = [];
@@ -935,13 +1090,13 @@ function renderBlocks(text) {
935
1090
  let code = null;
936
1091
  const flushParagraph = () => {
937
1092
  if (paragraph.length === 0) return;
938
- blocks.push(/* @__PURE__ */ jsx9("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1093
+ blocks.push(/* @__PURE__ */ jsx10("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
939
1094
  paragraph = [];
940
1095
  };
941
1096
  const flushList = () => {
942
1097
  if (!list) return;
943
- const items = list.items.map((item, index) => /* @__PURE__ */ jsx9("li", { children: renderInline(item) }, index));
944
- blocks.push(list.ordered ? /* @__PURE__ */ jsx9("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx9("ul", { children: items }, `l${blocks.length}`));
1098
+ const items = list.items.map((item, index) => /* @__PURE__ */ jsx10("li", { children: renderInline(item) }, index));
1099
+ blocks.push(list.ordered ? /* @__PURE__ */ jsx10("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx10("ul", { children: items }, `l${blocks.length}`));
945
1100
  list = null;
946
1101
  };
947
1102
  for (const line of lines) {
@@ -949,7 +1104,7 @@ function renderBlocks(text) {
949
1104
  if (fence) {
950
1105
  if (code) {
951
1106
  blocks.push(
952
- /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1107
+ /* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
953
1108
  );
954
1109
  code = null;
955
1110
  } else {
@@ -980,11 +1135,13 @@ function renderBlocks(text) {
980
1135
  flushList();
981
1136
  continue;
982
1137
  }
983
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
1138
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
984
1139
  if (heading) {
985
1140
  flushParagraph();
986
1141
  flushList();
987
- blocks.push(/* @__PURE__ */ jsx9("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1142
+ const level = (heading[1] ?? "#").length;
1143
+ const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
1144
+ blocks.push(/* @__PURE__ */ jsx10(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
988
1145
  continue;
989
1146
  }
990
1147
  flushList();
@@ -992,7 +1149,7 @@ function renderBlocks(text) {
992
1149
  }
993
1150
  if (code) {
994
1151
  blocks.push(
995
- /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1152
+ /* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
996
1153
  );
997
1154
  }
998
1155
  flushParagraph();
@@ -1007,20 +1164,20 @@ function renderInline(text) {
1007
1164
  const token = match[0];
1008
1165
  const start = match.index;
1009
1166
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
1010
- nodes.push(/* @__PURE__ */ jsx9(Fragment2, { children: renderToken(token) }, start));
1167
+ nodes.push(/* @__PURE__ */ jsx10(Fragment2, { children: renderToken(token) }, start));
1011
1168
  lastIndex = start + token.length;
1012
1169
  }
1013
1170
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1014
1171
  return nodes;
1015
1172
  }
1016
1173
  function renderToken(token) {
1017
- if (token.startsWith("**")) return /* @__PURE__ */ jsx9("strong", { children: token.slice(2, -2) });
1018
- if (token.startsWith("`")) return /* @__PURE__ */ jsx9("code", { children: token.slice(1, -1) });
1174
+ if (token.startsWith("**")) return /* @__PURE__ */ jsx10("strong", { children: token.slice(2, -2) });
1175
+ if (token.startsWith("`")) return /* @__PURE__ */ jsx10("code", { children: token.slice(1, -1) });
1019
1176
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1020
1177
  const href = link ? link[2] ?? "" : token;
1021
1178
  const label = link ? link[1] ?? "" : token;
1022
1179
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
1023
- return /* @__PURE__ */ jsx9("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1180
+ return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1024
1181
  }
1025
1182
 
1026
1183
  // src/components/chat/ToolActivityList.tsx
@@ -1028,7 +1185,7 @@ import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react"
1028
1185
 
1029
1186
  // src/components/ui/badge.tsx
1030
1187
  import { cva as cva2 } from "class-variance-authority";
1031
- import { jsx as jsx10 } from "react/jsx-runtime";
1188
+ import { jsx as jsx11 } from "react/jsx-runtime";
1032
1189
  var badgeVariants = cva2(
1033
1190
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1034
1191
  {
@@ -1048,44 +1205,44 @@ function Badge({
1048
1205
  variant,
1049
1206
  ...props
1050
1207
  }) {
1051
- return /* @__PURE__ */ jsx10("span", { className: cn(badgeVariants({ variant }), className), ...props });
1208
+ return /* @__PURE__ */ jsx11("span", { className: cn(badgeVariants({ variant }), className), ...props });
1052
1209
  }
1053
1210
 
1054
1211
  // src/components/chat/ToolActivityList.tsx
1055
- import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
1212
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1056
1213
  function ToolActivityList({ tools, renderTool }) {
1057
1214
  if (tools.length === 0) return null;
1058
1215
  const running = tools.some((tool) => tool.status === "running");
1059
- return /* @__PURE__ */ jsxs3("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1060
- /* @__PURE__ */ jsxs3("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: [
1061
- /* @__PURE__ */ jsx11(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1062
- /* @__PURE__ */ jsx11("span", { children: running ? "Working" : "Activity" }),
1063
- /* @__PURE__ */ jsxs3("span", { className: "text-[11px]", children: [
1216
+ return /* @__PURE__ */ jsxs4("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1217
+ /* @__PURE__ */ jsxs4("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: [
1218
+ /* @__PURE__ */ jsx12(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1219
+ /* @__PURE__ */ jsx12("span", { children: running ? "Working" : "Activity" }),
1220
+ /* @__PURE__ */ jsxs4("span", { className: "text-[11px]", children: [
1064
1221
  "\xB7 ",
1065
1222
  tools.length,
1066
1223
  " tool call",
1067
1224
  tools.length === 1 ? "" : "s"
1068
1225
  ] })
1069
1226
  ] }),
1070
- /* @__PURE__ */ jsx11("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx11("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx11(ToolActivityRow, { tool }) }, tool.id)) })
1227
+ /* @__PURE__ */ jsx12("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx12("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx12(ToolActivityRow, { tool }) }, tool.id)) })
1071
1228
  ] });
1072
1229
  }
1073
1230
  function ToolActivityRow({ tool }) {
1074
- return /* @__PURE__ */ jsxs3(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1075
- /* @__PURE__ */ jsx11(ToolIcon, { status: tool.status }),
1076
- /* @__PURE__ */ jsx11("span", { className: "truncate", children: tool.label })
1231
+ return /* @__PURE__ */ jsxs4(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1232
+ /* @__PURE__ */ jsx12(ToolIcon, { status: tool.status }),
1233
+ /* @__PURE__ */ jsx12("span", { className: "truncate", children: tool.label })
1077
1234
  ] });
1078
1235
  }
1079
1236
  function ToolIcon({ status }) {
1080
1237
  switch (status) {
1081
1238
  case "running":
1082
- return /* @__PURE__ */ jsx11(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1239
+ return /* @__PURE__ */ jsx12(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1083
1240
  case "done":
1084
- return /* @__PURE__ */ jsx11(Check, { className: "size-3", "aria-hidden": "true" });
1241
+ return /* @__PURE__ */ jsx12(Check, { className: "size-3", "aria-hidden": "true" });
1085
1242
  case "denied":
1086
- return /* @__PURE__ */ jsx11(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1243
+ return /* @__PURE__ */ jsx12(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1087
1244
  default:
1088
- return /* @__PURE__ */ jsx11(X2, { className: "size-3", "aria-hidden": "true" });
1245
+ return /* @__PURE__ */ jsx12(X2, { className: "size-3", "aria-hidden": "true" });
1089
1246
  }
1090
1247
  }
1091
1248
  function badgeVariant(status) {
@@ -1096,47 +1253,54 @@ function badgeVariant(status) {
1096
1253
  }
1097
1254
 
1098
1255
  // src/components/chat/MessageBubble.tsx
1099
- import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1100
- function MessageBubble({
1256
+ import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
1257
+ var MessageBubble = memo2(function MessageBubble2({
1101
1258
  message,
1102
1259
  assistantName,
1103
1260
  avatarUrl,
1104
1261
  showToolActivity,
1105
- renderTool
1262
+ renderTool,
1263
+ onChoose,
1264
+ onRequestInput,
1265
+ active,
1266
+ busy
1106
1267
  }) {
1107
1268
  if (message.role === "user") {
1108
- return /* @__PURE__ */ jsxs4("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1109
- message.attachments?.length ? /* @__PURE__ */ jsx12("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs4(
1269
+ return /* @__PURE__ */ jsxs5("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1270
+ message.attachments?.length ? /* @__PURE__ */ jsx13("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs5(
1110
1271
  "span",
1111
1272
  {
1112
1273
  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",
1113
1274
  children: [
1114
- /* @__PURE__ */ jsx12(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1115
- /* @__PURE__ */ jsx12("span", { className: "truncate", children: file.name })
1275
+ /* @__PURE__ */ jsx13(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1276
+ /* @__PURE__ */ jsx13("span", { className: "truncate", children: file.name })
1116
1277
  ]
1117
1278
  },
1118
1279
  `${file.name}-${index}`
1119
1280
  )) }) : null,
1120
- message.text ? /* @__PURE__ */ jsxs4("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1121
- /* @__PURE__ */ jsx12("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1122
- /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text })
1281
+ message.text ? /* @__PURE__ */ jsxs5("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1282
+ /* @__PURE__ */ jsx13("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1283
+ /* @__PURE__ */ jsx13(MessageCopyButton, { text: message.text })
1123
1284
  ] }) : null
1124
1285
  ] });
1125
1286
  }
1126
1287
  const empty = message.text.length === 0 && message.tools.length === 0;
1127
- return /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-message flex gap-2", children: [
1128
- /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1129
- /* @__PURE__ */ jsx12(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1130
- message.text ? /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text }) : null
1288
+ const { body, options } = extractChoices(message.text);
1289
+ const showChoices = Boolean(active && onChoose && options && message.status === "done");
1290
+ return /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-message flex gap-2", children: [
1291
+ /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1292
+ /* @__PURE__ */ jsx13(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1293
+ body ? /* @__PURE__ */ jsx13(MessageCopyButton, { text: body }) : null
1131
1294
  ] }),
1132
- /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1133
- message.status === "error" ? /* @__PURE__ */ jsx12("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__ */ jsx12(TypingDots, {}) : /* @__PURE__ */ jsx12(Markdown, { text: message.text, className: "text-sm" }),
1134
- showToolActivity ? /* @__PURE__ */ jsx12(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1295
+ /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1296
+ message.status === "error" ? /* @__PURE__ */ jsx13("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__ */ jsx13(TypingDots, {}) : /* @__PURE__ */ jsx13(Markdown, { text: body, className: "text-sm" }),
1297
+ showChoices ? /* @__PURE__ */ jsx13(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1298
+ showToolActivity ? /* @__PURE__ */ jsx13(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1135
1299
  ] })
1136
1300
  ] });
1137
- }
1301
+ });
1138
1302
  function MessageCopyButton({ text }) {
1139
- const [copied, setCopied] = useState4(false);
1303
+ const [copied, setCopied] = useState5(false);
1140
1304
  const copy = async () => {
1141
1305
  try {
1142
1306
  await navigator.clipboard.writeText(text);
@@ -1145,12 +1309,12 @@ function MessageCopyButton({ text }) {
1145
1309
  } catch {
1146
1310
  }
1147
1311
  };
1148
- return /* @__PURE__ */ jsx12("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__ */ jsx12(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1312
+ return /* @__PURE__ */ jsx13("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__ */ jsx13(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx13(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1149
1313
  }
1150
1314
  function TypingDots() {
1151
- return /* @__PURE__ */ jsxs4("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1152
- /* @__PURE__ */ jsx12("span", { className: "frankie-thinking-label", children: "Thinking" }),
1153
- [0, 1, 2].map((index) => /* @__PURE__ */ jsx12(
1315
+ return /* @__PURE__ */ jsxs5("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1316
+ /* @__PURE__ */ jsx13("span", { className: "frankie-thinking-label", children: "Thinking" }),
1317
+ [0, 1, 2].map((index) => /* @__PURE__ */ jsx13(
1154
1318
  "span",
1155
1319
  {
1156
1320
  className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
@@ -1162,14 +1326,14 @@ function TypingDots() {
1162
1326
  }
1163
1327
 
1164
1328
  // src/components/chat/SuggestedPrompts.tsx
1165
- import { jsx as jsx13 } from "react/jsx-runtime";
1329
+ import { jsx as jsx14 } from "react/jsx-runtime";
1166
1330
  function SuggestedPrompts({
1167
1331
  prompts,
1168
1332
  onPick,
1169
1333
  disabled
1170
1334
  }) {
1171
1335
  if (prompts.length === 0) return null;
1172
- return /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx13(
1336
+ return /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx14(
1173
1337
  Button,
1174
1338
  {
1175
1339
  type: "button",
@@ -1184,7 +1348,7 @@ function SuggestedPrompts({
1184
1348
  }
1185
1349
 
1186
1350
  // src/components/chat/ChatPanel.tsx
1187
- import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1351
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1188
1352
  function ChatPanel({
1189
1353
  className,
1190
1354
  header,
@@ -1204,8 +1368,10 @@ function ChatPanel({
1204
1368
  const atBottomRef = useRef4(true);
1205
1369
  const features = config?.features;
1206
1370
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1207
- const [model, setModel] = useState5(void 0);
1208
- useEffect4(() => {
1371
+ const [model, setModel] = useState6(void 0);
1372
+ const composerFocus = useRef4(null);
1373
+ const focusComposer = () => composerFocus.current?.();
1374
+ useEffect5(() => {
1209
1375
  const node = scrollRef.current;
1210
1376
  if (!node) return;
1211
1377
  const onScroll = () => {
@@ -1214,14 +1380,14 @@ function ChatPanel({
1214
1380
  node.addEventListener("scroll", onScroll, { passive: true });
1215
1381
  return () => node.removeEventListener("scroll", onScroll);
1216
1382
  }, []);
1217
- useEffect4(() => {
1383
+ useEffect5(() => {
1218
1384
  const node = scrollRef.current;
1219
1385
  if (!node || !atBottomRef.current) return;
1220
1386
  node.scrollTop = node.scrollHeight;
1221
1387
  }, [messages]);
1222
1388
  const name = config?.branding.name ?? "Assistant";
1223
1389
  const mode = resolveMode(theme?.mode);
1224
- return /* @__PURE__ */ jsxs5(
1390
+ return /* @__PURE__ */ jsxs6(
1225
1391
  Card,
1226
1392
  {
1227
1393
  className: cn(
@@ -1234,44 +1400,49 @@ function ChatPanel({
1234
1400
  style: themeToCssVars(theme),
1235
1401
  "data-frankie-mode": mode,
1236
1402
  children: [
1237
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs5(CardHeader, { children: [
1238
- /* @__PURE__ */ jsx14(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1239
- /* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
1240
- /* @__PURE__ */ jsx14(CardTitle, { children: name }),
1241
- config?.branding.tagline ? /* @__PURE__ */ jsx14(CardDescription, { children: config.branding.tagline }) : null
1403
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs6(CardHeader, { children: [
1404
+ /* @__PURE__ */ jsx15(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1405
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
1406
+ /* @__PURE__ */ jsx15(CardTitle, { children: name }),
1407
+ config?.branding.tagline ? /* @__PURE__ */ jsx15(CardDescription, { children: config.branding.tagline }) : null
1242
1408
  ] }),
1243
- /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx14(RotateCcw, {}) }),
1244
- onClose ? /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx14(X3, {}) }) : null
1409
+ /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx15(RotateCcw, {}) }),
1410
+ onClose ? /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx15(X3, {}) }) : null
1245
1411
  ] }),
1246
- /* @__PURE__ */ jsx14(AgentContext, {}),
1247
- /* @__PURE__ */ jsx14(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx14(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs5("div", { className: "frankie-config-error", role: "alert", children: [
1248
- /* @__PURE__ */ jsx14("strong", { children: "Frankie couldn\u2019t start" }),
1249
- /* @__PURE__ */ jsx14("p", { children: configError }),
1250
- /* @__PURE__ */ jsx14("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1251
- ] }) : loading ? /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
1252
- /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1253
- /* @__PURE__ */ jsx14(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1254
- ] }) : /* @__PURE__ */ jsx14("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1255
- (message) => renderMessage ? /* @__PURE__ */ jsx14("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx14(
1412
+ /* @__PURE__ */ jsx15(AgentContext, {}),
1413
+ /* @__PURE__ */ jsx15(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx15(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs6("div", { className: "frankie-config-error", role: "alert", children: [
1414
+ /* @__PURE__ */ jsx15("strong", { children: "Frankie couldn\u2019t start" }),
1415
+ /* @__PURE__ */ jsx15("p", { children: configError }),
1416
+ /* @__PURE__ */ jsx15("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1417
+ ] }) : loading ? /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-4", children: [
1418
+ /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1419
+ /* @__PURE__ */ jsx15(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1420
+ ] }) : /* @__PURE__ */ jsx15("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1421
+ (message, index) => renderMessage ? /* @__PURE__ */ jsx15("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx15(
1256
1422
  MessageBubble,
1257
1423
  {
1258
1424
  message,
1259
1425
  assistantName: name,
1260
1426
  ...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
1261
1427
  showToolActivity: config?.showToolActivity ?? true,
1262
- ...renderTool ? { renderTool } : {}
1428
+ ...renderTool ? { renderTool } : {},
1429
+ onChoose: send,
1430
+ onRequestInput: focusComposer,
1431
+ active: index === messages.length - 1,
1432
+ busy
1263
1433
  },
1264
1434
  message.id
1265
1435
  )
1266
1436
  ) }) }) }),
1267
- /* @__PURE__ */ jsxs5(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1268
- error ? /* @__PURE__ */ jsx14("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1269
- /* @__PURE__ */ jsx14("div", { className: "w-full", children: /* @__PURE__ */ jsx14(
1437
+ /* @__PURE__ */ jsxs6(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1438
+ error ? /* @__PURE__ */ jsx15("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1439
+ /* @__PURE__ */ jsx15("div", { className: "w-full", children: /* @__PURE__ */ jsx15(
1270
1440
  Composer,
1271
1441
  {
1272
1442
  onSend: send,
1273
1443
  onStop: stop,
1274
1444
  busy,
1445
+ focusHandleRef: composerFocus,
1275
1446
  disabled: loading || restoring || Boolean(configError),
1276
1447
  ...placeholder ? { placeholder } : {},
1277
1448
  ...autoFocus ? { autoFocus } : {},
@@ -1287,7 +1458,7 @@ function ChatPanel({
1287
1458
  }
1288
1459
 
1289
1460
  // src/components/chat/FrankieWidget.tsx
1290
- import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1461
+ import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
1291
1462
  function FrankieWidget({
1292
1463
  position = "bottom-right",
1293
1464
  offset = 24,
@@ -1300,13 +1471,15 @@ function FrankieWidget({
1300
1471
  }) {
1301
1472
  const { config } = useFrankie();
1302
1473
  const theme = useFrankieTheme();
1303
- const [uncontrolledOpen, setUncontrolledOpen] = useState6(defaultOpen);
1474
+ const rootRef = useRef5(null);
1475
+ const stylesReady = useStylesReady(rootRef);
1476
+ const [uncontrolledOpen, setUncontrolledOpen] = useState7(defaultOpen);
1304
1477
  const open = controlledOpen ?? uncontrolledOpen;
1305
1478
  const setOpen = (next) => {
1306
1479
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1307
1480
  onOpenChange?.(next);
1308
1481
  };
1309
- useEffect5(() => {
1482
+ useEffect6(() => {
1310
1483
  if (!open) return;
1311
1484
  const onKeyDown = (event) => {
1312
1485
  if (event.key === "Escape") setOpen(false);
@@ -1317,15 +1490,17 @@ function FrankieWidget({
1317
1490
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1318
1491
  const mode = resolveMode(theme?.mode);
1319
1492
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1320
- return /* @__PURE__ */ jsx15(
1493
+ return /* @__PURE__ */ jsx16(
1321
1494
  "div",
1322
1495
  {
1496
+ ref: rootRef,
1323
1497
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1324
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1498
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
1325
1499
  "data-frankie-mode": mode,
1326
- children: /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1327
- open ? /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx15(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1328
- /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx15(
1500
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1501
+ children: /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1502
+ open ? /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1503
+ /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx16(
1329
1504
  Button,
1330
1505
  {
1331
1506
  size: "icon",
@@ -1334,7 +1509,7 @@ function FrankieWidget({
1334
1509
  "aria-expanded": open,
1335
1510
  "aria-label": open ? "Close the assistant" : label,
1336
1511
  title: label,
1337
- children: open ? /* @__PURE__ */ jsx15(X4, { className: "size-5" }) : /* @__PURE__ */ jsx15(MessageCircle, { className: "size-5" })
1512
+ children: open ? /* @__PURE__ */ jsx16(X4, { className: "size-5" }) : /* @__PURE__ */ jsx16(MessageCircle, { className: "size-5" })
1338
1513
  }
1339
1514
  ) })
1340
1515
  ] })
@@ -1343,39 +1518,44 @@ function FrankieWidget({
1343
1518
  }
1344
1519
 
1345
1520
  // src/components/chat/FrankieInline.tsx
1346
- import { jsx as jsx16 } from "react/jsx-runtime";
1521
+ import { useRef as useRef6 } from "react";
1522
+ import { jsx as jsx17 } from "react/jsx-runtime";
1347
1523
  function FrankieInline({ className, style, ...panelProps }) {
1348
1524
  const theme = useFrankieTheme();
1349
1525
  const mode = resolveMode(theme?.mode);
1350
- return /* @__PURE__ */ jsx16(
1526
+ const rootRef = useRef6(null);
1527
+ const stylesReady = useStylesReady(rootRef);
1528
+ return /* @__PURE__ */ jsx17(
1351
1529
  "div",
1352
1530
  {
1531
+ ref: rootRef,
1353
1532
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1354
- style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1533
+ style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
1355
1534
  "data-frankie-mode": mode,
1356
- children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, fill: true })
1535
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1536
+ children: /* @__PURE__ */ jsx17(ChatPanel, { ...panelProps, fill: true })
1357
1537
  }
1358
1538
  );
1359
1539
  }
1360
1540
 
1361
1541
  // src/components/chat/FrankieChatPage.tsx
1362
- import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1542
+ import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
1363
1543
  import { ChevronsLeft, ChevronsRight, History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
1364
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
1544
+ import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1365
1545
  function PageEmptyState() {
1366
1546
  const { config } = useFrankie();
1367
1547
  const { send, busy, restoring } = useFrankieChat();
1368
1548
  const name = config?.branding.name ?? "Frankie";
1369
- return /* @__PURE__ */ jsxs7("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1370
- /* @__PURE__ */ jsx17("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1371
- /* @__PURE__ */ jsxs7("div", { children: [
1372
- /* @__PURE__ */ jsx17("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1373
- /* @__PURE__ */ jsx17("h1", { children: "How can I help?" }),
1374
- /* @__PURE__ */ jsx17("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1549
+ return /* @__PURE__ */ jsxs8("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1550
+ /* @__PURE__ */ jsx18("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1551
+ /* @__PURE__ */ jsxs8("div", { children: [
1552
+ /* @__PURE__ */ jsx18("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1553
+ /* @__PURE__ */ jsx18("h1", { children: "How can I help?" }),
1554
+ /* @__PURE__ */ jsx18("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1375
1555
  ] }),
1376
- /* @__PURE__ */ jsx17("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1377
- /* @__PURE__ */ jsx17("strong", { children: prompt }),
1378
- /* @__PURE__ */ jsx17("b", { "aria-hidden": true, children: "\u2197" })
1556
+ /* @__PURE__ */ jsx18("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1557
+ /* @__PURE__ */ jsx18("strong", { children: prompt }),
1558
+ /* @__PURE__ */ jsx18("b", { "aria-hidden": true, children: "\u2197" })
1379
1559
  ] }, prompt)) })
1380
1560
  ] });
1381
1561
  }
@@ -1383,21 +1563,21 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1383
1563
  const { client, config } = useFrankie();
1384
1564
  const { busy, messages } = useFrankieChat();
1385
1565
  const theme = useFrankieTheme();
1386
- const [conversations, setConversations] = useState7([]);
1387
- const [historyError, setHistoryError] = useState7("");
1388
- const [historyLoading, setHistoryLoading] = useState7(false);
1389
- const [drawerOpen, setDrawerOpen] = useState7(false);
1390
- const [collapsed, setCollapsed] = useState7(() => {
1566
+ const [conversations, setConversations] = useState8([]);
1567
+ const [historyError, setHistoryError] = useState8("");
1568
+ const [historyLoading, setHistoryLoading] = useState8(false);
1569
+ const [drawerOpen, setDrawerOpen] = useState8(false);
1570
+ const [collapsed, setCollapsed] = useState8(() => {
1391
1571
  try {
1392
1572
  return window.localStorage.getItem("frankie.history-collapsed") === "1";
1393
1573
  } catch {
1394
1574
  return false;
1395
1575
  }
1396
1576
  });
1397
- const [query, setQuery] = useState7("");
1398
- const dialog = useRef5(null);
1399
- const menu = useRef5(null);
1400
- const requestVersion = useRef5(0);
1577
+ const [query, setQuery] = useState8("");
1578
+ const dialog = useRef7(null);
1579
+ const menu = useRef7(null);
1580
+ const requestVersion = useRef7(0);
1401
1581
  const refresh = useCallback3(async () => {
1402
1582
  if (!showHistory || !config || config.allowAnonymous) {
1403
1583
  setHistoryError("");
@@ -1419,19 +1599,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1419
1599
  if (version === requestVersion.current) setHistoryLoading(false);
1420
1600
  }
1421
1601
  }, [client, showHistory, config?.allowAnonymous]);
1422
- useEffect6(() => {
1602
+ useEffect7(() => {
1423
1603
  if (!busy) void refresh();
1424
1604
  return () => {
1425
1605
  requestVersion.current++;
1426
1606
  };
1427
1607
  }, [refresh, busy]);
1428
- useEffect6(() => {
1608
+ useEffect7(() => {
1429
1609
  try {
1430
1610
  window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
1431
1611
  } catch {
1432
1612
  }
1433
1613
  }, [collapsed]);
1434
- useEffect6(() => {
1614
+ useEffect7(() => {
1435
1615
  const node = dialog.current;
1436
1616
  if (drawerOpen) {
1437
1617
  node?.showModal?.();
@@ -1449,79 +1629,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1449
1629
  close();
1450
1630
  }
1451
1631
  };
1452
- const history = /* @__PURE__ */ jsxs7(Fragment3, { children: [
1453
- /* @__PURE__ */ jsxs7("div", { className: "frankie-history-heading", children: [
1454
- /* @__PURE__ */ jsx17("strong", { children: "Conversations" }),
1455
- /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx17(X5, { size: 18 }) })
1632
+ const history = /* @__PURE__ */ jsxs8(Fragment3, { children: [
1633
+ /* @__PURE__ */ jsxs8("div", { className: "frankie-history-heading", children: [
1634
+ /* @__PURE__ */ jsx18("strong", { children: "Conversations" }),
1635
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx18(X5, { size: 18 }) })
1456
1636
  ] }),
1457
- /* @__PURE__ */ jsxs7("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1637
+ /* @__PURE__ */ jsxs8("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1458
1638
  onNew();
1459
1639
  close();
1460
1640
  }, children: [
1461
- /* @__PURE__ */ jsx17(Plus2, { size: 17 }),
1641
+ /* @__PURE__ */ jsx18(Plus2, { size: 17 }),
1462
1642
  "New chat"
1463
1643
  ] }),
1464
- /* @__PURE__ */ jsxs7("label", { className: "frankie-history-search", children: [
1465
- /* @__PURE__ */ jsx17("span", { children: "Search conversations" }),
1466
- /* @__PURE__ */ jsx17("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1644
+ /* @__PURE__ */ jsxs8("label", { className: "frankie-history-search", children: [
1645
+ /* @__PURE__ */ jsx18("span", { children: "Search conversations" }),
1646
+ /* @__PURE__ */ jsx18("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1467
1647
  ] }),
1468
- /* @__PURE__ */ jsx17("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs7("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1469
- /* @__PURE__ */ jsx17("span", { children: item.title }),
1470
- /* @__PURE__ */ jsx17("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1648
+ /* @__PURE__ */ jsx18("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1649
+ /* @__PURE__ */ jsx18("span", { children: item.title }),
1650
+ /* @__PURE__ */ jsx18("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1471
1651
  ] }, item.id)) }),
1472
- historyLoading ? /* @__PURE__ */ jsx17("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs7("p", { role: "status", children: [
1652
+ historyLoading ? /* @__PURE__ */ jsx18("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs8("p", { role: "status", children: [
1473
1653
  historyError,
1474
- /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1475
- ] }) : !conversations.length ? /* @__PURE__ */ jsx17("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1476
- navigation ? /* @__PURE__ */ jsx17("div", { className: "frankie-host-navigation", children: navigation }) : null
1654
+ /* @__PURE__ */ jsx18("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1655
+ ] }) : !conversations.length ? /* @__PURE__ */ jsx18("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1656
+ navigation ? /* @__PURE__ */ jsx18("div", { className: "frankie-host-navigation", children: navigation }) : null
1477
1657
  ] });
1478
- return /* @__PURE__ */ jsxs7("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1479
- showHistory ? /* @__PURE__ */ jsxs7("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1480
- /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx17(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx17(ChevronsLeft, { size: 16 }) }),
1658
+ return /* @__PURE__ */ jsxs8("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1659
+ showHistory ? /* @__PURE__ */ jsxs8("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1660
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx18(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx18(ChevronsLeft, { size: 16 }) }),
1481
1661
  collapsed ? null : history
1482
1662
  ] }) : null,
1483
- /* @__PURE__ */ jsxs7("div", { className: "frankie-page-main", children: [
1484
- /* @__PURE__ */ jsxs7("header", { className: "frankie-page-header", children: [
1485
- /* @__PURE__ */ jsxs7("div", { children: [
1486
- showHistory ? /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx17(Menu, { size: 20 }) }) : null,
1487
- /* @__PURE__ */ jsx17("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1663
+ /* @__PURE__ */ jsxs8("div", { className: "frankie-page-main", children: [
1664
+ /* @__PURE__ */ jsxs8("header", { className: "frankie-page-header", children: [
1665
+ /* @__PURE__ */ jsxs8("div", { children: [
1666
+ showHistory ? /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx18(Menu, { size: 20 }) }) : null,
1667
+ /* @__PURE__ */ jsx18("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1488
1668
  ] }),
1489
- /* @__PURE__ */ jsxs7("div", { children: [
1490
- /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1491
- /* @__PURE__ */ jsx17(Plus2, { size: 16 }),
1492
- /* @__PURE__ */ jsx17("span", { children: "New chat" })
1669
+ /* @__PURE__ */ jsxs8("div", { children: [
1670
+ /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1671
+ /* @__PURE__ */ jsx18(Plus2, { size: 16 }),
1672
+ /* @__PURE__ */ jsx18("span", { children: "New chat" })
1493
1673
  ] }),
1494
- /* @__PURE__ */ jsxs7("details", { className: "frankie-chat-settings", children: [
1495
- /* @__PURE__ */ jsx17("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx17(Settings, { size: 19 }) }),
1496
- /* @__PURE__ */ jsxs7("div", { children: [
1497
- /* @__PURE__ */ jsx17("strong", { children: "Chat settings" }),
1498
- /* @__PURE__ */ jsx17("p", { children: "History syncs automatically for signed-in users." }),
1499
- /* @__PURE__ */ jsxs7("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1500
- /* @__PURE__ */ jsx17(History, { size: 16 }),
1674
+ /* @__PURE__ */ jsxs8("details", { className: "frankie-chat-settings", children: [
1675
+ /* @__PURE__ */ jsx18("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx18(Settings, { size: 19 }) }),
1676
+ /* @__PURE__ */ jsxs8("div", { children: [
1677
+ /* @__PURE__ */ jsx18("strong", { children: "Chat settings" }),
1678
+ /* @__PURE__ */ jsx18("p", { children: "History syncs automatically for signed-in users." }),
1679
+ /* @__PURE__ */ jsxs8("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1680
+ /* @__PURE__ */ jsx18(History, { size: 16 }),
1501
1681
  "Refresh history"
1502
1682
  ] }),
1503
- /* @__PURE__ */ jsx17("small", { children: "Skills are checked at the start of each message." })
1683
+ /* @__PURE__ */ jsx18("small", { children: "Skills are checked at the start of each message." })
1504
1684
  ] })
1505
1685
  ] })
1506
1686
  ] })
1507
1687
  ] }),
1508
- /* @__PURE__ */ jsx17("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx17(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx17(PageEmptyState, {}) }) })
1688
+ /* @__PURE__ */ jsx18("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx18(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx18(PageEmptyState, {}) }) })
1509
1689
  ] }),
1510
- showHistory ? /* @__PURE__ */ jsx17("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1690
+ showHistory ? /* @__PURE__ */ jsx18("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1511
1691
  if (event.target === event.currentTarget) close();
1512
- }, children: /* @__PURE__ */ jsx17("aside", { children: history }) }) : null
1692
+ }, children: /* @__PURE__ */ jsx18("aside", { children: history }) }) : null
1513
1693
  ] });
1514
1694
  }
1515
1695
  function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1516
- const [id, setId] = useState7(() => provider.conversationId ?? crypto.randomUUID());
1696
+ const [id, setId] = useState8(() => provider.conversationId ?? crypto.randomUUID());
1517
1697
  const onNew = () => setId(crypto.randomUUID());
1518
- return /* @__PURE__ */ jsx17("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ jsx17(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx17(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1698
+ const pageRef = useRef7(null);
1699
+ const stylesReady = useStylesReady(pageRef);
1700
+ return /* @__PURE__ */ jsx18("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__ */ jsx18(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx18(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1519
1701
  }
1520
1702
 
1521
1703
  // src/components/ui/separator.tsx
1522
- import { jsx as jsx18 } from "react/jsx-runtime";
1704
+ import { jsx as jsx19 } from "react/jsx-runtime";
1523
1705
  function Separator({ className }) {
1524
- return /* @__PURE__ */ jsx18("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1706
+ return /* @__PURE__ */ jsx19("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1525
1707
  }
1526
1708
  export {
1527
1709
  Avatar,
@@ -1534,6 +1716,7 @@ export {
1534
1716
  CardHeader,
1535
1717
  CardTitle,
1536
1718
  ChatPanel,
1719
+ Choices,
1537
1720
  Composer,
1538
1721
  FRANKIE_CLOUD_URL,
1539
1722
  FrankieApiError,
@@ -1554,6 +1737,7 @@ export {
1554
1737
  applyEvent,
1555
1738
  buttonVariants,
1556
1739
  cn,
1740
+ extractChoices,
1557
1741
  newAssistantMessage,
1558
1742
  newUserMessage,
1559
1743
  readEventStream,