SynapseAI / src /App.js
kaiiddo's picture
Update src/App.js
7940110 verified
import React, { useState, useRef, useEffect } from 'react';
import { modelCompanies, allModels, findModelById } from './models/modelConfig';
import { HuggingFaceService } from './services/huggingfaceService';
import './App.css';
// Import Lucide icons
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);
// Initialize Lucide icons
useEffect(() => {
createIcons({
icons: {
Brain,
Key,
Sun,
Moon,
X,
ChevronDown,
Cpu,
BrainCircuit,
Atom,
Check,
Send,
AlertCircle,
Sparkles
}
});
}, []);
// Check for stored token
useEffect(() => {
const storedToken = localStorage.getItem('hf_token');
if (storedToken) {
setHfToken(storedToken);
setShowAuth(false);
}
}, []);
// Scroll to bottom
useEffect(() => {
scrollToBottom();
}, [messages]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setShowModelDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Auto-resize textarea
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px';
}
}, [inputValue]);
// Toggle dark mode
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('');
// Create assistant message for streaming
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;
// Auth Modal
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;