@frankieai/react 0.2.7 → 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 +394 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -9
- package/dist/index.d.ts +63 -9
- package/dist/index.js +377 -175
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/components/chat/AgentContext.tsx +2 -1
- 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 +65 -3
- 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/src/widget.test.tsx +17 -0
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
CardHeader: () => CardHeader,
|
|
32
32
|
CardTitle: () => CardTitle,
|
|
33
33
|
ChatPanel: () => ChatPanel,
|
|
34
|
+
Choices: () => Choices,
|
|
34
35
|
Composer: () => Composer,
|
|
35
36
|
FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
|
|
36
37
|
FrankieApiError: () => FrankieApiError,
|
|
@@ -51,6 +52,7 @@ __export(index_exports, {
|
|
|
51
52
|
applyEvent: () => applyEvent,
|
|
52
53
|
buttonVariants: () => buttonVariants,
|
|
53
54
|
cn: () => cn,
|
|
55
|
+
extractChoices: () => extractChoices,
|
|
54
56
|
newAssistantMessage: () => newAssistantMessage,
|
|
55
57
|
newUserMessage: () => newUserMessage,
|
|
56
58
|
readEventStream: () => readEventStream,
|
|
@@ -376,6 +378,7 @@ function useFrankieChatState() {
|
|
|
376
378
|
const [error, setError] = (0, import_react.useState)(void 0);
|
|
377
379
|
const [restored, setRestored] = (0, import_react.useState)(false);
|
|
378
380
|
const [loadedSkills, setLoadedSkills] = (0, import_react.useState)(null);
|
|
381
|
+
const [activeApis, setActiveApis] = (0, import_react.useState)(null);
|
|
379
382
|
const abortRef = (0, import_react.useRef)(null);
|
|
380
383
|
const mounted = (0, import_react.useRef)(true);
|
|
381
384
|
const messagesRef = (0, import_react.useRef)([]);
|
|
@@ -447,6 +450,7 @@ function useFrankieChatState() {
|
|
|
447
450
|
const assistantId = nextId("a");
|
|
448
451
|
setError(void 0);
|
|
449
452
|
setLoadedSkills(null);
|
|
453
|
+
setActiveApis(null);
|
|
450
454
|
setBusy(true);
|
|
451
455
|
const next = [
|
|
452
456
|
...messagesRef.current,
|
|
@@ -461,6 +465,41 @@ function useFrankieChatState() {
|
|
|
461
465
|
setMessages(next);
|
|
462
466
|
const abort = new AbortController();
|
|
463
467
|
abortRef.current = abort;
|
|
468
|
+
let bufferedText = "";
|
|
469
|
+
let pendingThinking = "";
|
|
470
|
+
let frame = null;
|
|
471
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
|
|
472
|
+
const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
|
|
473
|
+
const revealTick = () => {
|
|
474
|
+
frame = null;
|
|
475
|
+
let addText = "";
|
|
476
|
+
if (bufferedText) {
|
|
477
|
+
const step = Math.max(3, Math.ceil(bufferedText.length / 6));
|
|
478
|
+
addText = bufferedText.slice(0, step);
|
|
479
|
+
bufferedText = bufferedText.slice(step);
|
|
480
|
+
}
|
|
481
|
+
const addThinking = pendingThinking;
|
|
482
|
+
pendingThinking = "";
|
|
483
|
+
if (addText || addThinking) {
|
|
484
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
485
|
+
}
|
|
486
|
+
if (bufferedText) scheduleReveal();
|
|
487
|
+
};
|
|
488
|
+
const scheduleReveal = () => {
|
|
489
|
+
if (frame === null) frame = raf(revealTick);
|
|
490
|
+
};
|
|
491
|
+
const flushDeltas = () => {
|
|
492
|
+
if (frame !== null) {
|
|
493
|
+
cancelRaf(frame);
|
|
494
|
+
frame = null;
|
|
495
|
+
}
|
|
496
|
+
if (!bufferedText && !pendingThinking) return;
|
|
497
|
+
const addText = bufferedText;
|
|
498
|
+
const addThinking = pendingThinking;
|
|
499
|
+
bufferedText = "";
|
|
500
|
+
pendingThinking = "";
|
|
501
|
+
patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
|
|
502
|
+
};
|
|
464
503
|
try {
|
|
465
504
|
let receivedText = false;
|
|
466
505
|
let receivedToolActivity = false;
|
|
@@ -477,12 +516,28 @@ function useFrankieChatState() {
|
|
|
477
516
|
setLoadedSkills(event.skills);
|
|
478
517
|
return;
|
|
479
518
|
}
|
|
519
|
+
if (event.type === "tools_active") {
|
|
520
|
+
setActiveApis(event.apis);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
480
523
|
if (event.type === "ping" || event.type === "session") return;
|
|
481
|
-
if (event.type === "text_delta")
|
|
524
|
+
if (event.type === "text_delta") {
|
|
525
|
+
receivedText = true;
|
|
526
|
+
bufferedText += event.text;
|
|
527
|
+
scheduleReveal();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (event.type === "thinking_delta") {
|
|
531
|
+
pendingThinking += event.text;
|
|
532
|
+
scheduleReveal();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
482
535
|
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
536
|
+
flushDeltas();
|
|
483
537
|
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
484
538
|
}
|
|
485
539
|
);
|
|
540
|
+
flushDeltas();
|
|
486
541
|
if (!receivedText) {
|
|
487
542
|
patchAssistant(assistantId, (message) => ({
|
|
488
543
|
...message,
|
|
@@ -491,6 +546,7 @@ function useFrankieChatState() {
|
|
|
491
546
|
}));
|
|
492
547
|
}
|
|
493
548
|
} catch (caught) {
|
|
549
|
+
flushDeltas();
|
|
494
550
|
if (abort.signal.aborted) {
|
|
495
551
|
patchAssistant(assistantId, (message) => ({
|
|
496
552
|
...message,
|
|
@@ -503,6 +559,7 @@ function useFrankieChatState() {
|
|
|
503
559
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
504
560
|
}
|
|
505
561
|
} finally {
|
|
562
|
+
flushDeltas();
|
|
506
563
|
patchAssistant(
|
|
507
564
|
assistantId,
|
|
508
565
|
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
@@ -527,12 +584,13 @@ function useFrankieChatState() {
|
|
|
527
584
|
setBusy(false);
|
|
528
585
|
setRestored(true);
|
|
529
586
|
setLoadedSkills(null);
|
|
587
|
+
setActiveApis(null);
|
|
530
588
|
try {
|
|
531
589
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
532
590
|
} catch {
|
|
533
591
|
}
|
|
534
592
|
}, [client]);
|
|
535
|
-
return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
|
|
593
|
+
return { messages, busy, loadedSkills, activeApis, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
|
|
536
594
|
}
|
|
537
595
|
function conversationTitle(messages) {
|
|
538
596
|
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
@@ -604,8 +662,45 @@ function useFrankieTheme() {
|
|
|
604
662
|
}
|
|
605
663
|
|
|
606
664
|
// src/components/chat/FrankieWidget.tsx
|
|
607
|
-
var
|
|
608
|
-
var
|
|
665
|
+
var import_lucide_react7 = require("lucide-react");
|
|
666
|
+
var import_react10 = require("react");
|
|
667
|
+
|
|
668
|
+
// src/lib/styles-ready.ts
|
|
669
|
+
var import_react3 = require("react");
|
|
670
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
|
|
671
|
+
var SENTINEL = "--frankie-styles-ready";
|
|
672
|
+
function useStylesReady(ref, timeoutMs = 600) {
|
|
673
|
+
const [ready, setReady] = (0, import_react3.useState)(false);
|
|
674
|
+
useIsomorphicLayoutEffect(() => {
|
|
675
|
+
if (ready) return;
|
|
676
|
+
const node = ref.current;
|
|
677
|
+
if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
|
|
678
|
+
setReady(true);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
|
|
682
|
+
if (applied()) {
|
|
683
|
+
setReady(true);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const deadline = Date.now() + timeoutMs;
|
|
687
|
+
let frame = 0;
|
|
688
|
+
const poll = () => {
|
|
689
|
+
if (applied() || Date.now() >= deadline) {
|
|
690
|
+
setReady(true);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
frame = window.requestAnimationFrame(poll);
|
|
694
|
+
};
|
|
695
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
696
|
+
setReady(true);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
frame = window.requestAnimationFrame(poll);
|
|
700
|
+
return () => window.cancelAnimationFrame(frame);
|
|
701
|
+
}, [ref, ready, timeoutMs]);
|
|
702
|
+
return ready;
|
|
703
|
+
}
|
|
609
704
|
|
|
610
705
|
// src/lib/theme.ts
|
|
611
706
|
function themeToCssVars(theme) {
|
|
@@ -665,8 +760,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
665
760
|
}
|
|
666
761
|
|
|
667
762
|
// src/components/chat/ChatPanel.tsx
|
|
668
|
-
var
|
|
669
|
-
var
|
|
763
|
+
var import_lucide_react6 = require("lucide-react");
|
|
764
|
+
var import_react9 = require("react");
|
|
670
765
|
|
|
671
766
|
// src/components/ui/avatar.tsx
|
|
672
767
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
@@ -716,9 +811,9 @@ function CardFooter({ className, ...props }) {
|
|
|
716
811
|
}
|
|
717
812
|
|
|
718
813
|
// src/components/ui/scroll-area.tsx
|
|
719
|
-
var
|
|
814
|
+
var import_react4 = require("react");
|
|
720
815
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
721
|
-
var ScrollArea = (0,
|
|
816
|
+
var ScrollArea = (0, import_react4.forwardRef)(
|
|
722
817
|
function ScrollArea2({ className, ...props }, ref) {
|
|
723
818
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref, className: cn("frankie-scroll overflow-y-auto overscroll-contain", className), ...props });
|
|
724
819
|
}
|
|
@@ -729,7 +824,7 @@ var import_lucide_react = require("lucide-react");
|
|
|
729
824
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
730
825
|
function AgentContext() {
|
|
731
826
|
const { loading, configError } = useFrankie();
|
|
732
|
-
const { loadedSkills, busy, restoring, error } = useFrankieChat();
|
|
827
|
+
const { loadedSkills, activeApis, busy, restoring, error } = useFrankieChat();
|
|
733
828
|
const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
|
|
734
829
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-agent-context-wrap", children: [
|
|
735
830
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { className: "frankie-agent-context", children: [
|
|
@@ -752,7 +847,18 @@ function AgentContext() {
|
|
|
752
847
|
skill.revision
|
|
753
848
|
] })
|
|
754
849
|
] }, skill.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
|
|
755
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
|
|
850
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." }),
|
|
851
|
+
activeApis?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "frankie-active-apis", children: activeApis.map((api) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
|
|
852
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: api.name }),
|
|
853
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("small", { children: [
|
|
854
|
+
"spec ",
|
|
855
|
+
api.specHash,
|
|
856
|
+
" \xB7 ",
|
|
857
|
+
api.operationCount,
|
|
858
|
+
" ",
|
|
859
|
+
api.operationCount === 1 ? "operation" : "operations"
|
|
860
|
+
] })
|
|
861
|
+
] }, api.name)) }) : null
|
|
756
862
|
] })
|
|
757
863
|
] }),
|
|
758
864
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
|
|
@@ -761,12 +867,12 @@ function AgentContext() {
|
|
|
761
867
|
|
|
762
868
|
// src/components/chat/Composer.tsx
|
|
763
869
|
var import_lucide_react2 = require("lucide-react");
|
|
764
|
-
var
|
|
870
|
+
var import_react6 = require("react");
|
|
765
871
|
|
|
766
872
|
// src/components/ui/textarea.tsx
|
|
767
|
-
var
|
|
873
|
+
var import_react5 = require("react");
|
|
768
874
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
769
|
-
var Textarea = (0,
|
|
875
|
+
var Textarea = (0, import_react5.forwardRef)(
|
|
770
876
|
function Textarea2({ className, ...props }, ref) {
|
|
771
877
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
772
878
|
"textarea",
|
|
@@ -785,21 +891,28 @@ var Textarea = (0, import_react4.forwardRef)(
|
|
|
785
891
|
// src/components/chat/Composer.tsx
|
|
786
892
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
787
893
|
var MAX_ROWS_PX = 140;
|
|
788
|
-
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
|
|
789
|
-
const [value, setValue] = (0,
|
|
790
|
-
const [files, setFiles] = (0,
|
|
791
|
-
const [fileError, setFileError] = (0,
|
|
792
|
-
const textareaRef = (0,
|
|
793
|
-
const fileInputRef = (0,
|
|
894
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
|
|
895
|
+
const [value, setValue] = (0, import_react6.useState)("");
|
|
896
|
+
const [files, setFiles] = (0, import_react6.useState)([]);
|
|
897
|
+
const [fileError, setFileError] = (0, import_react6.useState)(void 0);
|
|
898
|
+
const textareaRef = (0, import_react6.useRef)(null);
|
|
899
|
+
const fileInputRef = (0, import_react6.useRef)(null);
|
|
900
|
+
(0, import_react6.useEffect)(() => {
|
|
901
|
+
if (!focusHandleRef) return;
|
|
902
|
+
focusHandleRef.current = () => textareaRef.current?.focus();
|
|
903
|
+
return () => {
|
|
904
|
+
focusHandleRef.current = null;
|
|
905
|
+
};
|
|
906
|
+
}, [focusHandleRef]);
|
|
794
907
|
const canAttach = Boolean(attachments?.enabled);
|
|
795
908
|
const models = modelPicker?.models ?? [];
|
|
796
|
-
(0,
|
|
909
|
+
(0, import_react6.useEffect)(() => {
|
|
797
910
|
const node = textareaRef.current;
|
|
798
911
|
if (!node) return;
|
|
799
912
|
node.style.height = "auto";
|
|
800
913
|
node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
|
|
801
914
|
}, [value]);
|
|
802
|
-
const addFiles = (0,
|
|
915
|
+
const addFiles = (0, import_react6.useCallback)(
|
|
803
916
|
async (list) => {
|
|
804
917
|
if (!list || !attachments) return;
|
|
805
918
|
setFileError(void 0);
|
|
@@ -832,7 +945,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
|
|
|
832
945
|
[attachments, files.length]
|
|
833
946
|
);
|
|
834
947
|
const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
|
|
835
|
-
const submit = (0,
|
|
948
|
+
const submit = (0, import_react6.useCallback)(() => {
|
|
836
949
|
const text = value.trim();
|
|
837
950
|
if (!text && files.length === 0 || busy || disabled) return;
|
|
838
951
|
setValue("");
|
|
@@ -962,15 +1075,77 @@ function readAsBase64(file) {
|
|
|
962
1075
|
}
|
|
963
1076
|
|
|
964
1077
|
// src/components/chat/MessageBubble.tsx
|
|
965
|
-
var
|
|
966
|
-
var
|
|
1078
|
+
var import_lucide_react5 = require("lucide-react");
|
|
1079
|
+
var import_react8 = require("react");
|
|
967
1080
|
|
|
968
|
-
// src/
|
|
969
|
-
var
|
|
1081
|
+
// src/lib/choices.ts
|
|
1082
|
+
var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
|
|
1083
|
+
var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
|
|
1084
|
+
function extractChoices(text) {
|
|
1085
|
+
const closed = CLOSED.exec(text);
|
|
1086
|
+
if (closed) {
|
|
1087
|
+
const options = parseOptionLines(closed[1] ?? "");
|
|
1088
|
+
const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1089
|
+
return { body, options: options.length ? options : null };
|
|
1090
|
+
}
|
|
1091
|
+
const open = OPEN.exec(text);
|
|
1092
|
+
if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
|
|
1093
|
+
return { body: text, options: null };
|
|
1094
|
+
}
|
|
1095
|
+
function parseOptionLines(block) {
|
|
1096
|
+
return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/components/chat/Choices.tsx
|
|
1100
|
+
var import_lucide_react3 = require("lucide-react");
|
|
970
1101
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
971
|
-
function
|
|
972
|
-
|
|
1102
|
+
function Choices({
|
|
1103
|
+
options,
|
|
1104
|
+
onPick,
|
|
1105
|
+
onSomethingElse,
|
|
1106
|
+
disabled
|
|
1107
|
+
}) {
|
|
1108
|
+
if (options.length === 0) return null;
|
|
1109
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
|
|
1110
|
+
options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
1111
|
+
"button",
|
|
1112
|
+
{
|
|
1113
|
+
type: "button",
|
|
1114
|
+
className: "frankie-choice",
|
|
1115
|
+
disabled,
|
|
1116
|
+
onClick: () => onPick(option),
|
|
1117
|
+
children: [
|
|
1118
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
|
|
1119
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: option }),
|
|
1120
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
|
|
1121
|
+
]
|
|
1122
|
+
},
|
|
1123
|
+
`${index}-${option}`
|
|
1124
|
+
)),
|
|
1125
|
+
onSomethingElse ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
1126
|
+
"button",
|
|
1127
|
+
{
|
|
1128
|
+
type: "button",
|
|
1129
|
+
className: "frankie-choice frankie-choice-other",
|
|
1130
|
+
disabled,
|
|
1131
|
+
onClick: onSomethingElse,
|
|
1132
|
+
children: [
|
|
1133
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.PencilLine, { className: "size-3.5" }) }),
|
|
1134
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
|
|
1135
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
) : null
|
|
1139
|
+
] });
|
|
973
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
// src/components/chat/Markdown.tsx
|
|
1143
|
+
var import_react7 = require("react");
|
|
1144
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1145
|
+
var Markdown = (0, import_react7.memo)(function Markdown2({ text, className }) {
|
|
1146
|
+
const blocks = (0, import_react7.useMemo)(() => renderBlocks(text), [text]);
|
|
1147
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn("frankie-prose", className), children: blocks });
|
|
1148
|
+
});
|
|
974
1149
|
function renderBlocks(text) {
|
|
975
1150
|
const lines = text.split("\n");
|
|
976
1151
|
const blocks = [];
|
|
@@ -979,13 +1154,13 @@ function renderBlocks(text) {
|
|
|
979
1154
|
let code = null;
|
|
980
1155
|
const flushParagraph = () => {
|
|
981
1156
|
if (paragraph.length === 0) return;
|
|
982
|
-
blocks.push(/* @__PURE__ */ (0,
|
|
1157
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
983
1158
|
paragraph = [];
|
|
984
1159
|
};
|
|
985
1160
|
const flushList = () => {
|
|
986
1161
|
if (!list) return;
|
|
987
|
-
const items = list.items.map((item, index) => /* @__PURE__ */ (0,
|
|
988
|
-
blocks.push(list.ordered ? /* @__PURE__ */ (0,
|
|
1162
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderInline(item) }, index));
|
|
1163
|
+
blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { children: items }, `l${blocks.length}`));
|
|
989
1164
|
list = null;
|
|
990
1165
|
};
|
|
991
1166
|
for (const line of lines) {
|
|
@@ -993,7 +1168,7 @@ function renderBlocks(text) {
|
|
|
993
1168
|
if (fence) {
|
|
994
1169
|
if (code) {
|
|
995
1170
|
blocks.push(
|
|
996
|
-
/* @__PURE__ */ (0,
|
|
1171
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
997
1172
|
);
|
|
998
1173
|
code = null;
|
|
999
1174
|
} else {
|
|
@@ -1024,11 +1199,13 @@ function renderBlocks(text) {
|
|
|
1024
1199
|
flushList();
|
|
1025
1200
|
continue;
|
|
1026
1201
|
}
|
|
1027
|
-
const heading =
|
|
1202
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
1028
1203
|
if (heading) {
|
|
1029
1204
|
flushParagraph();
|
|
1030
1205
|
flushList();
|
|
1031
|
-
|
|
1206
|
+
const level = (heading[1] ?? "#").length;
|
|
1207
|
+
const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
|
|
1208
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
|
|
1032
1209
|
continue;
|
|
1033
1210
|
}
|
|
1034
1211
|
flushList();
|
|
@@ -1036,7 +1213,7 @@ function renderBlocks(text) {
|
|
|
1036
1213
|
}
|
|
1037
1214
|
if (code) {
|
|
1038
1215
|
blocks.push(
|
|
1039
|
-
/* @__PURE__ */ (0,
|
|
1216
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
1040
1217
|
);
|
|
1041
1218
|
}
|
|
1042
1219
|
flushParagraph();
|
|
@@ -1051,28 +1228,28 @@ function renderInline(text) {
|
|
|
1051
1228
|
const token = match[0];
|
|
1052
1229
|
const start = match.index;
|
|
1053
1230
|
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
1054
|
-
nodes.push(/* @__PURE__ */ (0,
|
|
1231
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react7.Fragment, { children: renderToken(token) }, start));
|
|
1055
1232
|
lastIndex = start + token.length;
|
|
1056
1233
|
}
|
|
1057
1234
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
1058
1235
|
return nodes;
|
|
1059
1236
|
}
|
|
1060
1237
|
function renderToken(token) {
|
|
1061
|
-
if (token.startsWith("**")) return /* @__PURE__ */ (0,
|
|
1062
|
-
if (token.startsWith("`")) return /* @__PURE__ */ (0,
|
|
1238
|
+
if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: token.slice(2, -2) });
|
|
1239
|
+
if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: token.slice(1, -1) });
|
|
1063
1240
|
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
1064
1241
|
const href = link ? link[2] ?? "" : token;
|
|
1065
1242
|
const label = link ? link[1] ?? "" : token;
|
|
1066
1243
|
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
1067
|
-
return /* @__PURE__ */ (0,
|
|
1244
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
1068
1245
|
}
|
|
1069
1246
|
|
|
1070
1247
|
// src/components/chat/ToolActivityList.tsx
|
|
1071
|
-
var
|
|
1248
|
+
var import_lucide_react4 = require("lucide-react");
|
|
1072
1249
|
|
|
1073
1250
|
// src/components/ui/badge.tsx
|
|
1074
1251
|
var import_class_variance_authority2 = require("class-variance-authority");
|
|
1075
|
-
var
|
|
1252
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1076
1253
|
var badgeVariants = (0, import_class_variance_authority2.cva)(
|
|
1077
1254
|
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
1078
1255
|
{
|
|
@@ -1092,44 +1269,44 @@ function Badge({
|
|
|
1092
1269
|
variant,
|
|
1093
1270
|
...props
|
|
1094
1271
|
}) {
|
|
1095
|
-
return /* @__PURE__ */ (0,
|
|
1272
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
1096
1273
|
}
|
|
1097
1274
|
|
|
1098
1275
|
// src/components/chat/ToolActivityList.tsx
|
|
1099
|
-
var
|
|
1276
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1100
1277
|
function ToolActivityList({ tools, renderTool }) {
|
|
1101
1278
|
if (tools.length === 0) return null;
|
|
1102
1279
|
const running = tools.some((tool) => tool.status === "running");
|
|
1103
|
-
return /* @__PURE__ */ (0,
|
|
1104
|
-
/* @__PURE__ */ (0,
|
|
1105
|
-
/* @__PURE__ */ (0,
|
|
1106
|
-
/* @__PURE__ */ (0,
|
|
1107
|
-
/* @__PURE__ */ (0,
|
|
1280
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.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: [
|
|
1282
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
1283
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: running ? "Working" : "Activity" }),
|
|
1284
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[11px]", children: [
|
|
1108
1285
|
"\xB7 ",
|
|
1109
1286
|
tools.length,
|
|
1110
1287
|
" tool call",
|
|
1111
1288
|
tools.length === 1 ? "" : "s"
|
|
1112
1289
|
] })
|
|
1113
1290
|
] }),
|
|
1114
|
-
/* @__PURE__ */ (0,
|
|
1291
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
|
|
1115
1292
|
] });
|
|
1116
1293
|
}
|
|
1117
1294
|
function ToolActivityRow({ tool }) {
|
|
1118
|
-
return /* @__PURE__ */ (0,
|
|
1119
|
-
/* @__PURE__ */ (0,
|
|
1120
|
-
/* @__PURE__ */ (0,
|
|
1295
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
1296
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolIcon, { status: tool.status }),
|
|
1297
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: tool.label })
|
|
1121
1298
|
] });
|
|
1122
1299
|
}
|
|
1123
1300
|
function ToolIcon({ status }) {
|
|
1124
1301
|
switch (status) {
|
|
1125
1302
|
case "running":
|
|
1126
|
-
return /* @__PURE__ */ (0,
|
|
1303
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
1127
1304
|
case "done":
|
|
1128
|
-
return /* @__PURE__ */ (0,
|
|
1305
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3", "aria-hidden": "true" });
|
|
1129
1306
|
case "denied":
|
|
1130
|
-
return /* @__PURE__ */ (0,
|
|
1307
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
1131
1308
|
default:
|
|
1132
|
-
return /* @__PURE__ */ (0,
|
|
1309
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.X, { className: "size-3", "aria-hidden": "true" });
|
|
1133
1310
|
}
|
|
1134
1311
|
}
|
|
1135
1312
|
function badgeVariant(status) {
|
|
@@ -1140,47 +1317,54 @@ function badgeVariant(status) {
|
|
|
1140
1317
|
}
|
|
1141
1318
|
|
|
1142
1319
|
// src/components/chat/MessageBubble.tsx
|
|
1143
|
-
var
|
|
1144
|
-
|
|
1320
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1321
|
+
var MessageBubble = (0, import_react8.memo)(function MessageBubble2({
|
|
1145
1322
|
message,
|
|
1146
1323
|
assistantName,
|
|
1147
1324
|
avatarUrl,
|
|
1148
1325
|
showToolActivity,
|
|
1149
|
-
renderTool
|
|
1326
|
+
renderTool,
|
|
1327
|
+
onChoose,
|
|
1328
|
+
onRequestInput,
|
|
1329
|
+
active,
|
|
1330
|
+
busy
|
|
1150
1331
|
}) {
|
|
1151
1332
|
if (message.role === "user") {
|
|
1152
|
-
return /* @__PURE__ */ (0,
|
|
1153
|
-
message.attachments?.length ? /* @__PURE__ */ (0,
|
|
1333
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
|
|
1334
|
+
message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
1154
1335
|
"span",
|
|
1155
1336
|
{
|
|
1156
1337
|
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",
|
|
1157
1338
|
children: [
|
|
1158
|
-
/* @__PURE__ */ (0,
|
|
1159
|
-
/* @__PURE__ */ (0,
|
|
1339
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
|
|
1340
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: file.name })
|
|
1160
1341
|
]
|
|
1161
1342
|
},
|
|
1162
1343
|
`${file.name}-${index}`
|
|
1163
1344
|
)) }) : null,
|
|
1164
|
-
message.text ? /* @__PURE__ */ (0,
|
|
1165
|
-
/* @__PURE__ */ (0,
|
|
1166
|
-
/* @__PURE__ */ (0,
|
|
1345
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1346
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1347
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: message.text })
|
|
1167
1348
|
] }) : null
|
|
1168
1349
|
] });
|
|
1169
1350
|
}
|
|
1170
1351
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1352
|
+
const { body, options } = extractChoices(message.text);
|
|
1353
|
+
const showChoices = Boolean(active && onChoose && options && message.status === "done");
|
|
1354
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
|
|
1355
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1356
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1357
|
+
body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: body }) : null
|
|
1175
1358
|
] }),
|
|
1176
|
-
/* @__PURE__ */ (0,
|
|
1177
|
-
message.status === "error" ? /* @__PURE__ */ (0,
|
|
1178
|
-
|
|
1359
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
|
|
1360
|
+
message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime13.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_runtime13.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }),
|
|
1361
|
+
showChoices ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
|
|
1362
|
+
showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
1179
1363
|
] })
|
|
1180
1364
|
] });
|
|
1181
|
-
}
|
|
1365
|
+
});
|
|
1182
1366
|
function MessageCopyButton({ text }) {
|
|
1183
|
-
const [copied, setCopied] = (0,
|
|
1367
|
+
const [copied, setCopied] = (0, import_react8.useState)(false);
|
|
1184
1368
|
const copy = async () => {
|
|
1185
1369
|
try {
|
|
1186
1370
|
await navigator.clipboard.writeText(text);
|
|
@@ -1189,12 +1373,12 @@ function MessageCopyButton({ text }) {
|
|
|
1189
1373
|
} catch {
|
|
1190
1374
|
}
|
|
1191
1375
|
};
|
|
1192
|
-
return /* @__PURE__ */ (0,
|
|
1376
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.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_runtime13.jsx)(import_lucide_react5.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1193
1377
|
}
|
|
1194
1378
|
function TypingDots() {
|
|
1195
|
-
return /* @__PURE__ */ (0,
|
|
1196
|
-
/* @__PURE__ */ (0,
|
|
1197
|
-
[0, 1, 2].map((index) => /* @__PURE__ */ (0,
|
|
1379
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
|
|
1380
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
|
|
1381
|
+
[0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1198
1382
|
"span",
|
|
1199
1383
|
{
|
|
1200
1384
|
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
@@ -1206,14 +1390,14 @@ function TypingDots() {
|
|
|
1206
1390
|
}
|
|
1207
1391
|
|
|
1208
1392
|
// src/components/chat/SuggestedPrompts.tsx
|
|
1209
|
-
var
|
|
1393
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
1210
1394
|
function SuggestedPrompts({
|
|
1211
1395
|
prompts,
|
|
1212
1396
|
onPick,
|
|
1213
1397
|
disabled
|
|
1214
1398
|
}) {
|
|
1215
1399
|
if (prompts.length === 0) return null;
|
|
1216
|
-
return /* @__PURE__ */ (0,
|
|
1400
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1217
1401
|
Button,
|
|
1218
1402
|
{
|
|
1219
1403
|
type: "button",
|
|
@@ -1228,7 +1412,7 @@ function SuggestedPrompts({
|
|
|
1228
1412
|
}
|
|
1229
1413
|
|
|
1230
1414
|
// src/components/chat/ChatPanel.tsx
|
|
1231
|
-
var
|
|
1415
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1232
1416
|
function ChatPanel({
|
|
1233
1417
|
className,
|
|
1234
1418
|
header,
|
|
@@ -1244,12 +1428,14 @@ function ChatPanel({
|
|
|
1244
1428
|
const { config, configError, loading } = useFrankie();
|
|
1245
1429
|
const theme = useFrankieTheme();
|
|
1246
1430
|
const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
|
|
1247
|
-
const scrollRef = (0,
|
|
1248
|
-
const atBottomRef = (0,
|
|
1431
|
+
const scrollRef = (0, import_react9.useRef)(null);
|
|
1432
|
+
const atBottomRef = (0, import_react9.useRef)(true);
|
|
1249
1433
|
const features = config?.features;
|
|
1250
1434
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
1251
|
-
const [model, setModel] = (0,
|
|
1252
|
-
(0,
|
|
1435
|
+
const [model, setModel] = (0, import_react9.useState)(void 0);
|
|
1436
|
+
const composerFocus = (0, import_react9.useRef)(null);
|
|
1437
|
+
const focusComposer = () => composerFocus.current?.();
|
|
1438
|
+
(0, import_react9.useEffect)(() => {
|
|
1253
1439
|
const node = scrollRef.current;
|
|
1254
1440
|
if (!node) return;
|
|
1255
1441
|
const onScroll = () => {
|
|
@@ -1258,14 +1444,14 @@ function ChatPanel({
|
|
|
1258
1444
|
node.addEventListener("scroll", onScroll, { passive: true });
|
|
1259
1445
|
return () => node.removeEventListener("scroll", onScroll);
|
|
1260
1446
|
}, []);
|
|
1261
|
-
(0,
|
|
1447
|
+
(0, import_react9.useEffect)(() => {
|
|
1262
1448
|
const node = scrollRef.current;
|
|
1263
1449
|
if (!node || !atBottomRef.current) return;
|
|
1264
1450
|
node.scrollTop = node.scrollHeight;
|
|
1265
1451
|
}, [messages]);
|
|
1266
1452
|
const name = config?.branding.name ?? "Assistant";
|
|
1267
1453
|
const mode = resolveMode(theme?.mode);
|
|
1268
|
-
return /* @__PURE__ */ (0,
|
|
1454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
1269
1455
|
Card,
|
|
1270
1456
|
{
|
|
1271
1457
|
className: cn(
|
|
@@ -1278,44 +1464,49 @@ function ChatPanel({
|
|
|
1278
1464
|
style: themeToCssVars(theme),
|
|
1279
1465
|
"data-frankie-mode": mode,
|
|
1280
1466
|
children: [
|
|
1281
|
-
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0,
|
|
1282
|
-
/* @__PURE__ */ (0,
|
|
1283
|
-
/* @__PURE__ */ (0,
|
|
1284
|
-
/* @__PURE__ */ (0,
|
|
1285
|
-
config?.branding.tagline ? /* @__PURE__ */ (0,
|
|
1467
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardHeader, { children: [
|
|
1468
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
1469
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1470
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardTitle, { children: name }),
|
|
1471
|
+
config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardDescription, { children: config.branding.tagline }) : null
|
|
1286
1472
|
] }),
|
|
1287
|
-
/* @__PURE__ */ (0,
|
|
1288
|
-
onClose ? /* @__PURE__ */ (0,
|
|
1473
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.RotateCcw, {}) }),
|
|
1474
|
+
onClose ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, {}) }) : null
|
|
1289
1475
|
] }),
|
|
1290
|
-
/* @__PURE__ */ (0,
|
|
1291
|
-
/* @__PURE__ */ (0,
|
|
1292
|
-
/* @__PURE__ */ (0,
|
|
1293
|
-
/* @__PURE__ */ (0,
|
|
1294
|
-
/* @__PURE__ */ (0,
|
|
1295
|
-
] }) : loading ? /* @__PURE__ */ (0,
|
|
1296
|
-
/* @__PURE__ */ (0,
|
|
1297
|
-
/* @__PURE__ */ (0,
|
|
1298
|
-
] }) : /* @__PURE__ */ (0,
|
|
1299
|
-
(message) => renderMessage ? /* @__PURE__ */ (0,
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AgentContext, {}),
|
|
1477
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1478
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1479
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: configError }),
|
|
1480
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1481
|
+
] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1484
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
|
|
1485
|
+
(message, index) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1300
1486
|
MessageBubble,
|
|
1301
1487
|
{
|
|
1302
1488
|
message,
|
|
1303
1489
|
assistantName: name,
|
|
1304
1490
|
...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
|
|
1305
1491
|
showToolActivity: config?.showToolActivity ?? true,
|
|
1306
|
-
...renderTool ? { renderTool } : {}
|
|
1492
|
+
...renderTool ? { renderTool } : {},
|
|
1493
|
+
onChoose: send,
|
|
1494
|
+
onRequestInput: focusComposer,
|
|
1495
|
+
active: index === messages.length - 1,
|
|
1496
|
+
busy
|
|
1307
1497
|
},
|
|
1308
1498
|
message.id
|
|
1309
1499
|
)
|
|
1310
1500
|
) }) }) }),
|
|
1311
|
-
/* @__PURE__ */ (0,
|
|
1312
|
-
error ? /* @__PURE__ */ (0,
|
|
1313
|
-
/* @__PURE__ */ (0,
|
|
1501
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
|
|
1502
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
1503
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1314
1504
|
Composer,
|
|
1315
1505
|
{
|
|
1316
1506
|
onSend: send,
|
|
1317
1507
|
onStop: stop,
|
|
1318
1508
|
busy,
|
|
1509
|
+
focusHandleRef: composerFocus,
|
|
1319
1510
|
disabled: loading || restoring || Boolean(configError),
|
|
1320
1511
|
...placeholder ? { placeholder } : {},
|
|
1321
1512
|
...autoFocus ? { autoFocus } : {},
|
|
@@ -1331,7 +1522,7 @@ function ChatPanel({
|
|
|
1331
1522
|
}
|
|
1332
1523
|
|
|
1333
1524
|
// src/components/chat/FrankieWidget.tsx
|
|
1334
|
-
var
|
|
1525
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1335
1526
|
function FrankieWidget({
|
|
1336
1527
|
position = "bottom-right",
|
|
1337
1528
|
offset = 24,
|
|
@@ -1344,13 +1535,15 @@ function FrankieWidget({
|
|
|
1344
1535
|
}) {
|
|
1345
1536
|
const { config } = useFrankie();
|
|
1346
1537
|
const theme = useFrankieTheme();
|
|
1347
|
-
const
|
|
1538
|
+
const rootRef = (0, import_react10.useRef)(null);
|
|
1539
|
+
const stylesReady = useStylesReady(rootRef);
|
|
1540
|
+
const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react10.useState)(defaultOpen);
|
|
1348
1541
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1349
1542
|
const setOpen = (next) => {
|
|
1350
1543
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
1351
1544
|
onOpenChange?.(next);
|
|
1352
1545
|
};
|
|
1353
|
-
(0,
|
|
1546
|
+
(0, import_react10.useEffect)(() => {
|
|
1354
1547
|
if (!open) return;
|
|
1355
1548
|
const onKeyDown = (event) => {
|
|
1356
1549
|
if (event.key === "Escape") setOpen(false);
|
|
@@ -1361,15 +1554,17 @@ function FrankieWidget({
|
|
|
1361
1554
|
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
1362
1555
|
const mode = resolveMode(theme?.mode);
|
|
1363
1556
|
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
1364
|
-
return /* @__PURE__ */ (0,
|
|
1557
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1365
1558
|
"div",
|
|
1366
1559
|
{
|
|
1560
|
+
ref: rootRef,
|
|
1367
1561
|
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
1368
|
-
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
|
|
1562
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
|
|
1369
1563
|
"data-frankie-mode": mode,
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
/* @__PURE__ */ (0,
|
|
1564
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1565
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
1566
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
|
|
1567
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1373
1568
|
Button,
|
|
1374
1569
|
{
|
|
1375
1570
|
size: "icon",
|
|
@@ -1378,7 +1573,7 @@ function FrankieWidget({
|
|
|
1378
1573
|
"aria-expanded": open,
|
|
1379
1574
|
"aria-label": open ? "Close the assistant" : label,
|
|
1380
1575
|
title: label,
|
|
1381
|
-
children: open ? /* @__PURE__ */ (0,
|
|
1576
|
+
children: open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.MessageCircle, { className: "size-5" })
|
|
1382
1577
|
}
|
|
1383
1578
|
) })
|
|
1384
1579
|
] })
|
|
@@ -1387,39 +1582,44 @@ function FrankieWidget({
|
|
|
1387
1582
|
}
|
|
1388
1583
|
|
|
1389
1584
|
// src/components/chat/FrankieInline.tsx
|
|
1390
|
-
var
|
|
1585
|
+
var import_react11 = require("react");
|
|
1586
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1391
1587
|
function FrankieInline({ className, style, ...panelProps }) {
|
|
1392
1588
|
const theme = useFrankieTheme();
|
|
1393
1589
|
const mode = resolveMode(theme?.mode);
|
|
1394
|
-
|
|
1590
|
+
const rootRef = (0, import_react11.useRef)(null);
|
|
1591
|
+
const stylesReady = useStylesReady(rootRef);
|
|
1592
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
1395
1593
|
"div",
|
|
1396
1594
|
{
|
|
1595
|
+
ref: rootRef,
|
|
1397
1596
|
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1398
|
-
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1597
|
+
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
|
|
1399
1598
|
"data-frankie-mode": mode,
|
|
1400
|
-
|
|
1599
|
+
"data-frankie-styles": stylesReady ? "ready" : "pending",
|
|
1600
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatPanel, { ...panelProps, fill: true })
|
|
1401
1601
|
}
|
|
1402
1602
|
);
|
|
1403
1603
|
}
|
|
1404
1604
|
|
|
1405
1605
|
// src/components/chat/FrankieChatPage.tsx
|
|
1406
|
-
var
|
|
1407
|
-
var
|
|
1408
|
-
var
|
|
1606
|
+
var import_react12 = require("react");
|
|
1607
|
+
var import_lucide_react8 = require("lucide-react");
|
|
1608
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1409
1609
|
function PageEmptyState() {
|
|
1410
1610
|
const { config } = useFrankie();
|
|
1411
1611
|
const { send, busy, restoring } = useFrankieChat();
|
|
1412
1612
|
const name = config?.branding.name ?? "Frankie";
|
|
1413
|
-
return /* @__PURE__ */ (0,
|
|
1414
|
-
/* @__PURE__ */ (0,
|
|
1415
|
-
/* @__PURE__ */ (0,
|
|
1416
|
-
/* @__PURE__ */ (0,
|
|
1417
|
-
/* @__PURE__ */ (0,
|
|
1418
|
-
/* @__PURE__ */ (0,
|
|
1613
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
|
|
1614
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
|
|
1615
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
1616
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
|
|
1617
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h1", { children: "How can I help?" }),
|
|
1618
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
|
|
1419
1619
|
] }),
|
|
1420
|
-
/* @__PURE__ */ (0,
|
|
1421
|
-
/* @__PURE__ */ (0,
|
|
1422
|
-
/* @__PURE__ */ (0,
|
|
1620
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
|
|
1621
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: prompt }),
|
|
1622
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("b", { "aria-hidden": true, children: "\u2197" })
|
|
1423
1623
|
] }, prompt)) })
|
|
1424
1624
|
] });
|
|
1425
1625
|
}
|
|
@@ -1427,22 +1627,22 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1427
1627
|
const { client, config } = useFrankie();
|
|
1428
1628
|
const { busy, messages } = useFrankieChat();
|
|
1429
1629
|
const theme = useFrankieTheme();
|
|
1430
|
-
const [conversations, setConversations] = (0,
|
|
1431
|
-
const [historyError, setHistoryError] = (0,
|
|
1432
|
-
const [historyLoading, setHistoryLoading] = (0,
|
|
1433
|
-
const [drawerOpen, setDrawerOpen] = (0,
|
|
1434
|
-
const [collapsed, setCollapsed] = (0,
|
|
1630
|
+
const [conversations, setConversations] = (0, import_react12.useState)([]);
|
|
1631
|
+
const [historyError, setHistoryError] = (0, import_react12.useState)("");
|
|
1632
|
+
const [historyLoading, setHistoryLoading] = (0, import_react12.useState)(false);
|
|
1633
|
+
const [drawerOpen, setDrawerOpen] = (0, import_react12.useState)(false);
|
|
1634
|
+
const [collapsed, setCollapsed] = (0, import_react12.useState)(() => {
|
|
1435
1635
|
try {
|
|
1436
1636
|
return window.localStorage.getItem("frankie.history-collapsed") === "1";
|
|
1437
1637
|
} catch {
|
|
1438
1638
|
return false;
|
|
1439
1639
|
}
|
|
1440
1640
|
});
|
|
1441
|
-
const [query, setQuery] = (0,
|
|
1442
|
-
const dialog = (0,
|
|
1443
|
-
const menu = (0,
|
|
1444
|
-
const requestVersion = (0,
|
|
1445
|
-
const refresh = (0,
|
|
1641
|
+
const [query, setQuery] = (0, import_react12.useState)("");
|
|
1642
|
+
const dialog = (0, import_react12.useRef)(null);
|
|
1643
|
+
const menu = (0, import_react12.useRef)(null);
|
|
1644
|
+
const requestVersion = (0, import_react12.useRef)(0);
|
|
1645
|
+
const refresh = (0, import_react12.useCallback)(async () => {
|
|
1446
1646
|
if (!showHistory || !config || config.allowAnonymous) {
|
|
1447
1647
|
setHistoryError("");
|
|
1448
1648
|
return;
|
|
@@ -1463,19 +1663,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1463
1663
|
if (version === requestVersion.current) setHistoryLoading(false);
|
|
1464
1664
|
}
|
|
1465
1665
|
}, [client, showHistory, config?.allowAnonymous]);
|
|
1466
|
-
(0,
|
|
1666
|
+
(0, import_react12.useEffect)(() => {
|
|
1467
1667
|
if (!busy) void refresh();
|
|
1468
1668
|
return () => {
|
|
1469
1669
|
requestVersion.current++;
|
|
1470
1670
|
};
|
|
1471
1671
|
}, [refresh, busy]);
|
|
1472
|
-
(0,
|
|
1672
|
+
(0, import_react12.useEffect)(() => {
|
|
1473
1673
|
try {
|
|
1474
1674
|
window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
|
|
1475
1675
|
} catch {
|
|
1476
1676
|
}
|
|
1477
1677
|
}, [collapsed]);
|
|
1478
|
-
(0,
|
|
1678
|
+
(0, import_react12.useEffect)(() => {
|
|
1479
1679
|
const node = dialog.current;
|
|
1480
1680
|
if (drawerOpen) {
|
|
1481
1681
|
node?.showModal?.();
|
|
@@ -1493,79 +1693,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
1493
1693
|
close();
|
|
1494
1694
|
}
|
|
1495
1695
|
};
|
|
1496
|
-
const history = /* @__PURE__ */ (0,
|
|
1497
|
-
/* @__PURE__ */ (0,
|
|
1498
|
-
/* @__PURE__ */ (0,
|
|
1499
|
-
/* @__PURE__ */ (0,
|
|
1696
|
+
const history = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
1697
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-history-heading", children: [
|
|
1698
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Conversations" }),
|
|
1699
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.X, { size: 18 }) })
|
|
1500
1700
|
] }),
|
|
1501
|
-
/* @__PURE__ */ (0,
|
|
1701
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
|
|
1502
1702
|
onNew();
|
|
1503
1703
|
close();
|
|
1504
1704
|
}, children: [
|
|
1505
|
-
/* @__PURE__ */ (0,
|
|
1705
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 17 }),
|
|
1506
1706
|
"New chat"
|
|
1507
1707
|
] }),
|
|
1508
|
-
/* @__PURE__ */ (0,
|
|
1509
|
-
/* @__PURE__ */ (0,
|
|
1510
|
-
/* @__PURE__ */ (0,
|
|
1708
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { className: "frankie-history-search", children: [
|
|
1709
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "Search conversations" }),
|
|
1710
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
|
|
1511
1711
|
] }),
|
|
1512
|
-
/* @__PURE__ */ (0,
|
|
1513
|
-
/* @__PURE__ */ (0,
|
|
1514
|
-
/* @__PURE__ */ (0,
|
|
1712
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
|
|
1713
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: item.title }),
|
|
1714
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
|
|
1515
1715
|
] }, item.id)) }),
|
|
1516
|
-
historyLoading ? /* @__PURE__ */ (0,
|
|
1716
|
+
historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { role: "status", children: [
|
|
1517
1717
|
historyError,
|
|
1518
|
-
/* @__PURE__ */ (0,
|
|
1519
|
-
] }) : !conversations.length ? /* @__PURE__ */ (0,
|
|
1520
|
-
navigation ? /* @__PURE__ */ (0,
|
|
1718
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
|
|
1719
|
+
] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
|
|
1720
|
+
navigation ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
|
|
1521
1721
|
] });
|
|
1522
|
-
return /* @__PURE__ */ (0,
|
|
1523
|
-
showHistory ? /* @__PURE__ */ (0,
|
|
1524
|
-
/* @__PURE__ */ (0,
|
|
1722
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
|
|
1723
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
|
|
1724
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsRight, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsLeft, { size: 16 }) }),
|
|
1525
1725
|
collapsed ? null : history
|
|
1526
1726
|
] }) : null,
|
|
1527
|
-
/* @__PURE__ */ (0,
|
|
1528
|
-
/* @__PURE__ */ (0,
|
|
1529
|
-
/* @__PURE__ */ (0,
|
|
1530
|
-
showHistory ? /* @__PURE__ */ (0,
|
|
1531
|
-
/* @__PURE__ */ (0,
|
|
1727
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-main", children: [
|
|
1728
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("header", { className: "frankie-page-header", children: [
|
|
1729
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
1730
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Menu, { size: 20 }) }) : null,
|
|
1731
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
|
|
1532
1732
|
] }),
|
|
1533
|
-
/* @__PURE__ */ (0,
|
|
1534
|
-
/* @__PURE__ */ (0,
|
|
1535
|
-
/* @__PURE__ */ (0,
|
|
1536
|
-
/* @__PURE__ */ (0,
|
|
1733
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
1734
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
|
|
1735
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 16 }),
|
|
1736
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "New chat" })
|
|
1537
1737
|
] }),
|
|
1538
|
-
/* @__PURE__ */ (0,
|
|
1539
|
-
/* @__PURE__ */ (0,
|
|
1540
|
-
/* @__PURE__ */ (0,
|
|
1541
|
-
/* @__PURE__ */ (0,
|
|
1542
|
-
/* @__PURE__ */ (0,
|
|
1543
|
-
/* @__PURE__ */ (0,
|
|
1544
|
-
/* @__PURE__ */ (0,
|
|
1738
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { className: "frankie-chat-settings", children: [
|
|
1739
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Settings, { size: 19 }) }),
|
|
1740
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
1741
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Chat settings" }),
|
|
1742
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: "History syncs automatically for signed-in users." }),
|
|
1743
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
|
|
1744
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.History, { size: 16 }),
|
|
1545
1745
|
"Refresh history"
|
|
1546
1746
|
] }),
|
|
1547
|
-
/* @__PURE__ */ (0,
|
|
1747
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: "Skills are checked at the start of each message." })
|
|
1548
1748
|
] })
|
|
1549
1749
|
] })
|
|
1550
1750
|
] })
|
|
1551
1751
|
] }),
|
|
1552
|
-
/* @__PURE__ */ (0,
|
|
1752
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageEmptyState, {}) }) })
|
|
1553
1753
|
] }),
|
|
1554
|
-
showHistory ? /* @__PURE__ */ (0,
|
|
1754
|
+
showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
|
|
1555
1755
|
if (event.target === event.currentTarget) close();
|
|
1556
|
-
}, children: /* @__PURE__ */ (0,
|
|
1756
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("aside", { children: history }) }) : null
|
|
1557
1757
|
] });
|
|
1558
1758
|
}
|
|
1559
1759
|
function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
|
|
1560
|
-
const [id, setId] = (0,
|
|
1760
|
+
const [id, setId] = (0, import_react12.useState)(() => provider.conversationId ?? crypto.randomUUID());
|
|
1561
1761
|
const onNew = () => setId(crypto.randomUUID());
|
|
1562
|
-
|
|
1762
|
+
const pageRef = (0, import_react12.useRef)(null);
|
|
1763
|
+
const stylesReady = useStylesReady(pageRef);
|
|
1764
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("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__ */ (0, import_jsx_runtime18.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
|
|
1563
1765
|
}
|
|
1564
1766
|
|
|
1565
1767
|
// src/components/ui/separator.tsx
|
|
1566
|
-
var
|
|
1768
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
1567
1769
|
function Separator({ className }) {
|
|
1568
|
-
return /* @__PURE__ */ (0,
|
|
1770
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1569
1771
|
}
|
|
1570
1772
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1571
1773
|
0 && (module.exports = {
|
|
@@ -1579,6 +1781,7 @@ function Separator({ className }) {
|
|
|
1579
1781
|
CardHeader,
|
|
1580
1782
|
CardTitle,
|
|
1581
1783
|
ChatPanel,
|
|
1784
|
+
Choices,
|
|
1582
1785
|
Composer,
|
|
1583
1786
|
FRANKIE_CLOUD_URL,
|
|
1584
1787
|
FrankieApiError,
|
|
@@ -1599,6 +1802,7 @@ function Separator({ className }) {
|
|
|
1599
1802
|
applyEvent,
|
|
1600
1803
|
buttonVariants,
|
|
1601
1804
|
cn,
|
|
1805
|
+
extractChoices,
|
|
1602
1806
|
newAssistantMessage,
|
|
1603
1807
|
newUserMessage,
|
|
1604
1808
|
readEventStream,
|