@vezlo/assistant-chat 1.5.0 → 1.7.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 CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  A React component library for integrating AI assistant chat functionality into web applications with realtime updates and human agent support.
6
6
 
7
+ **📋 [Changelog](https://github.com/vezlo/assistant-chat/blob/main/CHANGELOG.md)** | **🐛 [Report Issue](https://github.com/vezlo/assistant-chat/issues)**
8
+
7
9
  > **📦 This is the NPM package documentation**
8
10
  > **🏠 Repository**: [assistant-chat](https://github.com/vezlo/assistant-chat) - Contains both this NPM package and a standalone admin application
9
11
  > **🖥️ Standalone App**: Want to run the admin dashboard? Visit the [main repository](https://github.com/vezlo/assistant-chat) for setup instructions
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  A complete chat widget solution with both a React component library and standalone admin application for AI-powered customer support.
6
6
 
7
+ **📋 [Changelog](./CHANGELOG.md)** | **🐛 [Report Issue](https://github.com/vezlo/assistant-chat/issues)**
8
+
7
9
  ## What's Included
8
10
 
9
11
  ### 📦 NPM Package
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Citation API Service
3
+ */
4
+ export interface CitationContext {
5
+ document_title: string;
6
+ document_type: string;
7
+ file_type?: string;
8
+ content: string;
9
+ }
10
+ export declare function getCitationContext(documentUuid: string, chunkIndices: number[], apiUrl?: string): Promise<CitationContext>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Citation API Service
3
+ */
4
+ const DEFAULT_API_BASE_URL = import.meta.env.VITE_ASSISTANT_SERVER_URL || 'http://localhost:3000';
5
+ export async function getCitationContext(documentUuid, chunkIndices, apiUrl) {
6
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
7
+ const indicesParam = chunkIndices.join(',');
8
+ const response = await fetch(`${API_BASE_URL}/api/knowledge/citations/${documentUuid}/context?chunk_indices=${indicesParam}`, {
9
+ method: 'GET',
10
+ headers: {
11
+ 'Accept': 'application/json',
12
+ },
13
+ });
14
+ if (!response.ok) {
15
+ throw new Error('Failed to fetch citation context');
16
+ }
17
+ return await response.json();
18
+ }
@@ -21,6 +21,7 @@ export interface ConversationListItem {
21
21
  last_message_at: string | null;
22
22
  joined_at: string | null;
23
23
  closed_at: string | null;
24
+ archived_at: string | null;
24
25
  created_at: string;
25
26
  updated_at: string;
26
27
  }
@@ -64,7 +65,7 @@ export declare function getConversation(uuid: string, apiUrl?: string): Promise<
64
65
  /**
65
66
  * Get paginated conversations for agent UI
66
67
  */
67
- export declare function getConversations(token: string, page?: number, pageSize?: number, orderBy?: string, apiUrl?: string): Promise<ConversationListResponse>;
68
+ export declare function getConversations(token: string, page?: number, pageSize?: number, orderBy?: string, apiUrl?: string, status?: 'active' | 'archived'): Promise<ConversationListResponse>;
68
69
  /**
69
70
  * Get messages within a conversation
70
71
  */
@@ -77,6 +78,13 @@ export declare function joinConversation(token: string, conversationUuid: string
77
78
  * Close a conversation as an agent
78
79
  */
79
80
  export declare function closeConversation(token: string, conversationUuid: string, apiUrl?: string): Promise<JoinConversationResponse>;
81
+ /**
82
+ * Archive a conversation as an agent
83
+ */
84
+ export declare function archiveConversation(token: string, conversationUuid: string, apiUrl?: string): Promise<{
85
+ success: boolean;
86
+ archived_at: string;
87
+ }>;
80
88
  /**
81
89
  * Send agent-authored message
82
90
  */
@@ -61,9 +61,10 @@ export async function getConversation(uuid, apiUrl) {
61
61
  /**
62
62
  * Get paginated conversations for agent UI
63
63
  */
64
- export async function getConversations(token, page = 1, pageSize = 20, orderBy = 'last_message_at', apiUrl) {
64
+ export async function getConversations(token, page = 1, pageSize = 20, orderBy = 'last_message_at', apiUrl, status) {
65
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}`, {
66
+ const statusParam = status ? `&status=${status}` : '';
67
+ const response = await fetch(`${API_BASE_URL}/api/conversations?page=${page}&page_size=${pageSize}&order_by=${orderBy}${statusParam}`, {
67
68
  method: 'GET',
68
69
  headers: {
69
70
  Accept: 'application/json',
@@ -130,6 +131,24 @@ export async function closeConversation(token, conversationUuid, apiUrl) {
130
131
  }
131
132
  return (await response.json());
132
133
  }
134
+ /**
135
+ * Archive a conversation as an agent
136
+ */
137
+ export async function archiveConversation(token, conversationUuid, apiUrl) {
138
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
139
+ const response = await fetch(`${API_BASE_URL}/api/conversations/${conversationUuid}/archive`, {
140
+ method: 'POST',
141
+ headers: {
142
+ Accept: 'application/json',
143
+ Authorization: `Bearer ${token}`,
144
+ },
145
+ });
146
+ if (!response.ok) {
147
+ const message = await parseErrorMessage(response);
148
+ throw new Error(message);
149
+ }
150
+ return (await response.json());
151
+ }
133
152
  /**
134
153
  * Send agent-authored message
135
154
  */
@@ -6,3 +6,4 @@ export * from './auth.js';
6
6
  export * from './conversation.js';
7
7
  export * from './message.js';
8
8
  export * from './analytics.js';
9
+ export * from './citation.js';
package/lib/api/index.js CHANGED
@@ -6,3 +6,4 @@ export * from './auth.js';
6
6
  export * from './conversation.js';
7
7
  export * from './message.js';
8
8
  export * from './analytics.js';
9
+ export * from './citation.js';
@@ -24,6 +24,11 @@ export interface StreamChunkEvent {
24
24
  type: 'chunk';
25
25
  content: string;
26
26
  done?: boolean;
27
+ sources?: Array<{
28
+ document_uuid: string;
29
+ document_title: string;
30
+ chunk_indices: number[];
31
+ }>;
27
32
  }
28
33
  export interface StreamCompletionEvent {
29
34
  type: 'completion';
@@ -39,7 +44,11 @@ export interface StreamErrorEvent {
39
44
  }
40
45
  export type StreamEvent = StreamChunkEvent | StreamCompletionEvent | StreamErrorEvent;
41
46
  export interface StreamCallbacks {
42
- onChunk?: (content: string, isDone?: boolean) => void;
47
+ onChunk?: (content: string, isDone?: boolean, sources?: Array<{
48
+ document_uuid: string;
49
+ document_title: string;
50
+ chunk_indices: number[];
51
+ }>) => void;
43
52
  onCompletion?: (data: StreamCompletionEvent) => void;
44
53
  onError?: (error: StreamErrorEvent) => void;
45
54
  onDone?: () => void;
@@ -121,7 +121,7 @@ export async function streamAIResponse(userMessageUuid, callbacks, apiUrl) {
121
121
  const event = JSON.parse(data);
122
122
  switch (event.type) {
123
123
  case 'chunk':
124
- callbacks.onChunk?.(event.content, event.done);
124
+ callbacks.onChunk?.(event.content, event.done, event.sources);
125
125
  break;
126
126
  case 'completion':
127
127
  callbacks.onCompletion?.(event);
@@ -1,9 +1,11 @@
1
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
- import { Send, X, MessageCircle, Bot, ThumbsUp, ThumbsDown } from 'lucide-react';
4
+ import { Send, X, MessageCircle, Bot, ThumbsUp, ThumbsDown, Copy, Check } from 'lucide-react';
5
5
  import { generateId, formatTimestamp } from '../utils/index.js';
6
+ import { markdownToHtml } from '../utils/markdown.js';
6
7
  import { VezloFooter } from './ui/VezloFooter.js';
8
+ import { CitationView } from './ui/CitationView.js';
7
9
  import { createConversation, createUserMessage, streamAIResponse } from '../api/index.js';
8
10
  import { submitFeedback, deleteFeedback } from '../api/message.js';
9
11
  import { subscribeToConversations } from '../services/conversationRealtime.js';
@@ -22,6 +24,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
22
24
  const [isLoading, setIsLoading] = useState(false);
23
25
  const [messageFeedback, setMessageFeedback] = useState({});
24
26
  const [messageFeedbackUuids, setMessageFeedbackUuids] = useState({});
27
+ const [copiedMessageId, setCopiedMessageId] = useState(null);
25
28
  const [streamingMessage, setStreamingMessage] = useState('');
26
29
  const [conversationUuid, setConversationUuid] = useState(null);
27
30
  const [companyUuid, setCompanyUuid] = useState(null);
@@ -83,6 +86,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
83
86
  id: generateId(),
84
87
  content: config.welcomeMessage || 'Hello! I\'m your AI assistant. How can I help you today?',
85
88
  role: 'assistant',
89
+ type: 'assistant', // Set type for button visibility
86
90
  timestamp: new Date(),
87
91
  };
88
92
  setMessages([welcomeMsg]);
@@ -96,6 +100,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
96
100
  id: generateId(),
97
101
  content: config.welcomeMessage || 'Hello! I\'m your AI assistant. How can I help you today?',
98
102
  role: 'assistant',
103
+ type: 'assistant', // Set type for button visibility
99
104
  timestamp: new Date(),
100
105
  };
101
106
  setMessages([welcomeMsg]);
@@ -184,7 +189,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
184
189
  const tempMessageId = `streaming-${userMessageResponse.uuid}`;
185
190
  // Stream AI response using SSE
186
191
  await streamAIResponse(userMessageResponse.uuid, {
187
- onChunk: (chunk, isDone) => {
192
+ onChunk: (chunk, isDone, sources) => {
188
193
  // Hide loading indicator on first chunk (streaming started)
189
194
  if (!hasReceivedChunks) {
190
195
  hasReceivedChunks = true;
@@ -198,12 +203,19 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
198
203
  // If this is the final chunk (done=true), convert to message immediately
199
204
  if (isDone && !streamingComplete) {
200
205
  streamingComplete = true;
201
- // Add message to array with temp ID (shows timestamp/icons)
206
+ console.log('Stream complete, sources:', sources);
207
+ // Add message to array with temp ID and sources
202
208
  const tempMessage = {
203
209
  id: tempMessageId,
204
210
  content: accumulatedContent,
205
211
  role: 'assistant',
212
+ type: 'assistant', // Set type for button visibility
206
213
  timestamp: new Date(),
214
+ sources: sources && sources.length > 0 ? sources.map(s => ({
215
+ document_uuid: s.document_uuid,
216
+ document_title: s.document_title,
217
+ chunk_indices: s.chunk_indices
218
+ })) : undefined
207
219
  };
208
220
  setStreamingMessage('');
209
221
  setMessages((prev) => [...prev, tempMessage]);
@@ -219,6 +231,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
219
231
  id: completionData.uuid,
220
232
  content: accumulatedContent,
221
233
  role: 'assistant',
234
+ type: 'assistant', // Set type for button visibility
222
235
  timestamp: new Date(completionData.created_at),
223
236
  };
224
237
  onMessage?.(finalMessage);
@@ -262,6 +275,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
262
275
  id: generateId(),
263
276
  content: 'Sorry, I encountered an error processing your message. Please try again.',
264
277
  role: 'assistant',
278
+ type: 'assistant', // Set type for button visibility
265
279
  timestamp: new Date(),
266
280
  };
267
281
  setMessages((prev) => [...prev, errorMessage]);
@@ -289,6 +303,19 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
289
303
  handleSendMessage();
290
304
  }
291
305
  };
306
+ const handleCopyMessage = async (messageId) => {
307
+ const message = messages.find(m => m.id === messageId);
308
+ if (!message || message.role !== 'assistant')
309
+ return;
310
+ try {
311
+ await navigator.clipboard.writeText(message.content);
312
+ setCopiedMessageId(messageId);
313
+ setTimeout(() => setCopiedMessageId(null), 2000);
314
+ }
315
+ catch (err) {
316
+ console.error('Failed to copy message:', err);
317
+ }
318
+ };
292
319
  const handleFeedback = async (messageId, type) => {
293
320
  // Find the message to get the real UUID (might be stored in _realUuid)
294
321
  const message = messages.find(m => m.id === messageId);
@@ -465,13 +492,13 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
465
492
  boxShadow: message.role === 'user'
466
493
  ? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
467
494
  : '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' && (() => {
495
+ }, children: message.role === 'user' ? (_jsx("p", { className: "text-sm whitespace-pre-wrap break-words leading-relaxed", children: message.content })) : (_jsxs("div", { children: [_jsx("div", { className: "text-sm prose prose-sm max-w-none prose-p:my-2 prose-headings:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-1 prose-pre:my-2 prose-code:text-xs [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_code]:break-words", dangerouslySetInnerHTML: { __html: markdownToHtml(message.content) } }), message.sources && message.sources.length > 0 && (_jsx(CitationView, { sources: message.sources }))] })) }), _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' && message.type === 'assistant' && (() => {
469
496
  // Check if message has real UUID (not temp ID)
470
497
  // Disable if: message has temp ID (starts with 'streaming-') AND no _realUuid yet
471
498
  const isTempId = message.id?.startsWith('streaming-') || false;
472
499
  const hasRealUuid = !!message._realUuid;
473
500
  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
501
+ return (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_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
475
502
  ? 'opacity-40 cursor-not-allowed'
476
503
  : 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'like'
477
504
  ? 'text-green-600'
@@ -0,0 +1,10 @@
1
+ interface Source {
2
+ document_uuid: string;
3
+ document_title: string;
4
+ chunk_indices: number[];
5
+ }
6
+ interface CitationViewProps {
7
+ sources: Source[];
8
+ }
9
+ export declare function CitationView({ sources }: CitationViewProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Download, Loader2 } from 'lucide-react';
4
+ import { getCitationContext } from '../../api/citation';
5
+ // MIME type to extension mapping
6
+ const mimeToExtension = {
7
+ 'text/javascript': '.js',
8
+ 'application/javascript': '.js',
9
+ 'text/jsx': '.jsx',
10
+ 'text/typescript': '.ts',
11
+ 'application/typescript': '.ts',
12
+ 'text/tsx': '.tsx',
13
+ 'text/x-python': '.py',
14
+ 'text/python': '.py',
15
+ 'text/markdown': '.md',
16
+ 'text/x-markdown': '.md',
17
+ 'application/json': '.json',
18
+ 'text/html': '.html',
19
+ 'text/css': '.css',
20
+ 'text/plain': '.txt',
21
+ 'text/xml': '.xml',
22
+ };
23
+ function getFileExtension(title, fileType) {
24
+ // First try to extract from filename
25
+ const match = title.match(/\.([a-zA-Z0-9]+)$/);
26
+ if (match) {
27
+ return `.${match[1]}`;
28
+ }
29
+ // Fallback to MIME type mapping
30
+ if (fileType && mimeToExtension[fileType]) {
31
+ return mimeToExtension[fileType];
32
+ }
33
+ // Default to .txt
34
+ return '.txt';
35
+ }
36
+ export function CitationView({ sources }) {
37
+ const [loadingUuid, setLoadingUuid] = useState(null);
38
+ const handleDownloadSource = async (source) => {
39
+ setLoadingUuid(source.document_uuid);
40
+ try {
41
+ if (!source.document_uuid || !source.chunk_indices || source.chunk_indices.length === 0) {
42
+ setLoadingUuid(null);
43
+ alert('Invalid source data. UUID or chunk indices are missing.');
44
+ return;
45
+ }
46
+ const context = await getCitationContext(source.document_uuid, source.chunk_indices);
47
+ if (!context || !context.content) {
48
+ setLoadingUuid(null);
49
+ alert('No content available for this source.');
50
+ return;
51
+ }
52
+ // Determine file extension
53
+ const extension = getFileExtension(context.document_title, context.file_type);
54
+ const filename = context.document_title.endsWith(extension)
55
+ ? context.document_title
56
+ : `${context.document_title}${extension}`;
57
+ // Create blob and download
58
+ const blob = new Blob([context.content], { type: 'text/plain' });
59
+ const url = URL.createObjectURL(blob);
60
+ const a = document.createElement('a');
61
+ a.href = url;
62
+ a.download = filename;
63
+ document.body.appendChild(a);
64
+ a.click();
65
+ document.body.removeChild(a);
66
+ URL.revokeObjectURL(url);
67
+ setLoadingUuid(null);
68
+ }
69
+ catch (err) {
70
+ console.error('Failed to download source:', err);
71
+ setLoadingUuid(null);
72
+ alert('Failed to download source content. Please try again.');
73
+ }
74
+ };
75
+ return (_jsxs("div", { className: "mt-2 border-t border-gray-100 pt-2", children: [_jsx("div", { className: "text-xs text-gray-500 mb-1 font-medium", children: "Sources:" }), _jsx("div", { className: "space-y-1", children: sources.map((source, index) => {
76
+ const isLoading = loadingUuid === source.document_uuid;
77
+ return (_jsxs("button", { onClick: () => handleDownloadSource(source), disabled: isLoading, className: "w-full text-left px-2 py-1.5 text-xs text-emerald-700 hover:text-emerald-800 hover:bg-emerald-50 rounded flex items-center gap-1 transition-colors group cursor-pointer disabled:opacity-70 disabled:cursor-wait", children: [isLoading ? (_jsx(Loader2, { className: "w-3 h-3 flex-shrink-0 animate-spin" })) : (_jsx(Download, { className: "w-3 h-3 flex-shrink-0" })), _jsx("span", { className: "truncate", children: source.document_title })] }, index));
78
+ }) })] }));
79
+ }
@@ -13,6 +13,7 @@ export interface MessageCreatedPayload {
13
13
  joined_at?: string;
14
14
  status?: string;
15
15
  closed_at?: string;
16
+ archived_at?: string;
16
17
  };
17
18
  }
18
19
  export interface ConversationCreatedPayload {
@@ -21,12 +21,16 @@ export interface ChatMessage {
21
21
  id: string;
22
22
  content: string;
23
23
  role: 'user' | 'assistant' | 'system';
24
+ type?: 'user' | 'assistant' | 'agent' | 'system';
24
25
  timestamp: Date;
25
26
  sources?: ChatSource[];
26
27
  _realUuid?: string;
27
28
  }
28
29
  export interface ChatSource {
29
- title: string;
30
+ document_uuid: string;
31
+ document_title: string;
32
+ chunk_indices: number[];
33
+ title?: string;
30
34
  url?: string;
31
35
  content?: string;
32
36
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Convert markdown to sanitized HTML
3
+ */
4
+ export declare function markdownToHtml(content: string): string;
@@ -0,0 +1,50 @@
1
+ import { marked } from 'marked';
2
+ import DOMPurify from 'dompurify';
3
+ /**
4
+ * Check if content contains markdown syntax
5
+ */
6
+ function hasMarkdownSyntax(content) {
7
+ // Check for common markdown patterns
8
+ const mdPatterns = [
9
+ /#{1,6}\s/, // Headers
10
+ /\*\*.*?\*\*/, // Bold
11
+ /__.*?__/, // Bold (alt)
12
+ /\*.*?\*/, // Italic
13
+ /_.*?_/, // Italic (alt)
14
+ /\[.*?\]\(.*?\)/, // Links
15
+ /`.*?`/, // Inline code
16
+ /```[\s\S]*?```/, // Code blocks
17
+ /^\s*[-*+]\s/m, // Unordered lists
18
+ /^\s*\d+\.\s/m, // Ordered lists
19
+ /^\s*>\s/m, // Blockquotes
20
+ ];
21
+ return mdPatterns.some(pattern => pattern.test(content));
22
+ }
23
+ /**
24
+ * Convert markdown to sanitized HTML
25
+ */
26
+ export function markdownToHtml(content) {
27
+ // If no markdown syntax detected, return as-is wrapped in paragraph
28
+ if (!hasMarkdownSyntax(content)) {
29
+ return `<p>${DOMPurify.sanitize(content)}</p>`;
30
+ }
31
+ // Configure marked for safe rendering
32
+ marked.setOptions({
33
+ breaks: true,
34
+ gfm: true,
35
+ });
36
+ // Convert markdown to HTML
37
+ const rawHtml = marked(content);
38
+ // Sanitize HTML to prevent XSS
39
+ return DOMPurify.sanitize(rawHtml, {
40
+ ALLOWED_TAGS: [
41
+ 'p', 'br', 'strong', 'em', 'u', 'code', 'pre',
42
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
43
+ 'ul', 'ol', 'li',
44
+ 'blockquote',
45
+ 'a',
46
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
47
+ ],
48
+ ALLOWED_ATTR: ['href', 'target', 'rel'],
49
+ });
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vezlo/assistant-chat",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",
@@ -34,7 +34,9 @@
34
34
  "@supabase/supabase-js": "^2.84.0",
35
35
  "clsx": "^2.1.1",
36
36
  "date-fns": "^4.1.0",
37
+ "dompurify": "^3.3.1",
37
38
  "lucide-react": "^0.544.0",
39
+ "marked": "^17.0.1",
38
40
  "react-router-dom": "^7.9.3",
39
41
  "tailwindcss": "^4.1.14"
40
42
  },
@@ -42,6 +44,7 @@
42
44
  "@eslint/js": "^9.36.0",
43
45
  "@tailwindcss/postcss": "^4.1.14",
44
46
  "@tailwindcss/typography": "^0.5.19",
47
+ "@types/dompurify": "^3.0.5",
45
48
  "@types/node": "^24.6.2",
46
49
  "@types/react": "^19.1.16",
47
50
  "@types/react-dom": "^19.1.9",