diff --git a/frontend-modern/src/components/AI/Chat/hooks/useChat.ts b/frontend-modern/src/components/AI/Chat/hooks/useChat.ts index 2cbae7ded..6ebb94379 100644 --- a/frontend-modern/src/components/AI/Chat/hooks/useChat.ts +++ b/frontend-modern/src/components/AI/Chat/hooks/useChat.ts @@ -86,6 +86,16 @@ export function useChat(options: UseChatOptions = {}) { }; // Process stream events + const extractText = (value: unknown): string => { + if (typeof value === 'string') return value; + if (value && typeof value === 'object') { + const record = value as Record; + if (typeof record.text === 'string') return record.text; + if (typeof record.content === 'string') return record.content; + } + return ''; + }; + const processEvent = ( assistantId: string, event: StreamEvent @@ -97,7 +107,7 @@ export function useChat(options: UseChatOptions = {}) { try { switch (event.type) { case 'content': { - const content = event.data as string; + const content = extractText(event.data); if (!content) return msg; const existing = msg.content || ''; // Add to streamEvents for chronological display @@ -109,7 +119,7 @@ export function useChat(options: UseChatOptions = {}) { } case 'thinking': { - const thinking = event.data as string; + const thinking = extractText(event.data); if (!thinking) return msg; // Add thinking to streamEvents const updated = addStreamEvent(msg, { type: 'thinking', thinking }); diff --git a/frontend-modern/src/components/AI/aiChatUtils.ts b/frontend-modern/src/components/AI/aiChatUtils.ts index 5e7d7efba..08f1aaf27 100644 --- a/frontend-modern/src/components/AI/aiChatUtils.ts +++ b/frontend-modern/src/components/AI/aiChatUtils.ts @@ -66,12 +66,28 @@ const configureDOMPurify = () => { }); }; +const coerceMarkdownInput = (content: unknown): string => { + if (typeof content === 'string') return content; + if (content && typeof content === 'object') { + const record = content as Record; + if (typeof record.text === 'string') return record.text; + if (typeof record.content === 'string') return record.content; + try { + return JSON.stringify(content); + } catch { + return String(content); + } + } + return content == null ? '' : String(content); +}; + // Helper to render markdown safely with XSS protection // LLM output should NEVER be trusted - always sanitize before rendering as HTML -export const renderMarkdown = (content: string): string => { +export const renderMarkdown = (content: unknown): string => { + const normalized = coerceMarkdownInput(content); try { configureDOMPurify(); - const rawHtml = marked.parse(content) as string; + const rawHtml = marked.parse(normalized) as string; // Sanitize to prevent XSS from malicious LLM output or injected content return DOMPurify.sanitize(rawHtml, { // Allow common formatting tags but block scripts, iframes, etc. @@ -84,7 +100,7 @@ export const renderMarkdown = (content: string): string => { }); } catch { // If parsing fails, escape HTML entities as fallback - return content.replace(/[&<>"']/g, (char) => { + return normalized.replace(/[&<>"']/g, (char) => { const entities: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return entities[char]; }); diff --git a/frontend-modern/src/types/ai.ts b/frontend-modern/src/types/ai.ts index 8cf9a8ab1..a8487448c 100644 --- a/frontend-modern/src/types/ai.ts +++ b/frontend-modern/src/types/ai.ts @@ -49,7 +49,7 @@ export interface AISettings { request_timeout_seconds?: number; // Infrastructure control settings - control_level?: 'read_only' | 'suggest' | 'controlled' | 'autonomous'; + control_level?: 'read_only' | 'controlled' | 'autonomous'; protected_guests?: string[]; } @@ -92,7 +92,7 @@ export interface AISettingsUpdateRequest { request_timeout_seconds?: number; // Infrastructure control settings - control_level?: 'read_only' | 'suggest' | 'controlled' | 'autonomous'; + control_level?: 'read_only' | 'controlled' | 'autonomous'; protected_guests?: string[]; }