@frankieai/react 0.1.5 → 0.2.1

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
@@ -63,7 +63,7 @@ __export(index_exports, {
63
63
  module.exports = __toCommonJS(index_exports);
64
64
 
65
65
  // src/hooks/FrankieProvider.tsx
66
- var import_react = require("react");
66
+ var import_react2 = require("react");
67
67
 
68
68
  // src/lib/client.ts
69
69
  var FRANKIE_CLOUD_URL = "https://frankie-data-gq5o5syq2q-uc.a.run.app";
@@ -82,11 +82,18 @@ var FrankieClient = class {
82
82
  this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
83
83
  this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
84
84
  this.session = this.readStored();
85
+ if (options.getUserToken && typeof window !== "undefined") {
86
+ try {
87
+ for (const suffix of ["session", "messages"]) window.localStorage.removeItem(`${this.namespace}.${suffix}`);
88
+ } catch {
89
+ }
90
+ }
85
91
  }
86
92
  options;
87
93
  baseUrl;
88
94
  doFetch;
89
95
  session;
96
+ sessionUserToken;
90
97
  get sessionId() {
91
98
  return this.session?.sessionId;
92
99
  }
@@ -95,7 +102,7 @@ var FrankieClient = class {
95
102
  }
96
103
  /** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
97
104
  get persist() {
98
- return this.options.persist !== false && typeof window !== "undefined";
105
+ return !this.options.getUserToken && this.options.persist !== false && typeof window !== "undefined";
99
106
  }
100
107
  /** localStorage namespace for this tenant/conversation, shared with the chat hook. */
101
108
  get namespace() {
@@ -128,15 +135,17 @@ var FrankieClient = class {
128
135
  return await this.unwrap(response);
129
136
  }
130
137
  async ensureSession(context) {
131
- if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
132
138
  const userToken = await this.options.getUserToken?.();
133
139
  if (this.options.getUserToken && !userToken) {
140
+ this.resetSession();
134
141
  throw new FrankieApiError(
135
142
  "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
136
143
  401,
137
144
  "user_token_unavailable"
138
145
  );
139
146
  }
147
+ if (userToken !== this.sessionUserToken) this.resetSession();
148
+ if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
140
149
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
141
150
  method: "POST",
142
151
  headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
@@ -147,6 +156,7 @@ var FrankieClient = class {
147
156
  })
148
157
  });
149
158
  this.session = await this.unwrap(response);
159
+ this.sessionUserToken = userToken;
150
160
  this.writeStored();
151
161
  return this.session;
152
162
  }
@@ -159,7 +169,7 @@ var FrankieClient = class {
159
169
  return Array.isArray(body.conversations) ? body.conversations : [];
160
170
  }
161
171
  async resumeConversation(id = this.options.conversationId) {
162
- if (!id) return void 0;
172
+ if (!id || !this.options.getUserToken) return void 0;
163
173
  const userToken = await this.requireUserToken();
164
174
  const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
165
175
  method: "POST",
@@ -168,6 +178,7 @@ var FrankieClient = class {
168
178
  if (response.status === 404) return void 0;
169
179
  const body = await this.unwrap(response);
170
180
  this.session = body.session;
181
+ this.sessionUserToken = userToken;
171
182
  this.writeStored();
172
183
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
173
184
  }
@@ -274,64 +285,8 @@ function parseFrame(frame) {
274
285
  }
275
286
  }
276
287
 
277
- // src/hooks/FrankieProvider.tsx
278
- var import_jsx_runtime = require("react/jsx-runtime");
279
- var FrankieContext = (0, import_react.createContext)(null);
280
- function FrankieProvider({
281
- children,
282
- context,
283
- theme,
284
- config: providedConfig,
285
- ...clientOptions
286
- }) {
287
- const client = (0, import_react.useMemo)(
288
- () => new FrankieClient(clientOptions),
289
- // eslint-disable-next-line react-hooks/exhaustive-deps
290
- [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
291
- );
292
- const [config, setConfig] = (0, import_react.useState)(providedConfig);
293
- const [configError, setConfigError] = (0, import_react.useState)(void 0);
294
- const [loading, setLoading] = (0, import_react.useState)(!providedConfig);
295
- (0, import_react.useEffect)(() => {
296
- if (providedConfig) return;
297
- const abort = new AbortController();
298
- setLoading(true);
299
- client.getConfig(abort.signal).then((next) => {
300
- setConfig(next);
301
- setConfigError(void 0);
302
- }).catch((error) => {
303
- if (abort.signal.aborted) return;
304
- setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
305
- }).finally(() => {
306
- if (!abort.signal.aborted) setLoading(false);
307
- });
308
- return () => abort.abort();
309
- }, [client, providedConfig]);
310
- const value = (0, import_react.useMemo)(
311
- () => ({
312
- client,
313
- ...config ? { config } : {},
314
- ...configError ? { configError } : {},
315
- loading,
316
- ...context ? { context } : {},
317
- ...theme ? { theme } : {}
318
- }),
319
- [client, config, configError, loading, context, theme]
320
- );
321
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children });
322
- }
323
- function useFrankie() {
324
- const value = (0, import_react.useContext)(FrankieContext);
325
- if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
326
- return value;
327
- }
328
- function useFrankieTheme() {
329
- const { config, theme } = useFrankie();
330
- return (0, import_react.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
331
- }
332
-
333
288
  // src/hooks/useFrankieChat.ts
334
- var import_react2 = require("react");
289
+ var import_react = require("react");
335
290
 
336
291
  // src/lib/messages.ts
337
292
  function applyEvent(message, event) {
@@ -408,16 +363,23 @@ function newUserMessage(id, text, attachments) {
408
363
  // src/hooks/useFrankieChat.ts
409
364
  var counter = 0;
410
365
  var nextId = (prefix) => `${prefix}_${Date.now().toString(36)}_${counter += 1}`;
366
+ var FrankieChatContext = (0, import_react.createContext)(null);
411
367
  function useFrankieChat() {
368
+ const value = (0, import_react.useContext)(FrankieChatContext);
369
+ if (!value) throw new Error("useFrankieChat requires FrankieProvider.");
370
+ return value;
371
+ }
372
+ function useFrankieChatState() {
412
373
  const { client, context } = useFrankie();
413
- const [messages, setMessages] = (0, import_react2.useState)([]);
414
- const [busy, setBusy] = (0, import_react2.useState)(false);
415
- const [error, setError] = (0, import_react2.useState)(void 0);
416
- const [restored, setRestored] = (0, import_react2.useState)(false);
417
- const abortRef = (0, import_react2.useRef)(null);
418
- const mounted = (0, import_react2.useRef)(true);
419
- const messagesRef = (0, import_react2.useRef)([]);
420
- (0, import_react2.useEffect)(() => {
374
+ const [messages, setMessages] = (0, import_react.useState)([]);
375
+ const [busy, setBusy] = (0, import_react.useState)(false);
376
+ const [error, setError] = (0, import_react.useState)(void 0);
377
+ const [restored, setRestored] = (0, import_react.useState)(false);
378
+ const [loadedSkills, setLoadedSkills] = (0, import_react.useState)(null);
379
+ const abortRef = (0, import_react.useRef)(null);
380
+ const mounted = (0, import_react.useRef)(true);
381
+ const messagesRef = (0, import_react.useRef)([]);
382
+ (0, import_react.useEffect)(() => {
421
383
  mounted.current = true;
422
384
  return () => {
423
385
  mounted.current = false;
@@ -425,7 +387,7 @@ function useFrankieChat() {
425
387
  };
426
388
  }, []);
427
389
  const storageKey = `${client.namespace}.messages`;
428
- (0, import_react2.useEffect)(() => {
390
+ (0, import_react.useEffect)(() => {
429
391
  let active = true;
430
392
  const restore = async () => {
431
393
  if (client.persist) {
@@ -447,6 +409,7 @@ function useFrankieChat() {
447
409
  setMessages(restoredMessages);
448
410
  }
449
411
  } catch {
412
+ if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
450
413
  } finally {
451
414
  if (active) setRestored(true);
452
415
  }
@@ -456,7 +419,7 @@ function useFrankieChat() {
456
419
  active = false;
457
420
  };
458
421
  }, [client, storageKey]);
459
- (0, import_react2.useEffect)(() => {
422
+ (0, import_react.useEffect)(() => {
460
423
  if (!client.persist || !restored || busy) return;
461
424
  try {
462
425
  if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
@@ -465,21 +428,22 @@ function useFrankieChat() {
465
428
  }
466
429
  if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
467
430
  }, [messages, busy, client, storageKey, restored]);
468
- const patchAssistant = (0, import_react2.useCallback)((id, update) => {
431
+ const patchAssistant = (0, import_react.useCallback)((id, update) => {
469
432
  const next = messagesRef.current.map(
470
433
  (message) => message.id === id && message.role === "assistant" ? update(message) : message
471
434
  );
472
435
  messagesRef.current = next;
473
436
  setMessages(next);
474
437
  }, []);
475
- const send = (0, import_react2.useCallback)(
438
+ const send = (0, import_react.useCallback)(
476
439
  async (text, opts) => {
477
440
  const trimmed = text.trim();
478
441
  const attachments = opts?.attachments ?? [];
479
- if (!trimmed && attachments.length === 0 || busy) return;
442
+ if (!trimmed && attachments.length === 0 || abortRef.current || !restored) return;
480
443
  const outgoing = trimmed || "Please take a look at the attached file(s).";
481
444
  const assistantId = nextId("a");
482
445
  setError(void 0);
446
+ setLoadedSkills(null);
483
447
  setBusy(true);
484
448
  const next = [
485
449
  ...messagesRef.current,
@@ -506,6 +470,10 @@ function useFrankieChat() {
506
470
  signal: abort.signal
507
471
  },
508
472
  (event) => {
473
+ if (event.type === "skills_loaded") {
474
+ setLoadedSkills(event.skills);
475
+ return;
476
+ }
509
477
  if (event.type === "ping" || event.type === "session") return;
510
478
  if (event.type === "text_delta") receivedText = true;
511
479
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
@@ -544,10 +512,10 @@ function useFrankieChat() {
544
512
  abortRef.current = null;
545
513
  }
546
514
  },
547
- [busy, client, context, patchAssistant]
515
+ [busy, client, context, patchAssistant, restored]
548
516
  );
549
- const stop = (0, import_react2.useCallback)(() => abortRef.current?.abort(), []);
550
- const reset = (0, import_react2.useCallback)(() => {
517
+ const stop = (0, import_react.useCallback)(() => abortRef.current?.abort(), []);
518
+ const reset = (0, import_react.useCallback)(() => {
551
519
  abortRef.current?.abort();
552
520
  client.resetSession();
553
521
  messagesRef.current = [];
@@ -555,12 +523,13 @@ function useFrankieChat() {
555
523
  setError(void 0);
556
524
  setBusy(false);
557
525
  setRestored(true);
526
+ setLoadedSkills(null);
558
527
  try {
559
528
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
560
529
  } catch {
561
530
  }
562
531
  }, [client]);
563
- return { messages, busy, ...error ? { error } : {}, send, stop, reset };
532
+ return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
564
533
  }
565
534
  function conversationTitle(messages) {
566
535
  const first = messages.find((message) => message.role === "user" && message.text.trim());
@@ -569,8 +538,70 @@ function conversationTitle(messages) {
569
538
  return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
570
539
  }
571
540
 
541
+ // src/hooks/FrankieProvider.tsx
542
+ var import_jsx_runtime = require("react/jsx-runtime");
543
+ var FrankieContext = (0, import_react2.createContext)(null);
544
+ function FrankieProvider({
545
+ children,
546
+ context,
547
+ theme,
548
+ config: providedConfig,
549
+ ...clientOptions
550
+ }) {
551
+ const tokenCallback = (0, import_react2.useRef)(clientOptions.getUserToken);
552
+ tokenCallback.current = clientOptions.getUserToken;
553
+ const client = (0, import_react2.useMemo)(
554
+ () => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
555
+ // eslint-disable-next-line react-hooks/exhaustive-deps
556
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
557
+ );
558
+ const [config, setConfig] = (0, import_react2.useState)(providedConfig);
559
+ const [configError, setConfigError] = (0, import_react2.useState)(void 0);
560
+ const [loading, setLoading] = (0, import_react2.useState)(!providedConfig);
561
+ (0, import_react2.useEffect)(() => {
562
+ if (providedConfig) return;
563
+ const abort = new AbortController();
564
+ setLoading(true);
565
+ client.getConfig(abort.signal).then((next) => {
566
+ setConfig(next);
567
+ setConfigError(void 0);
568
+ }).catch((error) => {
569
+ if (abort.signal.aborted) return;
570
+ setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
571
+ }).finally(() => {
572
+ if (!abort.signal.aborted) setLoading(false);
573
+ });
574
+ return () => abort.abort();
575
+ }, [client, providedConfig]);
576
+ const value = (0, import_react2.useMemo)(
577
+ () => ({
578
+ client,
579
+ ...config ? { config } : {},
580
+ ...configError ? { configError } : {},
581
+ loading,
582
+ ...context ? { context } : {},
583
+ ...theme ? { theme } : {}
584
+ }),
585
+ [client, config, configError, loading, context, theme]
586
+ );
587
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
588
+ }
589
+ function ConversationState({ children }) {
590
+ const chat = useFrankieChatState();
591
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieChatContext.Provider, { value: chat, children });
592
+ }
593
+ function useFrankie() {
594
+ const value = (0, import_react2.useContext)(FrankieContext);
595
+ if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
596
+ return value;
597
+ }
598
+ function useFrankieTheme() {
599
+ const { config, theme } = useFrankie();
600
+ return (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
601
+ }
602
+
572
603
  // src/components/chat/FrankieWidget.tsx
573
- var import_lucide_react5 = require("lucide-react");
604
+ var import_lucide_react6 = require("lucide-react");
574
605
  var import_react9 = require("react");
575
606
 
576
607
  // src/lib/theme.ts
@@ -631,7 +662,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
631
662
  }
632
663
 
633
664
  // src/components/chat/ChatPanel.tsx
634
- var import_lucide_react4 = require("lucide-react");
665
+ var import_lucide_react5 = require("lucide-react");
635
666
  var import_react8 = require("react");
636
667
 
637
668
  // src/components/ui/avatar.tsx
@@ -690,16 +721,51 @@ var ScrollArea = (0, import_react3.forwardRef)(
690
721
  }
691
722
  );
692
723
 
693
- // src/components/chat/Composer.tsx
724
+ // src/components/chat/AgentContext.tsx
694
725
  var import_lucide_react = require("lucide-react");
726
+ var import_jsx_runtime6 = require("react/jsx-runtime");
727
+ function AgentContext() {
728
+ const { loading, configError } = useFrankie();
729
+ const { loadedSkills, busy, restoring, error } = useFrankieChat();
730
+ const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
731
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-agent-context-wrap", children: [
732
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { className: "frankie-agent-context", children: [
733
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("summary", { "aria-label": "Agent context", children: [
734
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react.Sparkles, { size: 17, "aria-hidden": "true" }),
735
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Agent context" }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-context-count", children: loadedSkills === null ? "Not checked" : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"}` }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "frankie-context-status", "data-active": status === "Active", children: [
738
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("i", { "aria-hidden": "true" }),
739
+ status
740
+ ] }),
741
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react.ArrowUpRight, { size: 15, "aria-hidden": "true" })
742
+ ] }),
743
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-context-detail", children: [
744
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: loadedSkills === null ? "Skills are checked when you send a message" : loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn" }),
745
+ loadedSkills?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: skill.name }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("small", { children: [
748
+ "v",
749
+ skill.revision
750
+ ] })
751
+ ] }, skill.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
752
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
753
+ ] })
754
+ ] }),
755
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
756
+ ] });
757
+ }
758
+
759
+ // src/components/chat/Composer.tsx
760
+ var import_lucide_react2 = require("lucide-react");
695
761
  var import_react5 = require("react");
696
762
 
697
763
  // src/components/ui/textarea.tsx
698
764
  var import_react4 = require("react");
699
- var import_jsx_runtime6 = require("react/jsx-runtime");
765
+ var import_jsx_runtime7 = require("react/jsx-runtime");
700
766
  var Textarea = (0, import_react4.forwardRef)(
701
767
  function Textarea2({ className, ...props }, ref) {
702
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
768
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
703
769
  "textarea",
704
770
  {
705
771
  ref,
@@ -714,7 +780,7 @@ var Textarea = (0, import_react4.forwardRef)(
714
780
  );
715
781
 
716
782
  // src/components/chat/Composer.tsx
717
- var import_jsx_runtime7 = require("react/jsx-runtime");
783
+ var import_jsx_runtime8 = require("react/jsx-runtime");
718
784
  var MAX_ROWS_PX = 140;
719
785
  function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
720
786
  const [value, setValue] = (0, import_react5.useState)("");
@@ -776,46 +842,46 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
776
842
  });
777
843
  }, [busy, disabled, files, modelPicker?.value, onSend, value]);
778
844
  const onKeyDown = (event) => {
779
- if (event.key === "Enter" && !event.shiftKey) {
845
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
780
846
  event.preventDefault();
781
847
  submit();
782
848
  }
783
849
  };
784
850
  const nothingToSend = value.trim().length === 0 && files.length === 0;
785
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
851
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
786
852
  "form",
787
853
  {
788
- className: "flex flex-col gap-2",
854
+ className: "frankie-composer flex flex-col gap-2",
789
855
  onSubmit: (event) => {
790
856
  event.preventDefault();
791
857
  submit();
792
858
  },
793
859
  children: [
794
- files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
860
+ files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
795
861
  "span",
796
862
  {
797
863
  className: "inline-flex max-w-[14rem] items-center gap-1.5 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
798
864
  children: [
799
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
800
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: file.name }),
801
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
865
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate", children: file.name }),
867
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
802
868
  "button",
803
869
  {
804
870
  type: "button",
805
871
  onClick: () => removeFile(file.id),
806
872
  "aria-label": `Remove ${file.name}`,
807
873
  className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
808
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.X, { className: "size-3" })
874
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.X, { className: "size-3" })
809
875
  }
810
876
  )
811
877
  ]
812
878
  },
813
879
  file.id
814
880
  )) }) : null,
815
- fileError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
816
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-end gap-2", children: [
817
- canAttach ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
818
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
881
+ fileError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
882
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "frankie-composer-controls flex items-end gap-2", children: [
883
+ canAttach ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
884
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
819
885
  "input",
820
886
  {
821
887
  ref: fileInputRef,
@@ -829,7 +895,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
829
895
  }
830
896
  }
831
897
  ),
832
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
898
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
833
899
  Button,
834
900
  {
835
901
  type: "button",
@@ -839,11 +905,11 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
839
905
  onClick: () => fileInputRef.current?.click(),
840
906
  "aria-label": "Attach files",
841
907
  title: "Attach files",
842
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Paperclip, {})
908
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Plus, {})
843
909
  }
844
910
  )
845
911
  ] }) : null,
846
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
912
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
847
913
  Textarea,
848
914
  {
849
915
  ref: textareaRef,
@@ -857,7 +923,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
857
923
  "aria-label": "Message"
858
924
  }
859
925
  ),
860
- models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
926
+ models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
861
927
  "select",
862
928
  {
863
929
  value: modelPicker?.value ?? models[0]?.id,
@@ -865,10 +931,10 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
865
931
  disabled,
866
932
  "aria-label": "Model",
867
933
  className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
868
- children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: model.id, children: model.label }, model.id))
934
+ children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: model.id, children: model.label }, model.id))
869
935
  }
870
936
  ) : null,
871
- busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.ArrowUp, {}) })
937
+ 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, {}) })
872
938
  ] })
873
939
  ]
874
940
  }
@@ -893,14 +959,14 @@ function readAsBase64(file) {
893
959
  }
894
960
 
895
961
  // src/components/chat/MessageBubble.tsx
896
- var import_lucide_react3 = require("lucide-react");
962
+ var import_lucide_react4 = require("lucide-react");
897
963
  var import_react7 = require("react");
898
964
 
899
965
  // src/components/chat/Markdown.tsx
900
966
  var import_react6 = require("react");
901
- var import_jsx_runtime8 = require("react/jsx-runtime");
967
+ var import_jsx_runtime9 = require("react/jsx-runtime");
902
968
  function Markdown({ text, className }) {
903
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
969
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
904
970
  }
905
971
  function renderBlocks(text) {
906
972
  const lines = text.split("\n");
@@ -910,13 +976,13 @@ function renderBlocks(text) {
910
976
  let code = null;
911
977
  const flushParagraph = () => {
912
978
  if (paragraph.length === 0) return;
913
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
979
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
914
980
  paragraph = [];
915
981
  };
916
982
  const flushList = () => {
917
983
  if (!list) return;
918
- const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("li", { children: renderInline(item) }, index));
919
- blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { children: items }, `l${blocks.length}`));
984
+ const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: renderInline(item) }, index));
985
+ 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}`));
920
986
  list = null;
921
987
  };
922
988
  for (const line of lines) {
@@ -924,7 +990,7 @@ function renderBlocks(text) {
924
990
  if (fence) {
925
991
  if (code) {
926
992
  blocks.push(
927
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
993
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
928
994
  );
929
995
  code = null;
930
996
  } else {
@@ -959,7 +1025,7 @@ function renderBlocks(text) {
959
1025
  if (heading) {
960
1026
  flushParagraph();
961
1027
  flushList();
962
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1028
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
963
1029
  continue;
964
1030
  }
965
1031
  flushList();
@@ -967,7 +1033,7 @@ function renderBlocks(text) {
967
1033
  }
968
1034
  if (code) {
969
1035
  blocks.push(
970
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1036
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
971
1037
  );
972
1038
  }
973
1039
  flushParagraph();
@@ -982,28 +1048,28 @@ function renderInline(text) {
982
1048
  const token = match[0];
983
1049
  const start = match.index;
984
1050
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
985
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
1051
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
986
1052
  lastIndex = start + token.length;
987
1053
  }
988
1054
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
989
1055
  return nodes;
990
1056
  }
991
1057
  function renderToken(token) {
992
- if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("strong", { children: token.slice(2, -2) });
993
- if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: token.slice(1, -1) });
1058
+ if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: token.slice(2, -2) });
1059
+ if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: token.slice(1, -1) });
994
1060
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
995
1061
  const href = link ? link[2] ?? "" : token;
996
1062
  const label = link ? link[1] ?? "" : token;
997
1063
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
998
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1064
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
999
1065
  }
1000
1066
 
1001
1067
  // src/components/chat/ToolActivityList.tsx
1002
- var import_lucide_react2 = require("lucide-react");
1068
+ var import_lucide_react3 = require("lucide-react");
1003
1069
 
1004
1070
  // src/components/ui/badge.tsx
1005
1071
  var import_class_variance_authority2 = require("class-variance-authority");
1006
- var import_jsx_runtime9 = require("react/jsx-runtime");
1072
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1007
1073
  var badgeVariants = (0, import_class_variance_authority2.cva)(
1008
1074
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1009
1075
  {
@@ -1023,44 +1089,44 @@ function Badge({
1023
1089
  variant,
1024
1090
  ...props
1025
1091
  }) {
1026
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1092
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1027
1093
  }
1028
1094
 
1029
1095
  // src/components/chat/ToolActivityList.tsx
1030
- var import_jsx_runtime10 = require("react/jsx-runtime");
1096
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1031
1097
  function ToolActivityList({ tools, renderTool }) {
1032
1098
  if (tools.length === 0) return null;
1033
1099
  const running = tools.some((tool) => tool.status === "running");
1034
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1035
- /* @__PURE__ */ (0, import_jsx_runtime10.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: [
1036
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1037
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: running ? "Working" : "Activity" }),
1038
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-[11px]", children: [
1100
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1101
+ /* @__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: [
1102
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: running ? "Working" : "Activity" }),
1104
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-[11px]", children: [
1039
1105
  "\xB7 ",
1040
1106
  tools.length,
1041
1107
  " tool call",
1042
1108
  tools.length === 1 ? "" : "s"
1043
1109
  ] })
1044
1110
  ] }),
1045
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1111
+ /* @__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)) })
1046
1112
  ] });
1047
1113
  }
1048
1114
  function ToolActivityRow({ tool }) {
1049
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1050
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolIcon, { status: tool.status }),
1051
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "truncate", children: tool.label })
1115
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1116
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolIcon, { status: tool.status }),
1117
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: tool.label })
1052
1118
  ] });
1053
1119
  }
1054
1120
  function ToolIcon({ status }) {
1055
1121
  switch (status) {
1056
1122
  case "running":
1057
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1058
1124
  case "done":
1059
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.Check, { className: "size-3", "aria-hidden": "true" });
1125
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3", "aria-hidden": "true" });
1060
1126
  case "denied":
1061
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1127
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1062
1128
  default:
1063
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.X, { className: "size-3", "aria-hidden": "true" });
1129
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.X, { className: "size-3", "aria-hidden": "true" });
1064
1130
  }
1065
1131
  }
1066
1132
  function badgeVariant(status) {
@@ -1071,7 +1137,7 @@ function badgeVariant(status) {
1071
1137
  }
1072
1138
 
1073
1139
  // src/components/chat/MessageBubble.tsx
1074
- var import_jsx_runtime11 = require("react/jsx-runtime");
1140
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1075
1141
  function MessageBubble({
1076
1142
  message,
1077
1143
  assistantName,
@@ -1080,33 +1146,33 @@ function MessageBubble({
1080
1146
  renderTool
1081
1147
  }) {
1082
1148
  if (message.role === "user") {
1083
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col items-end gap-1", children: [
1084
- message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1149
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1150
+ 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)(
1085
1151
  "span",
1086
1152
  {
1087
1153
  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",
1088
1154
  children: [
1089
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1090
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: file.name })
1155
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: file.name })
1091
1157
  ]
1092
1158
  },
1093
1159
  `${file.name}-${index}`
1094
1160
  )) }) : null,
1095
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1096
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1097
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text })
1161
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1162
+ /* @__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 }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text })
1098
1164
  ] }) : null
1099
1165
  ] });
1100
1166
  }
1101
1167
  const empty = message.text.length === 0 && message.tools.length === 0;
1102
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
1103
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1104
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1105
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text }) : null
1168
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1169
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1170
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1171
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text }) : null
1106
1172
  ] }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "min-w-0 max-w-[85%]", children: [
1108
- message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text, className: "text-sm" }),
1109
- showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1173
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1174
+ 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" }),
1175
+ showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1110
1176
  ] })
1111
1177
  ] });
1112
1178
  }
@@ -1120,28 +1186,31 @@ function MessageCopyButton({ text }) {
1120
1186
  } catch {
1121
1187
  }
1122
1188
  };
1123
- return /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1189
+ 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" }) });
1124
1190
  }
1125
1191
  function TypingDots() {
1126
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1127
- "span",
1128
- {
1129
- className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
1130
- style: { animationDelay: `${index * 150}ms` }
1131
- },
1132
- index
1133
- )) });
1192
+ 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: [
1193
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1194
+ [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1195
+ "span",
1196
+ {
1197
+ className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
1198
+ style: { animationDelay: `${index * 150}ms` }
1199
+ },
1200
+ index
1201
+ ))
1202
+ ] });
1134
1203
  }
1135
1204
 
1136
1205
  // src/components/chat/SuggestedPrompts.tsx
1137
- var import_jsx_runtime12 = require("react/jsx-runtime");
1206
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1138
1207
  function SuggestedPrompts({
1139
1208
  prompts,
1140
1209
  onPick,
1141
1210
  disabled
1142
1211
  }) {
1143
1212
  if (prompts.length === 0) return null;
1144
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1213
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1145
1214
  Button,
1146
1215
  {
1147
1216
  type: "button",
@@ -1156,7 +1225,7 @@ function SuggestedPrompts({
1156
1225
  }
1157
1226
 
1158
1227
  // src/components/chat/ChatPanel.tsx
1159
- var import_jsx_runtime13 = require("react/jsx-runtime");
1228
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1160
1229
  function ChatPanel({
1161
1230
  className,
1162
1231
  header,
@@ -1171,7 +1240,7 @@ function ChatPanel({
1171
1240
  }) {
1172
1241
  const { config, configError, loading } = useFrankie();
1173
1242
  const theme = useFrankieTheme();
1174
- const { messages, busy, error, send, stop, reset } = useFrankieChat();
1243
+ const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
1175
1244
  const scrollRef = (0, import_react8.useRef)(null);
1176
1245
  const features = config?.features;
1177
1246
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
@@ -1184,11 +1253,12 @@ function ChatPanel({
1184
1253
  }, [messages]);
1185
1254
  const name = config?.branding.name ?? "Assistant";
1186
1255
  const mode = resolveMode(theme?.mode);
1187
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1256
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1188
1257
  Card,
1189
1258
  {
1190
1259
  className: cn(
1191
- "frankie-root",
1260
+ "frankie-root frankie-chat-panel",
1261
+ fill && "frankie-chat-panel-fill",
1192
1262
  fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
1193
1263
  mode === "dark" && "dark",
1194
1264
  className
@@ -1196,24 +1266,25 @@ function ChatPanel({
1196
1266
  style: themeToCssVars(theme),
1197
1267
  "data-frankie-mode": mode,
1198
1268
  children: [
1199
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(CardHeader, { children: [
1200
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1201
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "min-w-0 flex-1", children: [
1202
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardTitle, { children: name }),
1203
- config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardDescription, { children: config.branding.tagline }) : null
1269
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardHeader, { children: [
1270
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1271
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "min-w-0 flex-1", children: [
1272
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardTitle, { children: name }),
1273
+ config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardDescription, { children: config.branding.tagline }) : null
1204
1274
  ] }),
1205
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.RotateCcw, {}) }),
1206
- onClose ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.X, {}) }) : null
1275
+ /* @__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, {}) }),
1276
+ 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
1207
1277
  ] }),
1208
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1209
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1210
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: configError }),
1211
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1212
- ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
1213
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1214
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1215
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-col gap-4", children: messages.map(
1216
- (message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1278
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AgentContext, {}),
1279
+ /* @__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: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1281
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: configError }),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1283
+ ] }) : 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: [
1284
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1285
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1286
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1287
+ (message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1217
1288
  MessageBubble,
1218
1289
  {
1219
1290
  message,
@@ -1225,15 +1296,15 @@ function ChatPanel({
1225
1296
  message.id
1226
1297
  )
1227
1298
  ) }) }) }),
1228
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(CardFooter, { className: "flex flex-col gap-2", children: [
1229
- error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1230
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1299
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1300
+ error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1301
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1231
1302
  Composer,
1232
1303
  {
1233
1304
  onSend: send,
1234
1305
  onStop: stop,
1235
1306
  busy,
1236
- disabled: Boolean(configError),
1307
+ disabled: loading || restoring || Boolean(configError),
1237
1308
  ...placeholder ? { placeholder } : {},
1238
1309
  ...autoFocus ? { autoFocus } : {},
1239
1310
  ...features?.attachments.enabled ? { attachments: features.attachments } : {},
@@ -1248,11 +1319,12 @@ function ChatPanel({
1248
1319
  }
1249
1320
 
1250
1321
  // src/components/chat/FrankieWidget.tsx
1251
- var import_jsx_runtime14 = require("react/jsx-runtime");
1322
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1252
1323
  function FrankieWidget({
1253
1324
  position = "bottom-right",
1254
1325
  offset = 24,
1255
1326
  defaultOpen = false,
1327
+ zIndex = 20,
1256
1328
  open: controlledOpen,
1257
1329
  onOpenChange,
1258
1330
  renderLauncher,
@@ -1277,15 +1349,15 @@ function FrankieWidget({
1277
1349
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1278
1350
  const mode = resolveMode(theme?.mode);
1279
1351
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1280
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1352
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1281
1353
  "div",
1282
1354
  {
1283
1355
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1284
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex: 2147483e3 },
1356
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1285
1357
  "data-frankie-mode": mode,
1286
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1287
- open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
1288
- renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1358
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1359
+ 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,
1360
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1289
1361
  Button,
1290
1362
  {
1291
1363
  size: "icon",
@@ -1294,125 +1366,174 @@ function FrankieWidget({
1294
1366
  "aria-expanded": open,
1295
1367
  "aria-label": open ? "Close the assistant" : label,
1296
1368
  title: label,
1297
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.MessageCircle, { className: "size-5" })
1369
+ 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" })
1298
1370
  }
1299
- )
1371
+ ) })
1300
1372
  ] })
1301
1373
  }
1302
1374
  );
1303
1375
  }
1304
1376
 
1305
1377
  // src/components/chat/FrankieInline.tsx
1306
- var import_jsx_runtime15 = require("react/jsx-runtime");
1378
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1307
1379
  function FrankieInline({ className, style, ...panelProps }) {
1308
1380
  const theme = useFrankieTheme();
1309
1381
  const mode = resolveMode(theme?.mode);
1310
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1382
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1311
1383
  "div",
1312
1384
  {
1313
1385
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1314
1386
  style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1315
1387
  "data-frankie-mode": mode,
1316
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatPanel, { ...panelProps, fill: true })
1388
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, fill: true })
1317
1389
  }
1318
1390
  );
1319
1391
  }
1320
1392
 
1321
1393
  // src/components/chat/FrankieChatPage.tsx
1322
- var import_jsx_runtime16 = require("react/jsx-runtime");
1394
+ var import_react10 = require("react");
1395
+ var import_lucide_react7 = require("lucide-react");
1396
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1323
1397
  function PageEmptyState() {
1324
1398
  const { config } = useFrankie();
1325
- const { send, busy } = useFrankieChat();
1399
+ const { send, busy, restoring } = useFrankieChat();
1326
1400
  const name = config?.branding.name ?? "Frankie";
1327
- const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
1328
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1329
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1330
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
1331
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "frankie-page-kicker", children: [
1332
- name,
1333
- " assistant"
1334
- ] }),
1335
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h1", { children: "What would you like to do?" }),
1336
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1401
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1402
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1403
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1404
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1405
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { children: "How can I help?" }),
1406
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1337
1407
  ] }),
1338
- prompts.length ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-prompts", children: prompts.map((prompt, index) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", disabled: busy, onClick: () => send(prompt), children: [
1339
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: String(index + 1).padStart(2, "0") }),
1340
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: prompt }),
1341
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1342
- ] }, prompt)) }) : null
1408
+ /* @__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: [
1409
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: prompt }),
1410
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1411
+ ] }, prompt)) })
1343
1412
  ] });
1344
1413
  }
1345
- function PageChrome({
1346
- header,
1347
- footer,
1348
- emptyState,
1349
- renderMessage,
1350
- renderTool,
1351
- placeholder,
1352
- autoFocus
1353
- }) {
1354
- const { config } = useFrankie();
1355
- const { reset } = useFrankieChat();
1356
- const name = config?.branding.name ?? "Frankie";
1357
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1358
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "frankie-page-header", children: [
1359
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
1360
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-status", "aria-hidden": true }),
1361
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: name })
1414
+ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
1415
+ const { client, config } = useFrankie();
1416
+ const { busy, messages } = useFrankieChat();
1417
+ const theme = useFrankieTheme();
1418
+ const [conversations, setConversations] = (0, import_react10.useState)([]);
1419
+ const [historyError, setHistoryError] = (0, import_react10.useState)("");
1420
+ const [historyLoading, setHistoryLoading] = (0, import_react10.useState)(false);
1421
+ const [drawerOpen, setDrawerOpen] = (0, import_react10.useState)(false);
1422
+ const [query, setQuery] = (0, import_react10.useState)("");
1423
+ const dialog = (0, import_react10.useRef)(null);
1424
+ const menu = (0, import_react10.useRef)(null);
1425
+ const requestVersion = (0, import_react10.useRef)(0);
1426
+ const refresh = (0, import_react10.useCallback)(async () => {
1427
+ if (!showHistory || !config || config.allowAnonymous) {
1428
+ setHistoryError("");
1429
+ return;
1430
+ }
1431
+ const version = ++requestVersion.current;
1432
+ setHistoryLoading(true);
1433
+ setHistoryError("");
1434
+ try {
1435
+ const items = await client.listConversations();
1436
+ if (version === requestVersion.current) setConversations(items);
1437
+ } catch {
1438
+ if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
1439
+ } finally {
1440
+ if (version === requestVersion.current) setHistoryLoading(false);
1441
+ }
1442
+ }, [client, showHistory, config?.allowAnonymous]);
1443
+ (0, import_react10.useEffect)(() => {
1444
+ if (!busy) void refresh();
1445
+ return () => {
1446
+ requestVersion.current++;
1447
+ };
1448
+ }, [refresh, busy]);
1449
+ (0, import_react10.useEffect)(() => {
1450
+ const node = dialog.current;
1451
+ if (drawerOpen) {
1452
+ node?.showModal?.();
1453
+ } else {
1454
+ node?.close?.();
1455
+ }
1456
+ }, [drawerOpen]);
1457
+ const close = () => {
1458
+ setDrawerOpen(false);
1459
+ menu.current?.focus();
1460
+ };
1461
+ const select = (id) => {
1462
+ if (!busy) {
1463
+ onSelect(id);
1464
+ close();
1465
+ }
1466
+ };
1467
+ const history = /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1468
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-history-heading", children: [
1469
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Conversations" }),
1470
+ /* @__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 }) })
1471
+ ] }),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1473
+ onNew();
1474
+ close();
1475
+ }, children: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 17 }),
1477
+ "New chat"
1478
+ ] }),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "frankie-history-search", children: [
1480
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "Search conversations" }),
1481
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1482
+ ] }),
1483
+ /* @__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: [
1484
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: item.title }),
1485
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1486
+ ] }, item.id)) }),
1487
+ 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: [
1488
+ historyError,
1489
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1490
+ ] }) : !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,
1491
+ navigation ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1492
+ ] });
1493
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1494
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { className: "frankie-history-desktop", children: history }) : null,
1495
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-main", children: [
1496
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("header", { className: "frankie-page-header", children: [
1497
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1498
+ 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,
1499
+ /* @__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" })
1500
+ ] }),
1501
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 16 }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "New chat" })
1505
+ ] }),
1506
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("details", { className: "frankie-chat-settings", children: [
1507
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Settings, { size: 19 }) }),
1508
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1509
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Chat settings" }),
1510
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1511
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1512
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.History, { size: 16 }),
1513
+ "Refresh history"
1514
+ ] }),
1515
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: "Skills are checked at the start of each message." })
1516
+ ] })
1517
+ ] })
1518
+ ] })
1362
1519
  ] }),
1363
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", onClick: reset, "aria-label": "Start a new conversation", children: "New chat" })
1520
+ /* @__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, {}) }) })
1364
1521
  ] }),
1365
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1366
- FrankieInline,
1367
- {
1368
- header: header ?? false,
1369
- footer,
1370
- emptyState: emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PageEmptyState, {}),
1371
- renderMessage,
1372
- renderTool,
1373
- placeholder,
1374
- autoFocus
1375
- }
1376
- ) })
1522
+ 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) => {
1523
+ if (event.target === event.currentTarget) close();
1524
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { children: history }) }) : null
1377
1525
  ] });
1378
1526
  }
1379
- function FrankieChatPage({
1380
- className,
1381
- style,
1382
- header,
1383
- footer,
1384
- emptyState,
1385
- renderMessage,
1386
- renderTool,
1387
- placeholder,
1388
- autoFocus,
1389
- ...providerProps
1390
- }) {
1391
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1392
- "main",
1393
- {
1394
- className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
1395
- style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
1396
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1397
- PageChrome,
1398
- {
1399
- header,
1400
- footer,
1401
- emptyState,
1402
- renderMessage,
1403
- renderTool,
1404
- placeholder,
1405
- autoFocus
1406
- }
1407
- ) })
1408
- }
1409
- );
1527
+ function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1528
+ const [id, setId] = (0, import_react10.useState)(() => provider.conversationId ?? crypto.randomUUID());
1529
+ const onNew = () => setId(crypto.randomUUID());
1530
+ 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 }) }) });
1410
1531
  }
1411
1532
 
1412
1533
  // src/components/ui/separator.tsx
1413
- var import_jsx_runtime17 = require("react/jsx-runtime");
1534
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1414
1535
  function Separator({ className }) {
1415
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1536
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1416
1537
  }
1417
1538
  // Annotate the CommonJS export names for ESM import in node:
1418
1539
  0 && (module.exports = {