Spaces:
Running
Running
import React, { useState, useEffect, useRef } from 'react'; | |
import { Send, Plus, MessageSquare, Settings, Users, Download, Trash2, RefreshCw, Bot } from 'lucide-react'; | |
const ChatFlowApp = () => { | |
const [messages, setMessages] = useState([]); | |
const [currentMessage, setCurrentMessage] = useState(''); | |
const [selectedModel, setSelectedModel] = useState('openai/gpt-3.5-turbo'); | |
const [isLoading, setIsLoading] = useState(false); | |
const [sessions, setSessions] = useState([]); | |
const [currentSessionId, setCurrentSessionId] = useState('default'); | |
const [onlineUsers, setOnlineUsers] = useState(1); | |
const [apiStatus, setApiStatus] = useState('Connected'); | |
const [autoSave, setAutoSave] = useState(true); | |
const messagesEndRef = useRef(null); | |
const [userId] = useState('User-' + Math.random().toString(36).substr(2, 8)); | |
const models = [ | |
{ name: "GPT-3.5 Turbo", id: "openai/gpt-3.5-turbo" }, | |
{ name: "LLaMA 3.1 8B", id: "meta-llama/llama-3.1-8b-instruct" }, | |
{ name: "LLaMA 3.1 70B", id: "meta-llama/llama-3.1-70b-instruct" }, | |
{ name: "DeepSeek Chat v3", id: "deepseek/deepseek-chat-v3-0324:free" }, | |
{ name: "DeepSeek R1", id: "deepseek/deepseek-r1-0528:free" }, | |
{ name: "Qwen3 Coder", id: "qwen/qwen3-coder:free" }, | |
{ name: "Microsoft MAI DS R1", id: "microsoft/mai-ds-r1:free" }, | |
{ name: "Gemma 3 27B", id: "google/gemma-3-27b-it:free" }, | |
{ name: "Gemma 3 4B", id: "google/gemma-3-4b-it:free" }, | |
{ name: "Auto (Best Available)", id: "openrouter/auto" } | |
]; | |
const scrollToBottom = () => { | |
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
}; | |
useEffect(() => { | |
scrollToBottom(); | |
}, [messages]); | |
useEffect(() => { | |
// Simulate online users update | |
const interval = setInterval(() => { | |
setOnlineUsers(Math.floor(Math.random() * 5) + 1); | |
}, 10000); | |
return () => clearInterval(interval); | |
}, []); | |
useEffect(() => { | |
// Check API status on component mount | |
const checkAPIStatus = async () => { | |
try { | |
const OPENROUTER_API_KEY = process.env.REACT_APP_OPENROUTER_API_KEY || | |
window.OPENROUTER_API_KEY || | |
process.env.OPENROUTER_API_KEY; | |
if (!OPENROUTER_API_KEY) { | |
setApiStatus('No API Key'); | |
return; | |
} | |
const response = await fetch("https://openrouter.ai/api/v1/models", { | |
headers: { "Authorization": `Bearer ${OPENROUTER_API_KEY}` } | |
}); | |
setApiStatus(response.ok ? 'Connected' : 'Error'); | |
} catch { | |
setApiStatus('Error'); | |
} | |
}; | |
checkAPIStatus(); | |
}, []); | |
const generateChatTitle = (msgs) => { | |
if (!msgs || msgs.length === 0) return "New Chat"; | |
const firstUserMessage = msgs.find(m => m.role === 'user'); | |
if (!firstUserMessage) return "New Chat"; | |
const content = firstUserMessage.content; | |
return content.length > 30 ? content.substring(0, 30) + "..." : content; | |
}; | |
const startNewChat = () => { | |
if (messages.length > 0) { | |
const newSession = { | |
id: 'session-' + Date.now(), | |
title: generateChatTitle(messages), | |
messages: [...messages], | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString() | |
}; | |
setSessions(prev => [newSession, ...prev]); | |
} | |
setMessages([]); | |
setCurrentSessionId('session-' + Date.now()); | |
}; | |
const loadSession = (session) => { | |
if (messages.length > 0) { | |
const currentSession = { | |
id: currentSessionId, | |
title: generateChatTitle(messages), | |
messages: [...messages], | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString() | |
}; | |
setSessions(prev => { | |
const filtered = prev.filter(s => s.id !== currentSessionId); | |
return [currentSession, ...filtered]; | |
}); | |
} | |
setMessages(session.messages); | |
setCurrentSessionId(session.id); | |
}; | |
const deleteSession = (sessionId) => { | |
setSessions(prev => prev.filter(s => s.id !== sessionId)); | |
if (sessionId === currentSessionId) { | |
startNewChat(); | |
} | |
}; | |
// OpenRouter API integration | |
const getAIResponse = async (userMessage) => { | |
setIsLoading(true); | |
try { | |
// Get API key from environment variables (Hugging Face Spaces secrets) | |
const OPENROUTER_API_KEY = process.env.REACT_APP_OPENROUTER_API_KEY || | |
window.OPENROUTER_API_KEY || | |
process.env.OPENROUTER_API_KEY; | |
if (!OPENROUTER_API_KEY) { | |
throw new Error("No API key found. Please add OPENROUTER_API_KEY to environment variables."); | |
} | |
const url = "https://openrouter.ai/api/v1/chat/completions"; | |
const headers = { | |
"Content-Type": "application/json", | |
"Authorization": `Bearer ${OPENROUTER_API_KEY}`, | |
"HTTP-Referer": "https://huggingface.co/spaces", | |
"X-Title": "Chat Flow AI Assistant" | |
}; | |
// Prepare messages for API | |
const apiMessages = [ | |
{ role: "system", content: "You are a helpful AI assistant. Provide clear and helpful responses." }, | |
...messages.map(msg => ({ | |
role: msg.role, | |
content: msg.content.split('\n\n---\n*Response created by:')[0] // Remove attribution from content | |
})), | |
{ role: "user", content: userMessage } | |
]; | |
const data = { | |
model: selectedModel, | |
messages: apiMessages, | |
stream: false, // Set to false for simpler handling in React | |
max_tokens: 2000, | |
temperature: 0.7, | |
top_p: 1, | |
frequency_penalty: 0, | |
presence_penalty: 0 | |
}; | |
const response = await fetch(url, { | |
method: 'POST', | |
headers: headers, | |
body: JSON.stringify(data) | |
}); | |
if (!response.ok) { | |
let errorDetail = ""; | |
try { | |
const errorData = await response.json(); | |
errorDetail = errorData.error?.message || `HTTP ${response.status}`; | |
} catch { | |
errorDetail = `HTTP ${response.status}: ${response.statusText}`; | |
} | |
throw new Error(`API Error: ${errorDetail}. Please try a different model or check your API key.`); | |
} | |
const result = await response.json(); | |
const aiResponse = result.choices[0].message.content; | |
const selectedModelName = models.find(m => m.id === selectedModel)?.name || "AI"; | |
setIsLoading(false); | |
return aiResponse + `\n\n---\n*Response created by: **${selectedModelName}***`; | |
} catch (error) { | |
setIsLoading(false); | |
console.error('API Error:', error); | |
if (error.message.includes('timeout')) { | |
return "Request timed out. Please try again with a shorter message or different model."; | |
} else if (error.message.includes('Connection')) { | |
return "Connection error. Please check your internet connection and try again."; | |
} else { | |
return `Error: ${error.message}`; | |
} | |
} | |
}; | |
const handleSendMessage = async (e) => { | |
e.preventDefault(); | |
if (!currentMessage.trim() || isLoading) return; | |
const userMessage = { | |
role: 'user', | |
content: currentMessage.trim(), | |
timestamp: new Date().toISOString() | |
}; | |
setMessages(prev => [...prev, userMessage]); | |
const messageToSend = currentMessage.trim(); | |
setCurrentMessage(''); | |
try { | |
const aiResponse = await getAIResponse(messageToSend); | |
const assistantMessage = { | |
role: 'assistant', | |
content: aiResponse, | |
timestamp: new Date().toISOString() | |
}; | |
setMessages(prev => [...prev, assistantMessage]); | |
} catch (error) { | |
const errorMessage = { | |
role: 'assistant', | |
content: 'Sorry, I encountered an error. Please try again.', | |
timestamp: new Date().toISOString() | |
}; | |
setMessages(prev => [...prev, errorMessage]); | |
} | |
}; | |
const clearChat = () => { | |
setMessages([]); | |
}; | |
const downloadHistory = () => { | |
const dataStr = JSON.stringify(messages, null, 2); | |
const dataBlob = new Blob([dataStr], { type: 'application/json' }); | |
const url = URL.createObjectURL(dataBlob); | |
const link = document.createElement('a'); | |
link.href = url; | |
link.download = `chat_history_${new Date().toISOString().split('T')[0]}.json`; | |
link.click(); | |
URL.revokeObjectURL(url); | |
}; | |
const getSelectedModelName = () => { | |
return models.find(m => m.id === selectedModel)?.name || "GPT-3.5 Turbo"; | |
}; | |
return ( | |
<div className="flex h-screen bg-gray-900 text-white"> | |
{/* Sidebar */} | |
<div className="w-80 bg-gray-800 border-r border-gray-700 flex flex-col"> | |
{/* Header */} | |
<div className="p-4 border-b border-gray-700"> | |
<div className="flex items-center gap-3 mb-4"> | |
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"> | |
<Bot className="w-5 h-5" /> | |
</div> | |
<h1 className="text-xl font-semibold">Chat Flow</h1> | |
</div> | |
<button | |
onClick={startNewChat} | |
className="w-full flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors" | |
> | |
<Plus className="w-4 h-4" /> | |
New Chat | |
</button> | |
</div> | |
{/* Chat History */} | |
<div className="flex-1 overflow-y-auto p-4"> | |
<h3 className="text-sm font-medium text-gray-400 mb-3">💬 Chat History</h3> | |
{sessions.length > 0 ? ( | |
<div className="space-y-2"> | |
{sessions.map((session) => ( | |
<div key={session.id} className="group flex items-center gap-2"> | |
<button | |
onClick={() => loadSession(session)} | |
className={`flex-1 text-left px-3 py-2 rounded-lg transition-colors ${ | |
session.id === currentSessionId | |
? 'bg-blue-600 text-white' | |
: 'bg-gray-700 hover:bg-gray-600 text-gray-300' | |
}`} | |
> | |
<div className="text-sm font-medium truncate">{session.title}</div> | |
<div className="text-xs text-gray-400"> | |
{new Date(session.updatedAt).toLocaleDateString()} | |
</div> | |
</button> | |
<button | |
onClick={() => deleteSession(session.id)} | |
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all" | |
> | |
<Trash2 className="w-4 h-4" /> | |
</button> | |
</div> | |
))} | |
</div> | |
) : ( | |
<p className="text-gray-500 text-sm">No previous chats yet</p> | |
)} | |
</div> | |
{/* Settings */} | |
<div className="p-4 border-t border-gray-700 space-y-4"> | |
{/* Online Users */} | |
<div> | |
<h3 className="text-sm font-medium text-gray-400 mb-2">👥 Who's Online</h3> | |
<div className="flex items-center gap-2 text-sm"> | |
<div className="w-2 h-2 bg-green-500 rounded-full"></div> | |
<span>{onlineUsers === 1 ? 'Just you online' : `${onlineUsers} people online`}</span> | |
</div> | |
<p className="text-xs text-gray-500 mt-1">You: {userId}</p> | |
</div> | |
{/* Model Selection */} | |
<div> | |
<label className="block text-sm font-medium text-gray-400 mb-2">AI Model</label> | |
<select | |
value={selectedModel} | |
onChange={(e) => setSelectedModel(e.target.value)} | |
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500" | |
> | |
{models.map((model) => ( | |
<option key={model.id} value={model.id}> | |
{model.name} | |
</option> | |
))} | |
</select> | |
<p className="text-xs text-green-400 mt-1 font-mono">{selectedModel}</p> | |
</div> | |
{/* API Status */} | |
<div className="flex items-center gap-2 text-sm"> | |
<div className={`w-2 h-2 rounded-full ${apiStatus === 'Connected' ? 'bg-green-500' : 'bg-red-500'}`}></div> | |
<span>{apiStatus === 'Connected' ? '🟢 API Connected' : '🔴 Connection Issue'}</span> | |
</div> | |
{/* Controls */} | |
<div className="flex gap-2"> | |
<button | |
onClick={downloadHistory} | |
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" | |
title="Download History" | |
> | |
<Download className="w-4 h-4" /> | |
</button> | |
<button | |
onClick={clearChat} | |
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" | |
title="Clear Chat" | |
> | |
<Trash2 className="w-4 h-4" /> | |
</button> | |
<button | |
onClick={() => window.location.reload()} | |
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" | |
title="Refresh" | |
> | |
<RefreshCw className="w-4 h-4" /> | |
</button> | |
</div> | |
</div> | |
</div> | |
{/* Main Chat Area */} | |
<div className="flex-1 flex flex-col"> | |
{/* Chat Messages */} | |
<div className="flex-1 overflow-y-auto"> | |
{messages.length === 0 ? ( | |
<div className="h-full flex items-center justify-center"> | |
<div className="text-center max-w-md mx-auto px-4"> | |
<div className="w-16 h-16 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-6"> | |
<Bot className="w-8 h-8 text-blue-400" /> | |
</div> | |
<h2 className="text-2xl font-semibold mb-4 text-gray-100">Your personal assistant</h2> | |
<p className="text-gray-400 leading-relaxed"> | |
A personal assistant streamlines your life by managing tasks, schedules, | |
and communications efficiently. | |
</p> | |
</div> | |
</div> | |
) : ( | |
<div className="p-6 space-y-6 max-w-4xl mx-auto"> | |
{messages.map((message, index) => ( | |
<div key={index} className="flex gap-4"> | |
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center flex-shrink-0"> | |
{message.role === 'user' ? ( | |
<div className="w-6 h-6 bg-blue-600 rounded-full"></div> | |
) : ( | |
<Bot className="w-5 h-5 text-blue-400" /> | |
)} | |
</div> | |
<div className="flex-1 min-w-0"> | |
<div className="prose prose-invert max-w-none"> | |
{message.role === 'assistant' && message.content.includes('---\n*Response created by:') ? ( | |
<> | |
<div className="whitespace-pre-wrap text-gray-100"> | |
{message.content.split('\n\n---\n*Response created by:')[0]} | |
</div> | |
<div className="text-xs text-gray-500 mt-2 italic"> | |
Response created by: <strong>{message.content.split('**')[1]}</strong> | |
</div> | |
</> | |
) : ( | |
<div className="whitespace-pre-wrap text-gray-100">{message.content}</div> | |
)} | |
</div> | |
</div> | |
</div> | |
))} | |
{isLoading && ( | |
<div className="flex gap-4"> | |
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center flex-shrink-0"> | |
<Bot className="w-5 h-5 text-blue-400" /> | |
</div> | |
<div className="flex-1"> | |
<div className="flex items-center gap-2 text-gray-400"> | |
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div> | |
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div> | |
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div> | |
</div> | |
</div> | |
</div> | |
)} | |
<div ref={messagesEndRef} /> | |
</div> | |
)} | |
</div> | |
{/* Message Input */} | |
<div className="border-t border-gray-700 p-4"> | |
<div className="max-w-4xl mx-auto"> | |
<div className="flex gap-3"> | |
<div className="flex-1 relative"> | |
<input | |
type="text" | |
value={currentMessage} | |
onChange={(e) => setCurrentMessage(e.target.value)} | |
onKeyDown={(e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
handleSendMessage(e); | |
} | |
}} | |
placeholder="Chat Smarter. Chat many Brains" | |
className="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
disabled={isLoading} | |
/> | |
</div> | |
<button | |
onClick={handleSendMessage} | |
disabled={!currentMessage.trim() || isLoading} | |
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors flex items-center gap-2" | |
> | |
<Send className="w-4 h-4" /> | |
Send | |
</button> | |
</div> | |
<div className="mt-2 text-center"> | |
<span className="text-xs text-gray-500">Currently using: <strong>{getSelectedModelName()}</strong></span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default ChatFlowApp; |