@vezlo/assistant-chat 1.1.1 β 1.3.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 +11 -2
- package/README.md +21 -0
- 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/components/Widget.js +120 -46
- package/lib/components/ui/VezloFooter.js +6 -4
- package/lib/config/theme.d.ts +31 -0
- package/lib/config/theme.js +36 -1
- package/lib/services/conversationRealtime.d.ts +27 -0
- package/lib/services/conversationRealtime.js +68 -0
- package/lib/types/index.d.ts +3 -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
|
|
|
@@ -83,6 +91,7 @@ This widget requires a running Assistant Server instance. The widget will:
|
|
|
83
91
|
1. Create conversations automatically
|
|
84
92
|
2. Send user messages to the server
|
|
85
93
|
3. Stream AI responses in real-time
|
|
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
|
@@ -20,6 +20,8 @@ A complete chat widget solution with both a React component library and standalo
|
|
|
20
20
|
- **Embed Code Generator**: Get ready-to-use embed codes
|
|
21
21
|
- **Docker Support**: Easy deployment with Docker Compose
|
|
22
22
|
- **Vercel Ready**: One-click deployment to Vercel
|
|
23
|
+
- **Shared Layout**: `MainLayout` + `Header` provide a consistent, Vercel-style shell across every page
|
|
24
|
+
- **Auth-Ready Context**: `AppProvider` exposes user/workspace state, token helpers, and `ProtectedRoute` support for future login flows
|
|
23
25
|
|
|
24
26
|
## Quick Start
|
|
25
27
|
|
|
@@ -65,12 +67,17 @@ npm run dev
|
|
|
65
67
|
- Live preview and playground
|
|
66
68
|
- Embed code generation
|
|
67
69
|
- Docker and Vercel deployment support
|
|
70
|
+
- Theme + widget matrices: see [`docs/THEME_WIDGET_CONFIG.md`](docs/THEME_WIDGET_CONFIG.md) for every field, color mapping tips, and widget overrides
|
|
68
71
|
|
|
69
72
|
## Prerequisites
|
|
70
73
|
|
|
71
74
|
- **Assistant Server**: Both components require a running Assistant Server
|
|
72
75
|
- Node.js 18+ and npm
|
|
73
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
|
|
74
81
|
|
|
75
82
|
## Features
|
|
76
83
|
|
|
@@ -79,6 +86,7 @@ npm run dev
|
|
|
79
86
|
- β
TypeScript support
|
|
80
87
|
- β
Tailwind CSS styling
|
|
81
88
|
- β
Real-time streaming
|
|
89
|
+
- β
**Realtime updates** (agent handoff, live message sync)
|
|
82
90
|
- β
Customizable themes
|
|
83
91
|
- β
Shadow DOM support
|
|
84
92
|
- β
API integration included
|
|
@@ -89,6 +97,8 @@ npm run dev
|
|
|
89
97
|
- β
Playground testing
|
|
90
98
|
- β
Embed code generation
|
|
91
99
|
- β
Multiple widget management
|
|
100
|
+
- β
**Human agent support** (conversation management, agent handoff)
|
|
101
|
+
- β
**Realtime updates** (live message synchronization)
|
|
92
102
|
- β
Docker support
|
|
93
103
|
- β
Vercel deployment
|
|
94
104
|
|
|
@@ -126,6 +136,9 @@ vercel
|
|
|
126
136
|
# Set environment variables (required)
|
|
127
137
|
vercel env add VITE_ASSISTANT_SERVER_URL
|
|
128
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
|
|
129
142
|
|
|
130
143
|
# Deploy to production
|
|
131
144
|
vercel --prod
|
|
@@ -160,10 +173,18 @@ assistant-chat/
|
|
|
160
173
|
### How It Works
|
|
161
174
|
|
|
162
175
|
- **Same Widget Code**: Both the NPM package and standalone app use the same `Widget.tsx` component
|
|
176
|
+
- **Embed & Playground**: `WidgetPage.tsx` powers the iframe, embed script, and playground preview using that same component
|
|
163
177
|
- **NPM Package**: Publishes the widget component as a reusable library
|
|
164
178
|
- **Standalone App**: Uses the widget component directly for admin interface and playground
|
|
165
179
|
- **No Duplication**: Single source of truth for the widget component
|
|
166
180
|
|
|
181
|
+
## Layout & Auth Architecture
|
|
182
|
+
|
|
183
|
+
- `AppProvider` manages `user`, `workspace`, auth token, and exposes `login/logout` helpers plus `ProtectedRoute` for future API integration.
|
|
184
|
+
- `Header` & `MainLayout` deliver the new full-width, two-row dashboard shell (logo, workspace switcher, company badge, primary nav, profile dropdown).
|
|
185
|
+
- `ConfigPage` (dashboard) and every other route render inside `MainLayout`, so all future pages automatically inherit the header and spacing.
|
|
186
|
+
- `LoginPage` lives outside the layout and is already wired to `AppProvider`, making it ready for real authentication APIs.
|
|
187
|
+
|
|
167
188
|
## Architecture
|
|
168
189
|
|
|
169
190
|
```
|
|
@@ -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/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
7
|
import { createConversation, createUserMessage, generateAIResponse } from '../api/index.js';
|
|
8
|
+
import { subscribeToConversations } from '../services/conversationRealtime.js';
|
|
9
|
+
import { THEME } from '../config/theme.js';
|
|
8
10
|
export function Widget({ config, isPlayground = false, onOpen, onClose, onMessage, onError, useShadowRoot = false, }) {
|
|
9
11
|
// Use defaultOpen from config, fallback to isPlayground for backward compatibility
|
|
10
12
|
const [isOpen, setIsOpen] = useState(config.defaultOpen ?? isPlayground);
|
|
@@ -20,6 +22,9 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
20
22
|
const [messageFeedback, setMessageFeedback] = useState({});
|
|
21
23
|
const [streamingMessage, setStreamingMessage] = useState('');
|
|
22
24
|
const [conversationUuid, setConversationUuid] = useState(null);
|
|
25
|
+
const [companyUuid, setCompanyUuid] = useState(null);
|
|
26
|
+
const [agentJoined, setAgentJoined] = useState(false);
|
|
27
|
+
const [conversationClosed, setConversationClosed] = useState(false);
|
|
23
28
|
const [isCreatingConversation, setIsCreatingConversation] = useState(false);
|
|
24
29
|
const messagesEndRef = useRef(null);
|
|
25
30
|
const hostRef = useRef(null);
|
|
@@ -66,7 +71,10 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
66
71
|
title: 'New Chat',
|
|
67
72
|
}, config.apiUrl);
|
|
68
73
|
setConversationUuid(conversation.uuid);
|
|
69
|
-
|
|
74
|
+
setCompanyUuid(conversation.company_uuid);
|
|
75
|
+
setAgentJoined(false);
|
|
76
|
+
setConversationClosed(false);
|
|
77
|
+
console.log('[Widget] Conversation created:', conversation.uuid, 'Company:', conversation.company_uuid);
|
|
70
78
|
// Add welcome message after conversation is created
|
|
71
79
|
const welcomeMsg = {
|
|
72
80
|
id: generateId(),
|
|
@@ -97,12 +105,45 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
97
105
|
};
|
|
98
106
|
initializeConversation();
|
|
99
107
|
}, [isOpen, conversationUuid, isCreatingConversation, config.welcomeMessage, onMessage, onError]);
|
|
108
|
+
// Subscribe to realtime updates for agent messages
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!companyUuid || !conversationUuid) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const handleMessageCreated = (payload) => {
|
|
114
|
+
if (payload.conversation_uuid !== conversationUuid) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const status = payload.conversation_update?.status;
|
|
118
|
+
if (status === 'in_progress') {
|
|
119
|
+
setAgentJoined(true);
|
|
120
|
+
setConversationClosed(false);
|
|
121
|
+
}
|
|
122
|
+
else if (status === 'closed') {
|
|
123
|
+
setAgentJoined(false);
|
|
124
|
+
setConversationClosed(true);
|
|
125
|
+
}
|
|
126
|
+
if (payload.message.type !== 'system' && payload.message.type !== 'agent') {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const newMessage = {
|
|
130
|
+
id: payload.message.uuid,
|
|
131
|
+
content: payload.message.content,
|
|
132
|
+
role: payload.message.type === 'agent' ? 'assistant' : 'system',
|
|
133
|
+
timestamp: new Date(payload.message.created_at),
|
|
134
|
+
};
|
|
135
|
+
setMessages(prev => [...prev, newMessage]);
|
|
136
|
+
};
|
|
137
|
+
const cleanup = subscribeToConversations(companyUuid, handleMessageCreated, () => { }, // No need to handle conversation:created in widget
|
|
138
|
+
config.supabaseUrl, config.supabaseAnonKey);
|
|
139
|
+
return cleanup;
|
|
140
|
+
}, [companyUuid, conversationUuid, config.supabaseUrl, config.supabaseAnonKey]);
|
|
100
141
|
useEffect(() => {
|
|
101
142
|
// Scroll to bottom when messages change or when streaming
|
|
102
143
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
103
144
|
}, [messages, streamingMessage]);
|
|
104
145
|
const handleSendMessage = async () => {
|
|
105
|
-
if (!input.trim() || isLoading || !conversationUuid)
|
|
146
|
+
if (!input.trim() || isLoading || !conversationUuid || conversationClosed)
|
|
106
147
|
return;
|
|
107
148
|
const userMessageContent = input;
|
|
108
149
|
const userMessage = {
|
|
@@ -123,35 +164,42 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
123
164
|
console.log('[Widget] User message created:', userMessageResponse.uuid);
|
|
124
165
|
// Update the user message with the actual UUID from server
|
|
125
166
|
setMessages((prev) => prev.map((msg) => msg.id === userMessage.id ? { ...msg, id: userMessageResponse.uuid } : msg));
|
|
126
|
-
// Step 2: Generate AI response
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
currentText
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
// Step 2: Generate AI response (only if agent hasn't joined)
|
|
168
|
+
if (!agentJoined) {
|
|
169
|
+
// Keep loading indicator visible until AI response is received
|
|
170
|
+
const aiResponse = await generateAIResponse(userMessageResponse.uuid, config.apiUrl);
|
|
171
|
+
console.log('[Widget] AI response received:', aiResponse.uuid);
|
|
172
|
+
// Hide loading indicator now that we have the response
|
|
173
|
+
setIsLoading(false);
|
|
174
|
+
// Stream the AI response character by character
|
|
175
|
+
const responseContent = aiResponse.content;
|
|
176
|
+
setStreamingMessage('');
|
|
177
|
+
let currentText = '';
|
|
178
|
+
const streamInterval = setInterval(() => {
|
|
179
|
+
if (currentText.length < responseContent.length) {
|
|
180
|
+
currentText += responseContent[currentText.length];
|
|
181
|
+
setStreamingMessage(currentText);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
clearInterval(streamInterval);
|
|
185
|
+
// Add the complete message to messages array
|
|
186
|
+
const assistantMessage = {
|
|
187
|
+
id: aiResponse.uuid,
|
|
188
|
+
content: responseContent,
|
|
189
|
+
role: 'assistant',
|
|
190
|
+
timestamp: new Date(aiResponse.created_at),
|
|
191
|
+
};
|
|
192
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
193
|
+
onMessage?.(assistantMessage);
|
|
194
|
+
setStreamingMessage('');
|
|
195
|
+
}
|
|
196
|
+
}, 15); // 15ms delay between characters for smooth streaming
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Agent has joined, don't generate AI response
|
|
200
|
+
setIsLoading(false);
|
|
201
|
+
console.log('[Widget] Skipping AI response - agent has joined');
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
catch (error) {
|
|
157
205
|
console.error('[Widget] Error sending message:', error);
|
|
@@ -168,7 +216,22 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
168
216
|
onMessage?.(errorMessage);
|
|
169
217
|
}
|
|
170
218
|
};
|
|
219
|
+
const handleStartNewChat = () => {
|
|
220
|
+
if (isCreatingConversation)
|
|
221
|
+
return;
|
|
222
|
+
setMessages([]);
|
|
223
|
+
setStreamingMessage('');
|
|
224
|
+
setIsLoading(false);
|
|
225
|
+
setMessageFeedback({});
|
|
226
|
+
setAgentJoined(false);
|
|
227
|
+
setConversationClosed(false);
|
|
228
|
+
setConversationUuid(null);
|
|
229
|
+
setCompanyUuid(null);
|
|
230
|
+
setInput('');
|
|
231
|
+
};
|
|
171
232
|
const handleKeyPress = (e) => {
|
|
233
|
+
if (conversationClosed)
|
|
234
|
+
return;
|
|
172
235
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
173
236
|
e.preventDefault();
|
|
174
237
|
handleSendMessage();
|
|
@@ -233,7 +296,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
233
296
|
} }), !isOpen && (_jsxs("div", { className: "relative animate-fadeIn", style: { pointerEvents: 'auto', width: 64, height: 64 }, children: [_jsx("div", { style: {
|
|
234
297
|
position: 'absolute',
|
|
235
298
|
inset: 0,
|
|
236
|
-
backgroundColor: config.themeColor ||
|
|
299
|
+
backgroundColor: config.themeColor || THEME.primary.hex,
|
|
237
300
|
borderRadius: 9999,
|
|
238
301
|
opacity: 0.2,
|
|
239
302
|
filter: 'blur(0px)'
|
|
@@ -243,7 +306,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
243
306
|
height: 64,
|
|
244
307
|
borderRadius: 9999,
|
|
245
308
|
color: '#fff',
|
|
246
|
-
background: `linear-gradient(135deg, ${config.themeColor}, ${config.themeColor}dd)`,
|
|
309
|
+
background: `linear-gradient(135deg, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd)`,
|
|
247
310
|
boxShadow: '0 10px 25px rgba(0,0,0,0.20)',
|
|
248
311
|
display: 'flex',
|
|
249
312
|
alignItems: 'center',
|
|
@@ -259,18 +322,29 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
259
322
|
pointerEvents: 'auto',
|
|
260
323
|
width: (config.size && config.size.width) ? config.size.width : 420,
|
|
261
324
|
height: (config.size && config.size.height) ? config.size.height : 600
|
|
262
|
-
}, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor}, ${config.themeColor}dd, ${config.themeColor}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-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
325
|
+
}, 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) => (_jsx("div", { className: `flex ${message.role === 'system'
|
|
326
|
+
? 'justify-center'
|
|
327
|
+
: message.role === 'user'
|
|
328
|
+
? 'justify-end'
|
|
329
|
+
: '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')
|
|
330
|
+
? 'bg-red-50 text-red-700 border-red-200'
|
|
331
|
+
: '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'
|
|
332
|
+
? 'text-white'
|
|
333
|
+
: 'bg-white text-gray-900 border border-gray-200'}`, style: {
|
|
334
|
+
backgroundColor: message.role === 'user' ? (config.themeColor || THEME.primary.hex) : undefined,
|
|
335
|
+
boxShadow: message.role === 'user'
|
|
336
|
+
? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
|
|
337
|
+
: '0 2px 8px rgba(0, 0, 0, 0.1)'
|
|
338
|
+
}, 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' && (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => handleFeedback(message.id, 'like'), className: `p-1 rounded transition-all duration-200 hover:scale-110 cursor-pointer ${messageFeedback[message.id] === 'like'
|
|
339
|
+
? 'text-green-600'
|
|
340
|
+
: 'text-gray-400 hover:text-green-600'}`, children: _jsx(ThumbsUp, { className: `w-4 h-4 ${messageFeedback[message.id] === 'like' ? 'fill-current' : ''}` }) }), _jsx("button", { onClick: () => handleFeedback(message.id, 'dislike'), className: `p-1 rounded transition-all duration-200 hover:scale-110 cursor-pointer ${messageFeedback[message.id] === 'dislike'
|
|
341
|
+
? 'text-red-600'
|
|
342
|
+
: 'text-gray-400 hover:text-red-600'}`, children: _jsx(ThumbsDown, { className: `w-4 h-4 ${messageFeedback[message.id] === 'dislike' ? 'fill-current' : ''}` }) })] }))] })] })] })) }, 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", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyPress: handleKeyPress, placeholder: conversationClosed
|
|
343
|
+
? 'Conversation closed. Start a new chat to continue.'
|
|
344
|
+
: 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: {
|
|
345
|
+
background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd)`,
|
|
346
|
+
opacity: (!input.trim() || isLoading || conversationClosed) ? 0.6 : 1
|
|
347
|
+
}, 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" }) })] }))] }));
|
|
274
348
|
if (useShadowRoot) {
|
|
275
349
|
// Ensure host exists in DOM
|
|
276
350
|
return (_jsx("div", { ref: hostRef, style: { all: 'initial' }, children: shadowReady && shadowMountRef.current ? createPortal(content, shadowMountRef.current) : null }));
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
export function VezloFooter({ size = '
|
|
4
|
-
const iconSize = size === 'sm' ? 'w-3 h-3' : 'w-4 h-4';
|
|
2
|
+
import { Zap } from 'lucide-react';
|
|
3
|
+
export function VezloFooter({ size = 'sm' }) {
|
|
5
4
|
const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
|
|
6
|
-
|
|
5
|
+
const logoHeight = size === 'sm' ? 48 : 68;
|
|
6
|
+
const iconSize = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5';
|
|
7
|
+
const gapClass = 'gap-1';
|
|
8
|
+
return (_jsxs("div", { className: `flex items-center justify-center ${gapClass}`, children: [_jsx("span", { className: `${textSize} text-gray-600 font-medium`, children: "Powered by" }), _jsx(Zap, { className: `${iconSize} flex-shrink-0 -mr-1`, style: { color: '#f5c518' } }), _jsx("img", { src: "/assets/vezlo.png", alt: "Vezlo", style: { height: `${logoHeight}px`, width: 'auto' }, className: "object-contain" })] }));
|
|
7
9
|
}
|
package/lib/config/theme.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export declare const THEME: {
|
|
|
6
6
|
readonly primary: {
|
|
7
7
|
readonly hex: "#059669";
|
|
8
8
|
readonly tailwind: "emerald";
|
|
9
|
+
readonly darker: "#047857";
|
|
10
|
+
readonly lighter: "#10b981";
|
|
11
|
+
readonly lightest: "#d1fae5";
|
|
9
12
|
};
|
|
10
13
|
readonly colors: {
|
|
11
14
|
readonly bg: "bg-emerald-600";
|
|
@@ -18,6 +21,34 @@ export declare const THEME: {
|
|
|
18
21
|
readonly border: "border-emerald-600";
|
|
19
22
|
readonly borderLight: "border-emerald-200";
|
|
20
23
|
};
|
|
24
|
+
readonly typography: {
|
|
25
|
+
readonly fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
|
|
26
|
+
readonly heading: {
|
|
27
|
+
readonly weight: "600";
|
|
28
|
+
readonly size: {
|
|
29
|
+
readonly sm: "0.875rem";
|
|
30
|
+
readonly md: "1rem";
|
|
31
|
+
readonly lg: "1.125rem";
|
|
32
|
+
readonly xl: "1.25rem";
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
readonly body: {
|
|
36
|
+
readonly weight: "400";
|
|
37
|
+
readonly size: {
|
|
38
|
+
readonly sm: "0.75rem";
|
|
39
|
+
readonly md: "0.875rem";
|
|
40
|
+
readonly lg: "1rem";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
readonly spacing: {
|
|
45
|
+
readonly xs: "0.5rem";
|
|
46
|
+
readonly sm: "0.75rem";
|
|
47
|
+
readonly md: "1rem";
|
|
48
|
+
readonly lg: "1.5rem";
|
|
49
|
+
readonly xl: "2rem";
|
|
50
|
+
};
|
|
21
51
|
};
|
|
22
52
|
export declare const getButtonGradient: (color?: string) => string;
|
|
23
53
|
export declare const getHeaderGradient: (color?: string) => string;
|
|
54
|
+
export declare const getHoverColor: () => "#047857";
|
package/lib/config/theme.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
* Change these values to update colors across the entire application
|
|
4
4
|
*/
|
|
5
5
|
export const THEME = {
|
|
6
|
-
// Primary brand color (emerald)
|
|
6
|
+
// Primary brand color (emerald/teal)
|
|
7
7
|
primary: {
|
|
8
8
|
hex: '#059669',
|
|
9
9
|
tailwind: 'emerald',
|
|
10
|
+
// Color variants for different use cases
|
|
11
|
+
darker: '#047857', // For hover states
|
|
12
|
+
lighter: '#10b981', // For accents
|
|
13
|
+
lightest: '#d1fae5', // For backgrounds
|
|
10
14
|
},
|
|
11
15
|
// Tailwind color variants
|
|
12
16
|
colors: {
|
|
@@ -20,6 +24,35 @@ export const THEME = {
|
|
|
20
24
|
border: 'border-emerald-600',
|
|
21
25
|
borderLight: 'border-emerald-200',
|
|
22
26
|
},
|
|
27
|
+
// Typography
|
|
28
|
+
typography: {
|
|
29
|
+
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
30
|
+
heading: {
|
|
31
|
+
weight: '600',
|
|
32
|
+
size: {
|
|
33
|
+
sm: '0.875rem',
|
|
34
|
+
md: '1rem',
|
|
35
|
+
lg: '1.125rem',
|
|
36
|
+
xl: '1.25rem',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
body: {
|
|
40
|
+
weight: '400',
|
|
41
|
+
size: {
|
|
42
|
+
sm: '0.75rem',
|
|
43
|
+
md: '0.875rem',
|
|
44
|
+
lg: '1rem',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
// Spacing (for consistency)
|
|
49
|
+
spacing: {
|
|
50
|
+
xs: '0.5rem',
|
|
51
|
+
sm: '0.75rem',
|
|
52
|
+
md: '1rem',
|
|
53
|
+
lg: '1.5rem',
|
|
54
|
+
xl: '2rem',
|
|
55
|
+
},
|
|
23
56
|
};
|
|
24
57
|
// Helper function to get gradient for buttons
|
|
25
58
|
export const getButtonGradient = (color = THEME.primary.hex) => {
|
|
@@ -29,3 +62,5 @@ export const getButtonGradient = (color = THEME.primary.hex) => {
|
|
|
29
62
|
export const getHeaderGradient = (color = THEME.primary.hex) => {
|
|
30
63
|
return `linear-gradient(to right, ${color}, ${color}dd, ${color}bb)`;
|
|
31
64
|
};
|
|
65
|
+
// Helper function to get darker shade for hover states
|
|
66
|
+
export const getHoverColor = () => THEME.primary.darker;
|
|
@@ -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,68 @@
|
|
|
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
|
+
realtime: { params: { eventsPerSecond: 2 } },
|
|
15
|
+
});
|
|
16
|
+
clientUrl = supabaseUrl;
|
|
17
|
+
clientKey = supabaseAnonKey;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('[ConversationRealtime] Failed to initialize client:', error instanceof Error ? error.message : 'Unknown error');
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return client;
|
|
25
|
+
}
|
|
26
|
+
export function subscribeToConversations(companyUuid, onMessageCreated, onConversationCreated, supabaseUrl, supabaseAnonKey) {
|
|
27
|
+
try {
|
|
28
|
+
const realtimeClient = getClient(supabaseUrl, supabaseAnonKey);
|
|
29
|
+
if (!realtimeClient) {
|
|
30
|
+
console.error('[ConversationRealtime] Client not available');
|
|
31
|
+
return () => { };
|
|
32
|
+
}
|
|
33
|
+
const channelName = `company:${companyUuid}:conversations`;
|
|
34
|
+
const channel = realtimeClient.channel(channelName, {
|
|
35
|
+
config: {
|
|
36
|
+
broadcast: { self: true, ack: false }
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
channel.on('broadcast', { event: 'message:created' }, ({ payload }) => {
|
|
40
|
+
console.info('[Realtime] Received update:', payload);
|
|
41
|
+
onMessageCreated(payload);
|
|
42
|
+
});
|
|
43
|
+
channel.on('broadcast', { event: 'conversation:created' }, ({ payload }) => {
|
|
44
|
+
console.info('[Realtime] Received update:', payload);
|
|
45
|
+
onConversationCreated(payload);
|
|
46
|
+
});
|
|
47
|
+
channel.subscribe((status, err) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
console.error('[Realtime] Subscription failed:', err.message || err);
|
|
50
|
+
}
|
|
51
|
+
else if (status === 'SUBSCRIBED') {
|
|
52
|
+
console.info(`[Realtime] Subscribed to channel: ${channelName}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
try {
|
|
57
|
+
channel.unsubscribe();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error('[Realtime] Unsubscribe error:', error instanceof Error ? error.message : 'Unknown error');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('[Realtime] Failed to setup listener:', error instanceof Error ? error.message : 'Unknown error');
|
|
66
|
+
return () => { };
|
|
67
|
+
}
|
|
68
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,11 +14,13 @@ 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[];
|
|
24
26
|
}
|
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.3.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"
|