@vezlo/assistant-chat 1.1.1 → 1.2.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/README.md +11 -0
- package/lib/components/Widget.js +10 -6
- package/lib/components/ui/VezloFooter.js +6 -4
- package/lib/config/theme.d.ts +31 -0
- package/lib/config/theme.js +36 -1
- package/package.json +1 -1
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,6 +67,7 @@ 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
|
|
|
@@ -160,10 +163,18 @@ assistant-chat/
|
|
|
160
163
|
### How It Works
|
|
161
164
|
|
|
162
165
|
- **Same Widget Code**: Both the NPM package and standalone app use the same `Widget.tsx` component
|
|
166
|
+
- **Embed & Playground**: `WidgetPage.tsx` powers the iframe, embed script, and playground preview using that same component
|
|
163
167
|
- **NPM Package**: Publishes the widget component as a reusable library
|
|
164
168
|
- **Standalone App**: Uses the widget component directly for admin interface and playground
|
|
165
169
|
- **No Duplication**: Single source of truth for the widget component
|
|
166
170
|
|
|
171
|
+
## Layout & Auth Architecture
|
|
172
|
+
|
|
173
|
+
- `AppProvider` manages `user`, `workspace`, auth token, and exposes `login/logout` helpers plus `ProtectedRoute` for future API integration.
|
|
174
|
+
- `Header` & `MainLayout` deliver the new full-width, two-row dashboard shell (logo, workspace switcher, company badge, primary nav, profile dropdown).
|
|
175
|
+
- `ConfigPage` (dashboard) and every other route render inside `MainLayout`, so all future pages automatically inherit the header and spacing.
|
|
176
|
+
- `LoginPage` lives outside the layout and is already wired to `AppProvider`, making it ready for real authentication APIs.
|
|
177
|
+
|
|
167
178
|
## Architecture
|
|
168
179
|
|
|
169
180
|
```
|
package/lib/components/Widget.js
CHANGED
|
@@ -5,6 +5,7 @@ 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 { THEME } from '../config/theme.js';
|
|
8
9
|
export function Widget({ config, isPlayground = false, onOpen, onClose, onMessage, onError, useShadowRoot = false, }) {
|
|
9
10
|
// Use defaultOpen from config, fallback to isPlayground for backward compatibility
|
|
10
11
|
const [isOpen, setIsOpen] = useState(config.defaultOpen ?? isPlayground);
|
|
@@ -233,7 +234,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
233
234
|
} }), !isOpen && (_jsxs("div", { className: "relative animate-fadeIn", style: { pointerEvents: 'auto', width: 64, height: 64 }, children: [_jsx("div", { style: {
|
|
234
235
|
position: 'absolute',
|
|
235
236
|
inset: 0,
|
|
236
|
-
backgroundColor: config.themeColor ||
|
|
237
|
+
backgroundColor: config.themeColor || THEME.primary.hex,
|
|
237
238
|
borderRadius: 9999,
|
|
238
239
|
opacity: 0.2,
|
|
239
240
|
filter: 'blur(0px)'
|
|
@@ -243,7 +244,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
243
244
|
height: 64,
|
|
244
245
|
borderRadius: 9999,
|
|
245
246
|
color: '#fff',
|
|
246
|
-
background: `linear-gradient(135deg, ${config.themeColor}, ${config.themeColor}dd)`,
|
|
247
|
+
background: `linear-gradient(135deg, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd)`,
|
|
247
248
|
boxShadow: '0 10px 25px rgba(0,0,0,0.20)',
|
|
248
249
|
display: 'flex',
|
|
249
250
|
alignItems: 'center',
|
|
@@ -259,18 +260,21 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
|
|
|
259
260
|
pointerEvents: 'auto',
|
|
260
261
|
width: (config.size && config.size.width) ? config.size.width : 420,
|
|
261
262
|
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
|
+
}, 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) => (_jsxs("div", { className: `flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-fadeIn`, style: { animationDelay: `${index * 0.1}s` }, 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'
|
|
263
264
|
? 'text-white'
|
|
264
265
|
: 'bg-white text-gray-900 border border-gray-200'}`, style: {
|
|
265
|
-
backgroundColor: message.role === 'user' ? config.themeColor : undefined,
|
|
266
|
+
backgroundColor: message.role === 'user' ? (config.themeColor || THEME.primary.hex) : undefined,
|
|
266
267
|
boxShadow: message.role === 'user'
|
|
267
|
-
? `0 4px 12px ${config.themeColor}4D` // 4D is ~30% opacity
|
|
268
|
+
? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
|
|
268
269
|
: '0 2px 8px rgba(0, 0, 0, 0.1)'
|
|
269
270
|
}, 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'
|
|
270
271
|
? 'text-green-600'
|
|
271
272
|
: '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'
|
|
272
273
|
? 'text-red-600'
|
|
273
|
-
: '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-
|
|
274
|
+
: '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 })] }), _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: config.placeholder, disabled: isLoading, 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, 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
|
+
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" }) })] }))] }));
|
|
274
278
|
if (useShadowRoot) {
|
|
275
279
|
// Ensure host exists in DOM
|
|
276
280
|
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;
|
package/package.json
CHANGED