@frankieai/react 0.2.8 → 0.2.10
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/frankie.css +1 -1
- package/dist/index.cjs +596 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -10
- package/dist/index.d.ts +114 -10
- package/dist/index.js +579 -189
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/components/chat/ChatPanel.tsx +9 -1
- package/src/components/chat/Choices.tsx +52 -0
- package/src/components/chat/Composer.tsx +33 -18
- package/src/components/chat/FrankieChatPage.tsx +4 -1
- package/src/components/chat/FrankieInline.tsx +9 -3
- package/src/components/chat/FrankieWidget.tsx +12 -3
- package/src/components/chat/Markdown.tsx +15 -8
- package/src/components/chat/MessageBubble.tsx +53 -10
- package/src/components/chat/ToolActivityList.tsx +2 -1
- package/src/generated/css-content.ts +2 -0
- package/src/hooks/FrankieProvider.tsx +30 -3
- package/src/hooks/useFrankieChat.ts +57 -1
- package/src/index.ts +4 -1
- package/src/lib/choices.ts +49 -0
- package/src/lib/client.ts +136 -5
- package/src/lib/ensure-frankie-styles.ts +32 -0
- package/src/lib/identity.ts +39 -0
- package/src/lib/styles-ready.ts +83 -0
- package/src/lib/theme-inherit.test.ts +68 -0
- package/src/lib/theme-inherit.ts +89 -0
- package/src/styles/frankie.css +136 -3
- package/src/styles-ready.test.tsx +98 -0
- package/src/widget.test.tsx +218 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
// src/hooks/FrankieProvider.tsx
|
|
4
4
|
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
5
5
|
|
|
6
|
+
// src/lib/identity.ts
|
|
7
|
+
function sessionIdentity(token) {
|
|
8
|
+
if (!token) return void 0;
|
|
9
|
+
return jwtSubject(token) ?? token;
|
|
10
|
+
}
|
|
11
|
+
function jwtSubject(token) {
|
|
12
|
+
const parts = token.split(".");
|
|
13
|
+
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
14
|
+
try {
|
|
15
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
16
|
+
return typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function base64UrlDecode(segment) {
|
|
22
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
23
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
24
|
+
if (typeof atob === "function") {
|
|
25
|
+
const bytes = atob(padded);
|
|
26
|
+
return decodeURIComponent(Array.from(bytes, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join(""));
|
|
27
|
+
}
|
|
28
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
// src/lib/client.ts
|
|
7
32
|
var FRANKIE_CLOUD_URL = "https://frankie-control-665333762719.us-central1.run.app";
|
|
8
33
|
var FrankieApiError = class extends Error {
|
|
@@ -31,7 +56,8 @@ var FrankieClient = class {
|
|
|
31
56
|
baseUrl;
|
|
32
57
|
doFetch;
|
|
33
58
|
session;
|
|
34
|
-
|
|
59
|
+
/** Identity (JWT `sub`, else the raw token) the cached session was issued for. */
|
|
60
|
+
sessionUserKey;
|
|
35
61
|
get sessionId() {
|
|
36
62
|
return this.session?.sessionId;
|
|
37
63
|
}
|
|
@@ -73,7 +99,18 @@ var FrankieClient = class {
|
|
|
73
99
|
return await this.unwrap(response);
|
|
74
100
|
}
|
|
75
101
|
async ensureSession(context) {
|
|
76
|
-
|
|
102
|
+
let userToken;
|
|
103
|
+
try {
|
|
104
|
+
userToken = await this.options.getUserToken?.();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.resetSession();
|
|
107
|
+
const reason = error instanceof Error && error.message.trim() ? error.message.trim() : void 0;
|
|
108
|
+
throw new FrankieApiError(
|
|
109
|
+
reason ?? "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
110
|
+
401,
|
|
111
|
+
"user_token_unavailable"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
77
114
|
if (this.options.getUserToken && !userToken) {
|
|
78
115
|
this.resetSession();
|
|
79
116
|
throw new FrankieApiError(
|
|
@@ -82,7 +119,8 @@ var FrankieClient = class {
|
|
|
82
119
|
"user_token_unavailable"
|
|
83
120
|
);
|
|
84
121
|
}
|
|
85
|
-
|
|
122
|
+
const userKey = sessionIdentity(userToken);
|
|
123
|
+
if (userKey !== this.sessionUserKey) this.resetSession();
|
|
86
124
|
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
87
125
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
88
126
|
method: "POST",
|
|
@@ -94,7 +132,7 @@ var FrankieClient = class {
|
|
|
94
132
|
})
|
|
95
133
|
});
|
|
96
134
|
this.session = await this.unwrap(response);
|
|
97
|
-
this.
|
|
135
|
+
this.sessionUserKey = userKey;
|
|
98
136
|
this.writeStored();
|
|
99
137
|
return this.session;
|
|
100
138
|
}
|
|
@@ -116,7 +154,7 @@ var FrankieClient = class {
|
|
|
116
154
|
if (response.status === 404) return void 0;
|
|
117
155
|
const body = await this.unwrap(response);
|
|
118
156
|
this.session = body.session;
|
|
119
|
-
this.
|
|
157
|
+
this.sessionUserKey = sessionIdentity(userToken);
|
|
120
158
|
this.writeStored();
|
|
121
159
|
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
122
160
|
}
|
|
@@ -175,9 +213,59 @@ var FrankieClient = class {
|
|
|
175
213
|
return;
|
|
176
214
|
}
|
|
177
215
|
for await (const event of readEventStream(response.body)) {
|
|
216
|
+
if (event.type === "tool_call_request") {
|
|
217
|
+
void this.runClientTool(session, event.callId, event.request);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
178
220
|
onEvent(event);
|
|
179
221
|
}
|
|
180
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Execute one host API call in the browser and report the outcome back to the
|
|
225
|
+
* server. Never throws: a transport failure or a missing `api` client becomes
|
|
226
|
+
* a result the model can see and explain, exactly like a non-2xx status.
|
|
227
|
+
*/
|
|
228
|
+
async runClientTool(session, callId, request) {
|
|
229
|
+
const result = await this.callHostApi(request);
|
|
230
|
+
await this.postToolResult(session, callId, result).catch(() => void 0);
|
|
231
|
+
}
|
|
232
|
+
async callHostApi(request) {
|
|
233
|
+
const api = this.options.api;
|
|
234
|
+
if (!api) {
|
|
235
|
+
return {
|
|
236
|
+
status: 0,
|
|
237
|
+
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)."
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const extra = api.headers ? await api.headers() : {};
|
|
242
|
+
const response = await this.doFetch(joinHostUrl(api.baseUrl, request.path), {
|
|
243
|
+
method: request.method,
|
|
244
|
+
credentials: api.credentials ?? "same-origin",
|
|
245
|
+
headers: {
|
|
246
|
+
accept: "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
247
|
+
...request.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
248
|
+
...extra
|
|
249
|
+
},
|
|
250
|
+
...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {}
|
|
251
|
+
});
|
|
252
|
+
const body = await response.text().catch(() => "");
|
|
253
|
+
return { status: response.status, body };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return { status: 0, error: error instanceof Error ? error.message : String(error) };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async postToolResult(session, callId, result) {
|
|
259
|
+
await this.doFetch(`${this.baseUrl}/v1/chat/tool-result`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: {
|
|
262
|
+
"content-type": "application/json",
|
|
263
|
+
"x-frankie-key": this.options.publishableKey,
|
|
264
|
+
authorization: `Bearer ${session.sessionToken}`
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({ callId, result })
|
|
267
|
+
});
|
|
268
|
+
}
|
|
181
269
|
async unwrap(response) {
|
|
182
270
|
if (response.ok) return response.json();
|
|
183
271
|
let code = "internal";
|
|
@@ -191,6 +279,12 @@ var FrankieClient = class {
|
|
|
191
279
|
throw new FrankieApiError(message, response.status, code);
|
|
192
280
|
}
|
|
193
281
|
};
|
|
282
|
+
function joinHostUrl(baseUrl, path) {
|
|
283
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) {
|
|
284
|
+
throw new Error("A client tool path must be relative to the configured host API base URL.");
|
|
285
|
+
}
|
|
286
|
+
return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
287
|
+
}
|
|
194
288
|
async function* readEventStream(body) {
|
|
195
289
|
const reader = body.getReader();
|
|
196
290
|
const decoder = new TextDecoder();
|
|
@@ -223,6 +317,68 @@ function parseFrame(frame) {
|
|
|
223
317
|
}
|
|
224
318
|
}
|
|
225
319
|
|
|
320
|
+
// src/lib/theme-inherit.ts
|
|
321
|
+
function parseRgb(value) {
|
|
322
|
+
const match = value.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/i);
|
|
323
|
+
if (!match) return null;
|
|
324
|
+
const alpha = match[4] === void 0 ? 1 : match[4].endsWith("%") ? Number(match[4].slice(0, -1)) / 100 : Number(match[4]);
|
|
325
|
+
return [Number(match[1]), Number(match[2]), Number(match[3]), alpha];
|
|
326
|
+
}
|
|
327
|
+
function toCss([r, g, b, a]) {
|
|
328
|
+
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
329
|
+
}
|
|
330
|
+
function sameColor(a, b) {
|
|
331
|
+
return Math.abs(a[0] - b[0]) < 12 && Math.abs(a[1] - b[1]) < 12 && Math.abs(a[2] - b[2]) < 12;
|
|
332
|
+
}
|
|
333
|
+
function luminance([r, g, b]) {
|
|
334
|
+
const channel = (c) => {
|
|
335
|
+
const v = c / 255;
|
|
336
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
337
|
+
};
|
|
338
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
339
|
+
}
|
|
340
|
+
function contrastRatio(a, b) {
|
|
341
|
+
const la = luminance(a), lb = luminance(b);
|
|
342
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
343
|
+
}
|
|
344
|
+
var ACCENT_SELECTORS = ["[data-frankie-accent]", "button[type=submit]", "a[href]", "button", "[class*=primary]", "[class*=btn]"];
|
|
345
|
+
function inferHostTheme() {
|
|
346
|
+
if (typeof document === "undefined" || typeof getComputedStyle !== "function") return null;
|
|
347
|
+
try {
|
|
348
|
+
const bodyStyle = getComputedStyle(document.body);
|
|
349
|
+
const background = parseRgb(bodyStyle.backgroundColor);
|
|
350
|
+
const foreground = parseRgb(bodyStyle.color);
|
|
351
|
+
if (!background || !foreground) return null;
|
|
352
|
+
if (background[3] < 0.5) return null;
|
|
353
|
+
if (contrastRatio(background, foreground) < 2) return null;
|
|
354
|
+
let accent = null;
|
|
355
|
+
for (const selector of ACCENT_SELECTORS) {
|
|
356
|
+
const element = document.querySelector(selector);
|
|
357
|
+
if (!element) continue;
|
|
358
|
+
const style = getComputedStyle(element);
|
|
359
|
+
const surface = parseRgb(style.backgroundColor);
|
|
360
|
+
if (surface && surface[3] > 0.5 && !sameColor(surface, background)) {
|
|
361
|
+
accent = surface;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
const text = parseRgb(style.color);
|
|
365
|
+
if (text && text[3] > 0.5 && !sameColor(text, foreground)) {
|
|
366
|
+
accent = text;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!accent) return null;
|
|
371
|
+
return {
|
|
372
|
+
background: toCss(background),
|
|
373
|
+
foreground: toCss(foreground),
|
|
374
|
+
primary: toCss(accent),
|
|
375
|
+
primaryForeground: luminance(accent) > 0.5 ? "#0a0a0a" : "#fafafa"
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
226
382
|
// src/hooks/useFrankieChat.ts
|
|
227
383
|
import { createContext, useContext, useCallback, useEffect, useRef, useState } from "react";
|
|
228
384
|
|
|
@@ -401,6 +557,41 @@ function useFrankieChatState() {
|
|
|
401
557
|
setMessages(next);
|
|
402
558
|
const abort = new AbortController();
|
|
403
559
|
abortRef.current = abort;
|
|
560
|
+
let bufferedText = "";
|
|
561
|
+
let pendingThinking = "";
|
|
562
|
+
let frame = null;
|
|
563
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
|
|
564
|
+
const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
|
|
565
|
+
const revealTick = () => {
|
|
566
|
+
frame = null;
|
|
567
|
+
let addText = "";
|
|
568
|
+
if (bufferedText) {
|
|
569
|
+
const step = Math.max(3, Math.ceil(bufferedText.length / 6));
|
|
570
|
+
addText = bufferedText.slice(0, step);
|
|
571
|
+
bufferedText = bufferedText.slice(step);
|
|
572
|
+
}
|
|
573
|
+
const addThinking = pendingThinking;
|
|
574
|
+
pendingThinking = "";
|
|
575
|
+
if (addText || addThinking) {
|
|
576
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
577
|
+
}
|
|
578
|
+
if (bufferedText) scheduleReveal();
|
|
579
|
+
};
|
|
580
|
+
const scheduleReveal = () => {
|
|
581
|
+
if (frame === null) frame = raf(revealTick);
|
|
582
|
+
};
|
|
583
|
+
const flushDeltas = () => {
|
|
584
|
+
if (frame !== null) {
|
|
585
|
+
cancelRaf(frame);
|
|
586
|
+
frame = null;
|
|
587
|
+
}
|
|
588
|
+
if (!bufferedText && !pendingThinking) return;
|
|
589
|
+
const addText = bufferedText;
|
|
590
|
+
const addThinking = pendingThinking;
|
|
591
|
+
bufferedText = "";
|
|
592
|
+
pendingThinking = "";
|
|
593
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
594
|
+
};
|
|
404
595
|
try {
|
|
405
596
|
let receivedText = false;
|
|
406
597
|
let receivedToolActivity = false;
|
|
@@ -422,11 +613,23 @@ function useFrankieChatState() {
|
|
|
422
613
|
return;
|
|
423
614
|
}
|
|
424
615
|
if (event.type === "ping" || event.type === "session") return;
|
|
425
|
-
if (event.type === "text_delta")
|
|
616
|
+
if (event.type === "text_delta") {
|
|
617
|
+
receivedText = true;
|
|
618
|
+
bufferedText += event.text;
|
|
619
|
+
scheduleReveal();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (event.type === "thinking_delta") {
|
|
623
|
+
pendingThinking += event.text;
|
|
624
|
+
scheduleReveal();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
426
627
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
628
|
+
flushDeltas();
|
|
427
629
|
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
428
630
|
}
|
|
429
631
|
);
|
|
632
|
+
flushDeltas();
|
|
430
633
|
if (!receivedText) {
|
|
431
634
|
patchAssistant(assistantId, (message) => ({
|
|
432
635
|
...message,
|
|
@@ -435,6 +638,7 @@ function useFrankieChatState() {
|
|
|
435
638
|
}));
|
|
436
639
|
}
|
|
437
640
|
} catch (caught) {
|
|
641
|
+
flushDeltas();
|
|
438
642
|
if (abort.signal.aborted) {
|
|
439
643
|
patchAssistant(assistantId, (message) => ({
|
|
440
644
|
...message,
|
|
@@ -447,6 +651,7 @@ function useFrankieChatState() {
|
|
|
447
651
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
448
652
|
}
|
|
449
653
|
} finally {
|
|
654
|
+
flushDeltas();
|
|
450
655
|
patchAssistant(
|
|
451
656
|
assistantId,
|
|
452
657
|
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
@@ -494,6 +699,7 @@ function FrankieProvider({
|
|
|
494
699
|
context,
|
|
495
700
|
theme,
|
|
496
701
|
config: providedConfig,
|
|
702
|
+
injectStyles = true,
|
|
497
703
|
...clientOptions
|
|
498
704
|
}) {
|
|
499
705
|
const tokenCallback = useRef2(clientOptions.getUserToken);
|
|
@@ -501,7 +707,7 @@ function FrankieProvider({
|
|
|
501
707
|
const client = useMemo(
|
|
502
708
|
() => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
|
|
503
709
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
504
|
-
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
|
|
710
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, clientOptions.api?.baseUrl, clientOptions.api?.credentials, Boolean(clientOptions.getUserToken)]
|
|
505
711
|
);
|
|
506
712
|
const [config, setConfig] = useState2(providedConfig);
|
|
507
713
|
const [configError, setConfigError] = useState2(void 0);
|
|
@@ -528,9 +734,10 @@ function FrankieProvider({
|
|
|
528
734
|
...configError ? { configError } : {},
|
|
529
735
|
loading,
|
|
530
736
|
...context ? { context } : {},
|
|
531
|
-
...theme ? { theme } : {}
|
|
737
|
+
...theme ? { theme } : {},
|
|
738
|
+
injectStyles
|
|
532
739
|
}),
|
|
533
|
-
[client, config, configError, loading, context, theme]
|
|
740
|
+
[client, config, configError, loading, context, theme, injectStyles]
|
|
534
741
|
);
|
|
535
742
|
return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children: /* @__PURE__ */ jsx(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
|
|
536
743
|
}
|
|
@@ -545,12 +752,82 @@ function useFrankie() {
|
|
|
545
752
|
}
|
|
546
753
|
function useFrankieTheme() {
|
|
547
754
|
const { config, theme } = useFrankie();
|
|
548
|
-
|
|
755
|
+
const merged = useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
756
|
+
const inheriting = merged?.source === "inherit";
|
|
757
|
+
const [inferred, setInferred] = useState2(null);
|
|
758
|
+
useEffect2(() => {
|
|
759
|
+
setInferred(inheriting ? inferHostTheme() : null);
|
|
760
|
+
}, [inheriting]);
|
|
761
|
+
return useMemo(() => {
|
|
762
|
+
if (!inheriting || !inferred) return merged;
|
|
763
|
+
return { ...merged, ...inferred };
|
|
764
|
+
}, [merged, inheriting, inferred]);
|
|
549
765
|
}
|
|
550
766
|
|
|
551
767
|
// src/components/chat/FrankieWidget.tsx
|
|
552
768
|
import { MessageCircle, X as X4 } from "lucide-react";
|
|
553
|
-
import { useEffect as
|
|
769
|
+
import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
770
|
+
|
|
771
|
+
// src/lib/styles-ready.ts
|
|
772
|
+
import { useEffect as useEffect3, useLayoutEffect, useState as useState3 } from "react";
|
|
773
|
+
|
|
774
|
+
// src/generated/css-content.ts
|
|
775
|
+
var FRANKIE_CSS_CONTENT = `/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
776
|
+
@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{:root,:host{--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-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}}`;
|
|
777
|
+
|
|
778
|
+
// src/lib/ensure-frankie-styles.ts
|
|
779
|
+
var STYLE_ELEMENT_ID = "frankie-fallback-styles";
|
|
780
|
+
var attempted = false;
|
|
781
|
+
function ensureFrankieStyles() {
|
|
782
|
+
if (attempted || typeof document === "undefined") return;
|
|
783
|
+
attempted = true;
|
|
784
|
+
if (document.getElementById(STYLE_ELEMENT_ID)) return;
|
|
785
|
+
const style = document.createElement("style");
|
|
786
|
+
style.id = STYLE_ELEMENT_ID;
|
|
787
|
+
style.textContent = FRANKIE_CSS_CONTENT;
|
|
788
|
+
document.head.appendChild(style);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/lib/styles-ready.ts
|
|
792
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect;
|
|
793
|
+
var SENTINEL = "--frankie-styles-ready";
|
|
794
|
+
function useStylesReady(ref, timeoutMs = 600, injectFallback = true) {
|
|
795
|
+
const [ready, setReady] = useState3(false);
|
|
796
|
+
useIsomorphicLayoutEffect(() => {
|
|
797
|
+
if (ready) return;
|
|
798
|
+
const node = ref.current;
|
|
799
|
+
if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
|
|
800
|
+
setReady(true);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
|
|
804
|
+
if (applied()) {
|
|
805
|
+
setReady(true);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const deadline = Date.now() + timeoutMs;
|
|
809
|
+
let frame = 0;
|
|
810
|
+
const poll = () => {
|
|
811
|
+
if (applied()) {
|
|
812
|
+
setReady(true);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (Date.now() >= deadline) {
|
|
816
|
+
if (injectFallback) ensureFrankieStyles();
|
|
817
|
+
setReady(true);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
frame = window.requestAnimationFrame(poll);
|
|
821
|
+
};
|
|
822
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
823
|
+
setReady(true);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
frame = window.requestAnimationFrame(poll);
|
|
827
|
+
return () => window.cancelAnimationFrame(frame);
|
|
828
|
+
}, [ref, ready, timeoutMs, injectFallback]);
|
|
829
|
+
return ready;
|
|
830
|
+
}
|
|
554
831
|
|
|
555
832
|
// src/lib/theme.ts
|
|
556
833
|
function themeToCssVars(theme) {
|
|
@@ -611,7 +888,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
611
888
|
|
|
612
889
|
// src/components/chat/ChatPanel.tsx
|
|
613
890
|
import { RotateCcw, X as X3 } from "lucide-react";
|
|
614
|
-
import { useEffect as
|
|
891
|
+
import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
|
|
615
892
|
|
|
616
893
|
// src/components/ui/avatar.tsx
|
|
617
894
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -717,7 +994,7 @@ function AgentContext() {
|
|
|
717
994
|
|
|
718
995
|
// src/components/chat/Composer.tsx
|
|
719
996
|
import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
|
|
720
|
-
import { useCallback as useCallback2, useEffect as
|
|
997
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
|
|
721
998
|
|
|
722
999
|
// src/components/ui/textarea.tsx
|
|
723
1000
|
import { forwardRef as forwardRef2 } from "react";
|
|
@@ -741,15 +1018,22 @@ var Textarea = forwardRef2(
|
|
|
741
1018
|
// src/components/chat/Composer.tsx
|
|
742
1019
|
import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
743
1020
|
var MAX_ROWS_PX = 140;
|
|
744
|
-
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
745
|
-
const [value, setValue] =
|
|
746
|
-
const [files, setFiles] =
|
|
747
|
-
const [fileError, setFileError] =
|
|
1021
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
|
|
1022
|
+
const [value, setValue] = useState4("");
|
|
1023
|
+
const [files, setFiles] = useState4([]);
|
|
1024
|
+
const [fileError, setFileError] = useState4(void 0);
|
|
748
1025
|
const textareaRef = useRef3(null);
|
|
749
1026
|
const fileInputRef = useRef3(null);
|
|
1027
|
+
useEffect4(() => {
|
|
1028
|
+
if (!focusHandleRef) return;
|
|
1029
|
+
focusHandleRef.current = () => textareaRef.current?.focus();
|
|
1030
|
+
return () => {
|
|
1031
|
+
focusHandleRef.current = null;
|
|
1032
|
+
};
|
|
1033
|
+
}, [focusHandleRef]);
|
|
750
1034
|
const canAttach = Boolean(attachments?.enabled);
|
|
751
1035
|
const models = modelPicker?.models ?? [];
|
|
752
|
-
|
|
1036
|
+
useEffect4(() => {
|
|
753
1037
|
const node = textareaRef.current;
|
|
754
1038
|
if (!node) return;
|
|
755
1039
|
node.style.height = "auto";
|
|
@@ -882,19 +1166,22 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
882
1166
|
"aria-label": "Message"
|
|
883
1167
|
}
|
|
884
1168
|
),
|
|
885
|
-
|
|
1169
|
+
busy ? /* @__PURE__ */ jsx8(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx8(Square, {}) }) : /* @__PURE__ */ jsx8(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx8(ArrowUp, {}) })
|
|
1170
|
+
] }),
|
|
1171
|
+
models.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "frankie-composer-meta", children: /* @__PURE__ */ jsxs2("label", { className: "frankie-model-picker", title: "Model", children: [
|
|
1172
|
+
/* @__PURE__ */ jsx8("span", { className: "frankie-sr-only", children: "Model" }),
|
|
1173
|
+
/* @__PURE__ */ jsx8(
|
|
886
1174
|
"select",
|
|
887
1175
|
{
|
|
888
1176
|
value: modelPicker?.value ?? models[0]?.id,
|
|
889
1177
|
onChange: (event) => modelPicker?.onChange(event.target.value),
|
|
890
1178
|
disabled,
|
|
891
1179
|
"aria-label": "Model",
|
|
892
|
-
className: "frankie-
|
|
1180
|
+
className: "frankie-model-select",
|
|
893
1181
|
children: models.map((model) => /* @__PURE__ */ jsx8("option", { value: model.id, children: model.label }, model.id))
|
|
894
1182
|
}
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
] })
|
|
1183
|
+
)
|
|
1184
|
+
] }) }) : null
|
|
898
1185
|
]
|
|
899
1186
|
}
|
|
900
1187
|
);
|
|
@@ -919,14 +1206,76 @@ function readAsBase64(file) {
|
|
|
919
1206
|
|
|
920
1207
|
// src/components/chat/MessageBubble.tsx
|
|
921
1208
|
import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
|
|
922
|
-
import { useState as
|
|
1209
|
+
import { memo as memo2, useState as useState5 } from "react";
|
|
923
1210
|
|
|
924
|
-
// src/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
function
|
|
928
|
-
|
|
1211
|
+
// src/lib/choices.ts
|
|
1212
|
+
var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
|
|
1213
|
+
var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
|
|
1214
|
+
function extractChoices(text) {
|
|
1215
|
+
const closed = CLOSED.exec(text);
|
|
1216
|
+
if (closed) {
|
|
1217
|
+
const options = parseOptionLines(closed[1] ?? "");
|
|
1218
|
+
const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1219
|
+
return { body, options: options.length ? options : null };
|
|
1220
|
+
}
|
|
1221
|
+
const open = OPEN.exec(text);
|
|
1222
|
+
if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
|
|
1223
|
+
return { body: text, options: null };
|
|
1224
|
+
}
|
|
1225
|
+
function parseOptionLines(block) {
|
|
1226
|
+
return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/components/chat/Choices.tsx
|
|
1230
|
+
import { ArrowRight, PencilLine } from "lucide-react";
|
|
1231
|
+
import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1232
|
+
function Choices({
|
|
1233
|
+
options,
|
|
1234
|
+
onPick,
|
|
1235
|
+
onSomethingElse,
|
|
1236
|
+
disabled
|
|
1237
|
+
}) {
|
|
1238
|
+
if (options.length === 0) return null;
|
|
1239
|
+
return /* @__PURE__ */ jsxs3("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
|
|
1240
|
+
options.map((option, index) => /* @__PURE__ */ jsxs3(
|
|
1241
|
+
"button",
|
|
1242
|
+
{
|
|
1243
|
+
type: "button",
|
|
1244
|
+
className: "frankie-choice",
|
|
1245
|
+
disabled,
|
|
1246
|
+
onClick: () => onPick(option),
|
|
1247
|
+
children: [
|
|
1248
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
|
|
1249
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: option }),
|
|
1250
|
+
/* @__PURE__ */ jsx9(ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
|
|
1251
|
+
]
|
|
1252
|
+
},
|
|
1253
|
+
`${index}-${option}`
|
|
1254
|
+
)),
|
|
1255
|
+
onSomethingElse ? /* @__PURE__ */ jsxs3(
|
|
1256
|
+
"button",
|
|
1257
|
+
{
|
|
1258
|
+
type: "button",
|
|
1259
|
+
className: "frankie-choice frankie-choice-other",
|
|
1260
|
+
disabled,
|
|
1261
|
+
onClick: onSomethingElse,
|
|
1262
|
+
children: [
|
|
1263
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ jsx9(PencilLine, { className: "size-3.5" }) }),
|
|
1264
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
|
|
1265
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
|
|
1266
|
+
]
|
|
1267
|
+
}
|
|
1268
|
+
) : null
|
|
1269
|
+
] });
|
|
929
1270
|
}
|
|
1271
|
+
|
|
1272
|
+
// src/components/chat/Markdown.tsx
|
|
1273
|
+
import { Fragment as Fragment2, memo, useMemo as useMemo2 } from "react";
|
|
1274
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1275
|
+
var Markdown = memo(function Markdown2({ text, className }) {
|
|
1276
|
+
const blocks = useMemo2(() => renderBlocks(text), [text]);
|
|
1277
|
+
return /* @__PURE__ */ jsx10("div", { className: cn("frankie-prose", className), children: blocks });
|
|
1278
|
+
});
|
|
930
1279
|
function renderBlocks(text) {
|
|
931
1280
|
const lines = text.split("\n");
|
|
932
1281
|
const blocks = [];
|
|
@@ -935,13 +1284,13 @@ function renderBlocks(text) {
|
|
|
935
1284
|
let code = null;
|
|
936
1285
|
const flushParagraph = () => {
|
|
937
1286
|
if (paragraph.length === 0) return;
|
|
938
|
-
blocks.push(/* @__PURE__ */
|
|
1287
|
+
blocks.push(/* @__PURE__ */ jsx10("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
939
1288
|
paragraph = [];
|
|
940
1289
|
};
|
|
941
1290
|
const flushList = () => {
|
|
942
1291
|
if (!list) return;
|
|
943
|
-
const items = list.items.map((item, index) => /* @__PURE__ */
|
|
944
|
-
blocks.push(list.ordered ? /* @__PURE__ */
|
|
1292
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ jsx10("li", { children: renderInline(item) }, index));
|
|
1293
|
+
blocks.push(list.ordered ? /* @__PURE__ */ jsx10("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx10("ul", { children: items }, `l${blocks.length}`));
|
|
945
1294
|
list = null;
|
|
946
1295
|
};
|
|
947
1296
|
for (const line of lines) {
|
|
@@ -949,7 +1298,7 @@ function renderBlocks(text) {
|
|
|
949
1298
|
if (fence) {
|
|
950
1299
|
if (code) {
|
|
951
1300
|
blocks.push(
|
|
952
|
-
/* @__PURE__ */
|
|
1301
|
+
/* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
953
1302
|
);
|
|
954
1303
|
code = null;
|
|
955
1304
|
} else {
|
|
@@ -980,11 +1329,13 @@ function renderBlocks(text) {
|
|
|
980
1329
|
flushList();
|
|
981
1330
|
continue;
|
|
982
1331
|
}
|
|
983
|
-
const heading =
|
|
1332
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
984
1333
|
if (heading) {
|
|
985
1334
|
flushParagraph();
|
|
986
1335
|
flushList();
|
|
987
|
-
|
|
1336
|
+
const level = (heading[1] ?? "#").length;
|
|
1337
|
+
const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
|
|
1338
|
+
blocks.push(/* @__PURE__ */ jsx10(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
|
|
988
1339
|
continue;
|
|
989
1340
|
}
|
|
990
1341
|
flushList();
|
|
@@ -992,7 +1343,7 @@ function renderBlocks(text) {
|
|
|
992
1343
|
}
|
|
993
1344
|
if (code) {
|
|
994
1345
|
blocks.push(
|
|
995
|
-
/* @__PURE__ */
|
|
1346
|
+
/* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
996
1347
|
);
|
|
997
1348
|
}
|
|
998
1349
|
flushParagraph();
|
|
@@ -1007,28 +1358,28 @@ function renderInline(text) {
|
|
|
1007
1358
|
const token = match[0];
|
|
1008
1359
|
const start = match.index;
|
|
1009
1360
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
1010
|
-
nodes.push(/* @__PURE__ */
|
|
1361
|
+
nodes.push(/* @__PURE__ */ jsx10(Fragment2, { children: renderToken(token) }, start));
|
|
1011
1362
|
lastIndex = start + token.length;
|
|
1012
1363
|
}
|
|
1013
1364
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
1014
1365
|
return nodes;
|
|
1015
1366
|
}
|
|
1016
1367
|
function renderToken(token) {
|
|
1017
|
-
if (token.startsWith("**")) return /* @__PURE__ */
|
|
1018
|
-
if (token.startsWith("`")) return /* @__PURE__ */
|
|
1368
|
+
if (token.startsWith("**")) return /* @__PURE__ */ jsx10("strong", { children: token.slice(2, -2) });
|
|
1369
|
+
if (token.startsWith("`")) return /* @__PURE__ */ jsx10("code", { children: token.slice(1, -1) });
|
|
1019
1370
|
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
1020
1371
|
const href = link ? link[2] ?? "" : token;
|
|
1021
1372
|
const label = link ? link[1] ?? "" : token;
|
|
1022
1373
|
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
1023
|
-
return /* @__PURE__ */
|
|
1374
|
+
return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
1024
1375
|
}
|
|
1025
1376
|
|
|
1026
1377
|
// src/components/chat/ToolActivityList.tsx
|
|
1027
|
-
import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
1378
|
+
import { Check, ChevronDown, Loader2, ShieldAlert, Wrench, X as X2 } from "lucide-react";
|
|
1028
1379
|
|
|
1029
1380
|
// src/components/ui/badge.tsx
|
|
1030
1381
|
import { cva as cva2 } from "class-variance-authority";
|
|
1031
|
-
import { jsx as
|
|
1382
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1032
1383
|
var badgeVariants = cva2(
|
|
1033
1384
|
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
1034
1385
|
{
|
|
@@ -1048,44 +1399,45 @@ function Badge({
|
|
|
1048
1399
|
variant,
|
|
1049
1400
|
...props
|
|
1050
1401
|
}) {
|
|
1051
|
-
return /* @__PURE__ */
|
|
1402
|
+
return /* @__PURE__ */ jsx11("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
1052
1403
|
}
|
|
1053
1404
|
|
|
1054
1405
|
// src/components/chat/ToolActivityList.tsx
|
|
1055
|
-
import { jsx as
|
|
1406
|
+
import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1056
1407
|
function ToolActivityList({ tools, renderTool }) {
|
|
1057
1408
|
if (tools.length === 0) return null;
|
|
1058
1409
|
const running = tools.some((tool) => tool.status === "running");
|
|
1059
|
-
return /* @__PURE__ */
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
/* @__PURE__ */
|
|
1062
|
-
/* @__PURE__ */
|
|
1063
|
-
/* @__PURE__ */
|
|
1410
|
+
return /* @__PURE__ */ jsxs4("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1411
|
+
/* @__PURE__ */ jsxs4("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: [
|
|
1412
|
+
/* @__PURE__ */ jsx12(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1413
|
+
running ? /* @__PURE__ */ jsx12(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Wrench, { className: "size-3.5", "aria-hidden": "true" }),
|
|
1414
|
+
/* @__PURE__ */ jsx12("span", { children: running ? "Working" : "Activity" }),
|
|
1415
|
+
/* @__PURE__ */ jsxs4("span", { className: "text-[11px]", children: [
|
|
1064
1416
|
"\xB7 ",
|
|
1065
1417
|
tools.length,
|
|
1066
1418
|
" tool call",
|
|
1067
1419
|
tools.length === 1 ? "" : "s"
|
|
1068
1420
|
] })
|
|
1069
1421
|
] }),
|
|
1070
|
-
/* @__PURE__ */
|
|
1422
|
+
/* @__PURE__ */ jsx12("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx12("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx12(ToolActivityRow, { tool }) }, tool.id)) })
|
|
1071
1423
|
] });
|
|
1072
1424
|
}
|
|
1073
1425
|
function ToolActivityRow({ tool }) {
|
|
1074
|
-
return /* @__PURE__ */
|
|
1075
|
-
/* @__PURE__ */
|
|
1076
|
-
/* @__PURE__ */
|
|
1426
|
+
return /* @__PURE__ */ jsxs4(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
1427
|
+
/* @__PURE__ */ jsx12(ToolIcon, { status: tool.status }),
|
|
1428
|
+
/* @__PURE__ */ jsx12("span", { className: "truncate", children: tool.label })
|
|
1077
1429
|
] });
|
|
1078
1430
|
}
|
|
1079
1431
|
function ToolIcon({ status }) {
|
|
1080
1432
|
switch (status) {
|
|
1081
1433
|
case "running":
|
|
1082
|
-
return /* @__PURE__ */
|
|
1434
|
+
return /* @__PURE__ */ jsx12(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
1083
1435
|
case "done":
|
|
1084
|
-
return /* @__PURE__ */
|
|
1436
|
+
return /* @__PURE__ */ jsx12(Check, { className: "size-3", "aria-hidden": "true" });
|
|
1085
1437
|
case "denied":
|
|
1086
|
-
return /* @__PURE__ */
|
|
1438
|
+
return /* @__PURE__ */ jsx12(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
1087
1439
|
default:
|
|
1088
|
-
return /* @__PURE__ */
|
|
1440
|
+
return /* @__PURE__ */ jsx12(X2, { className: "size-3", "aria-hidden": "true" });
|
|
1089
1441
|
}
|
|
1090
1442
|
}
|
|
1091
1443
|
function badgeVariant(status) {
|
|
@@ -1096,47 +1448,56 @@ function badgeVariant(status) {
|
|
|
1096
1448
|
}
|
|
1097
1449
|
|
|
1098
1450
|
// src/components/chat/MessageBubble.tsx
|
|
1099
|
-
import { jsx as
|
|
1100
|
-
|
|
1451
|
+
import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1452
|
+
var MessageBubble = memo2(function MessageBubble2({
|
|
1101
1453
|
message,
|
|
1102
1454
|
assistantName,
|
|
1103
1455
|
avatarUrl,
|
|
1104
1456
|
showToolActivity,
|
|
1105
|
-
renderTool
|
|
1457
|
+
renderTool,
|
|
1458
|
+
onChoose,
|
|
1459
|
+
onRequestInput,
|
|
1460
|
+
active,
|
|
1461
|
+
busy
|
|
1106
1462
|
}) {
|
|
1107
1463
|
if (message.role === "user") {
|
|
1108
|
-
return /* @__PURE__ */
|
|
1109
|
-
message.attachments?.length ? /* @__PURE__ */
|
|
1464
|
+
return /* @__PURE__ */ jsxs5("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
|
|
1465
|
+
message.attachments?.length ? /* @__PURE__ */ jsx13("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs5(
|
|
1110
1466
|
"span",
|
|
1111
1467
|
{
|
|
1112
1468
|
className: "inline-flex max-w-[12rem] items-center gap-1 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
|
|
1113
1469
|
children: [
|
|
1114
|
-
/* @__PURE__ */
|
|
1115
|
-
/* @__PURE__ */
|
|
1470
|
+
/* @__PURE__ */ jsx13(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
|
|
1471
|
+
/* @__PURE__ */ jsx13("span", { className: "truncate", children: file.name })
|
|
1116
1472
|
]
|
|
1117
1473
|
},
|
|
1118
1474
|
`${file.name}-${index}`
|
|
1119
1475
|
)) }) : null,
|
|
1120
|
-
message.text ? /* @__PURE__ */
|
|
1121
|
-
/* @__PURE__ */
|
|
1122
|
-
/* @__PURE__ */
|
|
1476
|
+
message.text ? /* @__PURE__ */ jsxs5("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1477
|
+
/* @__PURE__ */ jsx13("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1478
|
+
/* @__PURE__ */ jsx13(MessageCopyButton, { text: message.text })
|
|
1123
1479
|
] }) : null
|
|
1124
1480
|
] });
|
|
1125
1481
|
}
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1482
|
+
const { body, options } = extractChoices(message.text);
|
|
1483
|
+
const showChoices = Boolean(active && onChoose && options && message.status === "done");
|
|
1484
|
+
const toolsRunning = message.tools.some((tool) => tool.status === "running");
|
|
1485
|
+
const waiting = message.status === "streaming" && body.length === 0 && !toolsRunning;
|
|
1486
|
+
return /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-message flex gap-2", children: [
|
|
1487
|
+
/* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1488
|
+
/* @__PURE__ */ jsx13(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1489
|
+
body ? /* @__PURE__ */ jsx13(MessageCopyButton, { text: body }) : null
|
|
1131
1490
|
] }),
|
|
1132
|
-
/* @__PURE__ */
|
|
1133
|
-
message.status === "error" ? /* @__PURE__ */
|
|
1134
|
-
|
|
1491
|
+
/* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
|
|
1492
|
+
message.status === "error" ? /* @__PURE__ */ jsx13("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__ */ jsx13(Markdown, { text: body, className: "text-sm" }) : null,
|
|
1493
|
+
showChoices ? /* @__PURE__ */ jsx13(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
|
|
1494
|
+
showToolActivity ? /* @__PURE__ */ jsx13(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null,
|
|
1495
|
+
waiting ? /* @__PURE__ */ jsx13(Thinking, {}) : null
|
|
1135
1496
|
] })
|
|
1136
1497
|
] });
|
|
1137
|
-
}
|
|
1498
|
+
});
|
|
1138
1499
|
function MessageCopyButton({ text }) {
|
|
1139
|
-
const [copied, setCopied] =
|
|
1500
|
+
const [copied, setCopied] = useState5(false);
|
|
1140
1501
|
const copy = async () => {
|
|
1141
1502
|
try {
|
|
1142
1503
|
await navigator.clipboard.writeText(text);
|
|
@@ -1145,12 +1506,12 @@ function MessageCopyButton({ text }) {
|
|
|
1145
1506
|
} catch {
|
|
1146
1507
|
}
|
|
1147
1508
|
};
|
|
1148
|
-
return /* @__PURE__ */
|
|
1509
|
+
return /* @__PURE__ */ jsx13("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__ */ jsx13(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx13(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1149
1510
|
}
|
|
1150
1511
|
function TypingDots() {
|
|
1151
|
-
return /* @__PURE__ */
|
|
1152
|
-
/* @__PURE__ */
|
|
1153
|
-
[0, 1, 2].map((index) => /* @__PURE__ */
|
|
1512
|
+
return /* @__PURE__ */ jsxs5("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
|
|
1513
|
+
/* @__PURE__ */ jsx13("span", { className: "frankie-thinking-label", children: "Thinking" }),
|
|
1514
|
+
[0, 1, 2].map((index) => /* @__PURE__ */ jsx13(
|
|
1154
1515
|
"span",
|
|
1155
1516
|
{
|
|
1156
1517
|
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
@@ -1160,16 +1521,23 @@ function TypingDots() {
|
|
|
1160
1521
|
))
|
|
1161
1522
|
] });
|
|
1162
1523
|
}
|
|
1524
|
+
function Thinking() {
|
|
1525
|
+
return /* @__PURE__ */ jsxs5("div", { className: "frankie-skeleton", role: "status", "aria-label": "Thinking", children: [
|
|
1526
|
+
/* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "62%" } }),
|
|
1527
|
+
/* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "92%" } }),
|
|
1528
|
+
/* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "48%" } })
|
|
1529
|
+
] });
|
|
1530
|
+
}
|
|
1163
1531
|
|
|
1164
1532
|
// src/components/chat/SuggestedPrompts.tsx
|
|
1165
|
-
import { jsx as
|
|
1533
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1166
1534
|
function SuggestedPrompts({
|
|
1167
1535
|
prompts,
|
|
1168
1536
|
onPick,
|
|
1169
1537
|
disabled
|
|
1170
1538
|
}) {
|
|
1171
1539
|
if (prompts.length === 0) return null;
|
|
1172
|
-
return /* @__PURE__ */
|
|
1540
|
+
return /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx14(
|
|
1173
1541
|
Button,
|
|
1174
1542
|
{
|
|
1175
1543
|
type: "button",
|
|
@@ -1184,7 +1552,7 @@ function SuggestedPrompts({
|
|
|
1184
1552
|
}
|
|
1185
1553
|
|
|
1186
1554
|
// src/components/chat/ChatPanel.tsx
|
|
1187
|
-
import { jsx as
|
|
1555
|
+
import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1188
1556
|
function ChatPanel({
|
|
1189
1557
|
className,
|
|
1190
1558
|
header,
|
|
@@ -1204,8 +1572,10 @@ function ChatPanel({
|
|
|
1204
1572
|
const atBottomRef = useRef4(true);
|
|
1205
1573
|
const features = config?.features;
|
|
1206
1574
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1207
|
-
const [model, setModel] =
|
|
1208
|
-
|
|
1575
|
+
const [model, setModel] = useState6(void 0);
|
|
1576
|
+
const composerFocus = useRef4(null);
|
|
1577
|
+
const focusComposer = () => composerFocus.current?.();
|
|
1578
|
+
useEffect5(() => {
|
|
1209
1579
|
const node = scrollRef.current;
|
|
1210
1580
|
if (!node) return;
|
|
1211
1581
|
const onScroll = () => {
|
|
@@ -1214,14 +1584,14 @@ function ChatPanel({
|
|
|
1214
1584
|
node.addEventListener("scroll", onScroll, { passive: true });
|
|
1215
1585
|
return () => node.removeEventListener("scroll", onScroll);
|
|
1216
1586
|
}, []);
|
|
1217
|
-
|
|
1587
|
+
useEffect5(() => {
|
|
1218
1588
|
const node = scrollRef.current;
|
|
1219
1589
|
if (!node || !atBottomRef.current) return;
|
|
1220
1590
|
node.scrollTop = node.scrollHeight;
|
|
1221
1591
|
}, [messages]);
|
|
1222
1592
|
const name = config?.branding.name ?? "Assistant";
|
|
1223
1593
|
const mode = resolveMode(theme?.mode);
|
|
1224
|
-
return /* @__PURE__ */
|
|
1594
|
+
return /* @__PURE__ */ jsxs6(
|
|
1225
1595
|
Card,
|
|
1226
1596
|
{
|
|
1227
1597
|
className: cn(
|
|
@@ -1234,44 +1604,49 @@ function ChatPanel({
|
|
|
1234
1604
|
style: themeToCssVars(theme),
|
|
1235
1605
|
"data-frankie-mode": mode,
|
|
1236
1606
|
children: [
|
|
1237
|
-
header === false ? null : header !== void 0 ? header : /* @__PURE__ */
|
|
1238
|
-
/* @__PURE__ */
|
|
1239
|
-
/* @__PURE__ */
|
|
1240
|
-
/* @__PURE__ */
|
|
1241
|
-
config?.branding.tagline ? /* @__PURE__ */
|
|
1607
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs6(CardHeader, { children: [
|
|
1608
|
+
/* @__PURE__ */ jsx15(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
1609
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
1610
|
+
/* @__PURE__ */ jsx15(CardTitle, { children: name }),
|
|
1611
|
+
config?.branding.tagline ? /* @__PURE__ */ jsx15(CardDescription, { children: config.branding.tagline }) : null
|
|
1242
1612
|
] }),
|
|
1243
|
-
/* @__PURE__ */
|
|
1244
|
-
onClose ? /* @__PURE__ */
|
|
1613
|
+
/* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx15(RotateCcw, {}) }),
|
|
1614
|
+
onClose ? /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx15(X3, {}) }) : null
|
|
1245
1615
|
] }),
|
|
1246
|
-
/* @__PURE__ */
|
|
1247
|
-
/* @__PURE__ */
|
|
1248
|
-
/* @__PURE__ */
|
|
1249
|
-
/* @__PURE__ */
|
|
1250
|
-
/* @__PURE__ */
|
|
1251
|
-
] }) : loading ? /* @__PURE__ */
|
|
1252
|
-
/* @__PURE__ */
|
|
1253
|
-
/* @__PURE__ */
|
|
1254
|
-
] }) : /* @__PURE__ */
|
|
1255
|
-
(message) => renderMessage ? /* @__PURE__ */
|
|
1616
|
+
/* @__PURE__ */ jsx15(AgentContext, {}),
|
|
1617
|
+
/* @__PURE__ */ jsx15(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx15(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs6("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1618
|
+
/* @__PURE__ */ jsx15("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1619
|
+
/* @__PURE__ */ jsx15("p", { children: configError }),
|
|
1620
|
+
/* @__PURE__ */ jsx15("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1621
|
+
] }) : loading ? /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-4", children: [
|
|
1622
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1623
|
+
/* @__PURE__ */ jsx15(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1624
|
+
] }) : /* @__PURE__ */ jsx15("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
|
|
1625
|
+
(message, index) => renderMessage ? /* @__PURE__ */ jsx15("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx15(
|
|
1256
1626
|
MessageBubble,
|
|
1257
1627
|
{
|
|
1258
1628
|
message,
|
|
1259
1629
|
assistantName: name,
|
|
1260
1630
|
...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
|
|
1261
1631
|
showToolActivity: config?.showToolActivity ?? true,
|
|
1262
|
-
...renderTool ? { renderTool } : {}
|
|
1632
|
+
...renderTool ? { renderTool } : {},
|
|
1633
|
+
onChoose: send,
|
|
1634
|
+
onRequestInput: focusComposer,
|
|
1635
|
+
active: index === messages.length - 1,
|
|
1636
|
+
busy
|
|
1263
1637
|
},
|
|
1264
1638
|
message.id
|
|
1265
1639
|
)
|
|
1266
1640
|
) }) }) }),
|
|
1267
|
-
/* @__PURE__ */
|
|
1268
|
-
error ? /* @__PURE__ */
|
|
1269
|
-
/* @__PURE__ */
|
|
1641
|
+
/* @__PURE__ */ jsxs6(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
|
|
1642
|
+
error ? /* @__PURE__ */ jsx15("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1643
|
+
/* @__PURE__ */ jsx15("div", { className: "w-full", children: /* @__PURE__ */ jsx15(
|
|
1270
1644
|
Composer,
|
|
1271
1645
|
{
|
|
1272
1646
|
onSend: send,
|
|
1273
1647
|
onStop: stop,
|
|
1274
1648
|
busy,
|
|
1649
|
+
focusHandleRef: composerFocus,
|
|
1275
1650
|
disabled: loading || restoring || Boolean(configError),
|
|
1276
1651
|
...placeholder ? { placeholder } : {},
|
|
1277
1652
|
...autoFocus ? { autoFocus } : {},
|
|
@@ -1287,7 +1662,7 @@ function ChatPanel({
|
|
|
1287
1662
|
}
|
|
1288
1663
|
|
|
1289
1664
|
// src/components/chat/FrankieWidget.tsx
|
|
1290
|
-
import { jsx as
|
|
1665
|
+
import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1291
1666
|
function FrankieWidget({
|
|
1292
1667
|
position = "bottom-right",
|
|
1293
1668
|
offset = 24,
|
|
@@ -1298,15 +1673,17 @@ function FrankieWidget({
|
|
|
1298
1673
|
renderLauncher,
|
|
1299
1674
|
...panelProps
|
|
1300
1675
|
}) {
|
|
1301
|
-
const { config } = useFrankie();
|
|
1676
|
+
const { config, injectStyles } = useFrankie();
|
|
1302
1677
|
const theme = useFrankieTheme();
|
|
1303
|
-
const
|
|
1678
|
+
const rootRef = useRef5(null);
|
|
1679
|
+
const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
|
|
1680
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState7(defaultOpen);
|
|
1304
1681
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1305
1682
|
const setOpen = (next) => {
|
|
1306
1683
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
1307
1684
|
onOpenChange?.(next);
|
|
1308
1685
|
};
|
|
1309
|
-
|
|
1686
|
+
useEffect6(() => {
|
|
1310
1687
|
if (!open) return;
|
|
1311
1688
|
const onKeyDown = (event) => {
|
|
1312
1689
|
if (event.key === "Escape") setOpen(false);
|
|
@@ -1317,15 +1694,17 @@ function FrankieWidget({
|
|
|
1317
1694
|
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
1318
1695
|
const mode = resolveMode(theme?.mode);
|
|
1319
1696
|
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
1320
|
-
return /* @__PURE__ */
|
|
1697
|
+
return /* @__PURE__ */ jsx16(
|
|
1321
1698
|
"div",
|
|
1322
1699
|
{
|
|
1700
|
+
ref: rootRef,
|
|
1323
1701
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1324
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1702
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
|
|
1325
1703
|
"data-frankie-mode": mode,
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
/* @__PURE__ */
|
|
1704
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1705
|
+
children: /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1706
|
+
open ? /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1707
|
+
/* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx16(
|
|
1329
1708
|
Button,
|
|
1330
1709
|
{
|
|
1331
1710
|
size: "icon",
|
|
@@ -1334,7 +1713,7 @@ function FrankieWidget({
|
|
|
1334
1713
|
"aria-expanded": open,
|
|
1335
1714
|
"aria-label": open ? "Close the assistant" : label,
|
|
1336
1715
|
title: label,
|
|
1337
|
-
children: open ? /* @__PURE__ */
|
|
1716
|
+
children: open ? /* @__PURE__ */ jsx16(X4, { className: "size-5" }) : /* @__PURE__ */ jsx16(MessageCircle, { className: "size-5" })
|
|
1338
1717
|
}
|
|
1339
1718
|
) })
|
|
1340
1719
|
] })
|
|
@@ -1343,39 +1722,45 @@ function FrankieWidget({
|
|
|
1343
1722
|
}
|
|
1344
1723
|
|
|
1345
1724
|
// src/components/chat/FrankieInline.tsx
|
|
1346
|
-
import {
|
|
1725
|
+
import { useRef as useRef6 } from "react";
|
|
1726
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1347
1727
|
function FrankieInline({ className, style, ...panelProps }) {
|
|
1728
|
+
const { injectStyles } = useFrankie();
|
|
1348
1729
|
const theme = useFrankieTheme();
|
|
1349
1730
|
const mode = resolveMode(theme?.mode);
|
|
1350
|
-
|
|
1731
|
+
const rootRef = useRef6(null);
|
|
1732
|
+
const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
|
|
1733
|
+
return /* @__PURE__ */ jsx17(
|
|
1351
1734
|
"div",
|
|
1352
1735
|
{
|
|
1736
|
+
ref: rootRef,
|
|
1353
1737
|
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1354
|
-
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1738
|
+
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
|
|
1355
1739
|
"data-frankie-mode": mode,
|
|
1356
|
-
|
|
1740
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1741
|
+
children: /* @__PURE__ */ jsx17(ChatPanel, { ...panelProps, fill: true })
|
|
1357
1742
|
}
|
|
1358
1743
|
);
|
|
1359
1744
|
}
|
|
1360
1745
|
|
|
1361
1746
|
// src/components/chat/FrankieChatPage.tsx
|
|
1362
|
-
import { useCallback as useCallback3, useEffect as
|
|
1747
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
|
|
1363
1748
|
import { ChevronsLeft, ChevronsRight, History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
|
|
1364
|
-
import { Fragment as Fragment3, jsx as
|
|
1749
|
+
import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1365
1750
|
function PageEmptyState() {
|
|
1366
1751
|
const { config } = useFrankie();
|
|
1367
1752
|
const { send, busy, restoring } = useFrankieChat();
|
|
1368
1753
|
const name = config?.branding.name ?? "Frankie";
|
|
1369
|
-
return /* @__PURE__ */
|
|
1370
|
-
/* @__PURE__ */
|
|
1371
|
-
/* @__PURE__ */
|
|
1372
|
-
/* @__PURE__ */
|
|
1373
|
-
/* @__PURE__ */
|
|
1374
|
-
/* @__PURE__ */
|
|
1754
|
+
return /* @__PURE__ */ jsxs8("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1755
|
+
/* @__PURE__ */ jsx18("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1756
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1757
|
+
/* @__PURE__ */ jsx18("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1758
|
+
/* @__PURE__ */ jsx18("h1", { children: "How can I help?" }),
|
|
1759
|
+
/* @__PURE__ */ jsx18("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1375
1760
|
] }),
|
|
1376
|
-
/* @__PURE__ */
|
|
1377
|
-
/* @__PURE__ */
|
|
1378
|
-
/* @__PURE__ */
|
|
1761
|
+
/* @__PURE__ */ jsx18("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1762
|
+
/* @__PURE__ */ jsx18("strong", { children: prompt }),
|
|
1763
|
+
/* @__PURE__ */ jsx18("b", { "aria-hidden": true, children: "\u2197" })
|
|
1379
1764
|
] }, prompt)) })
|
|
1380
1765
|
] });
|
|
1381
1766
|
}
|
|
@@ -1383,21 +1768,21 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1383
1768
|
const { client, config } = useFrankie();
|
|
1384
1769
|
const { busy, messages } = useFrankieChat();
|
|
1385
1770
|
const theme = useFrankieTheme();
|
|
1386
|
-
const [conversations, setConversations] =
|
|
1387
|
-
const [historyError, setHistoryError] =
|
|
1388
|
-
const [historyLoading, setHistoryLoading] =
|
|
1389
|
-
const [drawerOpen, setDrawerOpen] =
|
|
1390
|
-
const [collapsed, setCollapsed] =
|
|
1771
|
+
const [conversations, setConversations] = useState8([]);
|
|
1772
|
+
const [historyError, setHistoryError] = useState8("");
|
|
1773
|
+
const [historyLoading, setHistoryLoading] = useState8(false);
|
|
1774
|
+
const [drawerOpen, setDrawerOpen] = useState8(false);
|
|
1775
|
+
const [collapsed, setCollapsed] = useState8(() => {
|
|
1391
1776
|
try {
|
|
1392
1777
|
return window.localStorage.getItem("frankie.history-collapsed") === "1";
|
|
1393
1778
|
} catch {
|
|
1394
1779
|
return false;
|
|
1395
1780
|
}
|
|
1396
1781
|
});
|
|
1397
|
-
const [query, setQuery] =
|
|
1398
|
-
const dialog =
|
|
1399
|
-
const menu =
|
|
1400
|
-
const requestVersion =
|
|
1782
|
+
const [query, setQuery] = useState8("");
|
|
1783
|
+
const dialog = useRef7(null);
|
|
1784
|
+
const menu = useRef7(null);
|
|
1785
|
+
const requestVersion = useRef7(0);
|
|
1401
1786
|
const refresh = useCallback3(async () => {
|
|
1402
1787
|
if (!showHistory || !config || config.allowAnonymous) {
|
|
1403
1788
|
setHistoryError("");
|
|
@@ -1419,19 +1804,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1419
1804
|
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1420
1805
|
}
|
|
1421
1806
|
}, [client, showHistory, config?.allowAnonymous]);
|
|
1422
|
-
|
|
1807
|
+
useEffect7(() => {
|
|
1423
1808
|
if (!busy) void refresh();
|
|
1424
1809
|
return () => {
|
|
1425
1810
|
requestVersion.current++;
|
|
1426
1811
|
};
|
|
1427
1812
|
}, [refresh, busy]);
|
|
1428
|
-
|
|
1813
|
+
useEffect7(() => {
|
|
1429
1814
|
try {
|
|
1430
1815
|
window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
|
|
1431
1816
|
} catch {
|
|
1432
1817
|
}
|
|
1433
1818
|
}, [collapsed]);
|
|
1434
|
-
|
|
1819
|
+
useEffect7(() => {
|
|
1435
1820
|
const node = dialog.current;
|
|
1436
1821
|
if (drawerOpen) {
|
|
1437
1822
|
node?.showModal?.();
|
|
@@ -1449,79 +1834,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1449
1834
|
close();
|
|
1450
1835
|
}
|
|
1451
1836
|
};
|
|
1452
|
-
const history = /* @__PURE__ */
|
|
1453
|
-
/* @__PURE__ */
|
|
1454
|
-
/* @__PURE__ */
|
|
1455
|
-
/* @__PURE__ */
|
|
1837
|
+
const history = /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1838
|
+
/* @__PURE__ */ jsxs8("div", { className: "frankie-history-heading", children: [
|
|
1839
|
+
/* @__PURE__ */ jsx18("strong", { children: "Conversations" }),
|
|
1840
|
+
/* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx18(X5, { size: 18 }) })
|
|
1456
1841
|
] }),
|
|
1457
|
-
/* @__PURE__ */
|
|
1842
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1458
1843
|
onNew();
|
|
1459
1844
|
close();
|
|
1460
1845
|
}, children: [
|
|
1461
|
-
/* @__PURE__ */
|
|
1846
|
+
/* @__PURE__ */ jsx18(Plus2, { size: 17 }),
|
|
1462
1847
|
"New chat"
|
|
1463
1848
|
] }),
|
|
1464
|
-
/* @__PURE__ */
|
|
1465
|
-
/* @__PURE__ */
|
|
1466
|
-
/* @__PURE__ */
|
|
1849
|
+
/* @__PURE__ */ jsxs8("label", { className: "frankie-history-search", children: [
|
|
1850
|
+
/* @__PURE__ */ jsx18("span", { children: "Search conversations" }),
|
|
1851
|
+
/* @__PURE__ */ jsx18("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1467
1852
|
] }),
|
|
1468
|
-
/* @__PURE__ */
|
|
1469
|
-
/* @__PURE__ */
|
|
1470
|
-
/* @__PURE__ */
|
|
1853
|
+
/* @__PURE__ */ jsx18("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1854
|
+
/* @__PURE__ */ jsx18("span", { children: item.title }),
|
|
1855
|
+
/* @__PURE__ */ jsx18("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1471
1856
|
] }, item.id)) }),
|
|
1472
|
-
historyLoading ? /* @__PURE__ */
|
|
1857
|
+
historyLoading ? /* @__PURE__ */ jsx18("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs8("p", { role: "status", children: [
|
|
1473
1858
|
historyError,
|
|
1474
|
-
/* @__PURE__ */
|
|
1475
|
-
] }) : !conversations.length ? /* @__PURE__ */
|
|
1476
|
-
navigation ? /* @__PURE__ */
|
|
1859
|
+
/* @__PURE__ */ jsx18("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1860
|
+
] }) : !conversations.length ? /* @__PURE__ */ jsx18("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1861
|
+
navigation ? /* @__PURE__ */ jsx18("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1477
1862
|
] });
|
|
1478
|
-
return /* @__PURE__ */
|
|
1479
|
-
showHistory ? /* @__PURE__ */
|
|
1480
|
-
/* @__PURE__ */
|
|
1863
|
+
return /* @__PURE__ */ jsxs8("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1864
|
+
showHistory ? /* @__PURE__ */ jsxs8("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
|
|
1865
|
+
/* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx18(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx18(ChevronsLeft, { size: 16 }) }),
|
|
1481
1866
|
collapsed ? null : history
|
|
1482
1867
|
] }) : null,
|
|
1483
|
-
/* @__PURE__ */
|
|
1484
|
-
/* @__PURE__ */
|
|
1485
|
-
/* @__PURE__ */
|
|
1486
|
-
showHistory ? /* @__PURE__ */
|
|
1487
|
-
/* @__PURE__ */
|
|
1868
|
+
/* @__PURE__ */ jsxs8("div", { className: "frankie-page-main", children: [
|
|
1869
|
+
/* @__PURE__ */ jsxs8("header", { className: "frankie-page-header", children: [
|
|
1870
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1871
|
+
showHistory ? /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx18(Menu, { size: 20 }) }) : null,
|
|
1872
|
+
/* @__PURE__ */ jsx18("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
|
|
1488
1873
|
] }),
|
|
1489
|
-
/* @__PURE__ */
|
|
1490
|
-
/* @__PURE__ */
|
|
1491
|
-
/* @__PURE__ */
|
|
1492
|
-
/* @__PURE__ */
|
|
1874
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1875
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1876
|
+
/* @__PURE__ */ jsx18(Plus2, { size: 16 }),
|
|
1877
|
+
/* @__PURE__ */ jsx18("span", { children: "New chat" })
|
|
1493
1878
|
] }),
|
|
1494
|
-
/* @__PURE__ */
|
|
1495
|
-
/* @__PURE__ */
|
|
1496
|
-
/* @__PURE__ */
|
|
1497
|
-
/* @__PURE__ */
|
|
1498
|
-
/* @__PURE__ */
|
|
1499
|
-
/* @__PURE__ */
|
|
1500
|
-
/* @__PURE__ */
|
|
1879
|
+
/* @__PURE__ */ jsxs8("details", { className: "frankie-chat-settings", children: [
|
|
1880
|
+
/* @__PURE__ */ jsx18("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx18(Settings, { size: 19 }) }),
|
|
1881
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1882
|
+
/* @__PURE__ */ jsx18("strong", { children: "Chat settings" }),
|
|
1883
|
+
/* @__PURE__ */ jsx18("p", { children: "History syncs automatically for signed-in users." }),
|
|
1884
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1885
|
+
/* @__PURE__ */ jsx18(History, { size: 16 }),
|
|
1501
1886
|
"Refresh history"
|
|
1502
1887
|
] }),
|
|
1503
|
-
/* @__PURE__ */
|
|
1888
|
+
/* @__PURE__ */ jsx18("small", { children: "Skills are checked at the start of each message." })
|
|
1504
1889
|
] })
|
|
1505
1890
|
] })
|
|
1506
1891
|
] })
|
|
1507
1892
|
] }),
|
|
1508
|
-
/* @__PURE__ */
|
|
1893
|
+
/* @__PURE__ */ jsx18("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx18(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx18(PageEmptyState, {}) }) })
|
|
1509
1894
|
] }),
|
|
1510
|
-
showHistory ? /* @__PURE__ */
|
|
1895
|
+
showHistory ? /* @__PURE__ */ jsx18("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1511
1896
|
if (event.target === event.currentTarget) close();
|
|
1512
|
-
}, children: /* @__PURE__ */
|
|
1897
|
+
}, children: /* @__PURE__ */ jsx18("aside", { children: history }) }) : null
|
|
1513
1898
|
] });
|
|
1514
1899
|
}
|
|
1515
1900
|
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1516
|
-
const [id, setId] =
|
|
1901
|
+
const [id, setId] = useState8(() => provider.conversationId ?? crypto.randomUUID());
|
|
1517
1902
|
const onNew = () => setId(crypto.randomUUID());
|
|
1518
|
-
|
|
1903
|
+
const pageRef = useRef7(null);
|
|
1904
|
+
const stylesReady = useStylesReady(pageRef, void 0, provider.injectStyles);
|
|
1905
|
+
return /* @__PURE__ */ jsx18("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__ */ jsx18(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx18(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1519
1906
|
}
|
|
1520
1907
|
|
|
1521
1908
|
// src/components/ui/separator.tsx
|
|
1522
|
-
import { jsx as
|
|
1909
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1523
1910
|
function Separator({ className }) {
|
|
1524
|
-
return /* @__PURE__ */
|
|
1911
|
+
return /* @__PURE__ */ jsx19("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1525
1912
|
}
|
|
1526
1913
|
export {
|
|
1527
1914
|
Avatar,
|
|
@@ -1534,6 +1921,7 @@ export {
|
|
|
1534
1921
|
CardHeader,
|
|
1535
1922
|
CardTitle,
|
|
1536
1923
|
ChatPanel,
|
|
1924
|
+
Choices,
|
|
1537
1925
|
Composer,
|
|
1538
1926
|
FRANKIE_CLOUD_URL,
|
|
1539
1927
|
FrankieApiError,
|
|
@@ -1548,12 +1936,14 @@ export {
|
|
|
1548
1936
|
Separator,
|
|
1549
1937
|
SuggestedPrompts,
|
|
1550
1938
|
Textarea,
|
|
1939
|
+
Thinking,
|
|
1551
1940
|
ToolActivityList,
|
|
1552
1941
|
ToolActivityRow,
|
|
1553
1942
|
TypingDots,
|
|
1554
1943
|
applyEvent,
|
|
1555
1944
|
buttonVariants,
|
|
1556
1945
|
cn,
|
|
1946
|
+
extractChoices,
|
|
1557
1947
|
newAssistantMessage,
|
|
1558
1948
|
newUserMessage,
|
|
1559
1949
|
readEventStream,
|