@frankieai/react 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frankie.css +1 -1
- package/dist/index.cjs +205 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +201 -173
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/AgentContext.tsx +22 -0
- package/src/components/chat/ChatPanel.tsx +9 -7
- package/src/components/chat/Composer.tsx +6 -5
- package/src/components/chat/FrankieChatPage.tsx +2 -3
- package/src/components/chat/MessageBubble.tsx +7 -5
- package/src/styles/frankie.css +61 -0
- package/src/widget.test.tsx +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frankieai/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Embeddable, themeable chat widget for the Frankie onboarding agent — shadcn/ui components, works with or without Tailwind in the host app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"lucide-react": "^0.460.0",
|
|
33
33
|
"tailwind-merge": "^2.5.0",
|
|
34
|
-
"@frankieai/core": "0.2.
|
|
34
|
+
"@frankieai/core": "0.2.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@tailwindcss/cli": "^4.0.0",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ArrowUpRight, Sparkles } from "lucide-react";
|
|
3
|
+
import { useFrankie } from "../../hooks/FrankieProvider.jsx";
|
|
4
|
+
import { useFrankieChat } from "../../hooks/useFrankieChat.js";
|
|
5
|
+
|
|
6
|
+
/** Reports server-confirmed skills for the current turn; never implies drafts are active. */
|
|
7
|
+
export function AgentContext() {
|
|
8
|
+
const { loading, configError } = useFrankie();
|
|
9
|
+
const { loadedSkills, busy, restoring, error } = useFrankieChat();
|
|
10
|
+
const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
|
|
11
|
+
return <div className="frankie-agent-context-wrap">
|
|
12
|
+
<details className="frankie-agent-context">
|
|
13
|
+
<summary aria-label="Agent context"><Sparkles size={17} aria-hidden="true" /><span>Agent context</span><span className="frankie-context-count">{loadedSkills === null ? "Not checked" : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"}`}</span><span className="frankie-context-status" data-active={status === "Active"}><i aria-hidden="true" />{status}</span><ArrowUpRight size={15} aria-hidden="true" /></summary>
|
|
14
|
+
<div className="frankie-context-detail">
|
|
15
|
+
<strong>{loadedSkills === null ? "Skills are checked when you send a message" : loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn"}</strong>
|
|
16
|
+
{loadedSkills?.length ? <ul>{loadedSkills.map(skill => <li key={skill.id}><span>{skill.name}</span><small>v{skill.revision}</small></li>)}</ul> : <p>Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded.</p>}
|
|
17
|
+
<p>Published skills and updates are checked on every message. Only approved, available tools can run.</p>
|
|
18
|
+
</div>
|
|
19
|
+
</details>
|
|
20
|
+
<span className="frankie-sr-only" role="status">{loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.`}</span>
|
|
21
|
+
</div>;
|
|
22
|
+
}
|
|
@@ -11,6 +11,7 @@ import { Avatar } from "../ui/avatar.js";
|
|
|
11
11
|
import { Button } from "../ui/button.js";
|
|
12
12
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card.js";
|
|
13
13
|
import { ScrollArea } from "../ui/scroll-area.js";
|
|
14
|
+
import { AgentContext } from "./AgentContext.js";
|
|
14
15
|
import { Composer } from "./Composer.js";
|
|
15
16
|
import { MessageBubble } from "./MessageBubble.js";
|
|
16
17
|
import { SuggestedPrompts } from "./SuggestedPrompts.js";
|
|
@@ -49,7 +50,7 @@ export function ChatPanel({
|
|
|
49
50
|
}: ChatPanelProps) {
|
|
50
51
|
const { config, configError, loading } = useFrankie();
|
|
51
52
|
const theme = useFrankieTheme();
|
|
52
|
-
const { messages, busy, restoring,
|
|
53
|
+
const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
|
|
53
54
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
54
55
|
|
|
55
56
|
const features = config?.features;
|
|
@@ -70,7 +71,8 @@ export function ChatPanel({
|
|
|
70
71
|
return (
|
|
71
72
|
<Card
|
|
72
73
|
className={cn(
|
|
73
|
-
"frankie-root",
|
|
74
|
+
"frankie-root frankie-chat-panel",
|
|
75
|
+
fill && "frankie-chat-panel-fill",
|
|
74
76
|
fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
|
|
75
77
|
mode === "dark" && "dark",
|
|
76
78
|
className,
|
|
@@ -98,8 +100,9 @@ export function ChatPanel({
|
|
|
98
100
|
</CardHeader>
|
|
99
101
|
)}
|
|
100
102
|
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
+
<AgentContext />
|
|
104
|
+
<CardContent className="frankie-conversation-content">
|
|
105
|
+
<ScrollArea ref={scrollRef} className="frankie-transcript flex h-full flex-col gap-4 p-4" aria-live="polite">
|
|
103
106
|
{configError ? (
|
|
104
107
|
<div className="frankie-config-error" role="alert">
|
|
105
108
|
<strong>Frankie couldn’t start</strong>
|
|
@@ -116,7 +119,7 @@ export function ChatPanel({
|
|
|
116
119
|
</div>
|
|
117
120
|
))
|
|
118
121
|
) : (
|
|
119
|
-
<div className="flex flex-col gap-4">
|
|
122
|
+
<div className="frankie-message-list flex flex-col gap-4">
|
|
120
123
|
{messages.map((message) =>
|
|
121
124
|
renderMessage ? (
|
|
122
125
|
<div key={message.id}>{renderMessage(message)}</div>
|
|
@@ -136,8 +139,7 @@ export function ChatPanel({
|
|
|
136
139
|
</ScrollArea>
|
|
137
140
|
</CardContent>
|
|
138
141
|
|
|
139
|
-
<CardFooter className="flex flex-col gap-2">
|
|
140
|
-
{loadedSkills !== null ? <details className="frankie-loaded-skills"><summary role="status">{loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn"}</summary>{loadedSkills.length ? <ul>{loadedSkills.map(skill => <li key={skill.id}>{skill.name}<small>v{skill.revision}</small></li>)}</ul> : <p>Your assistant can still answer using its available tools.</p>}</details> : null}
|
|
142
|
+
<CardFooter className="frankie-composer-footer flex flex-col gap-2">
|
|
141
143
|
{error ? <p className="w-full text-xs text-red-600 dark:text-red-400">{error}</p> : null}
|
|
142
144
|
<div className="w-full">
|
|
143
145
|
<Composer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ArrowUp, Paperclip, Square, X } from "lucide-react";
|
|
3
|
+
import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
|
|
4
4
|
import { useCallback, useEffect, useRef, useState, type KeyboardEvent } from "react";
|
|
5
5
|
import type { ChatAttachment } from "@frankieai/core";
|
|
6
6
|
import { Button } from "../ui/button.js";
|
|
@@ -107,7 +107,7 @@ export function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocu
|
|
|
107
107
|
}, [busy, disabled, files, modelPicker?.value, onSend, value]);
|
|
108
108
|
|
|
109
109
|
const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
110
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
110
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
111
111
|
event.preventDefault();
|
|
112
112
|
submit();
|
|
113
113
|
}
|
|
@@ -117,7 +117,7 @@ export function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocu
|
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
119
|
<form
|
|
120
|
-
className="flex flex-col gap-2"
|
|
120
|
+
className="frankie-composer flex flex-col gap-2"
|
|
121
121
|
onSubmit={(event) => {
|
|
122
122
|
event.preventDefault();
|
|
123
123
|
submit();
|
|
@@ -147,7 +147,7 @@ export function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocu
|
|
|
147
147
|
|
|
148
148
|
{fileError ? <p className="text-xs text-red-600 dark:text-red-400">{fileError}</p> : null}
|
|
149
149
|
|
|
150
|
-
<div className="flex items-end gap-2">
|
|
150
|
+
<div className="frankie-composer-controls flex items-end gap-2">
|
|
151
151
|
{canAttach ? (
|
|
152
152
|
<>
|
|
153
153
|
<input
|
|
@@ -170,7 +170,7 @@ export function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocu
|
|
|
170
170
|
aria-label="Attach files"
|
|
171
171
|
title="Attach files"
|
|
172
172
|
>
|
|
173
|
-
<
|
|
173
|
+
<Plus />
|
|
174
174
|
</Button>
|
|
175
175
|
</>
|
|
176
176
|
) : null}
|
|
@@ -236,3 +236,4 @@ function readAsBase64(file: File): Promise<string> {
|
|
|
236
236
|
reader.readAsDataURL(file);
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
|
+
|
|
@@ -33,7 +33,7 @@ function PageEmptyState() {
|
|
|
33
33
|
|
|
34
34
|
function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...panel }: Pick<FrankieChatPageProps, "header" | "footer" | "emptyState" | "renderMessage" | "renderTool" | "placeholder" | "autoFocus" | "navigation"> & { showHistory: boolean; currentId: string; onSelect: (id: string) => void; onNew: () => void }) {
|
|
35
35
|
const { client, config } = useFrankie();
|
|
36
|
-
const { busy } = useFrankieChat();
|
|
36
|
+
const { busy, messages } = useFrankieChat();
|
|
37
37
|
const theme = useFrankieTheme();
|
|
38
38
|
const [conversations, setConversations] = useState<ChatConversation[]>([]);
|
|
39
39
|
const [historyError, setHistoryError] = useState("");
|
|
@@ -43,7 +43,6 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
43
43
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
44
44
|
const menu = useRef<HTMLButtonElement>(null);
|
|
45
45
|
const requestVersion = useRef(0);
|
|
46
|
-
const name = config?.branding.name ?? "Frankie";
|
|
47
46
|
const refresh = useCallback(async () => {
|
|
48
47
|
if (!showHistory || !config || config.allowAnonymous) { setHistoryError(""); return; }
|
|
49
48
|
const version = ++requestVersion.current;
|
|
@@ -71,7 +70,7 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
|
|
|
71
70
|
return <div className={"frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : "")} style={themeToCssVars(theme)}>
|
|
72
71
|
{showHistory ? <aside className="frankie-history-desktop">{history}</aside> : null}
|
|
73
72
|
<div className="frankie-page-main">
|
|
74
|
-
<header className="frankie-page-header"><div>{showHistory ? <button type="button" className="frankie-mobile-only" ref={menu} aria-label="Open conversations" aria-haspopup="dialog" aria-expanded={drawerOpen} onClick={() => setDrawerOpen(true)}><Menu size={20} /></button> : null}<strong
|
|
73
|
+
<header className="frankie-page-header"><div>{showHistory ? <button type="button" className="frankie-mobile-only" ref={menu} aria-label="Open conversations" aria-haspopup="dialog" aria-expanded={drawerOpen} onClick={() => setDrawerOpen(true)}><Menu size={20} /></button> : null}<strong className="frankie-thread-title">{conversations.find(item => item.id === currentId)?.title ?? messages.find(message => message.role === "user")?.text ?? "New chat"}</strong></div><div>
|
|
75
74
|
<button type="button" disabled={busy} onClick={onNew} aria-label="Start a new conversation"><Plus size={16} /><span>New chat</span></button>
|
|
76
75
|
<details className="frankie-chat-settings"><summary aria-label="Chat settings"><Settings size={19} /></summary><div><strong>Chat settings</strong><p>History syncs automatically for signed-in users.</p><button type="button" onClick={() => void refresh()} disabled={historyLoading || !showHistory}><History size={16} />Refresh history</button><small>Skills are checked at the start of each message.</small></div></details>
|
|
77
76
|
</div></header>
|
|
@@ -26,7 +26,7 @@ export function MessageBubble({
|
|
|
26
26
|
}: MessageBubbleProps) {
|
|
27
27
|
if (message.role === "user") {
|
|
28
28
|
return (
|
|
29
|
-
<div className="flex flex-col items-end gap-1">
|
|
29
|
+
<div className="frankie-user-message flex flex-col items-end gap-1">
|
|
30
30
|
{message.attachments?.length ? (
|
|
31
31
|
<div className="flex max-w-[85%] flex-wrap justify-end gap-1">
|
|
32
32
|
{message.attachments.map((file, index) => (
|
|
@@ -42,7 +42,7 @@ export function MessageBubble({
|
|
|
42
42
|
) : null}
|
|
43
43
|
{message.text ? (
|
|
44
44
|
<div className="flex max-w-[85%] items-end gap-1">
|
|
45
|
-
<div className="rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground">{message.text}</div>
|
|
45
|
+
<div className="frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground">{message.text}</div>
|
|
46
46
|
<MessageCopyButton text={message.text} />
|
|
47
47
|
</div>
|
|
48
48
|
) : null}
|
|
@@ -53,12 +53,12 @@ export function MessageBubble({
|
|
|
53
53
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
<div className="flex gap-2">
|
|
56
|
+
<div className="frankie-assistant-message flex gap-2">
|
|
57
57
|
<div className="frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1">
|
|
58
58
|
<Avatar {...(avatarUrl ? { src: avatarUrl } : {})} name={assistantName} />
|
|
59
59
|
{message.text ? <MessageCopyButton text={message.text} /> : null}
|
|
60
60
|
</div>
|
|
61
|
-
<div className="min-w-0 max-w-[85%]">
|
|
61
|
+
<div className="frankie-assistant-content min-w-0 max-w-[85%]">
|
|
62
62
|
{message.status === "error" ? (
|
|
63
63
|
<p className="rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">
|
|
64
64
|
{message.error}
|
|
@@ -90,7 +90,8 @@ function MessageCopyButton({ text }: { text: string }) {
|
|
|
90
90
|
|
|
91
91
|
export function TypingDots() {
|
|
92
92
|
return (
|
|
93
|
-
<span className="inline-flex items-center gap-1 py-2" role="status" aria-label="Thinking">
|
|
93
|
+
<span className="frankie-thinking inline-flex items-center gap-1 py-2" role="status" aria-label="Thinking">
|
|
94
|
+
<span className="frankie-thinking-label">Thinking</span>
|
|
94
95
|
{[0, 1, 2].map((index) => (
|
|
95
96
|
<span
|
|
96
97
|
key={index}
|
|
@@ -101,3 +102,4 @@ export function TypingDots() {
|
|
|
101
102
|
</span>
|
|
102
103
|
);
|
|
103
104
|
}
|
|
105
|
+
|
package/src/styles/frankie.css
CHANGED
|
@@ -346,3 +346,64 @@
|
|
|
346
346
|
.frankie-page-canvas .frankie-root > .border-t { padding: .65rem .75rem calc(.65rem + env(safe-area-inset-bottom)); }
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
/* Shared conversation surface: SDK pages, embedded panels and Frankie's app. */
|
|
351
|
+
.frankie-root.frankie-chat-panel { min-height:0; position:relative; }
|
|
352
|
+
.frankie-root .frankie-agent-context-wrap { flex:none; display:flex; justify-content:center; padding:12px 10px; }
|
|
353
|
+
.frankie-root .frankie-agent-context { position:relative; max-width:100%; color:var(--frankie-foreground); font-size:13px; }
|
|
354
|
+
.frankie-root .frankie-agent-context > summary { display:flex; align-items:center; justify-content:center; gap:10px; min-height:44px; padding:7px 13px; border:1px solid var(--frankie-border); border-radius:999px; background:var(--frankie-background); box-shadow:0 1px 3px #0000000d; cursor:pointer; list-style:none; white-space:nowrap; }
|
|
355
|
+
.frankie-root .frankie-agent-context > summary::-webkit-details-marker { display:none; }
|
|
356
|
+
.frankie-root .frankie-agent-context > summary > svg { flex:none; color:var(--frankie-muted-foreground); }
|
|
357
|
+
.frankie-root .frankie-context-count { padding:2px 10px; border-radius:999px; background:var(--frankie-muted); font-size:12px; color:var(--frankie-muted-foreground); }
|
|
358
|
+
.frankie-root .frankie-context-status { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--frankie-muted-foreground); }
|
|
359
|
+
.frankie-root .frankie-context-status i { width:7px; height:7px; border-radius:50%; background:currentColor; }
|
|
360
|
+
.frankie-root .frankie-context-status[data-active="true"] { color:#00976b; }
|
|
361
|
+
.frankie-root .frankie-context-detail { position:absolute; z-index:10; top:calc(100% + 10px); left:50%; transform:translateX(-50%); width:340px; max-width:calc(100vw - 32px); padding:18px; border:1px solid var(--frankie-border); border-radius:16px; background:var(--frankie-background); box-shadow:0 12px 32px #00000016; }
|
|
362
|
+
.frankie-root .frankie-context-detail p { margin:10px 0 0; font-size:12px; line-height:1.6; color:var(--frankie-muted-foreground); }
|
|
363
|
+
.frankie-root .frankie-context-detail ul { list-style:none; margin:12px 0; padding:0; max-height:220px; overflow:auto; }
|
|
364
|
+
.frankie-root .frankie-context-detail li { display:flex; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px solid var(--frankie-border); overflow-wrap:anywhere; }
|
|
365
|
+
.frankie-root .frankie-context-detail small { flex:none; color:var(--frankie-muted-foreground); }
|
|
366
|
+
.frankie-root .frankie-sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
|
367
|
+
.frankie-root.frankie-chat-panel-fill { --frankie-canvas:color-mix(in srgb,var(--frankie-background) 60%,var(--frankie-muted)); background:var(--frankie-canvas); }
|
|
368
|
+
.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap { padding:44px 16px 26px; }
|
|
369
|
+
.frankie-root.frankie-chat-panel-fill .frankie-transcript { padding:14px max(24px,calc((100% - 960px)/2)) 24px; }
|
|
370
|
+
.frankie-root.frankie-chat-panel-fill .frankie-message-list { gap:38px; }
|
|
371
|
+
.frankie-root.frankie-chat-panel-fill .frankie-user-bubble { border-radius:24px; padding:12px 20px; background:var(--frankie-muted); color:var(--frankie-foreground); font-size:15px; white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
372
|
+
.frankie-root.frankie-chat-panel-fill .frankie-assistant-content { max-width:100%; flex:1; font-size:15px; line-height:1.7; }
|
|
373
|
+
.frankie-root.frankie-chat-panel-fill .frankie-assistant-message { gap:14px; }
|
|
374
|
+
.frankie-root .frankie-thinking { color:var(--frankie-muted-foreground); gap:5px; }
|
|
375
|
+
.frankie-root .frankie-thinking-label { margin-right:6px; font-size:14px; }
|
|
376
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer-footer { border:0; padding:16px max(24px,calc((100% - 1000px)/2)) 24px; background:transparent; flex:none; }
|
|
377
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer { padding:14px; border:1px solid var(--frankie-border); border-radius:28px; background:var(--frankie-background); box-shadow:0 2px 4px #0000000a; }
|
|
378
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within { border-color:color-mix(in srgb,var(--frankie-foreground) 32%,var(--frankie-border)); box-shadow:0 0 0 2px color-mix(in srgb,var(--frankie-primary) 8%,transparent); }
|
|
379
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer-controls { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
|
|
380
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer textarea { order:-1; flex:0 0 100%; width:100%; min-height:32px; max-height:140px; margin-bottom:4px; padding:2px 6px; border:0; border-radius:0; resize:none; background:transparent; color:var(--frankie-foreground); font-size:16px; box-shadow:none; outline:none; }
|
|
381
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Send"],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Stop"] { margin-left:auto; width:42px; height:42px; border-radius:50%; background:var(--frankie-primary); color:var(--frankie-primary-foreground); border:0; }
|
|
382
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Attach files"] { width:42px; height:42px; border-radius:50%; }
|
|
383
|
+
.frankie-root .frankie-history-desktop { width:290px; background:var(--frankie-background); padding:20px 12px; }
|
|
384
|
+
.frankie-root .frankie-new-chat { justify-content:flex-start; border:0; background:transparent; padding:8px 12px; font-size:15px; }
|
|
385
|
+
.frankie-root .frankie-history-heading { font-size:12px; font-weight:400; color:var(--frankie-muted-foreground); padding:0 12px; }
|
|
386
|
+
.frankie-root .frankie-history-list button { min-height:44px; padding:10px 12px; }
|
|
387
|
+
.frankie-root .frankie-history-list button span { font-size:14px; }
|
|
388
|
+
.frankie-root .frankie-history-list button[aria-current],.frankie-root .frankie-history-list button:hover { border-color:transparent; background:var(--frankie-muted); }
|
|
389
|
+
.frankie-root .frankie-history-list small { display:none; }
|
|
390
|
+
.frankie-root .frankie-page-header { min-height:56px; height:56px; background:var(--frankie-background); }
|
|
391
|
+
.frankie-root .frankie-page-header > div { min-width:0; }
|
|
392
|
+
.frankie-root .frankie-thread-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:55vw; font-weight:500; }
|
|
393
|
+
.frankie-root .frankie-page-header button { border-color:transparent; background:transparent; }
|
|
394
|
+
.frankie-root .frankie-page-canvas > .frankie-inline { max-width:none; }
|
|
395
|
+
@media(max-width:760px) {
|
|
396
|
+
.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap { padding:20px 8px 14px; }
|
|
397
|
+
.frankie-root.frankie-chat-panel-fill .frankie-transcript { padding:12px 16px 16px; }
|
|
398
|
+
.frankie-root.frankie-chat-panel-fill .frankie-composer-footer { padding:10px 12px calc(12px + env(safe-area-inset-bottom)); }
|
|
399
|
+
.frankie-root .frankie-agent-context > summary { gap:6px; padding:7px 9px; font-size:12px; }
|
|
400
|
+
.frankie-root .frankie-context-count { padding:2px 7px; font-size:11px; }
|
|
401
|
+
.frankie-root .frankie-context-status { font-size:11px; gap:4px; }
|
|
402
|
+
.frankie-root .frankie-agent-context > summary > svg { width:14px; }
|
|
403
|
+
.frankie-root .frankie-thread-title { max-width:42vw; }
|
|
404
|
+
}
|
|
405
|
+
.frankie-root .frankie-agent-context > summary > svg:first-child { box-sizing:content-box; padding:4px; border-radius:50%; background:var(--frankie-muted); }
|
|
406
|
+
.frankie-root.frankie-chat-panel-fill .frankie-agent-context > summary { font-size:14px; }
|
|
407
|
+
|
|
408
|
+
.frankie-root .frankie-agent-context > summary > span:first-of-type { border-right:1px solid var(--frankie-border); padding-right:10px; }
|
|
409
|
+
@media(max-width:760px) { .frankie-root .frankie-page-header button > span { display:none; } }
|
package/src/widget.test.tsx
CHANGED
|
@@ -226,6 +226,35 @@ describe("ChatPanel", () => {
|
|
|
226
226
|
expect(screen.queryByText("1 skill loaded")).toBeNull();
|
|
227
227
|
cleanup();
|
|
228
228
|
});
|
|
229
|
+
it("refreshes the context count and revisions from each server turn", async () => {
|
|
230
|
+
cleanup();
|
|
231
|
+
let turn = 0;
|
|
232
|
+
const fetchImpl = vi.fn(async (url: string | URL | Request) => {
|
|
233
|
+
if (String(url).endsWith("/v1/session")) return Response.json({ sessionId: "context", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } });
|
|
234
|
+
turn++;
|
|
235
|
+
const skills = turn === 1 ? [{ id: "one", name: "Reservations", revision: 2 }, { id: "two", name: "Locations", revision: 1 }, { id: "three", name: "Service", revision: 3 }] : [];
|
|
236
|
+
return new Response(sseStream([{ type: "skills_loaded", skills }, { type: "text_delta", text: `Response ${turn}` }, { type: "turn_end", stopReason: "end_turn" }]));
|
|
237
|
+
});
|
|
238
|
+
render(<FrankieProvider publishableKey="pk_context_fixture" config={config} fetchImpl={fetchImpl} persist={false}><ChatPanel fill /></FrankieProvider>);
|
|
239
|
+
expect(screen.getByText("Not checked")).toBeTruthy();
|
|
240
|
+
await waitFor(() => expect((screen.getByLabelText("Message") as HTMLTextAreaElement).disabled).toBe(false));
|
|
241
|
+
fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Review service" } });
|
|
242
|
+
fireEvent.click(screen.getByRole("button", { name: "Send" }));
|
|
243
|
+
await screen.findByText("Response 1");
|
|
244
|
+
expect(screen.getByText("3 skills")).toBeTruthy();
|
|
245
|
+
expect(screen.getByText("Active")).toBeTruthy();
|
|
246
|
+
fireEvent.click(screen.getByLabelText("Agent context"));
|
|
247
|
+
expect(screen.getByText("Reservations")).toBeTruthy();
|
|
248
|
+
expect(screen.getByText("v3")).toBeTruthy();
|
|
249
|
+
await waitFor(() => expect(screen.getByRole("button", { name: "Send" })).toBeTruthy());
|
|
250
|
+
fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Try again" } });
|
|
251
|
+
fireEvent.click(screen.getByRole("button", { name: "Send" }));
|
|
252
|
+
await screen.findByText("Response 2");
|
|
253
|
+
expect(screen.getByText("0 skills")).toBeTruthy();
|
|
254
|
+
expect(screen.queryByText("Active")).toBeNull();
|
|
255
|
+
expect(screen.queryByText("Reservations")).toBeNull();
|
|
256
|
+
cleanup();
|
|
257
|
+
});
|
|
229
258
|
it("shows the greeting and suggested prompts before the first message", async () => {
|
|
230
259
|
render(
|
|
231
260
|
<FrankieProvider baseUrl="https://agent.acme.com" publishableKey="pk_1" config={config}>
|