feat: improve AI chat utilities and types

- Enhance useChat hook
- Improve AI chat utilities
- Update AI types
This commit is contained in:
rcourtman 2026-01-22 22:32:22 +00:00
parent 1657beeb92
commit da9c322ddd
3 changed files with 33 additions and 7 deletions

View file

@ -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<string, unknown>;
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 });

View file

@ -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<string, unknown>;
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<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return entities[char];
});

View file

@ -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[];
}