| import React, { useState, useRef, useEffect } from 'react'; |
| import { modelCompanies, allModels, findModelById } from './models/modelConfig'; |
| import { HuggingFaceService } from './services/huggingfaceService'; |
| import './App.css'; |
|
|
| |
| import { createIcons, Brain, Key, Sun, Moon, X, ChevronDown, Cpu, BrainCircuit, Atom, Check, Send, AlertCircle, Sparkles } from 'lucide-react'; |
|
|
| function App() { |
| const [messages, setMessages] = useState([]); |
| const [inputValue, setInputValue] = useState(''); |
| const [isLoading, setIsLoading] = useState(false); |
| const [isDarkMode, setIsDarkMode] = useState(false); |
| const [showModelDropdown, setShowModelDropdown] = useState(false); |
| const [selectedModel, setSelectedModel] = useState('deepseek-v3.2-exp'); |
| const [hfToken, setHfToken] = useState(''); |
| const [showAuth, setShowAuth] = useState(true); |
| const [error, setError] = useState(''); |
|
|
| const messagesEndRef = useRef(null); |
| const textareaRef = useRef(null); |
| const dropdownRef = useRef(null); |
| const currentMessageRef = useRef(null); |
|
|
| |
| useEffect(() => { |
| createIcons({ |
| icons: { |
| Brain, |
| Key, |
| Sun, |
| Moon, |
| X, |
| ChevronDown, |
| Cpu, |
| BrainCircuit, |
| Atom, |
| Check, |
| Send, |
| AlertCircle, |
| Sparkles |
| } |
| }); |
| }, []); |
|
|
| |
| useEffect(() => { |
| const storedToken = localStorage.getItem('hf_token'); |
| if (storedToken) { |
| setHfToken(storedToken); |
| setShowAuth(false); |
| } |
| }, []); |
|
|
| |
| useEffect(() => { |
| scrollToBottom(); |
| }, [messages]); |
|
|
| |
| useEffect(() => { |
| const handleClickOutside = (event) => { |
| if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { |
| setShowModelDropdown(false); |
| } |
| }; |
| document.addEventListener('mousedown', handleClickOutside); |
| return () => document.removeEventListener('mousedown', handleClickOutside); |
| }, []); |
|
|
| |
| useEffect(() => { |
| if (textareaRef.current) { |
| textareaRef.current.style.height = 'auto'; |
| textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px'; |
| } |
| }, [inputValue]); |
|
|
| |
| useEffect(() => { |
| if (isDarkMode) { |
| document.body.classList.add('dark'); |
| } else { |
| document.body.classList.remove('dark'); |
| } |
| }, [isDarkMode]); |
|
|
| const scrollToBottom = () => { |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }; |
|
|
| const handleTokenSubmit = () => { |
| if (!hfToken.trim()) { |
| setError('Please enter your Hugging Face token'); |
| return; |
| } |
| if (!hfToken.startsWith('hf_')) { |
| setError('Please enter a valid Hugging Face token'); |
| return; |
| } |
| localStorage.setItem('hf_token', hfToken.trim()); |
| setShowAuth(false); |
| setError(''); |
| }; |
|
|
| const handleClearToken = () => { |
| localStorage.removeItem('hf_token'); |
| setHfToken(''); |
| setShowAuth(true); |
| setMessages([]); |
| }; |
|
|
| const handleSendMessage = async () => { |
| if (!inputValue.trim() || isLoading || !hfToken) return; |
|
|
| const userMessage = { |
| id: Date.now(), |
| content: inputValue.trim(), |
| role: 'user', |
| timestamp: new Date() |
| }; |
|
|
| setMessages(prev => [...prev, userMessage]); |
| setInputValue(''); |
| setIsLoading(true); |
| setError(''); |
|
|
| |
| const assistantMessageId = Date.now() + 1; |
| const assistantMessage = { |
| id: assistantMessageId, |
| content: '', |
| role: 'assistant', |
| timestamp: new Date() |
| }; |
|
|
| setMessages(prev => [...prev, assistantMessage]); |
| currentMessageRef.current = assistantMessageId; |
|
|
| try { |
| const currentModelConfig = findModelById(selectedModel); |
| const hfService = new HuggingFaceService(hfToken); |
| |
| const chatMessages = [ |
| ...messages.filter(msg => msg.role !== 'assistant' || msg.content), |
| userMessage |
| ].map(msg => ({ |
| role: msg.role, |
| content: msg.content |
| })); |
|
|
| await hfService.streamChatCompletion( |
| chatMessages, |
| currentModelConfig, |
| (chunk) => { |
| setMessages(prev => prev.map(msg => |
| msg.id === assistantMessageId |
| ? { ...msg, content: msg.content + (chunk || '') } |
| : msg |
| )); |
| }, |
| () => { |
| setIsLoading(false); |
| currentMessageRef.current = null; |
| }, |
| (errorMsg) => { |
| setError(`Model error: ${errorMsg}`); |
| setIsLoading(false); |
| currentMessageRef.current = null; |
| setMessages(prev => prev.filter(msg => msg.id !== assistantMessageId)); |
| } |
| ); |
|
|
| } catch (err) { |
| console.error('Chat error:', err); |
| setError(`Failed to connect to AI model: ${err.message}`); |
| setIsLoading(false); |
| currentMessageRef.current = null; |
| setMessages(prev => prev.filter(msg => msg.id !== assistantMessageId)); |
| } |
| }; |
|
|
| const handleKeyPress = (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| handleSendMessage(); |
| } |
| }; |
|
|
| const handleModelSelect = (modelId) => { |
| setSelectedModel(modelId); |
| setShowModelDropdown(false); |
| }; |
|
|
| const currentModel = findModelById(selectedModel); |
| const groupedModels = modelCompanies; |
|
|
| |
| if (showAuth) { |
| return ( |
| <div className={`App ${isDarkMode ? 'dark' : ''}`}> |
| <div className="auth-modal"> |
| <div className="auth-content"> |
| <div className="logo" style={{ justifyContent: 'center', marginBottom: '24px' }}> |
| <i data-lucide="brain"></i> |
| <span style={{ fontSize: '28px' }}>SynapseAI</span> |
| </div> |
| |
| <h2 className="auth-title">Welcome to SynapseAI</h2> |
| <p className="auth-description"> |
| Enter your Hugging Face token to start chatting with AI models |
| </p> |
| {error && ( |
| <div className="error-message"> |
| <i data-lucide="alert-circle"></i> |
| {error} |
| </div> |
| )} |
| <div className="auth-input"> |
| <input |
| type="password" |
| className="input" |
| placeholder="Enter your Hugging Face token (hf_...)" |
| value={hfToken} |
| onChange={(e) => setHfToken(e.target.value)} |
| onKeyPress={(e) => e.key === 'Enter' && handleTokenSubmit()} |
| /> |
| </div> |
| <div className="auth-actions"> |
| <button className="btn primary" onClick={handleTokenSubmit}> |
| <i data-lucide="key"></i> |
| Start Chatting |
| </button> |
| </div> |
| <div className="token-info"> |
| <h4>How to get your Hugging Face token:</h4> |
| <ol> |
| <li>Go to <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer">huggingface.co</a></li> |
| <li>Sign in to your account</li> |
| <li>Go to Settings → Access Tokens</li> |
| <li>Create a new token with read permissions</li> |
| <li>Copy and paste it here</li> |
| </ol> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className={`App ${isDarkMode ? 'dark' : ''}`}> |
| <div className="chat-container"> |
| {/* Header */} |
| <header className="header"> |
| <div className="logo"> |
| <i data-lucide="brain"></i> |
| <span>SynapseAI</span> |
| </div> |
| |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> |
| <div className="token-display"> |
| <i data-lucide="key"></i> |
| <span className="token-text">Token: {hfToken.substring(0, 10)}...</span> |
| <div className="clear-token" onClick={handleClearToken} title="Clear token"> |
| <i data-lucide="x"></i> |
| </div> |
| </div> |
| |
| <button |
| className="btn ghost theme-toggle" |
| onClick={() => setIsDarkMode(!isDarkMode)} |
| > |
| <i data-lucide={isDarkMode ? "sun" : "moon"}></i> |
| </button> |
| </div> |
| </header> |
| |
| {/* Chat Messages */} |
| <div className="chat-messages"> |
| {messages.length === 0 && ( |
| <div className="welcome-message"> |
| <div className="card" style={{ maxWidth: '600px', margin: '0 auto' }}> |
| <i data-lucide="sparkles"></i> |
| <h2 style={{ marginBottom: '8px', fontSize: '24px', fontWeight: '600' }}>Welcome to SynapseAI</h2> |
| <p style={{ color: '#71717a', lineHeight: '1.5', marginBottom: '16px' }}> |
| Start a conversation with AI models. Select your preferred model below. |
| </p> |
| <p style={{ fontSize: '14px', color: '#a1a1aa' }}> |
| Current Model: <strong>{currentModel?.name}</strong> by {currentModel?.company} |
| </p> |
| </div> |
| </div> |
| )} |
| {messages.map((message) => ( |
| <div key={message.id} className={`message ${message.role}`}> |
| <div className="message-content"> |
| {message.content || (message.role === 'assistant' && isLoading && '...')} |
| </div> |
| </div> |
| ))} |
| {isLoading && !currentMessageRef.current && ( |
| <div className="message assistant"> |
| <div className="typing-indicator"> |
| <div className="typing-dot"></div> |
| <div className="typing-dot"></div> |
| <div className="typing-dot"></div> |
| </div> |
| </div> |
| )} |
| |
| <div ref={messagesEndRef} /> |
| </div> |
| |
| {/* Error Display */} |
| {error && ( |
| <div className="error-message"> |
| <i data-lucide="alert-circle"></i> |
| {error} |
| </div> |
| )} |
| |
| {/* Input Area */} |
| <div className="input-container"> |
| <div className="chat-input-wrapper"> |
| {/* Model Selector */} |
| <div className="dropdown" ref={dropdownRef}> |
| <button |
| className="btn model-selector" |
| onClick={() => setShowModelDropdown(!showModelDropdown)} |
| > |
| <i data-lucide={currentModel?.companyLogo || "cpu"}></i> |
| <span style={{ flex: 1, textAlign: 'left' }}>{currentModel?.name}</span> |
| <i data-lucide="chevron-down"></i> |
| </button> |
| {showModelDropdown && ( |
| <div className="dropdown-content"> |
| {groupedModels.map((company) => ( |
| <div key={company.id} className="company-section"> |
| <div className="company-header"> |
| <i data-lucide={company.logo}></i> |
| {company.name} |
| </div> |
| {company.models.map((model) => ( |
| <div |
| key={model.id} |
| className={`dropdown-item ${selectedModel === model.id ? 'active' : ''}`} |
| onClick={() => handleModelSelect(model.id)} |
| > |
| <div className="model-info"> |
| <div className="model-name">{model.name}</div> |
| <div className="model-description">{model.description}</div> |
| </div> |
| <div className="model-check"> |
| {selectedModel === model.id && <i data-lucide="check"></i>} |
| </div> |
| </div> |
| ))} |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| |
| {/* Chat Input */} |
| <textarea |
| ref={textareaRef} |
| className="input chat-input" |
| placeholder="Message SynapseAI..." |
| value={inputValue} |
| onChange={(e) => setInputValue(e.target.value)} |
| onKeyPress={handleKeyPress} |
| disabled={isLoading} |
| rows={1} |
| /> |
| |
| {/* Send Button */} |
| <button |
| className="send-button" |
| onClick={handleSendMessage} |
| disabled={!inputValue.trim() || isLoading} |
| > |
| <i data-lucide="send"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| export default App; |