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); } | |
| } | |
| .sidebar-transition { transition: transform 0.3s ease-in-out; } | |
| .sidebar-hidden { transform: translateX(-100%); } | |
| .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 relative"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="w-80 bg-gray-800 border-r border-gray-700 flex flex-col sidebar-transition z-10"> | |
| <!-- Sidebar Toggle Button --> | |
| <button id="sidebarToggle" class="absolute -right-8 top-4 bg-black text-white p-2 rounded-r-lg hover:bg-gray-800 transition-colors z-20"> | |
| <svg id="toggleIcon" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> | |
| <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> | |
| </svg> | |
| </button> | |
| <!-- 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 id="newChatBtn" class="w-full flex items-center gap-2 px-4 py-2 bg-black text-white hover:bg-gray-800 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="chatHistoryList" class="space-y-2"> | |
| <p class="text-gray-500 text-sm">No saved chats yet</p> | |
| </div> | |
| <button id="saveCurrentChatBtn" class="w-full mt-3 px-3 py-2 bg-black text-white hover:bg-gray-800 rounded-lg transition-colors text-sm"> | |
| 💾 Save Current Chat | |
| </button> | |
| </div> | |
| <!-- Settings --> | |
| <div class="p-4 border-t border-gray-700 space-y-4"> | |
| <!-- Location Info --> | |
| <div> | |
| <h3 class="text-sm font-medium text-gray-400 mb-2">📍 Your Location</h3> | |
| <div class="bg-gray-700 rounded-lg p-2 text-xs"> | |
| <div id="locationInfo" class="text-green-400">🌍 Getting location...</div> | |
| </div> | |
| </div> | |
| <!-- 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="onlineStatus">Just you online</span> | |
| </div> | |
| <div class="text-xs text-gray-500" id="currentUser">You: Loading...</div> | |
| </div> | |
| <!-- Model Selection --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-400 mb-2">AI Model</label> | |
| <select id="modelSelect" 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="modelId">openai/gpt-3.5-turbo</p> | |
| </div> | |
| <!-- API Status --> | |
| <div class="flex items-center gap-2 text-sm"> | |
| <div id="apiDot" class="w-2 h-2 rounded-full bg-green-500"></div> | |
| <span id="apiStatus">🟢 API Connected</span> | |
| </div> | |
| <!-- Controls --> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="downloadBtn" class="flex items-center justify-center px-3 py-2 bg-black text-white hover:bg-gray-800 rounded-lg transition-colors text-sm" title="Download History"> | |
| <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 24 24"> | |
| <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/> | |
| </svg> | |
| Download | |
| </button> | |
| <button id="clearBtn" class="flex items-center justify-center px-3 py-2 bg-black text-white hover:bg-gray-800 rounded-lg transition-colors text-sm" title="Clear Chat"> | |
| <svg class="w-4 h-4 mr-1" 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> | |
| Clear | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Chat Area --> | |
| <div id="mainArea" class="flex-1 flex flex-col transition-all duration-300"> | |
| <!-- Chat Messages --> | |
| <div id="messagesContainer" class="flex-1 overflow-y-auto chat-scroll"> | |
| <!-- Welcome Screen --> | |
| <div id="welcomeScreen" 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="messagesArea" 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"> | |
| <input | |
| type="text" | |
| id="messageInput" | |
| placeholder="Chat Smarter. Chat many Brains" | |
| class="flex-1 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" | |
| /> | |
| <button | |
| id="sendBtn" | |
| 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="currentModelName">GPT-3.5 Turbo</strong></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // 🔑 OpenRouter API Key | |
| const API_KEY = "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"; | |
| // Global variables | |
| let messages = []; | |
| let savedChats = JSON.parse(localStorage.getItem('chatHistory') || '[]'); | |
| let selectedModel = 'openai/gpt-3.5-turbo'; | |
| let isLoading = false; | |
| let userId = 'User-' + Math.random().toString(36).substr(2, 8); | |
| let userLocation = { city: 'Unknown', country: 'Unknown' }; | |
| let sidebarVisible = true; | |
| 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 app | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| console.log('App starting...'); | |
| setupEventListeners(); | |
| await getUserLocation(); | |
| updateUserDisplay(); | |
| updateChatHistory(); | |
| console.log('App ready!'); | |
| }); | |
| function setupEventListeners() { | |
| // Sidebar toggle | |
| document.getElementById('sidebarToggle').onclick = toggleSidebar; | |
| // Send button | |
| document.getElementById('sendBtn').onclick = function() { | |
| console.log('Send button clicked'); | |
| sendMessage(); | |
| }; | |
| // Enter key | |
| document.getElementById('messageInput').onkeypress = function(e) { | |
| if (e.key === 'Enter') { | |
| console.log('Enter pressed'); | |
| sendMessage(); | |
| } | |
| }; | |
| // Model selection | |
| document.getElementById('modelSelect').onchange = function(e) { | |
| selectedModel = e.target.value; | |
| document.getElementById('modelId').textContent = selectedModel; | |
| document.getElementById('currentModelName').textContent = models[selectedModel]; | |
| console.log('Model changed to:', selectedModel); | |
| }; | |
| // New chat | |
| document.getElementById('newChatBtn').onclick = function() { | |
| messages = []; | |
| updateDisplay(); | |
| console.log('New chat started'); | |
| }; | |
| // Save current chat | |
| document.getElementById('saveCurrentChatBtn').onclick = function() { | |
| saveCurrentChat(); | |
| }; | |
| // Clear chat | |
| document.getElementById('clearBtn').onclick = function() { | |
| messages = []; | |
| updateDisplay(); | |
| console.log('Chat cleared'); | |
| }; | |
| // Download chat | |
| document.getElementById('downloadBtn').onclick = function() { | |
| downloadCurrentChat(); | |
| }; | |
| console.log('Event listeners setup complete'); | |
| } | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const mainArea = document.getElementById('mainArea'); | |
| const toggleIcon = document.getElementById('toggleIcon'); | |
| sidebarVisible = !sidebarVisible; | |
| if (sidebarVisible) { | |
| sidebar.classList.remove('sidebar-hidden'); | |
| toggleIcon.innerHTML = '<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>'; | |
| } else { | |
| sidebar.classList.add('sidebar-hidden'); | |
| toggleIcon.innerHTML = '<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/>'; | |
| } | |
| } | |
| // Get user's real location (like your Streamlit code) | |
| async function getUserLocation() { | |
| try { | |
| 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 | |
| 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) { | |
| await getLocationByIP(); | |
| } | |
| } else { | |
| await getLocationByIP(); | |
| } | |
| } catch (error) { | |
| await getLocationByIP(); | |
| } | |
| updateLocationDisplay(); | |
| } | |
| 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) { | |
| userLocation = { city: 'Unknown', country: 'Unknown' }; | |
| } | |
| } | |
| function updateLocationDisplay() { | |
| document.getElementById('locationInfo').textContent = `📍 ${userLocation.city}, ${userLocation.country}`; | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| const message = input.value.trim(); | |
| console.log('Sending message:', message); | |
| if (!message || isLoading) { | |
| console.log('No message or loading'); | |
| return; | |
| } | |
| // Add user message | |
| messages.push({ | |
| role: 'user', | |
| content: message | |
| }); | |
| input.value = ''; | |
| updateDisplay(); | |
| setLoading(true); | |
| try { | |
| // Start streaming response - like your Streamlit code | |
| await streamAIResponse(message); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| messages.push({ | |
| role: 'assistant', | |
| content: 'Sorry, there was an error. Please try again.' | |
| }); | |
| updateDisplay(); | |
| } | |
| setLoading(false); | |
| } | |
| // Streaming implementation like your Streamlit code | |
| async function streamAIResponse(userMessage) { | |
| console.log('Starting stream...'); | |
| // Add empty assistant message for streaming | |
| messages.push({ | |
| role: 'assistant', | |
| content: '' | |
| }); | |
| updateDisplay(); | |
| const apiMessages = [{ | |
| role: "system", | |
| content: "You are a helpful AI assistant. Provide clear and helpful responses." | |
| }]; | |
| // Build conversation history (like your Streamlit extend) | |
| messages.slice(0, -1).forEach(msg => { | |
| if (msg.role !== 'assistant' || !msg.content.includes('Response created by:')) { | |
| apiMessages.push({ | |
| role: msg.role, | |
| content: msg.content.split('\n\n---\n*Response created by:')[0] | |
| }); | |
| } | |
| }); | |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { | |
| method: 'POST', | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": "Bearer " + API_KEY, | |
| "HTTP-Referer": "https://huggingface.co/spaces", | |
| "X-Title": "Chat Flow AI Assistant" | |
| }, | |
| body: JSON.stringify({ | |
| model: selectedModel, | |
| messages: apiMessages, | |
| stream: true, // Enable streaming like Streamlit | |
| max_tokens: 2000, | |
| temperature: 0.7 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('API Error: ' + response.status); | |
| } | |
| 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, { stream: true }); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.trim() === '') continue; | |
| if (line.startsWith('data: ')) { | |
| const data = line.slice(6).trim(); | |
| if (data === '[DONE]') { | |
| // Finalize response with attribution (like Streamlit) | |
| const modelName = models[selectedModel] || "AI"; | |
| const finalResponse = fullResponse + "\n\n---\n*Response created by: **" + modelName + "***"; | |
| messages[messages.length - 1].content = finalResponse; | |
| updateDisplay(); | |
| return; | |
| } | |
| if (data === '') continue; | |
| try { | |
| const parsed = JSON.parse(data); | |
| const delta = parsed.choices?.[0]?.delta?.content; | |
| if (delta) { | |
| fullResponse += delta; | |
| // Update display in real-time (like Streamlit placeholder.markdown) | |
| updateStreamingMessage(fullResponse); | |
| } | |
| } catch (e) { | |
| continue; | |
| } | |
| } | |
| } | |
| } | |
| } finally { | |
| reader.releaseLock(); | |
| } | |
| } | |
| function updateStreamingMessage(partialResponse) { | |
| // Update the last message with streaming content + cursor | |
| if (messages.length > 0) { | |
| const lastMessage = messages[messages.length - 1]; | |
| if (lastMessage.role === 'assistant') { | |
| lastMessage.content = partialResponse; | |
| // Update display with cursor (like Streamlit "▌") | |
| const messagesArea = document.getElementById('messagesArea'); | |
| const messageElements = messagesArea.children; | |
| if (messageElements.length > 0) { | |
| const lastMessageElement = messageElements[messageElements.length - 1]; | |
| const contentDiv = lastMessageElement.querySelector('.whitespace-pre-wrap'); | |
| if (contentDiv) { | |
| contentDiv.textContent = partialResponse + " ▌"; | |
| } | |
| } | |
| messagesArea.scrollTop = messagesArea.scrollHeight; | |
| } | |
| } | |
| } | |
| function updateDisplay() { | |
| const welcome = document.getElementById('welcomeScreen'); | |
| const messagesArea = document.getElementById('messagesArea'); | |
| if (messages.length === 0) { | |
| welcome.style.display = 'flex'; | |
| messagesArea.style.display = 'none'; | |
| return; | |
| } | |
| welcome.style.display = 'none'; | |
| messagesArea.style.display = 'block'; | |
| let html = ''; | |
| messages.forEach(msg => { | |
| const isUser = msg.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 = msg.content; | |
| let attribution = ''; | |
| if (msg.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> | |
| `; | |
| }); | |
| messagesArea.innerHTML = html; | |
| messagesArea.scrollTop = messagesArea.scrollHeight; | |
| } | |
| function setLoading(loading) { | |
| isLoading = loading; | |
| const btn = document.getElementById('sendBtn'); | |
| const input = document.getElementById('messageInput'); | |
| btn.disabled = loading; | |
| input.disabled = loading; | |
| } | |
| function updateUserDisplay() { | |
| document.getElementById('currentUser').textContent = 'You: ' + userId; | |
| document.getElementById('onlineStatus').textContent = 'Just you online'; | |
| } | |
| // Chat save functionality (like your Streamlit save_chat_history) | |
| function saveCurrentChat() { | |
| if (messages.length === 0) { | |
| alert('No messages to save!'); | |
| return; | |
| } | |
| const title = generateChatTitle(); | |
| const chatData = { | |
| id: Date.now(), | |
| title: title, | |
| messages: JSON.parse(JSON.stringify(messages)), | |
| createdAt: new Date().toISOString(), | |
| location: userLocation | |
| }; | |
| savedChats.unshift(chatData); | |
| localStorage.setItem('chatHistory', JSON.stringify(savedChats)); | |
| updateChatHistory(); | |
| alert('Chat saved successfully!'); | |
| } | |
| function generateChatTitle() { | |
| const firstUserMessage = messages.find(m => m.role === 'user'); | |
| if (!firstUserMessage) return 'Empty Chat'; | |
| const content = firstUserMessage.content; | |
| return content.length > 30 ? content.substring(0, 30) + '...' : content; | |
| } | |
| function updateChatHistory() { | |
| const historyList = document.getElementById('chatHistoryList'); | |
| if (savedChats.length === 0) { | |
| historyList.innerHTML = '<p class="text-gray-500 text-sm">No saved chats yet</p>'; | |
| return; | |
| } | |
| let html = ''; | |
| savedChats.slice(0, 10).forEach(chat => { // Show last 10 chats | |
| const date = new Date(chat.createdAt).toLocaleDateString(); | |
| html += ` | |
| <div class="group flex items-center gap-2"> | |
| <button onclick="loadChat(${chat.id})" class="flex-1 text-left px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-gray-300 transition-colors"> | |
| <div class="text-sm font-medium truncate">${chat.title}</div> | |
| <div class="text-xs text-gray-400">${date}</div> | |
| </button> | |
| <button onclick="deleteChat(${chat.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> | |
| `; | |
| }); | |
| historyList.innerHTML = html; | |
| } | |
| function loadChat(chatId) { | |
| const chat = savedChats.find(c => c.id === chatId); | |
| if (chat) { | |
| messages = JSON.parse(JSON.stringify(chat.messages)); | |
| updateDisplay(); | |
| console.log('Chat loaded:', chat.title); | |
| } | |
| } | |
| function deleteChat(chatId) { | |
| if (confirm('Delete this chat?')) { | |
| savedChats = savedChats.filter(c => c.id !== chatId); | |
| localStorage.setItem('chatHistory', JSON.stringify(savedChats)); | |
| updateChatHistory(); | |
| } | |
| } | |
| function downloadCurrentChat() { | |
| if (messages.length === 0) { | |
| alert('No messages to download!'); | |
| return; | |
| } | |
| const chatData = { | |
| title: generateChatTitle(), | |
| messages: messages, | |
| exportedAt: new Date().toISOString(), | |
| location: userLocation, | |
| userId: userId | |
| }; | |
| const dataStr = JSON.stringify(chatData, 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_${new Date().toISOString().split('T')[0]}.json`; | |
| link.click(); | |
| URL.revokeObjectURL(url); |