@frankieai/react 0.2.8 → 0.2.9
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 +373 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -8
- package/dist/index.d.ts +60 -8
- package/dist/index.js +356 -172
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/components/chat/ChatPanel.tsx +9 -1
- package/src/components/chat/Choices.tsx +52 -0
- package/src/components/chat/Composer.tsx +12 -2
- package/src/components/chat/FrankieChatPage.tsx +4 -1
- package/src/components/chat/FrankieInline.tsx +7 -2
- package/src/components/chat/FrankieWidget.tsx +11 -2
- package/src/components/chat/Markdown.tsx +15 -8
- package/src/components/chat/MessageBubble.tsx +29 -5
- package/src/hooks/useFrankieChat.ts +57 -1
- package/src/index.ts +3 -0
- package/src/lib/choices.ts +49 -0
- package/src/lib/styles-ready.ts +71 -0
- package/src/styles/frankie.css +106 -3
- package/src/styles-ready.test.tsx +71 -0
package/dist/index.js
CHANGED
|
@@ -401,6 +401,41 @@ function useFrankieChatState() {
|
|
|
401
401
|
setMessages(next);
|
|
402
402
|
const abort = new AbortController();
|
|
403
403
|
abortRef.current = abort;
|
|
404
|
+
let bufferedText = "";
|
|
405
|
+
let pendingThinking = "";
|
|
406
|
+
let frame = null;
|
|
407
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
|
|
408
|
+
const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
|
|
409
|
+
const revealTick = () => {
|
|
410
|
+
frame = null;
|
|
411
|
+
let addText = "";
|
|
412
|
+
if (bufferedText) {
|
|
413
|
+
const step = Math.max(3, Math.ceil(bufferedText.length / 6));
|
|
414
|
+
addText = bufferedText.slice(0, step);
|
|
415
|
+
bufferedText = bufferedText.slice(step);
|
|
416
|
+
}
|
|
417
|
+
const addThinking = pendingThinking;
|
|
418
|
+
pendingThinking = "";
|
|
419
|
+
if (addText || addThinking) {
|
|
420
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
421
|
+
}
|
|
422
|
+
if (bufferedText) scheduleReveal();
|
|
423
|
+
};
|
|
424
|
+
const scheduleReveal = () => {
|
|
425
|
+
if (frame === null) frame = raf(revealTick);
|
|
426
|
+
};
|
|
427
|
+
const flushDeltas = () => {
|
|
428
|
+
if (frame !== null) {
|
|
429
|
+
cancelRaf(frame);
|
|
430
|
+
frame = null;
|
|
431
|
+
}
|
|
432
|
+
if (!bufferedText && !pendingThinking) return;
|
|
433
|
+
const addText = bufferedText;
|
|
434
|
+
const addThinking = pendingThinking;
|
|
435
|
+
bufferedText = "";
|
|
436
|
+
pendingThinking = "";
|
|
437
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
438
|
+
};
|
|
404
439
|
try {
|
|
405
440
|
let receivedText = false;
|
|
406
441
|
let receivedToolActivity = false;
|
|
@@ -422,11 +457,23 @@ function useFrankieChatState() {
|
|
|
422
457
|
return;
|
|
423
458
|
}
|
|
424
459
|
if (event.type === "ping" || event.type === "session") return;
|
|
425
|
-
if (event.type === "text_delta")
|
|
460
|
+
if (event.type === "text_delta") {
|
|
461
|
+
receivedText = true;
|
|
462
|
+
bufferedText += event.text;
|
|
463
|
+
scheduleReveal();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (event.type === "thinking_delta") {
|
|
467
|
+
pendingThinking += event.text;
|
|
468
|
+
scheduleReveal();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
426
471
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
472
|
+
flushDeltas();
|
|
427
473
|
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
428
474
|
}
|
|
429
475
|
);
|
|
476
|
+
flushDeltas();
|
|
430
477
|
if (!receivedText) {
|
|
431
478
|
patchAssistant(assistantId, (message) => ({
|
|
432
479
|
...message,
|
|
@@ -435,6 +482,7 @@ function useFrankieChatState() {
|
|
|
435
482
|
}));
|
|
436
483
|
}
|
|
437
484
|
} catch (caught) {
|
|
485
|
+
flushDeltas();
|
|
438
486
|
if (abort.signal.aborted) {
|
|
439
487
|
patchAssistant(assistantId, (message) => ({
|
|
440
488
|
...message,
|
|
@@ -447,6 +495,7 @@ function useFrankieChatState() {
|
|
|
447
495
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
448
496
|
}
|
|
449
497
|
} finally {
|
|
498
|
+
flushDeltas();
|
|
450
499
|
patchAssistant(
|
|
451
500
|
assistantId,
|
|
452
501
|
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
@@ -550,7 +599,44 @@ function useFrankieTheme() {
|
|
|
550
599
|
|
|
551
600
|
// src/components/chat/FrankieWidget.tsx
|
|
552
601
|
import { MessageCircle, X as X4 } from "lucide-react";
|
|
553
|
-
import { useEffect as
|
|
602
|
+
import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
603
|
+
|
|
604
|
+
// src/lib/styles-ready.ts
|
|
605
|
+
import { useEffect as useEffect3, useLayoutEffect, useState as useState3 } from "react";
|
|
606
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect;
|
|
607
|
+
var SENTINEL = "--frankie-styles-ready";
|
|
608
|
+
function useStylesReady(ref, timeoutMs = 600) {
|
|
609
|
+
const [ready, setReady] = useState3(false);
|
|
610
|
+
useIsomorphicLayoutEffect(() => {
|
|
611
|
+
if (ready) return;
|
|
612
|
+
const node = ref.current;
|
|
613
|
+
if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
|
|
614
|
+
setReady(true);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
|
|
618
|
+
if (applied()) {
|
|
619
|
+
setReady(true);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const deadline = Date.now() + timeoutMs;
|
|
623
|
+
let frame = 0;
|
|
624
|
+
const poll = () => {
|
|
625
|
+
if (applied() || Date.now() >= deadline) {
|
|
626
|
+
setReady(true);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
frame = window.requestAnimationFrame(poll);
|
|
630
|
+
};
|
|
631
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
632
|
+
setReady(true);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
frame = window.requestAnimationFrame(poll);
|
|
636
|
+
return () => window.cancelAnimationFrame(frame);
|
|
637
|
+
}, [ref, ready, timeoutMs]);
|
|
638
|
+
return ready;
|
|
639
|
+
}
|
|
554
640
|
|
|
555
641
|
// src/lib/theme.ts
|
|
556
642
|
function themeToCssVars(theme) {
|
|
@@ -611,7 +697,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
611
697
|
|
|
612
698
|
// src/components/chat/ChatPanel.tsx
|
|
613
699
|
import { RotateCcw, X as X3 } from "lucide-react";
|
|
614
|
-
import { useEffect as
|
|
700
|
+
import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
|
|
615
701
|
|
|
616
702
|
// src/components/ui/avatar.tsx
|
|
617
703
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -717,7 +803,7 @@ function AgentContext() {
|
|
|
717
803
|
|
|
718
804
|
// src/components/chat/Composer.tsx
|
|
719
805
|
import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
|
|
720
|
-
import { useCallback as useCallback2, useEffect as
|
|
806
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
|
|
721
807
|
|
|
722
808
|
// src/components/ui/textarea.tsx
|
|
723
809
|
import { forwardRef as forwardRef2 } from "react";
|
|
@@ -741,15 +827,22 @@ var Textarea = forwardRef2(
|
|
|
741
827
|
// src/components/chat/Composer.tsx
|
|
742
828
|
import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
743
829
|
var MAX_ROWS_PX = 140;
|
|
744
|
-
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
745
|
-
const [value, setValue] =
|
|
746
|
-
const [files, setFiles] =
|
|
747
|
-
const [fileError, setFileError] =
|
|
830
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
|
|
831
|
+
const [value, setValue] = useState4("");
|
|
832
|
+
const [files, setFiles] = useState4([]);
|
|
833
|
+
const [fileError, setFileError] = useState4(void 0);
|
|
748
834
|
const textareaRef = useRef3(null);
|
|
749
835
|
const fileInputRef = useRef3(null);
|
|
836
|
+
useEffect4(() => {
|
|
837
|
+
if (!focusHandleRef) return;
|
|
838
|
+
focusHandleRef.current = () => textareaRef.current?.focus();
|
|
839
|
+
return () => {
|
|
840
|
+
focusHandleRef.current = null;
|
|
841
|
+
};
|
|
842
|
+
}, [focusHandleRef]);
|
|
750
843
|
const canAttach = Boolean(attachments?.enabled);
|
|
751
844
|
const models = modelPicker?.models ?? [];
|
|
752
|
-
|
|
845
|
+
useEffect4(() => {
|
|
753
846
|
const node = textareaRef.current;
|
|
754
847
|
if (!node) return;
|
|
755
848
|
node.style.height = "auto";
|
|
@@ -919,14 +1012,76 @@ function readAsBase64(file) {
|
|
|
919
1012
|
|
|
920
1013
|
// src/components/chat/MessageBubble.tsx
|
|
921
1014
|
import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
|
|
922
|
-
import { useState as
|
|
1015
|
+
import { memo as memo2, useState as useState5 } from "react";
|
|
923
1016
|
|
|
924
|
-
// src/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
function
|
|
928
|
-
|
|
1017
|
+
// src/lib/choices.ts
|
|
1018
|
+
var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
|
|
1019
|
+
var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
|
|
1020
|
+
function extractChoices(text) {
|
|
1021
|
+
const closed = CLOSED.exec(text);
|
|
1022
|
+
if (closed) {
|
|
1023
|
+
const options = parseOptionLines(closed[1] ?? "");
|
|
1024
|
+
const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1025
|
+
return { body, options: options.length ? options : null };
|
|
1026
|
+
}
|
|
1027
|
+
const open = OPEN.exec(text);
|
|
1028
|
+
if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
|
|
1029
|
+
return { body: text, options: null };
|
|
1030
|
+
}
|
|
1031
|
+
function parseOptionLines(block) {
|
|
1032
|
+
return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/components/chat/Choices.tsx
|
|
1036
|
+
import { ArrowRight, PencilLine } from "lucide-react";
|
|
1037
|
+
import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1038
|
+
function Choices({
|
|
1039
|
+
options,
|
|
1040
|
+
onPick,
|
|
1041
|
+
onSomethingElse,
|
|
1042
|
+
disabled
|
|
1043
|
+
}) {
|
|
1044
|
+
if (options.length === 0) return null;
|
|
1045
|
+
return /* @__PURE__ */ jsxs3("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
|
|
1046
|
+
options.map((option, index) => /* @__PURE__ */ jsxs3(
|
|
1047
|
+
"button",
|
|
1048
|
+
{
|
|
1049
|
+
type: "button",
|
|
1050
|
+
className: "frankie-choice",
|
|
1051
|
+
disabled,
|
|
1052
|
+
onClick: () => onPick(option),
|
|
1053
|
+
children: [
|
|
1054
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
|
|
1055
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: option }),
|
|
1056
|
+
/* @__PURE__ */ jsx9(ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
|
|
1057
|
+
]
|
|
1058
|
+
},
|
|
1059
|
+
`${index}-${option}`
|
|
1060
|
+
)),
|
|
1061
|
+
onSomethingElse ? /* @__PURE__ */ jsxs3(
|
|
1062
|
+
"button",
|
|
1063
|
+
{
|
|
1064
|
+
type: "button",
|
|
1065
|
+
className: "frankie-choice frankie-choice-other",
|
|
1066
|
+
disabled,
|
|
1067
|
+
onClick: onSomethingElse,
|
|
1068
|
+
children: [
|
|
1069
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ jsx9(PencilLine, { className: "size-3.5" }) }),
|
|
1070
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
|
|
1071
|
+
/* @__PURE__ */ jsx9("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
|
|
1072
|
+
]
|
|
1073
|
+
}
|
|
1074
|
+
) : null
|
|
1075
|
+
] });
|
|
929
1076
|
}
|
|
1077
|
+
|
|
1078
|
+
// src/components/chat/Markdown.tsx
|
|
1079
|
+
import { Fragment as Fragment2, memo, useMemo as useMemo2 } from "react";
|
|
1080
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1081
|
+
var Markdown = memo(function Markdown2({ text, className }) {
|
|
1082
|
+
const blocks = useMemo2(() => renderBlocks(text), [text]);
|
|
1083
|
+
return /* @__PURE__ */ jsx10("div", { className: cn("frankie-prose", className), children: blocks });
|
|
1084
|
+
});
|
|
930
1085
|
function renderBlocks(text) {
|
|
931
1086
|
const lines = text.split("\n");
|
|
932
1087
|
const blocks = [];
|
|
@@ -935,13 +1090,13 @@ function renderBlocks(text) {
|
|
|
935
1090
|
let code = null;
|
|
936
1091
|
const flushParagraph = () => {
|
|
937
1092
|
if (paragraph.length === 0) return;
|
|
938
|
-
blocks.push(/* @__PURE__ */
|
|
1093
|
+
blocks.push(/* @__PURE__ */ jsx10("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
939
1094
|
paragraph = [];
|
|
940
1095
|
};
|
|
941
1096
|
const flushList = () => {
|
|
942
1097
|
if (!list) return;
|
|
943
|
-
const items = list.items.map((item, index) => /* @__PURE__ */
|
|
944
|
-
blocks.push(list.ordered ? /* @__PURE__ */
|
|
1098
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ jsx10("li", { children: renderInline(item) }, index));
|
|
1099
|
+
blocks.push(list.ordered ? /* @__PURE__ */ jsx10("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx10("ul", { children: items }, `l${blocks.length}`));
|
|
945
1100
|
list = null;
|
|
946
1101
|
};
|
|
947
1102
|
for (const line of lines) {
|
|
@@ -949,7 +1104,7 @@ function renderBlocks(text) {
|
|
|
949
1104
|
if (fence) {
|
|
950
1105
|
if (code) {
|
|
951
1106
|
blocks.push(
|
|
952
|
-
/* @__PURE__ */
|
|
1107
|
+
/* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
953
1108
|
);
|
|
954
1109
|
code = null;
|
|
955
1110
|
} else {
|
|
@@ -980,11 +1135,13 @@ function renderBlocks(text) {
|
|
|
980
1135
|
flushList();
|
|
981
1136
|
continue;
|
|
982
1137
|
}
|
|
983
|
-
const heading =
|
|
1138
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
984
1139
|
if (heading) {
|
|
985
1140
|
flushParagraph();
|
|
986
1141
|
flushList();
|
|
987
|
-
|
|
1142
|
+
const level = (heading[1] ?? "#").length;
|
|
1143
|
+
const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
|
|
1144
|
+
blocks.push(/* @__PURE__ */ jsx10(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
|
|
988
1145
|
continue;
|
|
989
1146
|
}
|
|
990
1147
|
flushList();
|
|
@@ -992,7 +1149,7 @@ function renderBlocks(text) {
|
|
|
992
1149
|
}
|
|
993
1150
|
if (code) {
|
|
994
1151
|
blocks.push(
|
|
995
|
-
/* @__PURE__ */
|
|
1152
|
+
/* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
996
1153
|
);
|
|
997
1154
|
}
|
|
998
1155
|
flushParagraph();
|
|
@@ -1007,20 +1164,20 @@ function renderInline(text) {
|
|
|
1007
1164
|
const token = match[0];
|
|
1008
1165
|
const start = match.index;
|
|
1009
1166
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
1010
|
-
nodes.push(/* @__PURE__ */
|
|
1167
|
+
nodes.push(/* @__PURE__ */ jsx10(Fragment2, { children: renderToken(token) }, start));
|
|
1011
1168
|
lastIndex = start + token.length;
|
|
1012
1169
|
}
|
|
1013
1170
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
1014
1171
|
return nodes;
|
|
1015
1172
|
}
|
|
1016
1173
|
function renderToken(token) {
|
|
1017
|
-
if (token.startsWith("**")) return /* @__PURE__ */
|
|
1018
|
-
if (token.startsWith("`")) return /* @__PURE__ */
|
|
1174
|
+
if (token.startsWith("**")) return /* @__PURE__ */ jsx10("strong", { children: token.slice(2, -2) });
|
|
1175
|
+
if (token.startsWith("`")) return /* @__PURE__ */ jsx10("code", { children: token.slice(1, -1) });
|
|
1019
1176
|
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
1020
1177
|
const href = link ? link[2] ?? "" : token;
|
|
1021
1178
|
const label = link ? link[1] ?? "" : token;
|
|
1022
1179
|
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
1023
|
-
return /* @__PURE__ */
|
|
1180
|
+
return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
1024
1181
|
}
|
|
1025
1182
|
|
|
1026
1183
|
// src/components/chat/ToolActivityList.tsx
|
|
@@ -1028,7 +1185,7 @@ import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react"
|
|
|
1028
1185
|
|
|
1029
1186
|
// src/components/ui/badge.tsx
|
|
1030
1187
|
import { cva as cva2 } from "class-variance-authority";
|
|
1031
|
-
import { jsx as
|
|
1188
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1032
1189
|
var badgeVariants = cva2(
|
|
1033
1190
|
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
1034
1191
|
{
|
|
@@ -1048,44 +1205,44 @@ function Badge({
|
|
|
1048
1205
|
variant,
|
|
1049
1206
|
...props
|
|
1050
1207
|
}) {
|
|
1051
|
-
return /* @__PURE__ */
|
|
1208
|
+
return /* @__PURE__ */ jsx11("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
1052
1209
|
}
|
|
1053
1210
|
|
|
1054
1211
|
// src/components/chat/ToolActivityList.tsx
|
|
1055
|
-
import { jsx as
|
|
1212
|
+
import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1056
1213
|
function ToolActivityList({ tools, renderTool }) {
|
|
1057
1214
|
if (tools.length === 0) return null;
|
|
1058
1215
|
const running = tools.some((tool) => tool.status === "running");
|
|
1059
|
-
return /* @__PURE__ */
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
/* @__PURE__ */
|
|
1062
|
-
/* @__PURE__ */
|
|
1063
|
-
/* @__PURE__ */
|
|
1216
|
+
return /* @__PURE__ */ jsxs4("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1217
|
+
/* @__PURE__ */ jsxs4("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: [
|
|
1218
|
+
/* @__PURE__ */ jsx12(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1219
|
+
/* @__PURE__ */ jsx12("span", { children: running ? "Working" : "Activity" }),
|
|
1220
|
+
/* @__PURE__ */ jsxs4("span", { className: "text-[11px]", children: [
|
|
1064
1221
|
"\xB7 ",
|
|
1065
1222
|
tools.length,
|
|
1066
1223
|
" tool call",
|
|
1067
1224
|
tools.length === 1 ? "" : "s"
|
|
1068
1225
|
] })
|
|
1069
1226
|
] }),
|
|
1070
|
-
/* @__PURE__ */
|
|
1227
|
+
/* @__PURE__ */ jsx12("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx12("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx12(ToolActivityRow, { tool }) }, tool.id)) })
|
|
1071
1228
|
] });
|
|
1072
1229
|
}
|
|
1073
1230
|
function ToolActivityRow({ tool }) {
|
|
1074
|
-
return /* @__PURE__ */
|
|
1075
|
-
/* @__PURE__ */
|
|
1076
|
-
/* @__PURE__ */
|
|
1231
|
+
return /* @__PURE__ */ jsxs4(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
1232
|
+
/* @__PURE__ */ jsx12(ToolIcon, { status: tool.status }),
|
|
1233
|
+
/* @__PURE__ */ jsx12("span", { className: "truncate", children: tool.label })
|
|
1077
1234
|
] });
|
|
1078
1235
|
}
|
|
1079
1236
|
function ToolIcon({ status }) {
|
|
1080
1237
|
switch (status) {
|
|
1081
1238
|
case "running":
|
|
1082
|
-
return /* @__PURE__ */
|
|
1239
|
+
return /* @__PURE__ */ jsx12(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
1083
1240
|
case "done":
|
|
1084
|
-
return /* @__PURE__ */
|
|
1241
|
+
return /* @__PURE__ */ jsx12(Check, { className: "size-3", "aria-hidden": "true" });
|
|
1085
1242
|
case "denied":
|
|
1086
|
-
return /* @__PURE__ */
|
|
1243
|
+
return /* @__PURE__ */ jsx12(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
1087
1244
|
default:
|
|
1088
|
-
return /* @__PURE__ */
|
|
1245
|
+
return /* @__PURE__ */ jsx12(X2, { className: "size-3", "aria-hidden": "true" });
|
|
1089
1246
|
}
|
|
1090
1247
|
}
|
|
1091
1248
|
function badgeVariant(status) {
|
|
@@ -1096,47 +1253,54 @@ function badgeVariant(status) {
|
|
|
1096
1253
|
}
|
|
1097
1254
|
|
|
1098
1255
|
// src/components/chat/MessageBubble.tsx
|
|
1099
|
-
import { jsx as
|
|
1100
|
-
|
|
1256
|
+
import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1257
|
+
var MessageBubble = memo2(function MessageBubble2({
|
|
1101
1258
|
message,
|
|
1102
1259
|
assistantName,
|
|
1103
1260
|
avatarUrl,
|
|
1104
1261
|
showToolActivity,
|
|
1105
|
-
renderTool
|
|
1262
|
+
renderTool,
|
|
1263
|
+
onChoose,
|
|
1264
|
+
onRequestInput,
|
|
1265
|
+
active,
|
|
1266
|
+
busy
|
|
1106
1267
|
}) {
|
|
1107
1268
|
if (message.role === "user") {
|
|
1108
|
-
return /* @__PURE__ */
|
|
1109
|
-
message.attachments?.length ? /* @__PURE__ */
|
|
1269
|
+
return /* @__PURE__ */ jsxs5("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
|
|
1270
|
+
message.attachments?.length ? /* @__PURE__ */ jsx13("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs5(
|
|
1110
1271
|
"span",
|
|
1111
1272
|
{
|
|
1112
1273
|
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",
|
|
1113
1274
|
children: [
|
|
1114
|
-
/* @__PURE__ */
|
|
1115
|
-
/* @__PURE__ */
|
|
1275
|
+
/* @__PURE__ */ jsx13(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
|
|
1276
|
+
/* @__PURE__ */ jsx13("span", { className: "truncate", children: file.name })
|
|
1116
1277
|
]
|
|
1117
1278
|
},
|
|
1118
1279
|
`${file.name}-${index}`
|
|
1119
1280
|
)) }) : null,
|
|
1120
|
-
message.text ? /* @__PURE__ */
|
|
1121
|
-
/* @__PURE__ */
|
|
1122
|
-
/* @__PURE__ */
|
|
1281
|
+
message.text ? /* @__PURE__ */ jsxs5("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1282
|
+
/* @__PURE__ */ jsx13("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1283
|
+
/* @__PURE__ */ jsx13(MessageCopyButton, { text: message.text })
|
|
1123
1284
|
] }) : null
|
|
1124
1285
|
] });
|
|
1125
1286
|
}
|
|
1126
1287
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1288
|
+
const { body, options } = extractChoices(message.text);
|
|
1289
|
+
const showChoices = Boolean(active && onChoose && options && message.status === "done");
|
|
1290
|
+
return /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-message flex gap-2", children: [
|
|
1291
|
+
/* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1292
|
+
/* @__PURE__ */ jsx13(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1293
|
+
body ? /* @__PURE__ */ jsx13(MessageCopyButton, { text: body }) : null
|
|
1131
1294
|
] }),
|
|
1132
|
-
/* @__PURE__ */
|
|
1133
|
-
message.status === "error" ? /* @__PURE__ */
|
|
1134
|
-
|
|
1295
|
+
/* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
|
|
1296
|
+
message.status === "error" ? /* @__PURE__ */ jsx13("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__ */ jsx13(TypingDots, {}) : /* @__PURE__ */ jsx13(Markdown, { text: body, className: "text-sm" }),
|
|
1297
|
+
showChoices ? /* @__PURE__ */ jsx13(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
|
|
1298
|
+
showToolActivity ? /* @__PURE__ */ jsx13(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
1135
1299
|
] })
|
|
1136
1300
|
] });
|
|
1137
|
-
}
|
|
1301
|
+
});
|
|
1138
1302
|
function MessageCopyButton({ text }) {
|
|
1139
|
-
const [copied, setCopied] =
|
|
1303
|
+
const [copied, setCopied] = useState5(false);
|
|
1140
1304
|
const copy = async () => {
|
|
1141
1305
|
try {
|
|
1142
1306
|
await navigator.clipboard.writeText(text);
|
|
@@ -1145,12 +1309,12 @@ function MessageCopyButton({ text }) {
|
|
|
1145
1309
|
} catch {
|
|
1146
1310
|
}
|
|
1147
1311
|
};
|
|
1148
|
-
return /* @__PURE__ */
|
|
1312
|
+
return /* @__PURE__ */ jsx13("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__ */ jsx13(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx13(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1149
1313
|
}
|
|
1150
1314
|
function TypingDots() {
|
|
1151
|
-
return /* @__PURE__ */
|
|
1152
|
-
/* @__PURE__ */
|
|
1153
|
-
[0, 1, 2].map((index) => /* @__PURE__ */
|
|
1315
|
+
return /* @__PURE__ */ jsxs5("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
|
|
1316
|
+
/* @__PURE__ */ jsx13("span", { className: "frankie-thinking-label", children: "Thinking" }),
|
|
1317
|
+
[0, 1, 2].map((index) => /* @__PURE__ */ jsx13(
|
|
1154
1318
|
"span",
|
|
1155
1319
|
{
|
|
1156
1320
|
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
@@ -1162,14 +1326,14 @@ function TypingDots() {
|
|
|
1162
1326
|
}
|
|
1163
1327
|
|
|
1164
1328
|
// src/components/chat/SuggestedPrompts.tsx
|
|
1165
|
-
import { jsx as
|
|
1329
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1166
1330
|
function SuggestedPrompts({
|
|
1167
1331
|
prompts,
|
|
1168
1332
|
onPick,
|
|
1169
1333
|
disabled
|
|
1170
1334
|
}) {
|
|
1171
1335
|
if (prompts.length === 0) return null;
|
|
1172
|
-
return /* @__PURE__ */
|
|
1336
|
+
return /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx14(
|
|
1173
1337
|
Button,
|
|
1174
1338
|
{
|
|
1175
1339
|
type: "button",
|
|
@@ -1184,7 +1348,7 @@ function SuggestedPrompts({
|
|
|
1184
1348
|
}
|
|
1185
1349
|
|
|
1186
1350
|
// src/components/chat/ChatPanel.tsx
|
|
1187
|
-
import { jsx as
|
|
1351
|
+
import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1188
1352
|
function ChatPanel({
|
|
1189
1353
|
className,
|
|
1190
1354
|
header,
|
|
@@ -1204,8 +1368,10 @@ function ChatPanel({
|
|
|
1204
1368
|
const atBottomRef = useRef4(true);
|
|
1205
1369
|
const features = config?.features;
|
|
1206
1370
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1207
|
-
const [model, setModel] =
|
|
1208
|
-
|
|
1371
|
+
const [model, setModel] = useState6(void 0);
|
|
1372
|
+
const composerFocus = useRef4(null);
|
|
1373
|
+
const focusComposer = () => composerFocus.current?.();
|
|
1374
|
+
useEffect5(() => {
|
|
1209
1375
|
const node = scrollRef.current;
|
|
1210
1376
|
if (!node) return;
|
|
1211
1377
|
const onScroll = () => {
|
|
@@ -1214,14 +1380,14 @@ function ChatPanel({
|
|
|
1214
1380
|
node.addEventListener("scroll", onScroll, { passive: true });
|
|
1215
1381
|
return () => node.removeEventListener("scroll", onScroll);
|
|
1216
1382
|
}, []);
|
|
1217
|
-
|
|
1383
|
+
useEffect5(() => {
|
|
1218
1384
|
const node = scrollRef.current;
|
|
1219
1385
|
if (!node || !atBottomRef.current) return;
|
|
1220
1386
|
node.scrollTop = node.scrollHeight;
|
|
1221
1387
|
}, [messages]);
|
|
1222
1388
|
const name = config?.branding.name ?? "Assistant";
|
|
1223
1389
|
const mode = resolveMode(theme?.mode);
|
|
1224
|
-
return /* @__PURE__ */
|
|
1390
|
+
return /* @__PURE__ */ jsxs6(
|
|
1225
1391
|
Card,
|
|
1226
1392
|
{
|
|
1227
1393
|
className: cn(
|
|
@@ -1234,44 +1400,49 @@ function ChatPanel({
|
|
|
1234
1400
|
style: themeToCssVars(theme),
|
|
1235
1401
|
"data-frankie-mode": mode,
|
|
1236
1402
|
children: [
|
|
1237
|
-
header === false ? null : header !== void 0 ? header : /* @__PURE__ */
|
|
1238
|
-
/* @__PURE__ */
|
|
1239
|
-
/* @__PURE__ */
|
|
1240
|
-
/* @__PURE__ */
|
|
1241
|
-
config?.branding.tagline ? /* @__PURE__ */
|
|
1403
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs6(CardHeader, { children: [
|
|
1404
|
+
/* @__PURE__ */ jsx15(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
1405
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
1406
|
+
/* @__PURE__ */ jsx15(CardTitle, { children: name }),
|
|
1407
|
+
config?.branding.tagline ? /* @__PURE__ */ jsx15(CardDescription, { children: config.branding.tagline }) : null
|
|
1242
1408
|
] }),
|
|
1243
|
-
/* @__PURE__ */
|
|
1244
|
-
onClose ? /* @__PURE__ */
|
|
1409
|
+
/* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx15(RotateCcw, {}) }),
|
|
1410
|
+
onClose ? /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx15(X3, {}) }) : null
|
|
1245
1411
|
] }),
|
|
1246
|
-
/* @__PURE__ */
|
|
1247
|
-
/* @__PURE__ */
|
|
1248
|
-
/* @__PURE__ */
|
|
1249
|
-
/* @__PURE__ */
|
|
1250
|
-
/* @__PURE__ */
|
|
1251
|
-
] }) : loading ? /* @__PURE__ */
|
|
1252
|
-
/* @__PURE__ */
|
|
1253
|
-
/* @__PURE__ */
|
|
1254
|
-
] }) : /* @__PURE__ */
|
|
1255
|
-
(message) => renderMessage ? /* @__PURE__ */
|
|
1412
|
+
/* @__PURE__ */ jsx15(AgentContext, {}),
|
|
1413
|
+
/* @__PURE__ */ jsx15(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx15(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs6("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1414
|
+
/* @__PURE__ */ jsx15("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1415
|
+
/* @__PURE__ */ jsx15("p", { children: configError }),
|
|
1416
|
+
/* @__PURE__ */ jsx15("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1417
|
+
] }) : loading ? /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-4", children: [
|
|
1418
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1419
|
+
/* @__PURE__ */ jsx15(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1420
|
+
] }) : /* @__PURE__ */ jsx15("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
|
|
1421
|
+
(message, index) => renderMessage ? /* @__PURE__ */ jsx15("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx15(
|
|
1256
1422
|
MessageBubble,
|
|
1257
1423
|
{
|
|
1258
1424
|
message,
|
|
1259
1425
|
assistantName: name,
|
|
1260
1426
|
...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
|
|
1261
1427
|
showToolActivity: config?.showToolActivity ?? true,
|
|
1262
|
-
...renderTool ? { renderTool } : {}
|
|
1428
|
+
...renderTool ? { renderTool } : {},
|
|
1429
|
+
onChoose: send,
|
|
1430
|
+
onRequestInput: focusComposer,
|
|
1431
|
+
active: index === messages.length - 1,
|
|
1432
|
+
busy
|
|
1263
1433
|
},
|
|
1264
1434
|
message.id
|
|
1265
1435
|
)
|
|
1266
1436
|
) }) }) }),
|
|
1267
|
-
/* @__PURE__ */
|
|
1268
|
-
error ? /* @__PURE__ */
|
|
1269
|
-
/* @__PURE__ */
|
|
1437
|
+
/* @__PURE__ */ jsxs6(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
|
|
1438
|
+
error ? /* @__PURE__ */ jsx15("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1439
|
+
/* @__PURE__ */ jsx15("div", { className: "w-full", children: /* @__PURE__ */ jsx15(
|
|
1270
1440
|
Composer,
|
|
1271
1441
|
{
|
|
1272
1442
|
onSend: send,
|
|
1273
1443
|
onStop: stop,
|
|
1274
1444
|
busy,
|
|
1445
|
+
focusHandleRef: composerFocus,
|
|
1275
1446
|
disabled: loading || restoring || Boolean(configError),
|
|
1276
1447
|
...placeholder ? { placeholder } : {},
|
|
1277
1448
|
...autoFocus ? { autoFocus } : {},
|
|
@@ -1287,7 +1458,7 @@ function ChatPanel({
|
|
|
1287
1458
|
}
|
|
1288
1459
|
|
|
1289
1460
|
// src/components/chat/FrankieWidget.tsx
|
|
1290
|
-
import { jsx as
|
|
1461
|
+
import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1291
1462
|
function FrankieWidget({
|
|
1292
1463
|
position = "bottom-right",
|
|
1293
1464
|
offset = 24,
|
|
@@ -1300,13 +1471,15 @@ function FrankieWidget({
|
|
|
1300
1471
|
}) {
|
|
1301
1472
|
const { config } = useFrankie();
|
|
1302
1473
|
const theme = useFrankieTheme();
|
|
1303
|
-
const
|
|
1474
|
+
const rootRef = useRef5(null);
|
|
1475
|
+
const stylesReady = useStylesReady(rootRef);
|
|
1476
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState7(defaultOpen);
|
|
1304
1477
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1305
1478
|
const setOpen = (next) => {
|
|
1306
1479
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
1307
1480
|
onOpenChange?.(next);
|
|
1308
1481
|
};
|
|
1309
|
-
|
|
1482
|
+
useEffect6(() => {
|
|
1310
1483
|
if (!open) return;
|
|
1311
1484
|
const onKeyDown = (event) => {
|
|
1312
1485
|
if (event.key === "Escape") setOpen(false);
|
|
@@ -1317,15 +1490,17 @@ function FrankieWidget({
|
|
|
1317
1490
|
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
1318
1491
|
const mode = resolveMode(theme?.mode);
|
|
1319
1492
|
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
1320
|
-
return /* @__PURE__ */
|
|
1493
|
+
return /* @__PURE__ */ jsx16(
|
|
1321
1494
|
"div",
|
|
1322
1495
|
{
|
|
1496
|
+
ref: rootRef,
|
|
1323
1497
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1324
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1498
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
|
|
1325
1499
|
"data-frankie-mode": mode,
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
/* @__PURE__ */
|
|
1500
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1501
|
+
children: /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1502
|
+
open ? /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1503
|
+
/* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx16(
|
|
1329
1504
|
Button,
|
|
1330
1505
|
{
|
|
1331
1506
|
size: "icon",
|
|
@@ -1334,7 +1509,7 @@ function FrankieWidget({
|
|
|
1334
1509
|
"aria-expanded": open,
|
|
1335
1510
|
"aria-label": open ? "Close the assistant" : label,
|
|
1336
1511
|
title: label,
|
|
1337
|
-
children: open ? /* @__PURE__ */
|
|
1512
|
+
children: open ? /* @__PURE__ */ jsx16(X4, { className: "size-5" }) : /* @__PURE__ */ jsx16(MessageCircle, { className: "size-5" })
|
|
1338
1513
|
}
|
|
1339
1514
|
) })
|
|
1340
1515
|
] })
|
|
@@ -1343,39 +1518,44 @@ function FrankieWidget({
|
|
|
1343
1518
|
}
|
|
1344
1519
|
|
|
1345
1520
|
// src/components/chat/FrankieInline.tsx
|
|
1346
|
-
import {
|
|
1521
|
+
import { useRef as useRef6 } from "react";
|
|
1522
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1347
1523
|
function FrankieInline({ className, style, ...panelProps }) {
|
|
1348
1524
|
const theme = useFrankieTheme();
|
|
1349
1525
|
const mode = resolveMode(theme?.mode);
|
|
1350
|
-
|
|
1526
|
+
const rootRef = useRef6(null);
|
|
1527
|
+
const stylesReady = useStylesReady(rootRef);
|
|
1528
|
+
return /* @__PURE__ */ jsx17(
|
|
1351
1529
|
"div",
|
|
1352
1530
|
{
|
|
1531
|
+
ref: rootRef,
|
|
1353
1532
|
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1354
|
-
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1533
|
+
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
|
|
1355
1534
|
"data-frankie-mode": mode,
|
|
1356
|
-
|
|
1535
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1536
|
+
children: /* @__PURE__ */ jsx17(ChatPanel, { ...panelProps, fill: true })
|
|
1357
1537
|
}
|
|
1358
1538
|
);
|
|
1359
1539
|
}
|
|
1360
1540
|
|
|
1361
1541
|
// src/components/chat/FrankieChatPage.tsx
|
|
1362
|
-
import { useCallback as useCallback3, useEffect as
|
|
1542
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
|
|
1363
1543
|
import { ChevronsLeft, ChevronsRight, History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
|
|
1364
|
-
import { Fragment as Fragment3, jsx as
|
|
1544
|
+
import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1365
1545
|
function PageEmptyState() {
|
|
1366
1546
|
const { config } = useFrankie();
|
|
1367
1547
|
const { send, busy, restoring } = useFrankieChat();
|
|
1368
1548
|
const name = config?.branding.name ?? "Frankie";
|
|
1369
|
-
return /* @__PURE__ */
|
|
1370
|
-
/* @__PURE__ */
|
|
1371
|
-
/* @__PURE__ */
|
|
1372
|
-
/* @__PURE__ */
|
|
1373
|
-
/* @__PURE__ */
|
|
1374
|
-
/* @__PURE__ */
|
|
1549
|
+
return /* @__PURE__ */ jsxs8("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1550
|
+
/* @__PURE__ */ jsx18("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1551
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1552
|
+
/* @__PURE__ */ jsx18("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1553
|
+
/* @__PURE__ */ jsx18("h1", { children: "How can I help?" }),
|
|
1554
|
+
/* @__PURE__ */ jsx18("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1375
1555
|
] }),
|
|
1376
|
-
/* @__PURE__ */
|
|
1377
|
-
/* @__PURE__ */
|
|
1378
|
-
/* @__PURE__ */
|
|
1556
|
+
/* @__PURE__ */ jsx18("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1557
|
+
/* @__PURE__ */ jsx18("strong", { children: prompt }),
|
|
1558
|
+
/* @__PURE__ */ jsx18("b", { "aria-hidden": true, children: "\u2197" })
|
|
1379
1559
|
] }, prompt)) })
|
|
1380
1560
|
] });
|
|
1381
1561
|
}
|
|
@@ -1383,21 +1563,21 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1383
1563
|
const { client, config } = useFrankie();
|
|
1384
1564
|
const { busy, messages } = useFrankieChat();
|
|
1385
1565
|
const theme = useFrankieTheme();
|
|
1386
|
-
const [conversations, setConversations] =
|
|
1387
|
-
const [historyError, setHistoryError] =
|
|
1388
|
-
const [historyLoading, setHistoryLoading] =
|
|
1389
|
-
const [drawerOpen, setDrawerOpen] =
|
|
1390
|
-
const [collapsed, setCollapsed] =
|
|
1566
|
+
const [conversations, setConversations] = useState8([]);
|
|
1567
|
+
const [historyError, setHistoryError] = useState8("");
|
|
1568
|
+
const [historyLoading, setHistoryLoading] = useState8(false);
|
|
1569
|
+
const [drawerOpen, setDrawerOpen] = useState8(false);
|
|
1570
|
+
const [collapsed, setCollapsed] = useState8(() => {
|
|
1391
1571
|
try {
|
|
1392
1572
|
return window.localStorage.getItem("frankie.history-collapsed") === "1";
|
|
1393
1573
|
} catch {
|
|
1394
1574
|
return false;
|
|
1395
1575
|
}
|
|
1396
1576
|
});
|
|
1397
|
-
const [query, setQuery] =
|
|
1398
|
-
const dialog =
|
|
1399
|
-
const menu =
|
|
1400
|
-
const requestVersion =
|
|
1577
|
+
const [query, setQuery] = useState8("");
|
|
1578
|
+
const dialog = useRef7(null);
|
|
1579
|
+
const menu = useRef7(null);
|
|
1580
|
+
const requestVersion = useRef7(0);
|
|
1401
1581
|
const refresh = useCallback3(async () => {
|
|
1402
1582
|
if (!showHistory || !config || config.allowAnonymous) {
|
|
1403
1583
|
setHistoryError("");
|
|
@@ -1419,19 +1599,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1419
1599
|
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1420
1600
|
}
|
|
1421
1601
|
}, [client, showHistory, config?.allowAnonymous]);
|
|
1422
|
-
|
|
1602
|
+
useEffect7(() => {
|
|
1423
1603
|
if (!busy) void refresh();
|
|
1424
1604
|
return () => {
|
|
1425
1605
|
requestVersion.current++;
|
|
1426
1606
|
};
|
|
1427
1607
|
}, [refresh, busy]);
|
|
1428
|
-
|
|
1608
|
+
useEffect7(() => {
|
|
1429
1609
|
try {
|
|
1430
1610
|
window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
|
|
1431
1611
|
} catch {
|
|
1432
1612
|
}
|
|
1433
1613
|
}, [collapsed]);
|
|
1434
|
-
|
|
1614
|
+
useEffect7(() => {
|
|
1435
1615
|
const node = dialog.current;
|
|
1436
1616
|
if (drawerOpen) {
|
|
1437
1617
|
node?.showModal?.();
|
|
@@ -1449,79 +1629,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1449
1629
|
close();
|
|
1450
1630
|
}
|
|
1451
1631
|
};
|
|
1452
|
-
const history = /* @__PURE__ */
|
|
1453
|
-
/* @__PURE__ */
|
|
1454
|
-
/* @__PURE__ */
|
|
1455
|
-
/* @__PURE__ */
|
|
1632
|
+
const history = /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1633
|
+
/* @__PURE__ */ jsxs8("div", { className: "frankie-history-heading", children: [
|
|
1634
|
+
/* @__PURE__ */ jsx18("strong", { children: "Conversations" }),
|
|
1635
|
+
/* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx18(X5, { size: 18 }) })
|
|
1456
1636
|
] }),
|
|
1457
|
-
/* @__PURE__ */
|
|
1637
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1458
1638
|
onNew();
|
|
1459
1639
|
close();
|
|
1460
1640
|
}, children: [
|
|
1461
|
-
/* @__PURE__ */
|
|
1641
|
+
/* @__PURE__ */ jsx18(Plus2, { size: 17 }),
|
|
1462
1642
|
"New chat"
|
|
1463
1643
|
] }),
|
|
1464
|
-
/* @__PURE__ */
|
|
1465
|
-
/* @__PURE__ */
|
|
1466
|
-
/* @__PURE__ */
|
|
1644
|
+
/* @__PURE__ */ jsxs8("label", { className: "frankie-history-search", children: [
|
|
1645
|
+
/* @__PURE__ */ jsx18("span", { children: "Search conversations" }),
|
|
1646
|
+
/* @__PURE__ */ jsx18("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1467
1647
|
] }),
|
|
1468
|
-
/* @__PURE__ */
|
|
1469
|
-
/* @__PURE__ */
|
|
1470
|
-
/* @__PURE__ */
|
|
1648
|
+
/* @__PURE__ */ jsx18("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1649
|
+
/* @__PURE__ */ jsx18("span", { children: item.title }),
|
|
1650
|
+
/* @__PURE__ */ jsx18("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1471
1651
|
] }, item.id)) }),
|
|
1472
|
-
historyLoading ? /* @__PURE__ */
|
|
1652
|
+
historyLoading ? /* @__PURE__ */ jsx18("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs8("p", { role: "status", children: [
|
|
1473
1653
|
historyError,
|
|
1474
|
-
/* @__PURE__ */
|
|
1475
|
-
] }) : !conversations.length ? /* @__PURE__ */
|
|
1476
|
-
navigation ? /* @__PURE__ */
|
|
1654
|
+
/* @__PURE__ */ jsx18("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1655
|
+
] }) : !conversations.length ? /* @__PURE__ */ jsx18("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1656
|
+
navigation ? /* @__PURE__ */ jsx18("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1477
1657
|
] });
|
|
1478
|
-
return /* @__PURE__ */
|
|
1479
|
-
showHistory ? /* @__PURE__ */
|
|
1480
|
-
/* @__PURE__ */
|
|
1658
|
+
return /* @__PURE__ */ jsxs8("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1659
|
+
showHistory ? /* @__PURE__ */ jsxs8("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
|
|
1660
|
+
/* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx18(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx18(ChevronsLeft, { size: 16 }) }),
|
|
1481
1661
|
collapsed ? null : history
|
|
1482
1662
|
] }) : null,
|
|
1483
|
-
/* @__PURE__ */
|
|
1484
|
-
/* @__PURE__ */
|
|
1485
|
-
/* @__PURE__ */
|
|
1486
|
-
showHistory ? /* @__PURE__ */
|
|
1487
|
-
/* @__PURE__ */
|
|
1663
|
+
/* @__PURE__ */ jsxs8("div", { className: "frankie-page-main", children: [
|
|
1664
|
+
/* @__PURE__ */ jsxs8("header", { className: "frankie-page-header", children: [
|
|
1665
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1666
|
+
showHistory ? /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx18(Menu, { size: 20 }) }) : null,
|
|
1667
|
+
/* @__PURE__ */ jsx18("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
|
|
1488
1668
|
] }),
|
|
1489
|
-
/* @__PURE__ */
|
|
1490
|
-
/* @__PURE__ */
|
|
1491
|
-
/* @__PURE__ */
|
|
1492
|
-
/* @__PURE__ */
|
|
1669
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1670
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1671
|
+
/* @__PURE__ */ jsx18(Plus2, { size: 16 }),
|
|
1672
|
+
/* @__PURE__ */ jsx18("span", { children: "New chat" })
|
|
1493
1673
|
] }),
|
|
1494
|
-
/* @__PURE__ */
|
|
1495
|
-
/* @__PURE__ */
|
|
1496
|
-
/* @__PURE__ */
|
|
1497
|
-
/* @__PURE__ */
|
|
1498
|
-
/* @__PURE__ */
|
|
1499
|
-
/* @__PURE__ */
|
|
1500
|
-
/* @__PURE__ */
|
|
1674
|
+
/* @__PURE__ */ jsxs8("details", { className: "frankie-chat-settings", children: [
|
|
1675
|
+
/* @__PURE__ */ jsx18("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx18(Settings, { size: 19 }) }),
|
|
1676
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1677
|
+
/* @__PURE__ */ jsx18("strong", { children: "Chat settings" }),
|
|
1678
|
+
/* @__PURE__ */ jsx18("p", { children: "History syncs automatically for signed-in users." }),
|
|
1679
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1680
|
+
/* @__PURE__ */ jsx18(History, { size: 16 }),
|
|
1501
1681
|
"Refresh history"
|
|
1502
1682
|
] }),
|
|
1503
|
-
/* @__PURE__ */
|
|
1683
|
+
/* @__PURE__ */ jsx18("small", { children: "Skills are checked at the start of each message." })
|
|
1504
1684
|
] })
|
|
1505
1685
|
] })
|
|
1506
1686
|
] })
|
|
1507
1687
|
] }),
|
|
1508
|
-
/* @__PURE__ */
|
|
1688
|
+
/* @__PURE__ */ jsx18("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx18(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx18(PageEmptyState, {}) }) })
|
|
1509
1689
|
] }),
|
|
1510
|
-
showHistory ? /* @__PURE__ */
|
|
1690
|
+
showHistory ? /* @__PURE__ */ jsx18("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1511
1691
|
if (event.target === event.currentTarget) close();
|
|
1512
|
-
}, children: /* @__PURE__ */
|
|
1692
|
+
}, children: /* @__PURE__ */ jsx18("aside", { children: history }) }) : null
|
|
1513
1693
|
] });
|
|
1514
1694
|
}
|
|
1515
1695
|
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1516
|
-
const [id, setId] =
|
|
1696
|
+
const [id, setId] = useState8(() => provider.conversationId ?? crypto.randomUUID());
|
|
1517
1697
|
const onNew = () => setId(crypto.randomUUID());
|
|
1518
|
-
|
|
1698
|
+
const pageRef = useRef7(null);
|
|
1699
|
+
const stylesReady = useStylesReady(pageRef);
|
|
1700
|
+
return /* @__PURE__ */ jsx18("div", { ref: pageRef, className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } }, "data-frankie-styles": stylesReady ? "ready" : "pending", children: /* @__PURE__ */ jsx18(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx18(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1519
1701
|
}
|
|
1520
1702
|
|
|
1521
1703
|
// src/components/ui/separator.tsx
|
|
1522
|
-
import { jsx as
|
|
1704
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1523
1705
|
function Separator({ className }) {
|
|
1524
|
-
return /* @__PURE__ */
|
|
1706
|
+
return /* @__PURE__ */ jsx19("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1525
1707
|
}
|
|
1526
1708
|
export {
|
|
1527
1709
|
Avatar,
|
|
@@ -1534,6 +1716,7 @@ export {
|
|
|
1534
1716
|
CardHeader,
|
|
1535
1717
|
CardTitle,
|
|
1536
1718
|
ChatPanel,
|
|
1719
|
+
Choices,
|
|
1537
1720
|
Composer,
|
|
1538
1721
|
FRANKIE_CLOUD_URL,
|
|
1539
1722
|
FrankieApiError,
|
|
@@ -1554,6 +1737,7 @@ export {
|
|
|
1554
1737
|
applyEvent,
|
|
1555
1738
|
buttonVariants,
|
|
1556
1739
|
cn,
|
|
1740
|
+
extractChoices,
|
|
1557
1741
|
newAssistantMessage,
|
|
1558
1742
|
newUserMessage,
|
|
1559
1743
|
readEventStream,
|