@frankieai/react 0.1.4 → 0.2.0

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.
@@ -45,6 +45,27 @@
45
45
  }
46
46
 
47
47
  @layer components {
48
+ .frankie-page-shell { display:flex; flex:1; min-height:0; height:100%; background:var(--frankie-background); color:var(--frankie-foreground); }
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; }
51
+ .frankie-history-heading { display:flex; align-items:center; justify-content:space-between; font-size:14px; }
52
+ .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
+ .frankie-history-search > span { display:block; font-size:12px; color:var(--frankie-muted-foreground); margin-bottom:6px; }
54
+ .frankie-history-search input { width:100%; min-height:40px; padding:8px 10px; border:1px solid var(--frankie-border); border-radius:8px; background:var(--frankie-background); }
55
+ .frankie-history-list { display:flex; flex-direction:column; gap:4px; flex:1; }
56
+ .frankie-history-list button { display:flex; flex-direction:column; align-items:flex-start; gap:5px; border:1px solid transparent; border-radius:8px; padding:10px; min-height:54px; background:transparent; text-align:left; cursor:pointer; }
57
+ .frankie-history-list button span { width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; }
58
+ .frankie-history-list small { font-size:11px; color:var(--frankie-muted-foreground); }.frankie-history-list button:hover,.frankie-history-list button[aria-current] { background:var(--frankie-background); border-color:var(--frankie-border); }
59
+ .frankie-history-desktop > p,.frankie-history-dialog p { font-size:13px; color:var(--frankie-muted-foreground); }
60
+ .frankie-header-caption { color:var(--frankie-muted-foreground); font-size:12px; }.frankie-page-header button { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-width:40px; min-height:40px; cursor:pointer; }
61
+ .frankie-chat-settings { position:relative; }.frankie-chat-settings summary { list-style:none; display:grid; place-items:center; width:40px; height:40px; cursor:pointer; border-radius:8px; }.frankie-chat-settings summary::-webkit-details-marker { display:none; }
62
+ .frankie-chat-settings > div { position:absolute; top:46px; right:0; z-index:5; width:min(280px,80vw); display:grid; gap:12px; padding:16px; border:1px solid var(--frankie-border); border-radius:12px; background:var(--frankie-background); box-shadow:0 10px 30px #0002; font-size:13px; }.frankie-chat-settings p { margin:0; color:var(--frankie-muted-foreground); }
63
+ .frankie-loaded-skills { width:100%; max-width:760px; font-size:12px; color:var(--frankie-muted-foreground); }.frankie-loaded-skills summary { cursor:pointer; padding:6px 0; }.frankie-loaded-skills ul { list-style:none; padding:4px 0; margin:0; }.frankie-loaded-skills li { display:flex; gap:8px; padding:4px 0; }.frankie-loaded-skills small { opacity:.7; }
64
+ .frankie-history-dialog { padding:0; border:0; margin:0; max-height:none; height:100dvh; max-width:none; width:min(320px,86vw); background:var(--frankie-secondary); color:var(--frankie-foreground); }.frankie-history-dialog::backdrop { background:#0008; }.frankie-history-dialog > aside { display:flex; flex-direction:column; gap:16px; height:100%; padding:calc(20px + env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom)); overflow:auto; }
65
+ .frankie-mobile-only { display:none!important; }.frankie-widget-panel { max-height:calc(100dvh - 100px); }
66
+ .frankie-root button:focus-visible,.frankie-root summary:focus-visible,.frankie-root input:focus-visible { outline:2px solid var(--frankie-primary); outline-offset:3px; }
67
+ .frankie-root button:disabled { opacity:.5; cursor:not-allowed; }
68
+ @media(max-width:760px) { .frankie-history-desktop { display:none; }.frankie-mobile-only { display:inline-flex!important; }.frankie-header-caption { display:none; }.frankie-page-header { padding:6px 10px; }.frankie-history-heading button { border:0; background:transparent; min-width:44px; min-height:44px; align-items:center; justify-content:center; }.frankie-page-prompts { gap:8px; }.frankie-widget-panel { width:calc(100vw - 32px); } }
48
69
  .frankie-root {
49
70
  --frankie-background: #ffffff;
50
71
  --frankie-foreground: #0a0a0a;
@@ -164,3 +185,164 @@
164
185
  padding: 0;
165
186
  }
166
187
  }
188
+
189
+
190
+ @layer components {
191
+ .frankie-chat-page {
192
+ display: flex;
193
+ min-width: 0;
194
+ flex-direction: column;
195
+ overflow: hidden;
196
+ background:
197
+ radial-gradient(60rem 24rem at 50% -12rem, color-mix(in srgb, var(--frankie-primary) 8%, transparent), transparent 65%),
198
+ var(--frankie-background);
199
+ color: var(--frankie-foreground);
200
+ }
201
+
202
+ .frankie-page-header {
203
+ display: flex;
204
+ min-height: 56px;
205
+ flex: 0 0 56px;
206
+ align-items: center;
207
+ justify-content: space-between;
208
+ gap: 16px;
209
+ padding: 0 24px;
210
+ border-bottom: 1px solid var(--frankie-border);
211
+ background: color-mix(in srgb, var(--frankie-background) 92%, transparent);
212
+ backdrop-filter: blur(18px);
213
+ }
214
+
215
+ .frankie-page-header > div {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ }
220
+
221
+ .frankie-page-header strong { font-size: 0.925rem; font-weight: 650; }
222
+
223
+ .frankie-page-status {
224
+ width: 10px;
225
+ height: 10px;
226
+ border-radius: 999px;
227
+ background: var(--frankie-primary);
228
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--frankie-primary) 16%, transparent);
229
+ }
230
+
231
+ .frankie-page-header button {
232
+ min-height: 36px;
233
+ padding: 0 13px;
234
+ border: 1px solid var(--frankie-border);
235
+ border-radius: calc(var(--frankie-radius) * .9);
236
+ background: var(--frankie-secondary);
237
+ cursor: pointer;
238
+ }
239
+
240
+ .frankie-page-header button:hover { border-color: var(--frankie-primary); }
241
+
242
+ .frankie-page-canvas {
243
+ display: flex;
244
+ min-width: 0;
245
+ min-height: 0;
246
+ flex: 1;
247
+ }
248
+
249
+ .frankie-page-canvas > .frankie-inline {
250
+ width: 100%;
251
+ max-width: 56rem;
252
+ margin: 0 auto;
253
+ }
254
+
255
+ .frankie-page-canvas .frankie-scroll { padding: 2rem clamp(1rem, 4vw, 4rem) 1.25rem; }
256
+ .frankie-page-canvas .frankie-root > .border-t {
257
+ padding: .75rem clamp(1rem, 4vw, 4rem) 1.5rem;
258
+ border-top: 0;
259
+ background: linear-gradient(180deg, transparent, var(--frankie-background) 30%);
260
+ }
261
+
262
+ .frankie-page-empty {
263
+ display: flex;
264
+ width: min(100%, 38rem);
265
+ min-height: 100%;
266
+ margin: auto;
267
+ padding: clamp(2rem, 7vh, 4.5rem) 1rem 2rem;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ justify-content: center;
271
+ gap: 1rem;
272
+ text-align: center;
273
+ }
274
+
275
+ .frankie-page-avatar {
276
+ display: grid;
277
+ width: 48px;
278
+ height: 48px;
279
+ place-items: center;
280
+ border-radius: calc(var(--frankie-radius) * 1.4);
281
+ background: var(--frankie-primary);
282
+ color: var(--frankie-primary-foreground);
283
+ font-weight: 750;
284
+ }
285
+
286
+ .frankie-page-kicker {
287
+ margin: 0 0 .4rem;
288
+ color: var(--frankie-primary);
289
+ font-size: .7rem;
290
+ font-weight: 750;
291
+ letter-spacing: .1em;
292
+ text-transform: uppercase;
293
+ }
294
+
295
+ .frankie-page-empty h1 {
296
+ margin: 0;
297
+ font-size: clamp(1.65rem, 4vw, 2.15rem);
298
+ line-height: 1.08;
299
+ letter-spacing: -.045em;
300
+ }
301
+
302
+ .frankie-page-empty h1 + p {
303
+ margin: .6rem auto 0;
304
+ max-width: 30rem;
305
+ color: var(--frankie-muted-foreground);
306
+ }
307
+
308
+ .frankie-page-prompts {
309
+ display: grid;
310
+ width: 100%;
311
+ gap: .55rem;
312
+ margin-top: .4rem;
313
+ }
314
+
315
+ .frankie-page-prompts button {
316
+ display: grid;
317
+ min-height: 60px;
318
+ grid-template-columns: minmax(0, 1fr) auto;
319
+ align-items: center;
320
+ gap: .65rem;
321
+ padding: .75rem .9rem;
322
+ border: 1px solid var(--frankie-border);
323
+ border-radius: calc(var(--frankie-radius) * 1.25);
324
+ background: var(--frankie-secondary);
325
+ text-align: left;
326
+ cursor: pointer;
327
+ }
328
+
329
+ .frankie-page-prompts button:hover {
330
+ border-color: color-mix(in srgb, var(--frankie-primary) 58%, var(--frankie-border));
331
+ transform: translateY(-1px);
332
+ }
333
+
334
+ .frankie-page-prompts button > span {
335
+ color: var(--frankie-muted-foreground);
336
+ font-size: .68rem;
337
+ font-weight: 700;
338
+ }
339
+
340
+ .frankie-page-prompts button > strong { font-size: .875rem; }
341
+ .frankie-page-prompts button > b { color: var(--frankie-primary); font-size: 1rem; }
342
+
343
+ @media (max-width: 640px) {
344
+ .frankie-page-header { min-height: 52px; flex-basis: 52px; padding: 0 12px; }
345
+ .frankie-page-canvas .frankie-scroll { padding: 1.25rem .75rem .5rem; }
346
+ .frankie-page-canvas .frankie-root > .border-t { padding: .65rem .75rem calc(.65rem + env(safe-area-inset-bottom)); }
347
+ }
348
+ }
@@ -1,4 +1,4 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen, waitFor } 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";
@@ -7,6 +7,7 @@ import { Markdown } from "./components/chat/Markdown.js";
7
7
  import { FrankieClient, readEventStream } from "./lib/client.js";
8
8
  import { applyEvent, newAssistantMessage } from "./lib/messages.js";
9
9
  import { themeToCssVars } from "./lib/theme.js";
10
+ import { useFrankieChat } from "./hooks/useFrankieChat.js";
10
11
 
11
12
  const config: PublicWidgetConfig = {
12
13
  tenantId: "acme",
@@ -97,6 +98,24 @@ describe("readEventStream", () => {
97
98
  });
98
99
 
99
100
  describe("FrankieClient", () => {
101
+ it("does not request signed-in history for an anonymous conversation", async () => {
102
+ const fetchImpl = vi.fn();
103
+ const client = new FrankieClient({ publishableKey: "pk_anonymous_fixture", conversationId: "new", fetchImpl, persist: false });
104
+ expect(await client.resumeConversation()).toBeUndefined();
105
+ expect(fetchImpl).not.toHaveBeenCalled();
106
+ });
107
+ it("keeps authenticated tokens out of storage and rejects a cached session after sign-out", async () => {
108
+ let token: string | undefined = "fixture-user-token";
109
+ const fetchImpl = vi.fn(async () => Response.json({ sessionId: "one", sessionToken: "fixture-bearer", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: false } }));
110
+ const client = new FrankieClient({ publishableKey: "pk_auth_fixture", getUserToken: () => token, fetchImpl });
111
+ await client.ensureSession();
112
+ expect(client.persist).toBe(false);
113
+ expect(window.localStorage.getItem("frankie.pk_auth_fixture.session")).toBeNull();
114
+ token = undefined;
115
+ await expect(client.ensureSession()).rejects.toThrow("sign in again");
116
+ expect(client.sessionId).toBeUndefined();
117
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
118
+ });
100
119
  // The client persists its session to localStorage; isolate tests that reuse
101
120
  // the same publishable-key namespace.
102
121
  beforeEach(() => {
@@ -192,6 +211,21 @@ describe("themeToCssVars", () => {
192
211
  });
193
212
 
194
213
  describe("ChatPanel", () => {
214
+ it("shares prompt controls, transcript and loaded skills across provider consumers", async () => {
215
+ cleanup();
216
+ const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).endsWith("/v1/session") ? Response.json({ sessionId: "shared", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } }) : new Response(sseStream([{ type: "skills_loaded", skills: [{ id: "review", name: "Review items", revision: 2 }] }, { type: "text_delta", text: "Shared response" }, { type: "turn_end", stopReason: "end_turn" }])));
217
+ function Prompt() { const chat = useFrankieChat(); return <button disabled={chat.restoring} onClick={() => void chat.send("Shared prompt")}>External prompt</button>; }
218
+ render(<FrankieProvider publishableKey="pk_shared_fixture" config={config} fetchImpl={fetchImpl} persist={false}><Prompt /><ChatPanel /></FrankieProvider>);
219
+ await waitFor(() => expect((screen.getByRole("button", { name: "External prompt" }) as HTMLButtonElement).disabled).toBe(false));
220
+ fireEvent.click(screen.getByRole("button", { name: "External prompt" }));
221
+ expect(await screen.findByText("Shared response")).toBeTruthy();
222
+ expect(screen.getByText("1 skill loaded")).toBeTruthy();
223
+ expect(fetchImpl.mock.calls.filter(([url]) => String(url).endsWith("/v1/chat"))).toHaveLength(1);
224
+ fireEvent.click(screen.getByRole("button", { name: "Start a new conversation" }));
225
+ await waitFor(() => expect(screen.queryByText("Shared response")).toBeNull());
226
+ expect(screen.queryByText("1 skill loaded")).toBeNull();
227
+ cleanup();
228
+ });
195
229
  it("shows the greeting and suggested prompts before the first message", async () => {
196
230
  render(
197
231
  <FrankieProvider baseUrl="https://agent.acme.com" publishableKey="pk_1" config={config}>