@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/hooks/FrankieProvider.tsx
4
- import { createContext, useContext, useEffect, useMemo, useState } from "react";
4
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
5
5
 
6
6
  // src/lib/client.ts
7
7
  var FRANKIE_CLOUD_URL = "https://frankie-data-gq5o5syq2q-uc.a.run.app";
@@ -20,11 +20,18 @@ var FrankieClient = class {
20
20
  this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
21
21
  this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
22
22
  this.session = this.readStored();
23
+ if (options.getUserToken && typeof window !== "undefined") {
24
+ try {
25
+ for (const suffix of ["session", "messages"]) window.localStorage.removeItem(`${this.namespace}.${suffix}`);
26
+ } catch {
27
+ }
28
+ }
23
29
  }
24
30
  options;
25
31
  baseUrl;
26
32
  doFetch;
27
33
  session;
34
+ sessionUserToken;
28
35
  get sessionId() {
29
36
  return this.session?.sessionId;
30
37
  }
@@ -33,7 +40,7 @@ var FrankieClient = class {
33
40
  }
34
41
  /** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
35
42
  get persist() {
36
- return this.options.persist !== false && typeof window !== "undefined";
43
+ return !this.options.getUserToken && this.options.persist !== false && typeof window !== "undefined";
37
44
  }
38
45
  /** localStorage namespace for this tenant/conversation, shared with the chat hook. */
39
46
  get namespace() {
@@ -66,15 +73,17 @@ var FrankieClient = class {
66
73
  return await this.unwrap(response);
67
74
  }
68
75
  async ensureSession(context) {
69
- if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
70
76
  const userToken = await this.options.getUserToken?.();
71
77
  if (this.options.getUserToken && !userToken) {
78
+ this.resetSession();
72
79
  throw new FrankieApiError(
73
80
  "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
74
81
  401,
75
82
  "user_token_unavailable"
76
83
  );
77
84
  }
85
+ if (userToken !== this.sessionUserToken) this.resetSession();
86
+ if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
78
87
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
79
88
  method: "POST",
80
89
  headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
@@ -85,6 +94,7 @@ var FrankieClient = class {
85
94
  })
86
95
  });
87
96
  this.session = await this.unwrap(response);
97
+ this.sessionUserToken = userToken;
88
98
  this.writeStored();
89
99
  return this.session;
90
100
  }
@@ -97,7 +107,7 @@ var FrankieClient = class {
97
107
  return Array.isArray(body.conversations) ? body.conversations : [];
98
108
  }
99
109
  async resumeConversation(id = this.options.conversationId) {
100
- if (!id) return void 0;
110
+ if (!id || !this.options.getUserToken) return void 0;
101
111
  const userToken = await this.requireUserToken();
102
112
  const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
103
113
  method: "POST",
@@ -106,6 +116,7 @@ var FrankieClient = class {
106
116
  if (response.status === 404) return void 0;
107
117
  const body = await this.unwrap(response);
108
118
  this.session = body.session;
119
+ this.sessionUserToken = userToken;
109
120
  this.writeStored();
110
121
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
111
122
  }
@@ -212,64 +223,8 @@ function parseFrame(frame) {
212
223
  }
213
224
  }
214
225
 
215
- // src/hooks/FrankieProvider.tsx
216
- import { jsx } from "react/jsx-runtime";
217
- var FrankieContext = createContext(null);
218
- function FrankieProvider({
219
- children,
220
- context,
221
- theme,
222
- config: providedConfig,
223
- ...clientOptions
224
- }) {
225
- const client = useMemo(
226
- () => new FrankieClient(clientOptions),
227
- // eslint-disable-next-line react-hooks/exhaustive-deps
228
- [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
229
- );
230
- const [config, setConfig] = useState(providedConfig);
231
- const [configError, setConfigError] = useState(void 0);
232
- const [loading, setLoading] = useState(!providedConfig);
233
- useEffect(() => {
234
- if (providedConfig) return;
235
- const abort = new AbortController();
236
- setLoading(true);
237
- client.getConfig(abort.signal).then((next) => {
238
- setConfig(next);
239
- setConfigError(void 0);
240
- }).catch((error) => {
241
- if (abort.signal.aborted) return;
242
- setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
243
- }).finally(() => {
244
- if (!abort.signal.aborted) setLoading(false);
245
- });
246
- return () => abort.abort();
247
- }, [client, providedConfig]);
248
- const value = useMemo(
249
- () => ({
250
- client,
251
- ...config ? { config } : {},
252
- ...configError ? { configError } : {},
253
- loading,
254
- ...context ? { context } : {},
255
- ...theme ? { theme } : {}
256
- }),
257
- [client, config, configError, loading, context, theme]
258
- );
259
- return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children });
260
- }
261
- function useFrankie() {
262
- const value = useContext(FrankieContext);
263
- if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
264
- return value;
265
- }
266
- function useFrankieTheme() {
267
- const { config, theme } = useFrankie();
268
- return useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
269
- }
270
-
271
226
  // src/hooks/useFrankieChat.ts
272
- import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
227
+ import { createContext, useContext, useCallback, useEffect, useRef, useState } from "react";
273
228
 
274
229
  // src/lib/messages.ts
275
230
  function applyEvent(message, event) {
@@ -346,16 +301,23 @@ function newUserMessage(id, text, attachments) {
346
301
  // src/hooks/useFrankieChat.ts
347
302
  var counter = 0;
348
303
  var nextId = (prefix) => `${prefix}_${Date.now().toString(36)}_${counter += 1}`;
304
+ var FrankieChatContext = createContext(null);
349
305
  function useFrankieChat() {
306
+ const value = useContext(FrankieChatContext);
307
+ if (!value) throw new Error("useFrankieChat requires FrankieProvider.");
308
+ return value;
309
+ }
310
+ function useFrankieChatState() {
350
311
  const { client, context } = useFrankie();
351
- const [messages, setMessages] = useState2([]);
352
- const [busy, setBusy] = useState2(false);
353
- const [error, setError] = useState2(void 0);
354
- const [restored, setRestored] = useState2(false);
312
+ const [messages, setMessages] = useState([]);
313
+ const [busy, setBusy] = useState(false);
314
+ const [error, setError] = useState(void 0);
315
+ const [restored, setRestored] = useState(false);
316
+ const [loadedSkills, setLoadedSkills] = useState(null);
355
317
  const abortRef = useRef(null);
356
318
  const mounted = useRef(true);
357
319
  const messagesRef = useRef([]);
358
- useEffect2(() => {
320
+ useEffect(() => {
359
321
  mounted.current = true;
360
322
  return () => {
361
323
  mounted.current = false;
@@ -363,7 +325,7 @@ function useFrankieChat() {
363
325
  };
364
326
  }, []);
365
327
  const storageKey = `${client.namespace}.messages`;
366
- useEffect2(() => {
328
+ useEffect(() => {
367
329
  let active = true;
368
330
  const restore = async () => {
369
331
  if (client.persist) {
@@ -385,6 +347,7 @@ function useFrankieChat() {
385
347
  setMessages(restoredMessages);
386
348
  }
387
349
  } catch {
350
+ if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
388
351
  } finally {
389
352
  if (active) setRestored(true);
390
353
  }
@@ -394,7 +357,7 @@ function useFrankieChat() {
394
357
  active = false;
395
358
  };
396
359
  }, [client, storageKey]);
397
- useEffect2(() => {
360
+ useEffect(() => {
398
361
  if (!client.persist || !restored || busy) return;
399
362
  try {
400
363
  if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
@@ -414,10 +377,11 @@ function useFrankieChat() {
414
377
  async (text, opts) => {
415
378
  const trimmed = text.trim();
416
379
  const attachments = opts?.attachments ?? [];
417
- if (!trimmed && attachments.length === 0 || busy) return;
380
+ if (!trimmed && attachments.length === 0 || abortRef.current || !restored) return;
418
381
  const outgoing = trimmed || "Please take a look at the attached file(s).";
419
382
  const assistantId = nextId("a");
420
383
  setError(void 0);
384
+ setLoadedSkills(null);
421
385
  setBusy(true);
422
386
  const next = [
423
387
  ...messagesRef.current,
@@ -444,6 +408,10 @@ function useFrankieChat() {
444
408
  signal: abort.signal
445
409
  },
446
410
  (event) => {
411
+ if (event.type === "skills_loaded") {
412
+ setLoadedSkills(event.skills);
413
+ return;
414
+ }
447
415
  if (event.type === "ping" || event.type === "session") return;
448
416
  if (event.type === "text_delta") receivedText = true;
449
417
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
@@ -482,7 +450,7 @@ function useFrankieChat() {
482
450
  abortRef.current = null;
483
451
  }
484
452
  },
485
- [busy, client, context, patchAssistant]
453
+ [busy, client, context, patchAssistant, restored]
486
454
  );
487
455
  const stop = useCallback(() => abortRef.current?.abort(), []);
488
456
  const reset = useCallback(() => {
@@ -493,12 +461,13 @@ function useFrankieChat() {
493
461
  setError(void 0);
494
462
  setBusy(false);
495
463
  setRestored(true);
464
+ setLoadedSkills(null);
496
465
  try {
497
466
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
498
467
  } catch {
499
468
  }
500
469
  }, [client]);
501
- return { messages, busy, ...error ? { error } : {}, send, stop, reset };
470
+ return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
502
471
  }
503
472
  function conversationTitle(messages) {
504
473
  const first = messages.find((message) => message.role === "user" && message.text.trim());
@@ -507,6 +476,68 @@ function conversationTitle(messages) {
507
476
  return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
508
477
  }
509
478
 
479
+ // src/hooks/FrankieProvider.tsx
480
+ import { jsx } from "react/jsx-runtime";
481
+ var FrankieContext = createContext2(null);
482
+ function FrankieProvider({
483
+ children,
484
+ context,
485
+ theme,
486
+ config: providedConfig,
487
+ ...clientOptions
488
+ }) {
489
+ const tokenCallback = useRef2(clientOptions.getUserToken);
490
+ tokenCallback.current = clientOptions.getUserToken;
491
+ const client = useMemo(
492
+ () => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
493
+ // eslint-disable-next-line react-hooks/exhaustive-deps
494
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
495
+ );
496
+ const [config, setConfig] = useState2(providedConfig);
497
+ const [configError, setConfigError] = useState2(void 0);
498
+ const [loading, setLoading] = useState2(!providedConfig);
499
+ useEffect2(() => {
500
+ if (providedConfig) return;
501
+ const abort = new AbortController();
502
+ setLoading(true);
503
+ client.getConfig(abort.signal).then((next) => {
504
+ setConfig(next);
505
+ setConfigError(void 0);
506
+ }).catch((error) => {
507
+ if (abort.signal.aborted) return;
508
+ setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
509
+ }).finally(() => {
510
+ if (!abort.signal.aborted) setLoading(false);
511
+ });
512
+ return () => abort.abort();
513
+ }, [client, providedConfig]);
514
+ const value = useMemo(
515
+ () => ({
516
+ client,
517
+ ...config ? { config } : {},
518
+ ...configError ? { configError } : {},
519
+ loading,
520
+ ...context ? { context } : {},
521
+ ...theme ? { theme } : {}
522
+ }),
523
+ [client, config, configError, loading, context, theme]
524
+ );
525
+ return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children: /* @__PURE__ */ jsx(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
526
+ }
527
+ function ConversationState({ children }) {
528
+ const chat = useFrankieChatState();
529
+ return /* @__PURE__ */ jsx(FrankieChatContext.Provider, { value: chat, children });
530
+ }
531
+ function useFrankie() {
532
+ const value = useContext2(FrankieContext);
533
+ if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
534
+ return value;
535
+ }
536
+ function useFrankieTheme() {
537
+ const { config, theme } = useFrankie();
538
+ return useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
539
+ }
540
+
510
541
  // src/components/chat/FrankieWidget.tsx
511
542
  import { MessageCircle, X as X4 } from "lucide-react";
512
543
  import { useEffect as useEffect5, useState as useState6 } from "react";
@@ -570,7 +601,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
570
601
 
571
602
  // src/components/chat/ChatPanel.tsx
572
603
  import { RotateCcw, X as X3 } from "lucide-react";
573
- import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
604
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
574
605
 
575
606
  // src/components/ui/avatar.tsx
576
607
  import { jsx as jsx3 } from "react/jsx-runtime";
@@ -628,16 +659,51 @@ var ScrollArea = forwardRef(
628
659
  }
629
660
  );
630
661
 
662
+ // src/components/chat/AgentContext.tsx
663
+ import { ArrowUpRight, Sparkles } from "lucide-react";
664
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
665
+ function AgentContext() {
666
+ const { loading, configError } = useFrankie();
667
+ const { loadedSkills, busy, restoring, error } = useFrankieChat();
668
+ const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
669
+ return /* @__PURE__ */ jsxs("div", { className: "frankie-agent-context-wrap", children: [
670
+ /* @__PURE__ */ jsxs("details", { className: "frankie-agent-context", children: [
671
+ /* @__PURE__ */ jsxs("summary", { "aria-label": "Agent context", children: [
672
+ /* @__PURE__ */ jsx6(Sparkles, { size: 17, "aria-hidden": "true" }),
673
+ /* @__PURE__ */ jsx6("span", { children: "Agent context" }),
674
+ /* @__PURE__ */ jsx6("span", { className: "frankie-context-count", children: loadedSkills === null ? "Not checked" : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"}` }),
675
+ /* @__PURE__ */ jsxs("span", { className: "frankie-context-status", "data-active": status === "Active", children: [
676
+ /* @__PURE__ */ jsx6("i", { "aria-hidden": "true" }),
677
+ status
678
+ ] }),
679
+ /* @__PURE__ */ jsx6(ArrowUpRight, { size: 15, "aria-hidden": "true" })
680
+ ] }),
681
+ /* @__PURE__ */ jsxs("div", { className: "frankie-context-detail", children: [
682
+ /* @__PURE__ */ jsx6("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" }),
683
+ loadedSkills?.length ? /* @__PURE__ */ jsx6("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ jsxs("li", { children: [
684
+ /* @__PURE__ */ jsx6("span", { children: skill.name }),
685
+ /* @__PURE__ */ jsxs("small", { children: [
686
+ "v",
687
+ skill.revision
688
+ ] })
689
+ ] }, skill.id)) }) : /* @__PURE__ */ jsx6("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
690
+ /* @__PURE__ */ jsx6("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
691
+ ] })
692
+ ] }),
693
+ /* @__PURE__ */ jsx6("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
694
+ ] });
695
+ }
696
+
631
697
  // src/components/chat/Composer.tsx
632
- import { ArrowUp, Paperclip, Square, X } from "lucide-react";
633
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
698
+ import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
699
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
634
700
 
635
701
  // src/components/ui/textarea.tsx
636
702
  import { forwardRef as forwardRef2 } from "react";
637
- import { jsx as jsx6 } from "react/jsx-runtime";
703
+ import { jsx as jsx7 } from "react/jsx-runtime";
638
704
  var Textarea = forwardRef2(
639
705
  function Textarea2({ className, ...props }, ref) {
640
- return /* @__PURE__ */ jsx6(
706
+ return /* @__PURE__ */ jsx7(
641
707
  "textarea",
642
708
  {
643
709
  ref,
@@ -652,14 +718,14 @@ var Textarea = forwardRef2(
652
718
  );
653
719
 
654
720
  // src/components/chat/Composer.tsx
655
- import { Fragment, jsx as jsx7, jsxs } from "react/jsx-runtime";
721
+ import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
656
722
  var MAX_ROWS_PX = 140;
657
723
  function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
658
724
  const [value, setValue] = useState3("");
659
725
  const [files, setFiles] = useState3([]);
660
726
  const [fileError, setFileError] = useState3(void 0);
661
- const textareaRef = useRef2(null);
662
- const fileInputRef = useRef2(null);
727
+ const textareaRef = useRef3(null);
728
+ const fileInputRef = useRef3(null);
663
729
  const canAttach = Boolean(attachments?.enabled);
664
730
  const models = modelPicker?.models ?? [];
665
731
  useEffect3(() => {
@@ -714,46 +780,46 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
714
780
  });
715
781
  }, [busy, disabled, files, modelPicker?.value, onSend, value]);
716
782
  const onKeyDown = (event) => {
717
- if (event.key === "Enter" && !event.shiftKey) {
783
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
718
784
  event.preventDefault();
719
785
  submit();
720
786
  }
721
787
  };
722
788
  const nothingToSend = value.trim().length === 0 && files.length === 0;
723
- return /* @__PURE__ */ jsxs(
789
+ return /* @__PURE__ */ jsxs2(
724
790
  "form",
725
791
  {
726
- className: "flex flex-col gap-2",
792
+ className: "frankie-composer flex flex-col gap-2",
727
793
  onSubmit: (event) => {
728
794
  event.preventDefault();
729
795
  submit();
730
796
  },
731
797
  children: [
732
- files.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ jsxs(
798
+ files.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ jsxs2(
733
799
  "span",
734
800
  {
735
801
  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",
736
802
  children: [
737
- /* @__PURE__ */ jsx7(Paperclip, { className: "size-3 shrink-0 opacity-70" }),
738
- /* @__PURE__ */ jsx7("span", { className: "truncate", children: file.name }),
739
- /* @__PURE__ */ jsx7(
803
+ /* @__PURE__ */ jsx8(Paperclip, { className: "size-3 shrink-0 opacity-70" }),
804
+ /* @__PURE__ */ jsx8("span", { className: "truncate", children: file.name }),
805
+ /* @__PURE__ */ jsx8(
740
806
  "button",
741
807
  {
742
808
  type: "button",
743
809
  onClick: () => removeFile(file.id),
744
810
  "aria-label": `Remove ${file.name}`,
745
811
  className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
746
- children: /* @__PURE__ */ jsx7(X, { className: "size-3" })
812
+ children: /* @__PURE__ */ jsx8(X, { className: "size-3" })
747
813
  }
748
814
  )
749
815
  ]
750
816
  },
751
817
  file.id
752
818
  )) }) : null,
753
- fileError ? /* @__PURE__ */ jsx7("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
754
- /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
755
- canAttach ? /* @__PURE__ */ jsxs(Fragment, { children: [
756
- /* @__PURE__ */ jsx7(
819
+ fileError ? /* @__PURE__ */ jsx8("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
820
+ /* @__PURE__ */ jsxs2("div", { className: "frankie-composer-controls flex items-end gap-2", children: [
821
+ canAttach ? /* @__PURE__ */ jsxs2(Fragment, { children: [
822
+ /* @__PURE__ */ jsx8(
757
823
  "input",
758
824
  {
759
825
  ref: fileInputRef,
@@ -767,7 +833,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
767
833
  }
768
834
  }
769
835
  ),
770
- /* @__PURE__ */ jsx7(
836
+ /* @__PURE__ */ jsx8(
771
837
  Button,
772
838
  {
773
839
  type: "button",
@@ -777,11 +843,11 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
777
843
  onClick: () => fileInputRef.current?.click(),
778
844
  "aria-label": "Attach files",
779
845
  title: "Attach files",
780
- children: /* @__PURE__ */ jsx7(Paperclip, {})
846
+ children: /* @__PURE__ */ jsx8(Plus, {})
781
847
  }
782
848
  )
783
849
  ] }) : null,
784
- /* @__PURE__ */ jsx7(
850
+ /* @__PURE__ */ jsx8(
785
851
  Textarea,
786
852
  {
787
853
  ref: textareaRef,
@@ -795,7 +861,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
795
861
  "aria-label": "Message"
796
862
  }
797
863
  ),
798
- models.length > 0 ? /* @__PURE__ */ jsx7(
864
+ models.length > 0 ? /* @__PURE__ */ jsx8(
799
865
  "select",
800
866
  {
801
867
  value: modelPicker?.value ?? models[0]?.id,
@@ -803,10 +869,10 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
803
869
  disabled,
804
870
  "aria-label": "Model",
805
871
  className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
806
- children: models.map((model) => /* @__PURE__ */ jsx7("option", { value: model.id, children: model.label }, model.id))
872
+ children: models.map((model) => /* @__PURE__ */ jsx8("option", { value: model.id, children: model.label }, model.id))
807
873
  }
808
874
  ) : null,
809
- busy ? /* @__PURE__ */ jsx7(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx7(Square, {}) }) : /* @__PURE__ */ jsx7(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx7(ArrowUp, {}) })
875
+ busy ? /* @__PURE__ */ jsx8(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx8(Square, {}) }) : /* @__PURE__ */ jsx8(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx8(ArrowUp, {}) })
810
876
  ] })
811
877
  ]
812
878
  }
@@ -836,9 +902,9 @@ import { useState as useState4 } from "react";
836
902
 
837
903
  // src/components/chat/Markdown.tsx
838
904
  import { Fragment as Fragment2 } from "react";
839
- import { jsx as jsx8 } from "react/jsx-runtime";
905
+ import { jsx as jsx9 } from "react/jsx-runtime";
840
906
  function Markdown({ text, className }) {
841
- return /* @__PURE__ */ jsx8("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
907
+ return /* @__PURE__ */ jsx9("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
842
908
  }
843
909
  function renderBlocks(text) {
844
910
  const lines = text.split("\n");
@@ -848,13 +914,13 @@ function renderBlocks(text) {
848
914
  let code = null;
849
915
  const flushParagraph = () => {
850
916
  if (paragraph.length === 0) return;
851
- blocks.push(/* @__PURE__ */ jsx8("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
917
+ blocks.push(/* @__PURE__ */ jsx9("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
852
918
  paragraph = [];
853
919
  };
854
920
  const flushList = () => {
855
921
  if (!list) return;
856
- const items = list.items.map((item, index) => /* @__PURE__ */ jsx8("li", { children: renderInline(item) }, index));
857
- blocks.push(list.ordered ? /* @__PURE__ */ jsx8("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx8("ul", { children: items }, `l${blocks.length}`));
922
+ const items = list.items.map((item, index) => /* @__PURE__ */ jsx9("li", { children: renderInline(item) }, index));
923
+ blocks.push(list.ordered ? /* @__PURE__ */ jsx9("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx9("ul", { children: items }, `l${blocks.length}`));
858
924
  list = null;
859
925
  };
860
926
  for (const line of lines) {
@@ -862,7 +928,7 @@ function renderBlocks(text) {
862
928
  if (fence) {
863
929
  if (code) {
864
930
  blocks.push(
865
- /* @__PURE__ */ jsx8("pre", { children: /* @__PURE__ */ jsx8("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
931
+ /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
866
932
  );
867
933
  code = null;
868
934
  } else {
@@ -897,7 +963,7 @@ function renderBlocks(text) {
897
963
  if (heading) {
898
964
  flushParagraph();
899
965
  flushList();
900
- blocks.push(/* @__PURE__ */ jsx8("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
966
+ blocks.push(/* @__PURE__ */ jsx9("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
901
967
  continue;
902
968
  }
903
969
  flushList();
@@ -905,7 +971,7 @@ function renderBlocks(text) {
905
971
  }
906
972
  if (code) {
907
973
  blocks.push(
908
- /* @__PURE__ */ jsx8("pre", { children: /* @__PURE__ */ jsx8("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
974
+ /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
909
975
  );
910
976
  }
911
977
  flushParagraph();
@@ -920,20 +986,20 @@ function renderInline(text) {
920
986
  const token = match[0];
921
987
  const start = match.index;
922
988
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
923
- nodes.push(/* @__PURE__ */ jsx8(Fragment2, { children: renderToken(token) }, start));
989
+ nodes.push(/* @__PURE__ */ jsx9(Fragment2, { children: renderToken(token) }, start));
924
990
  lastIndex = start + token.length;
925
991
  }
926
992
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
927
993
  return nodes;
928
994
  }
929
995
  function renderToken(token) {
930
- if (token.startsWith("**")) return /* @__PURE__ */ jsx8("strong", { children: token.slice(2, -2) });
931
- if (token.startsWith("`")) return /* @__PURE__ */ jsx8("code", { children: token.slice(1, -1) });
996
+ if (token.startsWith("**")) return /* @__PURE__ */ jsx9("strong", { children: token.slice(2, -2) });
997
+ if (token.startsWith("`")) return /* @__PURE__ */ jsx9("code", { children: token.slice(1, -1) });
932
998
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
933
999
  const href = link ? link[2] ?? "" : token;
934
1000
  const label = link ? link[1] ?? "" : token;
935
1001
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
936
- return /* @__PURE__ */ jsx8("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1002
+ return /* @__PURE__ */ jsx9("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
937
1003
  }
938
1004
 
939
1005
  // src/components/chat/ToolActivityList.tsx
@@ -941,7 +1007,7 @@ import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react"
941
1007
 
942
1008
  // src/components/ui/badge.tsx
943
1009
  import { cva as cva2 } from "class-variance-authority";
944
- import { jsx as jsx9 } from "react/jsx-runtime";
1010
+ import { jsx as jsx10 } from "react/jsx-runtime";
945
1011
  var badgeVariants = cva2(
946
1012
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
947
1013
  {
@@ -961,44 +1027,44 @@ function Badge({
961
1027
  variant,
962
1028
  ...props
963
1029
  }) {
964
- return /* @__PURE__ */ jsx9("span", { className: cn(badgeVariants({ variant }), className), ...props });
1030
+ return /* @__PURE__ */ jsx10("span", { className: cn(badgeVariants({ variant }), className), ...props });
965
1031
  }
966
1032
 
967
1033
  // src/components/chat/ToolActivityList.tsx
968
- import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
1034
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
969
1035
  function ToolActivityList({ tools, renderTool }) {
970
1036
  if (tools.length === 0) return null;
971
1037
  const running = tools.some((tool) => tool.status === "running");
972
- return /* @__PURE__ */ jsxs2("details", { className: "frankie-tool-activity mt-2", open: running, children: [
973
- /* @__PURE__ */ jsxs2("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: [
974
- /* @__PURE__ */ jsx10(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
975
- /* @__PURE__ */ jsx10("span", { children: running ? "Working" : "Activity" }),
976
- /* @__PURE__ */ jsxs2("span", { className: "text-[11px]", children: [
1038
+ return /* @__PURE__ */ jsxs3("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1039
+ /* @__PURE__ */ jsxs3("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1040
+ /* @__PURE__ */ jsx11(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1041
+ /* @__PURE__ */ jsx11("span", { children: running ? "Working" : "Activity" }),
1042
+ /* @__PURE__ */ jsxs3("span", { className: "text-[11px]", children: [
977
1043
  "\xB7 ",
978
1044
  tools.length,
979
1045
  " tool call",
980
1046
  tools.length === 1 ? "" : "s"
981
1047
  ] })
982
1048
  ] }),
983
- /* @__PURE__ */ jsx10("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx10("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx10(ToolActivityRow, { tool }) }, tool.id)) })
1049
+ /* @__PURE__ */ jsx11("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx11("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx11(ToolActivityRow, { tool }) }, tool.id)) })
984
1050
  ] });
985
1051
  }
986
1052
  function ToolActivityRow({ tool }) {
987
- return /* @__PURE__ */ jsxs2(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
988
- /* @__PURE__ */ jsx10(ToolIcon, { status: tool.status }),
989
- /* @__PURE__ */ jsx10("span", { className: "truncate", children: tool.label })
1053
+ return /* @__PURE__ */ jsxs3(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1054
+ /* @__PURE__ */ jsx11(ToolIcon, { status: tool.status }),
1055
+ /* @__PURE__ */ jsx11("span", { className: "truncate", children: tool.label })
990
1056
  ] });
991
1057
  }
992
1058
  function ToolIcon({ status }) {
993
1059
  switch (status) {
994
1060
  case "running":
995
- return /* @__PURE__ */ jsx10(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1061
+ return /* @__PURE__ */ jsx11(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
996
1062
  case "done":
997
- return /* @__PURE__ */ jsx10(Check, { className: "size-3", "aria-hidden": "true" });
1063
+ return /* @__PURE__ */ jsx11(Check, { className: "size-3", "aria-hidden": "true" });
998
1064
  case "denied":
999
- return /* @__PURE__ */ jsx10(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1065
+ return /* @__PURE__ */ jsx11(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1000
1066
  default:
1001
- return /* @__PURE__ */ jsx10(X2, { className: "size-3", "aria-hidden": "true" });
1067
+ return /* @__PURE__ */ jsx11(X2, { className: "size-3", "aria-hidden": "true" });
1002
1068
  }
1003
1069
  }
1004
1070
  function badgeVariant(status) {
@@ -1009,7 +1075,7 @@ function badgeVariant(status) {
1009
1075
  }
1010
1076
 
1011
1077
  // src/components/chat/MessageBubble.tsx
1012
- import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
1078
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1013
1079
  function MessageBubble({
1014
1080
  message,
1015
1081
  assistantName,
@@ -1018,33 +1084,33 @@ function MessageBubble({
1018
1084
  renderTool
1019
1085
  }) {
1020
1086
  if (message.role === "user") {
1021
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-end gap-1", children: [
1022
- message.attachments?.length ? /* @__PURE__ */ jsx11("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs3(
1087
+ return /* @__PURE__ */ jsxs4("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1088
+ message.attachments?.length ? /* @__PURE__ */ jsx12("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs4(
1023
1089
  "span",
1024
1090
  {
1025
1091
  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",
1026
1092
  children: [
1027
- /* @__PURE__ */ jsx11(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1028
- /* @__PURE__ */ jsx11("span", { className: "truncate", children: file.name })
1093
+ /* @__PURE__ */ jsx12(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1094
+ /* @__PURE__ */ jsx12("span", { className: "truncate", children: file.name })
1029
1095
  ]
1030
1096
  },
1031
1097
  `${file.name}-${index}`
1032
1098
  )) }) : null,
1033
- message.text ? /* @__PURE__ */ jsxs3("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1034
- /* @__PURE__ */ jsx11("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1035
- /* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text })
1099
+ message.text ? /* @__PURE__ */ jsxs4("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1100
+ /* @__PURE__ */ jsx12("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1101
+ /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text })
1036
1102
  ] }) : null
1037
1103
  ] });
1038
1104
  }
1039
1105
  const empty = message.text.length === 0 && message.tools.length === 0;
1040
- return /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
1041
- /* @__PURE__ */ jsxs3("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1042
- /* @__PURE__ */ jsx11(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1043
- message.text ? /* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text }) : null
1106
+ return /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-message flex gap-2", children: [
1107
+ /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1108
+ /* @__PURE__ */ jsx12(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1109
+ message.text ? /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text }) : null
1044
1110
  ] }),
1045
- /* @__PURE__ */ jsxs3("div", { className: "min-w-0 max-w-[85%]", children: [
1046
- message.status === "error" ? /* @__PURE__ */ jsx11("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__ */ jsx11(TypingDots, {}) : /* @__PURE__ */ jsx11(Markdown, { text: message.text, className: "text-sm" }),
1047
- showToolActivity ? /* @__PURE__ */ jsx11(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1111
+ /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1112
+ message.status === "error" ? /* @__PURE__ */ jsx12("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ jsx12(TypingDots, {}) : /* @__PURE__ */ jsx12(Markdown, { text: message.text, className: "text-sm" }),
1113
+ showToolActivity ? /* @__PURE__ */ jsx12(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1048
1114
  ] })
1049
1115
  ] });
1050
1116
  }
@@ -1058,28 +1124,31 @@ function MessageCopyButton({ text }) {
1058
1124
  } catch {
1059
1125
  }
1060
1126
  };
1061
- return /* @__PURE__ */ jsx11("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__ */ jsx11(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1127
+ return /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ jsx12(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1062
1128
  }
1063
1129
  function TypingDots() {
1064
- return /* @__PURE__ */ jsx11("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ jsx11(
1065
- "span",
1066
- {
1067
- className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
1068
- style: { animationDelay: `${index * 150}ms` }
1069
- },
1070
- index
1071
- )) });
1130
+ return /* @__PURE__ */ jsxs4("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1131
+ /* @__PURE__ */ jsx12("span", { className: "frankie-thinking-label", children: "Thinking" }),
1132
+ [0, 1, 2].map((index) => /* @__PURE__ */ jsx12(
1133
+ "span",
1134
+ {
1135
+ className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
1136
+ style: { animationDelay: `${index * 150}ms` }
1137
+ },
1138
+ index
1139
+ ))
1140
+ ] });
1072
1141
  }
1073
1142
 
1074
1143
  // src/components/chat/SuggestedPrompts.tsx
1075
- import { jsx as jsx12 } from "react/jsx-runtime";
1144
+ import { jsx as jsx13 } from "react/jsx-runtime";
1076
1145
  function SuggestedPrompts({
1077
1146
  prompts,
1078
1147
  onPick,
1079
1148
  disabled
1080
1149
  }) {
1081
1150
  if (prompts.length === 0) return null;
1082
- return /* @__PURE__ */ jsx12("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx12(
1151
+ return /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx13(
1083
1152
  Button,
1084
1153
  {
1085
1154
  type: "button",
@@ -1094,7 +1163,7 @@ function SuggestedPrompts({
1094
1163
  }
1095
1164
 
1096
1165
  // src/components/chat/ChatPanel.tsx
1097
- import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
1166
+ import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1098
1167
  function ChatPanel({
1099
1168
  className,
1100
1169
  header,
@@ -1109,8 +1178,8 @@ function ChatPanel({
1109
1178
  }) {
1110
1179
  const { config, configError, loading } = useFrankie();
1111
1180
  const theme = useFrankieTheme();
1112
- const { messages, busy, error, send, stop, reset } = useFrankieChat();
1113
- const scrollRef = useRef3(null);
1181
+ const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
1182
+ const scrollRef = useRef4(null);
1114
1183
  const features = config?.features;
1115
1184
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1116
1185
  const [model, setModel] = useState5(void 0);
@@ -1122,11 +1191,12 @@ function ChatPanel({
1122
1191
  }, [messages]);
1123
1192
  const name = config?.branding.name ?? "Assistant";
1124
1193
  const mode = resolveMode(theme?.mode);
1125
- return /* @__PURE__ */ jsxs4(
1194
+ return /* @__PURE__ */ jsxs5(
1126
1195
  Card,
1127
1196
  {
1128
1197
  className: cn(
1129
- "frankie-root",
1198
+ "frankie-root frankie-chat-panel",
1199
+ fill && "frankie-chat-panel-fill",
1130
1200
  fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
1131
1201
  mode === "dark" && "dark",
1132
1202
  className
@@ -1134,24 +1204,25 @@ function ChatPanel({
1134
1204
  style: themeToCssVars(theme),
1135
1205
  "data-frankie-mode": mode,
1136
1206
  children: [
1137
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs4(CardHeader, { children: [
1138
- /* @__PURE__ */ jsx13(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1139
- /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
1140
- /* @__PURE__ */ jsx13(CardTitle, { children: name }),
1141
- config?.branding.tagline ? /* @__PURE__ */ jsx13(CardDescription, { children: config.branding.tagline }) : null
1207
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs5(CardHeader, { children: [
1208
+ /* @__PURE__ */ jsx14(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1209
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
1210
+ /* @__PURE__ */ jsx14(CardTitle, { children: name }),
1211
+ config?.branding.tagline ? /* @__PURE__ */ jsx14(CardDescription, { children: config.branding.tagline }) : null
1142
1212
  ] }),
1143
- /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx13(RotateCcw, {}) }),
1144
- onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(X3, {}) }) : null
1213
+ /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx14(RotateCcw, {}) }),
1214
+ onClose ? /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx14(X3, {}) }) : null
1145
1215
  ] }),
1146
- /* @__PURE__ */ jsx13(CardContent, { children: /* @__PURE__ */ jsx13(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs4("div", { className: "frankie-config-error", role: "alert", children: [
1147
- /* @__PURE__ */ jsx13("strong", { children: "Frankie couldn\u2019t start" }),
1148
- /* @__PURE__ */ jsx13("p", { children: configError }),
1149
- /* @__PURE__ */ jsx13("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1150
- ] }) : loading ? /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
1151
- /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1152
- /* @__PURE__ */ jsx13(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1153
- ] }) : /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-4", children: messages.map(
1154
- (message) => renderMessage ? /* @__PURE__ */ jsx13("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx13(
1216
+ /* @__PURE__ */ jsx14(AgentContext, {}),
1217
+ /* @__PURE__ */ jsx14(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx14(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs5("div", { className: "frankie-config-error", role: "alert", children: [
1218
+ /* @__PURE__ */ jsx14("strong", { children: "Frankie couldn\u2019t start" }),
1219
+ /* @__PURE__ */ jsx14("p", { children: configError }),
1220
+ /* @__PURE__ */ jsx14("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1221
+ ] }) : loading ? /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
1222
+ /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1223
+ /* @__PURE__ */ jsx14(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1224
+ ] }) : /* @__PURE__ */ jsx14("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1225
+ (message) => renderMessage ? /* @__PURE__ */ jsx14("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx14(
1155
1226
  MessageBubble,
1156
1227
  {
1157
1228
  message,
@@ -1163,15 +1234,15 @@ function ChatPanel({
1163
1234
  message.id
1164
1235
  )
1165
1236
  ) }) }) }),
1166
- /* @__PURE__ */ jsxs4(CardFooter, { className: "flex flex-col gap-2", children: [
1167
- error ? /* @__PURE__ */ jsx13("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1168
- /* @__PURE__ */ jsx13("div", { className: "w-full", children: /* @__PURE__ */ jsx13(
1237
+ /* @__PURE__ */ jsxs5(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1238
+ error ? /* @__PURE__ */ jsx14("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1239
+ /* @__PURE__ */ jsx14("div", { className: "w-full", children: /* @__PURE__ */ jsx14(
1169
1240
  Composer,
1170
1241
  {
1171
1242
  onSend: send,
1172
1243
  onStop: stop,
1173
1244
  busy,
1174
- disabled: Boolean(configError),
1245
+ disabled: loading || restoring || Boolean(configError),
1175
1246
  ...placeholder ? { placeholder } : {},
1176
1247
  ...autoFocus ? { autoFocus } : {},
1177
1248
  ...features?.attachments.enabled ? { attachments: features.attachments } : {},
@@ -1186,11 +1257,12 @@ function ChatPanel({
1186
1257
  }
1187
1258
 
1188
1259
  // src/components/chat/FrankieWidget.tsx
1189
- import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1260
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1190
1261
  function FrankieWidget({
1191
1262
  position = "bottom-right",
1192
1263
  offset = 24,
1193
1264
  defaultOpen = false,
1265
+ zIndex = 20,
1194
1266
  open: controlledOpen,
1195
1267
  onOpenChange,
1196
1268
  renderLauncher,
@@ -1215,15 +1287,15 @@ function FrankieWidget({
1215
1287
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1216
1288
  const mode = resolveMode(theme?.mode);
1217
1289
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1218
- return /* @__PURE__ */ jsx14(
1290
+ return /* @__PURE__ */ jsx15(
1219
1291
  "div",
1220
1292
  {
1221
1293
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1222
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex: 2147483e3 },
1294
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1223
1295
  "data-frankie-mode": mode,
1224
- children: /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1225
- open ? /* @__PURE__ */ jsx14(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
1226
- renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx14(
1296
+ children: /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1297
+ open ? /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx15(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1298
+ /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx15(
1227
1299
  Button,
1228
1300
  {
1229
1301
  size: "icon",
@@ -1232,125 +1304,174 @@ function FrankieWidget({
1232
1304
  "aria-expanded": open,
1233
1305
  "aria-label": open ? "Close the assistant" : label,
1234
1306
  title: label,
1235
- children: open ? /* @__PURE__ */ jsx14(X4, { className: "size-5" }) : /* @__PURE__ */ jsx14(MessageCircle, { className: "size-5" })
1307
+ children: open ? /* @__PURE__ */ jsx15(X4, { className: "size-5" }) : /* @__PURE__ */ jsx15(MessageCircle, { className: "size-5" })
1236
1308
  }
1237
- )
1309
+ ) })
1238
1310
  ] })
1239
1311
  }
1240
1312
  );
1241
1313
  }
1242
1314
 
1243
1315
  // src/components/chat/FrankieInline.tsx
1244
- import { jsx as jsx15 } from "react/jsx-runtime";
1316
+ import { jsx as jsx16 } from "react/jsx-runtime";
1245
1317
  function FrankieInline({ className, style, ...panelProps }) {
1246
1318
  const theme = useFrankieTheme();
1247
1319
  const mode = resolveMode(theme?.mode);
1248
- return /* @__PURE__ */ jsx15(
1320
+ return /* @__PURE__ */ jsx16(
1249
1321
  "div",
1250
1322
  {
1251
1323
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1252
1324
  style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1253
1325
  "data-frankie-mode": mode,
1254
- children: /* @__PURE__ */ jsx15(ChatPanel, { ...panelProps, fill: true })
1326
+ children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, fill: true })
1255
1327
  }
1256
1328
  );
1257
1329
  }
1258
1330
 
1259
1331
  // src/components/chat/FrankieChatPage.tsx
1260
- import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
1332
+ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1333
+ import { History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
1334
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
1261
1335
  function PageEmptyState() {
1262
1336
  const { config } = useFrankie();
1263
- const { send, busy } = useFrankieChat();
1337
+ const { send, busy, restoring } = useFrankieChat();
1264
1338
  const name = config?.branding.name ?? "Frankie";
1265
- const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
1266
- return /* @__PURE__ */ jsxs6("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1267
- /* @__PURE__ */ jsx16("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1268
- /* @__PURE__ */ jsxs6("div", { children: [
1269
- /* @__PURE__ */ jsxs6("p", { className: "frankie-page-kicker", children: [
1270
- name,
1271
- " assistant"
1272
- ] }),
1273
- /* @__PURE__ */ jsx16("h1", { children: "What would you like to do?" }),
1274
- /* @__PURE__ */ jsx16("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1339
+ return /* @__PURE__ */ jsxs7("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1340
+ /* @__PURE__ */ jsx17("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1341
+ /* @__PURE__ */ jsxs7("div", { children: [
1342
+ /* @__PURE__ */ jsx17("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1343
+ /* @__PURE__ */ jsx17("h1", { children: "How can I help?" }),
1344
+ /* @__PURE__ */ jsx17("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1275
1345
  ] }),
1276
- prompts.length ? /* @__PURE__ */ jsx16("div", { className: "frankie-page-prompts", children: prompts.map((prompt, index) => /* @__PURE__ */ jsxs6("button", { type: "button", disabled: busy, onClick: () => send(prompt), children: [
1277
- /* @__PURE__ */ jsx16("span", { children: String(index + 1).padStart(2, "0") }),
1278
- /* @__PURE__ */ jsx16("strong", { children: prompt }),
1279
- /* @__PURE__ */ jsx16("b", { "aria-hidden": true, children: "\u2197" })
1280
- ] }, prompt)) }) : null
1346
+ /* @__PURE__ */ jsx17("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1347
+ /* @__PURE__ */ jsx17("strong", { children: prompt }),
1348
+ /* @__PURE__ */ jsx17("b", { "aria-hidden": true, children: "\u2197" })
1349
+ ] }, prompt)) })
1281
1350
  ] });
1282
1351
  }
1283
- function PageChrome({
1284
- header,
1285
- footer,
1286
- emptyState,
1287
- renderMessage,
1288
- renderTool,
1289
- placeholder,
1290
- autoFocus
1291
- }) {
1292
- const { config } = useFrankie();
1293
- const { reset } = useFrankieChat();
1294
- const name = config?.branding.name ?? "Frankie";
1295
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1296
- /* @__PURE__ */ jsxs6("header", { className: "frankie-page-header", children: [
1297
- /* @__PURE__ */ jsxs6("div", { children: [
1298
- /* @__PURE__ */ jsx16("span", { className: "frankie-page-status", "aria-hidden": true }),
1299
- /* @__PURE__ */ jsx16("strong", { children: name })
1352
+ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
1353
+ const { client, config } = useFrankie();
1354
+ const { busy, messages } = useFrankieChat();
1355
+ const theme = useFrankieTheme();
1356
+ const [conversations, setConversations] = useState7([]);
1357
+ const [historyError, setHistoryError] = useState7("");
1358
+ const [historyLoading, setHistoryLoading] = useState7(false);
1359
+ const [drawerOpen, setDrawerOpen] = useState7(false);
1360
+ const [query, setQuery] = useState7("");
1361
+ const dialog = useRef5(null);
1362
+ const menu = useRef5(null);
1363
+ const requestVersion = useRef5(0);
1364
+ const refresh = useCallback3(async () => {
1365
+ if (!showHistory || !config || config.allowAnonymous) {
1366
+ setHistoryError("");
1367
+ return;
1368
+ }
1369
+ const version = ++requestVersion.current;
1370
+ setHistoryLoading(true);
1371
+ setHistoryError("");
1372
+ try {
1373
+ const items = await client.listConversations();
1374
+ if (version === requestVersion.current) setConversations(items);
1375
+ } catch {
1376
+ if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
1377
+ } finally {
1378
+ if (version === requestVersion.current) setHistoryLoading(false);
1379
+ }
1380
+ }, [client, showHistory, config?.allowAnonymous]);
1381
+ useEffect6(() => {
1382
+ if (!busy) void refresh();
1383
+ return () => {
1384
+ requestVersion.current++;
1385
+ };
1386
+ }, [refresh, busy]);
1387
+ useEffect6(() => {
1388
+ const node = dialog.current;
1389
+ if (drawerOpen) {
1390
+ node?.showModal?.();
1391
+ } else {
1392
+ node?.close?.();
1393
+ }
1394
+ }, [drawerOpen]);
1395
+ const close = () => {
1396
+ setDrawerOpen(false);
1397
+ menu.current?.focus();
1398
+ };
1399
+ const select = (id) => {
1400
+ if (!busy) {
1401
+ onSelect(id);
1402
+ close();
1403
+ }
1404
+ };
1405
+ const history = /* @__PURE__ */ jsxs7(Fragment3, { children: [
1406
+ /* @__PURE__ */ jsxs7("div", { className: "frankie-history-heading", children: [
1407
+ /* @__PURE__ */ jsx17("strong", { children: "Conversations" }),
1408
+ /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx17(X5, { size: 18 }) })
1409
+ ] }),
1410
+ /* @__PURE__ */ jsxs7("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1411
+ onNew();
1412
+ close();
1413
+ }, children: [
1414
+ /* @__PURE__ */ jsx17(Plus2, { size: 17 }),
1415
+ "New chat"
1416
+ ] }),
1417
+ /* @__PURE__ */ jsxs7("label", { className: "frankie-history-search", children: [
1418
+ /* @__PURE__ */ jsx17("span", { children: "Search conversations" }),
1419
+ /* @__PURE__ */ jsx17("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1420
+ ] }),
1421
+ /* @__PURE__ */ jsx17("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs7("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1422
+ /* @__PURE__ */ jsx17("span", { children: item.title }),
1423
+ /* @__PURE__ */ jsx17("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1424
+ ] }, item.id)) }),
1425
+ historyLoading ? /* @__PURE__ */ jsx17("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs7("p", { role: "status", children: [
1426
+ historyError,
1427
+ /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1428
+ ] }) : !conversations.length ? /* @__PURE__ */ jsx17("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1429
+ navigation ? /* @__PURE__ */ jsx17("div", { className: "frankie-host-navigation", children: navigation }) : null
1430
+ ] });
1431
+ return /* @__PURE__ */ jsxs7("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1432
+ showHistory ? /* @__PURE__ */ jsx17("aside", { className: "frankie-history-desktop", children: history }) : null,
1433
+ /* @__PURE__ */ jsxs7("div", { className: "frankie-page-main", children: [
1434
+ /* @__PURE__ */ jsxs7("header", { className: "frankie-page-header", children: [
1435
+ /* @__PURE__ */ jsxs7("div", { children: [
1436
+ showHistory ? /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx17(Menu, { size: 20 }) }) : null,
1437
+ /* @__PURE__ */ jsx17("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1438
+ ] }),
1439
+ /* @__PURE__ */ jsxs7("div", { children: [
1440
+ /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1441
+ /* @__PURE__ */ jsx17(Plus2, { size: 16 }),
1442
+ /* @__PURE__ */ jsx17("span", { children: "New chat" })
1443
+ ] }),
1444
+ /* @__PURE__ */ jsxs7("details", { className: "frankie-chat-settings", children: [
1445
+ /* @__PURE__ */ jsx17("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx17(Settings, { size: 19 }) }),
1446
+ /* @__PURE__ */ jsxs7("div", { children: [
1447
+ /* @__PURE__ */ jsx17("strong", { children: "Chat settings" }),
1448
+ /* @__PURE__ */ jsx17("p", { children: "History syncs automatically for signed-in users." }),
1449
+ /* @__PURE__ */ jsxs7("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1450
+ /* @__PURE__ */ jsx17(History, { size: 16 }),
1451
+ "Refresh history"
1452
+ ] }),
1453
+ /* @__PURE__ */ jsx17("small", { children: "Skills are checked at the start of each message." })
1454
+ ] })
1455
+ ] })
1456
+ ] })
1300
1457
  ] }),
1301
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: reset, "aria-label": "Start a new conversation", children: "New chat" })
1458
+ /* @__PURE__ */ jsx17("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx17(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx17(PageEmptyState, {}) }) })
1302
1459
  ] }),
1303
- /* @__PURE__ */ jsx16("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx16(
1304
- FrankieInline,
1305
- {
1306
- header: header ?? false,
1307
- footer,
1308
- emptyState: emptyState ?? /* @__PURE__ */ jsx16(PageEmptyState, {}),
1309
- renderMessage,
1310
- renderTool,
1311
- placeholder,
1312
- autoFocus
1313
- }
1314
- ) })
1460
+ showHistory ? /* @__PURE__ */ jsx17("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1461
+ if (event.target === event.currentTarget) close();
1462
+ }, children: /* @__PURE__ */ jsx17("aside", { children: history }) }) : null
1315
1463
  ] });
1316
1464
  }
1317
- function FrankieChatPage({
1318
- className,
1319
- style,
1320
- header,
1321
- footer,
1322
- emptyState,
1323
- renderMessage,
1324
- renderTool,
1325
- placeholder,
1326
- autoFocus,
1327
- ...providerProps
1328
- }) {
1329
- return /* @__PURE__ */ jsx16(
1330
- "main",
1331
- {
1332
- className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
1333
- style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
1334
- children: /* @__PURE__ */ jsx16(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ jsx16(
1335
- PageChrome,
1336
- {
1337
- header,
1338
- footer,
1339
- emptyState,
1340
- renderMessage,
1341
- renderTool,
1342
- placeholder,
1343
- autoFocus
1344
- }
1345
- ) })
1346
- }
1347
- );
1465
+ function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1466
+ const [id, setId] = useState7(() => provider.conversationId ?? crypto.randomUUID());
1467
+ const onNew = () => setId(crypto.randomUUID());
1468
+ return /* @__PURE__ */ jsx17("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ jsx17(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx17(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1348
1469
  }
1349
1470
 
1350
1471
  // src/components/ui/separator.tsx
1351
- import { jsx as jsx17 } from "react/jsx-runtime";
1472
+ import { jsx as jsx18 } from "react/jsx-runtime";
1352
1473
  function Separator({ className }) {
1353
- return /* @__PURE__ */ jsx17("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1474
+ return /* @__PURE__ */ jsx18("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1354
1475
  }
1355
1476
  export {
1356
1477
  Avatar,