LLMChat.tsx
1 import { useState, useEffect } from 'react'; 2 import { useLLM, useProviderStatus } from '../hooks/useLLM'; 3 import { useAppStore } from '../store'; 4 import { LLMService } from '../services/llm'; 5 import type { Message, Provider } from '../types/llm'; 6 import './LLMChat.css'; 7 8 export function LLMChat() { 9 const [input, setInput] = useState(''); 10 const [providers, setProviders] = useState<string[]>([]); 11 12 // Global state 13 const { 14 messages, 15 addMessage, 16 clearMessages, 17 currentProvider, 18 setProvider, 19 } = useAppStore(); 20 21 // LLM hook 22 const { loading, streaming, error, streamContent, streamMessage } = useLLM({ 23 provider: currentProvider, 24 }); 25 26 // Provider status 27 const { status, checkStatus } = useProviderStatus(currentProvider); 28 29 // Load providers on mount 30 useEffect(() => { 31 const loadProviders = async () => { 32 try { 33 const providerList = await LLMService.listProviders(); 34 setProviders(providerList); 35 } catch (err) { 36 console.error('Failed to load providers:', err); 37 } 38 }; 39 40 loadProviders(); 41 checkStatus(); 42 }, [checkStatus]); 43 44 const handleSend = async () => { 45 if (!input.trim() || loading) return; 46 47 const userMessage: Message = { 48 role: 'user', 49 content: input, 50 }; 51 52 addMessage(userMessage); 53 setInput(''); 54 55 const messageHistory = [...messages, userMessage]; 56 await streamMessage(messageHistory); 57 }; 58 59 const handleProviderChange = async (provider: string) => { 60 await LLMService.setCurrentProvider(provider as Provider); 61 setProvider(provider as Provider); 62 await checkStatus(); 63 }; 64 65 return ( 66 <div className="llm-chat"> 67 <div className="chat-header"> 68 <div className="provider-controls"> 69 <label>Provider:</label> 70 <select 71 value={currentProvider} 72 onChange={(e) => handleProviderChange(e.target.value)} 73 className="provider-select" 74 > 75 {providers.map((p) => ( 76 <option key={p} value={p}> 77 {p} 78 </option> 79 ))} 80 </select> 81 82 <button onClick={clearMessages} className="btn-secondary"> 83 Clear History 84 </button> 85 </div> 86 87 {status && ( 88 <div className="status-bar"> 89 <span className={`status-indicator ${status.available ? 'online' : 'offline'}`}> 90 {status.available ? '●' : '○'} 91 </span> 92 <span className="status-text"> 93 {status.name} - {status.configured ? 'Configured' : 'Not Configured'} 94 </span> 95 {status.error && <span className="status-error">({status.error})</span>} 96 </div> 97 )} 98 </div> 99 100 <div className="messages-container"> 101 {messages.length === 0 && ( 102 <div className="empty-state"> 103 <p>No messages yet. Start a conversation!</p> 104 </div> 105 )} 106 107 {messages.map((msg, idx) => ( 108 <div key={idx} className={`message message-${msg.role}`}> 109 <div className="message-role">{msg.role}</div> 110 <div className="message-content">{msg.content}</div> 111 </div> 112 ))} 113 114 {streaming && ( 115 <div className="message message-assistant streaming"> 116 <div className="message-role">assistant</div> 117 <div className="message-content"> 118 {streamContent} 119 <span className="cursor">▋</span> 120 </div> 121 </div> 122 )} 123 124 {error && ( 125 <div className="message message-error"> 126 <div className="message-role">error</div> 127 <div className="message-content">{error}</div> 128 </div> 129 )} 130 </div> 131 132 <div className="input-container"> 133 <input 134 type="text" 135 value={input} 136 onChange={(e) => setInput(e.target.value)} 137 onKeyPress={(e) => e.key === 'Enter' && handleSend()} 138 placeholder="Type your message..." 139 disabled={loading} 140 className="message-input" 141 /> 142 <button 143 onClick={handleSend} 144 disabled={loading || !input.trim()} 145 className="btn-send" 146 > 147 {loading ? (streaming ? 'Streaming...' : 'Sending...') : 'Send'} 148 </button> 149 </div> 150 151 <div className="stats"> 152 <span>{messages.length} messages</span> 153 <span>{streaming ? 'Streaming active' : 'Ready'}</span> 154 </div> 155 </div> 156 ); 157 }