@frankieai/react 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frankie.css +1 -1
- package/dist/index.cjs +396 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +71 -21
- package/dist/index.d.ts +71 -21
- package/dist/index.js +397 -74
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +15 -3
- package/src/components/chat/Composer.tsx +190 -27
- package/src/components/chat/FrankieChatPage.tsx +53 -0
- package/src/components/chat/MessageBubble.tsx +39 -6
- package/src/components/chat/ToolActivityList.tsx +10 -6
- package/src/components/chat/message-actions.css +13 -0
- package/src/hooks/FrankieProvider.tsx +1 -9
- package/src/hooks/useFrankieChat.ts +91 -23
- package/src/index.ts +4 -2
- package/src/lib/client.ts +92 -16
- package/src/lib/messages.ts +9 -2
- package/src/styles/frankie.css +12 -4
- package/src/widget.test.tsx +4 -0
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { FrankieProvider, useFrankie, useFrankieTheme } from "./hooks/FrankieProvider.jsx";
|
|
2
2
|
export type { FrankieProviderProps } from "./hooks/FrankieProvider.jsx";
|
|
3
3
|
export { useFrankieChat } from "./hooks/useFrankieChat.js";
|
|
4
|
-
export type { UseFrankieChatResult } from "./hooks/useFrankieChat.js";
|
|
4
|
+
export type { UseFrankieChatResult, SendOptions } from "./hooks/useFrankieChat.js";
|
|
5
5
|
|
|
6
6
|
export { FrankieWidget } from "./components/chat/FrankieWidget.js";
|
|
7
7
|
export type { FrankieWidgetProps } from "./components/chat/FrankieWidget.js";
|
|
8
8
|
export { FrankieInline } from "./components/chat/FrankieInline.js";
|
|
9
9
|
export type { FrankieInlineProps } from "./components/chat/FrankieInline.js";
|
|
10
|
+
export { FrankieChatPage } from "./components/chat/FrankieChatPage.js";
|
|
11
|
+
export type { FrankieChatPageProps } from "./components/chat/FrankieChatPage.js";
|
|
10
12
|
export { ChatPanel } from "./components/chat/ChatPanel.js";
|
|
11
13
|
export type { ChatPanelProps } from "./components/chat/ChatPanel.js";
|
|
12
14
|
export { Composer } from "./components/chat/Composer.js";
|
|
@@ -27,7 +29,7 @@ export { Textarea } from "./components/ui/textarea.js";
|
|
|
27
29
|
export { FrankieClient, FrankieApiError, readEventStream, FRANKIE_CLOUD_URL } from "./lib/client.js";
|
|
28
30
|
export type { FrankieClientOptions, PageContext } from "./lib/client.js";
|
|
29
31
|
export { applyEvent, newAssistantMessage, newUserMessage } from "./lib/messages.js";
|
|
30
|
-
export type { AssistantChatMessage, ChatMessage, ToolActivity, UserChatMessage } from "./lib/messages.js";
|
|
32
|
+
export type { AssistantChatMessage, ChatMessage, MessageAttachment, ToolActivity, UserChatMessage } from "./lib/messages.js";
|
|
31
33
|
export { resolveMode, themeToCssVars } from "./lib/theme.js";
|
|
32
34
|
export { cn } from "./lib/utils.js";
|
|
33
35
|
|
package/src/lib/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ChatAttachment,
|
|
2
3
|
ChatRequestBody,
|
|
3
4
|
CreateSessionResponse,
|
|
4
5
|
FrankieServerEvent,
|
|
@@ -31,6 +32,17 @@ export interface FrankieClientOptions {
|
|
|
31
32
|
* set false for an ephemeral chat that starts fresh every load.
|
|
32
33
|
*/
|
|
33
34
|
persist?: boolean;
|
|
35
|
+
/** Override localStorage namespace for session and transcript. */
|
|
36
|
+
storageNamespace?: string;
|
|
37
|
+
/** Stable id for a server-backed conversation. */
|
|
38
|
+
conversationId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChatConversation {
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
updatedAt: number;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
export class FrankieApiError extends Error {
|
|
@@ -58,14 +70,18 @@ export class FrankieClient {
|
|
|
58
70
|
return this.session?.sessionId;
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
get conversationId(): string | undefined {
|
|
74
|
+
return this.options.conversationId;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
62
78
|
get persist(): boolean {
|
|
63
79
|
return this.options.persist !== false && typeof window !== "undefined";
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
82
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
67
83
|
get namespace(): string {
|
|
68
|
-
return `frankie.${this.options.publishableKey}`;
|
|
84
|
+
return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
private readStored(): CreateSessionResponse | undefined {
|
|
@@ -74,7 +90,6 @@ export class FrankieClient {
|
|
|
74
90
|
const raw = window.localStorage.getItem(`${this.namespace}.session`);
|
|
75
91
|
if (!raw) return undefined;
|
|
76
92
|
const session = JSON.parse(raw) as CreateSessionResponse;
|
|
77
|
-
// A stored session past its expiry is useless; drop it.
|
|
78
93
|
return new Date(session.expiresAt).getTime() > Date.now() + 30_000 ? session : undefined;
|
|
79
94
|
} catch {
|
|
80
95
|
return undefined;
|
|
@@ -99,34 +114,95 @@ export class FrankieClient {
|
|
|
99
114
|
return (await this.unwrap(response)) as PublicWidgetConfig;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
/** Opens (or reuses) a chat session. */
|
|
103
117
|
async ensureSession(context?: PageContext): Promise<CreateSessionResponse> {
|
|
104
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 30_000)
|
|
105
|
-
return this.session;
|
|
106
|
-
}
|
|
118
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 30_000) return this.session;
|
|
107
119
|
const userToken = await this.options.getUserToken?.();
|
|
120
|
+
if (this.options.getUserToken && !userToken) {
|
|
121
|
+
throw new FrankieApiError(
|
|
122
|
+
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
123
|
+
401,
|
|
124
|
+
"user_token_unavailable",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
108
127
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
109
128
|
method: "POST",
|
|
110
129
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
111
|
-
body: JSON.stringify({
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
...(userToken ? { userToken } : {}),
|
|
132
|
+
...(context ? { context } : {}),
|
|
133
|
+
...(this.options.conversationId ? { conversationId: this.options.conversationId } : {}),
|
|
134
|
+
}),
|
|
112
135
|
});
|
|
113
136
|
this.session = (await this.unwrap(response)) as CreateSessionResponse;
|
|
114
137
|
this.writeStored();
|
|
115
138
|
return this.session;
|
|
116
139
|
}
|
|
117
140
|
|
|
118
|
-
|
|
141
|
+
async listConversations(): Promise<ChatConversation[]> {
|
|
142
|
+
const userToken = await this.requireUserToken();
|
|
143
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
|
|
144
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` },
|
|
145
|
+
});
|
|
146
|
+
const body = (await this.unwrap(response)) as { conversations?: ChatConversation[] };
|
|
147
|
+
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async resumeConversation(id = this.options.conversationId): Promise<{ messages: unknown[]; title: string } | undefined> {
|
|
151
|
+
if (!id) return undefined;
|
|
152
|
+
const userToken = await this.requireUserToken();
|
|
153
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` },
|
|
156
|
+
});
|
|
157
|
+
if (response.status === 404) return undefined;
|
|
158
|
+
const body = (await this.unwrap(response)) as { session: CreateSessionResponse; messages?: unknown[]; title?: string };
|
|
159
|
+
this.session = body.session;
|
|
160
|
+
this.writeStored();
|
|
161
|
+
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async syncConversation(messages: unknown[], title?: string): Promise<void> {
|
|
165
|
+
if (!this.session || !this.options.conversationId) return;
|
|
166
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
|
|
167
|
+
method: "PUT",
|
|
168
|
+
headers: {
|
|
169
|
+
"content-type": "application/json",
|
|
170
|
+
"x-frankie-key": this.options.publishableKey,
|
|
171
|
+
authorization: `Bearer ${this.session.sessionToken}`,
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({ messages, ...(title ? { title } : {}) }),
|
|
174
|
+
});
|
|
175
|
+
await this.unwrap(response);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async deleteConversation(id: string): Promise<void> {
|
|
179
|
+
const userToken = await this.requireUserToken();
|
|
180
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
|
|
181
|
+
method: "DELETE",
|
|
182
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` },
|
|
183
|
+
});
|
|
184
|
+
if (response.status !== 404) await this.unwrap(response);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async requireUserToken(): Promise<string> {
|
|
188
|
+
const token = await this.options.getUserToken?.();
|
|
189
|
+
if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
|
|
190
|
+
return token;
|
|
191
|
+
}
|
|
192
|
+
|
|
119
193
|
resetSession(): void {
|
|
120
194
|
this.session = undefined;
|
|
121
195
|
this.writeStored();
|
|
122
196
|
}
|
|
123
197
|
|
|
124
|
-
/**
|
|
125
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
126
|
-
* Resolves when the turn ends.
|
|
127
|
-
*/
|
|
128
198
|
async send(
|
|
129
|
-
input: {
|
|
199
|
+
input: {
|
|
200
|
+
message: string;
|
|
201
|
+
context?: PageContext;
|
|
202
|
+
model?: string;
|
|
203
|
+
attachments?: ChatAttachment[];
|
|
204
|
+
signal?: AbortSignal;
|
|
205
|
+
},
|
|
130
206
|
onEvent: (event: FrankieServerEvent) => void,
|
|
131
207
|
): Promise<void> {
|
|
132
208
|
const session = await this.ensureSession(input.context);
|
|
@@ -134,6 +210,8 @@ export class FrankieClient {
|
|
|
134
210
|
sessionId: session.sessionId,
|
|
135
211
|
message: input.message,
|
|
136
212
|
...(input.context ? { context: input.context } : {}),
|
|
213
|
+
...(input.model ? { model: input.model } : {}),
|
|
214
|
+
...(input.attachments?.length ? { attachments: input.attachments } : {}),
|
|
137
215
|
};
|
|
138
216
|
|
|
139
217
|
const response = await this.doFetch(`${this.baseUrl}/v1/chat`, {
|
|
@@ -148,7 +226,6 @@ export class FrankieClient {
|
|
|
148
226
|
});
|
|
149
227
|
|
|
150
228
|
if (!response.ok || !response.body) {
|
|
151
|
-
// A dropped session is recoverable: clear it so the next send re-auths.
|
|
152
229
|
if (response.status === 401 || response.status === 404) this.resetSession();
|
|
153
230
|
await this.unwrap(response);
|
|
154
231
|
return;
|
|
@@ -174,7 +251,6 @@ export class FrankieClient {
|
|
|
174
251
|
}
|
|
175
252
|
}
|
|
176
253
|
|
|
177
|
-
/** Parse the server's `data:`-only SSE stream into typed events. */
|
|
178
254
|
export async function* readEventStream(
|
|
179
255
|
body: ReadableStream<Uint8Array>,
|
|
180
256
|
): AsyncGenerator<FrankieServerEvent> {
|
package/src/lib/messages.ts
CHANGED
|
@@ -8,10 +8,17 @@ export interface ToolActivity {
|
|
|
8
8
|
detail?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** A file shown on a user message (name/type only — the bytes aren't kept in the transcript). */
|
|
12
|
+
export interface MessageAttachment {
|
|
13
|
+
name: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export interface UserChatMessage {
|
|
12
18
|
id: string;
|
|
13
19
|
role: "user";
|
|
14
20
|
text: string;
|
|
21
|
+
attachments?: MessageAttachment[];
|
|
15
22
|
createdAt: number;
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -116,6 +123,6 @@ export function newAssistantMessage(id: string): AssistantChatMessage {
|
|
|
116
123
|
return { id, role: "assistant", text: "", thinking: "", tools: [], status: "streaming", createdAt: Date.now() };
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
export function newUserMessage(id: string, text: string): UserChatMessage {
|
|
120
|
-
return { id, role: "user", text, createdAt: Date.now() };
|
|
126
|
+
export function newUserMessage(id: string, text: string, attachments?: MessageAttachment[]): UserChatMessage {
|
|
127
|
+
return { id, role: "user", text, ...(attachments?.length ? { attachments } : {}), createdAt: Date.now() };
|
|
121
128
|
}
|
package/src/styles/frankie.css
CHANGED
|
@@ -7,15 +7,23 @@
|
|
|
7
7
|
* a CSS variable scoped to `.frankie-root`, so a host can restyle the widget
|
|
8
8
|
* without touching this file.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* widget then compiles against their design tokens.
|
|
10
|
+
* This stylesheet is self-contained. Widget utilities are compiled ahead of
|
|
11
|
+
* time and prefixed with `.frankie-root`, so they cannot style the host app.
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
@layer theme, components, utilities;
|
|
16
15
|
|
|
17
16
|
@import "tailwindcss/theme.css" layer(theme);
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The widget is embedded inside arbitrary applications. Native CSS scoping
|
|
20
|
+
* keeps Tailwind's generated utilities inside the widget instead of relying on
|
|
21
|
+
* Tailwind's invalid selector-important import syntax (which made the whole
|
|
22
|
+
* utility layer disappear in production).
|
|
23
|
+
*/
|
|
24
|
+
@scope (.frankie-root) {
|
|
25
|
+
@tailwind utilities;
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
@source "../";
|
|
21
29
|
|
package/src/widget.test.tsx
CHANGED
|
@@ -16,6 +16,10 @@ const config: PublicWidgetConfig = {
|
|
|
16
16
|
theme: { mode: "light" },
|
|
17
17
|
allowAnonymous: true,
|
|
18
18
|
showToolActivity: true,
|
|
19
|
+
features: {
|
|
20
|
+
attachments: { enabled: false, accept: ["image/*"], maxMb: 10, maxCount: 4 },
|
|
21
|
+
modelPicker: { enabled: false, models: [] },
|
|
22
|
+
},
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
function sseStream(events: FrankieServerEvent[]): ReadableStream<Uint8Array> {
|