@frankieai/react 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  Separator: () => Separator,
47
47
  SuggestedPrompts: () => SuggestedPrompts,
48
48
  Textarea: () => Textarea,
49
+ Thinking: () => Thinking,
49
50
  ToolActivityList: () => ToolActivityList,
50
51
  ToolActivityRow: () => ToolActivityRow,
51
52
  TypingDots: () => TypingDots,
@@ -67,6 +68,31 @@ module.exports = __toCommonJS(index_exports);
67
68
  // src/hooks/FrankieProvider.tsx
68
69
  var import_react2 = require("react");
69
70
 
71
+ // src/lib/identity.ts
72
+ function sessionIdentity(token) {
73
+ if (!token) return void 0;
74
+ return jwtSubject(token) ?? token;
75
+ }
76
+ function jwtSubject(token) {
77
+ const parts = token.split(".");
78
+ if (parts.length !== 3 || !parts[1]) return void 0;
79
+ try {
80
+ const payload = JSON.parse(base64UrlDecode(parts[1]));
81
+ return typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ function base64UrlDecode(segment) {
87
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
88
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
89
+ if (typeof atob === "function") {
90
+ const bytes = atob(padded);
91
+ return decodeURIComponent(Array.from(bytes, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join(""));
92
+ }
93
+ return Buffer.from(padded, "base64").toString("utf8");
94
+ }
95
+
70
96
  // src/lib/client.ts
71
97
  var FRANKIE_CLOUD_URL = "https://frankie-control-665333762719.us-central1.run.app";
72
98
  var FrankieApiError = class extends Error {
@@ -95,7 +121,8 @@ var FrankieClient = class {
95
121
  baseUrl;
96
122
  doFetch;
97
123
  session;
98
- sessionUserToken;
124
+ /** Identity (JWT `sub`, else the raw token) the cached session was issued for. */
125
+ sessionUserKey;
99
126
  get sessionId() {
100
127
  return this.session?.sessionId;
101
128
  }
@@ -137,7 +164,18 @@ var FrankieClient = class {
137
164
  return await this.unwrap(response);
138
165
  }
139
166
  async ensureSession(context) {
140
- const userToken = await this.options.getUserToken?.();
167
+ let userToken;
168
+ try {
169
+ userToken = await this.options.getUserToken?.();
170
+ } catch (error) {
171
+ this.resetSession();
172
+ const reason = error instanceof Error && error.message.trim() ? error.message.trim() : void 0;
173
+ throw new FrankieApiError(
174
+ reason ?? "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
175
+ 401,
176
+ "user_token_unavailable"
177
+ );
178
+ }
141
179
  if (this.options.getUserToken && !userToken) {
142
180
  this.resetSession();
143
181
  throw new FrankieApiError(
@@ -146,7 +184,8 @@ var FrankieClient = class {
146
184
  "user_token_unavailable"
147
185
  );
148
186
  }
149
- if (userToken !== this.sessionUserToken) this.resetSession();
187
+ const userKey = sessionIdentity(userToken);
188
+ if (userKey !== this.sessionUserKey) this.resetSession();
150
189
  if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
151
190
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
152
191
  method: "POST",
@@ -158,7 +197,7 @@ var FrankieClient = class {
158
197
  })
159
198
  });
160
199
  this.session = await this.unwrap(response);
161
- this.sessionUserToken = userToken;
200
+ this.sessionUserKey = userKey;
162
201
  this.writeStored();
163
202
  return this.session;
164
203
  }
@@ -180,7 +219,7 @@ var FrankieClient = class {
180
219
  if (response.status === 404) return void 0;
181
220
  const body = await this.unwrap(response);
182
221
  this.session = body.session;
183
- this.sessionUserToken = userToken;
222
+ this.sessionUserKey = sessionIdentity(userToken);
184
223
  this.writeStored();
185
224
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
186
225
  }
@@ -239,9 +278,59 @@ var FrankieClient = class {
239
278
  return;
240
279
  }
241
280
  for await (const event of readEventStream(response.body)) {
281
+ if (event.type === "tool_call_request") {
282
+ void this.runClientTool(session, event.callId, event.request);
283
+ continue;
284
+ }
242
285
  onEvent(event);
243
286
  }
244
287
  }
288
+ /**
289
+ * Execute one host API call in the browser and report the outcome back to the
290
+ * server. Never throws: a transport failure or a missing `api` client becomes
291
+ * a result the model can see and explain, exactly like a non-2xx status.
292
+ */
293
+ async runClientTool(session, callId, request) {
294
+ const result = await this.callHostApi(request);
295
+ await this.postToolResult(session, callId, result).catch(() => void 0);
296
+ }
297
+ async callHostApi(request) {
298
+ const api = this.options.api;
299
+ if (!api) {
300
+ return {
301
+ status: 0,
302
+ error: "This assistant tried to call your API from the browser, but no host API client is configured in the Frankie SDK (the `api` option)."
303
+ };
304
+ }
305
+ try {
306
+ const extra = api.headers ? await api.headers() : {};
307
+ const response = await this.doFetch(joinHostUrl(api.baseUrl, request.path), {
308
+ method: request.method,
309
+ credentials: api.credentials ?? "same-origin",
310
+ headers: {
311
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
312
+ ...request.body !== void 0 ? { "content-type": "application/json" } : {},
313
+ ...extra
314
+ },
315
+ ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {}
316
+ });
317
+ const body = await response.text().catch(() => "");
318
+ return { status: response.status, body };
319
+ } catch (error) {
320
+ return { status: 0, error: error instanceof Error ? error.message : String(error) };
321
+ }
322
+ }
323
+ async postToolResult(session, callId, result) {
324
+ await this.doFetch(`${this.baseUrl}/v1/chat/tool-result`, {
325
+ method: "POST",
326
+ headers: {
327
+ "content-type": "application/json",
328
+ "x-frankie-key": this.options.publishableKey,
329
+ authorization: `Bearer ${session.sessionToken}`
330
+ },
331
+ body: JSON.stringify({ callId, result })
332
+ });
333
+ }
245
334
  async unwrap(response) {
246
335
  if (response.ok) return response.json();
247
336
  let code = "internal";
@@ -255,6 +344,12 @@ var FrankieClient = class {
255
344
  throw new FrankieApiError(message, response.status, code);
256
345
  }
257
346
  };
347
+ function joinHostUrl(baseUrl, path) {
348
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) {
349
+ throw new Error("A client tool path must be relative to the configured host API base URL.");
350
+ }
351
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
352
+ }
258
353
  async function* readEventStream(body) {
259
354
  const reader = body.getReader();
260
355
  const decoder = new TextDecoder();
@@ -287,6 +382,68 @@ function parseFrame(frame) {
287
382
  }
288
383
  }
289
384
 
385
+ // src/lib/theme-inherit.ts
386
+ function parseRgb(value) {
387
+ const match = value.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/i);
388
+ if (!match) return null;
389
+ const alpha = match[4] === void 0 ? 1 : match[4].endsWith("%") ? Number(match[4].slice(0, -1)) / 100 : Number(match[4]);
390
+ return [Number(match[1]), Number(match[2]), Number(match[3]), alpha];
391
+ }
392
+ function toCss([r, g, b, a]) {
393
+ return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
394
+ }
395
+ function sameColor(a, b) {
396
+ return Math.abs(a[0] - b[0]) < 12 && Math.abs(a[1] - b[1]) < 12 && Math.abs(a[2] - b[2]) < 12;
397
+ }
398
+ function luminance([r, g, b]) {
399
+ const channel = (c) => {
400
+ const v = c / 255;
401
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
402
+ };
403
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
404
+ }
405
+ function contrastRatio(a, b) {
406
+ const la = luminance(a), lb = luminance(b);
407
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
408
+ }
409
+ var ACCENT_SELECTORS = ["[data-frankie-accent]", "button[type=submit]", "a[href]", "button", "[class*=primary]", "[class*=btn]"];
410
+ function inferHostTheme() {
411
+ if (typeof document === "undefined" || typeof getComputedStyle !== "function") return null;
412
+ try {
413
+ const bodyStyle = getComputedStyle(document.body);
414
+ const background = parseRgb(bodyStyle.backgroundColor);
415
+ const foreground = parseRgb(bodyStyle.color);
416
+ if (!background || !foreground) return null;
417
+ if (background[3] < 0.5) return null;
418
+ if (contrastRatio(background, foreground) < 2) return null;
419
+ let accent = null;
420
+ for (const selector of ACCENT_SELECTORS) {
421
+ const element = document.querySelector(selector);
422
+ if (!element) continue;
423
+ const style = getComputedStyle(element);
424
+ const surface = parseRgb(style.backgroundColor);
425
+ if (surface && surface[3] > 0.5 && !sameColor(surface, background)) {
426
+ accent = surface;
427
+ break;
428
+ }
429
+ const text = parseRgb(style.color);
430
+ if (text && text[3] > 0.5 && !sameColor(text, foreground)) {
431
+ accent = text;
432
+ break;
433
+ }
434
+ }
435
+ if (!accent) return null;
436
+ return {
437
+ background: toCss(background),
438
+ foreground: toCss(foreground),
439
+ primary: toCss(accent),
440
+ primaryForeground: luminance(accent) > 0.5 ? "#0a0a0a" : "#fafafa"
441
+ };
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+
290
447
  // src/hooks/useFrankieChat.ts
291
448
  var import_react = require("react");
292
449
 
@@ -607,6 +764,7 @@ function FrankieProvider({
607
764
  context,
608
765
  theme,
609
766
  config: providedConfig,
767
+ injectStyles = true,
610
768
  ...clientOptions
611
769
  }) {
612
770
  const tokenCallback = (0, import_react2.useRef)(clientOptions.getUserToken);
@@ -614,7 +772,7 @@ function FrankieProvider({
614
772
  const client = (0, import_react2.useMemo)(
615
773
  () => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
616
774
  // eslint-disable-next-line react-hooks/exhaustive-deps
617
- [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
775
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, clientOptions.api?.baseUrl, clientOptions.api?.credentials, Boolean(clientOptions.getUserToken)]
618
776
  );
619
777
  const [config, setConfig] = (0, import_react2.useState)(providedConfig);
620
778
  const [configError, setConfigError] = (0, import_react2.useState)(void 0);
@@ -641,9 +799,10 @@ function FrankieProvider({
641
799
  ...configError ? { configError } : {},
642
800
  loading,
643
801
  ...context ? { context } : {},
644
- ...theme ? { theme } : {}
802
+ ...theme ? { theme } : {},
803
+ injectStyles
645
804
  }),
646
- [client, config, configError, loading, context, theme]
805
+ [client, config, configError, loading, context, theme, injectStyles]
647
806
  );
648
807
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
649
808
  }
@@ -658,7 +817,16 @@ function useFrankie() {
658
817
  }
659
818
  function useFrankieTheme() {
660
819
  const { config, theme } = useFrankie();
661
- return (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
820
+ const merged = (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
821
+ const inheriting = merged?.source === "inherit";
822
+ const [inferred, setInferred] = (0, import_react2.useState)(null);
823
+ (0, import_react2.useEffect)(() => {
824
+ setInferred(inheriting ? inferHostTheme() : null);
825
+ }, [inheriting]);
826
+ return (0, import_react2.useMemo)(() => {
827
+ if (!inheriting || !inferred) return merged;
828
+ return { ...merged, ...inferred };
829
+ }, [merged, inheriting, inferred]);
662
830
  }
663
831
 
664
832
  // src/components/chat/FrankieWidget.tsx
@@ -667,9 +835,28 @@ var import_react10 = require("react");
667
835
 
668
836
  // src/lib/styles-ready.ts
669
837
  var import_react3 = require("react");
838
+
839
+ // src/generated/css-content.ts
840
+ var FRANKIE_CSS_CONTENT = `/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
841
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{.frankie-root{--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-tight:1.25;--radius-md:.375rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.frankie-page-shell{background:var(--frankie-background);height:100%;min-height:0;color:var(--frankie-foreground);flex:1;display:flex}.frankie-page-main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.frankie-history-desktop{border-right:1px solid var(--frankie-border);background:var(--frankie-secondary);flex-direction:column;flex-shrink:0;gap:16px;width:260px;padding:20px 14px;transition:width .16s,padding .16s;display:flex;overflow:auto}.frankie-history-toggle{border:1px solid var(--frankie-border);background:var(--frankie-background);width:32px;height:32px;color:var(--frankie-muted-foreground);cursor:pointer;border-radius:8px;flex-shrink:0;justify-content:center;align-self:flex-end;align-items:center;display:flex}.frankie-history-toggle:hover{color:var(--frankie-foreground)}.frankie-history-collapsed{width:56px;padding:20px 8px;overflow:visible}.frankie-history-collapsed .frankie-history-toggle{align-self:center}.frankie-history-heading{justify-content:space-between;align-items:center;font-size:14px;display:flex}.frankie-new-chat{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;gap:8px;min-height:44px;font-weight:600;display:flex}.frankie-history-search>span{color:var(--frankie-muted-foreground);margin-bottom:6px;font-size:12px;display:block}.frankie-history-search input{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:8px;width:100%;min-height:40px;padding:8px 10px}.frankie-history-list{flex-direction:column;flex:1;gap:4px;display:flex}.frankie-history-list button{text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:8px;flex-direction:column;align-items:flex-start;gap:5px;min-height:54px;padding:10px;display:flex}.frankie-history-list button span{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:13px;overflow:hidden}.frankie-history-list small{color:var(--frankie-muted-foreground);font-size:11px}.frankie-history-list button:hover,.frankie-history-list button[aria-current]{background:var(--frankie-background);border-color:var(--frankie-border)}.frankie-history-desktop>p,.frankie-history-dialog p{color:var(--frankie-muted-foreground);font-size:13px}.frankie-header-caption{color:var(--frankie-muted-foreground);font-size:12px}.frankie-page-header button{cursor:pointer;justify-content:center;align-items:center;gap:6px;min-width:40px;min-height:40px;display:inline-flex}.frankie-chat-settings{position:relative}.frankie-chat-settings summary{cursor:pointer;border-radius:8px;place-items:center;width:40px;height:40px;list-style:none;display:grid}.frankie-chat-settings summary::-webkit-details-marker{display:none}.frankie-chat-settings>div{z-index:5;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:12px;gap:12px;width:min(280px,80vw);padding:16px;font-size:13px;display:grid;position:absolute;top:46px;right:0;box-shadow:0 10px 30px #0002}.frankie-chat-settings p{color:var(--frankie-muted-foreground);margin:0}.frankie-loaded-skills{width:100%;max-width:760px;color:var(--frankie-muted-foreground);font-size:12px}.frankie-loaded-skills summary{cursor:pointer;padding:6px 0}.frankie-loaded-skills ul{margin:0;padding:4px 0;list-style:none}.frankie-loaded-skills li{gap:8px;padding:4px 0;display:flex}.frankie-loaded-skills small{opacity:.7}.frankie-history-dialog{background:var(--frankie-secondary);width:min(320px,86vw);max-width:none;height:100dvh;max-height:none;color:var(--frankie-foreground);border:0;margin:0;padding:0}.frankie-history-dialog::backdrop{background:#0008}.frankie-history-dialog>aside{height:100%;padding:calc(20px + env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom));flex-direction:column;gap:16px;display:flex;overflow:auto}.frankie-mobile-only{display:none!important}.frankie-widget-panel{max-height:calc(100dvh - 100px)}.frankie-root button:focus-visible,.frankie-root summary:focus-visible,.frankie-root input:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:3px}.frankie-root button:disabled{opacity:.5;cursor:not-allowed}@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{background:0 0;border:0;justify-content:center;align-items:center;min-width:44px;min-height:44px}.frankie-page-prompts{gap:8px}.frankie-widget-panel{width:calc(100vw - 32px)}}.frankie-root{--frankie-styles-ready:1;--frankie-background:#fff;--frankie-foreground:#0a0a0a;--frankie-primary:#171717;--frankie-primary-foreground:#fafafa;--frankie-secondary:#f5f5f5;--frankie-secondary-foreground:#171717;--frankie-muted:#f5f5f5;--frankie-muted-foreground:#737373;--frankie-border:#e5e5e5;--frankie-radius:.625rem;--frankie-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;font-family:var(--frankie-font);color:var(--frankie-foreground);-webkit-font-smoothing:antialiased;line-height:1.5}.frankie-root.dark{--frankie-background:#0a0a0a;--frankie-foreground:#fafafa;--frankie-primary:#fafafa;--frankie-primary-foreground:#171717;--frankie-secondary:#262626;--frankie-secondary-foreground:#fafafa;--frankie-muted:#262626;--frankie-muted-foreground:#a3a3a3;--frankie-border:#262626}.frankie-root,.frankie-root *,.frankie-root :before,.frankie-root :after{box-sizing:border-box}.frankie-root button,.frankie-root textarea,.frankie-root input{font:inherit;color:inherit;margin:0}.frankie-root textarea{background-color:var(--frankie-background)}.frankie-focus:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:2px}.frankie-scroll{scrollbar-width:thin;scrollbar-color:var(--frankie-border) transparent}.frankie-prose>*+*{margin-top:.6rem}.frankie-prose p{white-space:pre-wrap;overflow-wrap:anywhere;margin:0;line-height:1.6}.frankie-prose>.frankie-h{letter-spacing:-.01em;margin-top:1.15rem;line-height:1.3}.frankie-prose>.frankie-h:first-child{margin-top:0}.frankie-prose h3.frankie-h{font-size:1.06em;font-weight:700}.frankie-prose h4.frankie-h{font-size:1em;font-weight:600}.frankie-prose h5.frankie-h{color:var(--frankie-muted-foreground);font-size:.92em;font-weight:600}.frankie-prose strong{font-weight:600}.frankie-prose ul,.frankie-prose ol{flex-direction:column;gap:.35rem;margin:0;padding-left:1.4rem;display:flex}.frankie-prose li{padding-left:.15rem;line-height:1.55}.frankie-prose li::marker{color:var(--frankie-muted-foreground)}.frankie-prose ul{list-style:outside}.frankie-prose ol{list-style:decimal}.frankie-prose a{color:var(--frankie-primary);text-underline-offset:2px;text-decoration:underline}.frankie-prose code{background-color:var(--frankie-muted);border-radius:.25rem;padding:.1em .3em;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.85em}.frankie-prose pre{border-radius:var(--frankie-radius);background-color:var(--frankie-muted);margin:0;padding:.625rem .75rem;overflow-x:auto}.frankie-prose pre code{background:0 0;padding:0}.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, var(--frankie-primary), transparent 65%), var(--frankie-background);flex-direction:column;min-width:0;display:flex;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, color-mix(in srgb, var(--frankie-primary) 8%, transparent), transparent 65%), var(--frankie-background)}}.frankie-chat-page{color:var(--frankie-foreground)}.frankie-page-header{border-bottom:1px solid var(--frankie-border);background:var(--frankie-background);flex:0 0 56px;justify-content:space-between;align-items:center;gap:16px;min-height:56px;padding:0 24px;display:flex}@supports (color:color-mix(in lab, red, red)){.frankie-page-header{background:color-mix(in srgb, var(--frankie-background) 92%, transparent)}}.frankie-page-header{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.frankie-page-header>div{align-items:center;gap:10px;display:flex}.frankie-page-header strong{font-size:.925rem;font-weight:650}.frankie-page-status{background:var(--frankie-primary);width:10px;height:10px;box-shadow:0 0 0 4px var(--frankie-primary);border-radius:999px}@supports (color:color-mix(in lab, red, red)){.frankie-page-status{box-shadow:0 0 0 4px color-mix(in srgb, var(--frankie-primary) 16%, transparent)}}.frankie-page-header button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * .9);background:var(--frankie-secondary);cursor:pointer;min-height:36px;padding:0 13px}.frankie-page-header button:hover{border-color:var(--frankie-primary)}.frankie-page-canvas{flex:1;min-width:0;min-height:0;display:flex}.frankie-page-canvas>.frankie-inline{width:100%;max-width:56rem;margin:0 auto}.frankie-page-canvas .frankie-scroll{padding:2rem clamp(1rem,4vw,4rem) 1.25rem}.frankie-page-canvas .frankie-root>.border-t{background:linear-gradient(180deg, transparent, var(--frankie-background) 30%);border-top:0;padding:.75rem clamp(1rem,4vw,4rem) 1.5rem}.frankie-page-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:1rem;width:min(100%,38rem);min-height:100%;margin:auto;padding:clamp(2rem,7vh,4.5rem) 1rem 2rem;display:flex}.frankie-page-avatar{border-radius:calc(var(--frankie-radius) * 1.4);background:var(--frankie-primary);width:48px;height:48px;color:var(--frankie-primary-foreground);place-items:center;font-weight:750;display:grid}.frankie-page-kicker{color:var(--frankie-primary);letter-spacing:.1em;text-transform:uppercase;margin:0 0 .4rem;font-size:.7rem;font-weight:750}.frankie-page-empty h1{letter-spacing:-.045em;margin:0;font-size:clamp(1.65rem,4vw,2.15rem);line-height:1.08}.frankie-page-empty h1+p{max-width:30rem;color:var(--frankie-muted-foreground);margin:.6rem auto 0}.frankie-page-prompts{gap:.55rem;width:100%;margin-top:.4rem;display:grid}.frankie-page-prompts button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * 1.25);background:var(--frankie-secondary);text-align:left;cursor:pointer;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.65rem;min-height:60px;padding:.75rem .9rem;display:grid}.frankie-page-prompts button:hover{border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-page-prompts button:hover{border-color:color-mix(in srgb, var(--frankie-primary) 58%, var(--frankie-border))}}.frankie-page-prompts button:hover{transform:translateY(-1px)}.frankie-page-prompts button>span{color:var(--frankie-muted-foreground);font-size:.68rem;font-weight:700}.frankie-page-prompts button>strong{font-size:.875rem}.frankie-page-prompts button>b{color:var(--frankie-primary);font-size:1rem}@media (max-width:640px){.frankie-page-header{flex-basis:52px;min-height:52px;padding:0 12px}.frankie-page-canvas .frankie-scroll{padding:1.25rem .75rem .5rem}.frankie-page-canvas .frankie-root>.border-t{padding:.65rem .75rem calc(.65rem + env(safe-area-inset-bottom))}}}@layer utilities;@scope(.frankie-root){.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.isolate{isolation:isolate}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0{margin-top:0}.mt-0\\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-1{width:var(--spacing);height:var(--spacing)}.size-1\\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-full{width:100%;height:100%}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-\\[32rem\\]{height:32rem}.h-full{height:100%}.h-px{height:1px}.min-h-7{min-height:calc(var(--spacing) * 7)}.min-h-\\[2\\.25rem\\]{min-height:2.25rem}.w-\\[22rem\\]{width:22rem}.w-full{width:100%}.max-w-\\[12rem\\]{max-width:12rem}.max-w-\\[14rem\\]{max-width:14rem}.max-w-\\[85\\%\\]{max-width:85%}.max-w-\\[calc\\(100vw-2rem\\)\\]{max-width:calc(100vw - 2rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\\[calc\\(var\\(--frankie-radius\\)\\+4px\\)\\]{border-radius:calc(var(--frankie-radius) + 4px)}.rounded-\\[calc\\(var\\(--frankie-radius\\)-2px\\)\\]{border-radius:calc(var(--frankie-radius) - 2px)}.rounded-\\[var\\(--frankie-radius\\)\\]{border-radius:var(--frankie-radius)}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--frankie-border)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--frankie-background)}.bg-border{background-color:var(--frankie-border)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-muted{background-color:var(--frankie-muted)}.bg-muted-foreground{background-color:var(--frankie-muted-foreground)}.bg-primary{background-color:var(--frankie-primary)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-secondary{background-color:var(--frankie-secondary)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\\[0\\.625rem\\]{font-size:.625rem}.text-\\[0\\.6875rem\\]{font-size:.6875rem}.text-\\[11px\\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-emerald-600{color:var(--color-emerald-600)}.text-foreground{color:var(--frankie-foreground)}.text-muted-foreground{color:var(--frankie-muted-foreground)}.text-primary{color:var(--frankie-primary)}.text-primary-foreground{color:var(--frankie-primary-foreground)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--frankie-secondary-foreground)}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.placeholder\\:text-muted-foreground::placeholder{color:var(--frankie-muted-foreground)}@media (hover:hover){.hover\\:border-border:hover{border-color:var(--frankie-border)}.hover\\:bg-muted:hover{background-color:var(--frankie-muted)}.hover\\:text-foreground:hover{color:var(--frankie-foreground)}.hover\\:underline:hover{text-decoration-line:underline}.hover\\:opacity-90:hover{opacity:.9}.hover\\:opacity-100:hover{opacity:1}}.focus-visible\\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\\:ring-primary:focus-visible{--tw-ring-color:var(--frankie-primary)}.focus-visible\\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.disabled\\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\\:opacity-50:disabled{opacity:.5}.dark\\:text-emerald-400:where(.dark,.dark *){color:var(--color-emerald-400)}.dark\\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.\\[\\&_svg\\]\\:pointer-events-none svg{pointer-events:none}.\\[\\&_svg\\]\\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\\[\\&_svg\\]\\:shrink-0 svg{flex-shrink:0}.\\[\\&\\:\\:-webkit-details-marker\\]\\:hidden::-webkit-details-marker{display:none}}.frankie-root.frankie-chat-panel{min-height:0;position:relative}.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px var(--frankie-primary), 0 12px 28px -12px #0f172a47, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px color-mix(in srgb, var(--frankie-primary) 22%, #0f172a6b), 0 12px 28px -12px #0f172a47, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 8%, transparent)}}.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 12%, transparent)}}.frankie-root .frankie-agent-context-wrap{flex:none;justify-content:center;padding:12px 10px;display:flex}.frankie-root .frankie-agent-context{max-width:100%;color:var(--frankie-foreground);font-size:13px;position:relative}.frankie-root .frankie-agent-context>summary{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;white-space:nowrap;border-radius:999px;justify-content:center;align-items:center;gap:10px;min-height:44px;padding:7px 13px;list-style:none;display:flex;box-shadow:0 1px 3px #0000000d}.frankie-root .frankie-agent-context>summary::-webkit-details-marker{display:none}.frankie-root .frankie-agent-context>summary>svg{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-context-count{background:var(--frankie-muted);color:var(--frankie-muted-foreground);border-radius:999px;padding:2px 10px;font-size:12px}.frankie-root .frankie-context-status{color:var(--frankie-muted-foreground);align-items:center;gap:6px;font-size:12px;display:flex}.frankie-root .frankie-context-status i{background:currentColor;border-radius:50%;width:7px;height:7px}.frankie-root .frankie-context-status[data-active=true]{color:#00976b}.frankie-root .frankie-context-detail{z-index:10;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:16px;width:340px;max-width:calc(100vw - 32px);padding:18px;position:absolute;top:calc(100% + 10px);left:50%;transform:translate(-50%);box-shadow:0 12px 32px #00000016}.frankie-root .frankie-context-detail p{color:var(--frankie-muted-foreground);margin:10px 0 0;font-size:12px;line-height:1.6}.frankie-root .frankie-context-detail ul{max-height:220px;margin:12px 0;padding:0;list-style:none;overflow:auto}.frankie-root .frankie-context-detail li{border-bottom:1px solid var(--frankie-border);overflow-wrap:anywhere;justify-content:space-between;gap:12px;padding:8px 0;display:flex}.frankie-root .frankie-context-detail small{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.frankie-root.frankie-chat-panel-fill{--frankie-canvas:var(--frankie-background)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill{--frankie-canvas:color-mix(in srgb,var(--frankie-background) 60%,var(--frankie-muted))}}.frankie-root.frankie-chat-panel-fill{background:var(--frankie-canvas)}.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:44px 16px 26px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:14px max(24px,50% - 480px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-message-list{gap:38px}.frankie-root.frankie-chat-panel-fill .frankie-user-bubble{background:var(--frankie-muted);color:var(--frankie-foreground);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:24px;padding:12px 20px;font-size:15px}.frankie-root.frankie-chat-panel-fill .frankie-assistant-content{flex:1;max-width:100%;font-size:15px;line-height:1.7}.frankie-root.frankie-chat-panel-fill .frankie-assistant-message{gap:14px}.frankie-root .frankie-thinking{color:var(--frankie-muted-foreground);gap:5px}.frankie-root .frankie-thinking-label{margin-right:6px;font-size:14px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{background:0 0;border:0;flex:none;padding:16px max(24px,50% - 500px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-composer{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:28px;padding:14px;box-shadow:0 2px 4px #0000000a}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:color-mix(in srgb,var(--frankie-foreground) 32%,var(--frankie-border))}}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px color-mix(in srgb,var(--frankie-primary) 8%,transparent)}}.frankie-root.frankie-chat-panel-fill .frankie-composer-controls{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.frankie-root.frankie-chat-panel-fill .frankie-composer textarea{resize:none;width:100%;min-height:32px;max-height:140px;color:var(--frankie-foreground);box-shadow:none;background:0 0;border:0;border-radius:0;outline:none;flex:0 0 100%;order:-1;margin-bottom:4px;padding:2px 6px;font-size:16px}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Send],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Stop]{background:var(--frankie-primary);width:42px;height:42px;color:var(--frankie-primary-foreground);border:0;border-radius:50%;margin-left:auto}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Attach\\ files]{border-radius:50%;width:42px;height:42px}.frankie-root .frankie-composer-actions{align-items:center;gap:6px;min-width:0;display:flex}.frankie-root .frankie-composer-meta{align-items:center;gap:8px;margin-top:6px;padding:0 2px;display:flex}.frankie-root .frankie-model-picker{align-items:center;min-width:0;display:inline-flex}.frankie-root .frankie-model-select{appearance:none;max-width:12rem;color:var(--frankie-muted-foreground);font:inherit;cursor:pointer;text-overflow:ellipsis;background-color:#0000;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b8b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");background-position:right 6px center;background-repeat:no-repeat;background-size:11px;border:0;border-radius:8px;padding:5px 22px 5px 8px;font-size:12px;font-weight:500;line-height:1;transition:background-color .12s,color .12s}.frankie-root .frankie-model-select:hover{background-color:var(--frankie-muted);color:var(--frankie-foreground)}.frankie-root .frankie-model-select:disabled{opacity:.5;cursor:not-allowed}.frankie-root .frankie-model-select option{color:var(--frankie-foreground);background:var(--frankie-background);font-weight:400}.frankie-root .frankie-history-desktop{background:var(--frankie-background);width:290px;padding:20px 12px}.frankie-root .frankie-history-collapsed{width:56px;padding:20px 8px}.frankie-root .frankie-new-chat{background:0 0;border:0;justify-content:flex-start;padding:8px 12px;font-size:15px}.frankie-root .frankie-history-heading{color:var(--frankie-muted-foreground);padding:0 12px;font-size:12px;font-weight:400}.frankie-root .frankie-history-list button{min-height:44px;padding:10px 12px}.frankie-root .frankie-history-list button span{font-size:14px}.frankie-root .frankie-history-list button[aria-current],.frankie-root .frankie-history-list button:hover{background:var(--frankie-muted);border-color:#0000}.frankie-root .frankie-history-list small{display:none}.frankie-root .frankie-page-header{background:var(--frankie-background);height:56px;min-height:56px}.frankie-root .frankie-page-header>div{min-width:0}.frankie-root .frankie-thread-title{text-overflow:ellipsis;white-space:nowrap;max-width:55vw;font-weight:500;overflow:hidden}.frankie-root .frankie-page-header button{background:0 0;border-color:#0000}.frankie-root .frankie-page-canvas>.frankie-inline{max-width:none}@media (max-width:760px){.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:20px 8px 14px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:12px 16px 16px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{padding:10px 12px calc(12px + env(safe-area-inset-bottom))}.frankie-root .frankie-agent-context>summary{gap:6px;padding:7px 9px;font-size:12px}.frankie-root .frankie-context-count{padding:2px 7px;font-size:11px}.frankie-root .frankie-context-status{gap:4px;font-size:11px}.frankie-root .frankie-agent-context>summary>svg{width:14px}.frankie-root .frankie-thread-title{max-width:42vw}}.frankie-root .frankie-agent-context>summary>svg:first-child{box-sizing:content-box;background:var(--frankie-muted);border-radius:50%;padding:4px}.frankie-root.frankie-chat-panel-fill .frankie-agent-context>summary{font-size:14px}.frankie-root .frankie-agent-context>summary>span:first-of-type{border-right:1px solid var(--frankie-border);padding-right:10px}@media (max-width:760px){.frankie-root .frankie-page-header button>span{display:none}}.frankie-root .frankie-choices{flex-direction:column;gap:8px;margin-top:12px;display:flex}.frankie-root .frankie-choice{text-align:left;cursor:pointer;border:1px solid var(--frankie-border);border-radius:var(--frankie-radius);background:var(--frankie-background);width:100%;color:var(--frankie-foreground);font:inherit;align-items:center;gap:12px;padding:11px 14px;font-size:14px;line-height:1.4;transition:border-color .15s,background-color .15s,transform .12s;display:flex}.frankie-root .frankie-choice:hover:not(:disabled){border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){border-color:color-mix(in srgb, var(--frankie-primary) 55%, var(--frankie-border))}}.frankie-root .frankie-choice:hover:not(:disabled){background:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){background:color-mix(in srgb, var(--frankie-primary) 6%, var(--frankie-background))}}.frankie-root .frankie-choice:active:not(:disabled){transform:scale(.99)}.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px var(--frankie-primary);outline:none}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, var(--frankie-primary) 40%, transparent)}}.frankie-root .frankie-choice:disabled{opacity:.6;cursor:default}.frankie-root .frankie-choice-index{border:1px solid var(--frankie-border);background:var(--frankie-muted);width:24px;height:24px;color:var(--frankie-muted-foreground);border-radius:50%;flex:none;place-items:center;font-size:12px;font-weight:600;transition:background-color .15s,color .15s,border-color .15s;display:grid}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-primary);border-color:var(--frankie-primary);color:var(--frankie-primary-foreground)}.frankie-root .frankie-choice-label{flex:1;min-width:0}.frankie-root .frankie-choice-go{color:var(--frankie-muted-foreground);opacity:0;flex:none;transition:opacity .15s,transform .15s;transform:translate(-4px)}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-go{opacity:1;color:var(--frankie-primary);transform:none}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-choice,.frankie-root .frankie-choice-go,.frankie-root .frankie-choice-index{transition:none}}.frankie-root .frankie-choice-other{color:var(--frankie-muted-foreground);border-style:dashed}.frankie-root .frankie-choice-other .frankie-choice-index{background:0 0}.frankie-root .frankie-choice-other:hover:not(:disabled){color:var(--frankie-foreground)}.frankie-root .frankie-choice-other:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-muted);border-color:var(--frankie-border);color:var(--frankie-foreground)}.frankie-root .frankie-choice-hint{color:var(--frankie-muted-foreground);flex:none;font-size:12px}.frankie-root .frankie-skeleton{flex-direction:column;gap:8px;max-width:min(420px,100%);padding:4px 0 6px;display:flex}.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, var(--frankie-muted-foreground) 25%, var(--frankie-muted-foreground) 37%, var(--frankie-muted-foreground) 63%);border-radius:6px;height:12px}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 25%, color-mix(in srgb, var(--frankie-muted-foreground) 26%, transparent) 37%, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 63%)}}.frankie-root .frankie-skeleton-line{background-size:400% 100%;animation:1.4s infinite frankie-shimmer}@keyframes frankie-shimmer{0%{background-position:100%}to{background-position:0}}@keyframes frankie-skeleton-pulse{0%,to{opacity:.45}50%{opacity:.85}}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-skeleton-line{background:var(--frankie-muted-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:color-mix(in srgb, var(--frankie-muted-foreground) 16%, transparent)}}.frankie-root .frankie-skeleton-line{animation:1.6s ease-in-out infinite frankie-skeleton-pulse}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}`;
842
+
843
+ // src/lib/ensure-frankie-styles.ts
844
+ var STYLE_ELEMENT_ID = "frankie-fallback-styles";
845
+ var attempted = false;
846
+ function ensureFrankieStyles() {
847
+ if (attempted || typeof document === "undefined") return;
848
+ attempted = true;
849
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
850
+ const style = document.createElement("style");
851
+ style.id = STYLE_ELEMENT_ID;
852
+ style.textContent = FRANKIE_CSS_CONTENT;
853
+ document.head.appendChild(style);
854
+ }
855
+
856
+ // src/lib/styles-ready.ts
670
857
  var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
671
858
  var SENTINEL = "--frankie-styles-ready";
672
- function useStylesReady(ref, timeoutMs = 600) {
859
+ function useStylesReady(ref, timeoutMs = 600, injectFallback = true) {
673
860
  const [ready, setReady] = (0, import_react3.useState)(false);
674
861
  useIsomorphicLayoutEffect(() => {
675
862
  if (ready) return;
@@ -686,7 +873,12 @@ function useStylesReady(ref, timeoutMs = 600) {
686
873
  const deadline = Date.now() + timeoutMs;
687
874
  let frame = 0;
688
875
  const poll = () => {
689
- if (applied() || Date.now() >= deadline) {
876
+ if (applied()) {
877
+ setReady(true);
878
+ return;
879
+ }
880
+ if (Date.now() >= deadline) {
881
+ if (injectFallback) ensureFrankieStyles();
690
882
  setReady(true);
691
883
  return;
692
884
  }
@@ -698,7 +890,7 @@ function useStylesReady(ref, timeoutMs = 600) {
698
890
  }
699
891
  frame = window.requestAnimationFrame(poll);
700
892
  return () => window.cancelAnimationFrame(frame);
701
- }, [ref, ready, timeoutMs]);
893
+ }, [ref, ready, timeoutMs, injectFallback]);
702
894
  return ready;
703
895
  }
704
896
 
@@ -996,35 +1188,6 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
996
1188
  )) }) : null,
997
1189
  fileError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
998
1190
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "frankie-composer-controls flex items-end gap-2", children: [
999
- canAttach ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1000
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1001
- "input",
1002
- {
1003
- ref: fileInputRef,
1004
- type: "file",
1005
- multiple: true,
1006
- hidden: true,
1007
- accept: attachments?.accept.join(","),
1008
- onChange: (event) => {
1009
- void addFiles(event.target.files);
1010
- event.target.value = "";
1011
- }
1012
- }
1013
- ),
1014
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1015
- Button,
1016
- {
1017
- type: "button",
1018
- size: "icon",
1019
- variant: "outline",
1020
- disabled: disabled || files.length >= (attachments?.maxCount ?? 0),
1021
- onClick: () => fileInputRef.current?.click(),
1022
- "aria-label": "Attach files",
1023
- title: "Attach files",
1024
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Plus, {})
1025
- }
1026
- )
1027
- ] }) : null,
1028
1191
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1029
1192
  Textarea,
1030
1193
  {
@@ -1039,17 +1202,51 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
1039
1202
  "aria-label": "Message"
1040
1203
  }
1041
1204
  ),
1042
- models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1043
- "select",
1044
- {
1045
- value: modelPicker?.value ?? models[0]?.id,
1046
- onChange: (event) => modelPicker?.onChange(event.target.value),
1047
- disabled,
1048
- "aria-label": "Model",
1049
- className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
1050
- children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: model.id, children: model.label }, model.id))
1051
- }
1052
- ) : null,
1205
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "frankie-composer-actions", children: [
1206
+ canAttach ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1207
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1208
+ "input",
1209
+ {
1210
+ ref: fileInputRef,
1211
+ type: "file",
1212
+ multiple: true,
1213
+ hidden: true,
1214
+ accept: attachments?.accept.join(","),
1215
+ onChange: (event) => {
1216
+ void addFiles(event.target.files);
1217
+ event.target.value = "";
1218
+ }
1219
+ }
1220
+ ),
1221
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1222
+ Button,
1223
+ {
1224
+ type: "button",
1225
+ size: "icon",
1226
+ variant: "outline",
1227
+ disabled: disabled || files.length >= (attachments?.maxCount ?? 0),
1228
+ onClick: () => fileInputRef.current?.click(),
1229
+ "aria-label": "Attach files",
1230
+ title: "Attach files",
1231
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Plus, {})
1232
+ }
1233
+ )
1234
+ ] }) : null,
1235
+ models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "frankie-model-picker", title: "Model", children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "frankie-sr-only", children: "Model" }),
1237
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1238
+ "select",
1239
+ {
1240
+ value: modelPicker?.value ?? models[0]?.id,
1241
+ onChange: (event) => modelPicker?.onChange(event.target.value),
1242
+ disabled,
1243
+ "aria-label": "Model",
1244
+ className: "frankie-model-select",
1245
+ children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: model.id, children: model.label }, model.id))
1246
+ }
1247
+ )
1248
+ ] }) : null
1249
+ ] }),
1053
1250
  busy ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.ArrowUp, {}) })
1054
1251
  ] })
1055
1252
  ]
@@ -1280,6 +1477,7 @@ function ToolActivityList({ tools, renderTool }) {
1280
1477
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1281
1478
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1282
1479
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1480
+ running ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Wrench, { className: "size-3.5", "aria-hidden": "true" }),
1283
1481
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: running ? "Working" : "Activity" }),
1284
1482
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[11px]", children: [
1285
1483
  "\xB7 ",
@@ -1348,18 +1546,20 @@ var MessageBubble = (0, import_react8.memo)(function MessageBubble2({
1348
1546
  ] }) : null
1349
1547
  ] });
1350
1548
  }
1351
- const empty = message.text.length === 0 && message.tools.length === 0;
1352
1549
  const { body, options } = extractChoices(message.text);
1353
1550
  const showChoices = Boolean(active && onChoose && options && message.status === "done");
1551
+ const toolsRunning = message.tools.some((tool) => tool.status === "running");
1552
+ const waiting = message.status === "streaming" && body.length === 0 && !toolsRunning;
1354
1553
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1355
1554
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1356
1555
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1357
1556
  body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: body }) : null
1358
1557
  ] }),
1359
1558
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1360
- message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }),
1559
+ message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }) : null,
1361
1560
  showChoices ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1362
- showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1561
+ showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null,
1562
+ waiting ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thinking, {}) : null
1363
1563
  ] })
1364
1564
  ] });
1365
1565
  });
@@ -1388,6 +1588,13 @@ function TypingDots() {
1388
1588
  ))
1389
1589
  ] });
1390
1590
  }
1591
+ function Thinking() {
1592
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-skeleton", role: "status", "aria-label": "Thinking", children: [
1593
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "62%" } }),
1594
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "92%" } }),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-skeleton-line", style: { width: "48%" } })
1596
+ ] });
1597
+ }
1391
1598
 
1392
1599
  // src/components/chat/SuggestedPrompts.tsx
1393
1600
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -1533,10 +1740,10 @@ function FrankieWidget({
1533
1740
  renderLauncher,
1534
1741
  ...panelProps
1535
1742
  }) {
1536
- const { config } = useFrankie();
1743
+ const { config, injectStyles } = useFrankie();
1537
1744
  const theme = useFrankieTheme();
1538
1745
  const rootRef = (0, import_react10.useRef)(null);
1539
- const stylesReady = useStylesReady(rootRef);
1746
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1540
1747
  const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react10.useState)(defaultOpen);
1541
1748
  const open = controlledOpen ?? uncontrolledOpen;
1542
1749
  const setOpen = (next) => {
@@ -1585,10 +1792,11 @@ function FrankieWidget({
1585
1792
  var import_react11 = require("react");
1586
1793
  var import_jsx_runtime17 = require("react/jsx-runtime");
1587
1794
  function FrankieInline({ className, style, ...panelProps }) {
1795
+ const { injectStyles } = useFrankie();
1588
1796
  const theme = useFrankieTheme();
1589
1797
  const mode = resolveMode(theme?.mode);
1590
1798
  const rootRef = (0, import_react11.useRef)(null);
1591
- const stylesReady = useStylesReady(rootRef);
1799
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1592
1800
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1593
1801
  "div",
1594
1802
  {
@@ -1760,7 +1968,7 @@ function FrankieChatPage({ className, style, header, footer, emptyState, renderM
1760
1968
  const [id, setId] = (0, import_react12.useState)(() => provider.conversationId ?? crypto.randomUUID());
1761
1969
  const onNew = () => setId(crypto.randomUUID());
1762
1970
  const pageRef = (0, import_react12.useRef)(null);
1763
- const stylesReady = useStylesReady(pageRef);
1971
+ const stylesReady = useStylesReady(pageRef, void 0, provider.injectStyles);
1764
1972
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { ref: pageRef, className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } }, "data-frankie-styles": stylesReady ? "ready" : "pending", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1765
1973
  }
1766
1974
 
@@ -1796,6 +2004,7 @@ function Separator({ className }) {
1796
2004
  Separator,
1797
2005
  SuggestedPrompts,
1798
2006
  Textarea,
2007
+ Thinking,
1799
2008
  ToolActivityList,
1800
2009
  ToolActivityRow,
1801
2010
  TypingDots,