import { useState, useEffect, useRef, useCallback } from 'react'; import _rwsConfig, { ReadyState } from 'react-use-websocket'; const useWebSocket = (() => { if (typeof _rwsConfig === 'function') return _rwsConfig; const a = _rwsConfig as any; if (typeof a?.default === 'function') return a.default; if (typeof a?.useWebSocket === 'function') return a.useWebSocket; return _rwsConfig; })() as typeof _rwsConfig; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import type { Session, DbMessage } from '../types'; import { t } from '../i18n'; import ToolUseBlock from './ToolUseBlock'; const WS_URL = '/ws'; // Config bot uses a single fixed session title const CONFIG_SESSION_TITLE = 'Config Bot'; interface DisplayMessage { id: string; role: 'user' | 'assistant' | 'tool_use' | 'tool_result'; content: string; toolName?: string; toolInput?: Record; timestamp: number; } function dbMessageToDisplay(m: DbMessage): DisplayMessage { let toolInput: Record | undefined; if (m.tool_input) { try { toolInput = JSON.parse(m.tool_input); } catch { toolInput = { raw: m.tool_input }; } } return { id: String(m.id), role: m.role, content: m.content ?? '', toolName: m.tool_name ?? undefined, toolInput, timestamp: new Date(m.created_at).getTime(), }; } interface MessageBubbleProps { message: DisplayMessage; } function MessageBubble({ message }: MessageBubbleProps) { const { role, content, toolName, toolInput, timestamp } = message; if (role === 'tool_use' && toolName) { return (
{new Date(timestamp).toLocaleTimeString()}
); } if (role === 'tool_result') { return (
result: {content.slice(0, 300)} {content.length > 300 ? '...' : ''}
); } const isUser = role === 'user'; return (
{isUser ? ( {content} ) : (
{content}
)}
{new Date(timestamp).toLocaleTimeString()}
); } const QUICK_CHIPS = [ { labelKey: 'config.chipSettings', value: 'Show me all current settings' }, { labelKey: 'config.chipSkills', value: 'List all available skills' }, { labelKey: 'config.chipSecrets', value: 'Help me add a new secret / API key' }, { labelKey: 'config.chipMcp', value: 'Show and manage MCP servers' }, { labelKey: 'config.chipChannels', value: 'Show and manage channel connections' }, { labelKey: 'config.chipCli', value: 'Detect installed AI CLIs' }, ]; export default function ConfigBotPage() { const [configSessionId, setConfigSessionId] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isRunning, setIsRunning] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const sessionIdRef = useRef(null); const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(WS_URL, { shouldReconnect: () => true, reconnectInterval: 2000, reconnectAttempts: 20, onOpen: () => { if (sessionIdRef.current) { sendJsonMessage({ type: 'subscribe', sessionId: sessionIdRef.current }); } }, }); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isRunning]); // Find or create the single Config Bot session useEffect(() => { fetch('/api/sessions') .then((r) => r.json()) .then((data: Session[]) => { if (!Array.isArray(data)) return; const existing = data.find((s) => s.title === CONFIG_SESSION_TITLE); if (existing) { setConfigSessionId(existing.id); sessionIdRef.current = existing.id; } else { // Create it fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: CONFIG_SESSION_TITLE }), }) .then((r) => r.json()) .then((s: Session) => { setConfigSessionId(s.id); sessionIdRef.current = s.id; }) .catch(console.error); } }) .catch(console.error); }, []); // Subscribe when session is ready and WS is open useEffect(() => { if (!configSessionId || readyState !== ReadyState.OPEN) return; sendJsonMessage({ type: 'subscribe', sessionId: configSessionId }); }, [configSessionId, readyState, sendJsonMessage]); // Handle WS messages useEffect(() => { if (!lastJsonMessage) return; const msg = lastJsonMessage as any; switch (msg.type) { case 'connected': if (sessionIdRef.current) { sendJsonMessage({ type: 'subscribe', sessionId: sessionIdRef.current }); } break; case 'history': { const displayed = (msg.messages as DbMessage[]).map(dbMessageToDisplay); setMessages(displayed); setIsRunning(msg.running); break; } case 'user_message': break; case 'assistant_message': { const assistantMsg: DisplayMessage = { id: `assistant-${Date.now()}-${Math.random()}`, role: 'assistant', content: msg.content, timestamp: Date.now(), }; setMessages((prev) => [...prev, assistantMsg]); break; } case 'tool_use': { const toolMsg: DisplayMessage = { id: `tool-${Date.now()}-${Math.random()}`, role: 'tool_use', content: '', toolName: msg.toolName, toolInput: msg.toolInput, timestamp: Date.now(), }; setMessages((prev) => [...prev, toolMsg]); break; } case 'tool_result': { const resultMsg: DisplayMessage = { id: `result-${Date.now()}-${Math.random()}`, role: 'tool_result', content: msg.content, timestamp: Date.now(), }; setMessages((prev) => [...prev, resultMsg]); break; } case 'result': setIsRunning(false); break; case 'interrupted': setIsRunning(false); break; case 'error': console.error('[ConfigBot WS error]:', msg.error); setIsRunning(false); break; } }, [lastJsonMessage, sendJsonMessage]); const sendMessage = useCallback( (text?: string) => { const content = (text ?? input).trim(); if (!content || readyState !== ReadyState.OPEN || isRunning || !configSessionId) return; const userMsg: DisplayMessage = { id: `local-${Date.now()}`, role: 'user', content, timestamp: Date.now(), }; setMessages((prev) => [...prev, userMsg]); if (!text) setInput(''); setIsRunning(true); sendJsonMessage({ type: 'chat', sessionId: configSessionId, content, cli: 'claude', configBot: true, }); }, [input, readyState, isRunning, configSessionId, sendJsonMessage] ); const interruptSession = useCallback(() => { if (!configSessionId) return; sendJsonMessage({ type: 'interrupt', sessionId: configSessionId }); setIsRunning(false); }, [configSessionId, sendJsonMessage]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const wsOpen = readyState === ReadyState.OPEN; return (
{/* Header */}
🛠️

{t('config.title')}

{t('config.subtitle')}

{isRunning && ( )}
{wsOpen ? 'Connected' : 'Disconnected'}
{/* Quick action chips */}
{QUICK_CHIPS.map(({ labelKey, value }) => ( ))}
{/* Messages */}
{messages.length === 0 && !isRunning && (
🛠️

{t('config.subtitle')}

Use the quick actions above or type a request below

)} {messages.map((msg) => ( ))} {isRunning && (
)}
{/* Input */}