@frankieai/react 0.1.5 → 0.2.1
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 +394 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -11
- package/dist/index.d.ts +16 -11
- package/dist/index.js +388 -267
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/AgentContext.tsx +22 -0
- package/src/components/chat/ChatPanel.tsx +10 -7
- package/src/components/chat/Composer.tsx +6 -5
- package/src/components/chat/FrankieChatPage.tsx +73 -102
- package/src/components/chat/FrankieWidget.tsx +7 -3
- package/src/components/chat/MessageBubble.tsx +7 -5
- package/src/hooks/FrankieProvider.tsx +12 -4
- package/src/hooks/useFrankieChat.ts +19 -6
- package/src/lib/client.ts +16 -6
- package/src/styles/frankie.css +83 -1
- package/src/widget.test.tsx +64 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/hooks/FrankieProvider.tsx
|
|
4
|
-
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
5
5
|
|
|
6
6
|
// src/lib/client.ts
|
|
7
7
|
var FRANKIE_CLOUD_URL = "https://frankie-data-gq5o5syq2q-uc.a.run.app";
|
|
@@ -20,11 +20,18 @@ var FrankieClient = class {
|
|
|
20
20
|
this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
|
|
21
21
|
this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
22
22
|
this.session = this.readStored();
|
|
23
|
+
if (options.getUserToken && typeof window !== "undefined") {
|
|
24
|
+
try {
|
|
25
|
+
for (const suffix of ["session", "messages"]) window.localStorage.removeItem(`${this.namespace}.${suffix}`);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
options;
|
|
25
31
|
baseUrl;
|
|
26
32
|
doFetch;
|
|
27
33
|
session;
|
|
34
|
+
sessionUserToken;
|
|
28
35
|
get sessionId() {
|
|
29
36
|
return this.session?.sessionId;
|
|
30
37
|
}
|
|
@@ -33,7 +40,7 @@ var FrankieClient = class {
|
|
|
33
40
|
}
|
|
34
41
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
35
42
|
get persist() {
|
|
36
|
-
return this.options.persist !== false && typeof window !== "undefined";
|
|
43
|
+
return !this.options.getUserToken && this.options.persist !== false && typeof window !== "undefined";
|
|
37
44
|
}
|
|
38
45
|
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
39
46
|
get namespace() {
|
|
@@ -66,15 +73,17 @@ var FrankieClient = class {
|
|
|
66
73
|
return await this.unwrap(response);
|
|
67
74
|
}
|
|
68
75
|
async ensureSession(context) {
|
|
69
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
70
76
|
const userToken = await this.options.getUserToken?.();
|
|
71
77
|
if (this.options.getUserToken && !userToken) {
|
|
78
|
+
this.resetSession();
|
|
72
79
|
throw new FrankieApiError(
|
|
73
80
|
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
74
81
|
401,
|
|
75
82
|
"user_token_unavailable"
|
|
76
83
|
);
|
|
77
84
|
}
|
|
85
|
+
if (userToken !== this.sessionUserToken) this.resetSession();
|
|
86
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
78
87
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
79
88
|
method: "POST",
|
|
80
89
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
@@ -85,6 +94,7 @@ var FrankieClient = class {
|
|
|
85
94
|
})
|
|
86
95
|
});
|
|
87
96
|
this.session = await this.unwrap(response);
|
|
97
|
+
this.sessionUserToken = userToken;
|
|
88
98
|
this.writeStored();
|
|
89
99
|
return this.session;
|
|
90
100
|
}
|
|
@@ -97,7 +107,7 @@ var FrankieClient = class {
|
|
|
97
107
|
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
98
108
|
}
|
|
99
109
|
async resumeConversation(id = this.options.conversationId) {
|
|
100
|
-
if (!id) return void 0;
|
|
110
|
+
if (!id || !this.options.getUserToken) return void 0;
|
|
101
111
|
const userToken = await this.requireUserToken();
|
|
102
112
|
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
103
113
|
method: "POST",
|
|
@@ -106,6 +116,7 @@ var FrankieClient = class {
|
|
|
106
116
|
if (response.status === 404) return void 0;
|
|
107
117
|
const body = await this.unwrap(response);
|
|
108
118
|
this.session = body.session;
|
|
119
|
+
this.sessionUserToken = userToken;
|
|
109
120
|
this.writeStored();
|
|
110
121
|
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
111
122
|
}
|
|
@@ -212,64 +223,8 @@ function parseFrame(frame) {
|
|
|
212
223
|
}
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
// src/hooks/FrankieProvider.tsx
|
|
216
|
-
import { jsx } from "react/jsx-runtime";
|
|
217
|
-
var FrankieContext = createContext(null);
|
|
218
|
-
function FrankieProvider({
|
|
219
|
-
children,
|
|
220
|
-
context,
|
|
221
|
-
theme,
|
|
222
|
-
config: providedConfig,
|
|
223
|
-
...clientOptions
|
|
224
|
-
}) {
|
|
225
|
-
const client = useMemo(
|
|
226
|
-
() => new FrankieClient(clientOptions),
|
|
227
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
228
|
-
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
|
|
229
|
-
);
|
|
230
|
-
const [config, setConfig] = useState(providedConfig);
|
|
231
|
-
const [configError, setConfigError] = useState(void 0);
|
|
232
|
-
const [loading, setLoading] = useState(!providedConfig);
|
|
233
|
-
useEffect(() => {
|
|
234
|
-
if (providedConfig) return;
|
|
235
|
-
const abort = new AbortController();
|
|
236
|
-
setLoading(true);
|
|
237
|
-
client.getConfig(abort.signal).then((next) => {
|
|
238
|
-
setConfig(next);
|
|
239
|
-
setConfigError(void 0);
|
|
240
|
-
}).catch((error) => {
|
|
241
|
-
if (abort.signal.aborted) return;
|
|
242
|
-
setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
|
|
243
|
-
}).finally(() => {
|
|
244
|
-
if (!abort.signal.aborted) setLoading(false);
|
|
245
|
-
});
|
|
246
|
-
return () => abort.abort();
|
|
247
|
-
}, [client, providedConfig]);
|
|
248
|
-
const value = useMemo(
|
|
249
|
-
() => ({
|
|
250
|
-
client,
|
|
251
|
-
...config ? { config } : {},
|
|
252
|
-
...configError ? { configError } : {},
|
|
253
|
-
loading,
|
|
254
|
-
...context ? { context } : {},
|
|
255
|
-
...theme ? { theme } : {}
|
|
256
|
-
}),
|
|
257
|
-
[client, config, configError, loading, context, theme]
|
|
258
|
-
);
|
|
259
|
-
return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children });
|
|
260
|
-
}
|
|
261
|
-
function useFrankie() {
|
|
262
|
-
const value = useContext(FrankieContext);
|
|
263
|
-
if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
|
|
264
|
-
return value;
|
|
265
|
-
}
|
|
266
|
-
function useFrankieTheme() {
|
|
267
|
-
const { config, theme } = useFrankie();
|
|
268
|
-
return useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
226
|
// src/hooks/useFrankieChat.ts
|
|
272
|
-
import { useCallback, useEffect
|
|
227
|
+
import { createContext, useContext, useCallback, useEffect, useRef, useState } from "react";
|
|
273
228
|
|
|
274
229
|
// src/lib/messages.ts
|
|
275
230
|
function applyEvent(message, event) {
|
|
@@ -346,16 +301,23 @@ function newUserMessage(id, text, attachments) {
|
|
|
346
301
|
// src/hooks/useFrankieChat.ts
|
|
347
302
|
var counter = 0;
|
|
348
303
|
var nextId = (prefix) => `${prefix}_${Date.now().toString(36)}_${counter += 1}`;
|
|
304
|
+
var FrankieChatContext = createContext(null);
|
|
349
305
|
function useFrankieChat() {
|
|
306
|
+
const value = useContext(FrankieChatContext);
|
|
307
|
+
if (!value) throw new Error("useFrankieChat requires FrankieProvider.");
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
function useFrankieChatState() {
|
|
350
311
|
const { client, context } = useFrankie();
|
|
351
|
-
const [messages, setMessages] =
|
|
352
|
-
const [busy, setBusy] =
|
|
353
|
-
const [error, setError] =
|
|
354
|
-
const [restored, setRestored] =
|
|
312
|
+
const [messages, setMessages] = useState([]);
|
|
313
|
+
const [busy, setBusy] = useState(false);
|
|
314
|
+
const [error, setError] = useState(void 0);
|
|
315
|
+
const [restored, setRestored] = useState(false);
|
|
316
|
+
const [loadedSkills, setLoadedSkills] = useState(null);
|
|
355
317
|
const abortRef = useRef(null);
|
|
356
318
|
const mounted = useRef(true);
|
|
357
319
|
const messagesRef = useRef([]);
|
|
358
|
-
|
|
320
|
+
useEffect(() => {
|
|
359
321
|
mounted.current = true;
|
|
360
322
|
return () => {
|
|
361
323
|
mounted.current = false;
|
|
@@ -363,7 +325,7 @@ function useFrankieChat() {
|
|
|
363
325
|
};
|
|
364
326
|
}, []);
|
|
365
327
|
const storageKey = `${client.namespace}.messages`;
|
|
366
|
-
|
|
328
|
+
useEffect(() => {
|
|
367
329
|
let active = true;
|
|
368
330
|
const restore = async () => {
|
|
369
331
|
if (client.persist) {
|
|
@@ -385,6 +347,7 @@ function useFrankieChat() {
|
|
|
385
347
|
setMessages(restoredMessages);
|
|
386
348
|
}
|
|
387
349
|
} catch {
|
|
350
|
+
if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
|
|
388
351
|
} finally {
|
|
389
352
|
if (active) setRestored(true);
|
|
390
353
|
}
|
|
@@ -394,7 +357,7 @@ function useFrankieChat() {
|
|
|
394
357
|
active = false;
|
|
395
358
|
};
|
|
396
359
|
}, [client, storageKey]);
|
|
397
|
-
|
|
360
|
+
useEffect(() => {
|
|
398
361
|
if (!client.persist || !restored || busy) return;
|
|
399
362
|
try {
|
|
400
363
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
@@ -414,10 +377,11 @@ function useFrankieChat() {
|
|
|
414
377
|
async (text, opts) => {
|
|
415
378
|
const trimmed = text.trim();
|
|
416
379
|
const attachments = opts?.attachments ?? [];
|
|
417
|
-
if (!trimmed && attachments.length === 0 ||
|
|
380
|
+
if (!trimmed && attachments.length === 0 || abortRef.current || !restored) return;
|
|
418
381
|
const outgoing = trimmed || "Please take a look at the attached file(s).";
|
|
419
382
|
const assistantId = nextId("a");
|
|
420
383
|
setError(void 0);
|
|
384
|
+
setLoadedSkills(null);
|
|
421
385
|
setBusy(true);
|
|
422
386
|
const next = [
|
|
423
387
|
...messagesRef.current,
|
|
@@ -444,6 +408,10 @@ function useFrankieChat() {
|
|
|
444
408
|
signal: abort.signal
|
|
445
409
|
},
|
|
446
410
|
(event) => {
|
|
411
|
+
if (event.type === "skills_loaded") {
|
|
412
|
+
setLoadedSkills(event.skills);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
447
415
|
if (event.type === "ping" || event.type === "session") return;
|
|
448
416
|
if (event.type === "text_delta") receivedText = true;
|
|
449
417
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
@@ -482,7 +450,7 @@ function useFrankieChat() {
|
|
|
482
450
|
abortRef.current = null;
|
|
483
451
|
}
|
|
484
452
|
},
|
|
485
|
-
[busy, client, context, patchAssistant]
|
|
453
|
+
[busy, client, context, patchAssistant, restored]
|
|
486
454
|
);
|
|
487
455
|
const stop = useCallback(() => abortRef.current?.abort(), []);
|
|
488
456
|
const reset = useCallback(() => {
|
|
@@ -493,12 +461,13 @@ function useFrankieChat() {
|
|
|
493
461
|
setError(void 0);
|
|
494
462
|
setBusy(false);
|
|
495
463
|
setRestored(true);
|
|
464
|
+
setLoadedSkills(null);
|
|
496
465
|
try {
|
|
497
466
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
498
467
|
} catch {
|
|
499
468
|
}
|
|
500
469
|
}, [client]);
|
|
501
|
-
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
470
|
+
return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
|
|
502
471
|
}
|
|
503
472
|
function conversationTitle(messages) {
|
|
504
473
|
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
@@ -507,6 +476,68 @@ function conversationTitle(messages) {
|
|
|
507
476
|
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
508
477
|
}
|
|
509
478
|
|
|
479
|
+
// src/hooks/FrankieProvider.tsx
|
|
480
|
+
import { jsx } from "react/jsx-runtime";
|
|
481
|
+
var FrankieContext = createContext2(null);
|
|
482
|
+
function FrankieProvider({
|
|
483
|
+
children,
|
|
484
|
+
context,
|
|
485
|
+
theme,
|
|
486
|
+
config: providedConfig,
|
|
487
|
+
...clientOptions
|
|
488
|
+
}) {
|
|
489
|
+
const tokenCallback = useRef2(clientOptions.getUserToken);
|
|
490
|
+
tokenCallback.current = clientOptions.getUserToken;
|
|
491
|
+
const client = useMemo(
|
|
492
|
+
() => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
|
|
493
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
494
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
|
|
495
|
+
);
|
|
496
|
+
const [config, setConfig] = useState2(providedConfig);
|
|
497
|
+
const [configError, setConfigError] = useState2(void 0);
|
|
498
|
+
const [loading, setLoading] = useState2(!providedConfig);
|
|
499
|
+
useEffect2(() => {
|
|
500
|
+
if (providedConfig) return;
|
|
501
|
+
const abort = new AbortController();
|
|
502
|
+
setLoading(true);
|
|
503
|
+
client.getConfig(abort.signal).then((next) => {
|
|
504
|
+
setConfig(next);
|
|
505
|
+
setConfigError(void 0);
|
|
506
|
+
}).catch((error) => {
|
|
507
|
+
if (abort.signal.aborted) return;
|
|
508
|
+
setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
|
|
509
|
+
}).finally(() => {
|
|
510
|
+
if (!abort.signal.aborted) setLoading(false);
|
|
511
|
+
});
|
|
512
|
+
return () => abort.abort();
|
|
513
|
+
}, [client, providedConfig]);
|
|
514
|
+
const value = useMemo(
|
|
515
|
+
() => ({
|
|
516
|
+
client,
|
|
517
|
+
...config ? { config } : {},
|
|
518
|
+
...configError ? { configError } : {},
|
|
519
|
+
loading,
|
|
520
|
+
...context ? { context } : {},
|
|
521
|
+
...theme ? { theme } : {}
|
|
522
|
+
}),
|
|
523
|
+
[client, config, configError, loading, context, theme]
|
|
524
|
+
);
|
|
525
|
+
return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children: /* @__PURE__ */ jsx(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
|
|
526
|
+
}
|
|
527
|
+
function ConversationState({ children }) {
|
|
528
|
+
const chat = useFrankieChatState();
|
|
529
|
+
return /* @__PURE__ */ jsx(FrankieChatContext.Provider, { value: chat, children });
|
|
530
|
+
}
|
|
531
|
+
function useFrankie() {
|
|
532
|
+
const value = useContext2(FrankieContext);
|
|
533
|
+
if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function useFrankieTheme() {
|
|
537
|
+
const { config, theme } = useFrankie();
|
|
538
|
+
return useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
539
|
+
}
|
|
540
|
+
|
|
510
541
|
// src/components/chat/FrankieWidget.tsx
|
|
511
542
|
import { MessageCircle, X as X4 } from "lucide-react";
|
|
512
543
|
import { useEffect as useEffect5, useState as useState6 } from "react";
|
|
@@ -570,7 +601,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
570
601
|
|
|
571
602
|
// src/components/chat/ChatPanel.tsx
|
|
572
603
|
import { RotateCcw, X as X3 } from "lucide-react";
|
|
573
|
-
import { useEffect as useEffect4, useRef as
|
|
604
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
|
|
574
605
|
|
|
575
606
|
// src/components/ui/avatar.tsx
|
|
576
607
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -628,16 +659,51 @@ var ScrollArea = forwardRef(
|
|
|
628
659
|
}
|
|
629
660
|
);
|
|
630
661
|
|
|
662
|
+
// src/components/chat/AgentContext.tsx
|
|
663
|
+
import { ArrowUpRight, Sparkles } from "lucide-react";
|
|
664
|
+
import { jsx as jsx6, jsxs } from "react/jsx-runtime";
|
|
665
|
+
function AgentContext() {
|
|
666
|
+
const { loading, configError } = useFrankie();
|
|
667
|
+
const { loadedSkills, busy, restoring, error } = useFrankieChat();
|
|
668
|
+
const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
|
|
669
|
+
return /* @__PURE__ */ jsxs("div", { className: "frankie-agent-context-wrap", children: [
|
|
670
|
+
/* @__PURE__ */ jsxs("details", { className: "frankie-agent-context", children: [
|
|
671
|
+
/* @__PURE__ */ jsxs("summary", { "aria-label": "Agent context", children: [
|
|
672
|
+
/* @__PURE__ */ jsx6(Sparkles, { size: 17, "aria-hidden": "true" }),
|
|
673
|
+
/* @__PURE__ */ jsx6("span", { children: "Agent context" }),
|
|
674
|
+
/* @__PURE__ */ jsx6("span", { className: "frankie-context-count", children: loadedSkills === null ? "Not checked" : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"}` }),
|
|
675
|
+
/* @__PURE__ */ jsxs("span", { className: "frankie-context-status", "data-active": status === "Active", children: [
|
|
676
|
+
/* @__PURE__ */ jsx6("i", { "aria-hidden": "true" }),
|
|
677
|
+
status
|
|
678
|
+
] }),
|
|
679
|
+
/* @__PURE__ */ jsx6(ArrowUpRight, { size: 15, "aria-hidden": "true" })
|
|
680
|
+
] }),
|
|
681
|
+
/* @__PURE__ */ jsxs("div", { className: "frankie-context-detail", children: [
|
|
682
|
+
/* @__PURE__ */ jsx6("strong", { children: loadedSkills === null ? "Skills are checked when you send a message" : loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn" }),
|
|
683
|
+
loadedSkills?.length ? /* @__PURE__ */ jsx6("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ jsxs("li", { children: [
|
|
684
|
+
/* @__PURE__ */ jsx6("span", { children: skill.name }),
|
|
685
|
+
/* @__PURE__ */ jsxs("small", { children: [
|
|
686
|
+
"v",
|
|
687
|
+
skill.revision
|
|
688
|
+
] })
|
|
689
|
+
] }, skill.id)) }) : /* @__PURE__ */ jsx6("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
|
|
690
|
+
/* @__PURE__ */ jsx6("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
|
|
691
|
+
] })
|
|
692
|
+
] }),
|
|
693
|
+
/* @__PURE__ */ jsx6("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
|
|
694
|
+
] });
|
|
695
|
+
}
|
|
696
|
+
|
|
631
697
|
// src/components/chat/Composer.tsx
|
|
632
|
-
import { ArrowUp, Paperclip, Square, X } from "lucide-react";
|
|
633
|
-
import { useCallback as useCallback2, useEffect as useEffect3, useRef as
|
|
698
|
+
import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
|
|
699
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
634
700
|
|
|
635
701
|
// src/components/ui/textarea.tsx
|
|
636
702
|
import { forwardRef as forwardRef2 } from "react";
|
|
637
|
-
import { jsx as
|
|
703
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
638
704
|
var Textarea = forwardRef2(
|
|
639
705
|
function Textarea2({ className, ...props }, ref) {
|
|
640
|
-
return /* @__PURE__ */
|
|
706
|
+
return /* @__PURE__ */ jsx7(
|
|
641
707
|
"textarea",
|
|
642
708
|
{
|
|
643
709
|
ref,
|
|
@@ -652,14 +718,14 @@ var Textarea = forwardRef2(
|
|
|
652
718
|
);
|
|
653
719
|
|
|
654
720
|
// src/components/chat/Composer.tsx
|
|
655
|
-
import { Fragment, jsx as
|
|
721
|
+
import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
656
722
|
var MAX_ROWS_PX = 140;
|
|
657
723
|
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
658
724
|
const [value, setValue] = useState3("");
|
|
659
725
|
const [files, setFiles] = useState3([]);
|
|
660
726
|
const [fileError, setFileError] = useState3(void 0);
|
|
661
|
-
const textareaRef =
|
|
662
|
-
const fileInputRef =
|
|
727
|
+
const textareaRef = useRef3(null);
|
|
728
|
+
const fileInputRef = useRef3(null);
|
|
663
729
|
const canAttach = Boolean(attachments?.enabled);
|
|
664
730
|
const models = modelPicker?.models ?? [];
|
|
665
731
|
useEffect3(() => {
|
|
@@ -714,46 +780,46 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
714
780
|
});
|
|
715
781
|
}, [busy, disabled, files, modelPicker?.value, onSend, value]);
|
|
716
782
|
const onKeyDown = (event) => {
|
|
717
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
783
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
718
784
|
event.preventDefault();
|
|
719
785
|
submit();
|
|
720
786
|
}
|
|
721
787
|
};
|
|
722
788
|
const nothingToSend = value.trim().length === 0 && files.length === 0;
|
|
723
|
-
return /* @__PURE__ */
|
|
789
|
+
return /* @__PURE__ */ jsxs2(
|
|
724
790
|
"form",
|
|
725
791
|
{
|
|
726
|
-
className: "flex flex-col gap-2",
|
|
792
|
+
className: "frankie-composer flex flex-col gap-2",
|
|
727
793
|
onSubmit: (event) => {
|
|
728
794
|
event.preventDefault();
|
|
729
795
|
submit();
|
|
730
796
|
},
|
|
731
797
|
children: [
|
|
732
|
-
files.length > 0 ? /* @__PURE__ */
|
|
798
|
+
files.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ jsxs2(
|
|
733
799
|
"span",
|
|
734
800
|
{
|
|
735
801
|
className: "inline-flex max-w-[14rem] items-center gap-1.5 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
|
|
736
802
|
children: [
|
|
737
|
-
/* @__PURE__ */
|
|
738
|
-
/* @__PURE__ */
|
|
739
|
-
/* @__PURE__ */
|
|
803
|
+
/* @__PURE__ */ jsx8(Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
804
|
+
/* @__PURE__ */ jsx8("span", { className: "truncate", children: file.name }),
|
|
805
|
+
/* @__PURE__ */ jsx8(
|
|
740
806
|
"button",
|
|
741
807
|
{
|
|
742
808
|
type: "button",
|
|
743
809
|
onClick: () => removeFile(file.id),
|
|
744
810
|
"aria-label": `Remove ${file.name}`,
|
|
745
811
|
className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
|
|
746
|
-
children: /* @__PURE__ */
|
|
812
|
+
children: /* @__PURE__ */ jsx8(X, { className: "size-3" })
|
|
747
813
|
}
|
|
748
814
|
)
|
|
749
815
|
]
|
|
750
816
|
},
|
|
751
817
|
file.id
|
|
752
818
|
)) }) : null,
|
|
753
|
-
fileError ? /* @__PURE__ */
|
|
754
|
-
/* @__PURE__ */
|
|
755
|
-
canAttach ? /* @__PURE__ */
|
|
756
|
-
/* @__PURE__ */
|
|
819
|
+
fileError ? /* @__PURE__ */ jsx8("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
|
|
820
|
+
/* @__PURE__ */ jsxs2("div", { className: "frankie-composer-controls flex items-end gap-2", children: [
|
|
821
|
+
canAttach ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
822
|
+
/* @__PURE__ */ jsx8(
|
|
757
823
|
"input",
|
|
758
824
|
{
|
|
759
825
|
ref: fileInputRef,
|
|
@@ -767,7 +833,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
767
833
|
}
|
|
768
834
|
}
|
|
769
835
|
),
|
|
770
|
-
/* @__PURE__ */
|
|
836
|
+
/* @__PURE__ */ jsx8(
|
|
771
837
|
Button,
|
|
772
838
|
{
|
|
773
839
|
type: "button",
|
|
@@ -777,11 +843,11 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
777
843
|
onClick: () => fileInputRef.current?.click(),
|
|
778
844
|
"aria-label": "Attach files",
|
|
779
845
|
title: "Attach files",
|
|
780
|
-
children: /* @__PURE__ */
|
|
846
|
+
children: /* @__PURE__ */ jsx8(Plus, {})
|
|
781
847
|
}
|
|
782
848
|
)
|
|
783
849
|
] }) : null,
|
|
784
|
-
/* @__PURE__ */
|
|
850
|
+
/* @__PURE__ */ jsx8(
|
|
785
851
|
Textarea,
|
|
786
852
|
{
|
|
787
853
|
ref: textareaRef,
|
|
@@ -795,7 +861,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
795
861
|
"aria-label": "Message"
|
|
796
862
|
}
|
|
797
863
|
),
|
|
798
|
-
models.length > 0 ? /* @__PURE__ */
|
|
864
|
+
models.length > 0 ? /* @__PURE__ */ jsx8(
|
|
799
865
|
"select",
|
|
800
866
|
{
|
|
801
867
|
value: modelPicker?.value ?? models[0]?.id,
|
|
@@ -803,10 +869,10 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
803
869
|
disabled,
|
|
804
870
|
"aria-label": "Model",
|
|
805
871
|
className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
|
|
806
|
-
children: models.map((model) => /* @__PURE__ */
|
|
872
|
+
children: models.map((model) => /* @__PURE__ */ jsx8("option", { value: model.id, children: model.label }, model.id))
|
|
807
873
|
}
|
|
808
874
|
) : null,
|
|
809
|
-
busy ? /* @__PURE__ */
|
|
875
|
+
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, {}) })
|
|
810
876
|
] })
|
|
811
877
|
]
|
|
812
878
|
}
|
|
@@ -836,9 +902,9 @@ import { useState as useState4 } from "react";
|
|
|
836
902
|
|
|
837
903
|
// src/components/chat/Markdown.tsx
|
|
838
904
|
import { Fragment as Fragment2 } from "react";
|
|
839
|
-
import { jsx as
|
|
905
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
840
906
|
function Markdown({ text, className }) {
|
|
841
|
-
return /* @__PURE__ */
|
|
907
|
+
return /* @__PURE__ */ jsx9("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
|
|
842
908
|
}
|
|
843
909
|
function renderBlocks(text) {
|
|
844
910
|
const lines = text.split("\n");
|
|
@@ -848,13 +914,13 @@ function renderBlocks(text) {
|
|
|
848
914
|
let code = null;
|
|
849
915
|
const flushParagraph = () => {
|
|
850
916
|
if (paragraph.length === 0) return;
|
|
851
|
-
blocks.push(/* @__PURE__ */
|
|
917
|
+
blocks.push(/* @__PURE__ */ jsx9("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
852
918
|
paragraph = [];
|
|
853
919
|
};
|
|
854
920
|
const flushList = () => {
|
|
855
921
|
if (!list) return;
|
|
856
|
-
const items = list.items.map((item, index) => /* @__PURE__ */
|
|
857
|
-
blocks.push(list.ordered ? /* @__PURE__ */
|
|
922
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ jsx9("li", { children: renderInline(item) }, index));
|
|
923
|
+
blocks.push(list.ordered ? /* @__PURE__ */ jsx9("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx9("ul", { children: items }, `l${blocks.length}`));
|
|
858
924
|
list = null;
|
|
859
925
|
};
|
|
860
926
|
for (const line of lines) {
|
|
@@ -862,7 +928,7 @@ function renderBlocks(text) {
|
|
|
862
928
|
if (fence) {
|
|
863
929
|
if (code) {
|
|
864
930
|
blocks.push(
|
|
865
|
-
/* @__PURE__ */
|
|
931
|
+
/* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
866
932
|
);
|
|
867
933
|
code = null;
|
|
868
934
|
} else {
|
|
@@ -897,7 +963,7 @@ function renderBlocks(text) {
|
|
|
897
963
|
if (heading) {
|
|
898
964
|
flushParagraph();
|
|
899
965
|
flushList();
|
|
900
|
-
blocks.push(/* @__PURE__ */
|
|
966
|
+
blocks.push(/* @__PURE__ */ jsx9("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
|
|
901
967
|
continue;
|
|
902
968
|
}
|
|
903
969
|
flushList();
|
|
@@ -905,7 +971,7 @@ function renderBlocks(text) {
|
|
|
905
971
|
}
|
|
906
972
|
if (code) {
|
|
907
973
|
blocks.push(
|
|
908
|
-
/* @__PURE__ */
|
|
974
|
+
/* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
909
975
|
);
|
|
910
976
|
}
|
|
911
977
|
flushParagraph();
|
|
@@ -920,20 +986,20 @@ function renderInline(text) {
|
|
|
920
986
|
const token = match[0];
|
|
921
987
|
const start = match.index;
|
|
922
988
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
923
|
-
nodes.push(/* @__PURE__ */
|
|
989
|
+
nodes.push(/* @__PURE__ */ jsx9(Fragment2, { children: renderToken(token) }, start));
|
|
924
990
|
lastIndex = start + token.length;
|
|
925
991
|
}
|
|
926
992
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
927
993
|
return nodes;
|
|
928
994
|
}
|
|
929
995
|
function renderToken(token) {
|
|
930
|
-
if (token.startsWith("**")) return /* @__PURE__ */
|
|
931
|
-
if (token.startsWith("`")) return /* @__PURE__ */
|
|
996
|
+
if (token.startsWith("**")) return /* @__PURE__ */ jsx9("strong", { children: token.slice(2, -2) });
|
|
997
|
+
if (token.startsWith("`")) return /* @__PURE__ */ jsx9("code", { children: token.slice(1, -1) });
|
|
932
998
|
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
933
999
|
const href = link ? link[2] ?? "" : token;
|
|
934
1000
|
const label = link ? link[1] ?? "" : token;
|
|
935
1001
|
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
936
|
-
return /* @__PURE__ */
|
|
1002
|
+
return /* @__PURE__ */ jsx9("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
937
1003
|
}
|
|
938
1004
|
|
|
939
1005
|
// src/components/chat/ToolActivityList.tsx
|
|
@@ -941,7 +1007,7 @@ import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react"
|
|
|
941
1007
|
|
|
942
1008
|
// src/components/ui/badge.tsx
|
|
943
1009
|
import { cva as cva2 } from "class-variance-authority";
|
|
944
|
-
import { jsx as
|
|
1010
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
945
1011
|
var badgeVariants = cva2(
|
|
946
1012
|
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
947
1013
|
{
|
|
@@ -961,44 +1027,44 @@ function Badge({
|
|
|
961
1027
|
variant,
|
|
962
1028
|
...props
|
|
963
1029
|
}) {
|
|
964
|
-
return /* @__PURE__ */
|
|
1030
|
+
return /* @__PURE__ */ jsx10("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
965
1031
|
}
|
|
966
1032
|
|
|
967
1033
|
// src/components/chat/ToolActivityList.tsx
|
|
968
|
-
import { jsx as
|
|
1034
|
+
import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
969
1035
|
function ToolActivityList({ tools, renderTool }) {
|
|
970
1036
|
if (tools.length === 0) return null;
|
|
971
1037
|
const running = tools.some((tool) => tool.status === "running");
|
|
972
|
-
return /* @__PURE__ */
|
|
973
|
-
/* @__PURE__ */
|
|
974
|
-
/* @__PURE__ */
|
|
975
|
-
/* @__PURE__ */
|
|
976
|
-
/* @__PURE__ */
|
|
1038
|
+
return /* @__PURE__ */ jsxs3("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1039
|
+
/* @__PURE__ */ jsxs3("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: [
|
|
1040
|
+
/* @__PURE__ */ jsx11(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1041
|
+
/* @__PURE__ */ jsx11("span", { children: running ? "Working" : "Activity" }),
|
|
1042
|
+
/* @__PURE__ */ jsxs3("span", { className: "text-[11px]", children: [
|
|
977
1043
|
"\xB7 ",
|
|
978
1044
|
tools.length,
|
|
979
1045
|
" tool call",
|
|
980
1046
|
tools.length === 1 ? "" : "s"
|
|
981
1047
|
] })
|
|
982
1048
|
] }),
|
|
983
|
-
/* @__PURE__ */
|
|
1049
|
+
/* @__PURE__ */ jsx11("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx11("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx11(ToolActivityRow, { tool }) }, tool.id)) })
|
|
984
1050
|
] });
|
|
985
1051
|
}
|
|
986
1052
|
function ToolActivityRow({ tool }) {
|
|
987
|
-
return /* @__PURE__ */
|
|
988
|
-
/* @__PURE__ */
|
|
989
|
-
/* @__PURE__ */
|
|
1053
|
+
return /* @__PURE__ */ jsxs3(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
1054
|
+
/* @__PURE__ */ jsx11(ToolIcon, { status: tool.status }),
|
|
1055
|
+
/* @__PURE__ */ jsx11("span", { className: "truncate", children: tool.label })
|
|
990
1056
|
] });
|
|
991
1057
|
}
|
|
992
1058
|
function ToolIcon({ status }) {
|
|
993
1059
|
switch (status) {
|
|
994
1060
|
case "running":
|
|
995
|
-
return /* @__PURE__ */
|
|
1061
|
+
return /* @__PURE__ */ jsx11(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
996
1062
|
case "done":
|
|
997
|
-
return /* @__PURE__ */
|
|
1063
|
+
return /* @__PURE__ */ jsx11(Check, { className: "size-3", "aria-hidden": "true" });
|
|
998
1064
|
case "denied":
|
|
999
|
-
return /* @__PURE__ */
|
|
1065
|
+
return /* @__PURE__ */ jsx11(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
1000
1066
|
default:
|
|
1001
|
-
return /* @__PURE__ */
|
|
1067
|
+
return /* @__PURE__ */ jsx11(X2, { className: "size-3", "aria-hidden": "true" });
|
|
1002
1068
|
}
|
|
1003
1069
|
}
|
|
1004
1070
|
function badgeVariant(status) {
|
|
@@ -1009,7 +1075,7 @@ function badgeVariant(status) {
|
|
|
1009
1075
|
}
|
|
1010
1076
|
|
|
1011
1077
|
// src/components/chat/MessageBubble.tsx
|
|
1012
|
-
import { jsx as
|
|
1078
|
+
import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1013
1079
|
function MessageBubble({
|
|
1014
1080
|
message,
|
|
1015
1081
|
assistantName,
|
|
@@ -1018,33 +1084,33 @@ function MessageBubble({
|
|
|
1018
1084
|
renderTool
|
|
1019
1085
|
}) {
|
|
1020
1086
|
if (message.role === "user") {
|
|
1021
|
-
return /* @__PURE__ */
|
|
1022
|
-
message.attachments?.length ? /* @__PURE__ */
|
|
1087
|
+
return /* @__PURE__ */ jsxs4("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
|
|
1088
|
+
message.attachments?.length ? /* @__PURE__ */ jsx12("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs4(
|
|
1023
1089
|
"span",
|
|
1024
1090
|
{
|
|
1025
1091
|
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",
|
|
1026
1092
|
children: [
|
|
1027
|
-
/* @__PURE__ */
|
|
1028
|
-
/* @__PURE__ */
|
|
1093
|
+
/* @__PURE__ */ jsx12(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
|
|
1094
|
+
/* @__PURE__ */ jsx12("span", { className: "truncate", children: file.name })
|
|
1029
1095
|
]
|
|
1030
1096
|
},
|
|
1031
1097
|
`${file.name}-${index}`
|
|
1032
1098
|
)) }) : null,
|
|
1033
|
-
message.text ? /* @__PURE__ */
|
|
1034
|
-
/* @__PURE__ */
|
|
1035
|
-
/* @__PURE__ */
|
|
1099
|
+
message.text ? /* @__PURE__ */ jsxs4("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx12("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1101
|
+
/* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text })
|
|
1036
1102
|
] }) : null
|
|
1037
1103
|
] });
|
|
1038
1104
|
}
|
|
1039
1105
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
1040
|
-
return /* @__PURE__ */
|
|
1041
|
-
/* @__PURE__ */
|
|
1042
|
-
/* @__PURE__ */
|
|
1043
|
-
message.text ? /* @__PURE__ */
|
|
1106
|
+
return /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-message flex gap-2", children: [
|
|
1107
|
+
/* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx12(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1109
|
+
message.text ? /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text }) : null
|
|
1044
1110
|
] }),
|
|
1045
|
-
/* @__PURE__ */
|
|
1046
|
-
message.status === "error" ? /* @__PURE__ */
|
|
1047
|
-
showToolActivity ? /* @__PURE__ */
|
|
1111
|
+
/* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
|
|
1112
|
+
message.status === "error" ? /* @__PURE__ */ jsx12("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ jsx12(TypingDots, {}) : /* @__PURE__ */ jsx12(Markdown, { text: message.text, className: "text-sm" }),
|
|
1113
|
+
showToolActivity ? /* @__PURE__ */ jsx12(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
1048
1114
|
] })
|
|
1049
1115
|
] });
|
|
1050
1116
|
}
|
|
@@ -1058,28 +1124,31 @@ function MessageCopyButton({ text }) {
|
|
|
1058
1124
|
} catch {
|
|
1059
1125
|
}
|
|
1060
1126
|
};
|
|
1061
|
-
return /* @__PURE__ */
|
|
1127
|
+
return /* @__PURE__ */ jsx12("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__ */ jsx12(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1062
1128
|
}
|
|
1063
1129
|
function TypingDots() {
|
|
1064
|
-
return /* @__PURE__ */
|
|
1065
|
-
"span",
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1130
|
+
return /* @__PURE__ */ jsxs4("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
|
|
1131
|
+
/* @__PURE__ */ jsx12("span", { className: "frankie-thinking-label", children: "Thinking" }),
|
|
1132
|
+
[0, 1, 2].map((index) => /* @__PURE__ */ jsx12(
|
|
1133
|
+
"span",
|
|
1134
|
+
{
|
|
1135
|
+
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
1136
|
+
style: { animationDelay: `${index * 150}ms` }
|
|
1137
|
+
},
|
|
1138
|
+
index
|
|
1139
|
+
))
|
|
1140
|
+
] });
|
|
1072
1141
|
}
|
|
1073
1142
|
|
|
1074
1143
|
// src/components/chat/SuggestedPrompts.tsx
|
|
1075
|
-
import { jsx as
|
|
1144
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1076
1145
|
function SuggestedPrompts({
|
|
1077
1146
|
prompts,
|
|
1078
1147
|
onPick,
|
|
1079
1148
|
disabled
|
|
1080
1149
|
}) {
|
|
1081
1150
|
if (prompts.length === 0) return null;
|
|
1082
|
-
return /* @__PURE__ */
|
|
1151
|
+
return /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx13(
|
|
1083
1152
|
Button,
|
|
1084
1153
|
{
|
|
1085
1154
|
type: "button",
|
|
@@ -1094,7 +1163,7 @@ function SuggestedPrompts({
|
|
|
1094
1163
|
}
|
|
1095
1164
|
|
|
1096
1165
|
// src/components/chat/ChatPanel.tsx
|
|
1097
|
-
import { jsx as
|
|
1166
|
+
import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1098
1167
|
function ChatPanel({
|
|
1099
1168
|
className,
|
|
1100
1169
|
header,
|
|
@@ -1109,8 +1178,8 @@ function ChatPanel({
|
|
|
1109
1178
|
}) {
|
|
1110
1179
|
const { config, configError, loading } = useFrankie();
|
|
1111
1180
|
const theme = useFrankieTheme();
|
|
1112
|
-
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
1113
|
-
const scrollRef =
|
|
1181
|
+
const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
|
|
1182
|
+
const scrollRef = useRef4(null);
|
|
1114
1183
|
const features = config?.features;
|
|
1115
1184
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1116
1185
|
const [model, setModel] = useState5(void 0);
|
|
@@ -1122,11 +1191,12 @@ function ChatPanel({
|
|
|
1122
1191
|
}, [messages]);
|
|
1123
1192
|
const name = config?.branding.name ?? "Assistant";
|
|
1124
1193
|
const mode = resolveMode(theme?.mode);
|
|
1125
|
-
return /* @__PURE__ */
|
|
1194
|
+
return /* @__PURE__ */ jsxs5(
|
|
1126
1195
|
Card,
|
|
1127
1196
|
{
|
|
1128
1197
|
className: cn(
|
|
1129
|
-
"frankie-root",
|
|
1198
|
+
"frankie-root frankie-chat-panel",
|
|
1199
|
+
fill && "frankie-chat-panel-fill",
|
|
1130
1200
|
fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
|
|
1131
1201
|
mode === "dark" && "dark",
|
|
1132
1202
|
className
|
|
@@ -1134,24 +1204,25 @@ function ChatPanel({
|
|
|
1134
1204
|
style: themeToCssVars(theme),
|
|
1135
1205
|
"data-frankie-mode": mode,
|
|
1136
1206
|
children: [
|
|
1137
|
-
header === false ? null : header !== void 0 ? header : /* @__PURE__ */
|
|
1138
|
-
/* @__PURE__ */
|
|
1139
|
-
/* @__PURE__ */
|
|
1140
|
-
/* @__PURE__ */
|
|
1141
|
-
config?.branding.tagline ? /* @__PURE__ */
|
|
1207
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs5(CardHeader, { children: [
|
|
1208
|
+
/* @__PURE__ */ jsx14(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
1209
|
+
/* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
|
|
1210
|
+
/* @__PURE__ */ jsx14(CardTitle, { children: name }),
|
|
1211
|
+
config?.branding.tagline ? /* @__PURE__ */ jsx14(CardDescription, { children: config.branding.tagline }) : null
|
|
1142
1212
|
] }),
|
|
1143
|
-
/* @__PURE__ */
|
|
1144
|
-
onClose ? /* @__PURE__ */
|
|
1213
|
+
/* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx14(RotateCcw, {}) }),
|
|
1214
|
+
onClose ? /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx14(X3, {}) }) : null
|
|
1145
1215
|
] }),
|
|
1146
|
-
/* @__PURE__ */
|
|
1147
|
-
|
|
1148
|
-
/* @__PURE__ */
|
|
1149
|
-
/* @__PURE__ */
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
/* @__PURE__ */
|
|
1153
|
-
|
|
1154
|
-
|
|
1216
|
+
/* @__PURE__ */ jsx14(AgentContext, {}),
|
|
1217
|
+
/* @__PURE__ */ jsx14(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx14(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs5("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1218
|
+
/* @__PURE__ */ jsx14("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1219
|
+
/* @__PURE__ */ jsx14("p", { children: configError }),
|
|
1220
|
+
/* @__PURE__ */ jsx14("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1221
|
+
] }) : loading ? /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
|
|
1222
|
+
/* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1223
|
+
/* @__PURE__ */ jsx14(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1224
|
+
] }) : /* @__PURE__ */ jsx14("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
|
|
1225
|
+
(message) => renderMessage ? /* @__PURE__ */ jsx14("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx14(
|
|
1155
1226
|
MessageBubble,
|
|
1156
1227
|
{
|
|
1157
1228
|
message,
|
|
@@ -1163,15 +1234,15 @@ function ChatPanel({
|
|
|
1163
1234
|
message.id
|
|
1164
1235
|
)
|
|
1165
1236
|
) }) }) }),
|
|
1166
|
-
/* @__PURE__ */
|
|
1167
|
-
error ? /* @__PURE__ */
|
|
1168
|
-
/* @__PURE__ */
|
|
1237
|
+
/* @__PURE__ */ jsxs5(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
|
|
1238
|
+
error ? /* @__PURE__ */ jsx14("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1239
|
+
/* @__PURE__ */ jsx14("div", { className: "w-full", children: /* @__PURE__ */ jsx14(
|
|
1169
1240
|
Composer,
|
|
1170
1241
|
{
|
|
1171
1242
|
onSend: send,
|
|
1172
1243
|
onStop: stop,
|
|
1173
1244
|
busy,
|
|
1174
|
-
disabled: Boolean(configError),
|
|
1245
|
+
disabled: loading || restoring || Boolean(configError),
|
|
1175
1246
|
...placeholder ? { placeholder } : {},
|
|
1176
1247
|
...autoFocus ? { autoFocus } : {},
|
|
1177
1248
|
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
@@ -1186,11 +1257,12 @@ function ChatPanel({
|
|
|
1186
1257
|
}
|
|
1187
1258
|
|
|
1188
1259
|
// src/components/chat/FrankieWidget.tsx
|
|
1189
|
-
import { jsx as
|
|
1260
|
+
import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1190
1261
|
function FrankieWidget({
|
|
1191
1262
|
position = "bottom-right",
|
|
1192
1263
|
offset = 24,
|
|
1193
1264
|
defaultOpen = false,
|
|
1265
|
+
zIndex = 20,
|
|
1194
1266
|
open: controlledOpen,
|
|
1195
1267
|
onOpenChange,
|
|
1196
1268
|
renderLauncher,
|
|
@@ -1215,15 +1287,15 @@ function FrankieWidget({
|
|
|
1215
1287
|
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
1216
1288
|
const mode = resolveMode(theme?.mode);
|
|
1217
1289
|
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
1218
|
-
return /* @__PURE__ */
|
|
1290
|
+
return /* @__PURE__ */ jsx15(
|
|
1219
1291
|
"div",
|
|
1220
1292
|
{
|
|
1221
1293
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1222
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex:
|
|
1294
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1223
1295
|
"data-frankie-mode": mode,
|
|
1224
|
-
children: /* @__PURE__ */
|
|
1225
|
-
open ? /* @__PURE__ */
|
|
1226
|
-
renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */
|
|
1296
|
+
children: /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1297
|
+
open ? /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx15(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1298
|
+
/* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx15(
|
|
1227
1299
|
Button,
|
|
1228
1300
|
{
|
|
1229
1301
|
size: "icon",
|
|
@@ -1232,125 +1304,174 @@ function FrankieWidget({
|
|
|
1232
1304
|
"aria-expanded": open,
|
|
1233
1305
|
"aria-label": open ? "Close the assistant" : label,
|
|
1234
1306
|
title: label,
|
|
1235
|
-
children: open ? /* @__PURE__ */
|
|
1307
|
+
children: open ? /* @__PURE__ */ jsx15(X4, { className: "size-5" }) : /* @__PURE__ */ jsx15(MessageCircle, { className: "size-5" })
|
|
1236
1308
|
}
|
|
1237
|
-
)
|
|
1309
|
+
) })
|
|
1238
1310
|
] })
|
|
1239
1311
|
}
|
|
1240
1312
|
);
|
|
1241
1313
|
}
|
|
1242
1314
|
|
|
1243
1315
|
// src/components/chat/FrankieInline.tsx
|
|
1244
|
-
import { jsx as
|
|
1316
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1245
1317
|
function FrankieInline({ className, style, ...panelProps }) {
|
|
1246
1318
|
const theme = useFrankieTheme();
|
|
1247
1319
|
const mode = resolveMode(theme?.mode);
|
|
1248
|
-
return /* @__PURE__ */
|
|
1320
|
+
return /* @__PURE__ */ jsx16(
|
|
1249
1321
|
"div",
|
|
1250
1322
|
{
|
|
1251
1323
|
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1252
1324
|
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1253
1325
|
"data-frankie-mode": mode,
|
|
1254
|
-
children: /* @__PURE__ */
|
|
1326
|
+
children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, fill: true })
|
|
1255
1327
|
}
|
|
1256
1328
|
);
|
|
1257
1329
|
}
|
|
1258
1330
|
|
|
1259
1331
|
// src/components/chat/FrankieChatPage.tsx
|
|
1260
|
-
import {
|
|
1332
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1333
|
+
import { History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
|
|
1334
|
+
import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1261
1335
|
function PageEmptyState() {
|
|
1262
1336
|
const { config } = useFrankie();
|
|
1263
|
-
const { send, busy } = useFrankieChat();
|
|
1337
|
+
const { send, busy, restoring } = useFrankieChat();
|
|
1264
1338
|
const name = config?.branding.name ?? "Frankie";
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
/* @__PURE__ */
|
|
1268
|
-
|
|
1269
|
-
/* @__PURE__ */
|
|
1270
|
-
|
|
1271
|
-
" assistant"
|
|
1272
|
-
] }),
|
|
1273
|
-
/* @__PURE__ */ jsx16("h1", { children: "What would you like to do?" }),
|
|
1274
|
-
/* @__PURE__ */ jsx16("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1339
|
+
return /* @__PURE__ */ jsxs7("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1340
|
+
/* @__PURE__ */ jsx17("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1341
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1342
|
+
/* @__PURE__ */ jsx17("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1343
|
+
/* @__PURE__ */ jsx17("h1", { children: "How can I help?" }),
|
|
1344
|
+
/* @__PURE__ */ jsx17("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1275
1345
|
] }),
|
|
1276
|
-
|
|
1277
|
-
/* @__PURE__ */
|
|
1278
|
-
/* @__PURE__ */
|
|
1279
|
-
|
|
1280
|
-
] }, prompt)) }) : null
|
|
1346
|
+
/* @__PURE__ */ jsx17("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1347
|
+
/* @__PURE__ */ jsx17("strong", { children: prompt }),
|
|
1348
|
+
/* @__PURE__ */ jsx17("b", { "aria-hidden": true, children: "\u2197" })
|
|
1349
|
+
] }, prompt)) })
|
|
1281
1350
|
] });
|
|
1282
1351
|
}
|
|
1283
|
-
function PageChrome({
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1352
|
+
function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
|
|
1353
|
+
const { client, config } = useFrankie();
|
|
1354
|
+
const { busy, messages } = useFrankieChat();
|
|
1355
|
+
const theme = useFrankieTheme();
|
|
1356
|
+
const [conversations, setConversations] = useState7([]);
|
|
1357
|
+
const [historyError, setHistoryError] = useState7("");
|
|
1358
|
+
const [historyLoading, setHistoryLoading] = useState7(false);
|
|
1359
|
+
const [drawerOpen, setDrawerOpen] = useState7(false);
|
|
1360
|
+
const [query, setQuery] = useState7("");
|
|
1361
|
+
const dialog = useRef5(null);
|
|
1362
|
+
const menu = useRef5(null);
|
|
1363
|
+
const requestVersion = useRef5(0);
|
|
1364
|
+
const refresh = useCallback3(async () => {
|
|
1365
|
+
if (!showHistory || !config || config.allowAnonymous) {
|
|
1366
|
+
setHistoryError("");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const version = ++requestVersion.current;
|
|
1370
|
+
setHistoryLoading(true);
|
|
1371
|
+
setHistoryError("");
|
|
1372
|
+
try {
|
|
1373
|
+
const items = await client.listConversations();
|
|
1374
|
+
if (version === requestVersion.current) setConversations(items);
|
|
1375
|
+
} catch {
|
|
1376
|
+
if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
|
|
1377
|
+
} finally {
|
|
1378
|
+
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1379
|
+
}
|
|
1380
|
+
}, [client, showHistory, config?.allowAnonymous]);
|
|
1381
|
+
useEffect6(() => {
|
|
1382
|
+
if (!busy) void refresh();
|
|
1383
|
+
return () => {
|
|
1384
|
+
requestVersion.current++;
|
|
1385
|
+
};
|
|
1386
|
+
}, [refresh, busy]);
|
|
1387
|
+
useEffect6(() => {
|
|
1388
|
+
const node = dialog.current;
|
|
1389
|
+
if (drawerOpen) {
|
|
1390
|
+
node?.showModal?.();
|
|
1391
|
+
} else {
|
|
1392
|
+
node?.close?.();
|
|
1393
|
+
}
|
|
1394
|
+
}, [drawerOpen]);
|
|
1395
|
+
const close = () => {
|
|
1396
|
+
setDrawerOpen(false);
|
|
1397
|
+
menu.current?.focus();
|
|
1398
|
+
};
|
|
1399
|
+
const select = (id) => {
|
|
1400
|
+
if (!busy) {
|
|
1401
|
+
onSelect(id);
|
|
1402
|
+
close();
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const history = /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
1406
|
+
/* @__PURE__ */ jsxs7("div", { className: "frankie-history-heading", children: [
|
|
1407
|
+
/* @__PURE__ */ jsx17("strong", { children: "Conversations" }),
|
|
1408
|
+
/* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx17(X5, { size: 18 }) })
|
|
1409
|
+
] }),
|
|
1410
|
+
/* @__PURE__ */ jsxs7("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1411
|
+
onNew();
|
|
1412
|
+
close();
|
|
1413
|
+
}, children: [
|
|
1414
|
+
/* @__PURE__ */ jsx17(Plus2, { size: 17 }),
|
|
1415
|
+
"New chat"
|
|
1416
|
+
] }),
|
|
1417
|
+
/* @__PURE__ */ jsxs7("label", { className: "frankie-history-search", children: [
|
|
1418
|
+
/* @__PURE__ */ jsx17("span", { children: "Search conversations" }),
|
|
1419
|
+
/* @__PURE__ */ jsx17("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1420
|
+
] }),
|
|
1421
|
+
/* @__PURE__ */ jsx17("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs7("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1422
|
+
/* @__PURE__ */ jsx17("span", { children: item.title }),
|
|
1423
|
+
/* @__PURE__ */ jsx17("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1424
|
+
] }, item.id)) }),
|
|
1425
|
+
historyLoading ? /* @__PURE__ */ jsx17("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs7("p", { role: "status", children: [
|
|
1426
|
+
historyError,
|
|
1427
|
+
/* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1428
|
+
] }) : !conversations.length ? /* @__PURE__ */ jsx17("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1429
|
+
navigation ? /* @__PURE__ */ jsx17("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1430
|
+
] });
|
|
1431
|
+
return /* @__PURE__ */ jsxs7("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1432
|
+
showHistory ? /* @__PURE__ */ jsx17("aside", { className: "frankie-history-desktop", children: history }) : null,
|
|
1433
|
+
/* @__PURE__ */ jsxs7("div", { className: "frankie-page-main", children: [
|
|
1434
|
+
/* @__PURE__ */ jsxs7("header", { className: "frankie-page-header", children: [
|
|
1435
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1436
|
+
showHistory ? /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx17(Menu, { size: 20 }) }) : null,
|
|
1437
|
+
/* @__PURE__ */ jsx17("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
|
|
1438
|
+
] }),
|
|
1439
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1440
|
+
/* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1441
|
+
/* @__PURE__ */ jsx17(Plus2, { size: 16 }),
|
|
1442
|
+
/* @__PURE__ */ jsx17("span", { children: "New chat" })
|
|
1443
|
+
] }),
|
|
1444
|
+
/* @__PURE__ */ jsxs7("details", { className: "frankie-chat-settings", children: [
|
|
1445
|
+
/* @__PURE__ */ jsx17("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx17(Settings, { size: 19 }) }),
|
|
1446
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1447
|
+
/* @__PURE__ */ jsx17("strong", { children: "Chat settings" }),
|
|
1448
|
+
/* @__PURE__ */ jsx17("p", { children: "History syncs automatically for signed-in users." }),
|
|
1449
|
+
/* @__PURE__ */ jsxs7("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1450
|
+
/* @__PURE__ */ jsx17(History, { size: 16 }),
|
|
1451
|
+
"Refresh history"
|
|
1452
|
+
] }),
|
|
1453
|
+
/* @__PURE__ */ jsx17("small", { children: "Skills are checked at the start of each message." })
|
|
1454
|
+
] })
|
|
1455
|
+
] })
|
|
1456
|
+
] })
|
|
1300
1457
|
] }),
|
|
1301
|
-
/* @__PURE__ */
|
|
1458
|
+
/* @__PURE__ */ jsx17("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx17(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx17(PageEmptyState, {}) }) })
|
|
1302
1459
|
] }),
|
|
1303
|
-
/* @__PURE__ */
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
header: header ?? false,
|
|
1307
|
-
footer,
|
|
1308
|
-
emptyState: emptyState ?? /* @__PURE__ */ jsx16(PageEmptyState, {}),
|
|
1309
|
-
renderMessage,
|
|
1310
|
-
renderTool,
|
|
1311
|
-
placeholder,
|
|
1312
|
-
autoFocus
|
|
1313
|
-
}
|
|
1314
|
-
) })
|
|
1460
|
+
showHistory ? /* @__PURE__ */ jsx17("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1461
|
+
if (event.target === event.currentTarget) close();
|
|
1462
|
+
}, children: /* @__PURE__ */ jsx17("aside", { children: history }) }) : null
|
|
1315
1463
|
] });
|
|
1316
1464
|
}
|
|
1317
|
-
function FrankieChatPage({
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
header,
|
|
1321
|
-
footer,
|
|
1322
|
-
emptyState,
|
|
1323
|
-
renderMessage,
|
|
1324
|
-
renderTool,
|
|
1325
|
-
placeholder,
|
|
1326
|
-
autoFocus,
|
|
1327
|
-
...providerProps
|
|
1328
|
-
}) {
|
|
1329
|
-
return /* @__PURE__ */ jsx16(
|
|
1330
|
-
"main",
|
|
1331
|
-
{
|
|
1332
|
-
className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
|
|
1333
|
-
style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
|
|
1334
|
-
children: /* @__PURE__ */ jsx16(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ jsx16(
|
|
1335
|
-
PageChrome,
|
|
1336
|
-
{
|
|
1337
|
-
header,
|
|
1338
|
-
footer,
|
|
1339
|
-
emptyState,
|
|
1340
|
-
renderMessage,
|
|
1341
|
-
renderTool,
|
|
1342
|
-
placeholder,
|
|
1343
|
-
autoFocus
|
|
1344
|
-
}
|
|
1345
|
-
) })
|
|
1346
|
-
}
|
|
1347
|
-
);
|
|
1465
|
+
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1466
|
+
const [id, setId] = useState7(() => provider.conversationId ?? crypto.randomUUID());
|
|
1467
|
+
const onNew = () => setId(crypto.randomUUID());
|
|
1468
|
+
return /* @__PURE__ */ jsx17("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ jsx17(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx17(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1348
1469
|
}
|
|
1349
1470
|
|
|
1350
1471
|
// src/components/ui/separator.tsx
|
|
1351
|
-
import { jsx as
|
|
1472
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1352
1473
|
function Separator({ className }) {
|
|
1353
|
-
return /* @__PURE__ */
|
|
1474
|
+
return /* @__PURE__ */ jsx18("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1354
1475
|
}
|
|
1355
1476
|
export {
|
|
1356
1477
|
Avatar,
|