import React, {useEffect, useRef, useState} from 'react'; import {Link} from 'react-router'; import {getAiCatalog, getChatModes} from '../api'; import {useAiChat} from '../contexts/AiChatContext'; import {InfoMessage} from './Common'; import {Button, Group, Icon, Loader, Text, TextInput} from './ui'; const MODE_LABELS = {help: 'Help', research: 'Research', system: 'System'}; const MODE_DESCRIPTIONS = { help: 'Learn how to use WROLPi', research: 'Find and summarize library content', system: 'Troubleshoot this WROLPi', }; // Render an assistant message with WROLPi links clickable. The model only ever returns // relative links (/videos/123), which the server computed — never external URLs. export function LinkifiedText({content}) { const parts = String(content || '').split(/(\/[a-zA-Z0-9_\-./?=&%#]+)/g); return {parts.map((part, i) => part.startsWith('/') && part.length > 1 ? {part} : part)} ; } function ToolActivity({activity}) { if (!activity || !activity.length) { return null; } return
{activity.map((item, i) => {item.pending ? : } {' '}using {String(item.tool || '').replace(/_/g, ' ')} )}
; } export function ChatMessage({message}) { if (message.role === 'user') { return
{message.content}
; } return
{message.status && {message.status}} {message.content && } {message.streaming && !message.content && !message.activity?.length && } {message.error && {message.error}}
; } export function AiChat() { const {mode, setMode, messages, sending, send, stop, disabledCode} = useAiChat(); const [modes, setModes] = useState([]); const [slowHardware, setSlowHardware] = useState(false); const [aiEnabled, setAiEnabled] = useState(null); const [input, setInput] = useState(''); const bottomRef = useRef(null); useEffect(() => { getChatModes().then(i => i && setModes(i)).catch(console.error); getAiCatalog().then(catalog => { if (catalog) { setSlowHardware(catalog.slow_hardware === true); setAiEnabled(Boolean(catalog.enabled && catalog.active_model)); } }).catch(console.error); }, []); useEffect(() => { bottomRef.current?.scrollIntoView?.({behavior: 'smooth'}); }, [messages]); const handleSend = async (content) => { setInput(''); await send(content); }; const notReady = aiEnabled === false || disabledCode === 'AI_DISABLED'; if (notReady) { return The AI assistant is not ready. Download a model and enable it on the Manage tab. ; } const suggestions = (modes.find(i => i.name === mode) || {}).suggestions || []; return <> {slowHardware && This hardware runs the assistant slowly — answers can take minutes. } {(modes.length ? modes.map(i => i.name) : Object.keys(MODE_LABELS)).map(name => )} {mode && {MODE_DESCRIPTIONS[mode]}} {!mode && Pick a mode to start: Help teaches WROLPi, Research searches your library, System troubleshoots this device. } {mode && <>
{messages.length === 0 && {suggestions.map(suggestion => )} } {messages.map((message, i) => )}
{ e.preventDefault(); if (!sending) { handleSend(input.trim()); } }}> setInput(e.target.value)} aria-label='Chat message' /> {sending ? : }
} ; }