@frankieai/react 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frankie.css +1 -1
- package/dist/index.cjs +247 -154
- 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 +245 -152
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +3 -2
- package/src/components/chat/FrankieChatPage.tsx +73 -101
- package/src/components/chat/FrankieWidget.tsx +7 -3
- 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 +22 -1
- package/src/widget.test.tsx +35 -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";
|
|
@@ -630,7 +661,7 @@ var ScrollArea = forwardRef(
|
|
|
630
661
|
|
|
631
662
|
// src/components/chat/Composer.tsx
|
|
632
663
|
import { ArrowUp, Paperclip, Square, X } from "lucide-react";
|
|
633
|
-
import { useCallback as useCallback2, useEffect as useEffect3, useRef as
|
|
664
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
634
665
|
|
|
635
666
|
// src/components/ui/textarea.tsx
|
|
636
667
|
import { forwardRef as forwardRef2 } from "react";
|
|
@@ -658,8 +689,8 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
658
689
|
const [value, setValue] = useState3("");
|
|
659
690
|
const [files, setFiles] = useState3([]);
|
|
660
691
|
const [fileError, setFileError] = useState3(void 0);
|
|
661
|
-
const textareaRef =
|
|
662
|
-
const fileInputRef =
|
|
692
|
+
const textareaRef = useRef3(null);
|
|
693
|
+
const fileInputRef = useRef3(null);
|
|
663
694
|
const canAttach = Boolean(attachments?.enabled);
|
|
664
695
|
const models = modelPicker?.models ?? [];
|
|
665
696
|
useEffect3(() => {
|
|
@@ -1109,8 +1140,8 @@ function ChatPanel({
|
|
|
1109
1140
|
}) {
|
|
1110
1141
|
const { config, configError, loading } = useFrankie();
|
|
1111
1142
|
const theme = useFrankieTheme();
|
|
1112
|
-
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
1113
|
-
const scrollRef =
|
|
1143
|
+
const { messages, busy, restoring, loadedSkills, error, send, stop, reset } = useFrankieChat();
|
|
1144
|
+
const scrollRef = useRef4(null);
|
|
1114
1145
|
const features = config?.features;
|
|
1115
1146
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1116
1147
|
const [model, setModel] = useState5(void 0);
|
|
@@ -1164,6 +1195,16 @@ function ChatPanel({
|
|
|
1164
1195
|
)
|
|
1165
1196
|
) }) }) }),
|
|
1166
1197
|
/* @__PURE__ */ jsxs4(CardFooter, { className: "flex flex-col gap-2", children: [
|
|
1198
|
+
loadedSkills !== null ? /* @__PURE__ */ jsxs4("details", { className: "frankie-loaded-skills", children: [
|
|
1199
|
+
/* @__PURE__ */ jsx13("summary", { role: "status", children: loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn" }),
|
|
1200
|
+
loadedSkills.length ? /* @__PURE__ */ jsx13("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ jsxs4("li", { children: [
|
|
1201
|
+
skill.name,
|
|
1202
|
+
/* @__PURE__ */ jsxs4("small", { children: [
|
|
1203
|
+
"v",
|
|
1204
|
+
skill.revision
|
|
1205
|
+
] })
|
|
1206
|
+
] }, skill.id)) }) : /* @__PURE__ */ jsx13("p", { children: "Your assistant can still answer using its available tools." })
|
|
1207
|
+
] }) : null,
|
|
1167
1208
|
error ? /* @__PURE__ */ jsx13("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1168
1209
|
/* @__PURE__ */ jsx13("div", { className: "w-full", children: /* @__PURE__ */ jsx13(
|
|
1169
1210
|
Composer,
|
|
@@ -1171,7 +1212,7 @@ function ChatPanel({
|
|
|
1171
1212
|
onSend: send,
|
|
1172
1213
|
onStop: stop,
|
|
1173
1214
|
busy,
|
|
1174
|
-
disabled: Boolean(configError),
|
|
1215
|
+
disabled: loading || restoring || Boolean(configError),
|
|
1175
1216
|
...placeholder ? { placeholder } : {},
|
|
1176
1217
|
...autoFocus ? { autoFocus } : {},
|
|
1177
1218
|
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
@@ -1191,6 +1232,7 @@ function FrankieWidget({
|
|
|
1191
1232
|
position = "bottom-right",
|
|
1192
1233
|
offset = 24,
|
|
1193
1234
|
defaultOpen = false,
|
|
1235
|
+
zIndex = 20,
|
|
1194
1236
|
open: controlledOpen,
|
|
1195
1237
|
onOpenChange,
|
|
1196
1238
|
renderLauncher,
|
|
@@ -1219,11 +1261,11 @@ function FrankieWidget({
|
|
|
1219
1261
|
"div",
|
|
1220
1262
|
{
|
|
1221
1263
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1222
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex:
|
|
1264
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1223
1265
|
"data-frankie-mode": mode,
|
|
1224
1266
|
children: /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1225
|
-
open ? /* @__PURE__ */ jsx14(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
|
|
1226
|
-
renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx14(
|
|
1267
|
+
open ? /* @__PURE__ */ jsx14("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx14(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1268
|
+
/* @__PURE__ */ jsx14("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx14(
|
|
1227
1269
|
Button,
|
|
1228
1270
|
{
|
|
1229
1271
|
size: "icon",
|
|
@@ -1234,7 +1276,7 @@ function FrankieWidget({
|
|
|
1234
1276
|
title: label,
|
|
1235
1277
|
children: open ? /* @__PURE__ */ jsx14(X4, { className: "size-5" }) : /* @__PURE__ */ jsx14(MessageCircle, { className: "size-5" })
|
|
1236
1278
|
}
|
|
1237
|
-
)
|
|
1279
|
+
) })
|
|
1238
1280
|
] })
|
|
1239
1281
|
}
|
|
1240
1282
|
);
|
|
@@ -1257,94 +1299,145 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1257
1299
|
}
|
|
1258
1300
|
|
|
1259
1301
|
// src/components/chat/FrankieChatPage.tsx
|
|
1302
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1303
|
+
import { History, Menu, Plus, Settings, X as X5 } from "lucide-react";
|
|
1260
1304
|
import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1261
1305
|
function PageEmptyState() {
|
|
1262
1306
|
const { config } = useFrankie();
|
|
1263
|
-
const { send, busy } = useFrankieChat();
|
|
1307
|
+
const { send, busy, restoring } = useFrankieChat();
|
|
1264
1308
|
const name = config?.branding.name ?? "Frankie";
|
|
1265
|
-
const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
|
|
1266
1309
|
return /* @__PURE__ */ jsxs6("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1267
1310
|
/* @__PURE__ */ jsx16("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1268
1311
|
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1269
|
-
/* @__PURE__ */
|
|
1270
|
-
|
|
1271
|
-
" assistant"
|
|
1272
|
-
] }),
|
|
1273
|
-
/* @__PURE__ */ jsx16("h1", { children: "What would you like to do?" }),
|
|
1312
|
+
/* @__PURE__ */ jsx16("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1313
|
+
/* @__PURE__ */ jsx16("h1", { children: "How can I help?" }),
|
|
1274
1314
|
/* @__PURE__ */ jsx16("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1275
1315
|
] }),
|
|
1276
|
-
|
|
1277
|
-
/* @__PURE__ */ jsx16("span", { children: String(index + 1).padStart(2, "0") }),
|
|
1316
|
+
/* @__PURE__ */ jsx16("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs6("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1278
1317
|
/* @__PURE__ */ jsx16("strong", { children: prompt }),
|
|
1279
1318
|
/* @__PURE__ */ jsx16("b", { "aria-hidden": true, children: "\u2197" })
|
|
1280
|
-
] }, prompt)) })
|
|
1319
|
+
] }, prompt)) })
|
|
1281
1320
|
] });
|
|
1282
1321
|
}
|
|
1283
|
-
function PageChrome({
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1322
|
+
function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
|
|
1323
|
+
const { client, config } = useFrankie();
|
|
1324
|
+
const { busy } = useFrankieChat();
|
|
1325
|
+
const theme = useFrankieTheme();
|
|
1326
|
+
const [conversations, setConversations] = useState7([]);
|
|
1327
|
+
const [historyError, setHistoryError] = useState7("");
|
|
1328
|
+
const [historyLoading, setHistoryLoading] = useState7(false);
|
|
1329
|
+
const [drawerOpen, setDrawerOpen] = useState7(false);
|
|
1330
|
+
const [query, setQuery] = useState7("");
|
|
1331
|
+
const dialog = useRef5(null);
|
|
1332
|
+
const menu = useRef5(null);
|
|
1333
|
+
const requestVersion = useRef5(0);
|
|
1294
1334
|
const name = config?.branding.name ?? "Frankie";
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1335
|
+
const refresh = useCallback3(async () => {
|
|
1336
|
+
if (!showHistory || !config || config.allowAnonymous) {
|
|
1337
|
+
setHistoryError("");
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const version = ++requestVersion.current;
|
|
1341
|
+
setHistoryLoading(true);
|
|
1342
|
+
setHistoryError("");
|
|
1343
|
+
try {
|
|
1344
|
+
const items = await client.listConversations();
|
|
1345
|
+
if (version === requestVersion.current) setConversations(items);
|
|
1346
|
+
} catch {
|
|
1347
|
+
if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
|
|
1348
|
+
} finally {
|
|
1349
|
+
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1350
|
+
}
|
|
1351
|
+
}, [client, showHistory, config?.allowAnonymous]);
|
|
1352
|
+
useEffect6(() => {
|
|
1353
|
+
if (!busy) void refresh();
|
|
1354
|
+
return () => {
|
|
1355
|
+
requestVersion.current++;
|
|
1356
|
+
};
|
|
1357
|
+
}, [refresh, busy]);
|
|
1358
|
+
useEffect6(() => {
|
|
1359
|
+
const node = dialog.current;
|
|
1360
|
+
if (drawerOpen) {
|
|
1361
|
+
node?.showModal?.();
|
|
1362
|
+
} else {
|
|
1363
|
+
node?.close?.();
|
|
1364
|
+
}
|
|
1365
|
+
}, [drawerOpen]);
|
|
1366
|
+
const close = () => {
|
|
1367
|
+
setDrawerOpen(false);
|
|
1368
|
+
menu.current?.focus();
|
|
1369
|
+
};
|
|
1370
|
+
const select = (id) => {
|
|
1371
|
+
if (!busy) {
|
|
1372
|
+
onSelect(id);
|
|
1373
|
+
close();
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
const history = /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1377
|
+
/* @__PURE__ */ jsxs6("div", { className: "frankie-history-heading", children: [
|
|
1378
|
+
/* @__PURE__ */ jsx16("strong", { children: "Conversations" }),
|
|
1379
|
+
/* @__PURE__ */ jsx16("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx16(X5, { size: 18 }) })
|
|
1380
|
+
] }),
|
|
1381
|
+
/* @__PURE__ */ jsxs6("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1382
|
+
onNew();
|
|
1383
|
+
close();
|
|
1384
|
+
}, children: [
|
|
1385
|
+
/* @__PURE__ */ jsx16(Plus, { size: 17 }),
|
|
1386
|
+
"New chat"
|
|
1387
|
+
] }),
|
|
1388
|
+
/* @__PURE__ */ jsxs6("label", { className: "frankie-history-search", children: [
|
|
1389
|
+
/* @__PURE__ */ jsx16("span", { children: "Search conversations" }),
|
|
1390
|
+
/* @__PURE__ */ jsx16("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1391
|
+
] }),
|
|
1392
|
+
/* @__PURE__ */ jsx16("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs6("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1393
|
+
/* @__PURE__ */ jsx16("span", { children: item.title }),
|
|
1394
|
+
/* @__PURE__ */ jsx16("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1395
|
+
] }, item.id)) }),
|
|
1396
|
+
historyLoading ? /* @__PURE__ */ jsx16("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs6("p", { role: "status", children: [
|
|
1397
|
+
historyError,
|
|
1398
|
+
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1399
|
+
] }) : !conversations.length ? /* @__PURE__ */ jsx16("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1400
|
+
navigation ? /* @__PURE__ */ jsx16("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1401
|
+
] });
|
|
1402
|
+
return /* @__PURE__ */ jsxs6("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1403
|
+
showHistory ? /* @__PURE__ */ jsx16("aside", { className: "frankie-history-desktop", children: history }) : null,
|
|
1404
|
+
/* @__PURE__ */ jsxs6("div", { className: "frankie-page-main", children: [
|
|
1405
|
+
/* @__PURE__ */ jsxs6("header", { className: "frankie-page-header", children: [
|
|
1406
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1407
|
+
showHistory ? /* @__PURE__ */ jsx16("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx16(Menu, { size: 20 }) }) : null,
|
|
1408
|
+
/* @__PURE__ */ jsx16("strong", { children: name }),
|
|
1409
|
+
/* @__PURE__ */ jsx16("span", { className: "frankie-header-caption", children: "Product assistant" })
|
|
1410
|
+
] }),
|
|
1411
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1412
|
+
/* @__PURE__ */ jsxs6("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1413
|
+
/* @__PURE__ */ jsx16(Plus, { size: 16 }),
|
|
1414
|
+
/* @__PURE__ */ jsx16("span", { children: "New chat" })
|
|
1415
|
+
] }),
|
|
1416
|
+
/* @__PURE__ */ jsxs6("details", { className: "frankie-chat-settings", children: [
|
|
1417
|
+
/* @__PURE__ */ jsx16("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx16(Settings, { size: 19 }) }),
|
|
1418
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1419
|
+
/* @__PURE__ */ jsx16("strong", { children: "Chat settings" }),
|
|
1420
|
+
/* @__PURE__ */ jsx16("p", { children: "History syncs automatically for signed-in users." }),
|
|
1421
|
+
/* @__PURE__ */ jsxs6("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1422
|
+
/* @__PURE__ */ jsx16(History, { size: 16 }),
|
|
1423
|
+
"Refresh history"
|
|
1424
|
+
] }),
|
|
1425
|
+
/* @__PURE__ */ jsx16("small", { children: "Skills are checked at the start of each message." })
|
|
1426
|
+
] })
|
|
1427
|
+
] })
|
|
1428
|
+
] })
|
|
1300
1429
|
] }),
|
|
1301
|
-
/* @__PURE__ */ jsx16("
|
|
1430
|
+
/* @__PURE__ */ jsx16("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx16(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx16(PageEmptyState, {}) }) })
|
|
1302
1431
|
] }),
|
|
1303
|
-
/* @__PURE__ */ jsx16("
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
header: header ?? false,
|
|
1307
|
-
footer,
|
|
1308
|
-
emptyState: emptyState ?? /* @__PURE__ */ jsx16(PageEmptyState, {}),
|
|
1309
|
-
renderMessage,
|
|
1310
|
-
renderTool,
|
|
1311
|
-
placeholder,
|
|
1312
|
-
autoFocus
|
|
1313
|
-
}
|
|
1314
|
-
) })
|
|
1432
|
+
showHistory ? /* @__PURE__ */ jsx16("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1433
|
+
if (event.target === event.currentTarget) close();
|
|
1434
|
+
}, children: /* @__PURE__ */ jsx16("aside", { children: history }) }) : null
|
|
1315
1435
|
] });
|
|
1316
1436
|
}
|
|
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
|
-
);
|
|
1437
|
+
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1438
|
+
const [id, setId] = useState7(() => provider.conversationId ?? crypto.randomUUID());
|
|
1439
|
+
const onNew = () => setId(crypto.randomUUID());
|
|
1440
|
+
return /* @__PURE__ */ jsx16("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ jsx16(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx16(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1348
1441
|
}
|
|
1349
1442
|
|
|
1350
1443
|
// src/components/ui/separator.tsx
|