@frankieai/react 0.1.0 → 0.1.4
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 +396 -72
- 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 +71 -21
- package/dist/index.d.ts +71 -21
- package/dist/index.js +397 -74
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +15 -3
- package/src/components/chat/Composer.tsx +190 -27
- package/src/components/chat/FrankieChatPage.tsx +53 -0
- package/src/components/chat/MessageBubble.tsx +39 -6
- 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 -23
- package/src/index.ts +4 -2
- package/src/lib/client.ts +92 -16
- package/src/lib/messages.ts +9 -2
- package/src/styles/frankie.css +12 -4
- package/src/widget.test.tsx +4 -0
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
Composer: () => Composer,
|
|
35
35
|
FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
|
|
36
36
|
FrankieApiError: () => FrankieApiError,
|
|
37
|
+
FrankieChatPage: () => FrankieChatPage,
|
|
37
38
|
FrankieClient: () => FrankieClient,
|
|
38
39
|
FrankieInline: () => FrankieInline,
|
|
39
40
|
FrankieProvider: () => FrankieProvider,
|
|
@@ -89,13 +90,16 @@ var FrankieClient = class {
|
|
|
89
90
|
get sessionId() {
|
|
90
91
|
return this.session?.sessionId;
|
|
91
92
|
}
|
|
93
|
+
get conversationId() {
|
|
94
|
+
return this.options.conversationId;
|
|
95
|
+
}
|
|
92
96
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
93
97
|
get persist() {
|
|
94
98
|
return this.options.persist !== false && typeof window !== "undefined";
|
|
95
99
|
}
|
|
96
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
100
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
97
101
|
get namespace() {
|
|
98
|
-
return `frankie.${this.options.publishableKey}`;
|
|
102
|
+
return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
|
|
99
103
|
}
|
|
100
104
|
readStored() {
|
|
101
105
|
if (!this.persist) return void 0;
|
|
@@ -123,36 +127,88 @@ var FrankieClient = class {
|
|
|
123
127
|
});
|
|
124
128
|
return await this.unwrap(response);
|
|
125
129
|
}
|
|
126
|
-
/** Opens (or reuses) a chat session. */
|
|
127
130
|
async ensureSession(context) {
|
|
128
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4)
|
|
129
|
-
return this.session;
|
|
130
|
-
}
|
|
131
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
131
132
|
const userToken = await this.options.getUserToken?.();
|
|
133
|
+
if (this.options.getUserToken && !userToken) {
|
|
134
|
+
throw new FrankieApiError(
|
|
135
|
+
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
136
|
+
401,
|
|
137
|
+
"user_token_unavailable"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
132
140
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
133
141
|
method: "POST",
|
|
134
142
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
135
|
-
body: JSON.stringify({
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
...userToken ? { userToken } : {},
|
|
145
|
+
...context ? { context } : {},
|
|
146
|
+
...this.options.conversationId ? { conversationId: this.options.conversationId } : {}
|
|
147
|
+
})
|
|
136
148
|
});
|
|
137
149
|
this.session = await this.unwrap(response);
|
|
138
150
|
this.writeStored();
|
|
139
151
|
return this.session;
|
|
140
152
|
}
|
|
141
|
-
|
|
153
|
+
async listConversations() {
|
|
154
|
+
const userToken = await this.requireUserToken();
|
|
155
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
|
|
156
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
157
|
+
});
|
|
158
|
+
const body = await this.unwrap(response);
|
|
159
|
+
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
160
|
+
}
|
|
161
|
+
async resumeConversation(id = this.options.conversationId) {
|
|
162
|
+
if (!id) return void 0;
|
|
163
|
+
const userToken = await this.requireUserToken();
|
|
164
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
167
|
+
});
|
|
168
|
+
if (response.status === 404) return void 0;
|
|
169
|
+
const body = await this.unwrap(response);
|
|
170
|
+
this.session = body.session;
|
|
171
|
+
this.writeStored();
|
|
172
|
+
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
173
|
+
}
|
|
174
|
+
async syncConversation(messages, title) {
|
|
175
|
+
if (!this.session || !this.options.conversationId) return;
|
|
176
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
|
|
177
|
+
method: "PUT",
|
|
178
|
+
headers: {
|
|
179
|
+
"content-type": "application/json",
|
|
180
|
+
"x-frankie-key": this.options.publishableKey,
|
|
181
|
+
authorization: `Bearer ${this.session.sessionToken}`
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({ messages, ...title ? { title } : {} })
|
|
184
|
+
});
|
|
185
|
+
await this.unwrap(response);
|
|
186
|
+
}
|
|
187
|
+
async deleteConversation(id) {
|
|
188
|
+
const userToken = await this.requireUserToken();
|
|
189
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
|
|
190
|
+
method: "DELETE",
|
|
191
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
192
|
+
});
|
|
193
|
+
if (response.status !== 404) await this.unwrap(response);
|
|
194
|
+
}
|
|
195
|
+
async requireUserToken() {
|
|
196
|
+
const token = await this.options.getUserToken?.();
|
|
197
|
+
if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
|
|
198
|
+
return token;
|
|
199
|
+
}
|
|
142
200
|
resetSession() {
|
|
143
201
|
this.session = void 0;
|
|
144
202
|
this.writeStored();
|
|
145
203
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
148
|
-
* Resolves when the turn ends.
|
|
149
|
-
*/
|
|
150
204
|
async send(input, onEvent) {
|
|
151
205
|
const session = await this.ensureSession(input.context);
|
|
152
206
|
const body = {
|
|
153
207
|
sessionId: session.sessionId,
|
|
154
208
|
message: input.message,
|
|
155
|
-
...input.context ? { context: input.context } : {}
|
|
209
|
+
...input.context ? { context: input.context } : {},
|
|
210
|
+
...input.model ? { model: input.model } : {},
|
|
211
|
+
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
156
212
|
};
|
|
157
213
|
const response = await this.doFetch(`${this.baseUrl}/v1/chat`, {
|
|
158
214
|
method: "POST",
|
|
@@ -230,9 +286,8 @@ function FrankieProvider({
|
|
|
230
286
|
}) {
|
|
231
287
|
const client = (0, import_react.useMemo)(
|
|
232
288
|
() => new FrankieClient(clientOptions),
|
|
233
|
-
// The client holds session state; rebuild it only when the endpoint changes.
|
|
234
289
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
-
[clientOptions.baseUrl, clientOptions.publishableKey]
|
|
290
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
|
|
236
291
|
);
|
|
237
292
|
const [config, setConfig] = (0, import_react.useState)(providedConfig);
|
|
238
293
|
const [configError, setConfigError] = (0, import_react.useState)(void 0);
|
|
@@ -346,8 +401,8 @@ function fallbackText(stopReason) {
|
|
|
346
401
|
function newAssistantMessage(id) {
|
|
347
402
|
return { id, role: "assistant", text: "", thinking: "", tools: [], status: "streaming", createdAt: Date.now() };
|
|
348
403
|
}
|
|
349
|
-
function newUserMessage(id, text) {
|
|
350
|
-
return { id, role: "user", text, createdAt: Date.now() };
|
|
404
|
+
function newUserMessage(id, text, attachments) {
|
|
405
|
+
return { id, role: "user", text, ...attachments?.length ? { attachments } : {}, createdAt: Date.now() };
|
|
351
406
|
}
|
|
352
407
|
|
|
353
408
|
// src/hooks/useFrankieChat.ts
|
|
@@ -358,6 +413,7 @@ function useFrankieChat() {
|
|
|
358
413
|
const [messages, setMessages] = (0, import_react2.useState)([]);
|
|
359
414
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
360
415
|
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
416
|
+
const [restored, setRestored] = (0, import_react2.useState)(false);
|
|
361
417
|
const abortRef = (0, import_react2.useRef)(null);
|
|
362
418
|
const mounted = (0, import_react2.useRef)(true);
|
|
363
419
|
(0, import_react2.useEffect)(() => {
|
|
@@ -369,22 +425,38 @@ function useFrankieChat() {
|
|
|
369
425
|
}, []);
|
|
370
426
|
const storageKey = `${client.namespace}.messages`;
|
|
371
427
|
(0, import_react2.useEffect)(() => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
428
|
+
let active = true;
|
|
429
|
+
const restore = async () => {
|
|
430
|
+
if (client.persist) {
|
|
431
|
+
try {
|
|
432
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
433
|
+
const stored = raw ? JSON.parse(raw) : [];
|
|
434
|
+
if (Array.isArray(stored) && stored.length && active) setMessages(stored);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const remote = await client.resumeConversation();
|
|
440
|
+
if (active && remote?.messages.length) setMessages(remote.messages);
|
|
441
|
+
} catch {
|
|
442
|
+
} finally {
|
|
443
|
+
if (active) setRestored(true);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
void restore();
|
|
447
|
+
return () => {
|
|
448
|
+
active = false;
|
|
449
|
+
};
|
|
450
|
+
}, [client, storageKey]);
|
|
380
451
|
(0, import_react2.useEffect)(() => {
|
|
381
|
-
if (!client.persist || busy) return;
|
|
452
|
+
if (!client.persist || !restored || busy) return;
|
|
382
453
|
try {
|
|
383
454
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
384
455
|
else window.localStorage.removeItem(storageKey);
|
|
385
456
|
} catch {
|
|
386
457
|
}
|
|
387
|
-
|
|
458
|
+
if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
|
|
459
|
+
}, [messages, busy, client, storageKey, restored]);
|
|
388
460
|
const patchAssistant = (0, import_react2.useCallback)((id, update) => {
|
|
389
461
|
setMessages(
|
|
390
462
|
(current) => current.map(
|
|
@@ -393,24 +465,50 @@ function useFrankieChat() {
|
|
|
393
465
|
);
|
|
394
466
|
}, []);
|
|
395
467
|
const send = (0, import_react2.useCallback)(
|
|
396
|
-
async (text) => {
|
|
468
|
+
async (text, opts) => {
|
|
397
469
|
const trimmed = text.trim();
|
|
398
|
-
|
|
470
|
+
const attachments = opts?.attachments ?? [];
|
|
471
|
+
if (!trimmed && attachments.length === 0 || busy) return;
|
|
472
|
+
const outgoing = trimmed || "Please take a look at the attached file(s).";
|
|
399
473
|
const assistantId = nextId("a");
|
|
400
474
|
setError(void 0);
|
|
401
475
|
setBusy(true);
|
|
402
476
|
setMessages((current) => [
|
|
403
477
|
...current,
|
|
404
|
-
newUserMessage(
|
|
478
|
+
newUserMessage(
|
|
479
|
+
nextId("u"),
|
|
480
|
+
trimmed,
|
|
481
|
+
attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
|
|
482
|
+
),
|
|
405
483
|
newAssistantMessage(assistantId)
|
|
406
484
|
]);
|
|
407
485
|
const abort = new AbortController();
|
|
408
486
|
abortRef.current = abort;
|
|
409
487
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
488
|
+
let receivedText = false;
|
|
489
|
+
let receivedToolActivity = false;
|
|
490
|
+
await client.send(
|
|
491
|
+
{
|
|
492
|
+
message: outgoing,
|
|
493
|
+
...context ? { context } : {},
|
|
494
|
+
...opts?.model ? { model: opts.model } : {},
|
|
495
|
+
...attachments.length ? { attachments } : {},
|
|
496
|
+
signal: abort.signal
|
|
497
|
+
},
|
|
498
|
+
(event) => {
|
|
499
|
+
if (event.type === "ping" || event.type === "session") return;
|
|
500
|
+
if (event.type === "text_delta") receivedText = true;
|
|
501
|
+
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
502
|
+
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
if (!receivedText) {
|
|
506
|
+
patchAssistant(assistantId, (message) => ({
|
|
507
|
+
...message,
|
|
508
|
+
status: "done",
|
|
509
|
+
text: receivedToolActivity ? "Completed. Open Agent activity for the details." : "Frankie didn\u2019t return an answer. Please try again."
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
414
512
|
} catch (caught) {
|
|
415
513
|
if (abort.signal.aborted) {
|
|
416
514
|
patchAssistant(assistantId, (message) => ({
|
|
@@ -419,7 +517,7 @@ function useFrankieChat() {
|
|
|
419
517
|
text: message.text || "Stopped."
|
|
420
518
|
}));
|
|
421
519
|
} else {
|
|
422
|
-
const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
520
|
+
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.";
|
|
423
521
|
if (mounted.current) setError(message);
|
|
424
522
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
425
523
|
}
|
|
@@ -443,6 +541,7 @@ function useFrankieChat() {
|
|
|
443
541
|
setMessages([]);
|
|
444
542
|
setError(void 0);
|
|
445
543
|
setBusy(false);
|
|
544
|
+
setRestored(true);
|
|
446
545
|
try {
|
|
447
546
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
448
547
|
} catch {
|
|
@@ -450,10 +549,16 @@ function useFrankieChat() {
|
|
|
450
549
|
}, [client]);
|
|
451
550
|
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
452
551
|
}
|
|
552
|
+
function conversationTitle(messages) {
|
|
553
|
+
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
554
|
+
if (!first) return void 0;
|
|
555
|
+
const title = first.text.trim().replace(/\s+/g, " ");
|
|
556
|
+
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
557
|
+
}
|
|
453
558
|
|
|
454
559
|
// src/components/chat/FrankieWidget.tsx
|
|
455
|
-
var
|
|
456
|
-
var
|
|
560
|
+
var import_lucide_react5 = require("lucide-react");
|
|
561
|
+
var import_react9 = require("react");
|
|
457
562
|
|
|
458
563
|
// src/lib/theme.ts
|
|
459
564
|
function themeToCssVars(theme) {
|
|
@@ -513,8 +618,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
513
618
|
}
|
|
514
619
|
|
|
515
620
|
// src/components/chat/ChatPanel.tsx
|
|
516
|
-
var
|
|
517
|
-
var
|
|
621
|
+
var import_lucide_react4 = require("lucide-react");
|
|
622
|
+
var import_react8 = require("react");
|
|
518
623
|
|
|
519
624
|
// src/components/ui/avatar.tsx
|
|
520
625
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
@@ -598,55 +703,185 @@ var Textarea = (0, import_react4.forwardRef)(
|
|
|
598
703
|
// src/components/chat/Composer.tsx
|
|
599
704
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
600
705
|
var MAX_ROWS_PX = 140;
|
|
601
|
-
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus }) {
|
|
706
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
602
707
|
const [value, setValue] = (0, import_react5.useState)("");
|
|
708
|
+
const [files, setFiles] = (0, import_react5.useState)([]);
|
|
709
|
+
const [fileError, setFileError] = (0, import_react5.useState)(void 0);
|
|
603
710
|
const textareaRef = (0, import_react5.useRef)(null);
|
|
711
|
+
const fileInputRef = (0, import_react5.useRef)(null);
|
|
712
|
+
const canAttach = Boolean(attachments?.enabled);
|
|
713
|
+
const models = modelPicker?.models ?? [];
|
|
604
714
|
(0, import_react5.useEffect)(() => {
|
|
605
715
|
const node = textareaRef.current;
|
|
606
716
|
if (!node) return;
|
|
607
717
|
node.style.height = "auto";
|
|
608
718
|
node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
|
|
609
719
|
}, [value]);
|
|
720
|
+
const addFiles = (0, import_react5.useCallback)(
|
|
721
|
+
async (list) => {
|
|
722
|
+
if (!list || !attachments) return;
|
|
723
|
+
setFileError(void 0);
|
|
724
|
+
const room = attachments.maxCount - files.length;
|
|
725
|
+
const chosen = Array.from(list).slice(0, Math.max(0, room));
|
|
726
|
+
if (Array.from(list).length > room) setFileError(`You can attach up to ${attachments.maxCount} file(s).`);
|
|
727
|
+
const next = [];
|
|
728
|
+
for (const file of chosen) {
|
|
729
|
+
if (!mimeAccepted(file.type || "application/octet-stream", attachments.accept)) {
|
|
730
|
+
setFileError(`Files of type ${file.type || "unknown"} aren't allowed.`);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (file.size > attachments.maxMb * 1024 * 1024) {
|
|
734
|
+
setFileError(`Each file must be under ${attachments.maxMb} MB.`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
next.push({
|
|
739
|
+
id: `${file.name}-${file.size}-${Math.random().toString(36).slice(2, 7)}`,
|
|
740
|
+
name: file.name,
|
|
741
|
+
mimeType: file.type || "application/octet-stream",
|
|
742
|
+
data: await readAsBase64(file)
|
|
743
|
+
});
|
|
744
|
+
} catch {
|
|
745
|
+
setFileError("Couldn't read that file.");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (next.length) setFiles((current) => [...current, ...next]);
|
|
749
|
+
},
|
|
750
|
+
[attachments, files.length]
|
|
751
|
+
);
|
|
752
|
+
const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
|
|
610
753
|
const submit = (0, import_react5.useCallback)(() => {
|
|
611
754
|
const text = value.trim();
|
|
612
|
-
if (!text || busy || disabled) return;
|
|
755
|
+
if (!text && files.length === 0 || busy || disabled) return;
|
|
613
756
|
setValue("");
|
|
614
|
-
|
|
615
|
-
|
|
757
|
+
const attached = files.map(({ name, mimeType, data }) => ({ name, mimeType, data }));
|
|
758
|
+
setFiles([]);
|
|
759
|
+
setFileError(void 0);
|
|
760
|
+
onSend(text, {
|
|
761
|
+
...attached.length ? { attachments: attached } : {},
|
|
762
|
+
...modelPicker?.value ? { model: modelPicker.value } : {}
|
|
763
|
+
});
|
|
764
|
+
}, [busy, disabled, files, modelPicker?.value, onSend, value]);
|
|
616
765
|
const onKeyDown = (event) => {
|
|
617
766
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
618
767
|
event.preventDefault();
|
|
619
768
|
submit();
|
|
620
769
|
}
|
|
621
770
|
};
|
|
771
|
+
const nothingToSend = value.trim().length === 0 && files.length === 0;
|
|
622
772
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
623
773
|
"form",
|
|
624
774
|
{
|
|
625
|
-
className: "flex
|
|
775
|
+
className: "flex flex-col gap-2",
|
|
626
776
|
onSubmit: (event) => {
|
|
627
777
|
event.preventDefault();
|
|
628
778
|
submit();
|
|
629
779
|
},
|
|
630
780
|
children: [
|
|
631
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
632
|
-
|
|
781
|
+
files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
782
|
+
"span",
|
|
633
783
|
{
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
784
|
+
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",
|
|
785
|
+
children: [
|
|
786
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: file.name }),
|
|
788
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
789
|
+
"button",
|
|
790
|
+
{
|
|
791
|
+
type: "button",
|
|
792
|
+
onClick: () => removeFile(file.id),
|
|
793
|
+
"aria-label": `Remove ${file.name}`,
|
|
794
|
+
className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
|
|
795
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.X, { className: "size-3" })
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
]
|
|
799
|
+
},
|
|
800
|
+
file.id
|
|
801
|
+
)) }) : null,
|
|
802
|
+
fileError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-end gap-2", children: [
|
|
804
|
+
canAttach ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
805
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
806
|
+
"input",
|
|
807
|
+
{
|
|
808
|
+
ref: fileInputRef,
|
|
809
|
+
type: "file",
|
|
810
|
+
multiple: true,
|
|
811
|
+
hidden: true,
|
|
812
|
+
accept: attachments?.accept.join(","),
|
|
813
|
+
onChange: (event) => {
|
|
814
|
+
void addFiles(event.target.files);
|
|
815
|
+
event.target.value = "";
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
),
|
|
819
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
820
|
+
Button,
|
|
821
|
+
{
|
|
822
|
+
type: "button",
|
|
823
|
+
size: "icon",
|
|
824
|
+
variant: "outline",
|
|
825
|
+
disabled: disabled || files.length >= (attachments?.maxCount ?? 0),
|
|
826
|
+
onClick: () => fileInputRef.current?.click(),
|
|
827
|
+
"aria-label": "Attach files",
|
|
828
|
+
title: "Attach files",
|
|
829
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Paperclip, {})
|
|
830
|
+
}
|
|
831
|
+
)
|
|
832
|
+
] }) : null,
|
|
833
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
834
|
+
Textarea,
|
|
835
|
+
{
|
|
836
|
+
ref: textareaRef,
|
|
837
|
+
rows: 1,
|
|
838
|
+
value,
|
|
839
|
+
disabled,
|
|
840
|
+
autoFocus,
|
|
841
|
+
onChange: (event) => setValue(event.target.value),
|
|
842
|
+
onKeyDown,
|
|
843
|
+
placeholder: placeholder ?? "Ask anything\u2026",
|
|
844
|
+
"aria-label": "Message"
|
|
845
|
+
}
|
|
846
|
+
),
|
|
847
|
+
models.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
848
|
+
"select",
|
|
849
|
+
{
|
|
850
|
+
value: modelPicker?.value ?? models[0]?.id,
|
|
851
|
+
onChange: (event) => modelPicker?.onChange(event.target.value),
|
|
852
|
+
disabled,
|
|
853
|
+
"aria-label": "Model",
|
|
854
|
+
className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
|
|
855
|
+
children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: model.id, children: model.label }, model.id))
|
|
856
|
+
}
|
|
857
|
+
) : null,
|
|
858
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.ArrowUp, {}) })
|
|
859
|
+
] })
|
|
646
860
|
]
|
|
647
861
|
}
|
|
648
862
|
);
|
|
649
863
|
}
|
|
864
|
+
function mimeAccepted(mimeType, accept) {
|
|
865
|
+
return accept.some(
|
|
866
|
+
(pattern) => pattern.endsWith("/*") ? mimeType.startsWith(pattern.slice(0, -1)) : pattern === mimeType
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
function readAsBase64(file) {
|
|
870
|
+
return new Promise((resolve, reject) => {
|
|
871
|
+
const reader = new FileReader();
|
|
872
|
+
reader.onload = () => {
|
|
873
|
+
const result = String(reader.result);
|
|
874
|
+
const comma = result.indexOf(",");
|
|
875
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
876
|
+
};
|
|
877
|
+
reader.onerror = () => reject(reader.error);
|
|
878
|
+
reader.readAsDataURL(file);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/components/chat/MessageBubble.tsx
|
|
883
|
+
var import_lucide_react3 = require("lucide-react");
|
|
884
|
+
var import_react7 = require("react");
|
|
650
885
|
|
|
651
886
|
// src/components/chat/Markdown.tsx
|
|
652
887
|
var import_react6 = require("react");
|
|
@@ -782,7 +1017,20 @@ function Badge({
|
|
|
782
1017
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
783
1018
|
function ToolActivityList({ tools, renderTool }) {
|
|
784
1019
|
if (tools.length === 0) return null;
|
|
785
|
-
|
|
1020
|
+
const running = tools.some((tool) => tool.status === "running");
|
|
1021
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1022
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.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: [
|
|
1023
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1024
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: running ? "Working" : "Activity" }),
|
|
1025
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-[11px]", children: [
|
|
1026
|
+
"\xB7 ",
|
|
1027
|
+
tools.length,
|
|
1028
|
+
" tool call",
|
|
1029
|
+
tools.length === 1 ? "" : "s"
|
|
1030
|
+
] })
|
|
1031
|
+
] }),
|
|
1032
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
|
|
1033
|
+
] });
|
|
786
1034
|
}
|
|
787
1035
|
function ToolActivityRow({ tool }) {
|
|
788
1036
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
@@ -819,17 +1067,48 @@ function MessageBubble({
|
|
|
819
1067
|
renderTool
|
|
820
1068
|
}) {
|
|
821
1069
|
if (message.role === "user") {
|
|
822
|
-
return /* @__PURE__ */ (0, import_jsx_runtime11.
|
|
1070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col items-end gap-1", children: [
|
|
1071
|
+
message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1072
|
+
"span",
|
|
1073
|
+
{
|
|
1074
|
+
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",
|
|
1075
|
+
children: [
|
|
1076
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
1077
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: file.name })
|
|
1078
|
+
]
|
|
1079
|
+
},
|
|
1080
|
+
`${file.name}-${index}`
|
|
1081
|
+
)) }) : null,
|
|
1082
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1083
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1084
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text })
|
|
1085
|
+
] }) : null
|
|
1086
|
+
] });
|
|
823
1087
|
}
|
|
824
1088
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
825
1089
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
|
|
826
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.
|
|
1090
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1092
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text }) : null
|
|
1093
|
+
] }),
|
|
827
1094
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "min-w-0 max-w-[85%]", children: [
|
|
828
1095
|
message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text, className: "text-sm" }),
|
|
829
1096
|
showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
830
1097
|
] })
|
|
831
1098
|
] });
|
|
832
1099
|
}
|
|
1100
|
+
function MessageCopyButton({ text }) {
|
|
1101
|
+
const [copied, setCopied] = (0, import_react7.useState)(false);
|
|
1102
|
+
const copy = async () => {
|
|
1103
|
+
try {
|
|
1104
|
+
await navigator.clipboard.writeText(text);
|
|
1105
|
+
setCopied(true);
|
|
1106
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1111
|
+
}
|
|
833
1112
|
function TypingDots() {
|
|
834
1113
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
835
1114
|
"span",
|
|
@@ -880,8 +1159,11 @@ function ChatPanel({
|
|
|
880
1159
|
const { config, configError, loading } = useFrankie();
|
|
881
1160
|
const theme = useFrankieTheme();
|
|
882
1161
|
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
883
|
-
const scrollRef = (0,
|
|
884
|
-
|
|
1162
|
+
const scrollRef = (0, import_react8.useRef)(null);
|
|
1163
|
+
const features = config?.features;
|
|
1164
|
+
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1165
|
+
const [model, setModel] = (0, import_react8.useState)(void 0);
|
|
1166
|
+
(0, import_react8.useEffect)(() => {
|
|
885
1167
|
const node = scrollRef.current;
|
|
886
1168
|
if (!node) return;
|
|
887
1169
|
const distanceFromBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
|
|
@@ -907,10 +1189,14 @@ function ChatPanel({
|
|
|
907
1189
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardTitle, { children: name }),
|
|
908
1190
|
config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardDescription, { children: config.branding.tagline }) : null
|
|
909
1191
|
] }),
|
|
910
|
-
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
911
|
-
onClose ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1192
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.RotateCcw, {}) }),
|
|
1193
|
+
onClose ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.X, {}) }) : null
|
|
912
1194
|
] }),
|
|
913
|
-
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.
|
|
1195
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1196
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1197
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: configError }),
|
|
1198
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1199
|
+
] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
914
1200
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
915
1201
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
916
1202
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-col gap-4", children: messages.map(
|
|
@@ -936,7 +1222,9 @@ function ChatPanel({
|
|
|
936
1222
|
busy,
|
|
937
1223
|
disabled: Boolean(configError),
|
|
938
1224
|
...placeholder ? { placeholder } : {},
|
|
939
|
-
...autoFocus ? { autoFocus } : {}
|
|
1225
|
+
...autoFocus ? { autoFocus } : {},
|
|
1226
|
+
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
1227
|
+
...pickerModels.length ? { modelPicker: { models: pickerModels, value: model ?? pickerModels[0]?.id, onChange: setModel } } : {}
|
|
940
1228
|
}
|
|
941
1229
|
) }),
|
|
942
1230
|
footer
|
|
@@ -959,13 +1247,13 @@ function FrankieWidget({
|
|
|
959
1247
|
}) {
|
|
960
1248
|
const { config } = useFrankie();
|
|
961
1249
|
const theme = useFrankieTheme();
|
|
962
|
-
const [uncontrolledOpen, setUncontrolledOpen] = (0,
|
|
1250
|
+
const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react9.useState)(defaultOpen);
|
|
963
1251
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
964
1252
|
const setOpen = (next) => {
|
|
965
1253
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
966
1254
|
onOpenChange?.(next);
|
|
967
1255
|
};
|
|
968
|
-
(0,
|
|
1256
|
+
(0, import_react9.useEffect)(() => {
|
|
969
1257
|
if (!open) return;
|
|
970
1258
|
const onKeyDown = (event) => {
|
|
971
1259
|
if (event.key === "Escape") setOpen(false);
|
|
@@ -993,7 +1281,7 @@ function FrankieWidget({
|
|
|
993
1281
|
"aria-expanded": open,
|
|
994
1282
|
"aria-label": open ? "Close the assistant" : label,
|
|
995
1283
|
title: label,
|
|
996
|
-
children: open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1284
|
+
children: open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.MessageCircle, { className: "size-5" })
|
|
997
1285
|
}
|
|
998
1286
|
)
|
|
999
1287
|
] })
|
|
@@ -1017,10 +1305,45 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1017
1305
|
);
|
|
1018
1306
|
}
|
|
1019
1307
|
|
|
1020
|
-
// src/components/
|
|
1308
|
+
// src/components/chat/FrankieChatPage.tsx
|
|
1021
1309
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1310
|
+
function FrankieChatPage({
|
|
1311
|
+
className,
|
|
1312
|
+
style,
|
|
1313
|
+
header,
|
|
1314
|
+
footer,
|
|
1315
|
+
emptyState,
|
|
1316
|
+
renderMessage,
|
|
1317
|
+
renderTool,
|
|
1318
|
+
placeholder,
|
|
1319
|
+
autoFocus,
|
|
1320
|
+
...providerProps
|
|
1321
|
+
}) {
|
|
1322
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1323
|
+
"main",
|
|
1324
|
+
{
|
|
1325
|
+
className,
|
|
1326
|
+
style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
|
|
1327
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1328
|
+
FrankieInline,
|
|
1329
|
+
{
|
|
1330
|
+
header,
|
|
1331
|
+
footer,
|
|
1332
|
+
emptyState,
|
|
1333
|
+
renderMessage,
|
|
1334
|
+
renderTool,
|
|
1335
|
+
placeholder,
|
|
1336
|
+
autoFocus
|
|
1337
|
+
}
|
|
1338
|
+
) })
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/components/ui/separator.tsx
|
|
1344
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1022
1345
|
function Separator({ className }) {
|
|
1023
|
-
return /* @__PURE__ */ (0,
|
|
1346
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1024
1347
|
}
|
|
1025
1348
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1026
1349
|
0 && (module.exports = {
|
|
@@ -1037,6 +1360,7 @@ function Separator({ className }) {
|
|
|
1037
1360
|
Composer,
|
|
1038
1361
|
FRANKIE_CLOUD_URL,
|
|
1039
1362
|
FrankieApiError,
|
|
1363
|
+
FrankieChatPage,
|
|
1040
1364
|
FrankieClient,
|
|
1041
1365
|
FrankieInline,
|
|
1042
1366
|
FrankieProvider,
|