@vezlo/assistant-chat 1.2.0 β 1.4.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/PACKAGE_README.md +12 -3
- package/README.md +12 -2
- package/lib/api/auth.d.ts +27 -0
- package/lib/api/auth.js +59 -0
- package/lib/api/conversation.d.ts +60 -1
- package/lib/api/conversation.js +99 -1
- package/lib/api/index.d.ts +1 -0
- package/lib/api/index.js +1 -0
- package/lib/api/message.d.ts +65 -1
- package/lib/api/message.js +177 -1
- package/lib/components/Widget.js +264 -53
- package/lib/services/conversationRealtime.d.ts +27 -0
- package/lib/services/conversationRealtime.js +73 -0
- package/lib/types/index.d.ts +4 -1
- package/package.json +4 -2
package/PACKAGE_README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@vezlo/assistant-chat) [](https://opensource.org/licenses/AGPL-3.0)
|
|
4
4
|
|
|
5
|
-
A React component library for integrating AI assistant chat functionality into web applications.
|
|
5
|
+
A React component library for integrating AI assistant chat functionality into web applications with realtime updates and human agent support.
|
|
6
6
|
|
|
7
7
|
> **π¦ This is the NPM package documentation**
|
|
8
8
|
> **π Repository**: [assistant-chat](https://github.com/vezlo/assistant-chat) - Contains both this NPM package and a standalone admin application
|
|
@@ -19,6 +19,7 @@ npm install @vezlo/assistant-chat
|
|
|
19
19
|
- React 18 or higher
|
|
20
20
|
- Tailwind CSS (for styling)
|
|
21
21
|
- Assistant Server running (see [Assistant Server](https://github.com/vezlo/assistant-server))
|
|
22
|
+
- **Realtime Updates** (Optional): Provide `supabaseUrl` + `supabaseAnonKey` for agent handoff / live message sync. Without these the widget still works, it just wonβt receive realtime pushes.
|
|
22
23
|
|
|
23
24
|
## Quick Start
|
|
24
25
|
|
|
@@ -37,7 +38,10 @@ function App() {
|
|
|
37
38
|
themeColor: '#10b981',
|
|
38
39
|
position: 'bottom-right',
|
|
39
40
|
size: { width: 400, height: 600 },
|
|
40
|
-
defaultOpen: false
|
|
41
|
+
defaultOpen: false,
|
|
42
|
+
// Optional realtime config
|
|
43
|
+
supabaseUrl: 'https://your-project.supabase.co',
|
|
44
|
+
supabaseAnonKey: 'your-anon-key'
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
return <Widget config={widgetConfig} />;
|
|
@@ -59,6 +63,8 @@ The `WidgetConfig` interface includes:
|
|
|
59
63
|
- `position`: Widget position ('bottom-right', 'bottom-left', 'top-right', 'top-left')
|
|
60
64
|
- `size`: Widget dimensions
|
|
61
65
|
- `defaultOpen`: Whether widget opens by default
|
|
66
|
+
- `supabaseUrl`: Supabase project URL (optional, required for realtime updates)
|
|
67
|
+
- `supabaseAnonKey`: Supabase anon key (optional, required for realtime updates)
|
|
62
68
|
|
|
63
69
|
### Configuration Options Table
|
|
64
70
|
|
|
@@ -75,6 +81,8 @@ The `WidgetConfig` interface includes:
|
|
|
75
81
|
| `defaultOpen` | boolean | `false` | Whether widget opens by default |
|
|
76
82
|
| `apiUrl` | string | Required | Assistant Server API URL |
|
|
77
83
|
| `apiKey` | string | Required | API key for authentication |
|
|
84
|
+
| `supabaseUrl` | string | Optional | Supabase project URL (for realtime updates) |
|
|
85
|
+
| `supabaseAnonKey` | string | Optional | Supabase anon key (for realtime updates) |
|
|
78
86
|
|
|
79
87
|
## API Integration
|
|
80
88
|
|
|
@@ -82,7 +90,8 @@ This widget requires a running Assistant Server instance. The widget will:
|
|
|
82
90
|
|
|
83
91
|
1. Create conversations automatically
|
|
84
92
|
2. Send user messages to the server
|
|
85
|
-
3. Stream AI responses in real-time
|
|
93
|
+
3. Stream AI responses in real-time using Server-Sent Events (SSE)
|
|
94
|
+
4. **Realtime Updates**: With `supabaseUrl` and `supabaseAnonKey` configured, the widget receives realtime updates for agent handoff and live message synchronization
|
|
86
95
|
|
|
87
96
|
Configure your Assistant Server URL in your application:
|
|
88
97
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A complete chat widget solution with both a React component library and standalo
|
|
|
10
10
|
- **Reusable React Widget**: Install via `npm install @vezlo/assistant-chat`
|
|
11
11
|
- **TypeScript Support**: Full type definitions included
|
|
12
12
|
- **Customizable**: Themes, colors, positioning, and behavior
|
|
13
|
-
- **Real-time Streaming**: Live AI responses with streaming support
|
|
13
|
+
- **Real-time Streaming**: Live AI responses with Server-Sent Events (SSE) streaming support
|
|
14
14
|
- **Style Isolation**: Shadow DOM support for conflict-free integration
|
|
15
15
|
- **π [Complete Package Documentation](PACKAGE_README.md)**
|
|
16
16
|
|
|
@@ -74,6 +74,10 @@ npm run dev
|
|
|
74
74
|
- **Assistant Server**: Both components require a running Assistant Server
|
|
75
75
|
- Node.js 18+ and npm
|
|
76
76
|
- React 18+ (for package usage)
|
|
77
|
+
- **Realtime Updates** (Optional): For agent handoff + live message sync, provide Supabase Realtime credentials
|
|
78
|
+
- `VITE_SUPABASE_URL`: Supabase project URL
|
|
79
|
+
- `VITE_SUPABASE_ANON_KEY`: Supabase anon/public key
|
|
80
|
+
- Without these, the widget works normally but wonβt receive realtime updates
|
|
77
81
|
|
|
78
82
|
## Features
|
|
79
83
|
|
|
@@ -81,7 +85,8 @@ npm run dev
|
|
|
81
85
|
- β
React component library
|
|
82
86
|
- β
TypeScript support
|
|
83
87
|
- β
Tailwind CSS styling
|
|
84
|
-
- β
Real-time streaming
|
|
88
|
+
- β
Real-time streaming (SSE-based backend streaming)
|
|
89
|
+
- β
**Realtime updates** (agent handoff, live message sync)
|
|
85
90
|
- β
Customizable themes
|
|
86
91
|
- β
Shadow DOM support
|
|
87
92
|
- β
API integration included
|
|
@@ -92,6 +97,8 @@ npm run dev
|
|
|
92
97
|
- β
Playground testing
|
|
93
98
|
- β
Embed code generation
|
|
94
99
|
- β
Multiple widget management
|
|
100
|
+
- β
**Human agent support** (conversation management, agent handoff)
|
|
101
|
+
- β
**Realtime updates** (live message synchronization)
|
|
95
102
|
- β
Docker support
|
|
96
103
|
- β
Vercel deployment
|
|
97
104
|
|
|
@@ -129,6 +136,9 @@ vercel
|
|
|
129
136
|
# Set environment variables (required)
|
|
130
137
|
vercel env add VITE_ASSISTANT_SERVER_URL
|
|
131
138
|
vercel env add VITE_ASSISTANT_SERVER_API_KEY
|
|
139
|
+
# Optional: For realtime updates
|
|
140
|
+
vercel env add VITE_SUPABASE_URL
|
|
141
|
+
vercel env add VITE_SUPABASE_ANON_KEY
|
|
132
142
|
|
|
133
143
|
# Deploy to production
|
|
134
144
|
vercel --prod
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth API Service
|
|
3
|
+
* Handles login, logout, and current user retrieval
|
|
4
|
+
*/
|
|
5
|
+
export interface LoginRequest {
|
|
6
|
+
email: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginResponse {
|
|
10
|
+
access_token: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MeResponse {
|
|
13
|
+
user: {
|
|
14
|
+
uuid: string;
|
|
15
|
+
email: string;
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
profile: {
|
|
19
|
+
uuid: string;
|
|
20
|
+
company_uuid: string;
|
|
21
|
+
company_name: string;
|
|
22
|
+
role: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function loginUser(payload: LoginRequest, apiUrl?: string): Promise<LoginResponse>;
|
|
26
|
+
export declare function logoutUser(token: string, apiUrl?: string): Promise<void>;
|
|
27
|
+
export declare function getCurrentUser(token: string, apiUrl?: string): Promise<MeResponse>;
|
package/lib/api/auth.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth API Service
|
|
3
|
+
* Handles login, logout, and current user retrieval
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_API_BASE_URL = import.meta.env.VITE_ASSISTANT_SERVER_URL || 'http://localhost:3000';
|
|
6
|
+
const parseErrorMessage = async (response) => {
|
|
7
|
+
const data = await response.json().catch(() => ({}));
|
|
8
|
+
return data.error || data.message || 'Unexpected server error';
|
|
9
|
+
};
|
|
10
|
+
export async function loginUser(payload, apiUrl) {
|
|
11
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
12
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
Accept: 'application/json',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(payload),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const message = await parseErrorMessage(response);
|
|
22
|
+
throw new Error(message);
|
|
23
|
+
}
|
|
24
|
+
const data = (await response.json());
|
|
25
|
+
if (!data.access_token) {
|
|
26
|
+
throw new Error('Login succeeded but no access token was returned.');
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
export async function logoutUser(token, apiUrl) {
|
|
31
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
32
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/logout`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
Authorization: `Bearer ${token}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok && response.status !== 401) {
|
|
40
|
+
// 401 just means the token is already invalid β safe to continue
|
|
41
|
+
const message = await parseErrorMessage(response);
|
|
42
|
+
throw new Error(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function getCurrentUser(token, apiUrl) {
|
|
46
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
47
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/me`, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const message = await parseErrorMessage(response);
|
|
56
|
+
throw new Error(message);
|
|
57
|
+
}
|
|
58
|
+
return (await response.json());
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation API Service
|
|
3
|
-
* Handles conversation creation and
|
|
3
|
+
* Handles conversation creation, listing, and agent messaging
|
|
4
4
|
*/
|
|
5
5
|
export interface CreateConversationRequest {
|
|
6
6
|
title?: string;
|
|
@@ -14,6 +14,45 @@ export interface ConversationResponse {
|
|
|
14
14
|
created_at: string;
|
|
15
15
|
updated_at: string;
|
|
16
16
|
}
|
|
17
|
+
export interface ConversationListItem {
|
|
18
|
+
uuid: string;
|
|
19
|
+
status: string;
|
|
20
|
+
message_count: number;
|
|
21
|
+
last_message_at: string | null;
|
|
22
|
+
joined_at: string | null;
|
|
23
|
+
closed_at: string | null;
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ConversationListResponse {
|
|
28
|
+
conversations: ConversationListItem[];
|
|
29
|
+
pagination: {
|
|
30
|
+
page: number;
|
|
31
|
+
page_size: number;
|
|
32
|
+
total: number;
|
|
33
|
+
has_more: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface ConversationMessage {
|
|
37
|
+
uuid: string;
|
|
38
|
+
content: string;
|
|
39
|
+
type: 'user' | 'assistant' | 'agent' | 'system';
|
|
40
|
+
author_id: number | null;
|
|
41
|
+
created_at: string;
|
|
42
|
+
pending?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ConversationMessagesResponse {
|
|
45
|
+
messages: ConversationMessage[];
|
|
46
|
+
pagination: {
|
|
47
|
+
page: number;
|
|
48
|
+
page_size: number;
|
|
49
|
+
total: number;
|
|
50
|
+
has_more: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export interface JoinConversationResponse {
|
|
54
|
+
message: ConversationMessage;
|
|
55
|
+
}
|
|
17
56
|
/**
|
|
18
57
|
* Create a new conversation
|
|
19
58
|
*/
|
|
@@ -22,3 +61,23 @@ export declare function createConversation(request: CreateConversationRequest, a
|
|
|
22
61
|
* Get conversation by UUID
|
|
23
62
|
*/
|
|
24
63
|
export declare function getConversation(uuid: string, apiUrl?: string): Promise<ConversationResponse>;
|
|
64
|
+
/**
|
|
65
|
+
* Get paginated conversations for agent UI
|
|
66
|
+
*/
|
|
67
|
+
export declare function getConversations(token: string, page?: number, pageSize?: number, orderBy?: string, apiUrl?: string): Promise<ConversationListResponse>;
|
|
68
|
+
/**
|
|
69
|
+
* Get messages within a conversation
|
|
70
|
+
*/
|
|
71
|
+
export declare function getConversationMessages(token: string, conversationUuid: string, page?: number, pageSize?: number, apiUrl?: string): Promise<ConversationMessagesResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Join a conversation as an agent
|
|
74
|
+
*/
|
|
75
|
+
export declare function joinConversation(token: string, conversationUuid: string, apiUrl?: string): Promise<JoinConversationResponse>;
|
|
76
|
+
/**
|
|
77
|
+
* Close a conversation as an agent
|
|
78
|
+
*/
|
|
79
|
+
export declare function closeConversation(token: string, conversationUuid: string, apiUrl?: string): Promise<JoinConversationResponse>;
|
|
80
|
+
/**
|
|
81
|
+
* Send agent-authored message
|
|
82
|
+
*/
|
|
83
|
+
export declare function sendAgentMessage(token: string, conversationUuid: string, content: string, apiUrl?: string): Promise<ConversationMessage>;
|
package/lib/api/conversation.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation API Service
|
|
3
|
-
* Handles conversation creation and
|
|
3
|
+
* Handles conversation creation, listing, and agent messaging
|
|
4
4
|
*/
|
|
5
5
|
const DEFAULT_API_BASE_URL = import.meta.env.VITE_ASSISTANT_SERVER_URL || 'http://localhost:3000';
|
|
6
|
+
const parseErrorMessage = async (response) => {
|
|
7
|
+
const data = await response.json().catch(() => ({}));
|
|
8
|
+
return (data.error ||
|
|
9
|
+
data.message ||
|
|
10
|
+
'Unexpected server error');
|
|
11
|
+
};
|
|
6
12
|
/**
|
|
7
13
|
* Create a new conversation
|
|
8
14
|
*/
|
|
@@ -52,3 +58,95 @@ export async function getConversation(uuid, apiUrl) {
|
|
|
52
58
|
throw error;
|
|
53
59
|
}
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Get paginated conversations for agent UI
|
|
63
|
+
*/
|
|
64
|
+
export async function getConversations(token, page = 1, pageSize = 20, orderBy = 'last_message_at', apiUrl) {
|
|
65
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
66
|
+
const response = await fetch(`${API_BASE_URL}/api/conversations?page=${page}&page_size=${pageSize}&order_by=${orderBy}`, {
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const message = await parseErrorMessage(response);
|
|
75
|
+
throw new Error(message);
|
|
76
|
+
}
|
|
77
|
+
return (await response.json());
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get messages within a conversation
|
|
81
|
+
*/
|
|
82
|
+
export async function getConversationMessages(token, conversationUuid, page = 1, pageSize = 50, apiUrl) {
|
|
83
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
84
|
+
const response = await fetch(`${API_BASE_URL}/api/conversations/${conversationUuid}/messages?page=${page}&page_size=${pageSize}`, {
|
|
85
|
+
method: 'GET',
|
|
86
|
+
headers: {
|
|
87
|
+
Accept: 'application/json',
|
|
88
|
+
Authorization: `Bearer ${token}`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const message = await parseErrorMessage(response);
|
|
93
|
+
throw new Error(message);
|
|
94
|
+
}
|
|
95
|
+
return (await response.json());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Join a conversation as an agent
|
|
99
|
+
*/
|
|
100
|
+
export async function joinConversation(token, conversationUuid, apiUrl) {
|
|
101
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
102
|
+
const response = await fetch(`${API_BASE_URL}/api/conversations/${conversationUuid}/join`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
Authorization: `Bearer ${token}`,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const message = await parseErrorMessage(response);
|
|
111
|
+
throw new Error(message);
|
|
112
|
+
}
|
|
113
|
+
return (await response.json());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Close a conversation as an agent
|
|
117
|
+
*/
|
|
118
|
+
export async function closeConversation(token, conversationUuid, apiUrl) {
|
|
119
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
120
|
+
const response = await fetch(`${API_BASE_URL}/api/conversations/${conversationUuid}/close`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
Authorization: `Bearer ${token}`,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const message = await parseErrorMessage(response);
|
|
129
|
+
throw new Error(message);
|
|
130
|
+
}
|
|
131
|
+
return (await response.json());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Send agent-authored message
|
|
135
|
+
*/
|
|
136
|
+
export async function sendAgentMessage(token, conversationUuid, content, apiUrl) {
|
|
137
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
138
|
+
const response = await fetch(`${API_BASE_URL}/api/conversations/${conversationUuid}/messages/agent`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
Accept: 'application/json',
|
|
143
|
+
Authorization: `Bearer ${token}`,
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({ content }),
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const message = await parseErrorMessage(response);
|
|
149
|
+
throw new Error(message);
|
|
150
|
+
}
|
|
151
|
+
return (await response.json());
|
|
152
|
+
}
|
package/lib/api/index.d.ts
CHANGED
package/lib/api/index.js
CHANGED
package/lib/api/message.d.ts
CHANGED
|
@@ -20,11 +20,75 @@ export interface GenerateMessageResponse {
|
|
|
20
20
|
status: 'completed' | 'pending' | 'error';
|
|
21
21
|
created_at: string;
|
|
22
22
|
}
|
|
23
|
+
export interface StreamChunkEvent {
|
|
24
|
+
type: 'chunk';
|
|
25
|
+
content: string;
|
|
26
|
+
done?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface StreamCompletionEvent {
|
|
29
|
+
type: 'completion';
|
|
30
|
+
uuid: string;
|
|
31
|
+
parent_message_uuid: string;
|
|
32
|
+
status: 'completed';
|
|
33
|
+
created_at: string;
|
|
34
|
+
}
|
|
35
|
+
export interface StreamErrorEvent {
|
|
36
|
+
type: 'error';
|
|
37
|
+
error: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
export type StreamEvent = StreamChunkEvent | StreamCompletionEvent | StreamErrorEvent;
|
|
41
|
+
export interface StreamCallbacks {
|
|
42
|
+
onChunk?: (content: string, isDone?: boolean) => void;
|
|
43
|
+
onCompletion?: (data: StreamCompletionEvent) => void;
|
|
44
|
+
onError?: (error: StreamErrorEvent) => void;
|
|
45
|
+
onDone?: () => void;
|
|
46
|
+
}
|
|
23
47
|
/**
|
|
24
48
|
* Create a user message in a conversation
|
|
25
49
|
*/
|
|
26
50
|
export declare function createUserMessage(conversationUuid: string, request: CreateMessageRequest, apiUrl?: string): Promise<MessageResponse>;
|
|
27
51
|
/**
|
|
28
|
-
* Generate AI response for a user message
|
|
52
|
+
* Generate AI response for a user message (legacy - returns full response)
|
|
53
|
+
* @deprecated Use streamAIResponse for better performance
|
|
29
54
|
*/
|
|
30
55
|
export declare function generateAIResponse(userMessageUuid: string, apiUrl?: string): Promise<GenerateMessageResponse>;
|
|
56
|
+
/**
|
|
57
|
+
* Stream AI response using Server-Sent Events (SSE)
|
|
58
|
+
* This is the recommended approach for real-time streaming
|
|
59
|
+
*/
|
|
60
|
+
export declare function streamAIResponse(userMessageUuid: string, callbacks: StreamCallbacks, apiUrl?: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Feedback API
|
|
63
|
+
*/
|
|
64
|
+
export interface SubmitFeedbackRequest {
|
|
65
|
+
message_uuid: string;
|
|
66
|
+
rating: 'positive' | 'negative';
|
|
67
|
+
category?: string;
|
|
68
|
+
comment?: string;
|
|
69
|
+
suggested_improvement?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface SubmitFeedbackResponse {
|
|
72
|
+
success: boolean;
|
|
73
|
+
feedback: {
|
|
74
|
+
uuid: string;
|
|
75
|
+
message_uuid: string;
|
|
76
|
+
rating: 'positive' | 'negative';
|
|
77
|
+
category?: string;
|
|
78
|
+
comment?: string;
|
|
79
|
+
suggested_improvement?: string;
|
|
80
|
+
created_at: string;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export interface DeleteFeedbackResponse {
|
|
84
|
+
success: boolean;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Submit feedback for a message (create or update) - Public API
|
|
89
|
+
*/
|
|
90
|
+
export declare function submitFeedback(request: SubmitFeedbackRequest, apiUrl?: string): Promise<SubmitFeedbackResponse>;
|
|
91
|
+
/**
|
|
92
|
+
* Delete/undo feedback for a message - Public API
|
|
93
|
+
*/
|
|
94
|
+
export declare function deleteFeedback(feedbackUuid: string, apiUrl?: string): Promise<DeleteFeedbackResponse>;
|
package/lib/api/message.js
CHANGED
|
@@ -30,7 +30,8 @@ export async function createUserMessage(conversationUuid, request, apiUrl) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Generate AI response for a user message
|
|
33
|
+
* Generate AI response for a user message (legacy - returns full response)
|
|
34
|
+
* @deprecated Use streamAIResponse for better performance
|
|
34
35
|
*/
|
|
35
36
|
export async function generateAIResponse(userMessageUuid, apiUrl) {
|
|
36
37
|
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
@@ -53,3 +54,178 @@ export async function generateAIResponse(userMessageUuid, apiUrl) {
|
|
|
53
54
|
throw error;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Stream AI response using Server-Sent Events (SSE)
|
|
59
|
+
* This is the recommended approach for real-time streaming
|
|
60
|
+
*/
|
|
61
|
+
export async function streamAIResponse(userMessageUuid, callbacks, apiUrl) {
|
|
62
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(`${API_BASE_URL}/api/messages/${userMessageUuid}/generate`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Accept': 'text/event-stream',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
// Try to parse error as JSON first
|
|
72
|
+
try {
|
|
73
|
+
const errorData = await response.json();
|
|
74
|
+
callbacks.onError?.({
|
|
75
|
+
type: 'error',
|
|
76
|
+
error: 'Failed to generate response',
|
|
77
|
+
message: errorData.message || `HTTP ${response.status}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// If not JSON, use status text
|
|
82
|
+
callbacks.onError?.({
|
|
83
|
+
type: 'error',
|
|
84
|
+
error: 'Failed to generate response',
|
|
85
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!response.body) {
|
|
91
|
+
callbacks.onError?.({
|
|
92
|
+
type: 'error',
|
|
93
|
+
error: 'No response body',
|
|
94
|
+
message: 'Server did not return a response stream',
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const reader = response.body.getReader();
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
let buffer = '';
|
|
101
|
+
try {
|
|
102
|
+
while (true) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
// Decode chunk and add to buffer
|
|
108
|
+
buffer += decoder.decode(value, { stream: true });
|
|
109
|
+
// Process complete SSE messages (lines ending with \n\n)
|
|
110
|
+
const lines = buffer.split('\n\n');
|
|
111
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
if (line.startsWith('data: ')) {
|
|
114
|
+
const data = line.slice(6); // Remove 'data: ' prefix
|
|
115
|
+
// Check for [DONE] marker
|
|
116
|
+
if (data.trim() === '[DONE]') {
|
|
117
|
+
callbacks.onDone?.();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const event = JSON.parse(data);
|
|
122
|
+
switch (event.type) {
|
|
123
|
+
case 'chunk':
|
|
124
|
+
callbacks.onChunk?.(event.content, event.done);
|
|
125
|
+
break;
|
|
126
|
+
case 'completion':
|
|
127
|
+
callbacks.onCompletion?.(event);
|
|
128
|
+
callbacks.onDone?.();
|
|
129
|
+
return;
|
|
130
|
+
case 'error':
|
|
131
|
+
callbacks.onError?.(event);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (parseError) {
|
|
136
|
+
console.warn('[Message API] Failed to parse SSE event:', data, parseError);
|
|
137
|
+
// Continue processing other events
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Process any remaining buffer
|
|
143
|
+
if (buffer.trim()) {
|
|
144
|
+
const lines = buffer.split('\n\n');
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (line.startsWith('data: ')) {
|
|
147
|
+
const data = line.slice(6);
|
|
148
|
+
if (data.trim() === '[DONE]') {
|
|
149
|
+
callbacks.onDone?.();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const event = JSON.parse(data);
|
|
154
|
+
if (event.type === 'completion') {
|
|
155
|
+
callbacks.onCompletion?.(event);
|
|
156
|
+
}
|
|
157
|
+
else if (event.type === 'error') {
|
|
158
|
+
callbacks.onError?.(event);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (parseError) {
|
|
162
|
+
console.warn('[Message API] Failed to parse final SSE event:', parseError);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
callbacks.onDone?.();
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
reader.releaseLock();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error('[Message API] Error streaming AI response:', error);
|
|
175
|
+
callbacks.onError?.({
|
|
176
|
+
type: 'error',
|
|
177
|
+
error: 'Stream error',
|
|
178
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Submit feedback for a message (create or update) - Public API
|
|
184
|
+
*/
|
|
185
|
+
export async function submitFeedback(request, apiUrl) {
|
|
186
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(`${API_BASE_URL}/api/feedback`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'Content-Type': 'application/json',
|
|
192
|
+
'Accept': 'application/json',
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(request),
|
|
195
|
+
});
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
const errorData = await response.json().catch(() => ({}));
|
|
198
|
+
throw new Error(errorData.message || `Failed to submit feedback: ${response.status}`);
|
|
199
|
+
}
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error('[Message API] Error submitting feedback:', error);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Delete/undo feedback for a message - Public API
|
|
210
|
+
*/
|
|
211
|
+
export async function deleteFeedback(feedbackUuid, apiUrl) {
|
|
212
|
+
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedbackUuid}`, {
|
|
215
|
+
method: 'DELETE',
|
|
216
|
+
headers: {
|
|
217
|
+
'Accept': 'application/json',
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
const errorData = await response.json().catch(() => ({}));
|
|
222
|
+
throw new Error(errorData.message || `Failed to delete feedback: ${response.status}`);
|
|
223
|
+
}
|
|
224
|
+
const data = await response.json();
|
|
225
|
+
return data;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error('[Message API] Error deleting feedback:', error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/lib/components/Widget.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { Send, X, MessageCircle, Bot, ThumbsUp, ThumbsDown } from 'lucide-react';
|
|
5
5
|
import { generateId, formatTimestamp } from '../utils/index.js';
|
|
6
6
|
import { VezloFooter } from './ui/VezloFooter.js';
|
|
7
|
-
import { createConversation, createUserMessage,
|
|
7
|
+
import { createConversation, createUserMessage, streamAIResponse } from '../api/index.js';
|
|
8
|
+
import { submitFeedback, deleteFeedback } from '../api/message.js';
|
|
9
|
+
import { subscribeToConversations } from '../services/conversationRealtime.js';
|
|
8
10
|
import { THEME } from '../config/theme.js';
|
|
9
11
|
export function Widget({ config, isPlayground = false, onOpen, onClose, onMessage, onError, useShadowRoot = false, }) {
|
|
10
12
|
// Use defaultOpen from config, fallback to isPlayground for backward compatibility
|
|
@@ -19,13 +21,18 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
19
21
|
const [input, setInput] = useState('');
|
|
20
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
21
23
|
const [messageFeedback, setMessageFeedback] = useState({});
|
|
24
|
+
const [messageFeedbackUuids, setMessageFeedbackUuids] = useState({});
|
|
22
25
|
const [streamingMessage, setStreamingMessage] = useState('');
|
|
23
26
|
const [conversationUuid, setConversationUuid] = useState(null);
|
|
27
|
+
const [companyUuid, setCompanyUuid] = useState(null);
|
|
28
|
+
const [agentJoined, setAgentJoined] = useState(false);
|
|
29
|
+
const [conversationClosed, setConversationClosed] = useState(false);
|
|
24
30
|
const [isCreatingConversation, setIsCreatingConversation] = useState(false);
|
|
25
31
|
const messagesEndRef = useRef(null);
|
|
26
32
|
const hostRef = useRef(null);
|
|
27
33
|
const shadowRef = useRef(null);
|
|
28
34
|
const shadowMountRef = useRef(null);
|
|
35
|
+
const inputRef = useRef(null);
|
|
29
36
|
const [shadowReady, setShadowReady] = useState(false);
|
|
30
37
|
useEffect(() => {
|
|
31
38
|
// No global scroll locking for embedded component usage
|
|
@@ -67,7 +74,10 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
67
74
|
title: 'New Chat',
|
|
68
75
|
}, config.apiUrl);
|
|
69
76
|
setConversationUuid(conversation.uuid);
|
|
70
|
-
|
|
77
|
+
setCompanyUuid(conversation.company_uuid);
|
|
78
|
+
setAgentJoined(false);
|
|
79
|
+
setConversationClosed(false);
|
|
80
|
+
console.log('[Widget] Conversation created:', conversation.uuid, 'Company:', conversation.company_uuid);
|
|
71
81
|
// Add welcome message after conversation is created
|
|
72
82
|
const welcomeMsg = {
|
|
73
83
|
id: generateId(),
|
|
@@ -98,12 +108,52 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
98
108
|
};
|
|
99
109
|
initializeConversation();
|
|
100
110
|
}, [isOpen, conversationUuid, isCreatingConversation, config.welcomeMessage, onMessage, onError]);
|
|
111
|
+
// Auto-focus input when it becomes enabled (isLoading becomes false)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (isOpen && !isLoading && !conversationClosed && inputRef.current) {
|
|
114
|
+
// Focus immediately when input becomes enabled
|
|
115
|
+
inputRef.current.focus();
|
|
116
|
+
}
|
|
117
|
+
}, [isLoading, isOpen, conversationClosed]);
|
|
118
|
+
// Subscribe to realtime updates for agent messages
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!companyUuid || !conversationUuid) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const handleMessageCreated = (payload) => {
|
|
124
|
+
if (payload.conversation_uuid !== conversationUuid) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const status = payload.conversation_update?.status;
|
|
128
|
+
if (status === 'in_progress') {
|
|
129
|
+
setAgentJoined(true);
|
|
130
|
+
setConversationClosed(false);
|
|
131
|
+
}
|
|
132
|
+
else if (status === 'closed') {
|
|
133
|
+
setAgentJoined(false);
|
|
134
|
+
setConversationClosed(true);
|
|
135
|
+
}
|
|
136
|
+
if (payload.message.type !== 'system' && payload.message.type !== 'agent') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const newMessage = {
|
|
140
|
+
id: payload.message.uuid,
|
|
141
|
+
content: payload.message.content,
|
|
142
|
+
role: payload.message.type === 'agent' ? 'assistant' : 'system',
|
|
143
|
+
timestamp: new Date(payload.message.created_at),
|
|
144
|
+
};
|
|
145
|
+
setMessages(prev => [...prev, newMessage]);
|
|
146
|
+
};
|
|
147
|
+
const cleanup = subscribeToConversations(companyUuid, handleMessageCreated, () => { }, // No need to handle conversation:created in widget
|
|
148
|
+
config.supabaseUrl, config.supabaseAnonKey);
|
|
149
|
+
return cleanup;
|
|
150
|
+
}, [companyUuid, conversationUuid, config.supabaseUrl, config.supabaseAnonKey]);
|
|
101
151
|
useEffect(() => {
|
|
102
152
|
// Scroll to bottom when messages change or when streaming
|
|
103
153
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
104
154
|
}, [messages, streamingMessage]);
|
|
105
155
|
const handleSendMessage = async () => {
|
|
106
|
-
if (!input.trim() || isLoading || !conversationUuid)
|
|
156
|
+
if (!input.trim() || isLoading || !conversationUuid || conversationClosed)
|
|
107
157
|
return;
|
|
108
158
|
const userMessageContent = input;
|
|
109
159
|
const userMessage = {
|
|
@@ -124,35 +174,84 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
124
174
|
console.log('[Widget] User message created:', userMessageResponse.uuid);
|
|
125
175
|
// Update the user message with the actual UUID from server
|
|
126
176
|
setMessages((prev) => prev.map((msg) => msg.id === userMessage.id ? { ...msg, id: userMessageResponse.uuid } : msg));
|
|
127
|
-
// Step 2: Generate AI response
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
// Step 2: Generate AI response (only if agent hasn't joined)
|
|
178
|
+
if (!agentJoined) {
|
|
179
|
+
// Initialize streaming state
|
|
180
|
+
setStreamingMessage('');
|
|
181
|
+
let accumulatedContent = '';
|
|
182
|
+
let hasReceivedChunks = false;
|
|
183
|
+
let streamingComplete = false;
|
|
184
|
+
const tempMessageId = `streaming-${userMessageResponse.uuid}`;
|
|
185
|
+
// Stream AI response using SSE
|
|
186
|
+
await streamAIResponse(userMessageResponse.uuid, {
|
|
187
|
+
onChunk: (chunk, isDone) => {
|
|
188
|
+
// Hide loading indicator on first chunk (streaming started)
|
|
189
|
+
if (!hasReceivedChunks) {
|
|
190
|
+
hasReceivedChunks = true;
|
|
191
|
+
setIsLoading(false);
|
|
192
|
+
}
|
|
193
|
+
// Accumulate content and update streaming message
|
|
194
|
+
if (chunk) {
|
|
195
|
+
accumulatedContent += chunk;
|
|
196
|
+
setStreamingMessage(accumulatedContent);
|
|
197
|
+
}
|
|
198
|
+
// If this is the final chunk (done=true), convert to message immediately
|
|
199
|
+
if (isDone && !streamingComplete) {
|
|
200
|
+
streamingComplete = true;
|
|
201
|
+
// Add message to array with temp ID (shows timestamp/icons)
|
|
202
|
+
const tempMessage = {
|
|
203
|
+
id: tempMessageId,
|
|
204
|
+
content: accumulatedContent,
|
|
205
|
+
role: 'assistant',
|
|
206
|
+
timestamp: new Date(),
|
|
207
|
+
};
|
|
208
|
+
setStreamingMessage('');
|
|
209
|
+
setMessages((prev) => [...prev, tempMessage]);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
onCompletion: (completionData) => {
|
|
213
|
+
// Store real UUID in _realUuid field (for feedback) without changing id (no jerk)
|
|
214
|
+
setMessages((prev) => prev.map((msg) => msg.id === tempMessageId
|
|
215
|
+
? { ...msg, _realUuid: completionData.uuid }
|
|
216
|
+
: msg));
|
|
217
|
+
setIsLoading(false);
|
|
218
|
+
const finalMessage = {
|
|
219
|
+
id: completionData.uuid,
|
|
220
|
+
content: accumulatedContent,
|
|
221
|
+
role: 'assistant',
|
|
222
|
+
timestamp: new Date(completionData.created_at),
|
|
223
|
+
};
|
|
224
|
+
onMessage?.(finalMessage);
|
|
225
|
+
// Log completion (no content in event anymore)
|
|
226
|
+
console.log('[Widget] AI response completed:', {
|
|
227
|
+
uuid: completionData.uuid,
|
|
228
|
+
parent_message_uuid: completionData.parent_message_uuid,
|
|
229
|
+
status: completionData.status,
|
|
230
|
+
created_at: completionData.created_at,
|
|
231
|
+
accumulated_content_length: accumulatedContent.length
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
onError: (errorData) => {
|
|
235
|
+
// Hide loading indicator
|
|
236
|
+
setIsLoading(false);
|
|
237
|
+
// Clear streaming message
|
|
238
|
+
setStreamingMessage('');
|
|
239
|
+
// Show error to user
|
|
240
|
+
const errorMessage = errorData.message || errorData.error || 'Failed to generate response';
|
|
241
|
+
onError?.(errorMessage);
|
|
242
|
+
console.error('[Widget] AI response error:', errorData);
|
|
243
|
+
},
|
|
244
|
+
onDone: () => {
|
|
245
|
+
// Ensure loading is hidden
|
|
246
|
+
setIsLoading(false);
|
|
247
|
+
},
|
|
248
|
+
}, config.apiUrl);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Agent has joined, don't generate AI response
|
|
252
|
+
setIsLoading(false);
|
|
253
|
+
console.log('[Widget] Skipping AI response - agent has joined');
|
|
254
|
+
}
|
|
156
255
|
}
|
|
157
256
|
catch (error) {
|
|
158
257
|
console.error('[Widget] Error sending message:', error);
|
|
@@ -169,17 +268,109 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
169
268
|
onMessage?.(errorMessage);
|
|
170
269
|
}
|
|
171
270
|
};
|
|
271
|
+
const handleStartNewChat = () => {
|
|
272
|
+
if (isCreatingConversation)
|
|
273
|
+
return;
|
|
274
|
+
setMessages([]);
|
|
275
|
+
setStreamingMessage('');
|
|
276
|
+
setIsLoading(false);
|
|
277
|
+
setMessageFeedback({});
|
|
278
|
+
setAgentJoined(false);
|
|
279
|
+
setConversationClosed(false);
|
|
280
|
+
setConversationUuid(null);
|
|
281
|
+
setCompanyUuid(null);
|
|
282
|
+
setInput('');
|
|
283
|
+
};
|
|
172
284
|
const handleKeyPress = (e) => {
|
|
285
|
+
if (conversationClosed)
|
|
286
|
+
return;
|
|
173
287
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
174
288
|
e.preventDefault();
|
|
175
289
|
handleSendMessage();
|
|
176
290
|
}
|
|
177
291
|
};
|
|
178
|
-
const handleFeedback = (messageId, type) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
292
|
+
const handleFeedback = async (messageId, type) => {
|
|
293
|
+
// Find the message to get the real UUID (might be stored in _realUuid)
|
|
294
|
+
const message = messages.find(m => m.id === messageId);
|
|
295
|
+
// Safety check: Don't proceed if message has temp ID and no real UUID yet
|
|
296
|
+
if (!message?._realUuid && messageId.startsWith('streaming-')) {
|
|
297
|
+
console.warn('[Widget] Cannot submit feedback: Message UUID not yet available');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const realUuid = message?._realUuid || messageId; // Use real UUID if available, fallback to ID
|
|
301
|
+
const currentFeedback = messageFeedback[messageId];
|
|
302
|
+
const feedbackUuid = messageFeedbackUuids[messageId];
|
|
303
|
+
// Map UI types to backend rating values
|
|
304
|
+
const rating = type === 'like' ? 'positive' : 'negative';
|
|
305
|
+
// Optimistic UI update - update immediately for better UX
|
|
306
|
+
const previousFeedback = currentFeedback;
|
|
307
|
+
const previousUuid = feedbackUuid;
|
|
308
|
+
// If clicking the same rating again, delete (undo)
|
|
309
|
+
if (currentFeedback === type && feedbackUuid) {
|
|
310
|
+
// Optimistically update UI (remove feedback)
|
|
311
|
+
setMessageFeedback(prev => ({
|
|
312
|
+
...prev,
|
|
313
|
+
[messageId]: null
|
|
314
|
+
}));
|
|
315
|
+
setMessageFeedbackUuids(prev => {
|
|
316
|
+
const updated = { ...prev };
|
|
317
|
+
delete updated[messageId];
|
|
318
|
+
return updated;
|
|
319
|
+
});
|
|
320
|
+
// Call API in background
|
|
321
|
+
try {
|
|
322
|
+
await deleteFeedback(feedbackUuid, config.apiUrl);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error('[Widget] Error deleting feedback:', error);
|
|
326
|
+
// Revert UI state on error
|
|
327
|
+
setMessageFeedback(prev => ({
|
|
328
|
+
...prev,
|
|
329
|
+
[messageId]: previousFeedback
|
|
330
|
+
}));
|
|
331
|
+
setMessageFeedbackUuids(prev => ({
|
|
332
|
+
...prev,
|
|
333
|
+
[messageId]: previousUuid
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Optimistically update UI (add/change feedback)
|
|
339
|
+
setMessageFeedback(prev => ({
|
|
340
|
+
...prev,
|
|
341
|
+
[messageId]: type
|
|
342
|
+
}));
|
|
343
|
+
// Call API in background
|
|
344
|
+
try {
|
|
345
|
+
const response = await submitFeedback({
|
|
346
|
+
message_uuid: realUuid,
|
|
347
|
+
rating,
|
|
348
|
+
}, config.apiUrl);
|
|
349
|
+
// Update with real feedback UUID from server
|
|
350
|
+
setMessageFeedbackUuids(prev => ({
|
|
351
|
+
...prev,
|
|
352
|
+
[messageId]: response.feedback.uuid
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error('[Widget] Error submitting feedback:', error);
|
|
357
|
+
// Revert UI state on error
|
|
358
|
+
setMessageFeedback(prev => ({
|
|
359
|
+
...prev,
|
|
360
|
+
[messageId]: previousFeedback
|
|
361
|
+
}));
|
|
362
|
+
setMessageFeedbackUuids(prev => {
|
|
363
|
+
if (previousUuid) {
|
|
364
|
+
return { ...prev, [messageId]: previousUuid };
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const updated = { ...prev };
|
|
368
|
+
delete updated[messageId];
|
|
369
|
+
return updated;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
183
374
|
};
|
|
184
375
|
const handleOpenWidget = () => {
|
|
185
376
|
setIsOpen(true);
|
|
@@ -249,7 +440,8 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
249
440
|
display: 'flex',
|
|
250
441
|
alignItems: 'center',
|
|
251
442
|
justifyContent: 'center',
|
|
252
|
-
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
|
443
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
444
|
+
cursor: 'pointer'
|
|
253
445
|
}, onMouseEnter: (e) => {
|
|
254
446
|
e.currentTarget.style.transform = 'scale(1.05)';
|
|
255
447
|
e.currentTarget.style.boxShadow = '0 14px 30px rgba(0,0,0,0.22)';
|
|
@@ -260,21 +452,40 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
260
452
|
pointerEvents: 'auto',
|
|
261
453
|
width: (config.size && config.size.width) ? config.size.width : 420,
|
|
262
454
|
height: (config.size && config.size.height) ? config.size.height : 600
|
|
263
|
-
}, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd, ${config.themeColor || THEME.primary.hex}bb)`, color: '#fff' }, children: [_jsxs("div", { className: "absolute inset-0 opacity-10", children: [_jsx("div", { className: "absolute top-0 left-0 w-full h-full bg-gradient-to-br from-white/20 to-transparent" }), _jsx("div", { className: "absolute bottom-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-8 translate-x-8" })] }), _jsxs("div", { className: "flex items-center gap-3 relative z-10", children: [_jsx("div", { className: "w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm flex-shrink-0", children: _jsx(Bot, { className: "w-6 h-6 text-white" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold text-lg leading-tight truncate", children: config.title }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx("div", { className: "w-2 h-2 bg-green-400 rounded-full animate-pulse flex-shrink-0" }), _jsxs("p", { className: "text-xs text-white/90 truncate", children: ["Online \u2022 ", config.subtitle] })] })] })] }), _jsx("button", { onClick: handleCloseWidget, className: "hover:bg-white/20 rounded-lg p-2 transition-all duration-200 hover:scale-110 relative z-10", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-gray-50 to-white", children: [messages.map((message, index) => (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
455
|
+
}, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd, ${config.themeColor || THEME.primary.hex}bb)`, color: '#fff' }, children: [_jsxs("div", { className: "absolute inset-0 opacity-10", children: [_jsx("div", { className: "absolute top-0 left-0 w-full h-full bg-gradient-to-br from-white/20 to-transparent" }), _jsx("div", { className: "absolute bottom-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-8 translate-x-8" })] }), _jsxs("div", { className: "flex items-center gap-3 relative z-10", children: [_jsx("div", { className: "w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm flex-shrink-0", children: _jsx(Bot, { className: "w-6 h-6 text-white" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold text-lg leading-tight truncate", children: config.title }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx("div", { className: "w-2 h-2 bg-green-400 rounded-full animate-pulse flex-shrink-0" }), _jsxs("p", { className: "text-xs text-white/90 truncate", children: ["Online \u2022 ", config.subtitle] })] })] })] }), _jsx("button", { onClick: handleCloseWidget, className: "hover:bg-white/20 rounded-lg p-2 transition-all duration-200 hover:scale-110 relative z-10 cursor-pointer", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-gray-50 to-white", children: [messages.map((message, index) => (_jsx("div", { className: `flex ${message.role === 'system'
|
|
456
|
+
? 'justify-center'
|
|
457
|
+
: message.role === 'user'
|
|
458
|
+
? 'justify-end'
|
|
459
|
+
: 'justify-start'} animate-fadeIn`, style: { animationDelay: `${index * 0.1}s` }, children: message.role === 'system' ? (_jsx("div", { className: `text-xs px-4 py-2 rounded-full border ${message.content.toLowerCase().includes('closed')
|
|
460
|
+
? 'bg-red-50 text-red-700 border-red-200'
|
|
461
|
+
: 'bg-blue-50 text-blue-700 border-blue-200'}`, children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { children: message.content }), _jsx("span", { className: "text-xs opacity-70", children: formatTimestamp(message.timestamp) })] }) })) : (_jsxs(_Fragment, { children: [message.role === 'assistant' && (_jsx("div", { className: "w-8 h-8 bg-emerald-50 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2 border border-emerald-100", children: _jsx(Bot, { className: "w-4 h-4 text-emerald-600" }) })), _jsxs("div", { className: "flex flex-col max-w-[75%]", children: [_jsx("div", { className: `rounded-2xl px-4 py-3 shadow-sm transition-all duration-200 hover:shadow-md ${message.role === 'user'
|
|
462
|
+
? 'text-white'
|
|
463
|
+
: 'bg-white text-gray-900 border border-gray-200'}`, style: {
|
|
464
|
+
backgroundColor: message.role === 'user' ? (config.themeColor || THEME.primary.hex) : undefined,
|
|
465
|
+
boxShadow: message.role === 'user'
|
|
466
|
+
? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
|
|
467
|
+
: '0 2px 8px rgba(0, 0, 0, 0.1)'
|
|
468
|
+
}, children: _jsx("p", { className: "text-sm whitespace-pre-wrap break-words leading-relaxed", children: message.content }) }), _jsxs("div", { className: "flex items-center justify-between mt-1", children: [_jsx("p", { className: `text-xs ${message.role === 'user' ? 'text-emerald-100' : 'text-gray-500'}`, children: formatTimestamp(message.timestamp) }), message.role === 'assistant' && (() => {
|
|
469
|
+
// Check if message has real UUID (not temp ID)
|
|
470
|
+
// Disable if: message has temp ID (starts with 'streaming-') AND no _realUuid yet
|
|
471
|
+
const isTempId = message.id?.startsWith('streaming-') || false;
|
|
472
|
+
const hasRealUuid = !!message._realUuid;
|
|
473
|
+
const isDisabled = isTempId && !hasRealUuid;
|
|
474
|
+
return (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => !isDisabled && handleFeedback(message.id, 'like'), disabled: isDisabled, className: `p-1 rounded transition-all duration-200 ${isDisabled
|
|
475
|
+
? 'opacity-40 cursor-not-allowed'
|
|
476
|
+
: 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'like'
|
|
477
|
+
? 'text-green-600'
|
|
478
|
+
: 'text-gray-400 hover:text-green-600'}`, title: isDisabled ? 'Waiting for message to be saved...' : 'Like this response', children: _jsx(ThumbsUp, { className: `w-4 h-4 ${messageFeedback[message.id] === 'like' ? 'fill-current' : ''}` }) }), _jsx("button", { onClick: () => !isDisabled && handleFeedback(message.id, 'dislike'), disabled: isDisabled, className: `p-1 rounded transition-all duration-200 ${isDisabled
|
|
479
|
+
? 'opacity-40 cursor-not-allowed'
|
|
480
|
+
: 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'dislike'
|
|
481
|
+
? 'text-red-600'
|
|
482
|
+
: 'text-gray-400 hover:text-red-600'}`, title: isDisabled ? 'Waiting for message to be saved...' : 'Dislike this response', children: _jsx(ThumbsDown, { className: `w-4 h-4 ${messageFeedback[message.id] === 'dislike' ? 'fill-current' : ''}` }) })] }));
|
|
483
|
+
})()] })] })] })) }, message.id))), streamingMessage && (_jsxs("div", { className: "flex justify-start animate-fadeIn", children: [_jsx("div", { className: "w-8 h-8 bg-emerald-50 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2 border border-emerald-100", children: _jsx(Bot, { className: "w-4 h-4 text-emerald-600" }) }), _jsx("div", { className: "flex flex-col max-w-[75%]", children: _jsx("div", { className: "bg-white text-gray-900 border border-gray-200 rounded-2xl px-4 py-3 shadow-sm", children: _jsxs("p", { className: "text-sm whitespace-pre-wrap break-words leading-relaxed", style: { color: '#111827' }, children: [streamingMessage, _jsx("span", { style: { display: 'inline-block', animation: 'vezloCaretBlink 1s steps(1, end) infinite' }, children: "|" })] }) }) })] })), isLoading && (_jsx("div", { className: "flex justify-start animate-fadeIn", children: _jsx("div", { className: "bg-white border border-gray-200 rounded-2xl px-4 py-3 flex items-center gap-3 shadow-sm", children: _jsxs("div", { className: "flex gap-1", style: { display: 'flex', gap: '4px' }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.15s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.3s' } })] }) }) })), _jsx("div", { ref: messagesEndRef })] }), conversationClosed && (_jsx("div", { className: "border-t border-gray-200 p-4 bg-white flex justify-center", children: _jsx("button", { onClick: handleStartNewChat, disabled: isCreatingConversation, className: "px-6 py-3 bg-gradient-to-r from-emerald-600 to-emerald-500 text-white text-sm font-medium rounded-lg hover:from-emerald-700 hover:to-emerald-600 transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none cursor-pointer", children: isCreatingConversation ? 'Starting new chat...' : 'Start New Chat' }) })), !conversationClosed && (_jsx("div", { className: "border-t border-gray-200 p-4 bg-white", children: _jsxs("div", { className: "flex gap-3", children: [_jsx("input", { ref: inputRef, type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyPress: handleKeyPress, placeholder: conversationClosed
|
|
484
|
+
? 'Conversation closed. Start a new chat to continue.'
|
|
485
|
+
: config.placeholder, disabled: isLoading || conversationClosed, className: "flex-1 px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-sm transition-all duration-200 placeholder:text-gray-400" }), _jsx("button", { onClick: handleSendMessage, disabled: !input.trim() || isLoading || conversationClosed, className: "text-white px-4 py-3 rounded-2xl transition-all duration-200 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center shadow-lg hover:shadow-xl disabled:shadow-none transform hover:scale-105 disabled:scale-100 min-w-[48px]", style: {
|
|
275
486
|
background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd)`,
|
|
276
|
-
opacity: (!input.trim() || isLoading) ? 0.6 : 1
|
|
277
|
-
}, children: _jsx(Send, { className: "w-4 h-4" }) })] }) }), _jsx("div", { className: "border-t border-gray-200 px-4 bg-gradient-to-r from-gray-50 to-white", style: { minHeight: 52 }, children: _jsx(VezloFooter, { size: "sm" }) })] }))] }));
|
|
487
|
+
opacity: (!input.trim() || isLoading || conversationClosed) ? 0.6 : 1
|
|
488
|
+
}, children: _jsx(Send, { className: "w-4 h-4" }) })] }) })), _jsx("div", { className: "border-t border-gray-200 px-4 bg-gradient-to-r from-gray-50 to-white", style: { minHeight: 52 }, children: _jsx(VezloFooter, { size: "sm" }) })] }))] }));
|
|
278
489
|
if (useShadowRoot) {
|
|
279
490
|
// Ensure host exists in DOM
|
|
280
491
|
return (_jsx("div", { ref: hostRef, style: { all: 'initial' }, children: shadowReady && shadowMountRef.current ? createPortal(content, shadowMountRef.current) : null }));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MessageCreatedPayload {
|
|
2
|
+
conversation_uuid: string;
|
|
3
|
+
message: {
|
|
4
|
+
uuid: string;
|
|
5
|
+
content: string;
|
|
6
|
+
type: 'user' | 'assistant' | 'agent' | 'system';
|
|
7
|
+
author_id: number | null;
|
|
8
|
+
created_at: string;
|
|
9
|
+
};
|
|
10
|
+
conversation_update: {
|
|
11
|
+
message_count: number;
|
|
12
|
+
last_message_at: string;
|
|
13
|
+
joined_at?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
closed_at?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface ConversationCreatedPayload {
|
|
19
|
+
conversation: {
|
|
20
|
+
uuid: string;
|
|
21
|
+
status: string;
|
|
22
|
+
message_count: number;
|
|
23
|
+
last_message_at: string | null;
|
|
24
|
+
created_at: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function subscribeToConversations(companyUuid: string, onMessageCreated: (payload: MessageCreatedPayload) => void, onConversationCreated: (payload: ConversationCreatedPayload) => void, supabaseUrl?: string, supabaseAnonKey?: string): () => void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
let client = null;
|
|
3
|
+
let clientUrl = null;
|
|
4
|
+
let clientKey = null;
|
|
5
|
+
function getClient(supabaseUrl, supabaseAnonKey) {
|
|
6
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
7
|
+
console.error('[ConversationRealtime] Missing supabaseUrl or supabaseAnonKey');
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
// Recreate the client if credentials change
|
|
11
|
+
if (!client || clientUrl !== supabaseUrl || clientKey !== supabaseAnonKey) {
|
|
12
|
+
try {
|
|
13
|
+
client = createClient(supabaseUrl, supabaseAnonKey, {
|
|
14
|
+
auth: {
|
|
15
|
+
persistSession: false, // Prevent multiple auth instances warning
|
|
16
|
+
autoRefreshToken: false,
|
|
17
|
+
detectSessionInUrl: false
|
|
18
|
+
},
|
|
19
|
+
realtime: { params: { eventsPerSecond: 2 } },
|
|
20
|
+
});
|
|
21
|
+
clientUrl = supabaseUrl;
|
|
22
|
+
clientKey = supabaseAnonKey;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error('[ConversationRealtime] Failed to initialize client:', error instanceof Error ? error.message : 'Unknown error');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return client;
|
|
30
|
+
}
|
|
31
|
+
export function subscribeToConversations(companyUuid, onMessageCreated, onConversationCreated, supabaseUrl, supabaseAnonKey) {
|
|
32
|
+
try {
|
|
33
|
+
const realtimeClient = getClient(supabaseUrl, supabaseAnonKey);
|
|
34
|
+
if (!realtimeClient) {
|
|
35
|
+
console.error('[ConversationRealtime] Client not available');
|
|
36
|
+
return () => { };
|
|
37
|
+
}
|
|
38
|
+
const channelName = `company:${companyUuid}:conversations`;
|
|
39
|
+
const channel = realtimeClient.channel(channelName, {
|
|
40
|
+
config: {
|
|
41
|
+
broadcast: { self: true, ack: false }
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
channel.on('broadcast', { event: 'message:created' }, ({ payload }) => {
|
|
45
|
+
console.info('[Realtime] Received update:', payload);
|
|
46
|
+
onMessageCreated(payload);
|
|
47
|
+
});
|
|
48
|
+
channel.on('broadcast', { event: 'conversation:created' }, ({ payload }) => {
|
|
49
|
+
console.info('[Realtime] Received update:', payload);
|
|
50
|
+
onConversationCreated(payload);
|
|
51
|
+
});
|
|
52
|
+
channel.subscribe((status, err) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
console.error('[Realtime] Subscription failed:', err.message || err);
|
|
55
|
+
}
|
|
56
|
+
else if (status === 'SUBSCRIBED') {
|
|
57
|
+
console.info(`[Realtime] Subscribed to channel: ${channelName}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return () => {
|
|
61
|
+
try {
|
|
62
|
+
channel.unsubscribe();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('[Realtime] Unsubscribe error:', error instanceof Error ? error.message : 'Unknown error');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error('[Realtime] Failed to setup listener:', error instanceof Error ? error.message : 'Unknown error');
|
|
71
|
+
return () => { };
|
|
72
|
+
}
|
|
73
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,13 +14,16 @@ export interface WidgetConfig {
|
|
|
14
14
|
apiKey: string;
|
|
15
15
|
themeColor?: string;
|
|
16
16
|
defaultOpen?: boolean;
|
|
17
|
+
supabaseUrl?: string;
|
|
18
|
+
supabaseAnonKey?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface ChatMessage {
|
|
19
21
|
id: string;
|
|
20
22
|
content: string;
|
|
21
|
-
role: 'user' | 'assistant';
|
|
23
|
+
role: 'user' | 'assistant' | 'system';
|
|
22
24
|
timestamp: Date;
|
|
23
25
|
sources?: ChatSource[];
|
|
26
|
+
_realUuid?: string;
|
|
24
27
|
}
|
|
25
28
|
export interface ChatSource {
|
|
26
29
|
title: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vezlo/assistant-chat",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "React component library for AI-powered chat widgets with RAG knowledge base integration and
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "React component library for AI-powered chat widgets with RAG knowledge base integration, realtime updates, and human agent support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"module": "lib/index.js",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"prepack": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@supabase/supabase-js": "^2.84.0",
|
|
34
35
|
"clsx": "^2.1.1",
|
|
36
|
+
"date-fns": "^4.1.0",
|
|
35
37
|
"lucide-react": "^0.544.0",
|
|
36
38
|
"react-router-dom": "^7.9.3",
|
|
37
39
|
"tailwindcss": "^4.1.14"
|