@frankieai/react 0.1.0
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 +2 -0
- package/dist/index.cjs +1065 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +310 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +1003 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/components/chat/ChatPanel.tsx +147 -0
- package/src/components/chat/Composer.tsx +75 -0
- package/src/components/chat/FrankieInline.tsx +35 -0
- package/src/components/chat/FrankieWidget.tsx +88 -0
- package/src/components/chat/Markdown.tsx +136 -0
- package/src/components/chat/MessageBubble.tsx +70 -0
- package/src/components/chat/SuggestedPrompts.tsx +31 -0
- package/src/components/chat/ToolActivityList.tsx +52 -0
- package/src/components/ui/avatar.tsx +30 -0
- package/src/components/ui/badge.tsx +28 -0
- package/src/components/ui/button.tsx +39 -0
- package/src/components/ui/card.tsx +36 -0
- package/src/components/ui/scroll-area.tsx +15 -0
- package/src/components/ui/separator.tsx +7 -0
- package/src/components/ui/textarea.tsx +20 -0
- package/src/hooks/FrankieProvider.tsx +94 -0
- package/src/hooks/useFrankieChat.ts +148 -0
- package/src/index.ts +34 -0
- package/src/lib/client.ts +217 -0
- package/src/lib/messages.ts +121 -0
- package/src/lib/theme.ts +30 -0
- package/src/lib/utils.ts +7 -0
- package/src/styles/frankie.css +158 -0
- package/src/widget.test.tsx +217 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
Avatar: () => Avatar,
|
|
25
|
+
Badge: () => Badge,
|
|
26
|
+
Button: () => Button,
|
|
27
|
+
Card: () => Card,
|
|
28
|
+
CardContent: () => CardContent,
|
|
29
|
+
CardDescription: () => CardDescription,
|
|
30
|
+
CardFooter: () => CardFooter,
|
|
31
|
+
CardHeader: () => CardHeader,
|
|
32
|
+
CardTitle: () => CardTitle,
|
|
33
|
+
ChatPanel: () => ChatPanel,
|
|
34
|
+
Composer: () => Composer,
|
|
35
|
+
FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
|
|
36
|
+
FrankieApiError: () => FrankieApiError,
|
|
37
|
+
FrankieClient: () => FrankieClient,
|
|
38
|
+
FrankieInline: () => FrankieInline,
|
|
39
|
+
FrankieProvider: () => FrankieProvider,
|
|
40
|
+
FrankieWidget: () => FrankieWidget,
|
|
41
|
+
Markdown: () => Markdown,
|
|
42
|
+
MessageBubble: () => MessageBubble,
|
|
43
|
+
ScrollArea: () => ScrollArea,
|
|
44
|
+
Separator: () => Separator,
|
|
45
|
+
SuggestedPrompts: () => SuggestedPrompts,
|
|
46
|
+
Textarea: () => Textarea,
|
|
47
|
+
ToolActivityList: () => ToolActivityList,
|
|
48
|
+
ToolActivityRow: () => ToolActivityRow,
|
|
49
|
+
TypingDots: () => TypingDots,
|
|
50
|
+
applyEvent: () => applyEvent,
|
|
51
|
+
buttonVariants: () => buttonVariants,
|
|
52
|
+
cn: () => cn,
|
|
53
|
+
newAssistantMessage: () => newAssistantMessage,
|
|
54
|
+
newUserMessage: () => newUserMessage,
|
|
55
|
+
readEventStream: () => readEventStream,
|
|
56
|
+
resolveMode: () => resolveMode,
|
|
57
|
+
themeToCssVars: () => themeToCssVars,
|
|
58
|
+
useFrankie: () => useFrankie,
|
|
59
|
+
useFrankieChat: () => useFrankieChat,
|
|
60
|
+
useFrankieTheme: () => useFrankieTheme
|
|
61
|
+
});
|
|
62
|
+
module.exports = __toCommonJS(index_exports);
|
|
63
|
+
|
|
64
|
+
// src/hooks/FrankieProvider.tsx
|
|
65
|
+
var import_react = require("react");
|
|
66
|
+
|
|
67
|
+
// src/lib/client.ts
|
|
68
|
+
var FRANKIE_CLOUD_URL = "https://frankie-data-gq5o5syq2q-uc.a.run.app";
|
|
69
|
+
var FrankieApiError = class extends Error {
|
|
70
|
+
constructor(message, status, code) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.status = status;
|
|
73
|
+
this.code = code;
|
|
74
|
+
}
|
|
75
|
+
status;
|
|
76
|
+
code;
|
|
77
|
+
};
|
|
78
|
+
var FrankieClient = class {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.options = options;
|
|
81
|
+
this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
|
|
82
|
+
this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
83
|
+
this.session = this.readStored();
|
|
84
|
+
}
|
|
85
|
+
options;
|
|
86
|
+
baseUrl;
|
|
87
|
+
doFetch;
|
|
88
|
+
session;
|
|
89
|
+
get sessionId() {
|
|
90
|
+
return this.session?.sessionId;
|
|
91
|
+
}
|
|
92
|
+
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
93
|
+
get persist() {
|
|
94
|
+
return this.options.persist !== false && typeof window !== "undefined";
|
|
95
|
+
}
|
|
96
|
+
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
97
|
+
get namespace() {
|
|
98
|
+
return `frankie.${this.options.publishableKey}`;
|
|
99
|
+
}
|
|
100
|
+
readStored() {
|
|
101
|
+
if (!this.persist) return void 0;
|
|
102
|
+
try {
|
|
103
|
+
const raw = window.localStorage.getItem(`${this.namespace}.session`);
|
|
104
|
+
if (!raw) return void 0;
|
|
105
|
+
const session = JSON.parse(raw);
|
|
106
|
+
return new Date(session.expiresAt).getTime() > Date.now() + 3e4 ? session : void 0;
|
|
107
|
+
} catch {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
writeStored() {
|
|
112
|
+
if (!this.persist) return;
|
|
113
|
+
try {
|
|
114
|
+
if (this.session) window.localStorage.setItem(`${this.namespace}.session`, JSON.stringify(this.session));
|
|
115
|
+
else window.localStorage.removeItem(`${this.namespace}.session`);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async getConfig(signal) {
|
|
120
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/config`, {
|
|
121
|
+
headers: { "x-frankie-key": this.options.publishableKey },
|
|
122
|
+
...signal ? { signal } : {}
|
|
123
|
+
});
|
|
124
|
+
return await this.unwrap(response);
|
|
125
|
+
}
|
|
126
|
+
/** Opens (or reuses) a chat session. */
|
|
127
|
+
async ensureSession(context) {
|
|
128
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) {
|
|
129
|
+
return this.session;
|
|
130
|
+
}
|
|
131
|
+
const userToken = await this.options.getUserToken?.();
|
|
132
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
135
|
+
body: JSON.stringify({ ...userToken ? { userToken } : {}, ...context ? { context } : {} })
|
|
136
|
+
});
|
|
137
|
+
this.session = await this.unwrap(response);
|
|
138
|
+
this.writeStored();
|
|
139
|
+
return this.session;
|
|
140
|
+
}
|
|
141
|
+
/** Forget the current session so the next send starts a fresh conversation. */
|
|
142
|
+
resetSession() {
|
|
143
|
+
this.session = void 0;
|
|
144
|
+
this.writeStored();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
148
|
+
* Resolves when the turn ends.
|
|
149
|
+
*/
|
|
150
|
+
async send(input, onEvent) {
|
|
151
|
+
const session = await this.ensureSession(input.context);
|
|
152
|
+
const body = {
|
|
153
|
+
sessionId: session.sessionId,
|
|
154
|
+
message: input.message,
|
|
155
|
+
...input.context ? { context: input.context } : {}
|
|
156
|
+
};
|
|
157
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/chat`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"content-type": "application/json",
|
|
161
|
+
"x-frankie-key": this.options.publishableKey,
|
|
162
|
+
authorization: `Bearer ${session.sessionToken}`
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify(body),
|
|
165
|
+
...input.signal ? { signal: input.signal } : {}
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok || !response.body) {
|
|
168
|
+
if (response.status === 401 || response.status === 404) this.resetSession();
|
|
169
|
+
await this.unwrap(response);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for await (const event of readEventStream(response.body)) {
|
|
173
|
+
onEvent(event);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async unwrap(response) {
|
|
177
|
+
if (response.ok) return response.json();
|
|
178
|
+
let code = "internal";
|
|
179
|
+
let message = `Request failed with status ${response.status}.`;
|
|
180
|
+
try {
|
|
181
|
+
const body = await response.json();
|
|
182
|
+
code = body.error?.code ?? code;
|
|
183
|
+
message = body.error?.message ?? message;
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
throw new FrankieApiError(message, response.status, code);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
async function* readEventStream(body) {
|
|
190
|
+
const reader = body.getReader();
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
let buffer = "";
|
|
193
|
+
try {
|
|
194
|
+
for (; ; ) {
|
|
195
|
+
const { done, value } = await reader.read();
|
|
196
|
+
if (done) break;
|
|
197
|
+
buffer += decoder.decode(value, { stream: true });
|
|
198
|
+
let split = buffer.indexOf("\n\n");
|
|
199
|
+
while (split !== -1) {
|
|
200
|
+
const frame = buffer.slice(0, split);
|
|
201
|
+
buffer = buffer.slice(split + 2);
|
|
202
|
+
const event = parseFrame(frame);
|
|
203
|
+
if (event) yield event;
|
|
204
|
+
split = buffer.indexOf("\n\n");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
reader.releaseLock();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function parseFrame(frame) {
|
|
212
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
213
|
+
if (!data) return null;
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(data);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/hooks/FrankieProvider.tsx
|
|
222
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
223
|
+
var FrankieContext = (0, import_react.createContext)(null);
|
|
224
|
+
function FrankieProvider({
|
|
225
|
+
children,
|
|
226
|
+
context,
|
|
227
|
+
theme,
|
|
228
|
+
config: providedConfig,
|
|
229
|
+
...clientOptions
|
|
230
|
+
}) {
|
|
231
|
+
const client = (0, import_react.useMemo)(
|
|
232
|
+
() => new FrankieClient(clientOptions),
|
|
233
|
+
// The client holds session state; rebuild it only when the endpoint changes.
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
[clientOptions.baseUrl, clientOptions.publishableKey]
|
|
236
|
+
);
|
|
237
|
+
const [config, setConfig] = (0, import_react.useState)(providedConfig);
|
|
238
|
+
const [configError, setConfigError] = (0, import_react.useState)(void 0);
|
|
239
|
+
const [loading, setLoading] = (0, import_react.useState)(!providedConfig);
|
|
240
|
+
(0, import_react.useEffect)(() => {
|
|
241
|
+
if (providedConfig) return;
|
|
242
|
+
const abort = new AbortController();
|
|
243
|
+
setLoading(true);
|
|
244
|
+
client.getConfig(abort.signal).then((next) => {
|
|
245
|
+
setConfig(next);
|
|
246
|
+
setConfigError(void 0);
|
|
247
|
+
}).catch((error) => {
|
|
248
|
+
if (abort.signal.aborted) return;
|
|
249
|
+
setConfigError(error instanceof Error ? error.message : "Could not load the assistant.");
|
|
250
|
+
}).finally(() => {
|
|
251
|
+
if (!abort.signal.aborted) setLoading(false);
|
|
252
|
+
});
|
|
253
|
+
return () => abort.abort();
|
|
254
|
+
}, [client, providedConfig]);
|
|
255
|
+
const value = (0, import_react.useMemo)(
|
|
256
|
+
() => ({
|
|
257
|
+
client,
|
|
258
|
+
...config ? { config } : {},
|
|
259
|
+
...configError ? { configError } : {},
|
|
260
|
+
loading,
|
|
261
|
+
...context ? { context } : {},
|
|
262
|
+
...theme ? { theme } : {}
|
|
263
|
+
}),
|
|
264
|
+
[client, config, configError, loading, context, theme]
|
|
265
|
+
);
|
|
266
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrankieContext.Provider, { value, children });
|
|
267
|
+
}
|
|
268
|
+
function useFrankie() {
|
|
269
|
+
const value = (0, import_react.useContext)(FrankieContext);
|
|
270
|
+
if (!value) throw new Error("Frankie components must be rendered inside <FrankieProvider>.");
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function useFrankieTheme() {
|
|
274
|
+
const { config, theme } = useFrankie();
|
|
275
|
+
return (0, import_react.useMemo)(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/hooks/useFrankieChat.ts
|
|
279
|
+
var import_react2 = require("react");
|
|
280
|
+
|
|
281
|
+
// src/lib/messages.ts
|
|
282
|
+
function applyEvent(message, event) {
|
|
283
|
+
switch (event.type) {
|
|
284
|
+
case "text_delta":
|
|
285
|
+
return { ...message, text: message.text + event.text };
|
|
286
|
+
case "thinking_delta":
|
|
287
|
+
return { ...message, thinking: message.thinking + event.text };
|
|
288
|
+
case "tool_start":
|
|
289
|
+
return {
|
|
290
|
+
...message,
|
|
291
|
+
tools: [
|
|
292
|
+
...message.tools,
|
|
293
|
+
{ id: event.toolUseId, name: event.name, label: event.label, status: "running" }
|
|
294
|
+
]
|
|
295
|
+
};
|
|
296
|
+
case "tool_end":
|
|
297
|
+
return {
|
|
298
|
+
...message,
|
|
299
|
+
tools: message.tools.map(
|
|
300
|
+
(tool) => tool.id === event.toolUseId ? {
|
|
301
|
+
...tool,
|
|
302
|
+
status: event.ok ? "done" : "failed",
|
|
303
|
+
...event.preview ? { detail: event.preview } : {}
|
|
304
|
+
} : tool
|
|
305
|
+
)
|
|
306
|
+
};
|
|
307
|
+
case "tool_denied":
|
|
308
|
+
return {
|
|
309
|
+
...message,
|
|
310
|
+
tools: upsertTool(message.tools, {
|
|
311
|
+
id: event.toolUseId,
|
|
312
|
+
name: event.name,
|
|
313
|
+
label: event.name,
|
|
314
|
+
status: "denied",
|
|
315
|
+
detail: event.reason
|
|
316
|
+
})
|
|
317
|
+
};
|
|
318
|
+
case "turn_end":
|
|
319
|
+
return {
|
|
320
|
+
...message,
|
|
321
|
+
status: message.status === "error" ? "error" : "done",
|
|
322
|
+
// A turn that produced nothing still needs to say something.
|
|
323
|
+
text: message.text || fallbackText(event.stopReason)
|
|
324
|
+
};
|
|
325
|
+
case "error":
|
|
326
|
+
return { ...message, status: "error", error: event.message };
|
|
327
|
+
default:
|
|
328
|
+
return message;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function upsertTool(tools, next) {
|
|
332
|
+
return tools.some((tool) => tool.id === next.id) ? tools.map((tool) => tool.id === next.id ? { ...tool, ...next } : tool) : [...tools, next];
|
|
333
|
+
}
|
|
334
|
+
function fallbackText(stopReason) {
|
|
335
|
+
switch (stopReason) {
|
|
336
|
+
case "max_turns":
|
|
337
|
+
return "I got stuck working on that. Could you narrow it down a little?";
|
|
338
|
+
case "aborted":
|
|
339
|
+
return "Stopped.";
|
|
340
|
+
case "error":
|
|
341
|
+
return "Something went wrong before I could answer. Try again?";
|
|
342
|
+
default:
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function newAssistantMessage(id) {
|
|
347
|
+
return { id, role: "assistant", text: "", thinking: "", tools: [], status: "streaming", createdAt: Date.now() };
|
|
348
|
+
}
|
|
349
|
+
function newUserMessage(id, text) {
|
|
350
|
+
return { id, role: "user", text, createdAt: Date.now() };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/hooks/useFrankieChat.ts
|
|
354
|
+
var counter = 0;
|
|
355
|
+
var nextId = (prefix) => `${prefix}_${Date.now().toString(36)}_${counter += 1}`;
|
|
356
|
+
function useFrankieChat() {
|
|
357
|
+
const { client, context } = useFrankie();
|
|
358
|
+
const [messages, setMessages] = (0, import_react2.useState)([]);
|
|
359
|
+
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
360
|
+
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
361
|
+
const abortRef = (0, import_react2.useRef)(null);
|
|
362
|
+
const mounted = (0, import_react2.useRef)(true);
|
|
363
|
+
(0, import_react2.useEffect)(() => {
|
|
364
|
+
mounted.current = true;
|
|
365
|
+
return () => {
|
|
366
|
+
mounted.current = false;
|
|
367
|
+
abortRef.current?.abort();
|
|
368
|
+
};
|
|
369
|
+
}, []);
|
|
370
|
+
const storageKey = `${client.namespace}.messages`;
|
|
371
|
+
(0, import_react2.useEffect)(() => {
|
|
372
|
+
if (!client.persist) return;
|
|
373
|
+
try {
|
|
374
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
375
|
+
const stored = raw ? JSON.parse(raw) : [];
|
|
376
|
+
if (Array.isArray(stored) && stored.length) setMessages(stored);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}, [storageKey]);
|
|
380
|
+
(0, import_react2.useEffect)(() => {
|
|
381
|
+
if (!client.persist || busy) return;
|
|
382
|
+
try {
|
|
383
|
+
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
384
|
+
else window.localStorage.removeItem(storageKey);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}, [messages, busy, client, storageKey]);
|
|
388
|
+
const patchAssistant = (0, import_react2.useCallback)((id, update) => {
|
|
389
|
+
setMessages(
|
|
390
|
+
(current) => current.map(
|
|
391
|
+
(message) => message.id === id && message.role === "assistant" ? update(message) : message
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
}, []);
|
|
395
|
+
const send = (0, import_react2.useCallback)(
|
|
396
|
+
async (text) => {
|
|
397
|
+
const trimmed = text.trim();
|
|
398
|
+
if (!trimmed || busy) return;
|
|
399
|
+
const assistantId = nextId("a");
|
|
400
|
+
setError(void 0);
|
|
401
|
+
setBusy(true);
|
|
402
|
+
setMessages((current) => [
|
|
403
|
+
...current,
|
|
404
|
+
newUserMessage(nextId("u"), trimmed),
|
|
405
|
+
newAssistantMessage(assistantId)
|
|
406
|
+
]);
|
|
407
|
+
const abort = new AbortController();
|
|
408
|
+
abortRef.current = abort;
|
|
409
|
+
try {
|
|
410
|
+
await client.send({ message: trimmed, ...context ? { context } : {}, signal: abort.signal }, (event) => {
|
|
411
|
+
if (event.type === "ping" || event.type === "session") return;
|
|
412
|
+
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
413
|
+
});
|
|
414
|
+
} catch (caught) {
|
|
415
|
+
if (abort.signal.aborted) {
|
|
416
|
+
patchAssistant(assistantId, (message) => ({
|
|
417
|
+
...message,
|
|
418
|
+
status: "done",
|
|
419
|
+
text: message.text || "Stopped."
|
|
420
|
+
}));
|
|
421
|
+
} else {
|
|
422
|
+
const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
423
|
+
if (mounted.current) setError(message);
|
|
424
|
+
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
if (mounted.current) {
|
|
428
|
+
setBusy(false);
|
|
429
|
+
patchAssistant(
|
|
430
|
+
assistantId,
|
|
431
|
+
(message) => message.status === "streaming" ? { ...message, status: "done" } : message
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
abortRef.current = null;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
[busy, client, context, patchAssistant]
|
|
438
|
+
);
|
|
439
|
+
const stop = (0, import_react2.useCallback)(() => abortRef.current?.abort(), []);
|
|
440
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
441
|
+
abortRef.current?.abort();
|
|
442
|
+
client.resetSession();
|
|
443
|
+
setMessages([]);
|
|
444
|
+
setError(void 0);
|
|
445
|
+
setBusy(false);
|
|
446
|
+
try {
|
|
447
|
+
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}, [client]);
|
|
451
|
+
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/components/chat/FrankieWidget.tsx
|
|
455
|
+
var import_lucide_react4 = require("lucide-react");
|
|
456
|
+
var import_react8 = require("react");
|
|
457
|
+
|
|
458
|
+
// src/lib/theme.ts
|
|
459
|
+
function themeToCssVars(theme) {
|
|
460
|
+
if (!theme) return {};
|
|
461
|
+
const vars = {};
|
|
462
|
+
if (theme.primary) vars["--frankie-primary"] = theme.primary;
|
|
463
|
+
if (theme.primaryForeground) vars["--frankie-primary-foreground"] = theme.primaryForeground;
|
|
464
|
+
if (theme.background) vars["--frankie-background"] = theme.background;
|
|
465
|
+
if (theme.foreground) vars["--frankie-foreground"] = theme.foreground;
|
|
466
|
+
if (theme.muted) vars["--frankie-muted"] = theme.muted;
|
|
467
|
+
if (theme.border) vars["--frankie-border"] = theme.border;
|
|
468
|
+
if (theme.radius) vars["--frankie-radius"] = `${theme.radius}rem`;
|
|
469
|
+
if (theme.fontFamily) vars["--frankie-font"] = theme.fontFamily;
|
|
470
|
+
return vars;
|
|
471
|
+
}
|
|
472
|
+
function resolveMode(mode) {
|
|
473
|
+
if (mode === "light" || mode === "dark") return mode;
|
|
474
|
+
if (typeof window === "undefined" || !window.matchMedia) return "light";
|
|
475
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/lib/utils.ts
|
|
479
|
+
var import_clsx = require("clsx");
|
|
480
|
+
var import_tailwind_merge = require("tailwind-merge");
|
|
481
|
+
function cn(...inputs) {
|
|
482
|
+
return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/components/ui/button.tsx
|
|
486
|
+
var import_react_slot = require("@radix-ui/react-slot");
|
|
487
|
+
var import_class_variance_authority = require("class-variance-authority");
|
|
488
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
489
|
+
var buttonVariants = (0, import_class_variance_authority.cva)(
|
|
490
|
+
"frankie-focus inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--frankie-radius)] text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
491
|
+
{
|
|
492
|
+
variants: {
|
|
493
|
+
variant: {
|
|
494
|
+
default: "bg-primary text-primary-foreground hover:opacity-90",
|
|
495
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-muted",
|
|
496
|
+
outline: "border border-border bg-background hover:bg-muted",
|
|
497
|
+
ghost: "hover:bg-muted",
|
|
498
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
499
|
+
},
|
|
500
|
+
size: {
|
|
501
|
+
default: "h-9 px-4 py-2",
|
|
502
|
+
sm: "h-8 rounded-[calc(var(--frankie-radius)-2px)] px-3 text-xs",
|
|
503
|
+
lg: "h-10 px-6",
|
|
504
|
+
icon: "size-9"
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
defaultVariants: { variant: "default", size: "default" }
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
function Button({ className, variant, size, asChild = false, ...props }) {
|
|
511
|
+
const Comp = asChild ? import_react_slot.Slot : "button";
|
|
512
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Comp, { className: cn(buttonVariants({ variant, size }), className), ...props });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/components/chat/ChatPanel.tsx
|
|
516
|
+
var import_lucide_react3 = require("lucide-react");
|
|
517
|
+
var import_react7 = require("react");
|
|
518
|
+
|
|
519
|
+
// src/components/ui/avatar.tsx
|
|
520
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
521
|
+
function Avatar({ src, name, className }) {
|
|
522
|
+
const initials = name.split(/\s+/).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
|
|
523
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
524
|
+
"span",
|
|
525
|
+
{
|
|
526
|
+
className: cn(
|
|
527
|
+
"inline-flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary text-[0.625rem] font-semibold text-primary-foreground",
|
|
528
|
+
className
|
|
529
|
+
),
|
|
530
|
+
"aria-hidden": "true",
|
|
531
|
+
children: src ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src, alt: "", className: "size-full object-cover" }) : initials
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/components/ui/card.tsx
|
|
537
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
538
|
+
function Card({ className, ...props }) {
|
|
539
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
540
|
+
"div",
|
|
541
|
+
{
|
|
542
|
+
className: cn(
|
|
543
|
+
"flex flex-col rounded-[calc(var(--frankie-radius)+4px)] border border-border bg-background text-foreground shadow-lg",
|
|
544
|
+
className
|
|
545
|
+
),
|
|
546
|
+
...props
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
function CardHeader({ className, ...props }) {
|
|
551
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("flex items-center gap-3 border-b border-border px-4 py-3", className), ...props });
|
|
552
|
+
}
|
|
553
|
+
function CardTitle({ className, ...props }) {
|
|
554
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("text-sm font-semibold leading-tight", className), ...props });
|
|
555
|
+
}
|
|
556
|
+
function CardDescription({ className, ...props }) {
|
|
557
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("text-xs text-muted-foreground", className), ...props });
|
|
558
|
+
}
|
|
559
|
+
function CardContent({ className, ...props }) {
|
|
560
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("flex-1 overflow-hidden", className), ...props });
|
|
561
|
+
}
|
|
562
|
+
function CardFooter({ className, ...props }) {
|
|
563
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("border-t border-border p-3", className), ...props });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/components/ui/scroll-area.tsx
|
|
567
|
+
var import_react3 = require("react");
|
|
568
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
569
|
+
var ScrollArea = (0, import_react3.forwardRef)(
|
|
570
|
+
function ScrollArea2({ className, ...props }, ref) {
|
|
571
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref, className: cn("frankie-scroll overflow-y-auto overscroll-contain", className), ...props });
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// src/components/chat/Composer.tsx
|
|
576
|
+
var import_lucide_react = require("lucide-react");
|
|
577
|
+
var import_react5 = require("react");
|
|
578
|
+
|
|
579
|
+
// src/components/ui/textarea.tsx
|
|
580
|
+
var import_react4 = require("react");
|
|
581
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
582
|
+
var Textarea = (0, import_react4.forwardRef)(
|
|
583
|
+
function Textarea2({ className, ...props }, ref) {
|
|
584
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
585
|
+
"textarea",
|
|
586
|
+
{
|
|
587
|
+
ref,
|
|
588
|
+
className: cn(
|
|
589
|
+
"frankie-focus flex min-h-[2.25rem] w-full resize-none rounded-[var(--frankie-radius)] border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
590
|
+
className
|
|
591
|
+
),
|
|
592
|
+
...props
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// src/components/chat/Composer.tsx
|
|
599
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
600
|
+
var MAX_ROWS_PX = 140;
|
|
601
|
+
function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus }) {
|
|
602
|
+
const [value, setValue] = (0, import_react5.useState)("");
|
|
603
|
+
const textareaRef = (0, import_react5.useRef)(null);
|
|
604
|
+
(0, import_react5.useEffect)(() => {
|
|
605
|
+
const node = textareaRef.current;
|
|
606
|
+
if (!node) return;
|
|
607
|
+
node.style.height = "auto";
|
|
608
|
+
node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
|
|
609
|
+
}, [value]);
|
|
610
|
+
const submit = (0, import_react5.useCallback)(() => {
|
|
611
|
+
const text = value.trim();
|
|
612
|
+
if (!text || busy || disabled) return;
|
|
613
|
+
setValue("");
|
|
614
|
+
onSend(text);
|
|
615
|
+
}, [busy, disabled, onSend, value]);
|
|
616
|
+
const onKeyDown = (event) => {
|
|
617
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
618
|
+
event.preventDefault();
|
|
619
|
+
submit();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
623
|
+
"form",
|
|
624
|
+
{
|
|
625
|
+
className: "flex items-end gap-2",
|
|
626
|
+
onSubmit: (event) => {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
submit();
|
|
629
|
+
},
|
|
630
|
+
children: [
|
|
631
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
632
|
+
Textarea,
|
|
633
|
+
{
|
|
634
|
+
ref: textareaRef,
|
|
635
|
+
rows: 1,
|
|
636
|
+
value,
|
|
637
|
+
disabled,
|
|
638
|
+
autoFocus,
|
|
639
|
+
onChange: (event) => setValue(event.target.value),
|
|
640
|
+
onKeyDown,
|
|
641
|
+
placeholder: placeholder ?? "Ask anything\u2026",
|
|
642
|
+
"aria-label": "Message"
|
|
643
|
+
}
|
|
644
|
+
),
|
|
645
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.Square, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { type: "submit", size: "icon", disabled: disabled || value.trim().length === 0, "aria-label": "Send", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react.ArrowUp, {}) })
|
|
646
|
+
]
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/components/chat/Markdown.tsx
|
|
652
|
+
var import_react6 = require("react");
|
|
653
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
654
|
+
function Markdown({ text, className }) {
|
|
655
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
|
|
656
|
+
}
|
|
657
|
+
function renderBlocks(text) {
|
|
658
|
+
const lines = text.split("\n");
|
|
659
|
+
const blocks = [];
|
|
660
|
+
let paragraph = [];
|
|
661
|
+
let list = null;
|
|
662
|
+
let code = null;
|
|
663
|
+
const flushParagraph = () => {
|
|
664
|
+
if (paragraph.length === 0) return;
|
|
665
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
|
|
666
|
+
paragraph = [];
|
|
667
|
+
};
|
|
668
|
+
const flushList = () => {
|
|
669
|
+
if (!list) return;
|
|
670
|
+
const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("li", { children: renderInline(item) }, index));
|
|
671
|
+
blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { children: items }, `l${blocks.length}`));
|
|
672
|
+
list = null;
|
|
673
|
+
};
|
|
674
|
+
for (const line of lines) {
|
|
675
|
+
const fence = /^```(\w*)\s*$/.exec(line);
|
|
676
|
+
if (fence) {
|
|
677
|
+
if (code) {
|
|
678
|
+
blocks.push(
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
680
|
+
);
|
|
681
|
+
code = null;
|
|
682
|
+
} else {
|
|
683
|
+
flushParagraph();
|
|
684
|
+
flushList();
|
|
685
|
+
code = { lang: fence[1] ?? "", lines: [] };
|
|
686
|
+
}
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (code) {
|
|
690
|
+
code.lines.push(line);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const bullet = /^\s*[-*+]\s+(.*)$/.exec(line);
|
|
694
|
+
const ordered = /^\s*\d+[.)]\s+(.*)$/.exec(line);
|
|
695
|
+
if (bullet ?? ordered) {
|
|
696
|
+
flushParagraph();
|
|
697
|
+
const isOrdered = Boolean(ordered);
|
|
698
|
+
if (!list || list.ordered !== isOrdered) {
|
|
699
|
+
flushList();
|
|
700
|
+
list = { ordered: isOrdered, items: [] };
|
|
701
|
+
}
|
|
702
|
+
list.items.push(ordered?.[1] ?? bullet?.[1] ?? "");
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (line.trim() === "") {
|
|
706
|
+
flushParagraph();
|
|
707
|
+
flushList();
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
711
|
+
if (heading) {
|
|
712
|
+
flushParagraph();
|
|
713
|
+
flushList();
|
|
714
|
+
blocks.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
flushList();
|
|
718
|
+
paragraph.push(line.trim());
|
|
719
|
+
}
|
|
720
|
+
if (code) {
|
|
721
|
+
blocks.push(
|
|
722
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
flushParagraph();
|
|
726
|
+
flushList();
|
|
727
|
+
return blocks;
|
|
728
|
+
}
|
|
729
|
+
var INLINE = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\((?:https?:\/\/|\/)[^)\s]+\)|\bhttps?:\/\/[^\s<>()]+)/g;
|
|
730
|
+
function renderInline(text) {
|
|
731
|
+
const nodes = [];
|
|
732
|
+
let lastIndex = 0;
|
|
733
|
+
for (const match of text.matchAll(INLINE)) {
|
|
734
|
+
const token = match[0];
|
|
735
|
+
const start = match.index;
|
|
736
|
+
if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
|
|
737
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
|
|
738
|
+
lastIndex = start + token.length;
|
|
739
|
+
}
|
|
740
|
+
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
741
|
+
return nodes;
|
|
742
|
+
}
|
|
743
|
+
function renderToken(token) {
|
|
744
|
+
if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("strong", { children: token.slice(2, -2) });
|
|
745
|
+
if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: token.slice(1, -1) });
|
|
746
|
+
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
747
|
+
const href = link ? link[2] ?? "" : token;
|
|
748
|
+
const label = link ? link[1] ?? "" : token;
|
|
749
|
+
if (!/^(https?:\/\/|\/)/.test(href)) return token;
|
|
750
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/components/chat/ToolActivityList.tsx
|
|
754
|
+
var import_lucide_react2 = require("lucide-react");
|
|
755
|
+
|
|
756
|
+
// src/components/ui/badge.tsx
|
|
757
|
+
var import_class_variance_authority2 = require("class-variance-authority");
|
|
758
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
759
|
+
var badgeVariants = (0, import_class_variance_authority2.cva)(
|
|
760
|
+
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
|
|
761
|
+
{
|
|
762
|
+
variants: {
|
|
763
|
+
variant: {
|
|
764
|
+
default: "border-transparent bg-muted text-muted-foreground",
|
|
765
|
+
success: "border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
|
766
|
+
danger: "border-transparent bg-red-500/10 text-red-600 dark:text-red-400",
|
|
767
|
+
outline: "border-border text-muted-foreground"
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
defaultVariants: { variant: "default" }
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
function Badge({
|
|
774
|
+
className,
|
|
775
|
+
variant,
|
|
776
|
+
...props
|
|
777
|
+
}) {
|
|
778
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/components/chat/ToolActivityList.tsx
|
|
782
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
783
|
+
function ToolActivityList({ tools, renderTool }) {
|
|
784
|
+
if (tools.length === 0) return null;
|
|
785
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "mt-2 flex flex-col gap-1", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolActivityRow, { tool }) }, tool.id)) });
|
|
786
|
+
}
|
|
787
|
+
function ToolActivityRow({ tool }) {
|
|
788
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolIcon, { status: tool.status }),
|
|
790
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "truncate", children: tool.label })
|
|
791
|
+
] });
|
|
792
|
+
}
|
|
793
|
+
function ToolIcon({ status }) {
|
|
794
|
+
switch (status) {
|
|
795
|
+
case "running":
|
|
796
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
|
|
797
|
+
case "done":
|
|
798
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.Check, { className: "size-3", "aria-hidden": "true" });
|
|
799
|
+
case "denied":
|
|
800
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
|
|
801
|
+
default:
|
|
802
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.X, { className: "size-3", "aria-hidden": "true" });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function badgeVariant(status) {
|
|
806
|
+
if (status === "done") return "success";
|
|
807
|
+
if (status === "failed") return "danger";
|
|
808
|
+
if (status === "denied") return "outline";
|
|
809
|
+
return "default";
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/components/chat/MessageBubble.tsx
|
|
813
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
814
|
+
function MessageBubble({
|
|
815
|
+
message,
|
|
816
|
+
assistantName,
|
|
817
|
+
avatarUrl,
|
|
818
|
+
showToolActivity,
|
|
819
|
+
renderTool
|
|
820
|
+
}) {
|
|
821
|
+
if (message.role === "user") {
|
|
822
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "max-w-[85%] rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }) });
|
|
823
|
+
}
|
|
824
|
+
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
825
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
|
|
826
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName, className: "mt-0.5" }),
|
|
827
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "min-w-0 max-w-[85%]", children: [
|
|
828
|
+
message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text, className: "text-sm" }),
|
|
829
|
+
showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
830
|
+
] })
|
|
831
|
+
] });
|
|
832
|
+
}
|
|
833
|
+
function TypingDots() {
|
|
834
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
835
|
+
"span",
|
|
836
|
+
{
|
|
837
|
+
className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
|
|
838
|
+
style: { animationDelay: `${index * 150}ms` }
|
|
839
|
+
},
|
|
840
|
+
index
|
|
841
|
+
)) });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/components/chat/SuggestedPrompts.tsx
|
|
845
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
846
|
+
function SuggestedPrompts({
|
|
847
|
+
prompts,
|
|
848
|
+
onPick,
|
|
849
|
+
disabled
|
|
850
|
+
}) {
|
|
851
|
+
if (prompts.length === 0) return null;
|
|
852
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
853
|
+
Button,
|
|
854
|
+
{
|
|
855
|
+
type: "button",
|
|
856
|
+
size: "sm",
|
|
857
|
+
variant: "outline",
|
|
858
|
+
disabled,
|
|
859
|
+
onClick: () => onPick(prompt),
|
|
860
|
+
children: prompt
|
|
861
|
+
},
|
|
862
|
+
prompt
|
|
863
|
+
)) });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/components/chat/ChatPanel.tsx
|
|
867
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
868
|
+
function ChatPanel({
|
|
869
|
+
className,
|
|
870
|
+
header,
|
|
871
|
+
footer,
|
|
872
|
+
emptyState,
|
|
873
|
+
renderMessage,
|
|
874
|
+
renderTool,
|
|
875
|
+
placeholder,
|
|
876
|
+
onClose,
|
|
877
|
+
autoFocus,
|
|
878
|
+
fill
|
|
879
|
+
}) {
|
|
880
|
+
const { config, configError, loading } = useFrankie();
|
|
881
|
+
const theme = useFrankieTheme();
|
|
882
|
+
const { messages, busy, error, send, stop, reset } = useFrankieChat();
|
|
883
|
+
const scrollRef = (0, import_react7.useRef)(null);
|
|
884
|
+
(0, import_react7.useEffect)(() => {
|
|
885
|
+
const node = scrollRef.current;
|
|
886
|
+
if (!node) return;
|
|
887
|
+
const distanceFromBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
|
|
888
|
+
if (distanceFromBottom < 160) node.scrollTop = node.scrollHeight;
|
|
889
|
+
}, [messages]);
|
|
890
|
+
const name = config?.branding.name ?? "Assistant";
|
|
891
|
+
const mode = resolveMode(theme?.mode);
|
|
892
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
893
|
+
Card,
|
|
894
|
+
{
|
|
895
|
+
className: cn(
|
|
896
|
+
"frankie-root",
|
|
897
|
+
fill ? "h-full w-full rounded-none border-0 shadow-none" : "h-[32rem] w-[22rem] max-w-[calc(100vw-2rem)]",
|
|
898
|
+
mode === "dark" && "dark",
|
|
899
|
+
className
|
|
900
|
+
),
|
|
901
|
+
style: themeToCssVars(theme),
|
|
902
|
+
"data-frankie-mode": mode,
|
|
903
|
+
children: [
|
|
904
|
+
header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(CardHeader, { children: [
|
|
905
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
|
|
906
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
907
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardTitle, { children: name }),
|
|
908
|
+
config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardDescription, { children: config.branding.tagline }) : null
|
|
909
|
+
] }),
|
|
910
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react3.RotateCcw, {}) }),
|
|
911
|
+
onClose ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react3.X, {}) }) : null
|
|
912
|
+
] }),
|
|
913
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: configError }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
914
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
915
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
916
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-col gap-4", children: messages.map(
|
|
917
|
+
(message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
918
|
+
MessageBubble,
|
|
919
|
+
{
|
|
920
|
+
message,
|
|
921
|
+
assistantName: name,
|
|
922
|
+
...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
|
|
923
|
+
showToolActivity: config?.showToolActivity ?? true,
|
|
924
|
+
...renderTool ? { renderTool } : {}
|
|
925
|
+
},
|
|
926
|
+
message.id
|
|
927
|
+
)
|
|
928
|
+
) }) }) }),
|
|
929
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(CardFooter, { className: "flex flex-col gap-2", children: [
|
|
930
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
|
|
931
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
932
|
+
Composer,
|
|
933
|
+
{
|
|
934
|
+
onSend: send,
|
|
935
|
+
onStop: stop,
|
|
936
|
+
busy,
|
|
937
|
+
disabled: Boolean(configError),
|
|
938
|
+
...placeholder ? { placeholder } : {},
|
|
939
|
+
...autoFocus ? { autoFocus } : {}
|
|
940
|
+
}
|
|
941
|
+
) }),
|
|
942
|
+
footer
|
|
943
|
+
] })
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/components/chat/FrankieWidget.tsx
|
|
950
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
951
|
+
function FrankieWidget({
|
|
952
|
+
position = "bottom-right",
|
|
953
|
+
offset = 24,
|
|
954
|
+
defaultOpen = false,
|
|
955
|
+
open: controlledOpen,
|
|
956
|
+
onOpenChange,
|
|
957
|
+
renderLauncher,
|
|
958
|
+
...panelProps
|
|
959
|
+
}) {
|
|
960
|
+
const { config } = useFrankie();
|
|
961
|
+
const theme = useFrankieTheme();
|
|
962
|
+
const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react8.useState)(defaultOpen);
|
|
963
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
964
|
+
const setOpen = (next) => {
|
|
965
|
+
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
966
|
+
onOpenChange?.(next);
|
|
967
|
+
};
|
|
968
|
+
(0, import_react8.useEffect)(() => {
|
|
969
|
+
if (!open) return;
|
|
970
|
+
const onKeyDown = (event) => {
|
|
971
|
+
if (event.key === "Escape") setOpen(false);
|
|
972
|
+
};
|
|
973
|
+
window.addEventListener("keydown", onKeyDown);
|
|
974
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
975
|
+
}, [open]);
|
|
976
|
+
const label = config?.branding.launcherLabel ?? "Need a hand?";
|
|
977
|
+
const mode = resolveMode(theme?.mode);
|
|
978
|
+
const side = position === "bottom-right" ? { right: offset } : { left: offset };
|
|
979
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
980
|
+
"div",
|
|
981
|
+
{
|
|
982
|
+
className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
|
|
983
|
+
style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex: 2147483e3 },
|
|
984
|
+
"data-frankie-mode": mode,
|
|
985
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
|
|
986
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { ...panelProps, onClose: () => setOpen(false), autoFocus: true }) : null,
|
|
987
|
+
renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
988
|
+
Button,
|
|
989
|
+
{
|
|
990
|
+
size: "icon",
|
|
991
|
+
className: "size-12 rounded-full shadow-lg",
|
|
992
|
+
onClick: () => setOpen(!open),
|
|
993
|
+
"aria-expanded": open,
|
|
994
|
+
"aria-label": open ? "Close the assistant" : label,
|
|
995
|
+
title: label,
|
|
996
|
+
children: open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react4.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react4.MessageCircle, { className: "size-5" })
|
|
997
|
+
}
|
|
998
|
+
)
|
|
999
|
+
] })
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/components/chat/FrankieInline.tsx
|
|
1005
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1006
|
+
function FrankieInline({ className, style, ...panelProps }) {
|
|
1007
|
+
const theme = useFrankieTheme();
|
|
1008
|
+
const mode = resolveMode(theme?.mode);
|
|
1009
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1010
|
+
"div",
|
|
1011
|
+
{
|
|
1012
|
+
className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
|
|
1013
|
+
style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
|
|
1014
|
+
"data-frankie-mode": mode,
|
|
1015
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatPanel, { ...panelProps, fill: true })
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/components/ui/separator.tsx
|
|
1021
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1022
|
+
function Separator({ className }) {
|
|
1023
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1024
|
+
}
|
|
1025
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1026
|
+
0 && (module.exports = {
|
|
1027
|
+
Avatar,
|
|
1028
|
+
Badge,
|
|
1029
|
+
Button,
|
|
1030
|
+
Card,
|
|
1031
|
+
CardContent,
|
|
1032
|
+
CardDescription,
|
|
1033
|
+
CardFooter,
|
|
1034
|
+
CardHeader,
|
|
1035
|
+
CardTitle,
|
|
1036
|
+
ChatPanel,
|
|
1037
|
+
Composer,
|
|
1038
|
+
FRANKIE_CLOUD_URL,
|
|
1039
|
+
FrankieApiError,
|
|
1040
|
+
FrankieClient,
|
|
1041
|
+
FrankieInline,
|
|
1042
|
+
FrankieProvider,
|
|
1043
|
+
FrankieWidget,
|
|
1044
|
+
Markdown,
|
|
1045
|
+
MessageBubble,
|
|
1046
|
+
ScrollArea,
|
|
1047
|
+
Separator,
|
|
1048
|
+
SuggestedPrompts,
|
|
1049
|
+
Textarea,
|
|
1050
|
+
ToolActivityList,
|
|
1051
|
+
ToolActivityRow,
|
|
1052
|
+
TypingDots,
|
|
1053
|
+
applyEvent,
|
|
1054
|
+
buttonVariants,
|
|
1055
|
+
cn,
|
|
1056
|
+
newAssistantMessage,
|
|
1057
|
+
newUserMessage,
|
|
1058
|
+
readEventStream,
|
|
1059
|
+
resolveMode,
|
|
1060
|
+
themeToCssVars,
|
|
1061
|
+
useFrankie,
|
|
1062
|
+
useFrankieChat,
|
|
1063
|
+
useFrankieTheme
|
|
1064
|
+
});
|
|
1065
|
+
//# sourceMappingURL=index.cjs.map
|