Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Chat + Speech-to-Text</title> | |
| <!-- Import FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* ============================== | |
| CSS VARIABLES & RESET | |
| ============================== */ | |
| :root { | |
| --bg-color: #f7f7f8; | |
| --sidebar-bg: #202123; | |
| --sidebar-text: #ececf1; | |
| --main-bg: #ffffff; | |
| --border-color: #e5e7eb; | |
| --primary-color: #10a37f; | |
| --primary-hover: #0d8a6c; | |
| --text-primary: #343541; | |
| --text-secondary: #6e6e80; | |
| --user-msg-bg: #ffffff; | |
| --ai-msg-bg: #f7f7f8; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| outline: none; | |
| } | |
| html, body { | |
| height: 100%; | |
| margin: 0; | |
| padding: 0; | |
| font-family: var(--font-family); | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| } | |
| /* ============================== | |
| LAYOUT | |
| ============================== */ | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| width: 100vw; | |
| overflow: hidden; | |
| } | |
| /* ============================== | |
| SIDEBAR | |
| ============================== */ | |
| .sidebar { | |
| width: 260px; | |
| background-color: var(--sidebar-bg); | |
| color: var(--sidebar-text); | |
| display: flex; | |
| flex-direction: column; | |
| padding: 10px; | |
| transition: transform 0.3s ease; | |
| z-index: 100; | |
| } | |
| .sidebar-header { | |
| padding: 10px; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| margin-bottom: 10px; | |
| } | |
| .login-btn { | |
| background: rgba(255,255,255,0.1); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| width: 100%; | |
| margin-bottom: 20px; | |
| transition: background 0.2s; | |
| } | |
| .login-btn:hover { | |
| background: rgba(255,255,255,0.2); | |
| } | |
| .settings-group { | |
| padding: 0 5px; | |
| margin-bottom: 15px; | |
| } | |
| .settings-group label { | |
| display: block; | |
| font-size: 0.75rem; | |
| margin-bottom: 5px; | |
| color: #8e8ea0; | |
| } | |
| .settings-input, .settings-textarea { | |
| width: 100%; | |
| background: rgba(0,0,0,0.2); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| color: white; | |
| padding: 8px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| resize: none; | |
| } | |
| .settings-slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .settings-slider { | |
| flex: 1; | |
| accent-color: var(--primary-color); | |
| } | |
| .slider-value { | |
| font-size: 0.75rem; | |
| width: 30px; | |
| text-align: right; | |
| } | |
| /* ============================== | |
| MAIN CONTENT | |
| ============================== */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background-color: var(--main-bg); | |
| position: relative; | |
| } | |
| /* Header */ | |
| .header { | |
| padding: 10px 20px; | |
| text-align: center; | |
| font-weight: 600; | |
| color: #202123; | |
| background: #ffffff; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .header-title { | |
| font-size: 1rem; | |
| } | |
| .brand-link { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .brand-link:hover { | |
| color: var(--primary-color); | |
| } | |
| /* Mobile Toggle */ | |
| .menu-toggle { | |
| display: none; | |
| background: none; | |
| border: none; | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| color: #555; | |
| margin-right: 10px; | |
| } | |
| /* Chat Area */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| scroll-behavior: smooth; | |
| } | |
| .message { | |
| display: flex; | |
| gap: 15px; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| width: 100%; | |
| line-height: 1.6; | |
| } | |
| .message.user { | |
| justify-content: flex-end; | |
| } | |
| .avatar { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| font-size: 0.8rem; | |
| } | |
| .avatar.ai { | |
| background: var(--primary-color); | |
| color: white; | |
| } | |
| .avatar.user { | |
| background: #555; | |
| color: white; | |
| order: 2; /* Avatar on right for user */ | |
| } | |
| .message-content { | |
| padding: 0 10px; | |
| font-size: 1rem; | |
| color: var(--text-primary); | |
| white-space: pre-wrap; | |
| } | |
| .message.user .message-content { | |
| background: #f0f0f0; | |
| padding: 10px 15px; | |
| border-radius: 12px; | |
| border-top-right-radius: 2px; | |
| } | |
| .message.ai .message-content { | |
| padding-top: 5px; | |
| } | |
| /* Typing indicator */ | |
| .typing-indicator span { | |
| display: inline-block; | |
| width: 6px; | |
| height: 6px; | |
| background-color: #ccc; | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite ease-in-out both; | |
| margin-right: 3px; | |
| } | |
| .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typing { | |
| 0%, 80%, 100% { transform: scale(0); } | |
| 40% { transform: scale(1); } | |
| } | |
| /* Input Area */ | |
| .input-bar { | |
| padding: 24px; | |
| background: linear-gradient(180deg, rgba(255,255,255,0) 0%, #ffffff 20%); | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .input-wrapper { | |
| width: 100%; | |
| max-width: 800px; | |
| position: relative; | |
| display: flex; | |
| align-items: flex-end; | |
| background: white; | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| box-shadow: var(--shadow-sm); | |
| padding: 10px; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: rgba(0,0,0,0.2); | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.05); | |
| } | |
| textarea { | |
| flex: 1; | |
| border: none; | |
| resize: none; | |
| max-height: 200px; | |
| min-height: 24px; | |
| padding: 0 10px; | |
| font-family: inherit; | |
| font-size: 1rem; | |
| line-height: 1.5; | |
| background: transparent; | |
| } | |
| .icon-btn { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: #8e8ea0; | |
| padding: 6px; | |
| border-radius: 6px; | |
| transition: background 0.2s, color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .icon-btn:hover { | |
| background: #f0f0f0; | |
| color: #333; | |
| } | |
| .icon-btn.recording { | |
| color: #ef4444; | |
| background: #fee2e2; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } | |
| 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } | |
| } | |
| .send-btn { | |
| background: var(--primary-color); | |
| color: white; | |
| border-radius: 8px; | |
| padding: 6px 12px; | |
| } | |
| .send-btn:hover { | |
| background: var(--primary-hover); | |
| } | |
| .send-btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #d1d5db; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #9ca3af; | |
| } | |
| /* ============================== | |
| RESPONSIVE DESIGN | |
| ============================== */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: absolute; | |
| height: 100%; | |
| transform: translateX(-100%); | |
| box-shadow: 2px 0 10px rgba(0,0,0,0.2); | |
| } | |
| .sidebar.active { | |
| transform: translateX(0); | |
| } | |
| .menu-toggle { | |
| display: block; | |
| } | |
| .input-bar { | |
| padding: 10px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- SIDEBAR --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <i class="fa-solid fa-sliders"></i> Settings | |
| </div> | |
| <button class="login-btn"> | |
| <i class="fa-brands fa-huggingface"></i> Login to HuggingFace | |
| </button> | |
| <div class="settings-group"> | |
| <label>System Prompt</label> | |
| <textarea class="settings-textarea" id="system-prompt" rows="4">You are a helpful assistant.</textarea> | |
| </div> | |
| <div class="settings-group"> | |
| <label>Max Tokens</label> | |
| <div class="settings-slider-container"> | |
| <input type="range" class="settings-slider" id="max-tokens" min="1" max="2048" value="2012"> | |
| <span class="slider-value" id="max-tokens-val">2012</span> | |
| </div> | |
| </div> | |
| <div class="settings-group"> | |
| <label>Temperature</label> | |
| <div class="settings-slider-container"> | |
| <input type="range" class="settings-slider" id="temperature" min="0.1" max="4.0" step="0.1" value="0.7"> | |
| <span class="slider-value" id="temperature-val">0.7</span> | |
| </div> | |
| </div> | |
| <div class="settings-group"> | |
| <label>Top P</label> | |
| <div class="settings-slider-container"> | |
| <input type="range" class="settings-slider" id="top-p" min="0.1" max="1.0" step="0.05" value="0.95"> | |
| <span class="slider-value" id="top-p-val">0.95</span> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- MAIN CONTENT --> | |
| <main class="main-content"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div style="display: flex; align-items: center;"> | |
| <button class="menu-toggle" id="menu-toggle"> | |
| <i class="fa-solid fa-bars"></i> | |
| </button> | |
| <div class="header-title">AI Chat + Speech-to-Text</div> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square" style="font-size: 0.7em;"></i> | |
| </a> | |
| </header> | |
| <!-- Chat Area --> | |
| <div class="chat-container" id="chat-container"> | |
| <!-- Messages will be injected here via JS --> | |
| <div class="message ai"> | |
| <div class="avatar ai"><i class="fa-solid fa-robot"></i></div> | |
| <div class="message-content">Hello! I am ready to chat. You can type or use the microphone to speak.</div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="input-bar"> | |
| <div class="input-wrapper"> | |
| <textarea id="message-input" placeholder="Message..." rows="1"></textarea> | |
| <button class="icon-btn" id="mic-btn" title="Speak"> | |
| <i class="fa-solid fa-microphone"></i> | |
| </button> | |
| <button class="icon-btn send-btn" id="send-btn" title="Send"> | |
| <i class="fa-solid fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // ============================== | |
| // STATE & DOM ELEMENTS | |
| // ============================== | |
| const state = { | |
| history: [], | |
| isRecording: false | |
| }; | |
| const elements = { | |
| chatContainer: document.getElementById('chat-container'), | |
| messageInput: document.getElementById('message-input'), | |
| sendBtn: document.getElementById('send-btn'), | |
| micBtn: document.getElementById('mic-btn'), | |
| menuToggle: document.getElementById('menu-toggle'), | |
| sidebar: document.getElementById('sidebar'), | |
| systemPrompt: document.getElementById('system-prompt'), | |
| sliders: { | |
| maxTokens: document.getElementById('max-tokens'), | |
| temperature: document.getElementById('temperature'), | |
| topP: document.getElementById('top-p') | |
| }, | |
| sliderVals: { | |
| maxTokens: document.getElementById('max-tokens-val'), | |
| temperature: document.getElementById('temperature-val'), | |
| topP: document.getElementById('top-p-val') | |
| } | |
| }; | |
| // ============================== | |
| // EVENT LISTENERS | |
| // ============================== | |
| // Auto-resize textarea | |
| elements.messageInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| if(this.value === '') this.style.height = 'auto'; | |
| }); | |
| // Send on Enter (Shift+Enter for new line) | |
| elements.messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSend(); | |
| } | |
| }); | |
| // Send Button Click | |
| elements.sendBtn.addEventListener('click', handleSend); | |
| // Mobile Menu Toggle | |
| elements.menuToggle.addEventListener('click', () => { | |
| elements.sidebar.classList.toggle('active'); | |
| }); | |
| // Close sidebar when clicking outside on mobile | |
| document.addEventListener('click', (e) => { | |
| if (window.innerWidth <= 768) { | |
| if (!elements.sidebar.contains(e.target) && !elements.menuToggle.contains(e.target)) { | |
| elements.sidebar.classList.remove('active'); | |
| } | |
| } | |
| }); | |
| // Update Slider Values Display | |
| Object.keys(elements.sliders).forEach(key => { | |
| elements.sliders[key].addEventListener('input', (e) => { | |
| elements.sliderVals[key].textContent = e.target.value; | |
| }); | |
| }); | |
| // ============================== | |
| // CHAT LOGIC | |
| // ============================== | |
| function handleSend() { | |
| const text = elements.messageInput.value.trim(); | |
| if (!text) return; | |
| // Add User Message | |
| addMessageToUI('user', text); | |
| state.history.push({ role: 'user', content: text }); | |
| // Clear Input | |
| elements.messageInput.value = ''; | |
| elements.messageInput.style.height = 'auto'; | |
| // Simulate AI Response (Streaming) | |
| simulateAIResponse(text); | |
| } | |
| function addMessageToUI(role, content, isTyping = false) { | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = `message ${role}`; | |
| const avatarDiv = document.createElement('div'); | |
| avatarDiv.className = `avatar ${role}`; | |
| if (role === 'ai') { | |
| avatarDiv.innerHTML = '<i class="fa-solid fa-robot"></i>'; | |
| } else { | |
| avatarDiv.innerHTML = '<i class="fa-solid fa-user"></i>'; | |
| } | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| if (isTyping) { | |
| contentDiv.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>'; | |
| } else { | |
| contentDiv.textContent = content; | |
| } | |
| msgDiv.appendChild(avatarDiv); | |
| msgDiv.appendChild(contentDiv); | |
| elements.chatContainer.appendChild(msgDiv); | |
| scrollToBottom(); | |
| return contentDiv; // Return content div for streaming updates | |
| } | |
| function scrollToBottom() { | |
| elements.chatContainer.scrollTop = elements.chatContainer.scrollHeight; | |
| } | |
| // ============================== | |
| // SIMULATED AI STREAMING | |
| // ============================== | |
| function simulateAIResponse(userMessage) { | |
| // 1. Add placeholder with typing indicator | |
| const contentDiv = addMessageToUI('ai', '', true); | |
| // 2. Simulate network delay | |
| setTimeout(() => { | |
| // Remove typing indicator | |
| contentDiv.innerHTML = ''; | |
| // Generate a dummy response based on settings | |
| const temp = parseFloat(elements.sliders.temperature.value); | |
| const sysPrompt = elements.systemPrompt.value; | |
| let responseText = ""; | |
| // Simple mock logic to make the tool feel alive | |
| if (userMessage.toLowerCase().includes('hello')) { | |
| responseText = "Hello there! I'm running in the browser using vanilla JS. How can I help you today?"; | |
| } else if (userMessage.toLowerCase().includes('code')) { | |
| responseText = "Sure, here is a Python example:\n\n```python\ndef greet():\n print('Hello World')\n```"; | |
| } else { | |
| responseText = `(Simulated Response based on Temp: ${temp})\nI received your message: "${userMessage}".\n\nSystem Prompt is currently set to: "${sysPrompt}".\n\nSince this is a client-side demo, I'm simulating the streaming text effect that the HuggingFace InferenceClient would provide.`; | |
| } | |
| // 3. Stream the text character by character | |
| let index = 0; | |
| const speed = 20; // ms per char | |
| function typeChar() { | |
| if (index < responseText.length) { | |
| // Handle simple newlines | |
| const char = responseText.charAt(index); | |
| contentDiv.textContent += char; | |
| index++; | |
| scrollToBottom(); | |
| setTimeout(typeChar, speed); | |
| } else { | |
| // Done streaming | |
| state.history.push({ role: 'assistant', content: responseText }); | |
| } | |
| } | |
| typeChar(); | |
| }, 800); | |
| } | |
| // ============================== | |
| // SPEECH TO TEXT (Web Speech API) | |
| // ============================== | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (SpeechRecognition) { | |
| const recognition = new SpeechRecognition(); | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| recognition.lang = 'en-US'; | |
| elements.micBtn.addEventListener('click', () => { | |
| if (state.isRecording) { | |
| recognition.stop(); | |
| } else { | |
| recognition.start(); | |
| } | |
| }); | |
| recognition.onstart = () => { | |
| state.isRecording = true; | |
| elements.micBtn.classList.add('recording'); | |
| elements.messageInput.placeholder = "Listening..."; | |
| }; | |
| recognition.onend = () => { | |
| state.isRecording = false; | |
| elements.micBtn.classList.remove('recording'); | |
| elements.messageInput.placeholder = "Message..."; | |
| }; | |
| recognition.onresult = (event) => { | |
| const transcript = event.results[0][0].transcript; | |
| elements.messageInput.value = transcript; | |
| elements.messageInput.style.height = 'auto'; // Reset height logic | |
| elements.messageInput.style.height = (elements.messageInput.scrollHeight) + 'px'; | |
| elements.messageInput.focus(); | |
| }; | |
| recognition.onerror = (event) => { | |
| console.error("Speech recognition error", event.error); | |
| state.isRecording = false; | |
| elements.micBtn.classList.remove('recording'); | |
| alert("Error accessing microphone: " + event.error); | |
| }; | |
| } else { | |
| // Fallback for browsers without support | |
| elements.micBtn.style.display = 'none'; | |
| console.warn("Web Speech API not supported in this browser."); | |
| } | |
| </script> | |
| </body> | |
| </html> |