@frankieai/react 0.1.0 → 0.1.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 +200 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -6
- package/dist/index.d.ts +43 -6
- package/dist/index.js +208 -44
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +9 -1
- package/src/components/chat/Composer.tsx +190 -27
- package/src/components/chat/MessageBubble.tsx +20 -4
- package/src/hooks/useFrankieChat.ts +31 -8
- package/src/index.ts +2 -2
- package/src/lib/client.ts +12 -1
- package/src/lib/messages.ts +9 -2
- package/src/widget.test.tsx +4 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, CSSProperties, ButtonHTMLAttributes, HTMLAttributes, TextareaHTMLAttributes } from 'react';
|
|
3
|
-
import { PublicWidgetConfig, CreateSessionResponse, FrankieServerEvent, WidgetTheme } from '@frankieai/core';
|
|
3
|
+
import { PublicWidgetConfig, CreateSessionResponse, ChatAttachment, FrankieServerEvent, WidgetTheme } from '@frankieai/core';
|
|
4
4
|
export { FrankieServerEvent, PublicWidgetConfig, WidgetTheme } from '@frankieai/core';
|
|
5
5
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
6
6
|
import { VariantProps } from 'class-variance-authority';
|
|
@@ -61,6 +61,10 @@ declare class FrankieClient {
|
|
|
61
61
|
send(input: {
|
|
62
62
|
message: string;
|
|
63
63
|
context?: PageContext;
|
|
64
|
+
/** Model id chosen by the user, honoured only if the workspace allows it. */
|
|
65
|
+
model?: string;
|
|
66
|
+
/** Files attached to this turn. */
|
|
67
|
+
attachments?: ChatAttachment[];
|
|
64
68
|
signal?: AbortSignal;
|
|
65
69
|
}, onEvent: (event: FrankieServerEvent) => void): Promise<void>;
|
|
66
70
|
private unwrap;
|
|
@@ -100,10 +104,16 @@ interface ToolActivity {
|
|
|
100
104
|
status: "running" | "done" | "failed" | "denied";
|
|
101
105
|
detail?: string;
|
|
102
106
|
}
|
|
107
|
+
/** A file shown on a user message (name/type only — the bytes aren't kept in the transcript). */
|
|
108
|
+
interface MessageAttachment {
|
|
109
|
+
name: string;
|
|
110
|
+
mimeType: string;
|
|
111
|
+
}
|
|
103
112
|
interface UserChatMessage {
|
|
104
113
|
id: string;
|
|
105
114
|
role: "user";
|
|
106
115
|
text: string;
|
|
116
|
+
attachments?: MessageAttachment[];
|
|
107
117
|
createdAt: number;
|
|
108
118
|
}
|
|
109
119
|
interface AssistantChatMessage {
|
|
@@ -125,15 +135,20 @@ type ChatMessage = UserChatMessage | AssistantChatMessage;
|
|
|
125
135
|
*/
|
|
126
136
|
declare function applyEvent(message: AssistantChatMessage, event: FrankieServerEvent): AssistantChatMessage;
|
|
127
137
|
declare function newAssistantMessage(id: string): AssistantChatMessage;
|
|
128
|
-
declare function newUserMessage(id: string, text: string): UserChatMessage;
|
|
138
|
+
declare function newUserMessage(id: string, text: string, attachments?: MessageAttachment[]): UserChatMessage;
|
|
129
139
|
|
|
140
|
+
/** Extra inputs a turn can carry: attached files and a chosen model. */
|
|
141
|
+
interface SendOptions {
|
|
142
|
+
attachments?: ChatAttachment[];
|
|
143
|
+
model?: string;
|
|
144
|
+
}
|
|
130
145
|
interface UseFrankieChatResult {
|
|
131
146
|
messages: ChatMessage[];
|
|
132
147
|
/** True from the moment a message is sent until the turn ends. */
|
|
133
148
|
busy: boolean;
|
|
134
149
|
/** Set when the turn failed outright (network, auth, rate limit). */
|
|
135
150
|
error?: string;
|
|
136
|
-
send: (text: string) => Promise<void>;
|
|
151
|
+
send: (text: string, opts?: SendOptions) => Promise<void>;
|
|
137
152
|
/** Abort the in-flight turn. The partial answer stays in the transcript. */
|
|
138
153
|
stop: () => void;
|
|
139
154
|
/** Clear the transcript and start a brand-new server session. */
|
|
@@ -200,15 +215,37 @@ interface FrankieInlineProps extends ChatPanelProps {
|
|
|
200
215
|
*/
|
|
201
216
|
declare function FrankieInline({ className, style, ...panelProps }: FrankieInlineProps): react.JSX.Element;
|
|
202
217
|
|
|
218
|
+
interface ComposerAttachmentsConfig {
|
|
219
|
+
enabled: boolean;
|
|
220
|
+
/** Accepted MIME patterns, e.g. "image/*". */
|
|
221
|
+
accept: string[];
|
|
222
|
+
maxCount: number;
|
|
223
|
+
maxMb: number;
|
|
224
|
+
}
|
|
225
|
+
interface ComposerModelPicker {
|
|
226
|
+
models: {
|
|
227
|
+
id: string;
|
|
228
|
+
label: string;
|
|
229
|
+
}[];
|
|
230
|
+
value?: string;
|
|
231
|
+
onChange: (id: string) => void;
|
|
232
|
+
}
|
|
203
233
|
interface ComposerProps {
|
|
204
|
-
onSend: (text: string
|
|
234
|
+
onSend: (text: string, opts?: {
|
|
235
|
+
attachments?: ChatAttachment[];
|
|
236
|
+
model?: string;
|
|
237
|
+
}) => void;
|
|
205
238
|
onStop: () => void;
|
|
206
239
|
busy: boolean;
|
|
207
240
|
placeholder?: string;
|
|
208
241
|
disabled?: boolean;
|
|
209
242
|
autoFocus?: boolean;
|
|
243
|
+
/** Enables the attach button when provided and `enabled`. */
|
|
244
|
+
attachments?: ComposerAttachmentsConfig;
|
|
245
|
+
/** Enables a model dropdown when provided with at least one model. */
|
|
246
|
+
modelPicker?: ComposerModelPicker;
|
|
210
247
|
}
|
|
211
|
-
declare function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus }: ComposerProps): react.JSX.Element;
|
|
248
|
+
declare function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }: ComposerProps): react.JSX.Element;
|
|
212
249
|
|
|
213
250
|
interface MessageBubbleProps {
|
|
214
251
|
message: ChatMessage;
|
|
@@ -307,4 +344,4 @@ declare function resolveMode(mode: WidgetTheme["mode"]): "light" | "dark";
|
|
|
307
344
|
/** shadcn/ui's class helper: conditional classes with Tailwind conflict resolution. */
|
|
308
345
|
declare function cn(...inputs: ClassValue[]): string;
|
|
309
346
|
|
|
310
|
-
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, MessageBubble, type PageContext, ScrollArea, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
|
347
|
+
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, type MessageAttachment, MessageBubble, type PageContext, ScrollArea, type SendOptions, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,9 @@ var FrankieClient = class {
|
|
|
91
91
|
const body = {
|
|
92
92
|
sessionId: session.sessionId,
|
|
93
93
|
message: input.message,
|
|
94
|
-
...input.context ? { context: input.context } : {}
|
|
94
|
+
...input.context ? { context: input.context } : {},
|
|
95
|
+
...input.model ? { model: input.model } : {},
|
|
96
|
+
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
95
97
|
};
|
|
96
98
|
const response = await this.doFetch(`${this.baseUrl}/v1/chat`, {
|
|
97
99
|
method: "POST",
|
|
@@ -285,8 +287,8 @@ function fallbackText(stopReason) {
|
|
|
285
287
|
function newAssistantMessage(id) {
|
|
286
288
|
return { id, role: "assistant", text: "", thinking: "", tools: [], status: "streaming", createdAt: Date.now() };
|
|
287
289
|
}
|
|
288
|
-
function newUserMessage(id, text) {
|
|
289
|
-
return { id, role: "user", text, createdAt: Date.now() };
|
|
290
|
+
function newUserMessage(id, text, attachments) {
|
|
291
|
+
return { id, role: "user", text, ...attachments?.length ? { attachments } : {}, createdAt: Date.now() };
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
// src/hooks/useFrankieChat.ts
|
|
@@ -332,24 +334,39 @@ function useFrankieChat() {
|
|
|
332
334
|
);
|
|
333
335
|
}, []);
|
|
334
336
|
const send = useCallback(
|
|
335
|
-
async (text) => {
|
|
337
|
+
async (text, opts) => {
|
|
336
338
|
const trimmed = text.trim();
|
|
337
|
-
|
|
339
|
+
const attachments = opts?.attachments ?? [];
|
|
340
|
+
if (!trimmed && attachments.length === 0 || busy) return;
|
|
341
|
+
const outgoing = trimmed || "Please take a look at the attached file(s).";
|
|
338
342
|
const assistantId = nextId("a");
|
|
339
343
|
setError(void 0);
|
|
340
344
|
setBusy(true);
|
|
341
345
|
setMessages((current) => [
|
|
342
346
|
...current,
|
|
343
|
-
newUserMessage(
|
|
347
|
+
newUserMessage(
|
|
348
|
+
nextId("u"),
|
|
349
|
+
trimmed,
|
|
350
|
+
attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
|
|
351
|
+
),
|
|
344
352
|
newAssistantMessage(assistantId)
|
|
345
353
|
]);
|
|
346
354
|
const abort = new AbortController();
|
|
347
355
|
abortRef.current = abort;
|
|
348
356
|
try {
|
|
349
|
-
await client.send(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
357
|
+
await client.send(
|
|
358
|
+
{
|
|
359
|
+
message: outgoing,
|
|
360
|
+
...context ? { context } : {},
|
|
361
|
+
...opts?.model ? { model: opts.model } : {},
|
|
362
|
+
...attachments.length ? { attachments } : {},
|
|
363
|
+
signal: abort.signal
|
|
364
|
+
},
|
|
365
|
+
(event) => {
|
|
366
|
+
if (event.type === "ping" || event.type === "session") return;
|
|
367
|
+
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
368
|
+
}
|
|
369
|
+
);
|
|
353
370
|
} catch (caught) {
|
|
354
371
|
if (abort.signal.aborted) {
|
|
355
372
|
patchAssistant(assistantId, (message) => ({
|
|
@@ -391,8 +408,8 @@ function useFrankieChat() {
|
|
|
391
408
|
}
|
|
392
409
|
|
|
393
410
|
// src/components/chat/FrankieWidget.tsx
|
|
394
|
-
import { MessageCircle, X as
|
|
395
|
-
import { useEffect as useEffect5, useState as
|
|
411
|
+
import { MessageCircle, X as X4 } from "lucide-react";
|
|
412
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
396
413
|
|
|
397
414
|
// src/lib/theme.ts
|
|
398
415
|
function themeToCssVars(theme) {
|
|
@@ -452,8 +469,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
452
469
|
}
|
|
453
470
|
|
|
454
471
|
// src/components/chat/ChatPanel.tsx
|
|
455
|
-
import { RotateCcw, X as
|
|
456
|
-
import { useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
472
|
+
import { RotateCcw, X as X3 } from "lucide-react";
|
|
473
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
|
|
457
474
|
|
|
458
475
|
// src/components/ui/avatar.tsx
|
|
459
476
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -512,7 +529,7 @@ var ScrollArea = forwardRef(
|
|
|
512
529
|
);
|
|
513
530
|
|
|
514
531
|
// src/components/chat/Composer.tsx
|
|
515
|
-
import { ArrowUp, Square } from "lucide-react";
|
|
532
|
+
import { ArrowUp, Paperclip, Square, X } from "lucide-react";
|
|
516
533
|
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
517
534
|
|
|
518
535
|
// src/components/ui/textarea.tsx
|
|
@@ -535,60 +552,189 @@ var Textarea = forwardRef2(
|
|
|
535
552
|
);
|
|
536
553
|
|
|
537
554
|
// src/components/chat/Composer.tsx
|
|
538
|
-
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
555
|
+
import { Fragment, jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
539
556
|
var MAX_ROWS_PX = 140;
|
|
540
|
-
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus }) {
|
|
557
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
541
558
|
const [value, setValue] = useState3("");
|
|
559
|
+
const [files, setFiles] = useState3([]);
|
|
560
|
+
const [fileError, setFileError] = useState3(void 0);
|
|
542
561
|
const textareaRef = useRef2(null);
|
|
562
|
+
const fileInputRef = useRef2(null);
|
|
563
|
+
const canAttach = Boolean(attachments?.enabled);
|
|
564
|
+
const models = modelPicker?.models ?? [];
|
|
543
565
|
useEffect3(() => {
|
|
544
566
|
const node = textareaRef.current;
|
|
545
567
|
if (!node) return;
|
|
546
568
|
node.style.height = "auto";
|
|
547
569
|
node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
|
|
548
570
|
}, [value]);
|
|
571
|
+
const addFiles = useCallback2(
|
|
572
|
+
async (list) => {
|
|
573
|
+
if (!list || !attachments) return;
|
|
574
|
+
setFileError(void 0);
|
|
575
|
+
const room = attachments.maxCount - files.length;
|
|
576
|
+
const chosen = Array.from(list).slice(0, Math.max(0, room));
|
|
577
|
+
if (Array.from(list).length > room) setFileError(`You can attach up to ${attachments.maxCount} file(s).`);
|
|
578
|
+
const next = [];
|
|
579
|
+
for (const file of chosen) {
|
|
580
|
+
if (!mimeAccepted(file.type || "application/octet-stream", attachments.accept)) {
|
|
581
|
+
setFileError(`Files of type ${file.type || "unknown"} aren't allowed.`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (file.size > attachments.maxMb * 1024 * 1024) {
|
|
585
|
+
setFileError(`Each file must be under ${attachments.maxMb} MB.`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
next.push({
|
|
590
|
+
id: `${file.name}-${file.size}-${Math.random().toString(36).slice(2, 7)}`,
|
|
591
|
+
name: file.name,
|
|
592
|
+
mimeType: file.type || "application/octet-stream",
|
|
593
|
+
data: await readAsBase64(file)
|
|
594
|
+
});
|
|
595
|
+
} catch {
|
|
596
|
+
setFileError("Couldn't read that file.");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (next.length) setFiles((current) => [...current, ...next]);
|
|
600
|
+
},
|
|
601
|
+
[attachments, files.length]
|
|
602
|
+
);
|
|
603
|
+
const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
|
|
549
604
|
const submit = useCallback2(() => {
|
|
550
605
|
const text = value.trim();
|
|
551
|
-
if (!text || busy || disabled) return;
|
|
606
|
+
if (!text && files.length === 0 || busy || disabled) return;
|
|
552
607
|
setValue("");
|
|
553
|
-
|
|
554
|
-
|
|
608
|
+
const attached = files.map(({ name, mimeType, data }) => ({ name, mimeType, data }));
|
|
609
|
+
setFiles([]);
|
|
610
|
+
setFileError(void 0);
|
|
611
|
+
onSend(text, {
|
|
612
|
+
...attached.length ? { attachments: attached } : {},
|
|
613
|
+
...modelPicker?.value ? { model: modelPicker.value } : {}
|
|
614
|
+
});
|
|
615
|
+
}, [busy, disabled, files, modelPicker?.value, onSend, value]);
|
|
555
616
|
const onKeyDown = (event) => {
|
|
556
617
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
557
618
|
event.preventDefault();
|
|
558
619
|
submit();
|
|
559
620
|
}
|
|
560
621
|
};
|
|
622
|
+
const nothingToSend = value.trim().length === 0 && files.length === 0;
|
|
561
623
|
return /* @__PURE__ */ jsxs(
|
|
562
624
|
"form",
|
|
563
625
|
{
|
|
564
|
-
className: "flex
|
|
626
|
+
className: "flex flex-col gap-2",
|
|
565
627
|
onSubmit: (event) => {
|
|
566
628
|
event.preventDefault();
|
|
567
629
|
submit();
|
|
568
630
|
},
|
|
569
631
|
children: [
|
|
570
|
-
/* @__PURE__ */ jsx7(
|
|
571
|
-
|
|
632
|
+
files.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2", children: files.map((file) => /* @__PURE__ */ jsxs(
|
|
633
|
+
"span",
|
|
572
634
|
{
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
635
|
+
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",
|
|
636
|
+
children: [
|
|
637
|
+
/* @__PURE__ */ jsx7(Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
638
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate", children: file.name }),
|
|
639
|
+
/* @__PURE__ */ jsx7(
|
|
640
|
+
"button",
|
|
641
|
+
{
|
|
642
|
+
type: "button",
|
|
643
|
+
onClick: () => removeFile(file.id),
|
|
644
|
+
"aria-label": `Remove ${file.name}`,
|
|
645
|
+
className: "frankie-focus shrink-0 opacity-60 hover:opacity-100",
|
|
646
|
+
children: /* @__PURE__ */ jsx7(X, { className: "size-3" })
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
]
|
|
650
|
+
},
|
|
651
|
+
file.id
|
|
652
|
+
)) }) : null,
|
|
653
|
+
fileError ? /* @__PURE__ */ jsx7("p", { className: "text-xs text-red-600 dark:text-red-400", children: fileError }) : null,
|
|
654
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
|
|
655
|
+
canAttach ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
656
|
+
/* @__PURE__ */ jsx7(
|
|
657
|
+
"input",
|
|
658
|
+
{
|
|
659
|
+
ref: fileInputRef,
|
|
660
|
+
type: "file",
|
|
661
|
+
multiple: true,
|
|
662
|
+
hidden: true,
|
|
663
|
+
accept: attachments?.accept.join(","),
|
|
664
|
+
onChange: (event) => {
|
|
665
|
+
void addFiles(event.target.files);
|
|
666
|
+
event.target.value = "";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
/* @__PURE__ */ jsx7(
|
|
671
|
+
Button,
|
|
672
|
+
{
|
|
673
|
+
type: "button",
|
|
674
|
+
size: "icon",
|
|
675
|
+
variant: "outline",
|
|
676
|
+
disabled: disabled || files.length >= (attachments?.maxCount ?? 0),
|
|
677
|
+
onClick: () => fileInputRef.current?.click(),
|
|
678
|
+
"aria-label": "Attach files",
|
|
679
|
+
title: "Attach files",
|
|
680
|
+
children: /* @__PURE__ */ jsx7(Paperclip, {})
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
] }) : null,
|
|
684
|
+
/* @__PURE__ */ jsx7(
|
|
685
|
+
Textarea,
|
|
686
|
+
{
|
|
687
|
+
ref: textareaRef,
|
|
688
|
+
rows: 1,
|
|
689
|
+
value,
|
|
690
|
+
disabled,
|
|
691
|
+
autoFocus,
|
|
692
|
+
onChange: (event) => setValue(event.target.value),
|
|
693
|
+
onKeyDown,
|
|
694
|
+
placeholder: placeholder ?? "Ask anything\u2026",
|
|
695
|
+
"aria-label": "Message"
|
|
696
|
+
}
|
|
697
|
+
),
|
|
698
|
+
models.length > 0 ? /* @__PURE__ */ jsx7(
|
|
699
|
+
"select",
|
|
700
|
+
{
|
|
701
|
+
value: modelPicker?.value ?? models[0]?.id,
|
|
702
|
+
onChange: (event) => modelPicker?.onChange(event.target.value),
|
|
703
|
+
disabled,
|
|
704
|
+
"aria-label": "Model",
|
|
705
|
+
className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
|
|
706
|
+
children: models.map((model) => /* @__PURE__ */ jsx7("option", { value: model.id, children: model.label }, model.id))
|
|
707
|
+
}
|
|
708
|
+
) : null,
|
|
709
|
+
busy ? /* @__PURE__ */ jsx7(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx7(Square, {}) }) : /* @__PURE__ */ jsx7(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx7(ArrowUp, {}) })
|
|
710
|
+
] })
|
|
585
711
|
]
|
|
586
712
|
}
|
|
587
713
|
);
|
|
588
714
|
}
|
|
715
|
+
function mimeAccepted(mimeType, accept) {
|
|
716
|
+
return accept.some(
|
|
717
|
+
(pattern) => pattern.endsWith("/*") ? mimeType.startsWith(pattern.slice(0, -1)) : pattern === mimeType
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
function readAsBase64(file) {
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
const reader = new FileReader();
|
|
723
|
+
reader.onload = () => {
|
|
724
|
+
const result = String(reader.result);
|
|
725
|
+
const comma = result.indexOf(",");
|
|
726
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
727
|
+
};
|
|
728
|
+
reader.onerror = () => reject(reader.error);
|
|
729
|
+
reader.readAsDataURL(file);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/components/chat/MessageBubble.tsx
|
|
734
|
+
import { Paperclip as Paperclip2 } from "lucide-react";
|
|
589
735
|
|
|
590
736
|
// src/components/chat/Markdown.tsx
|
|
591
|
-
import { Fragment } from "react";
|
|
737
|
+
import { Fragment as Fragment2 } from "react";
|
|
592
738
|
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
593
739
|
function Markdown({ text, className }) {
|
|
594
740
|
return /* @__PURE__ */ jsx8("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
|
|
@@ -673,7 +819,7 @@ function renderInline(text) {
|
|
|
673
819
|
const token = match[0];
|
|
674
820
|
const start = match.index;
|
|
675
821
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
676
|
-
nodes.push(/* @__PURE__ */ jsx8(
|
|
822
|
+
nodes.push(/* @__PURE__ */ jsx8(Fragment2, { children: renderToken(token) }, start));
|
|
677
823
|
lastIndex = start + token.length;
|
|
678
824
|
}
|
|
679
825
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
@@ -690,7 +836,7 @@ function renderToken(token) {
|
|
|
690
836
|
}
|
|
691
837
|
|
|
692
838
|
// src/components/chat/ToolActivityList.tsx
|
|
693
|
-
import { Check, Loader2, ShieldAlert, X } from "lucide-react";
|
|
839
|
+
import { Check, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
694
840
|
|
|
695
841
|
// src/components/ui/badge.tsx
|
|
696
842
|
import { cva as cva2 } from "class-variance-authority";
|
|
@@ -738,7 +884,7 @@ function ToolIcon({ status }) {
|
|
|
738
884
|
case "denied":
|
|
739
885
|
return /* @__PURE__ */ jsx10(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
740
886
|
default:
|
|
741
|
-
return /* @__PURE__ */ jsx10(
|
|
887
|
+
return /* @__PURE__ */ jsx10(X2, { className: "size-3", "aria-hidden": "true" });
|
|
742
888
|
}
|
|
743
889
|
}
|
|
744
890
|
function badgeVariant(status) {
|
|
@@ -758,7 +904,20 @@ function MessageBubble({
|
|
|
758
904
|
renderTool
|
|
759
905
|
}) {
|
|
760
906
|
if (message.role === "user") {
|
|
761
|
-
return /* @__PURE__ */
|
|
907
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-end gap-1", children: [
|
|
908
|
+
message.attachments?.length ? /* @__PURE__ */ jsx11("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs3(
|
|
909
|
+
"span",
|
|
910
|
+
{
|
|
911
|
+
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",
|
|
912
|
+
children: [
|
|
913
|
+
/* @__PURE__ */ jsx11(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
|
|
914
|
+
/* @__PURE__ */ jsx11("span", { className: "truncate", children: file.name })
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
`${file.name}-${index}`
|
|
918
|
+
)) }) : null,
|
|
919
|
+
message.text ? /* @__PURE__ */ jsx11("div", { className: "max-w-[85%] rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }) : null
|
|
920
|
+
] });
|
|
762
921
|
}
|
|
763
922
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
764
923
|
return /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
|
|
@@ -820,6 +979,9 @@ function ChatPanel({
|
|
|
820
979
|
const theme = useFrankieTheme();
|
|
821
980
|
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
822
981
|
const scrollRef = useRef3(null);
|
|
982
|
+
const features = config?.features;
|
|
983
|
+
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
984
|
+
const [model, setModel] = useState4(void 0);
|
|
823
985
|
useEffect4(() => {
|
|
824
986
|
const node = scrollRef.current;
|
|
825
987
|
if (!node) return;
|
|
@@ -847,7 +1009,7 @@ function ChatPanel({
|
|
|
847
1009
|
config?.branding.tagline ? /* @__PURE__ */ jsx13(CardDescription, { children: config.branding.tagline }) : null
|
|
848
1010
|
] }),
|
|
849
1011
|
/* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx13(RotateCcw, {}) }),
|
|
850
|
-
onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(
|
|
1012
|
+
onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(X3, {}) }) : null
|
|
851
1013
|
] }),
|
|
852
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__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: configError }) : loading ? /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: "Loading\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
|
|
853
1015
|
/* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
@@ -875,7 +1037,9 @@ function ChatPanel({
|
|
|
875
1037
|
busy,
|
|
876
1038
|
disabled: Boolean(configError),
|
|
877
1039
|
...placeholder ? { placeholder } : {},
|
|
878
|
-
...autoFocus ? { autoFocus } : {}
|
|
1040
|
+
...autoFocus ? { autoFocus } : {},
|
|
1041
|
+
...features?.attachments.enabled ? { attachments: features.attachments } : {},
|
|
1042
|
+
...pickerModels.length ? { modelPicker: { models: pickerModels, value: model ?? pickerModels[0]?.id, onChange: setModel } } : {}
|
|
879
1043
|
}
|
|
880
1044
|
) }),
|
|
881
1045
|
footer
|
|
@@ -898,7 +1062,7 @@ function FrankieWidget({
|
|
|
898
1062
|
}) {
|
|
899
1063
|
const { config } = useFrankie();
|
|
900
1064
|
const theme = useFrankieTheme();
|
|
901
|
-
const [uncontrolledOpen, setUncontrolledOpen] =
|
|
1065
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState5(defaultOpen);
|
|
902
1066
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
903
1067
|
const setOpen = (next) => {
|
|
904
1068
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
@@ -932,7 +1096,7 @@ function FrankieWidget({
|
|
|
932
1096
|
"aria-expanded": open,
|
|
933
1097
|
"aria-label": open ? "Close the assistant" : label,
|
|
934
1098
|
title: label,
|
|
935
|
-
children: open ? /* @__PURE__ */ jsx14(
|
|
1099
|
+
children: open ? /* @__PURE__ */ jsx14(X4, { className: "size-5" }) : /* @__PURE__ */ jsx14(MessageCircle, { className: "size-5" })
|
|
936
1100
|
}
|
|
937
1101
|
)
|
|
938
1102
|
] })
|