@vezlo/assistant-chat 1.6.0 → 1.8.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.
@@ -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
+ }
@@ -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,16 @@ 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
+ }>;
32
+ validation?: {
33
+ confidence: number;
34
+ valid: boolean;
35
+ status: string;
36
+ };
27
37
  }
28
38
  export interface StreamCompletionEvent {
29
39
  type: 'completion';
@@ -39,7 +49,15 @@ export interface StreamErrorEvent {
39
49
  }
40
50
  export type StreamEvent = StreamChunkEvent | StreamCompletionEvent | StreamErrorEvent;
41
51
  export interface StreamCallbacks {
42
- onChunk?: (content: string, isDone?: boolean) => void;
52
+ onChunk?: (content: string, isDone?: boolean, sources?: Array<{
53
+ document_uuid: string;
54
+ document_title: string;
55
+ chunk_indices: number[];
56
+ }>, validation?: {
57
+ confidence: number;
58
+ valid: boolean;
59
+ status: string;
60
+ }) => void;
43
61
  onCompletion?: (data: StreamCompletionEvent) => void;
44
62
  onError?: (error: StreamErrorEvent) => void;
45
63
  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, event.validation);
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, validation) => {
188
193
  // Hide loading indicator on first chunk (streaming started)
189
194
  if (!hasReceivedChunks) {
190
195
  hasReceivedChunks = true;
@@ -198,12 +203,20 @@ 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, 'validation:', validation);
207
+ // Add message to array with temp ID, sources, and validation
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,
219
+ validation: validation
207
220
  };
208
221
  setStreamingMessage('');
209
222
  setMessages((prev) => [...prev, tempMessage]);
@@ -219,6 +232,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
219
232
  id: completionData.uuid,
220
233
  content: accumulatedContent,
221
234
  role: 'assistant',
235
+ type: 'assistant', // Set type for button visibility
222
236
  timestamp: new Date(completionData.created_at),
223
237
  };
224
238
  onMessage?.(finalMessage);
@@ -262,6 +276,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
262
276
  id: generateId(),
263
277
  content: 'Sorry, I encountered an error processing your message. Please try again.',
264
278
  role: 'assistant',
279
+ type: 'assistant', // Set type for button visibility
265
280
  timestamp: new Date(),
266
281
  };
267
282
  setMessages((prev) => [...prev, errorMessage]);
@@ -289,6 +304,19 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
289
304
  handleSendMessage();
290
305
  }
291
306
  };
307
+ const handleCopyMessage = async (messageId) => {
308
+ const message = messages.find(m => m.id === messageId);
309
+ if (!message || message.role !== 'assistant')
310
+ return;
311
+ try {
312
+ await navigator.clipboard.writeText(message.content);
313
+ setCopiedMessageId(messageId);
314
+ setTimeout(() => setCopiedMessageId(null), 2000);
315
+ }
316
+ catch (err) {
317
+ console.error('Failed to copy message:', err);
318
+ }
319
+ };
292
320
  const handleFeedback = async (messageId, type) => {
293
321
  // Find the message to get the real UUID (might be stored in _realUuid)
294
322
  const message = messages.find(m => m.id === messageId);
@@ -465,13 +493,49 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
465
493
  boxShadow: message.role === 'user'
466
494
  ? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
467
495
  : '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' && (() => {
496
+ }, 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
497
  // Check if message has real UUID (not temp ID)
470
498
  // Disable if: message has temp ID (starts with 'streaming-') AND no _realUuid yet
471
499
  const isTempId = message.id?.startsWith('streaming-') || false;
472
500
  const hasRealUuid = !!message._realUuid;
473
501
  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
502
+ 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
503
+ ? 'fill-green-500'
504
+ : message.validation.confidence > 0.5
505
+ ? 'fill-amber-500'
506
+ : 'fill-red-500' }), _jsx("path", { d: "M12 0.5 L13.5 3 L10.5 3 Z", className: message.validation.confidence > 0.7
507
+ ? 'fill-green-500'
508
+ : message.validation.confidence > 0.5
509
+ ? 'fill-amber-500'
510
+ : 'fill-red-500' }), _jsx("path", { d: "M20.5 4.5 L19 7 L18 5 Z", className: message.validation.confidence > 0.7
511
+ ? 'fill-green-500'
512
+ : message.validation.confidence > 0.5
513
+ ? 'fill-amber-500'
514
+ : 'fill-red-500' }), _jsx("path", { d: "M23.5 12 L21 13.5 L21 10.5 Z", className: message.validation.confidence > 0.7
515
+ ? 'fill-green-500'
516
+ : message.validation.confidence > 0.5
517
+ ? 'fill-amber-500'
518
+ : 'fill-red-500' }), _jsx("path", { d: "M20.5 19.5 L18 19 L19 17 Z", className: message.validation.confidence > 0.7
519
+ ? 'fill-green-500'
520
+ : message.validation.confidence > 0.5
521
+ ? 'fill-amber-500'
522
+ : 'fill-red-500' }), _jsx("path", { d: "M12 23.5 L10.5 21 L13.5 21 Z", className: message.validation.confidence > 0.7
523
+ ? 'fill-green-500'
524
+ : message.validation.confidence > 0.5
525
+ ? 'fill-amber-500'
526
+ : 'fill-red-500' }), _jsx("path", { d: "M3.5 19.5 L5 17 L6 19 Z", className: message.validation.confidence > 0.7
527
+ ? 'fill-green-500'
528
+ : message.validation.confidence > 0.5
529
+ ? 'fill-amber-500'
530
+ : 'fill-red-500' }), _jsx("path", { d: "M0.5 12 L3 10.5 L3 13.5 Z", className: message.validation.confidence > 0.7
531
+ ? 'fill-green-500'
532
+ : message.validation.confidence > 0.5
533
+ ? 'fill-amber-500'
534
+ : 'fill-red-500' }), _jsx("path", { d: "M3.5 4.5 L6 5 L5 7 Z", className: message.validation.confidence > 0.7
535
+ ? 'fill-green-500'
536
+ : message.validation.confidence > 0.5
537
+ ? 'fill-amber-500'
538
+ : '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
475
539
  ? 'opacity-40 cursor-not-allowed'
476
540
  : 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'like'
477
541
  ? '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
+ }
@@ -21,12 +21,36 @@ 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[];
27
+ validation?: {
28
+ confidence: number;
29
+ valid: boolean;
30
+ status: string;
31
+ accuracy?: {
32
+ verified: boolean;
33
+ verification_rate: number;
34
+ reason?: string;
35
+ };
36
+ hallucination?: {
37
+ detected: boolean;
38
+ risk: number;
39
+ reason?: string;
40
+ };
41
+ context?: {
42
+ source_relevance: number;
43
+ source_usage_rate: number;
44
+ valid: boolean;
45
+ };
46
+ };
26
47
  _realUuid?: string;
27
48
  }
28
49
  export interface ChatSource {
29
- title: string;
50
+ document_uuid: string;
51
+ document_title: string;
52
+ chunk_indices: number[];
53
+ title?: string;
30
54
  url?: string;
31
55
  content?: string;
32
56
  }
@@ -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.6.0",
3
+ "version": "1.8.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",