@frankieai/react 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frankie.css +1 -1
- package/dist/index.cjs +198 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +31 -18
- package/dist/index.d.ts +31 -18
- package/dist/index.js +196 -37
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/ChatPanel.tsx +6 -2
- package/src/components/chat/FrankieChatPage.tsx +53 -0
- package/src/components/chat/MessageBubble.tsx +22 -5
- package/src/components/chat/ToolActivityList.tsx +10 -6
- package/src/components/chat/message-actions.css +13 -0
- package/src/hooks/FrankieProvider.tsx +1 -9
- package/src/hooks/useFrankieChat.ts +60 -15
- package/src/index.ts +2 -0
- package/src/lib/client.ts +82 -17
- package/src/styles/frankie.css +12 -4
package/dist/index.d.cts
CHANGED
|
@@ -30,6 +30,16 @@ interface FrankieClientOptions {
|
|
|
30
30
|
* set false for an ephemeral chat that starts fresh every load.
|
|
31
31
|
*/
|
|
32
32
|
persist?: boolean;
|
|
33
|
+
/** Override localStorage namespace for session and transcript. */
|
|
34
|
+
storageNamespace?: string;
|
|
35
|
+
/** Stable id for a server-backed conversation. */
|
|
36
|
+
conversationId?: string;
|
|
37
|
+
}
|
|
38
|
+
interface ChatConversation {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
createdAt: number;
|
|
42
|
+
updatedAt: number;
|
|
33
43
|
}
|
|
34
44
|
declare class FrankieApiError extends Error {
|
|
35
45
|
readonly status: number;
|
|
@@ -43,45 +53,39 @@ declare class FrankieClient {
|
|
|
43
53
|
private session?;
|
|
44
54
|
constructor(options: FrankieClientOptions);
|
|
45
55
|
get sessionId(): string | undefined;
|
|
56
|
+
get conversationId(): string | undefined;
|
|
46
57
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
47
58
|
get persist(): boolean;
|
|
48
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
59
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
49
60
|
get namespace(): string;
|
|
50
61
|
private readStored;
|
|
51
62
|
private writeStored;
|
|
52
63
|
getConfig(signal?: AbortSignal): Promise<PublicWidgetConfig>;
|
|
53
|
-
/** Opens (or reuses) a chat session. */
|
|
54
64
|
ensureSession(context?: PageContext): Promise<CreateSessionResponse>;
|
|
55
|
-
|
|
65
|
+
listConversations(): Promise<ChatConversation[]>;
|
|
66
|
+
resumeConversation(id?: string | undefined): Promise<{
|
|
67
|
+
messages: unknown[];
|
|
68
|
+
title: string;
|
|
69
|
+
} | undefined>;
|
|
70
|
+
syncConversation(messages: unknown[], title?: string): Promise<void>;
|
|
71
|
+
deleteConversation(id: string): Promise<void>;
|
|
72
|
+
private requireUserToken;
|
|
56
73
|
resetSession(): void;
|
|
57
|
-
/**
|
|
58
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
59
|
-
* Resolves when the turn ends.
|
|
60
|
-
*/
|
|
61
74
|
send(input: {
|
|
62
75
|
message: string;
|
|
63
76
|
context?: PageContext;
|
|
64
|
-
/** Model id chosen by the user, honoured only if the workspace allows it. */
|
|
65
77
|
model?: string;
|
|
66
|
-
/** Files attached to this turn. */
|
|
67
78
|
attachments?: ChatAttachment[];
|
|
68
79
|
signal?: AbortSignal;
|
|
69
80
|
}, onEvent: (event: FrankieServerEvent) => void): Promise<void>;
|
|
70
81
|
private unwrap;
|
|
71
82
|
}
|
|
72
|
-
/** Parse the server's `data:`-only SSE stream into typed events. */
|
|
73
83
|
declare function readEventStream(body: ReadableStream<Uint8Array>): AsyncGenerator<FrankieServerEvent>;
|
|
74
84
|
|
|
75
85
|
interface FrankieProviderProps extends FrankieClientOptions {
|
|
76
86
|
children: ReactNode;
|
|
77
|
-
/**
|
|
78
|
-
* Page context sent with every turn — current route, plan tier, whatever the
|
|
79
|
-
* agent should know. Treated as data by the server, never as instructions.
|
|
80
|
-
*/
|
|
81
87
|
context?: PageContext;
|
|
82
|
-
/** Overrides for the server-side theme, so a host can restyle without a redeploy. */
|
|
83
88
|
theme?: WidgetTheme;
|
|
84
|
-
/** Skip the `/v1/config` fetch by supplying the config yourself. */
|
|
85
89
|
config?: PublicWidgetConfig;
|
|
86
90
|
}
|
|
87
91
|
interface FrankieContextValue {
|
|
@@ -94,7 +98,6 @@ interface FrankieContextValue {
|
|
|
94
98
|
}
|
|
95
99
|
declare function FrankieProvider({ children, context, theme, config: providedConfig, ...clientOptions }: FrankieProviderProps): react.JSX.Element;
|
|
96
100
|
declare function useFrankie(): FrankieContextValue;
|
|
97
|
-
/** The theme actually in effect: provider override wins over server config. */
|
|
98
101
|
declare function useFrankieTheme(): WidgetTheme | undefined;
|
|
99
102
|
|
|
100
103
|
interface ToolActivity {
|
|
@@ -215,6 +218,16 @@ interface FrankieInlineProps extends ChatPanelProps {
|
|
|
215
218
|
*/
|
|
216
219
|
declare function FrankieInline({ className, style, ...panelProps }: FrankieInlineProps): react.JSX.Element;
|
|
217
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A complete chat route in one component. This is the simplest way to add a
|
|
223
|
+
* dedicated /chat page to an app while keeping the existing floating widget.
|
|
224
|
+
*/
|
|
225
|
+
interface FrankieChatPageProps extends Omit<FrankieProviderProps, "children">, Pick<ChatPanelProps, "header" | "footer" | "emptyState" | "renderMessage" | "renderTool" | "placeholder" | "autoFocus"> {
|
|
226
|
+
className?: string;
|
|
227
|
+
style?: CSSProperties;
|
|
228
|
+
}
|
|
229
|
+
declare function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, ...providerProps }: FrankieChatPageProps): react.JSX.Element;
|
|
230
|
+
|
|
218
231
|
interface ComposerAttachmentsConfig {
|
|
219
232
|
enabled: boolean;
|
|
220
233
|
/** Accepted MIME patterns, e.g. "image/*". */
|
|
@@ -344,4 +357,4 @@ declare function resolveMode(mode: WidgetTheme["mode"]): "light" | "dark";
|
|
|
344
357
|
/** shadcn/ui's class helper: conditional classes with Tailwind conflict resolution. */
|
|
345
358
|
declare function cn(...inputs: ClassValue[]): string;
|
|
346
359
|
|
|
347
|
-
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, type MessageAttachment, MessageBubble, type PageContext, ScrollArea, type SendOptions, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
|
360
|
+
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieChatPage, type FrankieChatPageProps, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, type MessageAttachment, MessageBubble, type PageContext, ScrollArea, type SendOptions, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ interface FrankieClientOptions {
|
|
|
30
30
|
* set false for an ephemeral chat that starts fresh every load.
|
|
31
31
|
*/
|
|
32
32
|
persist?: boolean;
|
|
33
|
+
/** Override localStorage namespace for session and transcript. */
|
|
34
|
+
storageNamespace?: string;
|
|
35
|
+
/** Stable id for a server-backed conversation. */
|
|
36
|
+
conversationId?: string;
|
|
37
|
+
}
|
|
38
|
+
interface ChatConversation {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
createdAt: number;
|
|
42
|
+
updatedAt: number;
|
|
33
43
|
}
|
|
34
44
|
declare class FrankieApiError extends Error {
|
|
35
45
|
readonly status: number;
|
|
@@ -43,45 +53,39 @@ declare class FrankieClient {
|
|
|
43
53
|
private session?;
|
|
44
54
|
constructor(options: FrankieClientOptions);
|
|
45
55
|
get sessionId(): string | undefined;
|
|
56
|
+
get conversationId(): string | undefined;
|
|
46
57
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
47
58
|
get persist(): boolean;
|
|
48
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
59
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
49
60
|
get namespace(): string;
|
|
50
61
|
private readStored;
|
|
51
62
|
private writeStored;
|
|
52
63
|
getConfig(signal?: AbortSignal): Promise<PublicWidgetConfig>;
|
|
53
|
-
/** Opens (or reuses) a chat session. */
|
|
54
64
|
ensureSession(context?: PageContext): Promise<CreateSessionResponse>;
|
|
55
|
-
|
|
65
|
+
listConversations(): Promise<ChatConversation[]>;
|
|
66
|
+
resumeConversation(id?: string | undefined): Promise<{
|
|
67
|
+
messages: unknown[];
|
|
68
|
+
title: string;
|
|
69
|
+
} | undefined>;
|
|
70
|
+
syncConversation(messages: unknown[], title?: string): Promise<void>;
|
|
71
|
+
deleteConversation(id: string): Promise<void>;
|
|
72
|
+
private requireUserToken;
|
|
56
73
|
resetSession(): void;
|
|
57
|
-
/**
|
|
58
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
59
|
-
* Resolves when the turn ends.
|
|
60
|
-
*/
|
|
61
74
|
send(input: {
|
|
62
75
|
message: string;
|
|
63
76
|
context?: PageContext;
|
|
64
|
-
/** Model id chosen by the user, honoured only if the workspace allows it. */
|
|
65
77
|
model?: string;
|
|
66
|
-
/** Files attached to this turn. */
|
|
67
78
|
attachments?: ChatAttachment[];
|
|
68
79
|
signal?: AbortSignal;
|
|
69
80
|
}, onEvent: (event: FrankieServerEvent) => void): Promise<void>;
|
|
70
81
|
private unwrap;
|
|
71
82
|
}
|
|
72
|
-
/** Parse the server's `data:`-only SSE stream into typed events. */
|
|
73
83
|
declare function readEventStream(body: ReadableStream<Uint8Array>): AsyncGenerator<FrankieServerEvent>;
|
|
74
84
|
|
|
75
85
|
interface FrankieProviderProps extends FrankieClientOptions {
|
|
76
86
|
children: ReactNode;
|
|
77
|
-
/**
|
|
78
|
-
* Page context sent with every turn — current route, plan tier, whatever the
|
|
79
|
-
* agent should know. Treated as data by the server, never as instructions.
|
|
80
|
-
*/
|
|
81
87
|
context?: PageContext;
|
|
82
|
-
/** Overrides for the server-side theme, so a host can restyle without a redeploy. */
|
|
83
88
|
theme?: WidgetTheme;
|
|
84
|
-
/** Skip the `/v1/config` fetch by supplying the config yourself. */
|
|
85
89
|
config?: PublicWidgetConfig;
|
|
86
90
|
}
|
|
87
91
|
interface FrankieContextValue {
|
|
@@ -94,7 +98,6 @@ interface FrankieContextValue {
|
|
|
94
98
|
}
|
|
95
99
|
declare function FrankieProvider({ children, context, theme, config: providedConfig, ...clientOptions }: FrankieProviderProps): react.JSX.Element;
|
|
96
100
|
declare function useFrankie(): FrankieContextValue;
|
|
97
|
-
/** The theme actually in effect: provider override wins over server config. */
|
|
98
101
|
declare function useFrankieTheme(): WidgetTheme | undefined;
|
|
99
102
|
|
|
100
103
|
interface ToolActivity {
|
|
@@ -215,6 +218,16 @@ interface FrankieInlineProps extends ChatPanelProps {
|
|
|
215
218
|
*/
|
|
216
219
|
declare function FrankieInline({ className, style, ...panelProps }: FrankieInlineProps): react.JSX.Element;
|
|
217
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A complete chat route in one component. This is the simplest way to add a
|
|
223
|
+
* dedicated /chat page to an app while keeping the existing floating widget.
|
|
224
|
+
*/
|
|
225
|
+
interface FrankieChatPageProps extends Omit<FrankieProviderProps, "children">, Pick<ChatPanelProps, "header" | "footer" | "emptyState" | "renderMessage" | "renderTool" | "placeholder" | "autoFocus"> {
|
|
226
|
+
className?: string;
|
|
227
|
+
style?: CSSProperties;
|
|
228
|
+
}
|
|
229
|
+
declare function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, ...providerProps }: FrankieChatPageProps): react.JSX.Element;
|
|
230
|
+
|
|
218
231
|
interface ComposerAttachmentsConfig {
|
|
219
232
|
enabled: boolean;
|
|
220
233
|
/** Accepted MIME patterns, e.g. "image/*". */
|
|
@@ -344,4 +357,4 @@ declare function resolveMode(mode: WidgetTheme["mode"]): "light" | "dark";
|
|
|
344
357
|
/** shadcn/ui's class helper: conditional classes with Tailwind conflict resolution. */
|
|
345
358
|
declare function cn(...inputs: ClassValue[]): string;
|
|
346
359
|
|
|
347
|
-
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, type MessageAttachment, MessageBubble, type PageContext, ScrollArea, type SendOptions, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
|
360
|
+
export { type AssistantChatMessage, Avatar, Badge, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type ChatMessage, ChatPanel, type ChatPanelProps, Composer, FRANKIE_CLOUD_URL, FrankieApiError, FrankieChatPage, type FrankieChatPageProps, FrankieClient, type FrankieClientOptions, FrankieInline, type FrankieInlineProps, FrankieProvider, type FrankieProviderProps, FrankieWidget, type FrankieWidgetProps, Markdown, type MessageAttachment, MessageBubble, type PageContext, ScrollArea, type SendOptions, Separator, SuggestedPrompts, Textarea, type ToolActivity, ToolActivityList, ToolActivityRow, TypingDots, type UseFrankieChatResult, type UserChatMessage, applyEvent, buttonVariants, cn, newAssistantMessage, newUserMessage, readEventStream, resolveMode, themeToCssVars, useFrankie, useFrankieChat, useFrankieTheme };
|
package/dist/index.js
CHANGED
|
@@ -28,13 +28,16 @@ var FrankieClient = class {
|
|
|
28
28
|
get sessionId() {
|
|
29
29
|
return this.session?.sessionId;
|
|
30
30
|
}
|
|
31
|
+
get conversationId() {
|
|
32
|
+
return this.options.conversationId;
|
|
33
|
+
}
|
|
31
34
|
/** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
|
|
32
35
|
get persist() {
|
|
33
36
|
return this.options.persist !== false && typeof window !== "undefined";
|
|
34
37
|
}
|
|
35
|
-
/** localStorage namespace for this tenant, shared with the chat hook. */
|
|
38
|
+
/** localStorage namespace for this tenant/conversation, shared with the chat hook. */
|
|
36
39
|
get namespace() {
|
|
37
|
-
return `frankie.${this.options.publishableKey}`;
|
|
40
|
+
return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
|
|
38
41
|
}
|
|
39
42
|
readStored() {
|
|
40
43
|
if (!this.persist) return void 0;
|
|
@@ -62,30 +65,80 @@ var FrankieClient = class {
|
|
|
62
65
|
});
|
|
63
66
|
return await this.unwrap(response);
|
|
64
67
|
}
|
|
65
|
-
/** Opens (or reuses) a chat session. */
|
|
66
68
|
async ensureSession(context) {
|
|
67
|
-
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4)
|
|
68
|
-
return this.session;
|
|
69
|
-
}
|
|
69
|
+
if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
|
|
70
70
|
const userToken = await this.options.getUserToken?.();
|
|
71
|
+
if (this.options.getUserToken && !userToken) {
|
|
72
|
+
throw new FrankieApiError(
|
|
73
|
+
"Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
|
|
74
|
+
401,
|
|
75
|
+
"user_token_unavailable"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
71
78
|
const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
|
|
72
79
|
method: "POST",
|
|
73
80
|
headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
|
|
74
|
-
body: JSON.stringify({
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
...userToken ? { userToken } : {},
|
|
83
|
+
...context ? { context } : {},
|
|
84
|
+
...this.options.conversationId ? { conversationId: this.options.conversationId } : {}
|
|
85
|
+
})
|
|
75
86
|
});
|
|
76
87
|
this.session = await this.unwrap(response);
|
|
77
88
|
this.writeStored();
|
|
78
89
|
return this.session;
|
|
79
90
|
}
|
|
80
|
-
|
|
91
|
+
async listConversations() {
|
|
92
|
+
const userToken = await this.requireUserToken();
|
|
93
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
|
|
94
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
95
|
+
});
|
|
96
|
+
const body = await this.unwrap(response);
|
|
97
|
+
return Array.isArray(body.conversations) ? body.conversations : [];
|
|
98
|
+
}
|
|
99
|
+
async resumeConversation(id = this.options.conversationId) {
|
|
100
|
+
if (!id) return void 0;
|
|
101
|
+
const userToken = await this.requireUserToken();
|
|
102
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
105
|
+
});
|
|
106
|
+
if (response.status === 404) return void 0;
|
|
107
|
+
const body = await this.unwrap(response);
|
|
108
|
+
this.session = body.session;
|
|
109
|
+
this.writeStored();
|
|
110
|
+
return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
|
|
111
|
+
}
|
|
112
|
+
async syncConversation(messages, title) {
|
|
113
|
+
if (!this.session || !this.options.conversationId) return;
|
|
114
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
|
|
115
|
+
method: "PUT",
|
|
116
|
+
headers: {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
"x-frankie-key": this.options.publishableKey,
|
|
119
|
+
authorization: `Bearer ${this.session.sessionToken}`
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ messages, ...title ? { title } : {} })
|
|
122
|
+
});
|
|
123
|
+
await this.unwrap(response);
|
|
124
|
+
}
|
|
125
|
+
async deleteConversation(id) {
|
|
126
|
+
const userToken = await this.requireUserToken();
|
|
127
|
+
const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
|
|
128
|
+
method: "DELETE",
|
|
129
|
+
headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
|
|
130
|
+
});
|
|
131
|
+
if (response.status !== 404) await this.unwrap(response);
|
|
132
|
+
}
|
|
133
|
+
async requireUserToken() {
|
|
134
|
+
const token = await this.options.getUserToken?.();
|
|
135
|
+
if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
|
|
136
|
+
return token;
|
|
137
|
+
}
|
|
81
138
|
resetSession() {
|
|
82
139
|
this.session = void 0;
|
|
83
140
|
this.writeStored();
|
|
84
141
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Sends a message and invokes `onEvent` for each server event as it arrives.
|
|
87
|
-
* Resolves when the turn ends.
|
|
88
|
-
*/
|
|
89
142
|
async send(input, onEvent) {
|
|
90
143
|
const session = await this.ensureSession(input.context);
|
|
91
144
|
const body = {
|
|
@@ -171,9 +224,8 @@ function FrankieProvider({
|
|
|
171
224
|
}) {
|
|
172
225
|
const client = useMemo(
|
|
173
226
|
() => new FrankieClient(clientOptions),
|
|
174
|
-
// The client holds session state; rebuild it only when the endpoint changes.
|
|
175
227
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
176
|
-
[clientOptions.baseUrl, clientOptions.publishableKey]
|
|
228
|
+
[clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
|
|
177
229
|
);
|
|
178
230
|
const [config, setConfig] = useState(providedConfig);
|
|
179
231
|
const [configError, setConfigError] = useState(void 0);
|
|
@@ -299,6 +351,7 @@ function useFrankieChat() {
|
|
|
299
351
|
const [messages, setMessages] = useState2([]);
|
|
300
352
|
const [busy, setBusy] = useState2(false);
|
|
301
353
|
const [error, setError] = useState2(void 0);
|
|
354
|
+
const [restored, setRestored] = useState2(false);
|
|
302
355
|
const abortRef = useRef(null);
|
|
303
356
|
const mounted = useRef(true);
|
|
304
357
|
useEffect2(() => {
|
|
@@ -310,22 +363,38 @@ function useFrankieChat() {
|
|
|
310
363
|
}, []);
|
|
311
364
|
const storageKey = `${client.namespace}.messages`;
|
|
312
365
|
useEffect2(() => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
366
|
+
let active = true;
|
|
367
|
+
const restore = async () => {
|
|
368
|
+
if (client.persist) {
|
|
369
|
+
try {
|
|
370
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
371
|
+
const stored = raw ? JSON.parse(raw) : [];
|
|
372
|
+
if (Array.isArray(stored) && stored.length && active) setMessages(stored);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const remote = await client.resumeConversation();
|
|
378
|
+
if (active && remote?.messages.length) setMessages(remote.messages);
|
|
379
|
+
} catch {
|
|
380
|
+
} finally {
|
|
381
|
+
if (active) setRestored(true);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
void restore();
|
|
385
|
+
return () => {
|
|
386
|
+
active = false;
|
|
387
|
+
};
|
|
388
|
+
}, [client, storageKey]);
|
|
321
389
|
useEffect2(() => {
|
|
322
|
-
if (!client.persist || busy) return;
|
|
390
|
+
if (!client.persist || !restored || busy) return;
|
|
323
391
|
try {
|
|
324
392
|
if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
325
393
|
else window.localStorage.removeItem(storageKey);
|
|
326
394
|
} catch {
|
|
327
395
|
}
|
|
328
|
-
|
|
396
|
+
if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
|
|
397
|
+
}, [messages, busy, client, storageKey, restored]);
|
|
329
398
|
const patchAssistant = useCallback((id, update) => {
|
|
330
399
|
setMessages(
|
|
331
400
|
(current) => current.map(
|
|
@@ -354,6 +423,8 @@ function useFrankieChat() {
|
|
|
354
423
|
const abort = new AbortController();
|
|
355
424
|
abortRef.current = abort;
|
|
356
425
|
try {
|
|
426
|
+
let receivedText = false;
|
|
427
|
+
let receivedToolActivity = false;
|
|
357
428
|
await client.send(
|
|
358
429
|
{
|
|
359
430
|
message: outgoing,
|
|
@@ -364,9 +435,18 @@ function useFrankieChat() {
|
|
|
364
435
|
},
|
|
365
436
|
(event) => {
|
|
366
437
|
if (event.type === "ping" || event.type === "session") return;
|
|
438
|
+
if (event.type === "text_delta") receivedText = true;
|
|
439
|
+
if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
|
|
367
440
|
patchAssistant(assistantId, (message) => applyEvent(message, event));
|
|
368
441
|
}
|
|
369
442
|
);
|
|
443
|
+
if (!receivedText) {
|
|
444
|
+
patchAssistant(assistantId, (message) => ({
|
|
445
|
+
...message,
|
|
446
|
+
status: "done",
|
|
447
|
+
text: receivedToolActivity ? "Completed. Open Agent activity for the details." : "Frankie didn\u2019t return an answer. Please try again."
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
370
450
|
} catch (caught) {
|
|
371
451
|
if (abort.signal.aborted) {
|
|
372
452
|
patchAssistant(assistantId, (message) => ({
|
|
@@ -375,7 +455,7 @@ function useFrankieChat() {
|
|
|
375
455
|
text: message.text || "Stopped."
|
|
376
456
|
}));
|
|
377
457
|
} else {
|
|
378
|
-
const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
458
|
+
const message = caught instanceof FrankieApiError ? caught.message : caught instanceof Error && caught.message.startsWith("Frankie") ? caught.message : "Could not reach the assistant. Check your connection and try again.";
|
|
379
459
|
if (mounted.current) setError(message);
|
|
380
460
|
patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
|
|
381
461
|
}
|
|
@@ -399,6 +479,7 @@ function useFrankieChat() {
|
|
|
399
479
|
setMessages([]);
|
|
400
480
|
setError(void 0);
|
|
401
481
|
setBusy(false);
|
|
482
|
+
setRestored(true);
|
|
402
483
|
try {
|
|
403
484
|
if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
|
|
404
485
|
} catch {
|
|
@@ -406,10 +487,16 @@ function useFrankieChat() {
|
|
|
406
487
|
}, [client]);
|
|
407
488
|
return { messages, busy, ...error ? { error } : {}, send, stop, reset };
|
|
408
489
|
}
|
|
490
|
+
function conversationTitle(messages) {
|
|
491
|
+
const first = messages.find((message) => message.role === "user" && message.text.trim());
|
|
492
|
+
if (!first) return void 0;
|
|
493
|
+
const title = first.text.trim().replace(/\s+/g, " ");
|
|
494
|
+
return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
|
|
495
|
+
}
|
|
409
496
|
|
|
410
497
|
// src/components/chat/FrankieWidget.tsx
|
|
411
498
|
import { MessageCircle, X as X4 } from "lucide-react";
|
|
412
|
-
import { useEffect as useEffect5, useState as
|
|
499
|
+
import { useEffect as useEffect5, useState as useState6 } from "react";
|
|
413
500
|
|
|
414
501
|
// src/lib/theme.ts
|
|
415
502
|
function themeToCssVars(theme) {
|
|
@@ -470,7 +557,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
|
|
|
470
557
|
|
|
471
558
|
// src/components/chat/ChatPanel.tsx
|
|
472
559
|
import { RotateCcw, X as X3 } from "lucide-react";
|
|
473
|
-
import { useEffect as useEffect4, useRef as useRef3, useState as
|
|
560
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
|
|
474
561
|
|
|
475
562
|
// src/components/ui/avatar.tsx
|
|
476
563
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
@@ -731,7 +818,8 @@ function readAsBase64(file) {
|
|
|
731
818
|
}
|
|
732
819
|
|
|
733
820
|
// src/components/chat/MessageBubble.tsx
|
|
734
|
-
import { Paperclip as Paperclip2 } from "lucide-react";
|
|
821
|
+
import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
|
|
822
|
+
import { useState as useState4 } from "react";
|
|
735
823
|
|
|
736
824
|
// src/components/chat/Markdown.tsx
|
|
737
825
|
import { Fragment as Fragment2 } from "react";
|
|
@@ -836,7 +924,7 @@ function renderToken(token) {
|
|
|
836
924
|
}
|
|
837
925
|
|
|
838
926
|
// src/components/chat/ToolActivityList.tsx
|
|
839
|
-
import { Check, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
927
|
+
import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react";
|
|
840
928
|
|
|
841
929
|
// src/components/ui/badge.tsx
|
|
842
930
|
import { cva as cva2 } from "class-variance-authority";
|
|
@@ -867,7 +955,20 @@ function Badge({
|
|
|
867
955
|
import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
868
956
|
function ToolActivityList({ tools, renderTool }) {
|
|
869
957
|
if (tools.length === 0) return null;
|
|
870
|
-
|
|
958
|
+
const running = tools.some((tool) => tool.status === "running");
|
|
959
|
+
return /* @__PURE__ */ jsxs2("details", { className: "frankie-tool-activity mt-2", open: running, children: [
|
|
960
|
+
/* @__PURE__ */ jsxs2("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
|
|
961
|
+
/* @__PURE__ */ jsx10(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
|
|
962
|
+
/* @__PURE__ */ jsx10("span", { children: running ? "Working" : "Activity" }),
|
|
963
|
+
/* @__PURE__ */ jsxs2("span", { className: "text-[11px]", children: [
|
|
964
|
+
"\xB7 ",
|
|
965
|
+
tools.length,
|
|
966
|
+
" tool call",
|
|
967
|
+
tools.length === 1 ? "" : "s"
|
|
968
|
+
] })
|
|
969
|
+
] }),
|
|
970
|
+
/* @__PURE__ */ jsx10("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx10("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx10(ToolActivityRow, { tool }) }, tool.id)) })
|
|
971
|
+
] });
|
|
871
972
|
}
|
|
872
973
|
function ToolActivityRow({ tool }) {
|
|
873
974
|
return /* @__PURE__ */ jsxs2(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
|
|
@@ -916,18 +1017,36 @@ function MessageBubble({
|
|
|
916
1017
|
},
|
|
917
1018
|
`${file.name}-${index}`
|
|
918
1019
|
)) }) : null,
|
|
919
|
-
message.text ? /* @__PURE__ */
|
|
1020
|
+
message.text ? /* @__PURE__ */ jsxs3("div", { className: "flex max-w-[85%] items-end gap-1", children: [
|
|
1021
|
+
/* @__PURE__ */ jsx11("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
|
|
1022
|
+
/* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text })
|
|
1023
|
+
] }) : null
|
|
920
1024
|
] });
|
|
921
1025
|
}
|
|
922
1026
|
const empty = message.text.length === 0 && message.tools.length === 0;
|
|
923
1027
|
return /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
|
|
924
|
-
/* @__PURE__ */
|
|
1028
|
+
/* @__PURE__ */ jsxs3("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
|
|
1029
|
+
/* @__PURE__ */ jsx11(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
|
|
1030
|
+
message.text ? /* @__PURE__ */ jsx11(MessageCopyButton, { text: message.text }) : null
|
|
1031
|
+
] }),
|
|
925
1032
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 max-w-[85%]", children: [
|
|
926
1033
|
message.status === "error" ? /* @__PURE__ */ jsx11("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__ */ jsx11(TypingDots, {}) : /* @__PURE__ */ jsx11(Markdown, { text: message.text, className: "text-sm" }),
|
|
927
1034
|
showToolActivity ? /* @__PURE__ */ jsx11(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
|
|
928
1035
|
] })
|
|
929
1036
|
] });
|
|
930
1037
|
}
|
|
1038
|
+
function MessageCopyButton({ text }) {
|
|
1039
|
+
const [copied, setCopied] = useState4(false);
|
|
1040
|
+
const copy = async () => {
|
|
1041
|
+
try {
|
|
1042
|
+
await navigator.clipboard.writeText(text);
|
|
1043
|
+
setCopied(true);
|
|
1044
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
return /* @__PURE__ */ jsx11("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ jsx11(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
|
|
1049
|
+
}
|
|
931
1050
|
function TypingDots() {
|
|
932
1051
|
return /* @__PURE__ */ jsx11("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ jsx11(
|
|
933
1052
|
"span",
|
|
@@ -981,7 +1100,7 @@ function ChatPanel({
|
|
|
981
1100
|
const scrollRef = useRef3(null);
|
|
982
1101
|
const features = config?.features;
|
|
983
1102
|
const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
|
|
984
|
-
const [model, setModel] =
|
|
1103
|
+
const [model, setModel] = useState5(void 0);
|
|
985
1104
|
useEffect4(() => {
|
|
986
1105
|
const node = scrollRef.current;
|
|
987
1106
|
if (!node) return;
|
|
@@ -1011,7 +1130,11 @@ function ChatPanel({
|
|
|
1011
1130
|
/* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx13(RotateCcw, {}) }),
|
|
1012
1131
|
onClose ? /* @__PURE__ */ jsx13(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx13(X3, {}) }) : null
|
|
1013
1132
|
] }),
|
|
1014
|
-
/* @__PURE__ */ jsx13(CardContent, { children: /* @__PURE__ */ jsx13(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */
|
|
1133
|
+
/* @__PURE__ */ jsx13(CardContent, { children: /* @__PURE__ */ jsx13(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs4("div", { className: "frankie-config-error", role: "alert", children: [
|
|
1134
|
+
/* @__PURE__ */ jsx13("strong", { children: "Frankie couldn\u2019t start" }),
|
|
1135
|
+
/* @__PURE__ */ jsx13("p", { children: configError }),
|
|
1136
|
+
/* @__PURE__ */ jsx13("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
|
|
1137
|
+
] }) : loading ? /* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
|
|
1015
1138
|
/* @__PURE__ */ jsx13("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
|
|
1016
1139
|
/* @__PURE__ */ jsx13(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
|
|
1017
1140
|
] }) : /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-4", children: messages.map(
|
|
@@ -1062,7 +1185,7 @@ function FrankieWidget({
|
|
|
1062
1185
|
}) {
|
|
1063
1186
|
const { config } = useFrankie();
|
|
1064
1187
|
const theme = useFrankieTheme();
|
|
1065
|
-
const [uncontrolledOpen, setUncontrolledOpen] =
|
|
1188
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState6(defaultOpen);
|
|
1066
1189
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
1067
1190
|
const setOpen = (next) => {
|
|
1068
1191
|
if (controlledOpen === void 0) setUncontrolledOpen(next);
|
|
@@ -1120,10 +1243,45 @@ function FrankieInline({ className, style, ...panelProps }) {
|
|
|
1120
1243
|
);
|
|
1121
1244
|
}
|
|
1122
1245
|
|
|
1123
|
-
// src/components/
|
|
1246
|
+
// src/components/chat/FrankieChatPage.tsx
|
|
1124
1247
|
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1248
|
+
function FrankieChatPage({
|
|
1249
|
+
className,
|
|
1250
|
+
style,
|
|
1251
|
+
header,
|
|
1252
|
+
footer,
|
|
1253
|
+
emptyState,
|
|
1254
|
+
renderMessage,
|
|
1255
|
+
renderTool,
|
|
1256
|
+
placeholder,
|
|
1257
|
+
autoFocus,
|
|
1258
|
+
...providerProps
|
|
1259
|
+
}) {
|
|
1260
|
+
return /* @__PURE__ */ jsx16(
|
|
1261
|
+
"main",
|
|
1262
|
+
{
|
|
1263
|
+
className,
|
|
1264
|
+
style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
|
|
1265
|
+
children: /* @__PURE__ */ jsx16(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ jsx16(
|
|
1266
|
+
FrankieInline,
|
|
1267
|
+
{
|
|
1268
|
+
header,
|
|
1269
|
+
footer,
|
|
1270
|
+
emptyState,
|
|
1271
|
+
renderMessage,
|
|
1272
|
+
renderTool,
|
|
1273
|
+
placeholder,
|
|
1274
|
+
autoFocus
|
|
1275
|
+
}
|
|
1276
|
+
) })
|
|
1277
|
+
}
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/components/ui/separator.tsx
|
|
1282
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1125
1283
|
function Separator({ className }) {
|
|
1126
|
-
return /* @__PURE__ */
|
|
1284
|
+
return /* @__PURE__ */ jsx17("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
|
|
1127
1285
|
}
|
|
1128
1286
|
export {
|
|
1129
1287
|
Avatar,
|
|
@@ -1139,6 +1297,7 @@ export {
|
|
|
1139
1297
|
Composer,
|
|
1140
1298
|
FRANKIE_CLOUD_URL,
|
|
1141
1299
|
FrankieApiError,
|
|
1300
|
+
FrankieChatPage,
|
|
1142
1301
|
FrankieClient,
|
|
1143
1302
|
FrankieInline,
|
|
1144
1303
|
FrankieProvider,
|