@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/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