@frankieai/react 0.1.1 → 0.1.5

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
@@ -28,13 +28,16 @@ var FrankieClient = class {
28
28
  get sessionId() {
29
29
  return this.session?.sessionId;
30
30
  }
31
+ get conversationId() {
32
+ return this.options.conversationId;
33
+ }
31
34
  /** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
32
35
  get persist() {
33
36
  return this.options.persist !== false && typeof window !== "undefined";
34
37
  }
35
- /** localStorage namespace for this tenant, shared with the chat hook. */
38
+ /** localStorage namespace for this tenant/conversation, shared with the chat hook. */
36
39
  get namespace() {
37
- return `frankie.${this.options.publishableKey}`;
40
+ return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
38
41
  }
39
42
  readStored() {
40
43
  if (!this.persist) return void 0;
@@ -62,30 +65,80 @@ var FrankieClient = class {
62
65
  });
63
66
  return await this.unwrap(response);
64
67
  }
65
- /** Opens (or reuses) a chat session. */
66
68
  async ensureSession(context) {
67
- if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) {
68
- return this.session;
69
- }
69
+ if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
70
70
  const userToken = await this.options.getUserToken?.();
71
+ if (this.options.getUserToken && !userToken) {
72
+ throw new FrankieApiError(
73
+ "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
74
+ 401,
75
+ "user_token_unavailable"
76
+ );
77
+ }
71
78
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
72
79
  method: "POST",
73
80
  headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
74
- body: JSON.stringify({ ...userToken ? { userToken } : {}, ...context ? { context } : {} })
81
+ body: JSON.stringify({
82
+ ...userToken ? { userToken } : {},
83
+ ...context ? { context } : {},
84
+ ...this.options.conversationId ? { conversationId: this.options.conversationId } : {}
85
+ })
75
86
  });
76
87
  this.session = await this.unwrap(response);
77
88
  this.writeStored();
78
89
  return this.session;
79
90
  }
80
- /** Forget the current session so the next send starts a fresh conversation. */
91
+ async listConversations() {
92
+ const userToken = await this.requireUserToken();
93
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
94
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
95
+ });
96
+ const body = await this.unwrap(response);
97
+ return Array.isArray(body.conversations) ? body.conversations : [];
98
+ }
99
+ async resumeConversation(id = this.options.conversationId) {
100
+ if (!id) return void 0;
101
+ const userToken = await this.requireUserToken();
102
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
103
+ method: "POST",
104
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
105
+ });
106
+ if (response.status === 404) return void 0;
107
+ const body = await this.unwrap(response);
108
+ this.session = body.session;
109
+ this.writeStored();
110
+ return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
111
+ }
112
+ async syncConversation(messages, title) {
113
+ if (!this.session || !this.options.conversationId) return;
114
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
115
+ method: "PUT",
116
+ headers: {
117
+ "content-type": "application/json",
118
+ "x-frankie-key": this.options.publishableKey,
119
+ authorization: `Bearer ${this.session.sessionToken}`
120
+ },
121
+ body: JSON.stringify({ messages, ...title ? { title } : {} })
122
+ });
123
+ await this.unwrap(response);
124
+ }
125
+ async deleteConversation(id) {
126
+ const userToken = await this.requireUserToken();
127
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
128
+ method: "DELETE",
129
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
130
+ });
131
+ if (response.status !== 404) await this.unwrap(response);
132
+ }
133
+ async requireUserToken() {
134
+ const token = await this.options.getUserToken?.();
135
+ if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
136
+ return token;
137
+ }
81
138
  resetSession() {
82
139
  this.session = void 0;
83
140
  this.writeStored();
84
141
  }
85
- /**
86
- * Sends a message and invokes `onEvent` for each server event as it arrives.
87
- * Resolves when the turn ends.
88
- */
89
142
  async send(input, onEvent) {
90
143
  const session = await this.ensureSession(input.context);
91
144
  const body = {
@@ -171,9 +224,8 @@ function FrankieProvider({
171
224
  }) {
172
225
  const client = useMemo(
173
226
  () => new FrankieClient(clientOptions),
174
- // The client holds session state; rebuild it only when the endpoint changes.
175
227
  // eslint-disable-next-line react-hooks/exhaustive-deps
176
- [clientOptions.baseUrl, clientOptions.publishableKey]
228
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
177
229
  );
178
230
  const [config, setConfig] = useState(providedConfig);
179
231
  const [configError, setConfigError] = useState(void 0);
@@ -299,8 +351,10 @@ function useFrankieChat() {
299
351
  const [messages, setMessages] = useState2([]);
300
352
  const [busy, setBusy] = useState2(false);
301
353
  const [error, setError] = useState2(void 0);
354
+ const [restored, setRestored] = useState2(false);
302
355
  const abortRef = useRef(null);
303
356
  const mounted = useRef(true);
357
+ const messagesRef = useRef([]);
304
358
  useEffect2(() => {
305
359
  mounted.current = true;
306
360
  return () => {
@@ -310,28 +364,51 @@ function useFrankieChat() {
310
364
  }, []);
311
365
  const storageKey = `${client.namespace}.messages`;
312
366
  useEffect2(() => {
313
- if (!client.persist) return;
314
- try {
315
- const raw = window.localStorage.getItem(storageKey);
316
- const stored = raw ? JSON.parse(raw) : [];
317
- if (Array.isArray(stored) && stored.length) setMessages(stored);
318
- } catch {
319
- }
320
- }, [storageKey]);
367
+ let active = true;
368
+ const restore = async () => {
369
+ if (client.persist) {
370
+ try {
371
+ const raw = window.localStorage.getItem(storageKey);
372
+ const stored = raw ? JSON.parse(raw) : [];
373
+ if (Array.isArray(stored) && stored.length && active) {
374
+ messagesRef.current = stored;
375
+ setMessages(stored);
376
+ }
377
+ } catch {
378
+ }
379
+ }
380
+ try {
381
+ const remote = await client.resumeConversation();
382
+ if (active && remote?.messages.length) {
383
+ const restoredMessages = remote.messages;
384
+ messagesRef.current = restoredMessages;
385
+ setMessages(restoredMessages);
386
+ }
387
+ } catch {
388
+ } finally {
389
+ if (active) setRestored(true);
390
+ }
391
+ };
392
+ void restore();
393
+ return () => {
394
+ active = false;
395
+ };
396
+ }, [client, storageKey]);
321
397
  useEffect2(() => {
322
- if (!client.persist || busy) return;
398
+ if (!client.persist || !restored || busy) return;
323
399
  try {
324
400
  if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
325
401
  else window.localStorage.removeItem(storageKey);
326
402
  } catch {
327
403
  }
328
- }, [messages, busy, client, storageKey]);
404
+ if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
405
+ }, [messages, busy, client, storageKey, restored]);
329
406
  const patchAssistant = useCallback((id, update) => {
330
- setMessages(
331
- (current) => current.map(
332
- (message) => message.id === id && message.role === "assistant" ? update(message) : message
333
- )
407
+ const next = messagesRef.current.map(
408
+ (message) => message.id === id && message.role === "assistant" ? update(message) : message
334
409
  );
410
+ messagesRef.current = next;
411
+ setMessages(next);
335
412
  }, []);
336
413
  const send = useCallback(
337
414
  async (text, opts) => {
@@ -342,18 +419,22 @@ function useFrankieChat() {
342
419
  const assistantId = nextId("a");
343
420
  setError(void 0);
344
421
  setBusy(true);
345
- setMessages((current) => [
346
- ...current,
422
+ const next = [
423
+ ...messagesRef.current,
347
424
  newUserMessage(
348
425
  nextId("u"),
349
426
  trimmed,
350
427
  attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
351
428
  ),
352
429
  newAssistantMessage(assistantId)
353
- ]);
430
+ ];
431
+ messagesRef.current = next;
432
+ setMessages(next);
354
433
  const abort = new AbortController();
355
434
  abortRef.current = abort;
356
435
  try {
436
+ let receivedText = false;
437
+ let receivedToolActivity = false;
357
438
  await client.send(
358
439
  {
359
440
  message: outgoing,
@@ -364,9 +445,18 @@ function useFrankieChat() {
364
445
  },
365
446
  (event) => {
366
447
  if (event.type === "ping" || event.type === "session") return;
448
+ if (event.type === "text_delta") receivedText = true;
449
+ if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
367
450
  patchAssistant(assistantId, (message) => applyEvent(message, event));
368
451
  }
369
452
  );
453
+ if (!receivedText) {
454
+ patchAssistant(assistantId, (message) => ({
455
+ ...message,
456
+ status: "done",
457
+ text: receivedToolActivity ? "Completed. Open Agent activity for the details." : "Frankie didn\u2019t return an answer. Please try again."
458
+ }));
459
+ }
370
460
  } catch (caught) {
371
461
  if (abort.signal.aborted) {
372
462
  patchAssistant(assistantId, (message) => ({
@@ -375,18 +465,20 @@ function useFrankieChat() {
375
465
  text: message.text || "Stopped."
376
466
  }));
377
467
  } else {
378
- const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
468
+ const message = caught instanceof FrankieApiError ? caught.message : caught instanceof Error && caught.message.startsWith("Frankie") ? caught.message : "Could not reach the assistant. Check your connection and try again.";
379
469
  if (mounted.current) setError(message);
380
470
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
381
471
  }
382
472
  } finally {
383
- if (mounted.current) {
384
- setBusy(false);
385
- patchAssistant(
386
- assistantId,
387
- (message) => message.status === "streaming" ? { ...message, status: "done" } : message
388
- );
473
+ patchAssistant(
474
+ assistantId,
475
+ (message) => message.status === "streaming" ? { ...message, status: "done" } : message
476
+ );
477
+ const transcript = messagesRef.current;
478
+ if (transcript.length) {
479
+ await client.syncConversation(transcript, conversationTitle(transcript)).catch(() => void 0);
389
480
  }
481
+ if (mounted.current) setBusy(false);
390
482
  abortRef.current = null;
391
483
  }
392
484
  },
@@ -396,9 +488,11 @@ function useFrankieChat() {
396
488
  const reset = useCallback(() => {
397
489
  abortRef.current?.abort();
398
490
  client.resetSession();
491
+ messagesRef.current = [];
399
492
  setMessages([]);
400
493
  setError(void 0);
401
494
  setBusy(false);
495
+ setRestored(true);
402
496
  try {
403
497
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
404
498
  } catch {
@@ -406,10 +500,16 @@ function useFrankieChat() {
406
500
  }, [client]);
407
501
  return { messages, busy, ...error ? { error } : {}, send, stop, reset };
408
502
  }
503
+ function conversationTitle(messages) {
504
+ const first = messages.find((message) => message.role === "user" && message.text.trim());
505
+ if (!first) return void 0;
506
+ const title = first.text.trim().replace(/\s+/g, " ");
507
+ return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
508
+ }
409
509
 
410
510
  // src/components/chat/FrankieWidget.tsx
411
511
  import { MessageCircle, X as X4 } from "lucide-react";
412
- import { useEffect as useEffect5, useState as useState5 } from "react";
512
+ import { useEffect as useEffect5, useState as useState6 } from "react";
413
513
 
414
514
  // src/lib/theme.ts
415
515
  function themeToCssVars(theme) {
@@ -470,7 +570,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
470
570
 
471
571
  // src/components/chat/ChatPanel.tsx
472
572
  import { RotateCcw, X as X3 } from "lucide-react";
473
- import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
573
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
474
574
 
475
575
  // src/components/ui/avatar.tsx
476
576
  import { jsx as jsx3 } from "react/jsx-runtime";
@@ -731,7 +831,8 @@ function readAsBase64(file) {
731
831
  }
732
832
 
733
833
  // src/components/chat/MessageBubble.tsx
734
- import { Paperclip as Paperclip2 } from "lucide-react";
834
+ import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
835
+ import { useState as useState4 } from "react";
735
836
 
736
837
  // src/components/chat/Markdown.tsx
737
838
  import { Fragment as Fragment2 } from "react";
@@ -836,7 +937,7 @@ function renderToken(token) {
836
937
  }
837
938
 
838
939
  // src/components/chat/ToolActivityList.tsx
839
- import { Check, Loader2, ShieldAlert, X as X2 } from "lucide-react";
940
+ import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react";
840
941
 
841
942
  // src/components/ui/badge.tsx
842
943
  import { cva as cva2 } from "class-variance-authority";
@@ -867,7 +968,20 @@ function Badge({
867
968
  import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
868
969
  function ToolActivityList({ tools, renderTool }) {
869
970
  if (tools.length === 0) return null;
870
- return /* @__PURE__ */ jsx10("ul", { className: "mt-2 flex flex-col gap-1", children: tools.map((tool) => /* @__PURE__ */ jsx10("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx10(ToolActivityRow, { tool }) }, tool.id)) });
971
+ 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: [
977
+ "\xB7 ",
978
+ tools.length,
979
+ " tool call",
980
+ tools.length === 1 ? "" : "s"
981
+ ] })
982
+ ] }),
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)) })
984
+ ] });
871
985
  }
872
986
  function ToolActivityRow({ tool }) {
873
987
  return /* @__PURE__ */ jsxs2(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
@@ -916,18 +1030,36 @@ function MessageBubble({
916
1030
  },
917
1031
  `${file.name}-${index}`
918
1032
  )) }) : null,
919
- message.text ? /* @__PURE__ */ jsx11("div", { className: "max-w-[85%] rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }) : 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 })
1036
+ ] }) : null
920
1037
  ] });
921
1038
  }
922
1039
  const empty = message.text.length === 0 && message.tools.length === 0;
923
1040
  return /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
924
- /* @__PURE__ */ jsx11(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName, className: "mt-0.5" }),
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
1044
+ ] }),
925
1045
  /* @__PURE__ */ jsxs3("div", { className: "min-w-0 max-w-[85%]", children: [
926
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" }),
927
1047
  showToolActivity ? /* @__PURE__ */ jsx11(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
928
1048
  ] })
929
1049
  ] });
930
1050
  }
1051
+ function MessageCopyButton({ text }) {
1052
+ const [copied, setCopied] = useState4(false);
1053
+ const copy = async () => {
1054
+ try {
1055
+ await navigator.clipboard.writeText(text);
1056
+ setCopied(true);
1057
+ window.setTimeout(() => setCopied(false), 1500);
1058
+ } catch {
1059
+ }
1060
+ };
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" }) });
1062
+ }
931
1063
  function TypingDots() {
932
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(
933
1065
  "span",
@@ -981,7 +1113,7 @@ function ChatPanel({
981
1113
  const scrollRef = useRef3(null);
982
1114
  const features = config?.features;
983
1115
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
984
- const [model, setModel] = useState4(void 0);
1116
+ const [model, setModel] = useState5(void 0);
985
1117
  useEffect4(() => {
986
1118
  const node = scrollRef.current;
987
1119
  if (!node) return;
@@ -1011,7 +1143,11 @@ function ChatPanel({
1011
1143
  /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx13(RotateCcw, {}) }),
1012
1144
  onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(X3, {}) }) : null
1013
1145
  ] }),
1014
- /* @__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__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: configError }) : loading ? /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: "Loading\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
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: [
1015
1151
  /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1016
1152
  /* @__PURE__ */ jsx13(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1017
1153
  ] }) : /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-4", children: messages.map(
@@ -1062,7 +1198,7 @@ function FrankieWidget({
1062
1198
  }) {
1063
1199
  const { config } = useFrankie();
1064
1200
  const theme = useFrankieTheme();
1065
- const [uncontrolledOpen, setUncontrolledOpen] = useState5(defaultOpen);
1201
+ const [uncontrolledOpen, setUncontrolledOpen] = useState6(defaultOpen);
1066
1202
  const open = controlledOpen ?? uncontrolledOpen;
1067
1203
  const setOpen = (next) => {
1068
1204
  if (controlledOpen === void 0) setUncontrolledOpen(next);
@@ -1120,10 +1256,101 @@ function FrankieInline({ className, style, ...panelProps }) {
1120
1256
  );
1121
1257
  }
1122
1258
 
1259
+ // src/components/chat/FrankieChatPage.tsx
1260
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
1261
+ function PageEmptyState() {
1262
+ const { config } = useFrankie();
1263
+ const { send, busy } = useFrankieChat();
1264
+ 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." })
1275
+ ] }),
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
1281
+ ] });
1282
+ }
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 })
1300
+ ] }),
1301
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: reset, "aria-label": "Start a new conversation", children: "New chat" })
1302
+ ] }),
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
+ ) })
1315
+ ] });
1316
+ }
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
+ );
1348
+ }
1349
+
1123
1350
  // src/components/ui/separator.tsx
1124
- import { jsx as jsx16 } from "react/jsx-runtime";
1351
+ import { jsx as jsx17 } from "react/jsx-runtime";
1125
1352
  function Separator({ className }) {
1126
- return /* @__PURE__ */ jsx16("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1353
+ return /* @__PURE__ */ jsx17("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1127
1354
  }
1128
1355
  export {
1129
1356
  Avatar,
@@ -1139,6 +1366,7 @@ export {
1139
1366
  Composer,
1140
1367
  FRANKIE_CLOUD_URL,
1141
1368
  FrankieApiError,
1369
+ FrankieChatPage,
1142
1370
  FrankieClient,
1143
1371
  FrankieInline,
1144
1372
  FrankieProvider,