@frankieai/react 0.1.4 → 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 +290 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -10
- package/dist/index.d.ts +16 -10
- package/dist/index.js +290 -128
- 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 +82 -42
- package/src/components/chat/FrankieWidget.tsx +7 -3
- package/src/hooks/FrankieProvider.tsx +12 -4
- package/src/hooks/useFrankieChat.ts +52 -21
- package/src/lib/client.ts +16 -6
- package/src/styles/frankie.css +182 -0
- 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,15 +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
|
-
(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)(() => {
|
|
420
383
|
mounted.current = true;
|
|
421
384
|
return () => {
|
|
422
385
|
mounted.current = false;
|
|
@@ -424,21 +387,29 @@ function useFrankieChat() {
|
|
|
424
387
|
};
|
|
425
388
|
}, []);
|
|
426
389
|
const storageKey = `${client.namespace}.messages`;
|
|
427
|
-
(0,
|
|
390
|
+
(0, import_react.useEffect)(() => {
|
|
428
391
|
let active = true;
|
|
429
392
|
const restore = async () => {
|
|
430
393
|
if (client.persist) {
|
|
431
394
|
try {
|
|
432
395
|
const raw = window.localStorage.getItem(storageKey);
|
|
433
396
|
const stored = raw ? JSON.parse(raw) : [];
|
|
434
|
-
if (Array.isArray(stored) && stored.length && active)
|
|
397
|
+
if (Array.isArray(stored) && stored.length && active) {
|
|
398
|
+
messagesRef.current = stored;
|
|
399
|
+
setMessages(stored);
|
|
400
|
+
}
|
|
435
401
|
} catch {
|
|
436
402
|
}
|
|
437
403
|
}
|
|
438
404
|
try {
|
|
439
405
|
const remote = await client.resumeConversation();
|
|
440
|
-
if (active && remote?.messages.length)
|
|
406
|
+
if (active && remote?.messages.length) {
|
|
407
|
+
const restoredMessages = remote.messages;
|
|
408
|
+
messagesRef.current = restoredMessages;
|
|
409
|
+
setMessages(restoredMessages);
|
|
410
|
+
}
|
|
441
411
|
} catch {
|
|
412
|
+
if (active) setError("Could not restore this conversation. Check your sign-in and connection, then try opening it again.");
|
|
442
413
|
} finally {
|
|
443
414
|
if (active) setRestored(true);
|
|
444
415
|
}
|
|
@@ -448,7 +419,7 @@ function useFrankieChat() {
|
|
|
448
419
|
active = false;
|
|
449
420
|
};
|
|
450
421
|
}, [client, storageKey]);
|
|
451
|
-
(0,
|
|
422
|
+
(0, import_react.useEffect)(() => {
|
|
452
423
|
if (!client.persist || !restored || busy) return;
|
|
453
424
|
try {
|
|
454
425
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
@@ -457,31 +428,34 @@ function useFrankieChat() {
|
|
|
457
428
|
}
|
|
458
429
|
if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
|
|
459
430
|
}, [messages, busy, client, storageKey, restored]);
|
|
460
|
-
const patchAssistant = (0,
|
|
461
|
-
|
|
462
|
-
(
|
|
463
|
-
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
464
|
-
)
|
|
431
|
+
const patchAssistant = (0, import_react.useCallback)((id, update) => {
|
|
432
|
+
const next = messagesRef.current.map(
|
|
433
|
+
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
465
434
|
);
|
|
435
|
+
messagesRef.current = next;
|
|
436
|
+
setMessages(next);
|
|
466
437
|
}, []);
|
|
467
|
-
const send = (0,
|
|
438
|
+
const send = (0, import_react.useCallback)(
|
|
468
439
|
async (text, opts) => {
|
|
469
440
|
const trimmed = text.trim();
|
|
470
441
|
const attachments = opts?.attachments ?? [];
|
|
471
|
-
if (!trimmed && attachments.length === 0 ||
|
|
442
|
+
if (!trimmed && attachments.length === 0 || abortRef.current || !restored) return;
|
|
472
443
|
const outgoing = trimmed || "Please take a look at the attached file(s).";
|
|
473
444
|
const assistantId = nextId("a");
|
|
474
445
|
setError(void 0);
|
|
446
|
+
setLoadedSkills(null);
|
|
475
447
|
setBusy(true);
|
|
476
|
-
|
|
477
|
-
...current,
|
|
448
|
+
const next = [
|
|
449
|
+
...messagesRef.current,
|
|
478
450
|
newUserMessage(
|
|
479
451
|
nextId("u"),
|
|
480
452
|
trimmed,
|
|
481
453
|
attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
|
|
482
454
|
),
|
|
483
455
|
newAssistantMessage(assistantId)
|
|
484
|
-
]
|
|
456
|
+
];
|
|
457
|
+
messagesRef.current = next;
|
|
458
|
+
setMessages(next);
|
|
485
459
|
const abort = new AbortController();
|
|
486
460
|
abortRef.current = abort;
|
|
487
461
|
try {
|
|
@@ -496,6 +470,10 @@ function useFrankieChat() {
|
|
|
496
470
|
signal: abort.signal
|
|
497
471
|
},
|
|
498
472
|
(event) => {
|
|
473
|
+
if (event.type === "skills_loaded") {
|
|
474
|
+
setLoadedSkills(event.skills);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
499
477
|
if (event.type === "ping" || event.type === "session") return;
|
|
500
478
|
if (event.type === "text_delta") receivedText = true;
|
|
501
479
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
@@ -522,32 +500,36 @@ function useFrankieChat() {
|
|
|
522
500
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
523
501
|
}
|
|
524
502
|
} finally {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
503
|
+
patchAssistant(
|
|
504
|
+
assistantId,
|
|
505
|
+
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
506
|
+
);
|
|
507
|
+
const transcript = messagesRef.current;
|
|
508
|
+
if (transcript.length) {
|
|
509
|
+
await client.syncConversation(transcript, conversationTitle(transcript)).catch(() => void 0);
|
|
531
510
|
}
|
|
511
|
+
if (mounted.current) setBusy(false);
|
|
532
512
|
abortRef.current = null;
|
|
533
513
|
}
|
|
534
514
|
},
|
|
535
|
-
[busy, client, context, patchAssistant]
|
|
515
|
+
[busy, client, context, patchAssistant, restored]
|
|
536
516
|
);
|
|
537
|
-
const stop = (0,
|
|
538
|
-
const reset = (0,
|
|
517
|
+
const stop = (0, import_react.useCallback)(() => abortRef.current?.abort(), []);
|
|
518
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
539
519
|
abortRef.current?.abort();
|
|
540
520
|
client.resetSession();
|
|
521
|
+
messagesRef.current = [];
|
|
541
522
|
setMessages([]);
|
|
542
523
|
setError(void 0);
|
|
543
524
|
setBusy(false);
|
|
544
525
|
setRestored(true);
|
|
526
|
+
setLoadedSkills(null);
|
|
545
527
|
try {
|
|
546
528
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
547
529
|
} catch {
|
|
548
530
|
}
|
|
549
531
|
}, [client]);
|
|
550
|
-
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
532
|
+
return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
|
|
551
533
|
}
|
|
552
534
|
function conversationTitle(messages) {
|
|
553
535
|
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
@@ -556,6 +538,68 @@ function conversationTitle(messages) {
|
|
|
556
538
|
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
557
539
|
}
|
|
558
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
|
+
|
|
559
603
|
// src/components/chat/FrankieWidget.tsx
|
|
560
604
|
var import_lucide_react5 = require("lucide-react");
|
|
561
605
|
var import_react9 = require("react");
|
|
@@ -1158,7 +1202,7 @@ function ChatPanel({
|
|
|
1158
1202
|
}) {
|
|
1159
1203
|
const { config, configError, loading } = useFrankie();
|
|
1160
1204
|
const theme = useFrankieTheme();
|
|
1161
|
-
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
1205
|
+
const { messages, busy, restoring, loadedSkills, error, send, stop, reset } = useFrankieChat();
|
|
1162
1206
|
const scrollRef = (0, import_react8.useRef)(null);
|
|
1163
1207
|
const features = config?.features;
|
|
1164
1208
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
@@ -1213,6 +1257,16 @@ function ChatPanel({
|
|
|
1213
1257
|
)
|
|
1214
1258
|
) }) }) }),
|
|
1215
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,
|
|
1216
1270
|
error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1217
1271
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1218
1272
|
Composer,
|
|
@@ -1220,7 +1274,7 @@ function ChatPanel({
|
|
|
1220
1274
|
onSend: send,
|
|
1221
1275
|
onStop: stop,
|
|
1222
1276
|
busy,
|
|
1223
|
-
disabled: Boolean(configError),
|
|
1277
|
+
disabled: loading || restoring || Boolean(configError),
|
|
1224
1278
|
...placeholder ? { placeholder } : {},
|
|
1225
1279
|
...autoFocus ? { autoFocus } : {},
|
|
1226
1280
|
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
@@ -1240,6 +1294,7 @@ function FrankieWidget({
|
|
|
1240
1294
|
position = "bottom-right",
|
|
1241
1295
|
offset = 24,
|
|
1242
1296
|
defaultOpen = false,
|
|
1297
|
+
zIndex = 20,
|
|
1243
1298
|
open: controlledOpen,
|
|
1244
1299
|
onOpenChange,
|
|
1245
1300
|
renderLauncher,
|
|
@@ -1268,11 +1323,11 @@ function FrankieWidget({
|
|
|
1268
1323
|
"div",
|
|
1269
1324
|
{
|
|
1270
1325
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1271
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex:
|
|
1326
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1272
1327
|
"data-frankie-mode": mode,
|
|
1273
1328
|
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1274
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
|
|
1275
|
-
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)(
|
|
1276
1331
|
Button,
|
|
1277
1332
|
{
|
|
1278
1333
|
size: "icon",
|
|
@@ -1283,7 +1338,7 @@ function FrankieWidget({
|
|
|
1283
1338
|
title: label,
|
|
1284
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" })
|
|
1285
1340
|
}
|
|
1286
|
-
)
|
|
1341
|
+
) })
|
|
1287
1342
|
] })
|
|
1288
1343
|
}
|
|
1289
1344
|
);
|
|
@@ -1306,38 +1361,145 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1306
1361
|
}
|
|
1307
1362
|
|
|
1308
1363
|
// src/components/chat/FrankieChatPage.tsx
|
|
1364
|
+
var import_react10 = require("react");
|
|
1365
|
+
var import_lucide_react6 = require("lucide-react");
|
|
1309
1366
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1367
|
+
function PageEmptyState() {
|
|
1368
|
+
const { config } = useFrankie();
|
|
1369
|
+
const { send, busy, restoring } = useFrankieChat();
|
|
1370
|
+
const name = config?.branding.name ?? "Frankie";
|
|
1371
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1372
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1373
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
|
|
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?" }),
|
|
1376
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1377
|
+
] }),
|
|
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: [
|
|
1379
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: prompt }),
|
|
1380
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("b", { "aria-hidden": true, children: "\u2197" })
|
|
1381
|
+
] }, prompt)) })
|
|
1382
|
+
] });
|
|
1383
|
+
}
|
|
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);
|
|
1396
|
+
const name = config?.branding.name ?? "Frankie";
|
|
1397
|
+
const refresh = (0, import_react10.useCallback)(async () => {
|
|
1398
|
+
if (!showHistory || !config || config.allowAnonymous) {
|
|
1399
|
+
setHistoryError("");
|
|
1400
|
+
return;
|
|
1339
1401
|
}
|
|
1340
|
-
|
|
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
|
+
] })
|
|
1491
|
+
] }),
|
|
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, {}) }) })
|
|
1493
|
+
] }),
|
|
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
|
|
1497
|
+
] });
|
|
1498
|
+
}
|
|
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 }) }) });
|
|
1341
1503
|
}
|
|
1342
1504
|
|
|
1343
1505
|
// src/components/ui/separator.tsx
|