@frankieai/react 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankieai/react",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Embeddable, themeable chat widget for the Frankie onboarding agent — shadcn/ui components, works with or without Tailwind in the host app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
4
- import { History, Menu, Plus, Settings, X } from "lucide-react";
4
+ import { ChevronsLeft, ChevronsRight, History, Menu, Plus, Settings, X } from "lucide-react";
5
5
  import type { FrankieProviderProps } from "../../hooks/FrankieProvider.jsx";
6
6
  import { FrankieProvider, useFrankie, useFrankieTheme } from "../../hooks/FrankieProvider.jsx";
7
7
  import { useFrankieChat } from "../../hooks/useFrankieChat.js";
8
8
  import { themeToCssVars, resolveMode } from "../../lib/theme.js";
9
- import type { ChatConversation } from "../../lib/client.js";
9
+ import { FrankieApiError, type ChatConversation } from "../../lib/client.js";
10
10
  import { FrankieInline } from "./FrankieInline.js";
11
11
  import type { ChatPanelProps } from "./ChatPanel.js";
12
12
 
@@ -39,6 +39,10 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
39
39
  const [historyError, setHistoryError] = useState("");
40
40
  const [historyLoading, setHistoryLoading] = useState(false);
41
41
  const [drawerOpen, setDrawerOpen] = useState(false);
42
+ const [collapsed, setCollapsed] = useState(() => {
43
+ try { return window.localStorage.getItem("frankie.history-collapsed") === "1"; }
44
+ catch { return false; }
45
+ });
42
46
  const [query, setQuery] = useState("");
43
47
  const dialog = useRef<HTMLDialogElement>(null);
44
48
  const menu = useRef<HTMLButtonElement>(null);
@@ -48,10 +52,23 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
48
52
  const version = ++requestVersion.current;
49
53
  setHistoryLoading(true); setHistoryError("");
50
54
  try { const items = await client.listConversations(); if (version === requestVersion.current) setConversations(items); }
51
- catch { if (version === requestVersion.current) setHistoryError("History couldn’t load. Check your sign-in and try again."); }
55
+ catch (caught) {
56
+ if (version !== requestVersion.current) { /* superseded by a newer request */ }
57
+ else {
58
+ const message =
59
+ caught instanceof FrankieApiError ? caught.message
60
+ : caught instanceof Error && caught.message.startsWith("Frankie") ? caught.message
61
+ : "History couldn’t load. Check your sign-in and try again.";
62
+ setHistoryError(message);
63
+ }
64
+ }
52
65
  finally { if (version === requestVersion.current) setHistoryLoading(false); }
53
66
  }, [client, showHistory, config?.allowAnonymous]);
54
67
  useEffect(() => { if (!busy) void refresh(); return () => { requestVersion.current++; }; }, [refresh, busy]);
68
+ useEffect(() => {
69
+ try { window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0"); }
70
+ catch { /* storage unavailable */ }
71
+ }, [collapsed]);
55
72
  useEffect(() => {
56
73
  const node = dialog.current;
57
74
  if (drawerOpen) { node?.showModal?.(); }
@@ -68,7 +85,10 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
68
85
  {navigation ? <div className="frankie-host-navigation">{navigation}</div> : null}
69
86
  </>;
70
87
  return <div className={"frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : "")} style={themeToCssVars(theme)}>
71
- {showHistory ? <aside className="frankie-history-desktop">{history}</aside> : null}
88
+ {showHistory ? <aside className={"frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : "")}>
89
+ <button type="button" className="frankie-history-toggle" aria-label={collapsed ? "Expand conversations" : "Collapse conversations"} aria-expanded={!collapsed} onClick={() => setCollapsed(value => !value)}>{collapsed ? <ChevronsRight size={16} /> : <ChevronsLeft size={16} />}</button>
90
+ {collapsed ? null : history}
91
+ </aside> : null}
72
92
  <div className="frankie-page-main">
73
93
  <header className="frankie-page-header"><div>{showHistory ? <button type="button" className="frankie-mobile-only" ref={menu} aria-label="Open conversations" aria-haspopup="dialog" aria-expanded={drawerOpen} onClick={() => setDrawerOpen(true)}><Menu size={20} /></button> : null}<strong className="frankie-thread-title">{conversations.find(item => item.id === currentId)?.title ?? messages.find(message => message.role === "user")?.text ?? "New chat"}</strong></div><div>
74
94
  <button type="button" disabled={busy} onClick={onNew} aria-label="Start a new conversation"><Plus size={16} /><span>New chat</span></button>
@@ -91,8 +91,16 @@ export function useFrankieChatState(): UseFrankieChatResult {
91
91
  messagesRef.current = restoredMessages;
92
92
  setMessages(restoredMessages);
93
93
  }
94
- } catch {
95
- if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
94
+ } catch (caught) {
95
+ if (active) {
96
+ const message =
97
+ caught instanceof FrankieApiError
98
+ ? caught.message
99
+ : caught instanceof Error && caught.message.startsWith("Frankie")
100
+ ? caught.message
101
+ : "Could not restore this conversation. Check your sign-in and connection, then try opening it again.";
102
+ setError(message);
103
+ }
96
104
  } finally {
97
105
  if (active) setRestored(true);
98
106
  }
@@ -47,7 +47,11 @@
47
47
  @layer components {
48
48
  .frankie-page-shell { display:flex; flex:1; min-height:0; height:100%; background:var(--frankie-background); color:var(--frankie-foreground); }
49
49
  .frankie-page-main { display:flex; flex:1; min-width:0; min-height:0; flex-direction:column; }
50
- .frankie-history-desktop { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:16px; padding:20px 14px; border-right:1px solid var(--frankie-border); background:var(--frankie-secondary); overflow:auto; }
50
+ .frankie-history-desktop { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:16px; padding:20px 14px; border-right:1px solid var(--frankie-border); background:var(--frankie-secondary); overflow:auto; transition:width .16s ease, padding .16s ease; }
51
+ .frankie-history-toggle { display:flex; align-items:center; justify-content:center; flex-shrink:0; width:32px; height:32px; border:1px solid var(--frankie-border); border-radius:8px; background:var(--frankie-background); color:var(--frankie-muted-foreground); cursor:pointer; align-self:flex-end; }
52
+ .frankie-history-toggle:hover { color:var(--frankie-foreground); }
53
+ .frankie-history-collapsed { width:56px; padding:20px 8px; overflow:visible; }
54
+ .frankie-history-collapsed .frankie-history-toggle { align-self:center; }
51
55
  .frankie-history-heading { display:flex; align-items:center; justify-content:space-between; font-size:14px; }
52
56
  .frankie-new-chat { display:flex; align-items:center; justify-content:center; gap:8px; border:1px solid var(--frankie-border); border-radius:10px; background:var(--frankie-background); min-height:44px; font-weight:600; cursor:pointer; }
53
57
  .frankie-history-search > span { display:block; font-size:12px; color:var(--frankie-muted-foreground); margin-bottom:6px; }
@@ -381,6 +385,7 @@
381
385
  .frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Send"],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Stop"] { margin-left:auto; width:42px; height:42px; border-radius:50%; background:var(--frankie-primary); color:var(--frankie-primary-foreground); border:0; }
382
386
  .frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Attach files"] { width:42px; height:42px; border-radius:50%; }
383
387
  .frankie-root .frankie-history-desktop { width:290px; background:var(--frankie-background); padding:20px 12px; }
388
+ .frankie-root .frankie-history-collapsed { width:56px; padding:20px 8px; }
384
389
  .frankie-root .frankie-new-chat { justify-content:flex-start; border:0; background:transparent; padding:8px 12px; font-size:15px; }
385
390
  .frankie-root .frankie-history-heading { font-size:12px; font-weight:400; color:var(--frankie-muted-foreground); padding:0 12px; }
386
391
  .frankie-root .frankie-history-list button { min-height:44px; padding:10px 12px; }
@@ -1,8 +1,9 @@
1
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type { FrankieServerEvent, PublicWidgetConfig } from "@frankieai/core";
4
4
  import { FrankieProvider } from "./hooks/FrankieProvider.jsx";
5
5
  import { ChatPanel } from "./components/chat/ChatPanel.js";
6
+ import { FrankieChatPage } from "./components/chat/FrankieChatPage.js";
6
7
  import { Markdown } from "./components/chat/Markdown.js";
7
8
  import { FrankieClient, readEventStream } from "./lib/client.js";
8
9
  import { applyEvent, newAssistantMessage } from "./lib/messages.js";
@@ -282,3 +283,35 @@ describe("ChatPanel", () => {
282
283
  expect(root.classList.contains("dark")).toBe(true);
283
284
  });
284
285
  });
286
+
287
+ describe("FrankieChatPage", () => {
288
+ it("collapses and expands the desktop conversations sidebar, remembering the choice", async () => {
289
+ cleanup();
290
+ window.localStorage.clear();
291
+ const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).endsWith("/v1/session") ? Response.json({ sessionId: "page", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } }) : new Response(sseStream([{ type: "turn_end", stopReason: "end_turn" }])));
292
+ const { container } = render(<FrankieChatPage publishableKey="pk_page_fixture" config={config} fetchImpl={fetchImpl} persist={false} />);
293
+ const aside = () => container.querySelector(".frankie-history-desktop") as HTMLElement;
294
+ await waitFor(() => expect(within(aside()).getByText("Conversations")).toBeTruthy());
295
+ fireEvent.click(within(aside()).getByRole("button", { name: "Collapse conversations" }));
296
+ expect(within(aside()).queryByText("Conversations")).toBeNull();
297
+ expect(aside().classList.contains("frankie-history-collapsed")).toBe(true);
298
+ expect(window.localStorage.getItem("frankie.history-collapsed")).toBe("1");
299
+ fireEvent.click(within(aside()).getByRole("button", { name: "Expand conversations" }));
300
+ expect(within(aside()).getByText("Conversations")).toBeTruthy();
301
+ expect(aside().classList.contains("frankie-history-collapsed")).toBe(false);
302
+ expect(window.localStorage.getItem("frankie.history-collapsed")).toBe("0");
303
+ cleanup();
304
+ });
305
+
306
+ it("starts collapsed when a prior visit left the sidebar collapsed", async () => {
307
+ cleanup();
308
+ window.localStorage.setItem("frankie.history-collapsed", "1");
309
+ const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).endsWith("/v1/session") ? Response.json({ sessionId: "page-2", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } }) : new Response(sseStream([{ type: "turn_end", stopReason: "end_turn" }])));
310
+ const { container } = render(<FrankieChatPage publishableKey="pk_page_fixture_2" config={config} fetchImpl={fetchImpl} persist={false} />);
311
+ const aside = () => container.querySelector(".frankie-history-desktop") as HTMLElement;
312
+ await waitFor(() => expect(within(aside()).getByRole("button", { name: "Expand conversations" })).toBeTruthy());
313
+ expect(within(aside()).queryByText("Conversations")).toBeNull();
314
+ window.localStorage.clear();
315
+ cleanup();
316
+ });
317
+ });