@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.cjs
CHANGED
|
@@ -63,7 +63,7 @@ __export(index_exports, {
|
|
|
63
63
|
module.exports = __toCommonJS(index_exports);
|
|
64
64
|
|
|
65
65
|
// src/hooks/FrankieProvider.tsx
|
|
66
|
-
var
|
|
66
|
+
var import_react2 = require("react");
|
|
67
67
|
|
|
68
68
|
// src/lib/client.ts
|
|
69
69
|
var FRANKIE_CLOUD_URL = "https://frankie-data-gq5o5syq2q-uc.a.run.app";
|
|
@@ -82,11 +82,18 @@ var FrankieClient = class {
|
|
|
82
82
|
this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
|
|
83
83
|
this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
84
84
|
this.session = this.readStored();
|
|
85
|
+
if (options.getUserToken && typeof window !== "undefined") {
|
|
86
|
+
try {
|
|
87
|
+
for (const suffix of ["session", "messages"]) window.localStorage.removeItem(`${this.namespace}.${suffix}`);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
85
91
|
}
|
|
86
92
|
options;
|
|
87
93
|
baseUrl;
|
|
88
94
|
doFetch;
|
|
89
95
|
session;
|
|
96
|
+
sessionUserToken;
|
|
90
97
|
get sessionId() {
|
|
91
98
|
return this.session?.sessionId;
|
|
92
99
|
}
|
|
@@ -95,7 +102,7 @@ var FrankieClient = class {
|
|
|
95
102
|
}
|
|
96
103
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
97
104
|
get persist() {
|
|
98
|
-
return this.options.persist !== false && typeof window !== "undefined";
|
|
105
|
+
return !this.options.getUserToken && this.options.persist !== false && typeof window !== "undefined";
|
|
99
106
|
}
|
|
100
107
|
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
101
108
|
get namespace() {
|
|
@@ -128,15 +135,17 @@ var FrankieClient = class {
|
|
|
128
135
|
return await this.unwrap(response);
|
|
129
136
|
}
|
|
130
137
|
async ensureSession(context) {
|
|
131
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
132
138
|
const userToken = await this.options.getUserToken?.();
|
|
133
139
|
if (this.options.getUserToken && !userToken) {
|
|
140
|
+
this.resetSession();
|
|
134
141
|
throw new FrankieApiError(
|
|
135
142
|
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
136
143
|
401,
|
|
137
144
|
"user_token_unavailable"
|
|
138
145
|
);
|
|
139
146
|
}
|
|
147
|
+
if (userToken !== this.sessionUserToken) this.resetSession();
|
|
148
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
140
149
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
141
150
|
method: "POST",
|
|
142
151
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
@@ -147,6 +156,7 @@ var FrankieClient = class {
|
|
|
147
156
|
})
|
|
148
157
|
});
|
|
149
158
|
this.session = await this.unwrap(response);
|
|
159
|
+
this.sessionUserToken = userToken;
|
|
150
160
|
this.writeStored();
|
|
151
161
|
return this.session;
|
|
152
162
|
}
|
|
@@ -159,7 +169,7 @@ var FrankieClient = class {
|
|
|
159
169
|
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
160
170
|
}
|
|
161
171
|
async resumeConversation(id = this.options.conversationId) {
|
|
162
|
-
if (!id) return void 0;
|
|
172
|
+
if (!id || !this.options.getUserToken) return void 0;
|
|
163
173
|
const userToken = await this.requireUserToken();
|
|
164
174
|
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
165
175
|
method: "POST",
|
|
@@ -168,6 +178,7 @@ var FrankieClient = class {
|
|
|
168
178
|
if (response.status === 404) return void 0;
|
|
169
179
|
const body = await this.unwrap(response);
|
|
170
180
|
this.session = body.session;
|
|
181
|
+
this.sessionUserToken = userToken;
|
|
171
182
|
this.writeStored();
|
|
172
183
|
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
173
184
|
}
|
|
@@ -274,64 +285,8 @@ function parseFrame(frame) {
|
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
287
|
|
|
277
|
-
// src/hooks/FrankieProvider.tsx
|
|
278
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
279
|
-
var FrankieContext = (0, import_react.createContext)(null);
|
|
280
|
-
function FrankieProvider({
|
|
281
|
-
children,
|
|
282
|
-
context,
|
|
283
|
-
theme,
|
|
284
|
-
config: providedConfig,
|
|
285
|
-
...clientOptions
|
|
286
|
-
}) {
|
|
287
|
-
const client = (0, import_react.useMemo)(
|
|
288
|
-
() => new FrankieClient(clientOptions),
|
|
289
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
-
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
|
|
291
|
-
);
|
|
292
|
-
const [config, setConfig] = (0, import_react.useState)(providedConfig);
|
|
293
|
-
const [configError, setConfigError] = (0, import_react.useState)(void 0);
|
|
294
|
-
const [loading, setLoading] = (0, import_react.useState)(!providedConfig);
|
|
295
|
-
(0, import_react.useEffect)(() => {
|
|
296
|
-
if (providedConfig) return;
|
|
297
|
-
const abort = new AbortController();
|
|
298
|
-
setLoading(true);
|
|
299
|
-
client.getConfig(abort.signal).then((next) => {
|
|
300
|
-
setConfig(next);
|
|
301
|
-
setConfigError(void 0);
|
|
302
|
-
}).catch((error) => {
|
|
303
|
-
if (abort.signal.aborted) return;
|
|
304
|
-
setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
|
|
305
|
-
}).finally(() => {
|
|
306
|
-
if (!abort.signal.aborted) setLoading(false);
|
|
307
|
-
});
|
|
308
|
-
return () => abort.abort();
|
|
309
|
-
}, [client, providedConfig]);
|
|
310
|
-
const value = (0, import_react.useMemo)(
|
|
311
|
-
() => ({
|
|
312
|
-
client,
|
|
313
|
-
...config ? { config } : {},
|
|
314
|
-
...configError ? { configError } : {},
|
|
315
|
-
loading,
|
|
316
|
-
...context ? { context } : {},
|
|
317
|
-
...theme ? { theme } : {}
|
|
318
|
-
}),
|
|
319
|
-
[client, config, configError, loading, context, theme]
|
|
320
|
-
);
|
|
321
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children });
|
|
322
|
-
}
|
|
323
|
-
function useFrankie() {
|
|
324
|
-
const value = (0, import_react.useContext)(FrankieContext);
|
|
325
|
-
if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
|
|
326
|
-
return value;
|
|
327
|
-
}
|
|
328
|
-
function useFrankieTheme() {
|
|
329
|
-
const { config, theme } = useFrankie();
|
|
330
|
-
return (0, import_react.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
288
|
// src/hooks/useFrankieChat.ts
|
|
334
|
-
var
|
|
289
|
+
var import_react = require("react");
|
|
335
290
|
|
|
336
291
|
// src/lib/messages.ts
|
|
337
292
|
function applyEvent(message, event) {
|
|
@@ -408,16 +363,23 @@ function newUserMessage(id, text, attachments) {
|
|
|
408
363
|
// src/hooks/useFrankieChat.ts
|
|
409
364
|
var counter = 0;
|
|
410
365
|
var nextId = (prefix) => `${prefix}_${Date.now().toString(36)}_${counter += 1}`;
|
|
366
|
+
var FrankieChatContext = (0, import_react.createContext)(null);
|
|
411
367
|
function useFrankieChat() {
|
|
368
|
+
const value = (0, import_react.useContext)(FrankieChatContext);
|
|
369
|
+
if (!value) throw new Error("useFrankieChat requires FrankieProvider.");
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
function useFrankieChatState() {
|
|
412
373
|
const { client, context } = useFrankie();
|
|
413
|
-
const [messages, setMessages] = (0,
|
|
414
|
-
const [busy, setBusy] = (0,
|
|
415
|
-
const [error, setError] = (0,
|
|
416
|
-
const [restored, setRestored] = (0,
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
(0,
|
|
374
|
+
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
375
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
376
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
377
|
+
const [restored, setRestored] = (0, import_react.useState)(false);
|
|
378
|
+
const [loadedSkills, setLoadedSkills] = (0, import_react.useState)(null);
|
|
379
|
+
const abortRef = (0, import_react.useRef)(null);
|
|
380
|
+
const mounted = (0, import_react.useRef)(true);
|
|
381
|
+
const messagesRef = (0, import_react.useRef)([]);
|
|
382
|
+
(0, import_react.useEffect)(() => {
|
|
421
383
|
mounted.current = true;
|
|
422
384
|
return () => {
|
|
423
385
|
mounted.current = false;
|
|
@@ -425,7 +387,7 @@ function useFrankieChat() {
|
|
|
425
387
|
};
|
|
426
388
|
}, []);
|
|
427
389
|
const storageKey = `${client.namespace}.messages`;
|
|
428
|
-
(0,
|
|
390
|
+
(0, import_react.useEffect)(() => {
|
|
429
391
|
let active = true;
|
|
430
392
|
const restore = async () => {
|
|
431
393
|
if (client.persist) {
|
|
@@ -447,6 +409,7 @@ function useFrankieChat() {
|
|
|
447
409
|
setMessages(restoredMessages);
|
|
448
410
|
}
|
|
449
411
|
} catch {
|
|
412
|
+
if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
|
|
450
413
|
} finally {
|
|
451
414
|
if (active) setRestored(true);
|
|
452
415
|
}
|
|
@@ -456,7 +419,7 @@ function useFrankieChat() {
|
|
|
456
419
|
active = false;
|
|
457
420
|
};
|
|
458
421
|
}, [client, storageKey]);
|
|
459
|
-
(0,
|
|
422
|
+
(0, import_react.useEffect)(() => {
|
|
460
423
|
if (!client.persist || !restored || busy) return;
|
|
461
424
|
try {
|
|
462
425
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
@@ -465,21 +428,22 @@ function useFrankieChat() {
|
|
|
465
428
|
}
|
|
466
429
|
if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
|
|
467
430
|
}, [messages, busy, client, storageKey, restored]);
|
|
468
|
-
const patchAssistant = (0,
|
|
431
|
+
const patchAssistant = (0, import_react.useCallback)((id, update) => {
|
|
469
432
|
const next = messagesRef.current.map(
|
|
470
433
|
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
471
434
|
);
|
|
472
435
|
messagesRef.current = next;
|
|
473
436
|
setMessages(next);
|
|
474
437
|
}, []);
|
|
475
|
-
const send = (0,
|
|
438
|
+
const send = (0, import_react.useCallback)(
|
|
476
439
|
async (text, opts) => {
|
|
477
440
|
const trimmed = text.trim();
|
|
478
441
|
const attachments = opts?.attachments ?? [];
|
|
479
|
-
if (!trimmed && attachments.length === 0 ||
|
|
442
|
+
if (!trimmed && attachments.length === 0 || abortRef.current || !restored) return;
|
|
480
443
|
const outgoing = trimmed || "Please take a look at the attached file(s).";
|
|
481
444
|
const assistantId = nextId("a");
|
|
482
445
|
setError(void 0);
|
|
446
|
+
setLoadedSkills(null);
|
|
483
447
|
setBusy(true);
|
|
484
448
|
const next = [
|
|
485
449
|
...messagesRef.current,
|
|
@@ -506,6 +470,10 @@ function useFrankieChat() {
|
|
|
506
470
|
signal: abort.signal
|
|
507
471
|
},
|
|
508
472
|
(event) => {
|
|
473
|
+
if (event.type === "skills_loaded") {
|
|
474
|
+
setLoadedSkills(event.skills);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
509
477
|
if (event.type === "ping" || event.type === "session") return;
|
|
510
478
|
if (event.type === "text_delta") receivedText = true;
|
|
511
479
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
@@ -544,10 +512,10 @@ function useFrankieChat() {
|
|
|
544
512
|
abortRef.current = null;
|
|
545
513
|
}
|
|
546
514
|
},
|
|
547
|
-
[busy, client, context, patchAssistant]
|
|
515
|
+
[busy, client, context, patchAssistant, restored]
|
|
548
516
|
);
|
|
549
|
-
const stop = (0,
|
|
550
|
-
const reset = (0,
|
|
517
|
+
const stop = (0, import_react.useCallback)(() => abortRef.current?.abort(), []);
|
|
518
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
551
519
|
abortRef.current?.abort();
|
|
552
520
|
client.resetSession();
|
|
553
521
|
messagesRef.current = [];
|
|
@@ -555,12 +523,13 @@ function useFrankieChat() {
|
|
|
555
523
|
setError(void 0);
|
|
556
524
|
setBusy(false);
|
|
557
525
|
setRestored(true);
|
|
526
|
+
setLoadedSkills(null);
|
|
558
527
|
try {
|
|
559
528
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
560
529
|
} catch {
|
|
561
530
|
}
|
|
562
531
|
}, [client]);
|
|
563
|
-
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
532
|
+
return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
|
|
564
533
|
}
|
|
565
534
|
function conversationTitle(messages) {
|
|
566
535
|
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
@@ -569,6 +538,68 @@ function conversationTitle(messages) {
|
|
|
569
538
|
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
570
539
|
}
|
|
571
540
|
|
|
541
|
+
// src/hooks/FrankieProvider.tsx
|
|
542
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
543
|
+
var FrankieContext = (0, import_react2.createContext)(null);
|
|
544
|
+
function FrankieProvider({
|
|
545
|
+
children,
|
|
546
|
+
context,
|
|
547
|
+
theme,
|
|
548
|
+
config: providedConfig,
|
|
549
|
+
...clientOptions
|
|
550
|
+
}) {
|
|
551
|
+
const tokenCallback = (0, import_react2.useRef)(clientOptions.getUserToken);
|
|
552
|
+
tokenCallback.current = clientOptions.getUserToken;
|
|
553
|
+
const client = (0, import_react2.useMemo)(
|
|
554
|
+
() => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
|
|
555
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
556
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
|
|
557
|
+
);
|
|
558
|
+
const [config, setConfig] = (0, import_react2.useState)(providedConfig);
|
|
559
|
+
const [configError, setConfigError] = (0, import_react2.useState)(void 0);
|
|
560
|
+
const [loading, setLoading] = (0, import_react2.useState)(!providedConfig);
|
|
561
|
+
(0, import_react2.useEffect)(() => {
|
|
562
|
+
if (providedConfig) return;
|
|
563
|
+
const abort = new AbortController();
|
|
564
|
+
setLoading(true);
|
|
565
|
+
client.getConfig(abort.signal).then((next) => {
|
|
566
|
+
setConfig(next);
|
|
567
|
+
setConfigError(void 0);
|
|
568
|
+
}).catch((error) => {
|
|
569
|
+
if (abort.signal.aborted) return;
|
|
570
|
+
setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
|
|
571
|
+
}).finally(() => {
|
|
572
|
+
if (!abort.signal.aborted) setLoading(false);
|
|
573
|
+
});
|
|
574
|
+
return () => abort.abort();
|
|
575
|
+
}, [client, providedConfig]);
|
|
576
|
+
const value = (0, import_react2.useMemo)(
|
|
577
|
+
() => ({
|
|
578
|
+
client,
|
|
579
|
+
...config ? { config } : {},
|
|
580
|
+
...configError ? { configError } : {},
|
|
581
|
+
loading,
|
|
582
|
+
...context ? { context } : {},
|
|
583
|
+
...theme ? { theme } : {}
|
|
584
|
+
}),
|
|
585
|
+
[client, config, configError, loading, context, theme]
|
|
586
|
+
);
|
|
587
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
|
|
588
|
+
}
|
|
589
|
+
function ConversationState({ children }) {
|
|
590
|
+
const chat = useFrankieChatState();
|
|
591
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieChatContext.Provider, { value: chat, children });
|
|
592
|
+
}
|
|
593
|
+
function useFrankie() {
|
|
594
|
+
const value = (0, import_react2.useContext)(FrankieContext);
|
|
595
|
+
if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
function useFrankieTheme() {
|
|
599
|
+
const { config, theme } = useFrankie();
|
|
600
|
+
return (0, import_react2.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
601
|
+
}
|
|
602
|
+
|
|
572
603
|
// src/components/chat/FrankieWidget.tsx
|
|
573
604
|
var import_lucide_react5 = require("lucide-react");
|
|
574
605
|
var import_react9 = require("react");
|
|
@@ -1171,7 +1202,7 @@ function ChatPanel({
|
|
|
1171
1202
|
}) {
|
|
1172
1203
|
const { config, configError, loading } = useFrankie();
|
|
1173
1204
|
const theme = useFrankieTheme();
|
|
1174
|
-
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
1205
|
+
const { messages, busy, restoring, loadedSkills, error, send, stop, reset } = useFrankieChat();
|
|
1175
1206
|
const scrollRef = (0, import_react8.useRef)(null);
|
|
1176
1207
|
const features = config?.features;
|
|
1177
1208
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
@@ -1226,6 +1257,16 @@ function ChatPanel({
|
|
|
1226
1257
|
)
|
|
1227
1258
|
) }) }) }),
|
|
1228
1259
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(CardFooter, { className: "flex flex-col gap-2", children: [
|
|
1260
|
+
loadedSkills !== null ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("details", { className: "frankie-loaded-skills", children: [
|
|
1261
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("summary", { role: "status", children: loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn" }),
|
|
1262
|
+
loadedSkills.length ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("li", { children: [
|
|
1263
|
+
skill.name,
|
|
1264
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("small", { children: [
|
|
1265
|
+
"v",
|
|
1266
|
+
skill.revision
|
|
1267
|
+
] })
|
|
1268
|
+
] }, skill.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: "Your assistant can still answer using its available tools." })
|
|
1269
|
+
] }) : null,
|
|
1229
1270
|
error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1230
1271
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1231
1272
|
Composer,
|
|
@@ -1233,7 +1274,7 @@ function ChatPanel({
|
|
|
1233
1274
|
onSend: send,
|
|
1234
1275
|
onStop: stop,
|
|
1235
1276
|
busy,
|
|
1236
|
-
disabled: Boolean(configError),
|
|
1277
|
+
disabled: loading || restoring || Boolean(configError),
|
|
1237
1278
|
...placeholder ? { placeholder } : {},
|
|
1238
1279
|
...autoFocus ? { autoFocus } : {},
|
|
1239
1280
|
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
@@ -1253,6 +1294,7 @@ function FrankieWidget({
|
|
|
1253
1294
|
position = "bottom-right",
|
|
1254
1295
|
offset = 24,
|
|
1255
1296
|
defaultOpen = false,
|
|
1297
|
+
zIndex = 20,
|
|
1256
1298
|
open: controlledOpen,
|
|
1257
1299
|
onOpenChange,
|
|
1258
1300
|
renderLauncher,
|
|
@@ -1281,11 +1323,11 @@ function FrankieWidget({
|
|
|
1281
1323
|
"div",
|
|
1282
1324
|
{
|
|
1283
1325
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1284
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex:
|
|
1326
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1285
1327
|
"data-frankie-mode": mode,
|
|
1286
1328
|
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1287
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
|
|
1288
|
-
renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1329
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1330
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1289
1331
|
Button,
|
|
1290
1332
|
{
|
|
1291
1333
|
size: "icon",
|
|
@@ -1296,7 +1338,7 @@ function FrankieWidget({
|
|
|
1296
1338
|
title: label,
|
|
1297
1339
|
children: open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.MessageCircle, { className: "size-5" })
|
|
1298
1340
|
}
|
|
1299
|
-
)
|
|
1341
|
+
) })
|
|
1300
1342
|
] })
|
|
1301
1343
|
}
|
|
1302
1344
|
);
|
|
@@ -1319,94 +1361,145 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1319
1361
|
}
|
|
1320
1362
|
|
|
1321
1363
|
// src/components/chat/FrankieChatPage.tsx
|
|
1364
|
+
var import_react10 = require("react");
|
|
1365
|
+
var import_lucide_react6 = require("lucide-react");
|
|
1322
1366
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1323
1367
|
function PageEmptyState() {
|
|
1324
1368
|
const { config } = useFrankie();
|
|
1325
|
-
const { send, busy } = useFrankieChat();
|
|
1369
|
+
const { send, busy, restoring } = useFrankieChat();
|
|
1326
1370
|
const name = config?.branding.name ?? "Frankie";
|
|
1327
|
-
const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
|
|
1328
1371
|
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1329
1372
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1330
1373
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
|
|
1331
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.
|
|
1332
|
-
|
|
1333
|
-
" assistant"
|
|
1334
|
-
] }),
|
|
1335
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h1", { children: "What would you like to do?" }),
|
|
1374
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1375
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h1", { children: "How can I help?" }),
|
|
1336
1376
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1337
1377
|
] }),
|
|
1338
|
-
|
|
1339
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: String(index + 1).padStart(2, "0") }),
|
|
1378
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1340
1379
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: prompt }),
|
|
1341
1380
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("b", { "aria-hidden": true, children: "\u2197" })
|
|
1342
|
-
] }, prompt)) })
|
|
1381
|
+
] }, prompt)) })
|
|
1343
1382
|
] });
|
|
1344
1383
|
}
|
|
1345
|
-
function PageChrome({
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1384
|
+
function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
|
|
1385
|
+
const { client, config } = useFrankie();
|
|
1386
|
+
const { busy } = useFrankieChat();
|
|
1387
|
+
const theme = useFrankieTheme();
|
|
1388
|
+
const [conversations, setConversations] = (0, import_react10.useState)([]);
|
|
1389
|
+
const [historyError, setHistoryError] = (0, import_react10.useState)("");
|
|
1390
|
+
const [historyLoading, setHistoryLoading] = (0, import_react10.useState)(false);
|
|
1391
|
+
const [drawerOpen, setDrawerOpen] = (0, import_react10.useState)(false);
|
|
1392
|
+
const [query, setQuery] = (0, import_react10.useState)("");
|
|
1393
|
+
const dialog = (0, import_react10.useRef)(null);
|
|
1394
|
+
const menu = (0, import_react10.useRef)(null);
|
|
1395
|
+
const requestVersion = (0, import_react10.useRef)(0);
|
|
1356
1396
|
const name = config?.branding.name ?? "Frankie";
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1397
|
+
const refresh = (0, import_react10.useCallback)(async () => {
|
|
1398
|
+
if (!showHistory || !config || config.allowAnonymous) {
|
|
1399
|
+
setHistoryError("");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const version = ++requestVersion.current;
|
|
1403
|
+
setHistoryLoading(true);
|
|
1404
|
+
setHistoryError("");
|
|
1405
|
+
try {
|
|
1406
|
+
const items = await client.listConversations();
|
|
1407
|
+
if (version === requestVersion.current) setConversations(items);
|
|
1408
|
+
} catch {
|
|
1409
|
+
if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
|
|
1410
|
+
} finally {
|
|
1411
|
+
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1412
|
+
}
|
|
1413
|
+
}, [client, showHistory, config?.allowAnonymous]);
|
|
1414
|
+
(0, import_react10.useEffect)(() => {
|
|
1415
|
+
if (!busy) void refresh();
|
|
1416
|
+
return () => {
|
|
1417
|
+
requestVersion.current++;
|
|
1418
|
+
};
|
|
1419
|
+
}, [refresh, busy]);
|
|
1420
|
+
(0, import_react10.useEffect)(() => {
|
|
1421
|
+
const node = dialog.current;
|
|
1422
|
+
if (drawerOpen) {
|
|
1423
|
+
node?.showModal?.();
|
|
1424
|
+
} else {
|
|
1425
|
+
node?.close?.();
|
|
1426
|
+
}
|
|
1427
|
+
}, [drawerOpen]);
|
|
1428
|
+
const close = () => {
|
|
1429
|
+
setDrawerOpen(false);
|
|
1430
|
+
menu.current?.focus();
|
|
1431
|
+
};
|
|
1432
|
+
const select = (id) => {
|
|
1433
|
+
if (!busy) {
|
|
1434
|
+
onSelect(id);
|
|
1435
|
+
close();
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
const history = /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
|
|
1439
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "frankie-history-heading", children: [
|
|
1440
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: "Conversations" }),
|
|
1441
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.X, { size: 18 }) })
|
|
1442
|
+
] }),
|
|
1443
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1444
|
+
onNew();
|
|
1445
|
+
close();
|
|
1446
|
+
}, children: [
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.Plus, { size: 17 }),
|
|
1448
|
+
"New chat"
|
|
1449
|
+
] }),
|
|
1450
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("label", { className: "frankie-history-search", children: [
|
|
1451
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "Search conversations" }),
|
|
1452
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1453
|
+
] }),
|
|
1454
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1455
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: item.title }),
|
|
1456
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1457
|
+
] }, item.id)) }),
|
|
1458
|
+
historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { role: "status", children: [
|
|
1459
|
+
historyError,
|
|
1460
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1461
|
+
] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1462
|
+
navigation ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1463
|
+
] });
|
|
1464
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1465
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("aside", { className: "frankie-history-desktop", children: history }) : null,
|
|
1466
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "frankie-page-main", children: [
|
|
1467
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "frankie-page-header", children: [
|
|
1468
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
|
|
1469
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.Menu, { size: 20 }) }) : null,
|
|
1470
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: name }),
|
|
1471
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-header-caption", children: "Product assistant" })
|
|
1472
|
+
] }),
|
|
1473
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
|
|
1474
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1475
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.Plus, { size: 16 }),
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "New chat" })
|
|
1477
|
+
] }),
|
|
1478
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("details", { className: "frankie-chat-settings", children: [
|
|
1479
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.Settings, { size: 19 }) }),
|
|
1480
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
|
|
1481
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: "Chat settings" }),
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: "History syncs automatically for signed-in users." }),
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1484
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.History, { size: 16 }),
|
|
1485
|
+
"Refresh history"
|
|
1486
|
+
] }),
|
|
1487
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("small", { children: "Skills are checked at the start of each message." })
|
|
1488
|
+
] })
|
|
1489
|
+
] })
|
|
1490
|
+
] })
|
|
1362
1491
|
] }),
|
|
1363
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("
|
|
1492
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PageEmptyState, {}) }) })
|
|
1364
1493
|
] }),
|
|
1365
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
header: header ?? false,
|
|
1369
|
-
footer,
|
|
1370
|
-
emptyState: emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PageEmptyState, {}),
|
|
1371
|
-
renderMessage,
|
|
1372
|
-
renderTool,
|
|
1373
|
-
placeholder,
|
|
1374
|
-
autoFocus
|
|
1375
|
-
}
|
|
1376
|
-
) })
|
|
1494
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1495
|
+
if (event.target === event.currentTarget) close();
|
|
1496
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("aside", { children: history }) }) : null
|
|
1377
1497
|
] });
|
|
1378
1498
|
}
|
|
1379
|
-
function FrankieChatPage({
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
header,
|
|
1383
|
-
footer,
|
|
1384
|
-
emptyState,
|
|
1385
|
-
renderMessage,
|
|
1386
|
-
renderTool,
|
|
1387
|
-
placeholder,
|
|
1388
|
-
autoFocus,
|
|
1389
|
-
...providerProps
|
|
1390
|
-
}) {
|
|
1391
|
-
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1392
|
-
"main",
|
|
1393
|
-
{
|
|
1394
|
-
className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
|
|
1395
|
-
style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
|
|
1396
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1397
|
-
PageChrome,
|
|
1398
|
-
{
|
|
1399
|
-
header,
|
|
1400
|
-
footer,
|
|
1401
|
-
emptyState,
|
|
1402
|
-
renderMessage,
|
|
1403
|
-
renderTool,
|
|
1404
|
-
placeholder,
|
|
1405
|
-
autoFocus
|
|
1406
|
-
}
|
|
1407
|
-
) })
|
|
1408
|
-
}
|
|
1409
|
-
);
|
|
1499
|
+
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1500
|
+
const [id, setId] = (0, import_react10.useState)(() => provider.conversationId ?? crypto.randomUUID());
|
|
1501
|
+
const onNew = () => setId(crypto.randomUUID());
|
|
1502
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1410
1503
|
}
|
|
1411
1504
|
|
|
1412
1505
|
// src/components/ui/separator.tsx
|