/ app / src / components / LLMChat.tsx
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  }