Spaces:
Sleeping
Sleeping
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Chat Flow π·</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<style> | |
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; } | |
.chat-scroll { scrollbar-width: thin; scrollbar-color: #4a5568 #2d3748; } | |
.chat-scroll::-webkit-scrollbar { width: 6px; } | |
.chat-scroll::-webkit-scrollbar-track { background: #2d3748; } | |
.chat-scroll::-webkit-scrollbar-thumb { background: #4a5568; border-radius: 3px; } | |
.animate-bounce { animation: bounce 1s infinite; } | |
0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1); } | |
50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1); } | |
} | |
.loading-dot-1 { animation-delay: 0s; } | |
.loading-dot-2 { animation-delay: 0.1s; } | |
.loading-dot-3 { animation-delay: 0.2s; } | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white h-screen overflow-hidden"> | |
<div class="flex h-screen"> | |
<!-- Sidebar --> | |
<div class="w-80 bg-gray-800 border-r border-gray-700 flex flex-col"> | |
<!-- Header --> | |
<div class="p-4 border-b border-gray-700"> | |
<div class="flex items-center gap-3 mb-4"> | |
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"> | |
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> | |
</svg> | |
</div> | |
<h1 class="text-xl font-semibold">Chat Flow</h1> | |
</div> | |
<button onclick="startNewChat()" class="w-full flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> | |
</svg> | |
New Chat | |
</button> | |
</div> | |
<!-- Chat History --> | |
<div class="flex-1 overflow-y-auto p-4 chat-scroll"> | |
<h3 class="text-sm font-medium text-gray-400 mb-3">π¬ Chat History</h3> | |
<div id="sessions-list" class="space-y-2"> | |
<p class="text-gray-500 text-sm">No previous chats yet</p> | |
</div> | |
</div> | |
<!-- Settings --> | |
<div class="p-4 border-t border-gray-700 space-y-4"> | |
<!-- Online Users --> | |
<div> | |
<h3 class="text-sm font-medium text-gray-400 mb-2">π₯ Who's Online</h3> | |
<div class="flex items-center gap-2 text-sm mb-2"> | |
<div class="w-2 h-2 bg-green-500 rounded-full"></div> | |
<span id="online-status">Just you online</span> | |
</div> | |
<!-- User Details --> | |
<div class="bg-gray-700 rounded-lg p-3 mb-2 max-h-32 overflow-y-auto chat-scroll"> | |
<div id="users-list" class="space-y-1 text-xs"> | |
<div class="text-green-400">π’ <span id="current-user-info">You: Loading...</span></div> | |
</div> | |
</div> | |
<button onclick="refreshUsers()" class="w-full text-xs px-2 py-1 bg-gray-600 hover:bg-gray-500 rounded transition-colors"> | |
π Refresh Users | |
</button> | |
</div> | |
<!-- Model Selection --> | |
<div> | |
<label class="block text-sm font-medium text-gray-400 mb-2">AI Model</label> | |
<select id="model-select" class="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"> | |
<option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo</option> | |
<option value="meta-llama/llama-3.1-8b-instruct">LLaMA 3.1 8B</option> | |
<option value="meta-llama/llama-3.1-70b-instruct">LLaMA 3.1 70B</option> | |
<option value="deepseek/deepseek-chat-v3-0324:free">DeepSeek Chat v3</option> | |
<option value="deepseek/deepseek-r1-0528:free">DeepSeek R1</option> | |
<option value="qwen/qwen3-coder:free">Qwen3 Coder</option> | |
<option value="microsoft/mai-ds-r1:free">Microsoft MAI DS R1</option> | |
<option value="google/gemma-3-27b-it:free">Gemma 3 27B</option> | |
<option value="google/gemma-3-4b-it:free">Gemma 3 4B</option> | |
<option value="openrouter/auto">Auto (Best Available)</option> | |
</select> | |
<p class="text-xs text-green-400 mt-1 font-mono" id="model-id">openai/gpt-3.5-turbo</p> | |
</div> | |
<!-- API Status --> | |
<div class="flex items-center gap-2 text-sm"> | |
<div id="api-dot" class="w-2 h-2 rounded-full bg-green-500"></div> | |
<span id="api-status">π’ API Connected</span> | |
</div> | |
<!-- Controls --> | |
<div class="flex gap-2"> | |
<button onclick="downloadHistory()" class="flex-1 flex items-center justify-center px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" title="Download History"> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/> | |
</svg> | |
</button> | |
<button onclick="clearChat()" class="flex-1 flex items-center justify-center px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" title="Clear Chat"> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/> | |
</svg> | |
</button> | |
<button onclick="window.location.reload()" class="flex-1 flex items-center justify-center px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm" title="Refresh"> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Main Chat Area --> | |
<div class="flex-1 flex flex-col"> | |
<!-- Chat Messages --> | |
<div id="messages-container" class="flex-1 overflow-y-auto chat-scroll"> | |
<!-- Welcome Screen --> | |
<div id="welcome-screen" class="h-full flex items-center justify-center"> | |
<div class="text-center max-w-md mx-auto px-4"> | |
<div class="w-16 h-16 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-6"> | |
<svg class="w-8 h-8 text-blue-400" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> | |
</svg> | |
</div> | |
<h2 class="text-2xl font-semibold mb-4 text-gray-100">Which model would you like to talk with today?</h2> | |
<p class="text-gray-400 leading-relaxed">Choose from 10 powerful AI models and start chatting. Each model has unique strengths for different tasks.</p> | |
</div> | |
</div> | |
<!-- Messages Area --> | |
<div id="messages-area" class="p-6 space-y-6 max-w-4xl mx-auto" style="display: none;"> | |
</div> | |
</div> | |
<!-- Message Input --> | |
<div class="border-t border-gray-700 p-4"> | |
<div class="max-w-4xl mx-auto"> | |
<div class="flex gap-3"> | |
<div class="flex-1 relative"> | |
<input | |
type="text" | |
id="message-input" | |
placeholder="Chat Smarter. Chat many Brains" | |
class="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" | |
/> | |
</div> | |
<button | |
onclick="sendMessage()" | |
id="send-button" | |
class="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" | |
> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/> | |
</svg> | |
Send | |
</button> | |
</div> | |
<div class="mt-2 text-center"> | |
<span class="text-xs text-gray-500">Currently using: <strong id="current-model-name">GPT-3.5 Turbo</strong></span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// π OpenRouter API Key | |
const OPENROUTER_API_KEY = "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"; | |
// Global variables | |
let messages = []; | |
let sessions = []; | |
let currentSessionId = 'default'; | |
let isLoading = false; | |
let selectedModel = 'openai/gpt-3.5-turbo'; | |
let userId = 'User-' + Math.random().toString(36).substr(2, 8); | |
const models = { | |
'openai/gpt-3.5-turbo': 'GPT-3.5 Turbo', | |
'meta-llama/llama-3.1-8b-instruct': 'LLaMA 3.1 8B', | |
'meta-llama/llama-3.1-70b-instruct': 'LLaMA 3.1 70B', | |
'deepseek/deepseek-chat-v3-0324:free': 'DeepSeek Chat v3', | |
'deepseek/deepseek-r1-0528:free': 'DeepSeek R1', | |
'qwen/qwen3-coder:free': 'Qwen3 Coder', | |
'microsoft/mai-ds-r1:free': 'Microsoft MAI DS R1', | |
'google/gemma-3-27b-it:free': 'Gemma 3 27B', | |
'google/gemma-3-4b-it:free': 'Gemma 3 4B', | |
'openrouter/auto': 'Auto (Best Available)' | |
}; | |
// Initialize | |
document.addEventListener('DOMContentLoaded', async function() { | |
setupEventListeners(); | |
// Get user location first | |
await getUserLocation(); | |
// Then update users and check API | |
updateOnlineUsers(); | |
checkAPIStatus(); | |
// Track activity for online users - update every 10 seconds | |
setInterval(updateOnlineUsers, 10000); | |
// Track user activity | |
document.addEventListener('click', trackActivity); | |
document.addEventListener('keypress', trackActivity); | |
document.addEventListener('scroll', trackActivity); | |
}); | |
function setupEventListeners() { | |
// Model selection | |
document.getElementById('model-select').addEventListener('change', function(e) { | |
selectedModel = e.target.value; | |
document.getElementById('model-id').textContent = selectedModel; | |
document.getElementById('current-model-name').textContent = models[selectedModel]; | |
}); | |
// Enter key to send message | |
document.getElementById('message-input').addEventListener('keydown', function(e) { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
} | |
async function sendMessage() { | |
const input = document.getElementById('message-input'); | |
const message = input.value.trim(); | |
if (!message || isLoading) return; | |
// Track activity when sending message | |
trackActivity(); | |
// Add user message | |
const userMessage = { | |
role: 'user', | |
content: message, | |
timestamp: new Date().toISOString() | |
}; | |
messages.push(userMessage); | |
input.value = ''; | |
updateMessagesDisplay(); | |
// Add empty assistant message for streaming | |
const assistantMessage = { | |
role: 'assistant', | |
content: '', | |
timestamp: new Date().toISOString() | |
}; | |
messages.push(assistantMessage); | |
setLoading(true); | |
updateMessagesDisplay(); // Show the empty assistant message with loading | |
try { | |
const aiResponse = await getAIResponse(message); | |
// Update the last message with final response | |
messages[messages.length - 1].content = aiResponse; | |
// Remove cursor and show final response | |
updateMessagesDisplay(); | |
// Track activity after receiving response | |
trackActivity(); | |
} catch (error) { | |
// Replace the empty message with error | |
messages[messages.length - 1].content = 'Sorry, I encountered an error. Please try again.'; | |
updateMessagesDisplay(); | |
} | |
setLoading(false); | |
} | |
async function getAIResponse(userMessage) { | |
if (!OPENROUTER_API_KEY || OPENROUTER_API_KEY === "YOUR_API_KEY_HERE") { | |
throw new Error("Please add your OpenRouter API key to the code."); | |
} | |
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" | |
}; | |
const cleanMessages = messages.map(msg => ({ | |
role: msg.role, | |
content: msg.content.split('\n\n---\n*Response created by:')[0] | |
})); | |
const apiMessages = [ | |
{ role: "system", content: "You are a helpful AI assistant. Provide clear and helpful responses." } | |
].concat(cleanMessages).concat([ | |
{ role: "user", content: userMessage } | |
]); | |
const data = { | |
model: selectedModel, | |
messages: apiMessages, | |
stream: true, // Enable streaming | |
max_tokens: 2000, | |
temperature: 0.7 | |
}; | |
const response = await fetch(url, { | |
method: 'POST', | |
headers: headers, | |
body: JSON.stringify(data) | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
throw new Error("API Error: " + response.status + " - " + errorText); | |
} | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let fullResponse = ""; | |
try { | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value); | |
const lines = chunk.split('\n'); | |
for (const line of lines) { | |
if (line.startsWith('data: ')) { | |
const data = line.slice(6); | |
if (data === '[DONE]') { | |
const modelName = models[selectedModel] || "AI"; | |
return fullResponse + "\n\n---\n*Response created by: **" + modelName + "***"; | |
} | |
try { | |
const parsed = JSON.parse(data); | |
const delta = parsed.choices?.[0]?.delta?.content || ''; | |
if (delta) { | |
fullResponse += delta; | |
// Update the display in real-time | |
updateStreamingResponse(fullResponse); | |
} | |
} catch (e) { | |
// Skip invalid JSON | |
continue; | |
} | |
} | |
} | |
} | |
} finally { | |
reader.releaseLock(); | |
} | |
const modelName = models[selectedModel] || "AI"; | |
return fullResponse + "\n\n---\n*Response created by: **" + modelName + "***"; | |
} | |
function updateStreamingResponse(partialResponse) { | |
// Find the last message area and update it with streaming text | |
const messagesArea = document.getElementById('messages-area'); | |
const messageElements = messagesArea.children; | |
if (messageElements.length > 0) { | |
const lastMessage = messageElements[messageElements.length - 1]; | |
const contentDiv = lastMessage.querySelector('.flex-1 .whitespace-pre-wrap'); | |
if (contentDiv) { | |
contentDiv.textContent = partialResponse + " β"; // Add cursor | |
} | |
} | |
} | |
function setLoading(loading) { | |
isLoading = loading; | |
const button = document.getElementById('send-button'); | |
const input = document.getElementById('message-input'); | |
if (loading) { | |
button.disabled = true; | |
input.disabled = true; | |
button.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div> Loading...'; | |
} else { | |
button.disabled = false; | |
input.disabled = false; | |
button.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Send'; | |
} | |
} | |
function updateMessagesDisplay() { | |
const welcomeScreen = document.getElementById('welcome-screen'); | |
const messagesArea = document.getElementById('messages-area'); | |
if (messages.length === 0) { | |
welcomeScreen.style.display = 'flex'; | |
messagesArea.style.display = 'none'; | |
return; | |
} | |
welcomeScreen.style.display = 'none'; | |
messagesArea.style.display = 'block'; | |
let html = ''; | |
messages.forEach((message, index) => { | |
const isUser = message.role === 'user'; | |
const avatar = isUser | |
? '<div class="w-6 h-6 bg-blue-600 rounded-full"></div>' | |
: '<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'; | |
let content = message.content; | |
let attribution = ''; | |
// Handle empty assistant messages (streaming in progress) | |
if (message.role === 'assistant' && content === '' && isLoading) { | |
content = ''; | |
} else if (message.role === 'assistant' && content.includes('---\n*Response created by:')) { | |
const parts = content.split('\n\n---\n*Response created by:'); | |
content = parts[0]; | |
if (parts[1]) { | |
const modelName = parts[1].replace(/\*\*/g, '').replace(/\*/g, ''); | |
attribution = '<div class="text-xs text-gray-500 mt-2 italic">Response created by: <strong>' + modelName + '</strong></div>'; | |
} | |
} | |
html += ` | |
<div class="flex gap-4"> | |
<div class="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center flex-shrink-0"> | |
${avatar} | |
</div> | |
<div class="flex-1 min-w-0"> | |
<div class="whitespace-pre-wrap text-gray-100">${content}</div> | |
${attribution} | |
</div> | |
</div> | |
`; | |
}); | |
// Show loading dots if streaming | |
if (isLoading && messages.length > 0 && messages[messages.length - 1].role === 'assistant' && messages[messages.length - 1].content === '') { | |
// Replace the last empty message with loading dots | |
html = html.replace(/<div class="whitespace-pre-wrap text-gray-100"><\/div>/, ` | |
<div class="flex items-center gap-2 text-gray-400"> | |
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce loading-dot-1"></div> | |
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce loading-dot-2"></div> | |
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce loading-dot-3"></div> | |
</div> | |
`); | |
} | |
messagesArea.innerHTML = html; | |
messagesArea.scrollTop = messagesArea.scrollHeight; | |
} | |
function startNewChat() { | |
if (messages.length > 0) { | |
const title = generateChatTitle(messages); | |
const newSession = { | |
id: 'session-' + Date.now(), | |
title: title, | |
messages: messages.slice(), | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString() | |
}; | |
sessions.unshift(newSession); | |
updateSessionsList(); | |
} | |
messages = []; | |
currentSessionId = 'session-' + Date.now(); | |
updateMessagesDisplay(); | |
} | |
function 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; | |
} | |
function updateSessionsList() { | |
const sessionsList = document.getElementById('sessions-list'); | |
if (sessions.length === 0) { | |
sessionsList.innerHTML = '<p class="text-gray-500 text-sm">No previous chats yet</p>'; | |
return; | |
} | |
let html = ''; | |
sessions.forEach(session => { | |
const isCurrent = session.id === currentSessionId; | |
html += ` | |
<div class="group flex items-center gap-2"> | |
<button onclick="loadSession('${session.id}')" class="flex-1 text-left px-3 py-2 rounded-lg transition-colors ${isCurrent ? 'bg-blue-600 text-white' : 'bg-gray-700 hover:bg-gray-600 text-gray-300'}"> | |
<div class="text-sm font-medium truncate">${session.title}</div> | |
<div class="text-xs text-gray-400">${new Date(session.updatedAt).toLocaleDateString()}</div> | |
</button> | |
<button onclick="deleteSession('${session.id}')" class="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"> | |
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg> | |
</button> | |
</div> | |
`; | |
}); | |
sessionsList.innerHTML = html; | |
} | |
function loadSession(sessionId) { | |
const session = sessions.find(s => s.id === sessionId); | |
if (!session) return; | |
messages = session.messages.slice(); | |
currentSessionId = sessionId; | |
updateMessagesDisplay(); | |
updateSessionsList(); | |
} | |
function deleteSession(sessionId) { | |
sessions = sessions.filter(s => s.id !== sessionId); | |
if (sessionId === currentSessionId) { | |
startNewChat(); | |
} | |
updateSessionsList(); | |
} | |
function clearChat() { | |
messages = []; | |
updateMessagesDisplay(); | |
} | |
function 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); | |
} | |
// Online users tracking with real geolocation | |
let onlineUsers = new Map(); // Map to store user details | |
let lastActivity = Date.now(); | |
let userLocation = { city: 'Unknown', country: 'Unknown' }; | |
let isGettingLocation = false; | |
// Get user's real location | |
async function getUserLocation() { | |
if (isGettingLocation) return userLocation; | |
isGettingLocation = true; | |
try { | |
// Try geolocation API first | |
if (navigator.geolocation) { | |
const position = await new Promise((resolve, reject) => { | |
navigator.geolocation.getCurrentPosition(resolve, reject, { | |
timeout: 10000, | |
enableHighAccuracy: false | |
}); | |
}); | |
const { latitude, longitude } = position.coords; | |
// Use reverse geocoding to get city and country | |
try { | |
const response = await fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}&localityLanguage=en`); | |
const data = await response.json(); | |
userLocation = { | |
city: data.city || data.locality || 'Unknown', | |
country: data.countryName || 'Unknown' | |
}; | |
} catch (geocodeError) { | |
console.log('Geocoding failed, using IP fallback'); | |
await getLocationByIP(); | |
} | |
} else { | |
await getLocationByIP(); | |
} | |
} catch (error) { | |
console.log('Geolocation failed, using IP fallback'); | |
await getLocationByIP(); | |
} | |
return userLocation; | |
} | |
// Fallback: Get location by IP | |
async function getLocationByIP() { | |
try { | |
const response = await fetch('https://ipapi.co/json/'); | |
const data = await response.json(); | |
userLocation = { | |
city: data.city || 'Unknown', | |
country: data.country_name || 'Unknown' | |
}; | |
} catch (error) { | |
console.log('IP location failed'); | |
userLocation = { city: 'Unknown', country: 'Unknown' }; | |
} | |
} | |
async function updateOnlineUsers() { | |
// Ensure we have user location | |
if (userLocation.city === 'Unknown') { | |
await getUserLocation(); | |
} | |
// Add current user with location | |
const currentUserData = { | |
id: userId, | |
location: userLocation, | |
lastSeen: Date.now(), | |
isCurrentUser: true | |
}; | |
onlineUsers.set(userId, currentUserData); | |
// Simulate other realistic users based on activity | |
const now = Date.now(); | |
const timeSinceLastActivity = now - lastActivity; | |
// Add new users when there's recent activity | |
if (timeSinceLastActivity < 10000 && Math.random() < 0.2) { | |
const newUserId = 'User-' + Math.random().toString(36).substr(2, 8); | |
const locations = [ | |
{ city: 'New York', country: 'United States' }, | |
{ city: 'London', country: 'United Kingdom' }, | |
{ city: 'Tokyo', country: 'Japan' }, | |
{ city: 'Paris', country: 'France' }, | |
{ city: 'Sydney', country: 'Australia' }, | |
{ city: 'Toronto', country: 'Canada' }, | |
{ city: 'Berlin', country: 'Germany' }, | |
{ city: 'Mumbai', country: 'India' }, | |
{ city: 'SΓ£o Paulo', country: 'Brazil' }, | |
{ city: 'Singapore', country: 'Singapore' } | |
]; | |
const randomLocation = locations[Math.floor(Math.random() * locations.length)]; | |
onlineUsers.set(newUserId, { | |
id: newUserId, | |
location: randomLocation, | |
lastSeen: now, | |
isCurrentUser: false | |
}); | |
} | |
// Remove users who have been inactive for more than 2 minutes | |
const cutoffTime = now - (2 * 60 * 1000); | |
for (const [id, userData] of onlineUsers.entries()) { | |
if (!userData.isCurrentUser && userData.lastSeen < cutoffTime) { | |
onlineUsers.delete(id); | |
} | |
} | |
// Occasionally remove a random user to simulate leaving | |
if (onlineUsers.size > 3 && Math.random() < 0.1) { | |
const nonCurrentUsers = Array.from(onlineUsers.entries()).filter(([id, data]) => !data.isCurrentUser); | |
if (nonCurrentUsers.length > 0) { | |
const randomUser = nonCurrentUsers[Math.floor(Math.random() * nonCurrentUsers.length)]; | |
onlineUsers.delete(randomUser[0]); | |
} | |
} | |
updateUsersDisplay(); | |
} | |
function updateUsersDisplay() { | |
const count = onlineUsers.size; | |
const status = count === 1 ? 'Just you online' : count + ' people online'; | |
document.getElementById('online-status').textContent = status; | |
// Update users list | |
const usersList = document.getElementById('users-list'); | |
const currentUserInfo = document.getElementById('current-user-info'); | |
// Update current user info | |
currentUserInfo.textContent = `You: ${userId} (${userLocation.city}, ${userLocation.country})`; | |
let html = `<div class="text-green-400">π’ You: ${userId}<br><span class="text-gray-400 ml-4">${userLocation.city}, ${userLocation.country}</span></div>`; | |
// Add other users | |
const otherUsers = Array.from(onlineUsers.values()).filter(userData => !userData.isCurrentUser); | |
otherUsers.forEach(userData => { | |
const timeAgo = Math.floor((Date.now() - userData.lastSeen) / 1000); | |
const timeText = timeAgo < 60 ? 'now' : `${Math.floor(timeAgo / 60)}m ago`; | |
html += ` | |
<div class="text-blue-400"> | |
π΅ ${userData.id}<br> | |
<span class="text-gray-400 ml-4">${userData.location.city}, ${userData.location.country}</span><br> | |
<span class="text-gray-500 ml-4 text-xs">Active ${timeText}</span> | |
</div> | |
`; | |
}); | |
usersList.innerHTML = html; | |
} | |
function refreshUsers() { | |
trackActivity(); | |
updateOnlineUsers(); | |
} | |
function trackActivity() { | |
lastActivity = Date.now(); | |
// Update current user's last seen time | |
if (onlineUsers.has(userId)) { | |
const userData = onlineUsers.get(userId); | |
userData.lastSeen = Date.now(); | |
onlineUsers.set(userId, userData); | |
} | |
updateOnlineUsers(); | |
} | |
async function checkAPIStatus() { | |
try { | |
if (!OPENROUTER_API_KEY || OPENROUTER_API_KEY === "YOUR_API_KEY_HERE") { | |
document.getElementById('api-status').textContent = 'π΄ No API Key'; | |
document.getElementById('api-dot').className = 'w-2 h-2 rounded-full bg-red-500'; | |
return; | |
} | |
const response = await fetch("https://openrouter.ai/api/v1/models", { | |
headers: { "Authorization": "Bearer " + OPENROUTER_API_KEY } | |
}); | |
if (response.ok) { | |
document.getElementById('api-status').textContent = 'π’ API Connected'; | |
document.getElementById('api-dot').className = 'w-2 h-2 rounded-full bg-green-500'; | |
} else { | |
document.getElementById('api-status').textContent = 'π΄ Connection Issue'; | |
document.getElementById('api-dot').className = 'w-2 h-2 rounded-full bg-red-500'; | |
} | |
} catch (error) { | |
document.getElementById('api-status').textContent = 'π΄ Connection Issue'; | |
document.getElementById('api-dot').className = 'w-2 h-2 rounded-full bg-red-500'; | |
} | |
} | |
</script> | |
</body> | |
</html> |