diff --git a/webapp/chat-app/package.json b/webapp/chat-app/package.json index a14e4cdf..1017c504 100644 --- a/webapp/chat-app/package.json +++ b/webapp/chat-app/package.json @@ -9,6 +9,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "dompurify": "^3.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^8.1.3", @@ -19,6 +20,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/dompurify": "^3.2.0", "@types/node": "^18.0.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", diff --git a/webapp/chat-app/src/components/ChatInterface.tsx b/webapp/chat-app/src/components/ChatInterface.tsx index f815f1df..9ad96ff0 100644 --- a/webapp/chat-app/src/components/ChatInterface.tsx +++ b/webapp/chat-app/src/components/ChatInterface.tsx @@ -7,6 +7,12 @@ import MessageList from './MessageList'; import InputArea from './InputArea'; import websocket from '@services/websocket'; +interface WebSocketMessage { + data: string; + isHtml: boolean; + timestamp: number; +} + interface ChatInterfaceProps { sessionId?: string; websocket: typeof websocket; @@ -32,10 +38,16 @@ const ChatInterface: React.FC = ({ useEffect(() => { console.log('[ChatInterface] Setting up message handler for sessionId:', sessionId); - const handleMessage = (data: string) => { + const handleMessage = (data: WebSocketMessage) => { console.log('[ChatInterface] Received message:', data); + // If data is an object with raw data property, use that instead + const messageData = typeof data === 'object' ? data.data : data; + if (!messageData || typeof messageData !== 'string') { + console.warn('[ChatInterface] Invalid message format received:', data); + return; + } - const [id, version, content] = data.split(','); + const [id, version, content] = messageData.split(','); const timestamp = Date.now(); const messageObject = { id: `${id}-${timestamp}`, diff --git a/webapp/chat-app/src/components/Menu/Menu.tsx b/webapp/chat-app/src/components/Menu/Menu.tsx index 6f020b55..854dbcf0 100644 --- a/webapp/chat-app/src/components/Menu/Menu.tsx +++ b/webapp/chat-app/src/components/Menu/Menu.tsx @@ -139,6 +139,12 @@ export const Menu: React.FC = () => { handleModalOpen('privacy')}>Privacy Policy handleModalOpen('tos')}>Terms of Service + + + + + console.log('About menu clicked')}>Config + diff --git a/webapp/chat-app/src/components/MessageList.tsx b/webapp/chat-app/src/components/MessageList.tsx index 68abe420..78e23ea0 100644 --- a/webapp/chat-app/src/components/MessageList.tsx +++ b/webapp/chat-app/src/components/MessageList.tsx @@ -50,9 +50,9 @@ const MessageList: React.FC = () => { {messages.map((message) => { logger.debug('MessageList - Rendering message', message); return ( - - {message.content} - + +
+ ); })} diff --git a/webapp/chat-app/src/hooks/useWebSocket.ts b/webapp/chat-app/src/hooks/useWebSocket.ts index eaa433c2..9aaeb7f8 100644 --- a/webapp/chat-app/src/hooks/useWebSocket.ts +++ b/webapp/chat-app/src/hooks/useWebSocket.ts @@ -1,30 +1,51 @@ import {useEffect, useState} from 'react'; +import {useDispatch} from 'react-redux'; +import {addMessage} from '../store/slices/messageSlice'; import WebSocketService from '../services/websocket'; +import {Message} from "../types"; export const useWebSocket = (sessionId: string) => { const [isConnected, setIsConnected] = useState(false); + const dispatch = useDispatch(); useEffect(() => { - console.log(`[WebSocket] Initializing with sessionId: ${sessionId}`); + console.debug(`[WebSocket] Initializing with sessionId: ${sessionId}`); + if (!sessionId) { + console.debug('[WebSocket] No sessionId provided, skipping connection'); + return; + } - const handleConnection = () => { - console.log('[WebSocket] Connected successfully'); - setIsConnected(true); + const handleMessage = (message: Message) => { + if (message.isHtml) { + console.debug('[WebSocket] Processing HTML message'); + const htmlMessage = { + id: Date.now().toString(), + content: message.content, + type: 'response' as const, + timestamp: message.timestamp, + isHtml: true, + rawHtml: message.rawHtml, + version: '1.0', + sanitized: false, + }; + console.debug('[WebSocket] Dispatching HTML message:', htmlMessage); + dispatch(addMessage(htmlMessage)); + } }; - const handleDisconnection = () => { + const handleConnectionChange = (connected: boolean) => { console.log('[WebSocket] Disconnected'); - setIsConnected(false); + setIsConnected(connected); }; - WebSocketService.addMessageHandler(handleConnection); - WebSocketService.addMessageHandler(handleDisconnection); + WebSocketService.addMessageHandler(handleMessage); + WebSocketService.addConnectionHandler(handleConnectionChange); WebSocketService.connect(sessionId); return () => { console.log('[WebSocket] Cleaning up connection'); - WebSocketService.removeMessageHandler(handleConnection); - WebSocketService.removeMessageHandler(handleDisconnection); + WebSocketService.removeMessageHandler(handleMessage); + WebSocketService.removeConnectionHandler(handleConnectionChange); WebSocketService.disconnect(); }; }, [sessionId]); diff --git a/webapp/chat-app/src/index.tsx b/webapp/chat-app/src/index.tsx index 485fbb91..d8a8c0e9 100644 --- a/webapp/chat-app/src/index.tsx +++ b/webapp/chat-app/src/index.tsx @@ -10,21 +10,21 @@ console.log('Application initializing...'); const rootElement = document.getElementById('root'); if (!rootElement) { - console.error('Failed to find root element in DOM'); - throw new Error('Failed to find the root element'); + console.error('Failed to find root element in DOM'); + throw new Error('Failed to find the root element'); } const root = createRoot(rootElement); console.log('React root created successfully'); try { -root.render( - - - -); - console.log('Application rendered successfully'); + root.render( + + + + ); + console.log('Application rendered successfully'); } catch (error) { - console.error('Failed to render application:', error); - throw error; + console.error('Failed to render application:', error); + throw error; } \ No newline at end of file diff --git a/webapp/chat-app/src/services/websocket.ts b/webapp/chat-app/src/services/websocket.ts index abd9bfbb..a115345c 100644 --- a/webapp/chat-app/src/services/websocket.ts +++ b/webapp/chat-app/src/services/websocket.ts @@ -1,172 +1,204 @@ -import { store } from '../store'; +import {store} from '../store'; +import {Message} from "../types"; export class WebSocketService { - private ws: WebSocket | null = null; - private reconnectAttempts = 0; - private maxReconnectAttempts = 10; - private sessionId = ''; - private messageHandlers: ((data: any) => void)[] = []; - - private getConfig() { - const state = store.getState(); - // Load from localStorage as fallback if store is not yet initialized - if (!state.config?.websocket) { - try { - const savedConfig = localStorage.getItem('websocketConfig'); - if (savedConfig) { - console.log('Using WebSocket config from localStorage:', JSON.parse(savedConfig)); - return JSON.parse(savedConfig); - } - } catch (error) { - console.error('Error reading WebSocket config from localStorage:', error); - } + private ws: WebSocket | null = null; + private reconnectAttempts = 0; + private maxReconnectAttempts = 10; + private sessionId = ''; + private messageHandlers: ((data: Message) => void)[] = []; + private connectionHandlers: ((connected: boolean) => void)[] = []; + private isReconnecting = false; + private connectionTimeout: NodeJS.Timeout | null = null; + + public addConnectionHandler(handler: (connected: boolean) => void): void { + this.connectionHandlers.push(handler); + console.log('[WebSocket] Connection handler added'); + } + + public removeConnectionHandler(handler: (connected: boolean) => void): void { + this.connectionHandlers = this.connectionHandlers.filter(h => h !== handler); + console.log('[WebSocket] Connection handler removed'); + } + + public getSessionId(): string { + return this.sessionId; + } + + public isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; } - return state.config.websocket; - } - private isReconnecting = false; - private connectionTimeout: NodeJS.Timeout | null = null; - - public getSessionId(): string { - return this.sessionId; - } - - public isConnected(): boolean { - return this.ws?.readyState === WebSocket.OPEN; - } - - connect(sessionId: string): void { - try { - console.log(`[WebSocket] Initiating connection with sessionId: ${sessionId}`); - const config = this.getConfig(); - if (!config) { - throw new Error('WebSocket configuration not available'); - } - - // Clear any existing connection timeout - if (this.connectionTimeout) { - clearTimeout(this.connectionTimeout); - } - - this.sessionId = sessionId; - const path = this.getWebSocketPath(); - // Only create new connection if not already connected or reconnecting - if (!this.isConnected() && !this.isReconnecting) { - const wsUrl = `${config.protocol}//${config.url}:${config.port}${path}ws?sessionId=${sessionId}`; - console.log(`[WebSocket] Connecting to: ${wsUrl}`); - this.ws = new WebSocket(wsUrl); - this.setupEventHandlers(); - // Set connection timeout - this.connectionTimeout = setTimeout(() => { - if (this.ws?.readyState !== WebSocket.OPEN) { - console.warn('[WebSocket] Connection timeout reached, attempting to reconnect'); - this.ws?.close(); + + connect(sessionId: string): void { + try { + if (!sessionId) { + throw new Error('[WebSocket] SessionId is required'); + } + console.log(`[WebSocket] Initiating connection with sessionId: ${sessionId}`); + const config = this.getConfig(); + if (!config) { + throw new Error('WebSocket configuration not available'); + } + + // Clear any existing connection timeout + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + } + + this.sessionId = sessionId; + const path = this.getWebSocketPath(); + // Only create new connection if not already connected or reconnecting + if (!this.isConnected() && !this.isReconnecting) { + const wsUrl = `${config.protocol}//${config.url}:${config.port}${path}ws?sessionId=${sessionId}`; + console.log(`[WebSocket] Connecting to: ${wsUrl}`); + this.ws = new WebSocket(wsUrl); + this.setupEventHandlers(); + // Set connection timeout + this.connectionTimeout = setTimeout(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.warn('[WebSocket] Connection timeout reached, attempting to reconnect'); + this.ws?.close(); + this.attemptReconnect(); + } + }, 5000); + } + } catch (error) { + console.error('[WebSocket] Connection error:', error); this.attemptReconnect(); - } - }, 5000); - } - } catch (error) { - console.error('[WebSocket] Connection error:', error); - this.attemptReconnect(); + } } - } - - removeMessageHandler(handler: (data: any) => void): void { - const handlersBeforeRemoval = this.messageHandlers.length; - this.messageHandlers = this.messageHandlers.filter((h) => h !== handler); - const handlersAfterRemoval = this.messageHandlers.length; - console.log(`[WebSocket] Message handler removed. Handlers count: ${handlersAfterRemoval}`); - } - - addMessageHandler(handler: (data: any) => void): void { - this.messageHandlers.push(handler); - console.log(`[WebSocket] New message handler added. Handlers count: ${this.messageHandlers.length}`); - } - - disconnect(): void { - if (this.ws) { - console.log('[WebSocket] Initiating disconnect'); - if (this.connectionTimeout) { - clearTimeout(this.connectionTimeout); - } - this.isReconnecting = false; - this.ws.close(); - this.ws = null; - console.log('[WebSocket] Disconnected successfully'); + + removeMessageHandler(handler: (data: any) => void): void { + this.messageHandlers = this.messageHandlers.filter((h) => h !== handler); + const handlersAfterRemoval = this.messageHandlers.length; + console.log(`[WebSocket] Message handler removed. Handlers count: ${handlersAfterRemoval}`); } - } - - send(message: string): void { - if (this.ws?.readyState === WebSocket.OPEN) { - console.debug('[WebSocket] Sending message:', message.substring(0, 100) + (message.length > 100 ? '...' : '')); - this.ws.send(message); - } else { - console.warn('[WebSocket] Cannot send message - connection not open'); + + addMessageHandler(handler: (data: any) => void): void { + this.messageHandlers.push(handler); + console.log(`[WebSocket] New message handler added. Handlers count: ${this.messageHandlers.length}`); } - } - - private getWebSocketPath(): string { - const path = window.location.pathname; - const strings = path.split('/'); - const wsPath = strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html' - ? '/' + strings[1] + '/' - : '/'; - console.debug(`[WebSocket] Calculated WebSocket path: ${wsPath}`); - return wsPath; - } - - private setupEventHandlers(): void { - if (!this.ws) { - console.warn('[WebSocket] Cannot setup event handlers - no WebSocket instance'); - return; + + disconnect(): void { + if (this.ws) { + console.log('[WebSocket] Initiating disconnect'); + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + } + this.isReconnecting = false; + this.ws.close(); + this.ws = null; + console.log('[WebSocket] Disconnected successfully'); + } } - this.ws.onopen = () => { - console.log('[WebSocket] Connection established successfully'); - this.reconnectAttempts = 0; - this.isReconnecting = false; - if (this.connectionTimeout) { - clearTimeout(this.connectionTimeout); - } - }; - this.ws.onmessage = (event) => { - console.debug('[WebSocket] Message received:', - typeof event.data === 'string' - ? event.data.substring(0, 100) + (event.data.length > 100 ? '...' : '') - : 'Binary data'); - this.messageHandlers.forEach((handler) => handler(event.data)); - }; - - this.ws.onclose = () => { - console.log('[WebSocket] Connection closed'); - if (!this.isReconnecting) { - this.attemptReconnect(); - } - }; - - this.ws.onerror = (error) => { - console.error('[WebSocket] Error occurred:', error); - if (this.ws?.readyState !== WebSocket.OPEN) { - this.attemptReconnect(); - } - }; - } - - private attemptReconnect(): void { - if (this.isReconnecting) return; - - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error(`[WebSocket] Max reconnection attempts (${this.maxReconnectAttempts}) reached`); - return; + send(message: string): void { + if (this.ws?.readyState === WebSocket.OPEN) { + console.debug('[WebSocket] Sending message:', message.substring(0, 100) + (message.length > 100 ? '...' : '')); + this.ws.send(message); + } else { + console.warn('[WebSocket] Cannot send message - connection not open'); + } + } + + private getConfig() { + const state = store.getState(); + // Load from localStorage as fallback if store is not yet initialized + if (!state.config?.websocket) { + try { + const savedConfig = localStorage.getItem('websocketConfig'); + if (savedConfig) { + console.log('Using WebSocket config from localStorage:', JSON.parse(savedConfig)); + return JSON.parse(savedConfig); + } + } catch (error) { + console.error('Error reading WebSocket config from localStorage:', error); + } + } + return state.config.websocket; + } + + private getWebSocketPath(): string { + const path = window.location.pathname; + const strings = path.split('/'); + const wsPath = strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html' + ? '/' + strings[1] + '/' + : '/'; + console.debug(`[WebSocket] Calculated WebSocket path: ${wsPath}`); + return wsPath; + } + + private setupEventHandlers(): void { + if (!this.ws) { + console.warn('[WebSocket] Cannot setup event handlers - no WebSocket instance'); + return; + } + + this.ws.onopen = () => { + console.log('[WebSocket] Connection established successfully'); + this.reconnectAttempts = 0; + this.isReconnecting = false; + this.connectionHandlers.forEach(handler => handler(true)); + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + } + }; + this.ws.onmessage = (event) => { + console.debug('[WebSocket] Message received:', + typeof event.data === 'string' + ? event.data.substring(0, 100) + (event.data.length > 100 ? '...' : '') + : 'Binary data'); + // Enhanced HTML detection and handling + const isHtml = typeof event.data === 'string' && + (/<[a-z][\s\S]*>/i.test(event.data)); + if (isHtml) { + console.debug('[WebSocket] HTML content detected, preserving markup'); + } + + this.messageHandlers.forEach((handler) => handler({ + id: event.data.split(',')[0], + type: 'response', + version: event.data.split(',')[1], + content: event.data.split(',')[2], + isHtml, + rawHtml: isHtml ? event.data.split(',')[2] : null, + timestamp: Date.now(), + sanitized: false + })) + }; + + this.ws.onclose = () => { + console.log('[WebSocket] Connection closed'); + this.connectionHandlers.forEach(handler => handler(false)); + if (!this.isReconnecting) { + this.attemptReconnect(); + } + }; + + this.ws.onerror = (error) => { + console.error('[WebSocket] Error occurred:', error); + if (this.ws?.readyState !== WebSocket.OPEN) { + this.attemptReconnect(); + } + }; + } + + private attemptReconnect(): void { + if (this.isReconnecting) return; + + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(`[WebSocket] Max reconnection attempts (${this.maxReconnectAttempts}) reached`); + return; + } + this.isReconnecting = true; + const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 30000); + console.log(`[WebSocket] Attempting reconnect #${this.reconnectAttempts + 1} in ${delay}ms`); + + setTimeout(() => { + this.reconnectAttempts++; + this.connect(this.sessionId); + }, delay); } - this.isReconnecting = true; - const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 30000); - console.log(`[WebSocket] Attempting reconnect #${this.reconnectAttempts + 1} in ${delay}ms`); - - setTimeout(() => { - this.reconnectAttempts++; - this.connect(this.sessionId); - }, delay); - } } export default new WebSocketService(); \ No newline at end of file diff --git a/webapp/chat-app/src/store/slices/messageSlice.ts b/webapp/chat-app/src/store/slices/messageSlice.ts index 95790dbc..77c3fb05 100644 --- a/webapp/chat-app/src/store/slices/messageSlice.ts +++ b/webapp/chat-app/src/store/slices/messageSlice.ts @@ -1,89 +1,108 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {Message} from '../../types'; +import DOMPurify from 'dompurify'; interface MessageState { - messages: Message[]; - pendingMessages: Message[]; - messageQueue: Message[]; - isProcessing: boolean; + messages: Message[]; + pendingMessages: Message[]; + messageQueue: Message[]; + isProcessing: boolean; } const initialState: MessageState = { - messages: [], - pendingMessages: [], - messageQueue: [], - isProcessing: false, + messages: [], + pendingMessages: [], + messageQueue: [], + isProcessing: false, +}; +const sanitizeHtmlContent = (content: string): string => { + console.debug('Sanitizing HTML content'); + return DOMPurify.sanitize(content, { + ALLOWED_TAGS: ['div', 'span', 'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'code', 'pre', 'table', 'tr', 'td', 'th', 'thead', 'tbody'], + ALLOWED_ATTR: ['class', 'href', 'target'] + }); }; const messageSlice = createSlice({ - name: 'messages', - initialState, - reducers: { - addMessage: (state: MessageState, action: PayloadAction) => { - console.log('Adding message:', action.payload); - state.messages.push(action.payload); - console.log('Updated messages state:', state.messages); - }, - updateMessage: (state: MessageState, action: PayloadAction<{ id: string; updates: Partial }>) => { - const { id, updates } = action.payload; - console.log('Updating message:', id, 'with updates:', updates); - const messageIndex = state.messages.findIndex((msg: Message) => msg.id === id); - if (messageIndex !== -1) { - state.messages[messageIndex] = { ...state.messages[messageIndex], ...updates }; - console.log('Message updated successfully'); - } else { - console.warn('Message not found for update:', id); - } - }, - deleteMessage: (state: MessageState, action: PayloadAction) => { - console.log('Deleting message:', action.payload); - state.messages = state.messages.filter((msg: Message) => msg.id !== action.payload); - console.log('Updated messages after deletion:', state.messages); - }, - addToPendingMessages: (state: MessageState, action: PayloadAction) => { - console.log('Adding pending message:', action.payload); - state.pendingMessages.push(action.payload); - console.log('Updated pending messages:', state.pendingMessages); - }, - removePendingMessage: (state: MessageState, action: PayloadAction) => { - console.log('Removing pending message:', action.payload); - state.pendingMessages = state.pendingMessages.filter((msg: Message) => msg.id !== action.payload); - console.log('Updated pending messages:', state.pendingMessages); - }, - addToMessageQueue: (state, action: PayloadAction) => { - console.log('Adding message to queue:', action.payload); - state.messageQueue.push(action.payload); - console.log('Updated message queue:', state.messageQueue); - }, - clearMessageQueue: (state: MessageState) => { - console.log('Clearing message queue'); - state.messageQueue = []; - }, - setProcessing: (state: MessageState, action: PayloadAction) => { - console.log('Setting processing state:', action.payload); - state.isProcessing = action.payload; - }, - clearMessages: (state: MessageState) => { - console.log('Clearing all messages and states'); - state.messages = []; - state.pendingMessages = []; - state.messageQueue = []; - state.isProcessing = false; - console.log('All states cleared'); + name: 'messages', + initialState, + reducers: { + addMessage: (state: MessageState, action: PayloadAction) => { + // Only log message ID and type to reduce noise + console.debug('Adding message:', { + id: action.payload.id, + type: action.payload.type, + isHtml: action.payload.isHtml + }); + // Only sanitize if not already sanitized + if (action.payload.isHtml && action.payload.rawHtml && !action.payload.sanitized) { + action.payload.content = sanitizeHtmlContent(action.payload.rawHtml); + action.payload.sanitized = true; + console.debug('HTML content sanitized'); + } + state.messages.push(action.payload); + console.debug('Messages updated, count:', state.messages.length); + }, + updateMessage: (state: MessageState, action: PayloadAction<{ id: string; updates: Partial }>) => { + const {id, updates} = action.payload; + console.log('Updating message:', id, 'with updates:', updates); + const messageIndex = state.messages.findIndex((msg: Message) => msg.id === id); + if (messageIndex !== -1) { + state.messages[messageIndex] = {...state.messages[messageIndex], ...updates}; + console.log('Message updated successfully'); + } else { + console.warn('Message not found for update:', id); + } + }, + deleteMessage: (state: MessageState, action: PayloadAction) => { + console.log('Deleting message:', action.payload); + state.messages = state.messages.filter((msg: Message) => msg.id !== action.payload); + console.log('Updated messages after deletion:', state.messages); + }, + addToPendingMessages: (state: MessageState, action: PayloadAction) => { + console.log('Adding pending message:', action.payload); + state.pendingMessages.push(action.payload); + console.log('Updated pending messages:', state.pendingMessages); + }, + removePendingMessage: (state: MessageState, action: PayloadAction) => { + console.log('Removing pending message:', action.payload); + state.pendingMessages = state.pendingMessages.filter((msg: Message) => msg.id !== action.payload); + console.log('Updated pending messages:', state.pendingMessages); + }, + addToMessageQueue: (state, action: PayloadAction) => { + console.log('Adding message to queue:', action.payload); + state.messageQueue.push(action.payload); + console.log('Updated message queue:', state.messageQueue); + }, + clearMessageQueue: (state: MessageState) => { + console.log('Clearing message queue'); + state.messageQueue = []; + }, + setProcessing: (state: MessageState, action: PayloadAction) => { + console.log('Setting processing state:', action.payload); + state.isProcessing = action.payload; + }, + clearMessages: (state: MessageState) => { + console.log('Clearing all messages and states'); + state.messages = []; + state.pendingMessages = []; + state.messageQueue = []; + state.isProcessing = false; + console.log('All states cleared'); + }, }, - }, }); export const { - addMessage, - updateMessage, - deleteMessage, - addToPendingMessages, - removePendingMessage, - addToMessageQueue, - clearMessageQueue, - setProcessing, - clearMessages, + addMessage, + updateMessage, + deleteMessage, + addToPendingMessages, + removePendingMessage, + addToMessageQueue, + clearMessageQueue, + setProcessing, + clearMessages, } = messageSlice.actions; export default messageSlice.reducer; \ No newline at end of file diff --git a/webapp/chat-app/src/types.ts b/webapp/chat-app/src/types.ts index 1df67261..82ffeb17 100644 --- a/webapp/chat-app/src/types.ts +++ b/webapp/chat-app/src/types.ts @@ -1,5 +1,5 @@ // Define theme names - export type ThemeName = 'main' | 'night' | 'forest' | 'pony' | 'alien'; +export type ThemeName = 'main' | 'night' | 'forest' | 'pony' | 'alien'; // Define log levels export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; @@ -14,54 +14,61 @@ export interface LogEntry { interface UIState { - activeTab: string; - modalOpen: boolean; - modalType: string | null; - verboseMode: boolean; - theme: ThemeName; + activeTab: string; + modalOpen: boolean; + modalType: string | null; + verboseMode: boolean; + theme: ThemeName; logHistory: LogEntry[]; } - // Message type - export interface Message { - id: string; - content: string; - type: 'user' | 'system' | 'response'; - version: string; - parentId?: string; - timestamp: number; - logLevel?: LogLevel; - } - // AppConfig type - export interface AppConfig { - singleInput: boolean; - stickyInput: boolean; - loadImages: boolean; - showMenubar: boolean; - applicationName?: string; - websocket: { - url: string; - port: string; - protocol: string; - }; - logging: { - enabled: boolean; - level: LogLevel; - maxEntries?: number; - persistLogs?: boolean; - }; - } - // UserInfo type - export interface UserInfo { - name: string; - isAuthenticated: boolean; - preferences?: Record; - } - export interface WebSocketState { - connected: boolean; - connecting: boolean; - error: string | null; - lastLog?: LogEntry; - } + +// Message type +export interface Message { + id: string; + content: string; + type: 'user' | 'system' | 'response'; + version: string; + parentId?: string; + timestamp: number; + logLevel?: LogLevel; + isHtml?: boolean; + rawHtml?: string | null; + sanitized?: boolean | null; +} + +// AppConfig type +export interface AppConfig { + singleInput: boolean; + stickyInput: boolean; + loadImages: boolean; + showMenubar: boolean; + applicationName?: string; + websocket: { + url: string; + port: string; + protocol: string; + }; + logging: { + enabled: boolean; + level: LogLevel; + maxEntries?: number; + persistLogs?: boolean; + }; +} + +// UserInfo type +export interface UserInfo { + name: string; + isAuthenticated: boolean; + preferences?: Record; +} + +export interface WebSocketState { + connected: boolean; + connecting: boolean; + error: string | null; + lastLog?: LogEntry; +} // Logger service interface export interface LoggerService { @@ -80,5 +87,5 @@ export interface LoggerService { // Export types export type { - UIState + UIState }; \ No newline at end of file diff --git a/webapp/chat-app/src/types/index.ts b/webapp/chat-app/src/types/index.ts index b9482a11..fd13d37f 100644 --- a/webapp/chat-app/src/types/index.ts +++ b/webapp/chat-app/src/types/index.ts @@ -30,6 +30,12 @@ export interface Message { parentId?: string; timestamp: number; logLevel?: LogLevel; + isHtml?: boolean; + rawHtml?: string | null; + sanitized?: boolean; + dangerouslySetInnerHTML?: { + __html: string; + }; } // AppConfig type diff --git a/webapp/chat-app/src/utils/logger.ts b/webapp/chat-app/src/utils/logger.ts index 96f6e494..19f83994 100644 --- a/webapp/chat-app/src/utils/logger.ts +++ b/webapp/chat-app/src/utils/logger.ts @@ -3,6 +3,7 @@ import {LogEntry, LogLevel} from '../types'; class Logger { private static instance: Logger; private logHistory: LogEntry[] = []; + private logLevel: LogLevel = 'info'; private constructor() { console.log('Logger initialized'); @@ -15,7 +16,17 @@ class Logger { return Logger.instance; } + public setLogLevel(level: LogLevel): void { + this.logLevel = level; + console.log(`Log level set to: ${level}`); + } + private log(level: LogLevel, message: string, data?: unknown): void { + // Skip debug logs unless debug level is enabled + if (level === 'debug' && this.logLevel !== 'debug') { + return; + } + const entry: LogEntry = { timestamp: Date.now(), level,