@vezlo/assistant-chat 1.7.0 → 1.9.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 +35 -0
- package/README.md +1 -0
- package/lib/api/message.d.ts +11 -2
- package/lib/api/message.js +4 -2
- package/lib/components/Widget.js +44 -6
- package/lib/components/ui/NextSteps.d.ts +8 -0
- package/lib/components/ui/NextSteps.js +156 -0
- package/lib/types/index.d.ts +21 -0
- package/package.json +2 -1
- package/public/widget.js +2 -1
package/PACKAGE_README.md
CHANGED
|
@@ -67,6 +67,7 @@ The `WidgetConfig` interface includes:
|
|
|
67
67
|
- `defaultOpen`: Whether widget opens by default
|
|
68
68
|
- `supabaseUrl`: Supabase project URL (optional, required for realtime updates)
|
|
69
69
|
- `supabaseAnonKey`: Supabase anon key (optional, required for realtime updates)
|
|
70
|
+
- `userContext`: Object containing user identifiers for database query filtering (optional)
|
|
70
71
|
|
|
71
72
|
### Configuration Options Table
|
|
72
73
|
|
|
@@ -85,6 +86,7 @@ The `WidgetConfig` interface includes:
|
|
|
85
86
|
| `apiKey` | string | Required | API key for authentication |
|
|
86
87
|
| `supabaseUrl` | string | Optional | Supabase project URL (for realtime updates) |
|
|
87
88
|
| `supabaseAnonKey` | string | Optional | Supabase anon key (for realtime updates) |
|
|
89
|
+
| `userContext` | object | Optional | User identifiers for database filtering (e.g., `{ user_uuid, company_uuid }`) |
|
|
88
90
|
|
|
89
91
|
## API Integration
|
|
90
92
|
|
|
@@ -156,6 +158,39 @@ function MyApp() {
|
|
|
156
158
|
}
|
|
157
159
|
```
|
|
158
160
|
|
|
161
|
+
### With User Context (Database Tools)
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { Widget } from '@vezlo/assistant-chat';
|
|
165
|
+
|
|
166
|
+
function MyApp() {
|
|
167
|
+
// Get current user from your auth system
|
|
168
|
+
const currentUser = useAuth(); // Your auth hook
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Widget
|
|
172
|
+
config={{
|
|
173
|
+
uuid: 'my-widget-123',
|
|
174
|
+
apiUrl: 'http://localhost:3000',
|
|
175
|
+
apiKey: 'your-api-key',
|
|
176
|
+
title: 'AI Assistant',
|
|
177
|
+
themeColor: '#10b981',
|
|
178
|
+
// Pass user context for database query filtering
|
|
179
|
+
userContext: {
|
|
180
|
+
user_uuid: currentUser.uuid,
|
|
181
|
+
company_uuid: currentUser.companyUuid,
|
|
182
|
+
// Optional: numeric IDs if your database uses them
|
|
183
|
+
user_id: currentUser.id,
|
|
184
|
+
company_id: currentUser.companyId
|
|
185
|
+
}
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Note:** `userContext` is optional. The widget works fine without it. If provided, it enables user-specific database queries when Database Tools are configured in the admin dashboard.
|
|
193
|
+
|
|
159
194
|
### With Callbacks
|
|
160
195
|
|
|
161
196
|
```tsx
|
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ npm run dev
|
|
|
101
101
|
- ✅ Multiple widget management
|
|
102
102
|
- ✅ **Human agent support** (conversation management, agent handoff)
|
|
103
103
|
- ✅ **Realtime updates** (live message synchronization)
|
|
104
|
+
- ✅ **Database tools** (connect external databases for natural language queries - [docs](./docs/DATABASE_TOOLS.md))
|
|
104
105
|
- ✅ Docker support
|
|
105
106
|
- ✅ Vercel deployment
|
|
106
107
|
|
package/lib/api/message.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export interface StreamChunkEvent {
|
|
|
29
29
|
document_title: string;
|
|
30
30
|
chunk_indices: number[];
|
|
31
31
|
}>;
|
|
32
|
+
validation?: {
|
|
33
|
+
confidence: number;
|
|
34
|
+
valid: boolean;
|
|
35
|
+
status: string;
|
|
36
|
+
};
|
|
32
37
|
}
|
|
33
38
|
export interface StreamCompletionEvent {
|
|
34
39
|
type: 'completion';
|
|
@@ -48,7 +53,11 @@ export interface StreamCallbacks {
|
|
|
48
53
|
document_uuid: string;
|
|
49
54
|
document_title: string;
|
|
50
55
|
chunk_indices: number[];
|
|
51
|
-
}
|
|
56
|
+
}>, validation?: {
|
|
57
|
+
confidence: number;
|
|
58
|
+
valid: boolean;
|
|
59
|
+
status: string;
|
|
60
|
+
}) => void;
|
|
52
61
|
onCompletion?: (data: StreamCompletionEvent) => void;
|
|
53
62
|
onError?: (error: StreamErrorEvent) => void;
|
|
54
63
|
onDone?: () => void;
|
|
@@ -66,7 +75,7 @@ export declare function generateAIResponse(userMessageUuid: string, apiUrl?: str
|
|
|
66
75
|
* Stream AI response using Server-Sent Events (SSE)
|
|
67
76
|
* This is the recommended approach for real-time streaming
|
|
68
77
|
*/
|
|
69
|
-
export declare function streamAIResponse(userMessageUuid: string, callbacks: StreamCallbacks, apiUrl?: string): Promise<void>;
|
|
78
|
+
export declare function streamAIResponse(userMessageUuid: string, callbacks: StreamCallbacks, apiUrl?: string, userContext?: Record<string, any>): Promise<void>;
|
|
70
79
|
/**
|
|
71
80
|
* Feedback API
|
|
72
81
|
*/
|
package/lib/api/message.js
CHANGED
|
@@ -58,14 +58,16 @@ export async function generateAIResponse(userMessageUuid, apiUrl) {
|
|
|
58
58
|
* Stream AI response using Server-Sent Events (SSE)
|
|
59
59
|
* This is the recommended approach for real-time streaming
|
|
60
60
|
*/
|
|
61
|
-
export async function streamAIResponse(userMessageUuid, callbacks, apiUrl) {
|
|
61
|
+
export async function streamAIResponse(userMessageUuid, callbacks, apiUrl, userContext) {
|
|
62
62
|
const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
|
|
63
63
|
try {
|
|
64
64
|
const response = await fetch(`${API_BASE_URL}/api/messages/${userMessageUuid}/generate`, {
|
|
65
65
|
method: 'POST',
|
|
66
66
|
headers: {
|
|
67
67
|
'Accept': 'text/event-stream',
|
|
68
|
+
'Content-Type': 'application/json',
|
|
68
69
|
},
|
|
70
|
+
body: userContext ? JSON.stringify({ user_context: userContext }) : undefined,
|
|
69
71
|
});
|
|
70
72
|
if (!response.ok) {
|
|
71
73
|
// Try to parse error as JSON first
|
|
@@ -121,7 +123,7 @@ export async function streamAIResponse(userMessageUuid, callbacks, apiUrl) {
|
|
|
121
123
|
const event = JSON.parse(data);
|
|
122
124
|
switch (event.type) {
|
|
123
125
|
case 'chunk':
|
|
124
|
-
callbacks.onChunk?.(event.content, event.done, event.sources);
|
|
126
|
+
callbacks.onChunk?.(event.content, event.done, event.sources, event.validation);
|
|
125
127
|
break;
|
|
126
128
|
case 'completion':
|
|
127
129
|
callbacks.onCompletion?.(event);
|
package/lib/components/Widget.js
CHANGED
|
@@ -189,7 +189,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
189
189
|
const tempMessageId = `streaming-${userMessageResponse.uuid}`;
|
|
190
190
|
// Stream AI response using SSE
|
|
191
191
|
await streamAIResponse(userMessageResponse.uuid, {
|
|
192
|
-
onChunk: (chunk, isDone, sources) => {
|
|
192
|
+
onChunk: (chunk, isDone, sources, validation) => {
|
|
193
193
|
// Hide loading indicator on first chunk (streaming started)
|
|
194
194
|
if (!hasReceivedChunks) {
|
|
195
195
|
hasReceivedChunks = true;
|
|
@@ -203,8 +203,8 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
203
203
|
// If this is the final chunk (done=true), convert to message immediately
|
|
204
204
|
if (isDone && !streamingComplete) {
|
|
205
205
|
streamingComplete = true;
|
|
206
|
-
console.log('Stream complete, sources:', sources);
|
|
207
|
-
// Add message to array with temp ID and
|
|
206
|
+
console.log('Stream complete, sources:', sources, 'validation:', validation);
|
|
207
|
+
// Add message to array with temp ID, sources, and validation
|
|
208
208
|
const tempMessage = {
|
|
209
209
|
id: tempMessageId,
|
|
210
210
|
content: accumulatedContent,
|
|
@@ -215,7 +215,8 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
215
215
|
document_uuid: s.document_uuid,
|
|
216
216
|
document_title: s.document_title,
|
|
217
217
|
chunk_indices: s.chunk_indices
|
|
218
|
-
})) : undefined
|
|
218
|
+
})) : undefined,
|
|
219
|
+
validation: validation
|
|
219
220
|
};
|
|
220
221
|
setStreamingMessage('');
|
|
221
222
|
setMessages((prev) => [...prev, tempMessage]);
|
|
@@ -258,7 +259,8 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
258
259
|
// Ensure loading is hidden
|
|
259
260
|
setIsLoading(false);
|
|
260
261
|
},
|
|
261
|
-
}, config.apiUrl
|
|
262
|
+
}, config.apiUrl, config.userContext // Pass user context for database filtering
|
|
263
|
+
);
|
|
262
264
|
}
|
|
263
265
|
else {
|
|
264
266
|
// Agent has joined, don't generate AI response
|
|
@@ -498,7 +500,43 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
498
500
|
const isTempId = message.id?.startsWith('streaming-') || false;
|
|
499
501
|
const hasRealUuid = !!message._realUuid;
|
|
500
502
|
const isDisabled = isTempId && !hasRealUuid;
|
|
501
|
-
return (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("
|
|
503
|
+
return (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [message.validation && (_jsx("div", { className: "relative w-5 h-5 cursor-pointer", title: `AI Validation\nConfidence: ${(message.validation.confidence * 100).toFixed(1)}%\nStatus: ${message.validation.status}${message.validation.accuracy ? `\n\nAccuracy:\n Verified: ${message.validation.accuracy.verified ? 'Yes' : 'No'}\n Rate: ${(message.validation.accuracy.verification_rate * 100).toFixed(1)}%` : ''}${message.validation.hallucination ? `\n\nHallucination:\n Detected: ${message.validation.hallucination.detected ? 'Yes' : 'No'}\n Risk: ${(message.validation.hallucination.risk * 100).toFixed(1)}%` : ''}${message.validation.context ? `\n\nContext:\n Relevance: ${(message.validation.context.source_relevance * 100).toFixed(1)}%\n Usage: ${(message.validation.context.source_usage_rate * 100).toFixed(1)}%` : ''}`, children: _jsxs("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "9.5", className: message.validation.confidence > 0.7
|
|
504
|
+
? 'fill-green-500'
|
|
505
|
+
: message.validation.confidence > 0.5
|
|
506
|
+
? 'fill-amber-500'
|
|
507
|
+
: 'fill-red-500' }), _jsx("path", { d: "M12 0.5 L13.5 3 L10.5 3 Z", className: message.validation.confidence > 0.7
|
|
508
|
+
? 'fill-green-500'
|
|
509
|
+
: message.validation.confidence > 0.5
|
|
510
|
+
? 'fill-amber-500'
|
|
511
|
+
: 'fill-red-500' }), _jsx("path", { d: "M20.5 4.5 L19 7 L18 5 Z", className: message.validation.confidence > 0.7
|
|
512
|
+
? 'fill-green-500'
|
|
513
|
+
: message.validation.confidence > 0.5
|
|
514
|
+
? 'fill-amber-500'
|
|
515
|
+
: 'fill-red-500' }), _jsx("path", { d: "M23.5 12 L21 13.5 L21 10.5 Z", className: message.validation.confidence > 0.7
|
|
516
|
+
? 'fill-green-500'
|
|
517
|
+
: message.validation.confidence > 0.5
|
|
518
|
+
? 'fill-amber-500'
|
|
519
|
+
: 'fill-red-500' }), _jsx("path", { d: "M20.5 19.5 L18 19 L19 17 Z", className: message.validation.confidence > 0.7
|
|
520
|
+
? 'fill-green-500'
|
|
521
|
+
: message.validation.confidence > 0.5
|
|
522
|
+
? 'fill-amber-500'
|
|
523
|
+
: 'fill-red-500' }), _jsx("path", { d: "M12 23.5 L10.5 21 L13.5 21 Z", className: message.validation.confidence > 0.7
|
|
524
|
+
? 'fill-green-500'
|
|
525
|
+
: message.validation.confidence > 0.5
|
|
526
|
+
? 'fill-amber-500'
|
|
527
|
+
: 'fill-red-500' }), _jsx("path", { d: "M3.5 19.5 L5 17 L6 19 Z", className: message.validation.confidence > 0.7
|
|
528
|
+
? 'fill-green-500'
|
|
529
|
+
: message.validation.confidence > 0.5
|
|
530
|
+
? 'fill-amber-500'
|
|
531
|
+
: 'fill-red-500' }), _jsx("path", { d: "M0.5 12 L3 10.5 L3 13.5 Z", className: message.validation.confidence > 0.7
|
|
532
|
+
? 'fill-green-500'
|
|
533
|
+
: message.validation.confidence > 0.5
|
|
534
|
+
? 'fill-amber-500'
|
|
535
|
+
: 'fill-red-500' }), _jsx("path", { d: "M3.5 4.5 L6 5 L5 7 Z", className: message.validation.confidence > 0.7
|
|
536
|
+
? 'fill-green-500'
|
|
537
|
+
: message.validation.confidence > 0.5
|
|
538
|
+
? 'fill-amber-500'
|
|
539
|
+
: 'fill-red-500' }), _jsx("path", { d: "M8.5 12L10.5 14L15.5 9", stroke: "white", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" })] }) })), _jsx("button", { onClick: () => handleCopyMessage(message.id), className: "p-1 rounded transition-all duration-200 hover:scale-110 cursor-pointer text-gray-400 hover:text-gray-600", title: "Copy response", children: copiedMessageId === message.id ? (_jsx(Check, { className: "w-4 h-4 text-green-600" })) : (_jsx(Copy, { className: "w-4 h-4" })) }), _jsx("button", { onClick: () => !isDisabled && handleFeedback(message.id, 'like'), disabled: isDisabled, className: `p-1 rounded transition-all duration-200 ${isDisabled
|
|
502
540
|
? 'opacity-40 cursor-not-allowed'
|
|
503
541
|
: 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'like'
|
|
504
542
|
? 'text-green-600'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChatSource } from '../../types';
|
|
2
|
+
interface NextStepsProps {
|
|
3
|
+
content: string;
|
|
4
|
+
sources?: ChatSource[];
|
|
5
|
+
}
|
|
6
|
+
export declare function extractRoutesFromText(text: string): string[];
|
|
7
|
+
export declare function NextSteps({ content, sources }: NextStepsProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowRight, Navigation, MapPin, ExternalLink } from 'lucide-react';
|
|
3
|
+
// Extract routes mentioned in text
|
|
4
|
+
export function extractRoutesFromText(text) {
|
|
5
|
+
const routes = [];
|
|
6
|
+
// Match routes like "/settings", "/login", "/widget/:uuid"
|
|
7
|
+
const routePattern = /(\/[\w\/:*-]+)/g;
|
|
8
|
+
const matches = text.matchAll(routePattern);
|
|
9
|
+
for (const match of matches) {
|
|
10
|
+
const route = match[1];
|
|
11
|
+
if (route && !routes.includes(route)) {
|
|
12
|
+
routes.push(route);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return routes;
|
|
16
|
+
}
|
|
17
|
+
export function NextSteps({ content, sources }) {
|
|
18
|
+
// Extract all routes from sources
|
|
19
|
+
const allRoutes = sources
|
|
20
|
+
?.filter(s => s.navigationMetadata?.routes && s.navigationMetadata.routes.length > 0)
|
|
21
|
+
.flatMap(s => s.navigationMetadata.routes) || [];
|
|
22
|
+
// Parse "Next Steps" section from AI response
|
|
23
|
+
const parseNextSteps = (text) => {
|
|
24
|
+
const steps = [];
|
|
25
|
+
// Look for "Next Steps:" or "**Next Steps:**" section
|
|
26
|
+
const nextStepsMatch = text.match(/(?:\*\*)?Next Steps:?\*\*(?:[\s\S]*?)(?:\n-|\n\*|\n\d+\.)\s*(.+?)(?=\n\n|\n\*\*|$)/gi);
|
|
27
|
+
if (!nextStepsMatch) {
|
|
28
|
+
// Try to find bullet points after "Next Steps"
|
|
29
|
+
const sectionMatch = text.match(/(?:Next Steps|Next steps|NEXT STEPS)[:\s]*\n((?:[-*•]\s*.+?\n?)+)/i);
|
|
30
|
+
if (sectionMatch) {
|
|
31
|
+
const bullets = sectionMatch[1].match(/[-*•]\s*(.+?)(?=\n[-*•]|\n\n|$)/g);
|
|
32
|
+
if (bullets) {
|
|
33
|
+
bullets.forEach(bullet => {
|
|
34
|
+
const stepText = bullet.replace(/^[-*•]\s*/, '').trim();
|
|
35
|
+
const route = extractRoute(stepText);
|
|
36
|
+
steps.push({
|
|
37
|
+
text: stepText,
|
|
38
|
+
route,
|
|
39
|
+
action: route ? 'navigate' : 'info'
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Parse structured next steps
|
|
47
|
+
const lines = text.split('\n');
|
|
48
|
+
let inNextSteps = false;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.match(/Next Steps/i)) {
|
|
51
|
+
inNextSteps = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (inNextSteps) {
|
|
55
|
+
// Stop at next section (## or **)
|
|
56
|
+
if (line.match(/^##|^\*\*/) && !line.match(/Next Steps/i)) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
// Match bullet points
|
|
60
|
+
const bulletMatch = line.match(/^[-*•]\s*(.+)$/);
|
|
61
|
+
if (bulletMatch) {
|
|
62
|
+
const stepText = bulletMatch[1].trim();
|
|
63
|
+
const route = extractRoute(stepText);
|
|
64
|
+
steps.push({
|
|
65
|
+
text: stepText,
|
|
66
|
+
route,
|
|
67
|
+
action: route ? 'navigate' : 'info'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// If no structured steps found, try to extract navigation suggestions from the content
|
|
74
|
+
if (steps.length === 0) {
|
|
75
|
+
const navigationPhrases = [
|
|
76
|
+
/(?:navigate to|go to|visit|access|open)\s+([\/\w:]+)/gi,
|
|
77
|
+
/(?:route|path)\s+([\/\w:]+)/gi,
|
|
78
|
+
/(?:at|to)\s+([\/\w:]+)/gi
|
|
79
|
+
];
|
|
80
|
+
navigationPhrases.forEach(pattern => {
|
|
81
|
+
const matches = text.matchAll(pattern);
|
|
82
|
+
for (const match of matches) {
|
|
83
|
+
const route = match[1];
|
|
84
|
+
if (route && allRoutes.some(r => r.path === route)) {
|
|
85
|
+
const context = text.substring(Math.max(0, match.index - 50), match.index + match[0].length + 50);
|
|
86
|
+
steps.push({
|
|
87
|
+
text: context.trim(),
|
|
88
|
+
route,
|
|
89
|
+
action: 'navigate'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return steps.slice(0, 5); // Limit to 5 steps
|
|
96
|
+
};
|
|
97
|
+
// Extract route path from text
|
|
98
|
+
const extractRoute = (text) => {
|
|
99
|
+
// Match routes like "/settings", "/login", "/widget/:uuid"
|
|
100
|
+
const routeMatch = text.match(/(\/[\w\/:*-]+)/);
|
|
101
|
+
if (routeMatch) {
|
|
102
|
+
const route = routeMatch[1];
|
|
103
|
+
// Verify it exists in available routes
|
|
104
|
+
if (allRoutes.some(r => r.path === route || route.startsWith(r.path))) {
|
|
105
|
+
return route;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
const steps = parseNextSteps(content);
|
|
111
|
+
// If no steps found, try to generate from available routes
|
|
112
|
+
if (steps.length === 0 && allRoutes.length > 0) {
|
|
113
|
+
// Generate suggestions based on common routes
|
|
114
|
+
const commonRoutes = allRoutes
|
|
115
|
+
.filter(r => !r.dynamic)
|
|
116
|
+
.slice(0, 3)
|
|
117
|
+
.map(r => ({
|
|
118
|
+
text: `Navigate to ${r.path}`,
|
|
119
|
+
route: r.path,
|
|
120
|
+
action: 'navigate'
|
|
121
|
+
}));
|
|
122
|
+
if (commonRoutes.length > 0) {
|
|
123
|
+
return (_jsxs("div", { className: "mt-3 bg-gradient-to-r from-emerald-50 to-teal-50 border-2 border-emerald-300 rounded-lg p-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-emerald-600 flex-shrink-0" }), _jsx("div", { className: "text-sm font-bold text-emerald-900", children: "\uD83E\uDDED Suggested Navigation" })] }), _jsx("div", { className: "space-y-2", children: commonRoutes.map((step, idx) => {
|
|
124
|
+
const fullUrl = step.route && typeof window !== 'undefined' ? `${window.location.origin}${step.route}` : step.route;
|
|
125
|
+
return (_jsxs("button", { onClick: () => {
|
|
126
|
+
if (fullUrl) {
|
|
127
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
128
|
+
window.location.href = fullUrl;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
navigator.clipboard.writeText(fullUrl);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, className: "w-full text-left px-3 py-2 bg-white border-2 border-emerald-400 rounded-md text-sm font-medium text-emerald-900 hover:bg-emerald-50 hover:border-emerald-500 hover:shadow-md transition-all flex items-center justify-between group cursor-pointer", title: `Click to navigate to ${fullUrl}`, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(MapPin, { className: "w-4 h-4 text-emerald-600" }), _jsx("span", { children: step.text })] }), _jsx(ArrowRight, { className: "w-4 h-4 text-emerald-600 opacity-0 group-hover:opacity-100 transition-opacity" })] }, idx));
|
|
135
|
+
}) })] }));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (steps.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return (_jsxs("div", { className: "mt-3 bg-gradient-to-r from-emerald-50 to-teal-50 border-2 border-emerald-300 rounded-lg p-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-emerald-600 flex-shrink-0" }), _jsx("div", { className: "text-sm font-bold text-emerald-900", children: "\uD83E\uDDED Next Steps" })] }), _jsx("div", { className: "space-y-2", children: steps.map((step, idx) => {
|
|
142
|
+
const fullUrl = step.route && typeof window !== 'undefined' ? `${window.location.origin}${step.route}` : step.route;
|
|
143
|
+
return (_jsxs("button", { onClick: () => {
|
|
144
|
+
if (fullUrl) {
|
|
145
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
146
|
+
window.location.href = fullUrl;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
navigator.clipboard.writeText(fullUrl);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}, className: `w-full text-left px-3 py-2 rounded-md text-sm transition-all flex items-start gap-2 group ${step.route
|
|
153
|
+
? 'bg-white border-2 border-emerald-400 text-emerald-900 hover:bg-emerald-50 hover:border-emerald-500 hover:shadow-md font-medium cursor-pointer'
|
|
154
|
+
: 'bg-emerald-100/50 border border-emerald-300 text-emerald-800 cursor-default'}`, title: fullUrl ? `Click to navigate to ${fullUrl}` : undefined, children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [step.route ? (_jsx(MapPin, { className: "w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" })) : (_jsx(ArrowRight, { className: "w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" })), _jsx("span", { className: "flex-1", children: step.text })] }), step.route && (_jsx(ExternalLink, { className: "w-4 h-4 text-emerald-600 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5" }))] }, idx));
|
|
155
|
+
}) })] }));
|
|
156
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface WidgetConfig {
|
|
|
16
16
|
defaultOpen?: boolean;
|
|
17
17
|
supabaseUrl?: string;
|
|
18
18
|
supabaseAnonKey?: string;
|
|
19
|
+
userContext?: Record<string, any>;
|
|
19
20
|
}
|
|
20
21
|
export interface ChatMessage {
|
|
21
22
|
id: string;
|
|
@@ -24,6 +25,26 @@ export interface ChatMessage {
|
|
|
24
25
|
type?: 'user' | 'assistant' | 'agent' | 'system';
|
|
25
26
|
timestamp: Date;
|
|
26
27
|
sources?: ChatSource[];
|
|
28
|
+
validation?: {
|
|
29
|
+
confidence: number;
|
|
30
|
+
valid: boolean;
|
|
31
|
+
status: string;
|
|
32
|
+
accuracy?: {
|
|
33
|
+
verified: boolean;
|
|
34
|
+
verification_rate: number;
|
|
35
|
+
reason?: string;
|
|
36
|
+
};
|
|
37
|
+
hallucination?: {
|
|
38
|
+
detected: boolean;
|
|
39
|
+
risk: number;
|
|
40
|
+
reason?: string;
|
|
41
|
+
};
|
|
42
|
+
context?: {
|
|
43
|
+
source_relevance: number;
|
|
44
|
+
source_usage_rate: number;
|
|
45
|
+
valid: boolean;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
27
48
|
_realUuid?: string;
|
|
28
49
|
}
|
|
29
50
|
export interface ChatSource {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vezlo/assistant-chat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"dompurify": "^3.3.1",
|
|
38
38
|
"lucide-react": "^0.544.0",
|
|
39
39
|
"marked": "^17.0.1",
|
|
40
|
+
"react-hot-toast": "^2.6.0",
|
|
40
41
|
"react-router-dom": "^7.9.3",
|
|
41
42
|
"tailwindcss": "^4.1.14"
|
|
42
43
|
},
|
package/public/widget.js
CHANGED
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
placeholder: 'Type your message...',
|
|
37
37
|
welcomeMessage: "Hello! I'm your AI assistant. How can I help you today?",
|
|
38
38
|
apiKey: '',
|
|
39
|
-
themeColor: '#059669'
|
|
39
|
+
themeColor: '#059669',
|
|
40
|
+
userContext: {} // Optional: user context for database filtering
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
// Merge with user config
|