@frankieai/react 0.1.1 → 0.1.5
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 +280 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +32 -18
- package/dist/index.d.ts +32 -18
- package/dist/index.js +278 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +6 -2
- package/src/components/chat/FrankieChatPage.tsx +121 -0
- package/src/components/chat/MessageBubble.tsx +22 -5
- package/src/components/chat/ToolActivityList.tsx +10 -6
- package/src/components/chat/message-actions.css +13 -0
- package/src/hooks/FrankieProvider.tsx +1 -9
- package/src/hooks/useFrankieChat.ts +91 -28
- package/src/index.ts +2 -0
- package/src/lib/client.ts +82 -17
- package/src/styles/frankie.css +173 -4
package/dist/index.js
CHANGED
|
@@ -28,13 +28,16 @@ var FrankieClient = class {
|
|
|
28
28
|
get sessionId() {
|
|
29
29
|
return this.session?.sessionId;
|
|
30
30
|
}
|
|
31
|
+
get conversationId() {
|
|
32
|
+
return this.options.conversationId;
|
|
33
|
+
}
|
|
31
34
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
32
35
|
get persist() {
|
|
33
36
|
return this.options.persist !== false && typeof window !== "undefined";
|
|
34
37
|
}
|
|
35
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
38
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
36
39
|
get namespace() {
|
|
37
|
-
return `frankie.${this.options.publishableKey}`;
|
|
40
|
+
return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
|
|
38
41
|
}
|
|
39
42
|
readStored() {
|
|
40
43
|
if (!this.persist) return void 0;
|
|
@@ -62,30 +65,80 @@ var FrankieClient = class {
|
|
|
62
65
|
});
|
|
63
66
|
return await this.unwrap(response);
|
|
64
67
|
}
|
|
65
|
-
/** Opens (or reuses) a chat session. */
|
|
66
68
|
async ensureSession(context) {
|
|
67
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4)
|
|
68
|
-
return this.session;
|
|
69
|
-
}
|
|
69
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
70
70
|
const userToken = await this.options.getUserToken?.();
|
|
71
|
+
if (this.options.getUserToken && !userToken) {
|
|
72
|
+
throw new FrankieApiError(
|
|
73
|
+
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
74
|
+
401,
|
|
75
|
+
"user_token_unavailable"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
71
78
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
72
79
|
method: "POST",
|
|
73
80
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
74
|
-
body: JSON.stringify({
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
...userToken ? { userToken } : {},
|
|
83
|
+
...context ? { context } : {},
|
|
84
|
+
...this.options.conversationId ? { conversationId: this.options.conversationId } : {}
|
|
85
|
+
})
|
|
75
86
|
});
|
|
76
87
|
this.session = await this.unwrap(response);
|
|
77
88
|
this.writeStored();
|
|
78
89
|
return this.session;
|
|
79
90
|
}
|
|
80
|
-
|
|
91
|
+
async listConversations() {
|
|
92
|
+
const userToken = await this.requireUserToken();
|
|
93
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
|
|
94
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
95
|
+
});
|
|
96
|
+
const body = await this.unwrap(response);
|
|
97
|
+
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
98
|
+
}
|
|
99
|
+
async resumeConversation(id = this.options.conversationId) {
|
|
100
|
+
if (!id) return void 0;
|
|
101
|
+
const userToken = await this.requireUserToken();
|
|
102
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
105
|
+
});
|
|
106
|
+
if (response.status === 404) return void 0;
|
|
107
|
+
const body = await this.unwrap(response);
|
|
108
|
+
this.session = body.session;
|
|
109
|
+
this.writeStored();
|
|
110
|
+
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
111
|
+
}
|
|
112
|
+
async syncConversation(messages, title) {
|
|
113
|
+
if (!this.session || !this.options.conversationId) return;
|
|
114
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
|
|
115
|
+
method: "PUT",
|
|
116
|
+
headers: {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
"x-frankie-key": this.options.publishableKey,
|
|
119
|
+
authorization: `Bearer ${this.session.sessionToken}`
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ messages, ...title ? { title } : {} })
|
|
122
|
+
});
|
|
123
|
+
await this.unwrap(response);
|
|
124
|
+
}
|
|
125
|
+
async deleteConversation(id) {
|
|
126
|
+
const userToken = await this.requireUserToken();
|
|
127
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
|
|
128
|
+
method: "DELETE",
|
|
129
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
130
|
+
});
|
|
131
|
+
if (response.status !== 404) await this.unwrap(response);
|
|
132
|
+
}
|
|
133
|
+
async requireUserToken() {
|
|
134
|
+
const token = await this.options.getUserToken?.();
|
|
135
|
+
if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
|
|
136
|
+
return token;
|
|
137
|
+
}
|
|
81
138
|
resetSession() {
|
|
82
139
|
this.session = void 0;
|
|
83
140
|
this.writeStored();
|
|
84
141
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
87
|
-
* Resolves when the turn ends.
|
|
88
|
-
*/
|
|
89
142
|
async send(input, onEvent) {
|
|
90
143
|
const session = await this.ensureSession(input.context);
|
|
91
144
|
const body = {
|
|
@@ -171,9 +224,8 @@ function FrankieProvider({
|
|
|
171
224
|
}) {
|
|
172
225
|
const client = useMemo(
|
|
173
226
|
() => new FrankieClient(clientOptions),
|
|
174
|
-
// The client holds session state; rebuild it only when the endpoint changes.
|
|
175
227
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
176
|
-
[clientOptions.baseUrl, clientOptions.publishableKey]
|
|
228
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
|
|
177
229
|
);
|
|
178
230
|
const [config, setConfig] = useState(providedConfig);
|
|
179
231
|
const [configError, setConfigError] = useState(void 0);
|
|
@@ -299,8 +351,10 @@ function useFrankieChat() {
|
|
|
299
351
|
const [messages, setMessages] = useState2([]);
|
|
300
352
|
const [busy, setBusy] = useState2(false);
|
|
301
353
|
const [error, setError] = useState2(void 0);
|
|
354
|
+
const [restored, setRestored] = useState2(false);
|
|
302
355
|
const abortRef = useRef(null);
|
|
303
356
|
const mounted = useRef(true);
|
|
357
|
+
const messagesRef = useRef([]);
|
|
304
358
|
useEffect2(() => {
|
|
305
359
|
mounted.current = true;
|
|
306
360
|
return () => {
|
|
@@ -310,28 +364,51 @@ function useFrankieChat() {
|
|
|
310
364
|
}, []);
|
|
311
365
|
const storageKey = `${client.namespace}.messages`;
|
|
312
366
|
useEffect2(() => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
367
|
+
let active = true;
|
|
368
|
+
const restore = async () => {
|
|
369
|
+
if (client.persist) {
|
|
370
|
+
try {
|
|
371
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
372
|
+
const stored = raw ? JSON.parse(raw) : [];
|
|
373
|
+
if (Array.isArray(stored) && stored.length && active) {
|
|
374
|
+
messagesRef.current = stored;
|
|
375
|
+
setMessages(stored);
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const remote = await client.resumeConversation();
|
|
382
|
+
if (active && remote?.messages.length) {
|
|
383
|
+
const restoredMessages = remote.messages;
|
|
384
|
+
messagesRef.current = restoredMessages;
|
|
385
|
+
setMessages(restoredMessages);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
} finally {
|
|
389
|
+
if (active) setRestored(true);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
void restore();
|
|
393
|
+
return () => {
|
|
394
|
+
active = false;
|
|
395
|
+
};
|
|
396
|
+
}, [client, storageKey]);
|
|
321
397
|
useEffect2(() => {
|
|
322
|
-
if (!client.persist || busy) return;
|
|
398
|
+
if (!client.persist || !restored || busy) return;
|
|
323
399
|
try {
|
|
324
400
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
325
401
|
else window.localStorage.removeItem(storageKey);
|
|
326
402
|
} catch {
|
|
327
403
|
}
|
|
328
|
-
|
|
404
|
+
if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
|
|
405
|
+
}, [messages, busy, client, storageKey, restored]);
|
|
329
406
|
const patchAssistant = useCallback((id, update) => {
|
|
330
|
-
|
|
331
|
-
(
|
|
332
|
-
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
333
|
-
)
|
|
407
|
+
const next = messagesRef.current.map(
|
|
408
|
+
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
334
409
|
);
|
|
410
|
+
messagesRef.current = next;
|
|
411
|
+
setMessages(next);
|
|
335
412
|
}, []);
|
|
336
413
|
const send = useCallback(
|
|
337
414
|
async (text, opts) => {
|
|
@@ -342,18 +419,22 @@ function useFrankieChat() {
|
|
|
342
419
|
const assistantId = nextId("a");
|
|
343
420
|
setError(void 0);
|
|
344
421
|
setBusy(true);
|
|
345
|
-
|
|
346
|
-
...current,
|
|
422
|
+
const next = [
|
|
423
|
+
...messagesRef.current,
|
|
347
424
|
newUserMessage(
|
|
348
425
|
nextId("u"),
|
|
349
426
|
trimmed,
|
|
350
427
|
attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
|
|
351
428
|
),
|
|
352
429
|
newAssistantMessage(assistantId)
|
|
353
|
-
]
|
|
430
|
+
];
|
|
431
|
+
messagesRef.current = next;
|
|
432
|
+
setMessages(next);
|
|
354
433
|
const abort = new AbortController();
|
|
355
434
|
abortRef.current = abort;
|
|
356
435
|
try {
|
|
436
|
+
let receivedText = false;
|
|
437
|
+
let receivedToolActivity = false;
|
|
357
438
|
await client.send(
|
|
358
439
|
{
|
|
359
440
|
message: outgoing,
|
|
@@ -364,9 +445,18 @@ function useFrankieChat() {
|
|
|
364
445
|
},
|
|
365
446
|
(event) => {
|
|
366
447
|
if (event.type === "ping" || event.type === "session") return;
|
|
448
|
+
if (event.type === "text_delta") receivedText = true;
|
|
449
|
+
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
367
450
|
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
368
451
|
}
|
|
369
452
|
);
|
|
453
|
+
if (!receivedText) {
|
|
454
|
+
patchAssistant(assistantId, (message) => ({
|
|
455
|
+
...message,
|
|
456
|
+
status: "done",
|
|
457
|
+
text: receivedToolActivity ? "Completed. Open Agent activity for the details." : "Frankie didn\u2019t return an answer. Please try again."
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
370
460
|
} catch (caught) {
|
|
371
461
|
if (abort.signal.aborted) {
|
|
372
462
|
patchAssistant(assistantId, (message) => ({
|
|
@@ -375,18 +465,20 @@ function useFrankieChat() {
|
|
|
375
465
|
text: message.text || "Stopped."
|
|
376
466
|
}));
|
|
377
467
|
} else {
|
|
378
|
-
const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
468
|
+
const message = caught instanceof FrankieApiError ? caught.message : caught instanceof Error && caught.message.startsWith("Frankie") ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
379
469
|
if (mounted.current) setError(message);
|
|
380
470
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
381
471
|
}
|
|
382
472
|
} finally {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
473
|
+
patchAssistant(
|
|
474
|
+
assistantId,
|
|
475
|
+
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
476
|
+
);
|
|
477
|
+
const transcript = messagesRef.current;
|
|
478
|
+
if (transcript.length) {
|
|
479
|
+
await client.syncConversation(transcript, conversationTitle(transcript)).catch(() => void 0);
|
|
389
480
|
}
|
|
481
|
+
if (mounted.current) setBusy(false);
|
|
390
482
|
abortRef.current = null;
|
|
391
483
|
}
|
|
392
484
|
},
|
|
@@ -396,9 +488,11 @@ function useFrankieChat() {
|
|
|
396
488
|
const reset = useCallback(() => {
|
|
397
489
|
abortRef.current?.abort();
|
|
398
490
|
client.resetSession();
|
|
491
|
+
messagesRef.current = [];
|
|
399
492
|
setMessages([]);
|
|
400
493
|
setError(void 0);
|
|
401
494
|
setBusy(false);
|
|
495
|
+
setRestored(true);
|
|
402
496
|
try {
|
|
403
497
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
404
498
|
} catch {
|
|
@@ -406,10 +500,16 @@ function useFrankieChat() {
|
|
|
406
500
|
}, [client]);
|
|
407
501
|
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
408
502
|
}
|
|
503
|
+
function conversationTitle(messages) {
|
|
504
|
+
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
505
|
+
if (!first) return void 0;
|
|
506
|
+
const title = first.text.trim().replace(/\s+/g, " ");
|
|
507
|
+
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
508
|
+
}
|
|
409
509
|
|
|
410
510
|
// src/components/chat/FrankieWidget.tsx
|
|
411
511
|
import { MessageCircle, X as X4 } from "lucide-react";
|
|
412
|
-
import { useEffect as useEffect5, useState as
|
|
512
|
+
import { useEffect as useEffect5, useState as useState6 } from "react";
|
|
413
513
|
|
|
414
514
|
// src/lib/theme.ts
|
|
415
515
|
function themeToCssVars(theme) {
|
|
@@ -470,7 +570,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
470
570
|
|
|
471
571
|
// src/components/chat/ChatPanel.tsx
|
|
472
572
|
import { RotateCcw, X as X3 } from "lucide-react";
|
|
473
|
-
import { useEffect as useEffect4, useRef as useRef3, useState as
|
|
573
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
|
|
474
574
|
|
|
475
575
|
// src/components/ui/avatar.tsx
|
|
476
576
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -731,7 +831,8 @@ function readAsBase64(file) {
|
|
|
731
831
|
}
|
|
732
832
|
|
|
733
833
|
// src/components/chat/MessageBubble.tsx
|
|
734
|
-
import { Paperclip as Paperclip2 } from "lucide-react";
|
|
834
|
+
import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
|
|
835
|
+
import { useState as useState4 } from "react";
|
|
735
836
|
|
|
736
837
|
// src/components/chat/Markdown.tsx
|
|
737
838
|
import { Fragment as Fragment2 } from "react";
|
|
@@ -836,7 +937,7 @@ function renderToken(token) {
|
|
|
836
937
|
}
|
|
837
938
|
|
|
838
939
|
// src/components/chat/ToolActivityList.tsx
|
|
839
|
-
import { Check, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
940
|
+
import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
840
941
|
|
|
841
942
|
// src/components/ui/badge.tsx
|
|
842
943
|
import { cva as cva2 } from "class-variance-authority";
|
|
@@ -867,7 +968,20 @@ function Badge({
|
|
|
867
968
|
import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
868
969
|
function ToolActivityList({ tools, renderTool }) {
|
|
869
970
|
if (tools.length === 0) return null;
|
|
870
|
-
|
|
971
|
+
const running = tools.some((tool) => tool.status === "running");
|
|
972
|
+
return /* @__PURE__ */ jsxs2("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
973
|
+
/* @__PURE__ */ jsxs2("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: [
|
|
974
|
+
/* @__PURE__ */ jsx10(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
975
|
+
/* @__PURE__ */ jsx10("span", { children: running ? "Working" : "Activity" }),
|
|
976
|
+
/* @__PURE__ */ jsxs2("span", { className: "text-[11px]", children: [
|
|
977
|
+
"\xB7 ",
|
|
978
|
+
tools.length,
|
|
979
|
+
" tool call",
|
|
980
|
+
tools.length === 1 ? "" : "s"
|
|
981
|
+
] })
|
|
982
|
+
] }),
|
|
983
|
+
/* @__PURE__ */ jsx10("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx10("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx10(ToolActivityRow, { tool }) }, tool.id)) })
|
|
984
|
+
] });
|
|
871
985
|
}
|
|
872
986
|
function ToolActivityRow({ tool }) {
|
|
873
987
|
return /* @__PURE__ */ jsxs2(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
@@ -916,18 +1030,36 @@ function MessageBubble({
|
|
|
916
1030
|
},
|
|
917
1031
|
`${file.name}-${index}`
|
|
918
1032
|
)) }) : null,
|
|
919
|
-
message.text ? /* @__PURE__ */
|
|
1033
|
+
message.text ? /* @__PURE__ */ jsxs3("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1034
|
+
/* @__PURE__ */ jsx11("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1035
|
+
/* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text })
|
|
1036
|
+
] }) : null
|
|
920
1037
|
] });
|
|
921
1038
|
}
|
|
922
1039
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
923
1040
|
return /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
|
|
924
|
-
/* @__PURE__ */
|
|
1041
|
+
/* @__PURE__ */ jsxs3("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1042
|
+
/* @__PURE__ */ jsx11(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1043
|
+
message.text ? /* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text }) : null
|
|
1044
|
+
] }),
|
|
925
1045
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 max-w-[85%]", children: [
|
|
926
1046
|
message.status === "error" ? /* @__PURE__ */ jsx11("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__ */ jsx11(TypingDots, {}) : /* @__PURE__ */ jsx11(Markdown, { text: message.text, className: "text-sm" }),
|
|
927
1047
|
showToolActivity ? /* @__PURE__ */ jsx11(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
928
1048
|
] })
|
|
929
1049
|
] });
|
|
930
1050
|
}
|
|
1051
|
+
function MessageCopyButton({ text }) {
|
|
1052
|
+
const [copied, setCopied] = useState4(false);
|
|
1053
|
+
const copy = async () => {
|
|
1054
|
+
try {
|
|
1055
|
+
await navigator.clipboard.writeText(text);
|
|
1056
|
+
setCopied(true);
|
|
1057
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
return /* @__PURE__ */ jsx11("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__ */ jsx11(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1062
|
+
}
|
|
931
1063
|
function TypingDots() {
|
|
932
1064
|
return /* @__PURE__ */ jsx11("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ jsx11(
|
|
933
1065
|
"span",
|
|
@@ -981,7 +1113,7 @@ function ChatPanel({
|
|
|
981
1113
|
const scrollRef = useRef3(null);
|
|
982
1114
|
const features = config?.features;
|
|
983
1115
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
984
|
-
const [model, setModel] =
|
|
1116
|
+
const [model, setModel] = useState5(void 0);
|
|
985
1117
|
useEffect4(() => {
|
|
986
1118
|
const node = scrollRef.current;
|
|
987
1119
|
if (!node) return;
|
|
@@ -1011,7 +1143,11 @@ function ChatPanel({
|
|
|
1011
1143
|
/* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx13(RotateCcw, {}) }),
|
|
1012
1144
|
onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(X3, {}) }) : null
|
|
1013
1145
|
] }),
|
|
1014
|
-
/* @__PURE__ */ jsx13(CardContent, { children: /* @__PURE__ */ jsx13(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */
|
|
1146
|
+
/* @__PURE__ */ jsx13(CardContent, { children: /* @__PURE__ */ jsx13(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs4("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1147
|
+
/* @__PURE__ */ jsx13("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1148
|
+
/* @__PURE__ */ jsx13("p", { children: configError }),
|
|
1149
|
+
/* @__PURE__ */ jsx13("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1150
|
+
] }) : loading ? /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
|
|
1015
1151
|
/* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1016
1152
|
/* @__PURE__ */ jsx13(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1017
1153
|
] }) : /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-4", children: messages.map(
|
|
@@ -1062,7 +1198,7 @@ function FrankieWidget({
|
|
|
1062
1198
|
}) {
|
|
1063
1199
|
const { config } = useFrankie();
|
|
1064
1200
|
const theme = useFrankieTheme();
|
|
1065
|
-
const [uncontrolledOpen, setUncontrolledOpen] =
|
|
1201
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState6(defaultOpen);
|
|
1066
1202
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1067
1203
|
const setOpen = (next) => {
|
|
1068
1204
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
@@ -1120,10 +1256,101 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1120
1256
|
);
|
|
1121
1257
|
}
|
|
1122
1258
|
|
|
1259
|
+
// src/components/chat/FrankieChatPage.tsx
|
|
1260
|
+
import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1261
|
+
function PageEmptyState() {
|
|
1262
|
+
const { config } = useFrankie();
|
|
1263
|
+
const { send, busy } = useFrankieChat();
|
|
1264
|
+
const name = config?.branding.name ?? "Frankie";
|
|
1265
|
+
const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
|
|
1266
|
+
return /* @__PURE__ */ jsxs6("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1267
|
+
/* @__PURE__ */ jsx16("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1268
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1269
|
+
/* @__PURE__ */ jsxs6("p", { className: "frankie-page-kicker", children: [
|
|
1270
|
+
name,
|
|
1271
|
+
" assistant"
|
|
1272
|
+
] }),
|
|
1273
|
+
/* @__PURE__ */ jsx16("h1", { children: "What would you like to do?" }),
|
|
1274
|
+
/* @__PURE__ */ jsx16("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1275
|
+
] }),
|
|
1276
|
+
prompts.length ? /* @__PURE__ */ jsx16("div", { className: "frankie-page-prompts", children: prompts.map((prompt, index) => /* @__PURE__ */ jsxs6("button", { type: "button", disabled: busy, onClick: () => send(prompt), children: [
|
|
1277
|
+
/* @__PURE__ */ jsx16("span", { children: String(index + 1).padStart(2, "0") }),
|
|
1278
|
+
/* @__PURE__ */ jsx16("strong", { children: prompt }),
|
|
1279
|
+
/* @__PURE__ */ jsx16("b", { "aria-hidden": true, children: "\u2197" })
|
|
1280
|
+
] }, prompt)) }) : null
|
|
1281
|
+
] });
|
|
1282
|
+
}
|
|
1283
|
+
function PageChrome({
|
|
1284
|
+
header,
|
|
1285
|
+
footer,
|
|
1286
|
+
emptyState,
|
|
1287
|
+
renderMessage,
|
|
1288
|
+
renderTool,
|
|
1289
|
+
placeholder,
|
|
1290
|
+
autoFocus
|
|
1291
|
+
}) {
|
|
1292
|
+
const { config } = useFrankie();
|
|
1293
|
+
const { reset } = useFrankieChat();
|
|
1294
|
+
const name = config?.branding.name ?? "Frankie";
|
|
1295
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1296
|
+
/* @__PURE__ */ jsxs6("header", { className: "frankie-page-header", children: [
|
|
1297
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1298
|
+
/* @__PURE__ */ jsx16("span", { className: "frankie-page-status", "aria-hidden": true }),
|
|
1299
|
+
/* @__PURE__ */ jsx16("strong", { children: name })
|
|
1300
|
+
] }),
|
|
1301
|
+
/* @__PURE__ */ jsx16("button", { type: "button", onClick: reset, "aria-label": "Start a new conversation", children: "New chat" })
|
|
1302
|
+
] }),
|
|
1303
|
+
/* @__PURE__ */ jsx16("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx16(
|
|
1304
|
+
FrankieInline,
|
|
1305
|
+
{
|
|
1306
|
+
header: header ?? false,
|
|
1307
|
+
footer,
|
|
1308
|
+
emptyState: emptyState ?? /* @__PURE__ */ jsx16(PageEmptyState, {}),
|
|
1309
|
+
renderMessage,
|
|
1310
|
+
renderTool,
|
|
1311
|
+
placeholder,
|
|
1312
|
+
autoFocus
|
|
1313
|
+
}
|
|
1314
|
+
) })
|
|
1315
|
+
] });
|
|
1316
|
+
}
|
|
1317
|
+
function FrankieChatPage({
|
|
1318
|
+
className,
|
|
1319
|
+
style,
|
|
1320
|
+
header,
|
|
1321
|
+
footer,
|
|
1322
|
+
emptyState,
|
|
1323
|
+
renderMessage,
|
|
1324
|
+
renderTool,
|
|
1325
|
+
placeholder,
|
|
1326
|
+
autoFocus,
|
|
1327
|
+
...providerProps
|
|
1328
|
+
}) {
|
|
1329
|
+
return /* @__PURE__ */ jsx16(
|
|
1330
|
+
"main",
|
|
1331
|
+
{
|
|
1332
|
+
className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
|
|
1333
|
+
style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
|
|
1334
|
+
children: /* @__PURE__ */ jsx16(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ jsx16(
|
|
1335
|
+
PageChrome,
|
|
1336
|
+
{
|
|
1337
|
+
header,
|
|
1338
|
+
footer,
|
|
1339
|
+
emptyState,
|
|
1340
|
+
renderMessage,
|
|
1341
|
+
renderTool,
|
|
1342
|
+
placeholder,
|
|
1343
|
+
autoFocus
|
|
1344
|
+
}
|
|
1345
|
+
) })
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1123
1350
|
// src/components/ui/separator.tsx
|
|
1124
|
-
import { jsx as
|
|
1351
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1125
1352
|
function Separator({ className }) {
|
|
1126
|
-
return /* @__PURE__ */
|
|
1353
|
+
return /* @__PURE__ */ jsx17("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1127
1354
|
}
|
|
1128
1355
|
export {
|
|
1129
1356
|
Avatar,
|
|
@@ -1139,6 +1366,7 @@ export {
|
|
|
1139
1366
|
Composer,
|
|
1140
1367
|
FRANKIE_CLOUD_URL,
|
|
1141
1368
|
FrankieApiError,
|
|
1369
|
+
FrankieChatPage,
|
|
1142
1370
|
FrankieClient,
|
|
1143
1371
|
FrankieInline,
|
|
1144
1372
|
FrankieProvider,
|