@frankieai/react 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frankie.css +1 -1
- package/dist/index.cjs +394 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -11
- package/dist/index.d.ts +16 -11
- package/dist/index.js +388 -267
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/AgentContext.tsx +22 -0
- package/src/components/chat/ChatPanel.tsx +10 -7
- package/src/components/chat/Composer.tsx +6 -5
- package/src/components/chat/FrankieChatPage.tsx +73 -102
- package/src/components/chat/FrankieWidget.tsx +7 -3
- package/src/components/chat/MessageBubble.tsx +7 -5
- package/src/hooks/FrankieProvider.tsx +12 -4
- package/src/hooks/useFrankieChat.ts +19 -6
- package/src/lib/client.ts +16 -6
- package/src/styles/frankie.css +83 -1
- package/src/widget.test.tsx +64 -1
package/dist/index.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,8 +538,70 @@ 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
|
-
var
|
|
604
|
+
var import_lucide_react6 = require("lucide-react");
|
|
574
605
|
var import_react9 = require("react");
|
|
575
606
|
|
|
576
607
|
// src/lib/theme.ts
|
|
@@ -631,7 +662,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
631
662
|
}
|
|
632
663
|
|
|
633
664
|
// src/components/chat/ChatPanel.tsx
|
|
634
|
-
var
|
|
665
|
+
var import_lucide_react5 = require("lucide-react");
|
|
635
666
|
var import_react8 = require("react");
|
|
636
667
|
|
|
637
668
|
// src/components/ui/avatar.tsx
|
|
@@ -690,16 +721,51 @@ var ScrollArea = (0, import_react3.forwardRef)(
|
|
|
690
721
|
}
|
|
691
722
|
);
|
|
692
723
|
|
|
693
|
-
// src/components/chat/
|
|
724
|
+
// src/components/chat/AgentContext.tsx
|
|
694
725
|
var import_lucide_react = require("lucide-react");
|
|
726
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
727
|
+
function AgentContext() {
|
|
728
|
+
const { loading, configError } = useFrankie();
|
|
729
|
+
const { loadedSkills, busy, restoring, error } = useFrankieChat();
|
|
730
|
+
const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
|
|
731
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-agent-context-wrap", children: [
|
|
732
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { className: "frankie-agent-context", children: [
|
|
733
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("summary", { "aria-label": "Agent context", children: [
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react.Sparkles, { size: 17, "aria-hidden": "true" }),
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Agent context" }),
|
|
736
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-context-count", children: loadedSkills === null ? "Not checked" : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"}` }),
|
|
737
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "frankie-context-status", "data-active": status === "Active", children: [
|
|
738
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("i", { "aria-hidden": "true" }),
|
|
739
|
+
status
|
|
740
|
+
] }),
|
|
741
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react.ArrowUpRight, { size: 15, "aria-hidden": "true" })
|
|
742
|
+
] }),
|
|
743
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-context-detail", children: [
|
|
744
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: loadedSkills === null ? "Skills are checked when you send a message" : loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn" }),
|
|
745
|
+
loadedSkills?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { children: loadedSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
|
|
746
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: skill.name }),
|
|
747
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("small", { children: [
|
|
748
|
+
"v",
|
|
749
|
+
skill.revision
|
|
750
|
+
] })
|
|
751
|
+
] }, skill.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
|
|
752
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
|
|
753
|
+
] })
|
|
754
|
+
] }),
|
|
755
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
|
|
756
|
+
] });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/components/chat/Composer.tsx
|
|
760
|
+
var import_lucide_react2 = require("lucide-react");
|
|
695
761
|
var import_react5 = require("react");
|
|
696
762
|
|
|
697
763
|
// src/components/ui/textarea.tsx
|
|
698
764
|
var import_react4 = require("react");
|
|
699
|
-
var
|
|
765
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
700
766
|
var Textarea = (0, import_react4.forwardRef)(
|
|
701
767
|
function Textarea2({ className, ...props }, ref) {
|
|
702
|
-
return /* @__PURE__ */ (0,
|
|
768
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
703
769
|
"textarea",
|
|
704
770
|
{
|
|
705
771
|
ref,
|
|
@@ -714,7 +780,7 @@ var Textarea = (0, import_react4.forwardRef)(
|
|
|
714
780
|
);
|
|
715
781
|
|
|
716
782
|
// src/components/chat/Composer.tsx
|
|
717
|
-
var
|
|
783
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
718
784
|
var MAX_ROWS_PX = 140;
|
|
719
785
|
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
720
786
|
const [value, setValue] = (0, import_react5.useState)("");
|
|
@@ -776,46 +842,46 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
776
842
|
});
|
|
777
843
|
}, [busy, disabled, files, modelPicker?.value, onSend, value]);
|
|
778
844
|
const onKeyDown = (event) => {
|
|
779
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
845
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
780
846
|
event.preventDefault();
|
|
781
847
|
submit();
|
|
782
848
|
}
|
|
783
849
|
};
|
|
784
850
|
const nothingToSend = value.trim().length === 0 && files.length === 0;
|
|
785
|
-
return /* @__PURE__ */ (0,
|
|
851
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
786
852
|
"form",
|
|
787
853
|
{
|
|
788
|
-
className: "flex flex-col gap-2",
|
|
854
|
+
className: "frankie-composer flex flex-col gap-2",
|
|
789
855
|
onSubmit: (event) => {
|
|
790
856
|
event.preventDefault();
|
|
791
857
|
submit();
|
|
792
858
|
},
|
|
793
859
|
children: [
|
|
794
|
-
files.length > 0 ? /* @__PURE__ */ (0,
|
|
860
|
+
files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
795
861
|
"span",
|
|
796
862
|
{
|
|
797
863
|
className: "inline-flex max-w-[14rem] items-center gap-1.5 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
|
|
798
864
|
children: [
|
|
799
|
-
/* @__PURE__ */ (0,
|
|
800
|
-
/* @__PURE__ */ (0,
|
|
801
|
-
/* @__PURE__ */ (0,
|
|
865
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
866
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate", children: file.name }),
|
|
867
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
802
868
|
"button",
|
|
803
869
|
{
|
|
804
870
|
type: "button",
|
|
805
871
|
onClick: () => removeFile(file.id),
|
|
806
872
|
"aria-label": `Remove ${file.name}`,
|
|
807
873
|
className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
|
|
808
|
-
children: /* @__PURE__ */ (0,
|
|
874
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.X, { className: "size-3" })
|
|
809
875
|
}
|
|
810
876
|
)
|
|
811
877
|
]
|
|
812
878
|
},
|
|
813
879
|
file.id
|
|
814
880
|
)) }) : null,
|
|
815
|
-
fileError ? /* @__PURE__ */ (0,
|
|
816
|
-
/* @__PURE__ */ (0,
|
|
817
|
-
canAttach ? /* @__PURE__ */ (0,
|
|
818
|
-
/* @__PURE__ */ (0,
|
|
881
|
+
fileError ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
|
|
882
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "frankie-composer-controls flex items-end gap-2", children: [
|
|
883
|
+
canAttach ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
884
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
819
885
|
"input",
|
|
820
886
|
{
|
|
821
887
|
ref: fileInputRef,
|
|
@@ -829,7 +895,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
829
895
|
}
|
|
830
896
|
}
|
|
831
897
|
),
|
|
832
|
-
/* @__PURE__ */ (0,
|
|
898
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
833
899
|
Button,
|
|
834
900
|
{
|
|
835
901
|
type: "button",
|
|
@@ -839,11 +905,11 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
839
905
|
onClick: () => fileInputRef.current?.click(),
|
|
840
906
|
"aria-label": "Attach files",
|
|
841
907
|
title: "Attach files",
|
|
842
|
-
children: /* @__PURE__ */ (0,
|
|
908
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Plus, {})
|
|
843
909
|
}
|
|
844
910
|
)
|
|
845
911
|
] }) : null,
|
|
846
|
-
/* @__PURE__ */ (0,
|
|
912
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
847
913
|
Textarea,
|
|
848
914
|
{
|
|
849
915
|
ref: textareaRef,
|
|
@@ -857,7 +923,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
857
923
|
"aria-label": "Message"
|
|
858
924
|
}
|
|
859
925
|
),
|
|
860
|
-
models.length > 0 ? /* @__PURE__ */ (0,
|
|
926
|
+
models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
861
927
|
"select",
|
|
862
928
|
{
|
|
863
929
|
value: modelPicker?.value ?? models[0]?.id,
|
|
@@ -865,10 +931,10 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
865
931
|
disabled,
|
|
866
932
|
"aria-label": "Model",
|
|
867
933
|
className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
|
|
868
|
-
children: models.map((model) => /* @__PURE__ */ (0,
|
|
934
|
+
children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: model.id, children: model.label }, model.id))
|
|
869
935
|
}
|
|
870
936
|
) : null,
|
|
871
|
-
busy ? /* @__PURE__ */ (0,
|
|
937
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.ArrowUp, {}) })
|
|
872
938
|
] })
|
|
873
939
|
]
|
|
874
940
|
}
|
|
@@ -893,14 +959,14 @@ function readAsBase64(file) {
|
|
|
893
959
|
}
|
|
894
960
|
|
|
895
961
|
// src/components/chat/MessageBubble.tsx
|
|
896
|
-
var
|
|
962
|
+
var import_lucide_react4 = require("lucide-react");
|
|
897
963
|
var import_react7 = require("react");
|
|
898
964
|
|
|
899
965
|
// src/components/chat/Markdown.tsx
|
|
900
966
|
var import_react6 = require("react");
|
|
901
|
-
var
|
|
967
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
902
968
|
function Markdown({ text, className }) {
|
|
903
|
-
return /* @__PURE__ */ (0,
|
|
969
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
|
|
904
970
|
}
|
|
905
971
|
function renderBlocks(text) {
|
|
906
972
|
const lines = text.split("\n");
|
|
@@ -910,13 +976,13 @@ function renderBlocks(text) {
|
|
|
910
976
|
let code = null;
|
|
911
977
|
const flushParagraph = () => {
|
|
912
978
|
if (paragraph.length === 0) return;
|
|
913
|
-
blocks.push(/* @__PURE__ */ (0,
|
|
979
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
914
980
|
paragraph = [];
|
|
915
981
|
};
|
|
916
982
|
const flushList = () => {
|
|
917
983
|
if (!list) return;
|
|
918
|
-
const items = list.items.map((item, index) => /* @__PURE__ */ (0,
|
|
919
|
-
blocks.push(list.ordered ? /* @__PURE__ */ (0,
|
|
984
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: renderInline(item) }, index));
|
|
985
|
+
blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: items }, `l${blocks.length}`));
|
|
920
986
|
list = null;
|
|
921
987
|
};
|
|
922
988
|
for (const line of lines) {
|
|
@@ -924,7 +990,7 @@ function renderBlocks(text) {
|
|
|
924
990
|
if (fence) {
|
|
925
991
|
if (code) {
|
|
926
992
|
blocks.push(
|
|
927
|
-
/* @__PURE__ */ (0,
|
|
993
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
928
994
|
);
|
|
929
995
|
code = null;
|
|
930
996
|
} else {
|
|
@@ -959,7 +1025,7 @@ function renderBlocks(text) {
|
|
|
959
1025
|
if (heading) {
|
|
960
1026
|
flushParagraph();
|
|
961
1027
|
flushList();
|
|
962
|
-
blocks.push(/* @__PURE__ */ (0,
|
|
1028
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
|
|
963
1029
|
continue;
|
|
964
1030
|
}
|
|
965
1031
|
flushList();
|
|
@@ -967,7 +1033,7 @@ function renderBlocks(text) {
|
|
|
967
1033
|
}
|
|
968
1034
|
if (code) {
|
|
969
1035
|
blocks.push(
|
|
970
|
-
/* @__PURE__ */ (0,
|
|
1036
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
971
1037
|
);
|
|
972
1038
|
}
|
|
973
1039
|
flushParagraph();
|
|
@@ -982,28 +1048,28 @@ function renderInline(text) {
|
|
|
982
1048
|
const token = match[0];
|
|
983
1049
|
const start = match.index;
|
|
984
1050
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
985
|
-
nodes.push(/* @__PURE__ */ (0,
|
|
1051
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
|
|
986
1052
|
lastIndex = start + token.length;
|
|
987
1053
|
}
|
|
988
1054
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
989
1055
|
return nodes;
|
|
990
1056
|
}
|
|
991
1057
|
function renderToken(token) {
|
|
992
|
-
if (token.startsWith("**")) return /* @__PURE__ */ (0,
|
|
993
|
-
if (token.startsWith("`")) return /* @__PURE__ */ (0,
|
|
1058
|
+
if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: token.slice(2, -2) });
|
|
1059
|
+
if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: token.slice(1, -1) });
|
|
994
1060
|
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
995
1061
|
const href = link ? link[2] ?? "" : token;
|
|
996
1062
|
const label = link ? link[1] ?? "" : token;
|
|
997
1063
|
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
998
|
-
return /* @__PURE__ */ (0,
|
|
1064
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
999
1065
|
}
|
|
1000
1066
|
|
|
1001
1067
|
// src/components/chat/ToolActivityList.tsx
|
|
1002
|
-
var
|
|
1068
|
+
var import_lucide_react3 = require("lucide-react");
|
|
1003
1069
|
|
|
1004
1070
|
// src/components/ui/badge.tsx
|
|
1005
1071
|
var import_class_variance_authority2 = require("class-variance-authority");
|
|
1006
|
-
var
|
|
1072
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1007
1073
|
var badgeVariants = (0, import_class_variance_authority2.cva)(
|
|
1008
1074
|
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
1009
1075
|
{
|
|
@@ -1023,44 +1089,44 @@ function Badge({
|
|
|
1023
1089
|
variant,
|
|
1024
1090
|
...props
|
|
1025
1091
|
}) {
|
|
1026
|
-
return /* @__PURE__ */ (0,
|
|
1092
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
1027
1093
|
}
|
|
1028
1094
|
|
|
1029
1095
|
// src/components/chat/ToolActivityList.tsx
|
|
1030
|
-
var
|
|
1096
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1031
1097
|
function ToolActivityList({ tools, renderTool }) {
|
|
1032
1098
|
if (tools.length === 0) return null;
|
|
1033
1099
|
const running = tools.some((tool) => tool.status === "running");
|
|
1034
|
-
return /* @__PURE__ */ (0,
|
|
1035
|
-
/* @__PURE__ */ (0,
|
|
1036
|
-
/* @__PURE__ */ (0,
|
|
1037
|
-
/* @__PURE__ */ (0,
|
|
1038
|
-
/* @__PURE__ */ (0,
|
|
1100
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1101
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
|
|
1102
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1103
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: running ? "Working" : "Activity" }),
|
|
1104
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-[11px]", children: [
|
|
1039
1105
|
"\xB7 ",
|
|
1040
1106
|
tools.length,
|
|
1041
1107
|
" tool call",
|
|
1042
1108
|
tools.length === 1 ? "" : "s"
|
|
1043
1109
|
] })
|
|
1044
1110
|
] }),
|
|
1045
|
-
/* @__PURE__ */ (0,
|
|
1111
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
|
|
1046
1112
|
] });
|
|
1047
1113
|
}
|
|
1048
1114
|
function ToolActivityRow({ tool }) {
|
|
1049
|
-
return /* @__PURE__ */ (0,
|
|
1050
|
-
/* @__PURE__ */ (0,
|
|
1051
|
-
/* @__PURE__ */ (0,
|
|
1115
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolIcon, { status: tool.status }),
|
|
1117
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: tool.label })
|
|
1052
1118
|
] });
|
|
1053
1119
|
}
|
|
1054
1120
|
function ToolIcon({ status }) {
|
|
1055
1121
|
switch (status) {
|
|
1056
1122
|
case "running":
|
|
1057
|
-
return /* @__PURE__ */ (0,
|
|
1123
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
1058
1124
|
case "done":
|
|
1059
|
-
return /* @__PURE__ */ (0,
|
|
1125
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3", "aria-hidden": "true" });
|
|
1060
1126
|
case "denied":
|
|
1061
|
-
return /* @__PURE__ */ (0,
|
|
1127
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
1062
1128
|
default:
|
|
1063
|
-
return /* @__PURE__ */ (0,
|
|
1129
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.X, { className: "size-3", "aria-hidden": "true" });
|
|
1064
1130
|
}
|
|
1065
1131
|
}
|
|
1066
1132
|
function badgeVariant(status) {
|
|
@@ -1071,7 +1137,7 @@ function badgeVariant(status) {
|
|
|
1071
1137
|
}
|
|
1072
1138
|
|
|
1073
1139
|
// src/components/chat/MessageBubble.tsx
|
|
1074
|
-
var
|
|
1140
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1075
1141
|
function MessageBubble({
|
|
1076
1142
|
message,
|
|
1077
1143
|
assistantName,
|
|
@@ -1080,33 +1146,33 @@ function MessageBubble({
|
|
|
1080
1146
|
renderTool
|
|
1081
1147
|
}) {
|
|
1082
1148
|
if (message.role === "user") {
|
|
1083
|
-
return /* @__PURE__ */ (0,
|
|
1084
|
-
message.attachments?.length ? /* @__PURE__ */ (0,
|
|
1149
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
|
|
1150
|
+
message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1085
1151
|
"span",
|
|
1086
1152
|
{
|
|
1087
1153
|
className: "inline-flex max-w-[12rem] items-center gap-1 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
|
|
1088
1154
|
children: [
|
|
1089
|
-
/* @__PURE__ */ (0,
|
|
1090
|
-
/* @__PURE__ */ (0,
|
|
1155
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
1156
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: file.name })
|
|
1091
1157
|
]
|
|
1092
1158
|
},
|
|
1093
1159
|
`${file.name}-${index}`
|
|
1094
1160
|
)) }) : null,
|
|
1095
|
-
message.text ? /* @__PURE__ */ (0,
|
|
1096
|
-
/* @__PURE__ */ (0,
|
|
1097
|
-
/* @__PURE__ */ (0,
|
|
1161
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1162
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1163
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text })
|
|
1098
1164
|
] }) : null
|
|
1099
1165
|
] });
|
|
1100
1166
|
}
|
|
1101
1167
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
1102
|
-
return /* @__PURE__ */ (0,
|
|
1103
|
-
/* @__PURE__ */ (0,
|
|
1104
|
-
/* @__PURE__ */ (0,
|
|
1105
|
-
message.text ? /* @__PURE__ */ (0,
|
|
1168
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
|
|
1169
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1170
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1171
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text }) : null
|
|
1106
1172
|
] }),
|
|
1107
|
-
/* @__PURE__ */ (0,
|
|
1108
|
-
message.status === "error" ? /* @__PURE__ */ (0,
|
|
1109
|
-
showToolActivity ? /* @__PURE__ */ (0,
|
|
1173
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
|
|
1174
|
+
message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Markdown, { text: message.text, className: "text-sm" }),
|
|
1175
|
+
showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
1110
1176
|
] })
|
|
1111
1177
|
] });
|
|
1112
1178
|
}
|
|
@@ -1120,28 +1186,31 @@ function MessageCopyButton({ text }) {
|
|
|
1120
1186
|
} catch {
|
|
1121
1187
|
}
|
|
1122
1188
|
};
|
|
1123
|
-
return /* @__PURE__ */ (0,
|
|
1189
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1124
1190
|
}
|
|
1125
1191
|
function TypingDots() {
|
|
1126
|
-
return /* @__PURE__ */ (0,
|
|
1127
|
-
"span",
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1192
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
|
|
1193
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
|
|
1194
|
+
[0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1195
|
+
"span",
|
|
1196
|
+
{
|
|
1197
|
+
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
1198
|
+
style: { animationDelay: `${index * 150}ms` }
|
|
1199
|
+
},
|
|
1200
|
+
index
|
|
1201
|
+
))
|
|
1202
|
+
] });
|
|
1134
1203
|
}
|
|
1135
1204
|
|
|
1136
1205
|
// src/components/chat/SuggestedPrompts.tsx
|
|
1137
|
-
var
|
|
1206
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1138
1207
|
function SuggestedPrompts({
|
|
1139
1208
|
prompts,
|
|
1140
1209
|
onPick,
|
|
1141
1210
|
disabled
|
|
1142
1211
|
}) {
|
|
1143
1212
|
if (prompts.length === 0) return null;
|
|
1144
|
-
return /* @__PURE__ */ (0,
|
|
1213
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1145
1214
|
Button,
|
|
1146
1215
|
{
|
|
1147
1216
|
type: "button",
|
|
@@ -1156,7 +1225,7 @@ function SuggestedPrompts({
|
|
|
1156
1225
|
}
|
|
1157
1226
|
|
|
1158
1227
|
// src/components/chat/ChatPanel.tsx
|
|
1159
|
-
var
|
|
1228
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
1160
1229
|
function ChatPanel({
|
|
1161
1230
|
className,
|
|
1162
1231
|
header,
|
|
@@ -1171,7 +1240,7 @@ function ChatPanel({
|
|
|
1171
1240
|
}) {
|
|
1172
1241
|
const { config, configError, loading } = useFrankie();
|
|
1173
1242
|
const theme = useFrankieTheme();
|
|
1174
|
-
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
1243
|
+
const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
|
|
1175
1244
|
const scrollRef = (0, import_react8.useRef)(null);
|
|
1176
1245
|
const features = config?.features;
|
|
1177
1246
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
@@ -1184,11 +1253,12 @@ function ChatPanel({
|
|
|
1184
1253
|
}, [messages]);
|
|
1185
1254
|
const name = config?.branding.name ?? "Assistant";
|
|
1186
1255
|
const mode = resolveMode(theme?.mode);
|
|
1187
|
-
return /* @__PURE__ */ (0,
|
|
1256
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1188
1257
|
Card,
|
|
1189
1258
|
{
|
|
1190
1259
|
className: cn(
|
|
1191
|
-
"frankie-root",
|
|
1260
|
+
"frankie-root frankie-chat-panel",
|
|
1261
|
+
fill && "frankie-chat-panel-fill",
|
|
1192
1262
|
fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
|
|
1193
1263
|
mode === "dark" && "dark",
|
|
1194
1264
|
className
|
|
@@ -1196,24 +1266,25 @@ function ChatPanel({
|
|
|
1196
1266
|
style: themeToCssVars(theme),
|
|
1197
1267
|
"data-frankie-mode": mode,
|
|
1198
1268
|
children: [
|
|
1199
|
-
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0,
|
|
1200
|
-
/* @__PURE__ */ (0,
|
|
1201
|
-
/* @__PURE__ */ (0,
|
|
1202
|
-
/* @__PURE__ */ (0,
|
|
1203
|
-
config?.branding.tagline ? /* @__PURE__ */ (0,
|
|
1269
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardHeader, { children: [
|
|
1270
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
1271
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1272
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardTitle, { children: name }),
|
|
1273
|
+
config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardDescription, { children: config.branding.tagline }) : null
|
|
1204
1274
|
] }),
|
|
1205
|
-
/* @__PURE__ */ (0,
|
|
1206
|
-
onClose ? /* @__PURE__ */ (0,
|
|
1275
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RotateCcw, {}) }),
|
|
1276
|
+
onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, {}) }) : null
|
|
1207
1277
|
] }),
|
|
1208
|
-
/* @__PURE__ */ (0,
|
|
1209
|
-
|
|
1210
|
-
/* @__PURE__ */ (0,
|
|
1211
|
-
/* @__PURE__ */ (0,
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
/* @__PURE__ */ (0,
|
|
1215
|
-
|
|
1216
|
-
|
|
1278
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AgentContext, {}),
|
|
1279
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1280
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: configError }),
|
|
1282
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1283
|
+
] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
1284
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1285
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1286
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
|
|
1287
|
+
(message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1217
1288
|
MessageBubble,
|
|
1218
1289
|
{
|
|
1219
1290
|
message,
|
|
@@ -1225,15 +1296,15 @@ function ChatPanel({
|
|
|
1225
1296
|
message.id
|
|
1226
1297
|
)
|
|
1227
1298
|
) }) }) }),
|
|
1228
|
-
/* @__PURE__ */ (0,
|
|
1229
|
-
error ? /* @__PURE__ */ (0,
|
|
1230
|
-
/* @__PURE__ */ (0,
|
|
1299
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
|
|
1300
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1301
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1231
1302
|
Composer,
|
|
1232
1303
|
{
|
|
1233
1304
|
onSend: send,
|
|
1234
1305
|
onStop: stop,
|
|
1235
1306
|
busy,
|
|
1236
|
-
disabled: Boolean(configError),
|
|
1307
|
+
disabled: loading || restoring || Boolean(configError),
|
|
1237
1308
|
...placeholder ? { placeholder } : {},
|
|
1238
1309
|
...autoFocus ? { autoFocus } : {},
|
|
1239
1310
|
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
@@ -1248,11 +1319,12 @@ function ChatPanel({
|
|
|
1248
1319
|
}
|
|
1249
1320
|
|
|
1250
1321
|
// src/components/chat/FrankieWidget.tsx
|
|
1251
|
-
var
|
|
1322
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1252
1323
|
function FrankieWidget({
|
|
1253
1324
|
position = "bottom-right",
|
|
1254
1325
|
offset = 24,
|
|
1255
1326
|
defaultOpen = false,
|
|
1327
|
+
zIndex = 20,
|
|
1256
1328
|
open: controlledOpen,
|
|
1257
1329
|
onOpenChange,
|
|
1258
1330
|
renderLauncher,
|
|
@@ -1277,15 +1349,15 @@ function FrankieWidget({
|
|
|
1277
1349
|
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
1278
1350
|
const mode = resolveMode(theme?.mode);
|
|
1279
1351
|
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
1280
|
-
return /* @__PURE__ */ (0,
|
|
1352
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1281
1353
|
"div",
|
|
1282
1354
|
{
|
|
1283
1355
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1284
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex:
|
|
1356
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1285
1357
|
"data-frankie-mode": mode,
|
|
1286
|
-
children: /* @__PURE__ */ (0,
|
|
1287
|
-
open ? /* @__PURE__ */ (0,
|
|
1288
|
-
renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0,
|
|
1358
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1359
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1360
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1289
1361
|
Button,
|
|
1290
1362
|
{
|
|
1291
1363
|
size: "icon",
|
|
@@ -1294,125 +1366,174 @@ function FrankieWidget({
|
|
|
1294
1366
|
"aria-expanded": open,
|
|
1295
1367
|
"aria-label": open ? "Close the assistant" : label,
|
|
1296
1368
|
title: label,
|
|
1297
|
-
children: open ? /* @__PURE__ */ (0,
|
|
1369
|
+
children: open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.MessageCircle, { className: "size-5" })
|
|
1298
1370
|
}
|
|
1299
|
-
)
|
|
1371
|
+
) })
|
|
1300
1372
|
] })
|
|
1301
1373
|
}
|
|
1302
1374
|
);
|
|
1303
1375
|
}
|
|
1304
1376
|
|
|
1305
1377
|
// src/components/chat/FrankieInline.tsx
|
|
1306
|
-
var
|
|
1378
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1307
1379
|
function FrankieInline({ className, style, ...panelProps }) {
|
|
1308
1380
|
const theme = useFrankieTheme();
|
|
1309
1381
|
const mode = resolveMode(theme?.mode);
|
|
1310
|
-
return /* @__PURE__ */ (0,
|
|
1382
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1311
1383
|
"div",
|
|
1312
1384
|
{
|
|
1313
1385
|
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1314
1386
|
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1315
1387
|
"data-frankie-mode": mode,
|
|
1316
|
-
children: /* @__PURE__ */ (0,
|
|
1388
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, fill: true })
|
|
1317
1389
|
}
|
|
1318
1390
|
);
|
|
1319
1391
|
}
|
|
1320
1392
|
|
|
1321
1393
|
// src/components/chat/FrankieChatPage.tsx
|
|
1322
|
-
var
|
|
1394
|
+
var import_react10 = require("react");
|
|
1395
|
+
var import_lucide_react7 = require("lucide-react");
|
|
1396
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1323
1397
|
function PageEmptyState() {
|
|
1324
1398
|
const { config } = useFrankie();
|
|
1325
|
-
const { send, busy } = useFrankieChat();
|
|
1399
|
+
const { send, busy, restoring } = useFrankieChat();
|
|
1326
1400
|
const name = config?.branding.name ?? "Frankie";
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
/* @__PURE__ */ (0,
|
|
1330
|
-
|
|
1331
|
-
/* @__PURE__ */ (0,
|
|
1332
|
-
|
|
1333
|
-
" assistant"
|
|
1334
|
-
] }),
|
|
1335
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h1", { children: "What would you like to do?" }),
|
|
1336
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1401
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1402
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1403
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
1404
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1405
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { children: "How can I help?" }),
|
|
1406
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1337
1407
|
] }),
|
|
1338
|
-
|
|
1339
|
-
/* @__PURE__ */ (0,
|
|
1340
|
-
/* @__PURE__ */ (0,
|
|
1341
|
-
|
|
1342
|
-
] }, prompt)) }) : null
|
|
1408
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1409
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: prompt }),
|
|
1410
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { "aria-hidden": true, children: "\u2197" })
|
|
1411
|
+
] }, prompt)) })
|
|
1343
1412
|
] });
|
|
1344
1413
|
}
|
|
1345
|
-
function PageChrome({
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1414
|
+
function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }) {
|
|
1415
|
+
const { client, config } = useFrankie();
|
|
1416
|
+
const { busy, messages } = useFrankieChat();
|
|
1417
|
+
const theme = useFrankieTheme();
|
|
1418
|
+
const [conversations, setConversations] = (0, import_react10.useState)([]);
|
|
1419
|
+
const [historyError, setHistoryError] = (0, import_react10.useState)("");
|
|
1420
|
+
const [historyLoading, setHistoryLoading] = (0, import_react10.useState)(false);
|
|
1421
|
+
const [drawerOpen, setDrawerOpen] = (0, import_react10.useState)(false);
|
|
1422
|
+
const [query, setQuery] = (0, import_react10.useState)("");
|
|
1423
|
+
const dialog = (0, import_react10.useRef)(null);
|
|
1424
|
+
const menu = (0, import_react10.useRef)(null);
|
|
1425
|
+
const requestVersion = (0, import_react10.useRef)(0);
|
|
1426
|
+
const refresh = (0, import_react10.useCallback)(async () => {
|
|
1427
|
+
if (!showHistory || !config || config.allowAnonymous) {
|
|
1428
|
+
setHistoryError("");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const version = ++requestVersion.current;
|
|
1432
|
+
setHistoryLoading(true);
|
|
1433
|
+
setHistoryError("");
|
|
1434
|
+
try {
|
|
1435
|
+
const items = await client.listConversations();
|
|
1436
|
+
if (version === requestVersion.current) setConversations(items);
|
|
1437
|
+
} catch {
|
|
1438
|
+
if (version === requestVersion.current) setHistoryError("History couldn\u2019t load. Check your sign-in and try again.");
|
|
1439
|
+
} finally {
|
|
1440
|
+
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1441
|
+
}
|
|
1442
|
+
}, [client, showHistory, config?.allowAnonymous]);
|
|
1443
|
+
(0, import_react10.useEffect)(() => {
|
|
1444
|
+
if (!busy) void refresh();
|
|
1445
|
+
return () => {
|
|
1446
|
+
requestVersion.current++;
|
|
1447
|
+
};
|
|
1448
|
+
}, [refresh, busy]);
|
|
1449
|
+
(0, import_react10.useEffect)(() => {
|
|
1450
|
+
const node = dialog.current;
|
|
1451
|
+
if (drawerOpen) {
|
|
1452
|
+
node?.showModal?.();
|
|
1453
|
+
} else {
|
|
1454
|
+
node?.close?.();
|
|
1455
|
+
}
|
|
1456
|
+
}, [drawerOpen]);
|
|
1457
|
+
const close = () => {
|
|
1458
|
+
setDrawerOpen(false);
|
|
1459
|
+
menu.current?.focus();
|
|
1460
|
+
};
|
|
1461
|
+
const select = (id) => {
|
|
1462
|
+
if (!busy) {
|
|
1463
|
+
onSelect(id);
|
|
1464
|
+
close();
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
const history = /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
|
|
1468
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-history-heading", children: [
|
|
1469
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Conversations" }),
|
|
1470
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.X, { size: 18 }) })
|
|
1471
|
+
] }),
|
|
1472
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1473
|
+
onNew();
|
|
1474
|
+
close();
|
|
1475
|
+
}, children: [
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 17 }),
|
|
1477
|
+
"New chat"
|
|
1478
|
+
] }),
|
|
1479
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "frankie-history-search", children: [
|
|
1480
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "Search conversations" }),
|
|
1481
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1482
|
+
] }),
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1484
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: item.title }),
|
|
1485
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1486
|
+
] }, item.id)) }),
|
|
1487
|
+
historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { role: "status", children: [
|
|
1488
|
+
historyError,
|
|
1489
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1490
|
+
] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1491
|
+
navigation ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1492
|
+
] });
|
|
1493
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1494
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { className: "frankie-history-desktop", children: history }) : null,
|
|
1495
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-main", children: [
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("header", { className: "frankie-page-header", children: [
|
|
1497
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
1498
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsx)(import_lucide_react7.Menu, { size: 20 }) }) : null,
|
|
1499
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
|
|
1500
|
+
] }),
|
|
1501
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
1502
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1503
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 16 }),
|
|
1504
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "New chat" })
|
|
1505
|
+
] }),
|
|
1506
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("details", { className: "frankie-chat-settings", children: [
|
|
1507
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Settings, { size: 19 }) }),
|
|
1508
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
1509
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Chat settings" }),
|
|
1510
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "History syncs automatically for signed-in users." }),
|
|
1511
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1512
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.History, { size: 16 }),
|
|
1513
|
+
"Refresh history"
|
|
1514
|
+
] }),
|
|
1515
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: "Skills are checked at the start of each message." })
|
|
1516
|
+
] })
|
|
1517
|
+
] })
|
|
1518
|
+
] })
|
|
1362
1519
|
] }),
|
|
1363
|
-
/* @__PURE__ */ (0,
|
|
1520
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageEmptyState, {}) }) })
|
|
1364
1521
|
] }),
|
|
1365
|
-
/* @__PURE__ */ (0,
|
|
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
|
-
) })
|
|
1522
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1523
|
+
if (event.target === event.currentTarget) close();
|
|
1524
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { children: history }) }) : null
|
|
1377
1525
|
] });
|
|
1378
1526
|
}
|
|
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
|
-
);
|
|
1527
|
+
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1528
|
+
const [id, setId] = (0, import_react10.useState)(() => provider.conversationId ?? crypto.randomUUID());
|
|
1529
|
+
const onNew = () => setId(crypto.randomUUID());
|
|
1530
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1410
1531
|
}
|
|
1411
1532
|
|
|
1412
1533
|
// src/components/ui/separator.tsx
|
|
1413
|
-
var
|
|
1534
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1414
1535
|
function Separator({ className }) {
|
|
1415
|
-
return /* @__PURE__ */ (0,
|
|
1536
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1416
1537
|
}
|
|
1417
1538
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1418
1539
|
0 && (module.exports = {
|