@frankieai/react 0.2.8 → 0.2.10

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,
@@ -45,12 +46,14 @@ __export(index_exports, {
45
46
  Separator: () => Separator,
46
47
  SuggestedPrompts: () => SuggestedPrompts,
47
48
  Textarea: () => Textarea,
49
+ Thinking: () => Thinking,
48
50
  ToolActivityList: () => ToolActivityList,
49
51
  ToolActivityRow: () => ToolActivityRow,
50
52
  TypingDots: () => TypingDots,
51
53
  applyEvent: () => applyEvent,
52
54
  buttonVariants: () => buttonVariants,
53
55
  cn: () => cn,
56
+ extractChoices: () => extractChoices,
54
57
  newAssistantMessage: () => newAssistantMessage,
55
58
  newUserMessage: () => newUserMessage,
56
59
  readEventStream: () => readEventStream,
@@ -65,6 +68,31 @@ module.exports = __toCommonJS(index_exports);
65
68
  // src/hooks/FrankieProvider.tsx
66
69
  var import_react2 = require("react");
67
70
 
71
+ // src/lib/identity.ts
72
+ function sessionIdentity(token) {
73
+ if (!token) return void 0;
74
+ return jwtSubject(token) ?? token;
75
+ }
76
+ function jwtSubject(token) {
77
+ const parts = token.split(".");
78
+ if (parts.length !== 3 || !parts[1]) return void 0;
79
+ try {
80
+ const payload = JSON.parse(base64UrlDecode(parts[1]));
81
+ return typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ function base64UrlDecode(segment) {
87
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
88
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
89
+ if (typeof atob === "function") {
90
+ const bytes = atob(padded);
91
+ return decodeURIComponent(Array.from(bytes, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join(""));
92
+ }
93
+ return Buffer.from(padded, "base64").toString("utf8");
94
+ }
95
+
68
96
  // src/lib/client.ts
69
97
  var FRANKIE_CLOUD_URL = "https://frankie-control-665333762719.us-central1.run.app";
70
98
  var FrankieApiError = class extends Error {
@@ -93,7 +121,8 @@ var FrankieClient = class {
93
121
  baseUrl;
94
122
  doFetch;
95
123
  session;
96
- sessionUserToken;
124
+ /** Identity (JWT `sub`, else the raw token) the cached session was issued for. */
125
+ sessionUserKey;
97
126
  get sessionId() {
98
127
  return this.session?.sessionId;
99
128
  }
@@ -135,7 +164,18 @@ var FrankieClient = class {
135
164
  return await this.unwrap(response);
136
165
  }
137
166
  async ensureSession(context) {
138
- const userToken = await this.options.getUserToken?.();
167
+ let userToken;
168
+ try {
169
+ userToken = await this.options.getUserToken?.();
170
+ } catch (error) {
171
+ this.resetSession();
172
+ const reason = error instanceof Error && error.message.trim() ? error.message.trim() : void 0;
173
+ throw new FrankieApiError(
174
+ reason ?? "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
175
+ 401,
176
+ "user_token_unavailable"
177
+ );
178
+ }
139
179
  if (this.options.getUserToken && !userToken) {
140
180
  this.resetSession();
141
181
  throw new FrankieApiError(
@@ -144,7 +184,8 @@ var FrankieClient = class {
144
184
  "user_token_unavailable"
145
185
  );
146
186
  }
147
- if (userToken !== this.sessionUserToken) this.resetSession();
187
+ const userKey = sessionIdentity(userToken);
188
+ if (userKey !== this.sessionUserKey) this.resetSession();
148
189
  if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
149
190
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
150
191
  method: "POST",
@@ -156,7 +197,7 @@ var FrankieClient = class {
156
197
  })
157
198
  });
158
199
  this.session = await this.unwrap(response);
159
- this.sessionUserToken = userToken;
200
+ this.sessionUserKey = userKey;
160
201
  this.writeStored();
161
202
  return this.session;
162
203
  }
@@ -178,7 +219,7 @@ var FrankieClient = class {
178
219
  if (response.status === 404) return void 0;
179
220
  const body = await this.unwrap(response);
180
221
  this.session = body.session;
181
- this.sessionUserToken = userToken;
222
+ this.sessionUserKey = sessionIdentity(userToken);
182
223
  this.writeStored();
183
224
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
184
225
  }
@@ -237,9 +278,59 @@ var FrankieClient = class {
237
278
  return;
238
279
  }
239
280
  for await (const event of readEventStream(response.body)) {
281
+ if (event.type === "tool_call_request") {
282
+ void this.runClientTool(session, event.callId, event.request);
283
+ continue;
284
+ }
240
285
  onEvent(event);
241
286
  }
242
287
  }
288
+ /**
289
+ * Execute one host API call in the browser and report the outcome back to the
290
+ * server. Never throws: a transport failure or a missing `api` client becomes
291
+ * a result the model can see and explain, exactly like a non-2xx status.
292
+ */
293
+ async runClientTool(session, callId, request) {
294
+ const result = await this.callHostApi(request);
295
+ await this.postToolResult(session, callId, result).catch(() => void 0);
296
+ }
297
+ async callHostApi(request) {
298
+ const api = this.options.api;
299
+ if (!api) {
300
+ return {
301
+ status: 0,
302
+ error: "This assistant tried to call your API from the browser, but no host API client is configured in the Frankie SDK (the `api` option)."
303
+ };
304
+ }
305
+ try {
306
+ const extra = api.headers ? await api.headers() : {};
307
+ const response = await this.doFetch(joinHostUrl(api.baseUrl, request.path), {
308
+ method: request.method,
309
+ credentials: api.credentials ?? "same-origin",
310
+ headers: {
311
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
312
+ ...request.body !== void 0 ? { "content-type": "application/json" } : {},
313
+ ...extra
314
+ },
315
+ ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {}
316
+ });
317
+ const body = await response.text().catch(() => "");
318
+ return { status: response.status, body };
319
+ } catch (error) {
320
+ return { status: 0, error: error instanceof Error ? error.message : String(error) };
321
+ }
322
+ }
323
+ async postToolResult(session, callId, result) {
324
+ await this.doFetch(`${this.baseUrl}/v1/chat/tool-result`, {
325
+ method: "POST",
326
+ headers: {
327
+ "content-type": "application/json",
328
+ "x-frankie-key": this.options.publishableKey,
329
+ authorization: `Bearer ${session.sessionToken}`
330
+ },
331
+ body: JSON.stringify({ callId, result })
332
+ });
333
+ }
243
334
  async unwrap(response) {
244
335
  if (response.ok) return response.json();
245
336
  let code = "internal";
@@ -253,6 +344,12 @@ var FrankieClient = class {
253
344
  throw new FrankieApiError(message, response.status, code);
254
345
  }
255
346
  };
347
+ function joinHostUrl(baseUrl, path) {
348
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) {
349
+ throw new Error("A client tool path must be relative to the configured host API base URL.");
350
+ }
351
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
352
+ }
256
353
  async function* readEventStream(body) {
257
354
  const reader = body.getReader();
258
355
  const decoder = new TextDecoder();
@@ -285,6 +382,68 @@ function parseFrame(frame) {
285
382
  }
286
383
  }
287
384
 
385
+ // src/lib/theme-inherit.ts
386
+ function parseRgb(value) {
387
+ const match = value.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/i);
388
+ if (!match) return null;
389
+ const alpha = match[4] === void 0 ? 1 : match[4].endsWith("%") ? Number(match[4].slice(0, -1)) / 100 : Number(match[4]);
390
+ return [Number(match[1]), Number(match[2]), Number(match[3]), alpha];
391
+ }
392
+ function toCss([r, g, b, a]) {
393
+ return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
394
+ }
395
+ function sameColor(a, b) {
396
+ return Math.abs(a[0] - b[0]) < 12 && Math.abs(a[1] - b[1]) < 12 && Math.abs(a[2] - b[2]) < 12;
397
+ }
398
+ function luminance([r, g, b]) {
399
+ const channel = (c) => {
400
+ const v = c / 255;
401
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
402
+ };
403
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
404
+ }
405
+ function contrastRatio(a, b) {
406
+ const la = luminance(a), lb = luminance(b);
407
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
408
+ }
409
+ var ACCENT_SELECTORS = ["[data-frankie-accent]", "button[type=submit]", "a[href]", "button", "[class*=primary]", "[class*=btn]"];
410
+ function inferHostTheme() {
411
+ if (typeof document === "undefined" || typeof getComputedStyle !== "function") return null;
412
+ try {
413
+ const bodyStyle = getComputedStyle(document.body);
414
+ const background = parseRgb(bodyStyle.backgroundColor);
415
+ const foreground = parseRgb(bodyStyle.color);
416
+ if (!background || !foreground) return null;
417
+ if (background[3] < 0.5) return null;
418
+ if (contrastRatio(background, foreground) < 2) return null;
419
+ let accent = null;
420
+ for (const selector of ACCENT_SELECTORS) {
421
+ const element = document.querySelector(selector);
422
+ if (!element) continue;
423
+ const style = getComputedStyle(element);
424
+ const surface = parseRgb(style.backgroundColor);
425
+ if (surface && surface[3] > 0.5 && !sameColor(surface, background)) {
426
+ accent = surface;
427
+ break;
428
+ }
429
+ const text = parseRgb(style.color);
430
+ if (text && text[3] > 0.5 && !sameColor(text, foreground)) {
431
+ accent = text;
432
+ break;
433
+ }
434
+ }
435
+ if (!accent) return null;
436
+ return {
437
+ background: toCss(background),
438
+ foreground: toCss(foreground),
439
+ primary: toCss(accent),
440
+ primaryForeground: luminance(accent) > 0.5 ? "#0a0a0a" : "#fafafa"
441
+ };
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+
288
447
  // src/hooks/useFrankieChat.ts
289
448
  var import_react = require("react");
290
449
 
@@ -463,6 +622,41 @@ function useFrankieChatState() {
463
622
  setMessages(next);
464
623
  const abort = new AbortController();
465
624
  abortRef.current = abort;
625
+ let bufferedText = "";
626
+ let pendingThinking = "";
627
+ let frame = null;
628
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
629
+ const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
630
+ const revealTick = () => {
631
+ frame = null;
632
+ let addText = "";
633
+ if (bufferedText) {
634
+ const step = Math.max(3, Math.ceil(bufferedText.length / 6));
635
+ addText = bufferedText.slice(0, step);
636
+ bufferedText = bufferedText.slice(step);
637
+ }
638
+ const addThinking = pendingThinking;
639
+ pendingThinking = "";
640
+ if (addText || addThinking) {
641
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
642
+ }
643
+ if (bufferedText) scheduleReveal();
644
+ };
645
+ const scheduleReveal = () => {
646
+ if (frame === null) frame = raf(revealTick);
647
+ };
648
+ const flushDeltas = () => {
649
+ if (frame !== null) {
650
+ cancelRaf(frame);
651
+ frame = null;
652
+ }
653
+ if (!bufferedText && !pendingThinking) return;
654
+ const addText = bufferedText;
655
+ const addThinking = pendingThinking;
656
+ bufferedText = "";
657
+ pendingThinking = "";
658
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
659
+ };
466
660
  try {
467
661
  let receivedText = false;
468
662
  let receivedToolActivity = false;
@@ -484,11 +678,23 @@ function useFrankieChatState() {
484
678
  return;
485
679
  }
486
680
  if (event.type === "ping" || event.type === "session") return;
487
- if (event.type === "text_delta") receivedText = true;
681
+ if (event.type === "text_delta") {
682
+ receivedText = true;
683
+ bufferedText += event.text;
684
+ scheduleReveal();
685
+ return;
686
+ }
687
+ if (event.type === "thinking_delta") {
688
+ pendingThinking += event.text;
689
+ scheduleReveal();
690
+ return;
691
+ }
488
692
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
693
+ flushDeltas();
489
694
  patchAssistant(assistantId, (message) => applyEvent(message, event));
490
695
  }
491
696
  );
697
+ flushDeltas();
492
698
  if (!receivedText) {
493
699
  patchAssistant(assistantId, (message) => ({
494
700
  ...message,
@@ -497,6 +703,7 @@ function useFrankieChatState() {
497
703
  }));
498
704
  }
499
705
  } catch (caught) {
706
+ flushDeltas();
500
707
  if (abort.signal.aborted) {
501
708
  patchAssistant(assistantId, (message) => ({
502
709
  ...message,
@@ -509,6 +716,7 @@ function useFrankieChatState() {
509
716
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
510
717
  }
511
718
  } finally {
719
+ flushDeltas();
512
720
  patchAssistant(
513
721
  assistantId,
514
722
  (message) => message.status === "streaming" ? { ...message, status: "done" } : message
@@ -556,6 +764,7 @@ function FrankieProvider({
556
764
  context,
557
765
  theme,
558
766
  config: providedConfig,
767
+ injectStyles = true,
559
768
  ...clientOptions
560
769
  }) {
561
770
  const tokenCallback = (0, import_react2.useRef)(clientOptions.getUserToken);
@@ -563,7 +772,7 @@ function FrankieProvider({
563
772
  const client = (0, import_react2.useMemo)(
564
773
  () => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
565
774
  // eslint-disable-next-line react-hooks/exhaustive-deps
566
- [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
775
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, clientOptions.api?.baseUrl, clientOptions.api?.credentials, Boolean(clientOptions.getUserToken)]
567
776
  );
568
777
  const [config, setConfig] = (0, import_react2.useState)(providedConfig);
569
778
  const [configError, setConfigError] = (0, import_react2.useState)(void 0);
@@ -590,9 +799,10 @@ function FrankieProvider({
590
799
  ...configError ? { configError } : {},
591
800
  loading,
592
801
  ...context ? { context } : {},
593
- ...theme ? { theme } : {}
802
+ ...theme ? { theme } : {},
803
+ injectStyles
594
804
  }),
595
- [client, config, configError, loading, context, theme]
805
+ [client, config, configError, loading, context, theme, injectStyles]
596
806
  );
597
807
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
598
808
  }
@@ -607,12 +817,82 @@ function useFrankie() {
607
817
  }
608
818
  function useFrankieTheme() {
609
819
  const { config, theme } = useFrankie();
610
- return (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
820
+ const merged = (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
821
+ const inheriting = merged?.source === "inherit";
822
+ const [inferred, setInferred] = (0, import_react2.useState)(null);
823
+ (0, import_react2.useEffect)(() => {
824
+ setInferred(inheriting ? inferHostTheme() : null);
825
+ }, [inheriting]);
826
+ return (0, import_react2.useMemo)(() => {
827
+ if (!inheriting || !inferred) return merged;
828
+ return { ...merged, ...inferred };
829
+ }, [merged, inheriting, inferred]);
611
830
  }
612
831
 
613
832
  // src/components/chat/FrankieWidget.tsx
614
- var import_lucide_react6 = require("lucide-react");
615
- var import_react9 = require("react");
833
+ var import_lucide_react7 = require("lucide-react");
834
+ var import_react10 = require("react");
835
+
836
+ // src/lib/styles-ready.ts
837
+ var import_react3 = require("react");
838
+
839
+ // src/generated/css-content.ts
840
+ var FRANKIE_CSS_CONTENT = `/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
841
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{:root,:host{--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-tight:1.25;--radius-md:.375rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.frankie-page-shell{background:var(--frankie-background);height:100%;min-height:0;color:var(--frankie-foreground);flex:1;display:flex}.frankie-page-main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.frankie-history-desktop{border-right:1px solid var(--frankie-border);background:var(--frankie-secondary);flex-direction:column;flex-shrink:0;gap:16px;width:260px;padding:20px 14px;transition:width .16s,padding .16s;display:flex;overflow:auto}.frankie-history-toggle{border:1px solid var(--frankie-border);background:var(--frankie-background);width:32px;height:32px;color:var(--frankie-muted-foreground);cursor:pointer;border-radius:8px;flex-shrink:0;justify-content:center;align-self:flex-end;align-items:center;display:flex}.frankie-history-toggle:hover{color:var(--frankie-foreground)}.frankie-history-collapsed{width:56px;padding:20px 8px;overflow:visible}.frankie-history-collapsed .frankie-history-toggle{align-self:center}.frankie-history-heading{justify-content:space-between;align-items:center;font-size:14px;display:flex}.frankie-new-chat{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;gap:8px;min-height:44px;font-weight:600;display:flex}.frankie-history-search>span{color:var(--frankie-muted-foreground);margin-bottom:6px;font-size:12px;display:block}.frankie-history-search input{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:8px;width:100%;min-height:40px;padding:8px 10px}.frankie-history-list{flex-direction:column;flex:1;gap:4px;display:flex}.frankie-history-list button{text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:8px;flex-direction:column;align-items:flex-start;gap:5px;min-height:54px;padding:10px;display:flex}.frankie-history-list button span{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:13px;overflow:hidden}.frankie-history-list small{color:var(--frankie-muted-foreground);font-size:11px}.frankie-history-list button:hover,.frankie-history-list button[aria-current]{background:var(--frankie-background);border-color:var(--frankie-border)}.frankie-history-desktop>p,.frankie-history-dialog p{color:var(--frankie-muted-foreground);font-size:13px}.frankie-header-caption{color:var(--frankie-muted-foreground);font-size:12px}.frankie-page-header button{cursor:pointer;justify-content:center;align-items:center;gap:6px;min-width:40px;min-height:40px;display:inline-flex}.frankie-chat-settings{position:relative}.frankie-chat-settings summary{cursor:pointer;border-radius:8px;place-items:center;width:40px;height:40px;list-style:none;display:grid}.frankie-chat-settings summary::-webkit-details-marker{display:none}.frankie-chat-settings>div{z-index:5;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:12px;gap:12px;width:min(280px,80vw);padding:16px;font-size:13px;display:grid;position:absolute;top:46px;right:0;box-shadow:0 10px 30px #0002}.frankie-chat-settings p{color:var(--frankie-muted-foreground);margin:0}.frankie-loaded-skills{width:100%;max-width:760px;color:var(--frankie-muted-foreground);font-size:12px}.frankie-loaded-skills summary{cursor:pointer;padding:6px 0}.frankie-loaded-skills ul{margin:0;padding:4px 0;list-style:none}.frankie-loaded-skills li{gap:8px;padding:4px 0;display:flex}.frankie-loaded-skills small{opacity:.7}.frankie-history-dialog{background:var(--frankie-secondary);width:min(320px,86vw);max-width:none;height:100dvh;max-height:none;color:var(--frankie-foreground);border:0;margin:0;padding:0}.frankie-history-dialog::backdrop{background:#0008}.frankie-history-dialog>aside{height:100%;padding:calc(20px + env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom));flex-direction:column;gap:16px;display:flex;overflow:auto}.frankie-mobile-only{display:none!important}.frankie-widget-panel{max-height:calc(100dvh - 100px)}.frankie-root button:focus-visible,.frankie-root summary:focus-visible,.frankie-root input:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:3px}.frankie-root button:disabled{opacity:.5;cursor:not-allowed}@media (max-width:760px){.frankie-history-desktop{display:none}.frankie-mobile-only{display:inline-flex!important}.frankie-header-caption{display:none}.frankie-page-header{padding:6px 10px}.frankie-history-heading button{background:0 0;border:0;justify-content:center;align-items:center;min-width:44px;min-height:44px}.frankie-page-prompts{gap:8px}.frankie-widget-panel{width:calc(100vw - 32px)}}.frankie-root{--frankie-styles-ready:1;--frankie-background:#fff;--frankie-foreground:#0a0a0a;--frankie-primary:#171717;--frankie-primary-foreground:#fafafa;--frankie-secondary:#f5f5f5;--frankie-secondary-foreground:#171717;--frankie-muted:#f5f5f5;--frankie-muted-foreground:#737373;--frankie-border:#e5e5e5;--frankie-radius:.625rem;--frankie-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;font-family:var(--frankie-font);color:var(--frankie-foreground);-webkit-font-smoothing:antialiased;line-height:1.5}.frankie-root.dark{--frankie-background:#0a0a0a;--frankie-foreground:#fafafa;--frankie-primary:#fafafa;--frankie-primary-foreground:#171717;--frankie-secondary:#262626;--frankie-secondary-foreground:#fafafa;--frankie-muted:#262626;--frankie-muted-foreground:#a3a3a3;--frankie-border:#262626}.frankie-root,.frankie-root *,.frankie-root :before,.frankie-root :after{box-sizing:border-box}.frankie-root button,.frankie-root textarea,.frankie-root input{font:inherit;color:inherit;margin:0}.frankie-root textarea{background-color:var(--frankie-background)}.frankie-focus:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:2px}.frankie-scroll{scrollbar-width:thin;scrollbar-color:var(--frankie-border) transparent}.frankie-prose>*+*{margin-top:.6rem}.frankie-prose p{white-space:pre-wrap;overflow-wrap:anywhere;margin:0;line-height:1.6}.frankie-prose>.frankie-h{letter-spacing:-.01em;margin-top:1.15rem;line-height:1.3}.frankie-prose>.frankie-h:first-child{margin-top:0}.frankie-prose h3.frankie-h{font-size:1.06em;font-weight:700}.frankie-prose h4.frankie-h{font-size:1em;font-weight:600}.frankie-prose h5.frankie-h{color:var(--frankie-muted-foreground);font-size:.92em;font-weight:600}.frankie-prose strong{font-weight:600}.frankie-prose ul,.frankie-prose ol{flex-direction:column;gap:.35rem;margin:0;padding-left:1.4rem;display:flex}.frankie-prose li{padding-left:.15rem;line-height:1.55}.frankie-prose li::marker{color:var(--frankie-muted-foreground)}.frankie-prose ul{list-style:outside}.frankie-prose ol{list-style:decimal}.frankie-prose a{color:var(--frankie-primary);text-underline-offset:2px;text-decoration:underline}.frankie-prose code{background-color:var(--frankie-muted);border-radius:.25rem;padding:.1em .3em;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.85em}.frankie-prose pre{border-radius:var(--frankie-radius);background-color:var(--frankie-muted);margin:0;padding:.625rem .75rem;overflow-x:auto}.frankie-prose pre code{background:0 0;padding:0}.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, var(--frankie-primary), transparent 65%), var(--frankie-background);flex-direction:column;min-width:0;display:flex;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, color-mix(in srgb, var(--frankie-primary) 8%, transparent), transparent 65%), var(--frankie-background)}}.frankie-chat-page{color:var(--frankie-foreground)}.frankie-page-header{border-bottom:1px solid var(--frankie-border);background:var(--frankie-background);flex:0 0 56px;justify-content:space-between;align-items:center;gap:16px;min-height:56px;padding:0 24px;display:flex}@supports (color:color-mix(in lab, red, red)){.frankie-page-header{background:color-mix(in srgb, var(--frankie-background) 92%, transparent)}}.frankie-page-header{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.frankie-page-header>div{align-items:center;gap:10px;display:flex}.frankie-page-header strong{font-size:.925rem;font-weight:650}.frankie-page-status{background:var(--frankie-primary);width:10px;height:10px;box-shadow:0 0 0 4px var(--frankie-primary);border-radius:999px}@supports (color:color-mix(in lab, red, red)){.frankie-page-status{box-shadow:0 0 0 4px color-mix(in srgb, var(--frankie-primary) 16%, transparent)}}.frankie-page-header button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * .9);background:var(--frankie-secondary);cursor:pointer;min-height:36px;padding:0 13px}.frankie-page-header button:hover{border-color:var(--frankie-primary)}.frankie-page-canvas{flex:1;min-width:0;min-height:0;display:flex}.frankie-page-canvas>.frankie-inline{width:100%;max-width:56rem;margin:0 auto}.frankie-page-canvas .frankie-scroll{padding:2rem clamp(1rem,4vw,4rem) 1.25rem}.frankie-page-canvas .frankie-root>.border-t{background:linear-gradient(180deg, transparent, var(--frankie-background) 30%);border-top:0;padding:.75rem clamp(1rem,4vw,4rem) 1.5rem}.frankie-page-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:1rem;width:min(100%,38rem);min-height:100%;margin:auto;padding:clamp(2rem,7vh,4.5rem) 1rem 2rem;display:flex}.frankie-page-avatar{border-radius:calc(var(--frankie-radius) * 1.4);background:var(--frankie-primary);width:48px;height:48px;color:var(--frankie-primary-foreground);place-items:center;font-weight:750;display:grid}.frankie-page-kicker{color:var(--frankie-primary);letter-spacing:.1em;text-transform:uppercase;margin:0 0 .4rem;font-size:.7rem;font-weight:750}.frankie-page-empty h1{letter-spacing:-.045em;margin:0;font-size:clamp(1.65rem,4vw,2.15rem);line-height:1.08}.frankie-page-empty h1+p{max-width:30rem;color:var(--frankie-muted-foreground);margin:.6rem auto 0}.frankie-page-prompts{gap:.55rem;width:100%;margin-top:.4rem;display:grid}.frankie-page-prompts button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * 1.25);background:var(--frankie-secondary);text-align:left;cursor:pointer;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.65rem;min-height:60px;padding:.75rem .9rem;display:grid}.frankie-page-prompts button:hover{border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-page-prompts button:hover{border-color:color-mix(in srgb, var(--frankie-primary) 58%, var(--frankie-border))}}.frankie-page-prompts button:hover{transform:translateY(-1px)}.frankie-page-prompts button>span{color:var(--frankie-muted-foreground);font-size:.68rem;font-weight:700}.frankie-page-prompts button>strong{font-size:.875rem}.frankie-page-prompts button>b{color:var(--frankie-primary);font-size:1rem}@media (max-width:640px){.frankie-page-header{flex-basis:52px;min-height:52px;padding:0 12px}.frankie-page-canvas .frankie-scroll{padding:1.25rem .75rem .5rem}.frankie-page-canvas .frankie-root>.border-t{padding:.65rem .75rem calc(.65rem + env(safe-area-inset-bottom))}}}@layer utilities;@scope(.frankie-root){.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.isolate{isolation:isolate}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0{margin-top:0}.mt-0\\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-1{width:var(--spacing);height:var(--spacing)}.size-1\\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-full{width:100%;height:100%}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-\\[32rem\\]{height:32rem}.h-full{height:100%}.h-px{height:1px}.min-h-7{min-height:calc(var(--spacing) * 7)}.min-h-\\[2\\.25rem\\]{min-height:2.25rem}.w-\\[22rem\\]{width:22rem}.w-full{width:100%}.max-w-\\[12rem\\]{max-width:12rem}.max-w-\\[14rem\\]{max-width:14rem}.max-w-\\[85\\%\\]{max-width:85%}.max-w-\\[calc\\(100vw-2rem\\)\\]{max-width:calc(100vw - 2rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\\[calc\\(var\\(--frankie-radius\\)\\+4px\\)\\]{border-radius:calc(var(--frankie-radius) + 4px)}.rounded-\\[calc\\(var\\(--frankie-radius\\)-2px\\)\\]{border-radius:calc(var(--frankie-radius) - 2px)}.rounded-\\[var\\(--frankie-radius\\)\\]{border-radius:var(--frankie-radius)}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--frankie-border)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--frankie-background)}.bg-border{background-color:var(--frankie-border)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-muted{background-color:var(--frankie-muted)}.bg-muted-foreground{background-color:var(--frankie-muted-foreground)}.bg-primary{background-color:var(--frankie-primary)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-secondary{background-color:var(--frankie-secondary)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\\[0\\.625rem\\]{font-size:.625rem}.text-\\[0\\.6875rem\\]{font-size:.6875rem}.text-\\[11px\\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-emerald-600{color:var(--color-emerald-600)}.text-foreground{color:var(--frankie-foreground)}.text-muted-foreground{color:var(--frankie-muted-foreground)}.text-primary{color:var(--frankie-primary)}.text-primary-foreground{color:var(--frankie-primary-foreground)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--frankie-secondary-foreground)}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.placeholder\\:text-muted-foreground::placeholder{color:var(--frankie-muted-foreground)}@media (hover:hover){.hover\\:border-border:hover{border-color:var(--frankie-border)}.hover\\:bg-muted:hover{background-color:var(--frankie-muted)}.hover\\:text-foreground:hover{color:var(--frankie-foreground)}.hover\\:underline:hover{text-decoration-line:underline}.hover\\:opacity-90:hover{opacity:.9}.hover\\:opacity-100:hover{opacity:1}}.focus-visible\\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\\:ring-primary:focus-visible{--tw-ring-color:var(--frankie-primary)}.focus-visible\\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.disabled\\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\\:opacity-50:disabled{opacity:.5}.dark\\:text-emerald-400:where(.dark,.dark *){color:var(--color-emerald-400)}.dark\\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.\\[\\&_svg\\]\\:pointer-events-none svg{pointer-events:none}.\\[\\&_svg\\]\\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\\[\\&_svg\\]\\:shrink-0 svg{flex-shrink:0}.\\[\\&\\:\\:-webkit-details-marker\\]\\:hidden::-webkit-details-marker{display:none}}.frankie-root.frankie-chat-panel{min-height:0;position:relative}.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px var(--frankie-primary), 0 12px 28px -12px #0f172a47, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px color-mix(in srgb, var(--frankie-primary) 22%, #0f172a6b), 0 12px 28px -12px #0f172a47, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 8%, transparent)}}.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 12%, transparent)}}.frankie-root .frankie-agent-context-wrap{flex:none;justify-content:center;padding:12px 10px;display:flex}.frankie-root .frankie-agent-context{max-width:100%;color:var(--frankie-foreground);font-size:13px;position:relative}.frankie-root .frankie-agent-context>summary{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;white-space:nowrap;border-radius:999px;justify-content:center;align-items:center;gap:10px;min-height:44px;padding:7px 13px;list-style:none;display:flex;box-shadow:0 1px 3px #0000000d}.frankie-root .frankie-agent-context>summary::-webkit-details-marker{display:none}.frankie-root .frankie-agent-context>summary>svg{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-context-count{background:var(--frankie-muted);color:var(--frankie-muted-foreground);border-radius:999px;padding:2px 10px;font-size:12px}.frankie-root .frankie-context-status{color:var(--frankie-muted-foreground);align-items:center;gap:6px;font-size:12px;display:flex}.frankie-root .frankie-context-status i{background:currentColor;border-radius:50%;width:7px;height:7px}.frankie-root .frankie-context-status[data-active=true]{color:#00976b}.frankie-root .frankie-context-detail{z-index:10;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:16px;width:340px;max-width:calc(100vw - 32px);padding:18px;position:absolute;top:calc(100% + 10px);left:50%;transform:translate(-50%);box-shadow:0 12px 32px #00000016}.frankie-root .frankie-context-detail p{color:var(--frankie-muted-foreground);margin:10px 0 0;font-size:12px;line-height:1.6}.frankie-root .frankie-context-detail ul{max-height:220px;margin:12px 0;padding:0;list-style:none;overflow:auto}.frankie-root .frankie-context-detail li{border-bottom:1px solid var(--frankie-border);overflow-wrap:anywhere;justify-content:space-between;gap:12px;padding:8px 0;display:flex}.frankie-root .frankie-context-detail small{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.frankie-root.frankie-chat-panel-fill{--frankie-canvas:var(--frankie-background)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill{--frankie-canvas:color-mix(in srgb,var(--frankie-background) 60%,var(--frankie-muted))}}.frankie-root.frankie-chat-panel-fill{background:var(--frankie-canvas)}.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:44px 16px 26px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:14px max(24px,50% - 480px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-message-list{gap:38px}.frankie-root.frankie-chat-panel-fill .frankie-user-bubble{background:var(--frankie-muted);color:var(--frankie-foreground);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:24px;padding:12px 20px;font-size:15px}.frankie-root.frankie-chat-panel-fill .frankie-assistant-content{flex:1;max-width:100%;font-size:15px;line-height:1.7}.frankie-root.frankie-chat-panel-fill .frankie-assistant-message{gap:14px}.frankie-root .frankie-thinking{color:var(--frankie-muted-foreground);gap:5px}.frankie-root .frankie-thinking-label{margin-right:6px;font-size:14px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{background:0 0;border:0;flex:none;padding:16px max(24px,50% - 500px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-composer{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:28px;padding:14px;box-shadow:0 2px 4px #0000000a}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:color-mix(in srgb,var(--frankie-foreground) 32%,var(--frankie-border))}}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px color-mix(in srgb,var(--frankie-primary) 8%,transparent)}}.frankie-root.frankie-chat-panel-fill .frankie-composer-controls{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.frankie-root.frankie-chat-panel-fill .frankie-composer textarea{resize:none;width:100%;min-height:32px;max-height:140px;color:var(--frankie-foreground);box-shadow:none;background:0 0;border:0;border-radius:0;outline:none;flex:0 0 100%;order:-1;margin-bottom:4px;padding:2px 6px;font-size:16px}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Send],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Stop]{background:var(--frankie-primary);width:42px;height:42px;color:var(--frankie-primary-foreground);border:0;border-radius:50%;margin-left:auto}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Attach\\ files]{border-radius:50%;width:42px;height:42px}.frankie-root .frankie-composer-meta{align-items:center;gap:8px;margin-top:6px;padding:0 2px;display:flex}.frankie-root .frankie-model-picker{align-items:center;min-width:0;display:inline-flex}.frankie-root .frankie-model-select{appearance:none;max-width:12rem;color:var(--frankie-muted-foreground);font:inherit;cursor:pointer;text-overflow:ellipsis;background-color:#0000;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b8b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");background-position:right 6px center;background-repeat:no-repeat;background-size:11px;border:0;border-radius:8px;padding:5px 22px 5px 8px;font-size:12px;font-weight:500;line-height:1;transition:background-color .12s,color .12s}.frankie-root .frankie-model-select:hover{background-color:var(--frankie-muted);color:var(--frankie-foreground)}.frankie-root .frankie-model-select:disabled{opacity:.5;cursor:not-allowed}.frankie-root .frankie-model-select option{color:var(--frankie-foreground);background:var(--frankie-background);font-weight:400}.frankie-root .frankie-history-desktop{background:var(--frankie-background);width:290px;padding:20px 12px}.frankie-root .frankie-history-collapsed{width:56px;padding:20px 8px}.frankie-root .frankie-new-chat{background:0 0;border:0;justify-content:flex-start;padding:8px 12px;font-size:15px}.frankie-root .frankie-history-heading{color:var(--frankie-muted-foreground);padding:0 12px;font-size:12px;font-weight:400}.frankie-root .frankie-history-list button{min-height:44px;padding:10px 12px}.frankie-root .frankie-history-list button span{font-size:14px}.frankie-root .frankie-history-list button[aria-current],.frankie-root .frankie-history-list button:hover{background:var(--frankie-muted);border-color:#0000}.frankie-root .frankie-history-list small{display:none}.frankie-root .frankie-page-header{background:var(--frankie-background);height:56px;min-height:56px}.frankie-root .frankie-page-header>div{min-width:0}.frankie-root .frankie-thread-title{text-overflow:ellipsis;white-space:nowrap;max-width:55vw;font-weight:500;overflow:hidden}.frankie-root .frankie-page-header button{background:0 0;border-color:#0000}.frankie-root .frankie-page-canvas>.frankie-inline{max-width:none}@media (max-width:760px){.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:20px 8px 14px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:12px 16px 16px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{padding:10px 12px calc(12px + env(safe-area-inset-bottom))}.frankie-root .frankie-agent-context>summary{gap:6px;padding:7px 9px;font-size:12px}.frankie-root .frankie-context-count{padding:2px 7px;font-size:11px}.frankie-root .frankie-context-status{gap:4px;font-size:11px}.frankie-root .frankie-agent-context>summary>svg{width:14px}.frankie-root .frankie-thread-title{max-width:42vw}}.frankie-root .frankie-agent-context>summary>svg:first-child{box-sizing:content-box;background:var(--frankie-muted);border-radius:50%;padding:4px}.frankie-root.frankie-chat-panel-fill .frankie-agent-context>summary{font-size:14px}.frankie-root .frankie-agent-context>summary>span:first-of-type{border-right:1px solid var(--frankie-border);padding-right:10px}@media (max-width:760px){.frankie-root .frankie-page-header button>span{display:none}}.frankie-root .frankie-choices{flex-direction:column;gap:8px;margin-top:12px;display:flex}.frankie-root .frankie-choice{text-align:left;cursor:pointer;border:1px solid var(--frankie-border);border-radius:var(--frankie-radius);background:var(--frankie-background);width:100%;color:var(--frankie-foreground);font:inherit;align-items:center;gap:12px;padding:11px 14px;font-size:14px;line-height:1.4;transition:border-color .15s,background-color .15s,transform .12s;display:flex}.frankie-root .frankie-choice:hover:not(:disabled){border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){border-color:color-mix(in srgb, var(--frankie-primary) 55%, var(--frankie-border))}}.frankie-root .frankie-choice:hover:not(:disabled){background:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){background:color-mix(in srgb, var(--frankie-primary) 6%, var(--frankie-background))}}.frankie-root .frankie-choice:active:not(:disabled){transform:scale(.99)}.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px var(--frankie-primary);outline:none}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, var(--frankie-primary) 40%, transparent)}}.frankie-root .frankie-choice:disabled{opacity:.6;cursor:default}.frankie-root .frankie-choice-index{border:1px solid var(--frankie-border);background:var(--frankie-muted);width:24px;height:24px;color:var(--frankie-muted-foreground);border-radius:50%;flex:none;place-items:center;font-size:12px;font-weight:600;transition:background-color .15s,color .15s,border-color .15s;display:grid}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-primary);border-color:var(--frankie-primary);color:var(--frankie-primary-foreground)}.frankie-root .frankie-choice-label{flex:1;min-width:0}.frankie-root .frankie-choice-go{color:var(--frankie-muted-foreground);opacity:0;flex:none;transition:opacity .15s,transform .15s;transform:translate(-4px)}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-go{opacity:1;color:var(--frankie-primary);transform:none}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-choice,.frankie-root .frankie-choice-go,.frankie-root .frankie-choice-index{transition:none}}.frankie-root .frankie-choice-other{color:var(--frankie-muted-foreground);border-style:dashed}.frankie-root .frankie-choice-other .frankie-choice-index{background:0 0}.frankie-root .frankie-choice-other:hover:not(:disabled){color:var(--frankie-foreground)}.frankie-root .frankie-choice-other:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-muted);border-color:var(--frankie-border);color:var(--frankie-foreground)}.frankie-root .frankie-choice-hint{color:var(--frankie-muted-foreground);flex:none;font-size:12px}.frankie-root .frankie-skeleton{flex-direction:column;gap:8px;max-width:min(420px,100%);padding:4px 0 6px;display:flex}.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, var(--frankie-muted-foreground) 25%, var(--frankie-muted-foreground) 37%, var(--frankie-muted-foreground) 63%);border-radius:6px;height:12px}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 25%, color-mix(in srgb, var(--frankie-muted-foreground) 26%, transparent) 37%, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 63%)}}.frankie-root .frankie-skeleton-line{background-size:400% 100%;animation:1.4s infinite frankie-shimmer}@keyframes frankie-shimmer{0%{background-position:100%}to{background-position:0}}@keyframes frankie-skeleton-pulse{0%,to{opacity:.45}50%{opacity:.85}}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-skeleton-line{background:var(--frankie-muted-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:color-mix(in srgb, var(--frankie-muted-foreground) 16%, transparent)}}.frankie-root .frankie-skeleton-line{animation:1.6s ease-in-out infinite frankie-skeleton-pulse}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}`;
842
+
843
+ // src/lib/ensure-frankie-styles.ts
844
+ var STYLE_ELEMENT_ID = "frankie-fallback-styles";
845
+ var attempted = false;
846
+ function ensureFrankieStyles() {
847
+ if (attempted || typeof document === "undefined") return;
848
+ attempted = true;
849
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
850
+ const style = document.createElement("style");
851
+ style.id = STYLE_ELEMENT_ID;
852
+ style.textContent = FRANKIE_CSS_CONTENT;
853
+ document.head.appendChild(style);
854
+ }
855
+
856
+ // src/lib/styles-ready.ts
857
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
858
+ var SENTINEL = "--frankie-styles-ready";
859
+ function useStylesReady(ref, timeoutMs = 600, injectFallback = true) {
860
+ const [ready, setReady] = (0, import_react3.useState)(false);
861
+ useIsomorphicLayoutEffect(() => {
862
+ if (ready) return;
863
+ const node = ref.current;
864
+ if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
865
+ setReady(true);
866
+ return;
867
+ }
868
+ const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
869
+ if (applied()) {
870
+ setReady(true);
871
+ return;
872
+ }
873
+ const deadline = Date.now() + timeoutMs;
874
+ let frame = 0;
875
+ const poll = () => {
876
+ if (applied()) {
877
+ setReady(true);
878
+ return;
879
+ }
880
+ if (Date.now() >= deadline) {
881
+ if (injectFallback) ensureFrankieStyles();
882
+ setReady(true);
883
+ return;
884
+ }
885
+ frame = window.requestAnimationFrame(poll);
886
+ };
887
+ if (typeof window.requestAnimationFrame !== "function") {
888
+ setReady(true);
889
+ return;
890
+ }
891
+ frame = window.requestAnimationFrame(poll);
892
+ return () => window.cancelAnimationFrame(frame);
893
+ }, [ref, ready, timeoutMs, injectFallback]);
894
+ return ready;
895
+ }
616
896
 
617
897
  // src/lib/theme.ts
618
898
  function themeToCssVars(theme) {
@@ -672,8 +952,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
672
952
  }
673
953
 
674
954
  // src/components/chat/ChatPanel.tsx
675
- var import_lucide_react5 = require("lucide-react");
676
- var import_react8 = require("react");
955
+ var import_lucide_react6 = require("lucide-react");
956
+ var import_react9 = require("react");
677
957
 
678
958
  // src/components/ui/avatar.tsx
679
959
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -723,9 +1003,9 @@ function CardFooter({ className, ...props }) {
723
1003
  }
724
1004
 
725
1005
  // src/components/ui/scroll-area.tsx
726
- var import_react3 = require("react");
1006
+ var import_react4 = require("react");
727
1007
  var import_jsx_runtime5 = require("react/jsx-runtime");
728
- var ScrollArea = (0, import_react3.forwardRef)(
1008
+ var ScrollArea = (0, import_react4.forwardRef)(
729
1009
  function ScrollArea2({ className, ...props }, ref) {
730
1010
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref, className: cn("frankie-scroll overflow-y-auto overscroll-contain", className), ...props });
731
1011
  }
@@ -779,12 +1059,12 @@ function AgentContext() {
779
1059
 
780
1060
  // src/components/chat/Composer.tsx
781
1061
  var import_lucide_react2 = require("lucide-react");
782
- var import_react5 = require("react");
1062
+ var import_react6 = require("react");
783
1063
 
784
1064
  // src/components/ui/textarea.tsx
785
- var import_react4 = require("react");
1065
+ var import_react5 = require("react");
786
1066
  var import_jsx_runtime7 = require("react/jsx-runtime");
787
- var Textarea = (0, import_react4.forwardRef)(
1067
+ var Textarea = (0, import_react5.forwardRef)(
788
1068
  function Textarea2({ className, ...props }, ref) {
789
1069
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
790
1070
  "textarea",
@@ -803,21 +1083,28 @@ var Textarea = (0, import_react4.forwardRef)(
803
1083
  // src/components/chat/Composer.tsx
804
1084
  var import_jsx_runtime8 = require("react/jsx-runtime");
805
1085
  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);
1086
+ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
1087
+ const [value, setValue] = (0, import_react6.useState)("");
1088
+ const [files, setFiles] = (0, import_react6.useState)([]);
1089
+ const [fileError, setFileError] = (0, import_react6.useState)(void 0);
1090
+ const textareaRef = (0, import_react6.useRef)(null);
1091
+ const fileInputRef = (0, import_react6.useRef)(null);
1092
+ (0, import_react6.useEffect)(() => {
1093
+ if (!focusHandleRef) return;
1094
+ focusHandleRef.current = () => textareaRef.current?.focus();
1095
+ return () => {
1096
+ focusHandleRef.current = null;
1097
+ };
1098
+ }, [focusHandleRef]);
812
1099
  const canAttach = Boolean(attachments?.enabled);
813
1100
  const models = modelPicker?.models ?? [];
814
- (0, import_react5.useEffect)(() => {
1101
+ (0, import_react6.useEffect)(() => {
815
1102
  const node = textareaRef.current;
816
1103
  if (!node) return;
817
1104
  node.style.height = "auto";
818
1105
  node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
819
1106
  }, [value]);
820
- const addFiles = (0, import_react5.useCallback)(
1107
+ const addFiles = (0, import_react6.useCallback)(
821
1108
  async (list) => {
822
1109
  if (!list || !attachments) return;
823
1110
  setFileError(void 0);
@@ -850,7 +1137,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
850
1137
  [attachments, files.length]
851
1138
  );
852
1139
  const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
853
- const submit = (0, import_react5.useCallback)(() => {
1140
+ const submit = (0, import_react6.useCallback)(() => {
854
1141
  const text = value.trim();
855
1142
  if (!text && files.length === 0 || busy || disabled) return;
856
1143
  setValue("");
@@ -944,19 +1231,22 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
944
1231
  "aria-label": "Message"
945
1232
  }
946
1233
  ),
947
- models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1234
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.ArrowUp, {}) })
1235
+ ] }),
1236
+ models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "frankie-composer-meta", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "frankie-model-picker", title: "Model", children: [
1237
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "frankie-sr-only", children: "Model" }),
1238
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
948
1239
  "select",
949
1240
  {
950
1241
  value: modelPicker?.value ?? models[0]?.id,
951
1242
  onChange: (event) => modelPicker?.onChange(event.target.value),
952
1243
  disabled,
953
1244
  "aria-label": "Model",
954
- className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
1245
+ className: "frankie-model-select",
955
1246
  children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: model.id, children: model.label }, model.id))
956
1247
  }
957
- ) : null,
958
- busy ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.ArrowUp, {}) })
959
- ] })
1248
+ )
1249
+ ] }) }) : null
960
1250
  ]
961
1251
  }
962
1252
  );
@@ -980,15 +1270,77 @@ function readAsBase64(file) {
980
1270
  }
981
1271
 
982
1272
  // src/components/chat/MessageBubble.tsx
983
- var import_lucide_react4 = require("lucide-react");
984
- var import_react7 = require("react");
1273
+ var import_lucide_react5 = require("lucide-react");
1274
+ var import_react8 = require("react");
985
1275
 
986
- // src/components/chat/Markdown.tsx
987
- var import_react6 = require("react");
1276
+ // src/lib/choices.ts
1277
+ var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
1278
+ var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
1279
+ function extractChoices(text) {
1280
+ const closed = CLOSED.exec(text);
1281
+ if (closed) {
1282
+ const options = parseOptionLines(closed[1] ?? "");
1283
+ const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
1284
+ return { body, options: options.length ? options : null };
1285
+ }
1286
+ const open = OPEN.exec(text);
1287
+ if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
1288
+ return { body: text, options: null };
1289
+ }
1290
+ function parseOptionLines(block) {
1291
+ return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
1292
+ }
1293
+
1294
+ // src/components/chat/Choices.tsx
1295
+ var import_lucide_react3 = require("lucide-react");
988
1296
  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) });
1297
+ function Choices({
1298
+ options,
1299
+ onPick,
1300
+ onSomethingElse,
1301
+ disabled
1302
+ }) {
1303
+ if (options.length === 0) return null;
1304
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
1305
+ options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1306
+ "button",
1307
+ {
1308
+ type: "button",
1309
+ className: "frankie-choice",
1310
+ disabled,
1311
+ onClick: () => onPick(option),
1312
+ children: [
1313
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: option }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
1316
+ ]
1317
+ },
1318
+ `${index}-${option}`
1319
+ )),
1320
+ onSomethingElse ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1321
+ "button",
1322
+ {
1323
+ type: "button",
1324
+ className: "frankie-choice frankie-choice-other",
1325
+ disabled,
1326
+ onClick: onSomethingElse,
1327
+ children: [
1328
+ /* @__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" }) }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
1331
+ ]
1332
+ }
1333
+ ) : null
1334
+ ] });
991
1335
  }
1336
+
1337
+ // src/components/chat/Markdown.tsx
1338
+ var import_react7 = require("react");
1339
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1340
+ var Markdown = (0, import_react7.memo)(function Markdown2({ text, className }) {
1341
+ const blocks = (0, import_react7.useMemo)(() => renderBlocks(text), [text]);
1342
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn("frankie-prose", className), children: blocks });
1343
+ });
992
1344
  function renderBlocks(text) {
993
1345
  const lines = text.split("\n");
994
1346
  const blocks = [];
@@ -997,13 +1349,13 @@ function renderBlocks(text) {
997
1349
  let code = null;
998
1350
  const flushParagraph = () => {
999
1351
  if (paragraph.length === 0) return;
1000
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1352
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1001
1353
  paragraph = [];
1002
1354
  };
1003
1355
  const flushList = () => {
1004
1356
  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}`));
1357
+ const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderInline(item) }, index));
1358
+ 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
1359
  list = null;
1008
1360
  };
1009
1361
  for (const line of lines) {
@@ -1011,7 +1363,7 @@ function renderBlocks(text) {
1011
1363
  if (fence) {
1012
1364
  if (code) {
1013
1365
  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}`)
1366
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1015
1367
  );
1016
1368
  code = null;
1017
1369
  } else {
@@ -1042,11 +1394,13 @@ function renderBlocks(text) {
1042
1394
  flushList();
1043
1395
  continue;
1044
1396
  }
1045
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
1397
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
1046
1398
  if (heading) {
1047
1399
  flushParagraph();
1048
1400
  flushList();
1049
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1401
+ const level = (heading[1] ?? "#").length;
1402
+ const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
1403
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
1050
1404
  continue;
1051
1405
  }
1052
1406
  flushList();
@@ -1054,7 +1408,7 @@ function renderBlocks(text) {
1054
1408
  }
1055
1409
  if (code) {
1056
1410
  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}`)
1411
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1058
1412
  );
1059
1413
  }
1060
1414
  flushParagraph();
@@ -1069,28 +1423,28 @@ function renderInline(text) {
1069
1423
  const token = match[0];
1070
1424
  const start = match.index;
1071
1425
  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));
1426
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react7.Fragment, { children: renderToken(token) }, start));
1073
1427
  lastIndex = start + token.length;
1074
1428
  }
1075
1429
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1076
1430
  return nodes;
1077
1431
  }
1078
1432
  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) });
1433
+ if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: token.slice(2, -2) });
1434
+ if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: token.slice(1, -1) });
1081
1435
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1082
1436
  const href = link ? link[2] ?? "" : token;
1083
1437
  const label = link ? link[1] ?? "" : token;
1084
1438
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
1085
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1439
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1086
1440
  }
1087
1441
 
1088
1442
  // src/components/chat/ToolActivityList.tsx
1089
- var import_lucide_react3 = require("lucide-react");
1443
+ var import_lucide_react4 = require("lucide-react");
1090
1444
 
1091
1445
  // src/components/ui/badge.tsx
1092
1446
  var import_class_variance_authority2 = require("class-variance-authority");
1093
- var import_jsx_runtime10 = require("react/jsx-runtime");
1447
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1094
1448
  var badgeVariants = (0, import_class_variance_authority2.cva)(
1095
1449
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1096
1450
  {
@@ -1110,44 +1464,45 @@ function Badge({
1110
1464
  variant,
1111
1465
  ...props
1112
1466
  }) {
1113
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1467
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1114
1468
  }
1115
1469
 
1116
1470
  // src/components/chat/ToolActivityList.tsx
1117
- var import_jsx_runtime11 = require("react/jsx-runtime");
1471
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1118
1472
  function ToolActivityList({ tools, renderTool }) {
1119
1473
  if (tools.length === 0) return null;
1120
1474
  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: [
1475
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1476
+ /* @__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: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1478
+ running ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Wrench, { className: "size-3.5", "aria-hidden": "true" }),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: running ? "Working" : "Activity" }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[11px]", children: [
1126
1481
  "\xB7 ",
1127
1482
  tools.length,
1128
1483
  " tool call",
1129
1484
  tools.length === 1 ? "" : "s"
1130
1485
  ] })
1131
1486
  ] }),
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)) })
1487
+ /* @__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
1488
  ] });
1134
1489
  }
1135
1490
  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 })
1491
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1492
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolIcon, { status: tool.status }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: tool.label })
1139
1494
  ] });
1140
1495
  }
1141
1496
  function ToolIcon({ status }) {
1142
1497
  switch (status) {
1143
1498
  case "running":
1144
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1499
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1145
1500
  case "done":
1146
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3", "aria-hidden": "true" });
1501
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3", "aria-hidden": "true" });
1147
1502
  case "denied":
1148
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1503
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1149
1504
  default:
1150
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.X, { className: "size-3", "aria-hidden": "true" });
1505
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.X, { className: "size-3", "aria-hidden": "true" });
1151
1506
  }
1152
1507
  }
1153
1508
  function badgeVariant(status) {
@@ -1158,47 +1513,56 @@ function badgeVariant(status) {
1158
1513
  }
1159
1514
 
1160
1515
  // src/components/chat/MessageBubble.tsx
1161
- var import_jsx_runtime12 = require("react/jsx-runtime");
1162
- function MessageBubble({
1516
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1517
+ var MessageBubble = (0, import_react8.memo)(function MessageBubble2({
1163
1518
  message,
1164
1519
  assistantName,
1165
1520
  avatarUrl,
1166
1521
  showToolActivity,
1167
- renderTool
1522
+ renderTool,
1523
+ onChoose,
1524
+ onRequestInput,
1525
+ active,
1526
+ busy
1168
1527
  }) {
1169
1528
  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)(
1529
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1530
+ 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
1531
  "span",
1173
1532
  {
1174
1533
  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
1534
  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 })
1535
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1536
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: file.name })
1178
1537
  ]
1179
1538
  },
1180
1539
  `${file.name}-${index}`
1181
1540
  )) }) : 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 })
1541
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1542
+ /* @__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 }),
1543
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: message.text })
1185
1544
  ] }) : null
1186
1545
  ] });
1187
1546
  }
1188
- 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
1547
+ const { body, options } = extractChoices(message.text);
1548
+ const showChoices = Boolean(active && onChoose && options && message.status === "done");
1549
+ const toolsRunning = message.tools.some((tool) => tool.status === "running");
1550
+ const waiting = message.status === "streaming" && body.length === 0 && !toolsRunning;
1551
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1552
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1553
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1554
+ body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: body }) : null
1193
1555
  ] }),
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
1556
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1557
+ 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 }) : body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }) : null,
1558
+ showChoices ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1559
+ showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null,
1560
+ waiting ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thinking, {}) : null
1197
1561
  ] })
1198
1562
  ] });
1199
- }
1563
+ });
1200
1564
  function MessageCopyButton({ text }) {
1201
- const [copied, setCopied] = (0, import_react7.useState)(false);
1565
+ const [copied, setCopied] = (0, import_react8.useState)(false);
1202
1566
  const copy = async () => {
1203
1567
  try {
1204
1568
  await navigator.clipboard.writeText(text);
@@ -1207,12 +1571,12 @@ function MessageCopyButton({ text }) {
1207
1571
  } catch {
1208
1572
  }
1209
1573
  };
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" }) });
1574
+ 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
1575
  }
1212
1576
  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)(
1577
+ 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: [
1578
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1579
+ [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1216
1580
  "span",
1217
1581
  {
1218
1582
  className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
@@ -1222,16 +1586,23 @@ function TypingDots() {
1222
1586
  ))
1223
1587
  ] });
1224
1588
  }
1589
+ function Thinking() {
1590
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-skeleton", role: "status", "aria-label": "Thinking", children: [
1591
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "62%" } }),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "92%" } }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "48%" } })
1594
+ ] });
1595
+ }
1225
1596
 
1226
1597
  // src/components/chat/SuggestedPrompts.tsx
1227
- var import_jsx_runtime13 = require("react/jsx-runtime");
1598
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1228
1599
  function SuggestedPrompts({
1229
1600
  prompts,
1230
1601
  onPick,
1231
1602
  disabled
1232
1603
  }) {
1233
1604
  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)(
1605
+ 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
1606
  Button,
1236
1607
  {
1237
1608
  type: "button",
@@ -1246,7 +1617,7 @@ function SuggestedPrompts({
1246
1617
  }
1247
1618
 
1248
1619
  // src/components/chat/ChatPanel.tsx
1249
- var import_jsx_runtime14 = require("react/jsx-runtime");
1620
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1250
1621
  function ChatPanel({
1251
1622
  className,
1252
1623
  header,
@@ -1262,12 +1633,14 @@ function ChatPanel({
1262
1633
  const { config, configError, loading } = useFrankie();
1263
1634
  const theme = useFrankieTheme();
1264
1635
  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);
1636
+ const scrollRef = (0, import_react9.useRef)(null);
1637
+ const atBottomRef = (0, import_react9.useRef)(true);
1267
1638
  const features = config?.features;
1268
1639
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1269
- const [model, setModel] = (0, import_react8.useState)(void 0);
1270
- (0, import_react8.useEffect)(() => {
1640
+ const [model, setModel] = (0, import_react9.useState)(void 0);
1641
+ const composerFocus = (0, import_react9.useRef)(null);
1642
+ const focusComposer = () => composerFocus.current?.();
1643
+ (0, import_react9.useEffect)(() => {
1271
1644
  const node = scrollRef.current;
1272
1645
  if (!node) return;
1273
1646
  const onScroll = () => {
@@ -1276,14 +1649,14 @@ function ChatPanel({
1276
1649
  node.addEventListener("scroll", onScroll, { passive: true });
1277
1650
  return () => node.removeEventListener("scroll", onScroll);
1278
1651
  }, []);
1279
- (0, import_react8.useEffect)(() => {
1652
+ (0, import_react9.useEffect)(() => {
1280
1653
  const node = scrollRef.current;
1281
1654
  if (!node || !atBottomRef.current) return;
1282
1655
  node.scrollTop = node.scrollHeight;
1283
1656
  }, [messages]);
1284
1657
  const name = config?.branding.name ?? "Assistant";
1285
1658
  const mode = resolveMode(theme?.mode);
1286
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1287
1660
  Card,
1288
1661
  {
1289
1662
  className: cn(
@@ -1296,44 +1669,49 @@ function ChatPanel({
1296
1669
  style: themeToCssVars(theme),
1297
1670
  "data-frankie-mode": mode,
1298
1671
  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
1672
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardHeader, { children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1674
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "min-w-0 flex-1", children: [
1675
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardTitle, { children: name }),
1676
+ config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardDescription, { children: config.branding.tagline }) : null
1304
1677
  ] }),
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
1678
+ /* @__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, {}) }),
1679
+ 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
1680
  ] }),
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)(
1681
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AgentContext, {}),
1682
+ /* @__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: [
1683
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1684
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: configError }),
1685
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1686
+ ] }) : 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: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1688
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1689
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1690
+ (message, index) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1318
1691
  MessageBubble,
1319
1692
  {
1320
1693
  message,
1321
1694
  assistantName: name,
1322
1695
  ...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
1323
1696
  showToolActivity: config?.showToolActivity ?? true,
1324
- ...renderTool ? { renderTool } : {}
1697
+ ...renderTool ? { renderTool } : {},
1698
+ onChoose: send,
1699
+ onRequestInput: focusComposer,
1700
+ active: index === messages.length - 1,
1701
+ busy
1325
1702
  },
1326
1703
  message.id
1327
1704
  )
1328
1705
  ) }) }) }),
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)(
1706
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1707
+ error ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1708
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1332
1709
  Composer,
1333
1710
  {
1334
1711
  onSend: send,
1335
1712
  onStop: stop,
1336
1713
  busy,
1714
+ focusHandleRef: composerFocus,
1337
1715
  disabled: loading || restoring || Boolean(configError),
1338
1716
  ...placeholder ? { placeholder } : {},
1339
1717
  ...autoFocus ? { autoFocus } : {},
@@ -1349,7 +1727,7 @@ function ChatPanel({
1349
1727
  }
1350
1728
 
1351
1729
  // src/components/chat/FrankieWidget.tsx
1352
- var import_jsx_runtime15 = require("react/jsx-runtime");
1730
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1353
1731
  function FrankieWidget({
1354
1732
  position = "bottom-right",
1355
1733
  offset = 24,
@@ -1360,15 +1738,17 @@ function FrankieWidget({
1360
1738
  renderLauncher,
1361
1739
  ...panelProps
1362
1740
  }) {
1363
- const { config } = useFrankie();
1741
+ const { config, injectStyles } = useFrankie();
1364
1742
  const theme = useFrankieTheme();
1365
- const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react9.useState)(defaultOpen);
1743
+ const rootRef = (0, import_react10.useRef)(null);
1744
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1745
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react10.useState)(defaultOpen);
1366
1746
  const open = controlledOpen ?? uncontrolledOpen;
1367
1747
  const setOpen = (next) => {
1368
1748
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1369
1749
  onOpenChange?.(next);
1370
1750
  };
1371
- (0, import_react9.useEffect)(() => {
1751
+ (0, import_react10.useEffect)(() => {
1372
1752
  if (!open) return;
1373
1753
  const onKeyDown = (event) => {
1374
1754
  if (event.key === "Escape") setOpen(false);
@@ -1379,15 +1759,17 @@ function FrankieWidget({
1379
1759
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1380
1760
  const mode = resolveMode(theme?.mode);
1381
1761
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1382
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1762
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1383
1763
  "div",
1384
1764
  {
1765
+ ref: rootRef,
1385
1766
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1386
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1767
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
1387
1768
  "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)(
1769
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1770
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1771
+ 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,
1772
+ /* @__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
1773
  Button,
1392
1774
  {
1393
1775
  size: "icon",
@@ -1396,7 +1778,7 @@ function FrankieWidget({
1396
1778
  "aria-expanded": open,
1397
1779
  "aria-label": open ? "Close the assistant" : label,
1398
1780
  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" })
1781
+ 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
1782
  }
1401
1783
  ) })
1402
1784
  ] })
@@ -1405,39 +1787,45 @@ function FrankieWidget({
1405
1787
  }
1406
1788
 
1407
1789
  // src/components/chat/FrankieInline.tsx
1408
- var import_jsx_runtime16 = require("react/jsx-runtime");
1790
+ var import_react11 = require("react");
1791
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1409
1792
  function FrankieInline({ className, style, ...panelProps }) {
1793
+ const { injectStyles } = useFrankie();
1410
1794
  const theme = useFrankieTheme();
1411
1795
  const mode = resolveMode(theme?.mode);
1412
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1796
+ const rootRef = (0, import_react11.useRef)(null);
1797
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1798
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1413
1799
  "div",
1414
1800
  {
1801
+ ref: rootRef,
1415
1802
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1416
- style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1803
+ style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
1417
1804
  "data-frankie-mode": mode,
1418
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, fill: true })
1805
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1806
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatPanel, { ...panelProps, fill: true })
1419
1807
  }
1420
1808
  );
1421
1809
  }
1422
1810
 
1423
1811
  // 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");
1812
+ var import_react12 = require("react");
1813
+ var import_lucide_react8 = require("lucide-react");
1814
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1427
1815
  function PageEmptyState() {
1428
1816
  const { config } = useFrankie();
1429
1817
  const { send, busy, restoring } = useFrankieChat();
1430
1818
  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." })
1819
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1820
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1821
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1822
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1823
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h1", { children: "How can I help?" }),
1824
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1437
1825
  ] }),
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" })
1826
+ /* @__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: [
1827
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: prompt }),
1828
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1441
1829
  ] }, prompt)) })
1442
1830
  ] });
1443
1831
  }
@@ -1445,22 +1833,22 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1445
1833
  const { client, config } = useFrankie();
1446
1834
  const { busy, messages } = useFrankieChat();
1447
1835
  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)(() => {
1836
+ const [conversations, setConversations] = (0, import_react12.useState)([]);
1837
+ const [historyError, setHistoryError] = (0, import_react12.useState)("");
1838
+ const [historyLoading, setHistoryLoading] = (0, import_react12.useState)(false);
1839
+ const [drawerOpen, setDrawerOpen] = (0, import_react12.useState)(false);
1840
+ const [collapsed, setCollapsed] = (0, import_react12.useState)(() => {
1453
1841
  try {
1454
1842
  return window.localStorage.getItem("frankie.history-collapsed") === "1";
1455
1843
  } catch {
1456
1844
  return false;
1457
1845
  }
1458
1846
  });
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 () => {
1847
+ const [query, setQuery] = (0, import_react12.useState)("");
1848
+ const dialog = (0, import_react12.useRef)(null);
1849
+ const menu = (0, import_react12.useRef)(null);
1850
+ const requestVersion = (0, import_react12.useRef)(0);
1851
+ const refresh = (0, import_react12.useCallback)(async () => {
1464
1852
  if (!showHistory || !config || config.allowAnonymous) {
1465
1853
  setHistoryError("");
1466
1854
  return;
@@ -1481,19 +1869,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1481
1869
  if (version === requestVersion.current) setHistoryLoading(false);
1482
1870
  }
1483
1871
  }, [client, showHistory, config?.allowAnonymous]);
1484
- (0, import_react10.useEffect)(() => {
1872
+ (0, import_react12.useEffect)(() => {
1485
1873
  if (!busy) void refresh();
1486
1874
  return () => {
1487
1875
  requestVersion.current++;
1488
1876
  };
1489
1877
  }, [refresh, busy]);
1490
- (0, import_react10.useEffect)(() => {
1878
+ (0, import_react12.useEffect)(() => {
1491
1879
  try {
1492
1880
  window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
1493
1881
  } catch {
1494
1882
  }
1495
1883
  }, [collapsed]);
1496
- (0, import_react10.useEffect)(() => {
1884
+ (0, import_react12.useEffect)(() => {
1497
1885
  const node = dialog.current;
1498
1886
  if (drawerOpen) {
1499
1887
  node?.showModal?.();
@@ -1511,79 +1899,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1511
1899
  close();
1512
1900
  }
1513
1901
  };
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 }) })
1902
+ const history = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1903
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-history-heading", children: [
1904
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Conversations" }),
1905
+ /* @__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
1906
  ] }),
1519
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1907
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1520
1908
  onNew();
1521
1909
  close();
1522
1910
  }, children: [
1523
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 17 }),
1911
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 17 }),
1524
1912
  "New chat"
1525
1913
  ] }),
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) })
1914
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { className: "frankie-history-search", children: [
1915
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "Search conversations" }),
1916
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1529
1917
  ] }),
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() })
1918
+ /* @__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: [
1919
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: item.title }),
1920
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1533
1921
  ] }, 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: [
1922
+ 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
1923
  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
1924
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1925
+ ] }) : !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,
1926
+ navigation ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1539
1927
  ] });
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 }) }),
1928
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1929
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1930
+ /* @__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
1931
  collapsed ? null : history
1544
1932
  ] }) : 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" })
1933
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-main", children: [
1934
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("header", { className: "frankie-page-header", children: [
1935
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1936
+ 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,
1937
+ /* @__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
1938
  ] }),
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" })
1939
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1940
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1941
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 16 }),
1942
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "New chat" })
1555
1943
  ] }),
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 }),
1944
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { className: "frankie-chat-settings", children: [
1945
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Settings, { size: 19 }) }),
1946
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1947
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Chat settings" }),
1948
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1949
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1950
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.History, { size: 16 }),
1563
1951
  "Refresh history"
1564
1952
  ] }),
1565
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: "Skills are checked at the start of each message." })
1953
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: "Skills are checked at the start of each message." })
1566
1954
  ] })
1567
1955
  ] })
1568
1956
  ] })
1569
1957
  ] }),
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, {}) }) })
1958
+ /* @__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
1959
  ] }),
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) => {
1960
+ 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
1961
  if (event.target === event.currentTarget) close();
1574
- }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { children: history }) }) : null
1962
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("aside", { children: history }) }) : null
1575
1963
  ] });
1576
1964
  }
1577
1965
  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());
1966
+ const [id, setId] = (0, import_react12.useState)(() => provider.conversationId ?? crypto.randomUUID());
1579
1967
  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 }) }) });
1968
+ const pageRef = (0, import_react12.useRef)(null);
1969
+ const stylesReady = useStylesReady(pageRef, void 0, provider.injectStyles);
1970
+ 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
1971
  }
1582
1972
 
1583
1973
  // src/components/ui/separator.tsx
1584
- var import_jsx_runtime18 = require("react/jsx-runtime");
1974
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1585
1975
  function Separator({ className }) {
1586
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1976
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1587
1977
  }
1588
1978
  // Annotate the CommonJS export names for ESM import in node:
1589
1979
  0 && (module.exports = {
@@ -1597,6 +1987,7 @@ function Separator({ className }) {
1597
1987
  CardHeader,
1598
1988
  CardTitle,
1599
1989
  ChatPanel,
1990
+ Choices,
1600
1991
  Composer,
1601
1992
  FRANKIE_CLOUD_URL,
1602
1993
  FrankieApiError,
@@ -1611,12 +2002,14 @@ function Separator({ className }) {
1611
2002
  Separator,
1612
2003
  SuggestedPrompts,
1613
2004
  Textarea,
2005
+ Thinking,
1614
2006
  ToolActivityList,
1615
2007
  ToolActivityRow,
1616
2008
  TypingDots,
1617
2009
  applyEvent,
1618
2010
  buttonVariants,
1619
2011
  cn,
2012
+ extractChoices,
1620
2013
  newAssistantMessage,
1621
2014
  newUserMessage,
1622
2015
  readEventStream,