Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DeepSeek V4 Pro</title> | |
| <!-- Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <!-- Marked.js for Markdown parsing --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <!-- Gradio Client --> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| window.GradioClient = Client; | |
| </script> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --text-color: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --input-bg: #1e293b; | |
| --border-color: #334155; | |
| --btn-bg: #3b82f6; | |
| --btn-color: #ffffff; | |
| --code-bg: #020617; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', system-ui, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| display: flex; | |
| justify-content: center; | |
| height: 100vh; | |
| overflow: hidden; | |
| font-size: 16px; | |
| } | |
| #app { | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| position: relative; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 12px 16px; | |
| display: flex; | |
| align-items: center; | |
| position: absolute; | |
| top: 0; | |
| width: 100%; | |
| z-index: 10; | |
| } | |
| .title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| transition: background 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .title span { | |
| font-size: 0.7em; | |
| color: #475569; | |
| } | |
| .title:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| /* Chat Container */ | |
| #chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding-top: 60px; | |
| padding-bottom: 120px; | |
| display: flex; | |
| flex-direction: column; | |
| scroll-behavior: smooth; | |
| } | |
| /* Scrollbar */ | |
| #chat-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #chat-container::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| #chat-container::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| /* Messages */ | |
| .message-wrapper { | |
| width: 100%; | |
| padding: 24px 16px; | |
| } | |
| .message { | |
| max-width: 768px; | |
| margin: 0 auto; | |
| display: flex; | |
| gap: 16px; | |
| } | |
| .avatar { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| font-weight: 600; | |
| flex-shrink: 0; | |
| } | |
| .user .avatar { | |
| background: #2563eb; | |
| color: white; | |
| } | |
| .bot .avatar { | |
| background: transparent; | |
| } | |
| .bot .avatar img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| border-radius: 50%; | |
| } | |
| .message-content { | |
| flex: 1; | |
| line-height: 1.6; | |
| word-wrap: break-word; | |
| min-width: 0; | |
| color: var(--text-color); | |
| } | |
| /* Markdown Styles in Bot Message */ | |
| .message-content p { | |
| margin-bottom: 1em; | |
| } | |
| .message-content p:last-child { | |
| margin-bottom: 0; | |
| } | |
| .message-content pre { | |
| background: var(--code-bg); | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 16px 0; | |
| border: 1px solid var(--border-color); | |
| } | |
| .message-content code { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9em; | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| } | |
| .message-content pre code { | |
| background: transparent; | |
| padding: 0; | |
| font-size: 0.85em; | |
| } | |
| /* Input Area */ | |
| #input-container { | |
| position: absolute; | |
| bottom: 0; | |
| width: 100%; | |
| background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 1) 30%); | |
| padding: 24px 16px; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| align-items: flex-end; | |
| background: var(--input-bg); | |
| border-radius: 24px; | |
| padding: 8px 12px; | |
| width: 100%; | |
| max-width: 768px; | |
| position: relative; | |
| box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |
| border: 1px solid var(--border-color); | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: #3b82f6; | |
| box-shadow: 0 0 15px rgba(59, 130, 246, 0.2); | |
| } | |
| #user-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-color); | |
| font-size: 1rem; | |
| font-family: 'Inter', sans-serif; | |
| resize: none; | |
| padding: 10px 48px 10px 12px; | |
| max-height: 200px; | |
| min-height: 24px; | |
| outline: none; | |
| line-height: 1.5; | |
| } | |
| #user-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| #send-btn { | |
| position: absolute; | |
| right: 12px; | |
| bottom: 12px; | |
| background: var(--btn-bg); | |
| color: var(--btn-color); | |
| border: none; | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: opacity 0.2s, transform 0.1s; | |
| } | |
| #send-btn:hover { | |
| opacity: 0.9; | |
| } | |
| #send-btn:active { | |
| transform: scale(0.95); | |
| } | |
| #send-btn:disabled { | |
| background: var(--border-color); | |
| color: var(--text-muted); | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| #send-btn svg { | |
| width: 16px; | |
| height: 16px; | |
| fill: currentColor; | |
| } | |
| /* Loading Indicator */ | |
| .typing-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 8px 0; | |
| width: fit-content; | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--text-muted); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| } | |
| .dot:nth-child(1) { | |
| animation-delay: -0.32s; | |
| } | |
| .dot:nth-child(2) { | |
| animation-delay: -0.16s; | |
| } | |
| @keyframes bounce { | |
| 0%, | |
| 80%, | |
| 100% { | |
| transform: scale(0.6); | |
| opacity: 0.5; | |
| } | |
| 40% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--text-muted); | |
| text-align: center; | |
| padding: 0 20px; | |
| margin-top: 40px; | |
| } | |
| .empty-state .logo { | |
| width: 64px; | |
| height: 64px; | |
| background: transparent; | |
| border-radius: 50%; | |
| margin-bottom: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| } | |
| .empty-state .logo img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .empty-state h2 { | |
| font-size: 1.5rem; | |
| font-weight: 500; | |
| color: var(--text-color); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <header> | |
| <div class="title">DeepSeek V4 Pro <span>▼</span></div> | |
| </header> | |
| <div id="chat-container"> | |
| <div class="empty-state" id="empty-state"> | |
| <div class="logo"> | |
| <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/6538815d1bdb3c40db94fbfa/xMBly9PUMphrFVMxLX4kq.png" | |
| alt="DeepSeek V4 Pro Logo"> | |
| </div> | |
| <h2>How can I help you today?</h2> | |
| </div> | |
| <div class="message-wrapper" style="display:none;" id="typing-indicator-wrapper"> | |
| <div class="message bot"> | |
| <div class="avatar"> | |
| <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/6538815d1bdb3c40db94fbfa/xMBly9PUMphrFVMxLX4kq.png" | |
| alt="Bot Avatar"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="typing-indicator" id="typing-indicator"> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="input-container"> | |
| <div class="input-wrapper"> | |
| <textarea id="user-input" rows="1" placeholder="Message DeepSeek V4 Pro..."></textarea> | |
| <button id="send-btn" disabled> | |
| <svg viewBox="0 0 24 24"> | |
| <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| let client; | |
| let history = []; | |
| let isGenerating = false; | |
| const chatContainer = document.getElementById('chat-container'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const typingIndicatorWrapper = document.getElementById('typing-indicator-wrapper'); | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| // Initialize Gradio Client | |
| async function initClient() { | |
| try { | |
| client = await window.GradioClient.connect(window.location.origin); | |
| console.log("Connected to Gradio Server"); | |
| userInput.placeholder = "Message DeepSeek V4 Pro..."; | |
| } catch (err) { | |
| console.error("Failed to connect to Gradio Server:", err); | |
| userInput.placeholder = "Failed to connect to server."; | |
| } | |
| } | |
| function updateSendButton() { | |
| sendBtn.disabled = isGenerating || userInput.value.trim().length === 0; | |
| } | |
| // Auto-resize textarea | |
| userInput.addEventListener('input', function () { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 200) + 'px'; | |
| updateSendButton(); | |
| }); | |
| // Handle Enter key (Shift+Enter for newline) | |
| userInput.addEventListener('keydown', function (e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| if (!isGenerating && userInput.value.trim().length > 0) { | |
| sendMessage(); | |
| } | |
| } | |
| }); | |
| sendBtn.addEventListener('click', sendMessage); | |
| function addMessage(role, content, isStreaming = false) { | |
| if (emptyState && emptyState.style.display !== 'none') { | |
| emptyState.style.display = 'none'; | |
| } | |
| const wrapperDiv = document.createElement('div'); | |
| wrapperDiv.className = 'message-wrapper'; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| const avatarDiv = document.createElement('div'); | |
| avatarDiv.className = 'avatar'; | |
| if (role === 'user') { | |
| avatarDiv.textContent = 'U'; | |
| } else { | |
| avatarDiv.innerHTML = '<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/6538815d1bdb3c40db94fbfa/xMBly9PUMphrFVMxLX4kq.png" alt="Bot Avatar">'; | |
| } | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| if (role === 'bot') { | |
| contentDiv.innerHTML = marked.parse(content || ""); | |
| } else { | |
| contentDiv.textContent = content; | |
| } | |
| messageDiv.appendChild(avatarDiv); | |
| messageDiv.appendChild(contentDiv); | |
| wrapperDiv.appendChild(messageDiv); | |
| // Insert before typing indicator wrapper if it's there | |
| chatContainer.insertBefore(wrapperDiv, typingIndicatorWrapper); | |
| scrollToBottom(); | |
| return contentDiv; | |
| } | |
| function scrollToBottom() { | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function finishGeneration() { | |
| isGenerating = false; | |
| typingIndicatorWrapper.style.display = 'none'; | |
| typingIndicator.style.display = 'none'; | |
| updateSendButton(); | |
| userInput.focus(); | |
| } | |
| async function sendMessage() { | |
| const text = userInput.value.trim(); | |
| if (!text || !client || isGenerating) return; | |
| isGenerating = true; | |
| // Reset input | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| updateSendButton(); | |
| // Add user message to UI | |
| addMessage('user', text); | |
| // Show typing indicator | |
| typingIndicatorWrapper.style.display = 'block'; | |
| typingIndicator.style.display = 'flex'; | |
| scrollToBottom(); | |
| // Setup streaming bot message | |
| const botContentDiv = addMessage('bot', '', true); | |
| let fullResponse = ""; | |
| try { | |
| const submission = client.submit("/chat", { | |
| message: text, | |
| history_json: history | |
| }); | |
| const iterator = submission[Symbol.asyncIterator](); | |
| let streamDone = false; | |
| let hasReceivedData = false; | |
| while (!streamDone) { | |
| // Dynamic timeout: 60s before first data (queue/connection wait), | |
| // 2s after first data (idle = stream finished, since tokens | |
| // arrive every few hundred ms during generation). | |
| const idleTimeout = hasReceivedData ? 2000 : 60000; | |
| const result = await Promise.race([ | |
| iterator.next(), | |
| new Promise(resolve => | |
| setTimeout(() => resolve({ value: undefined, done: true }), idleTimeout) | |
| ) | |
| ]); | |
| if (result.done) { | |
| streamDone = true; | |
| break; | |
| } | |
| const event = result.value; | |
| if (event.type === "data") { | |
| hasReceivedData = true; | |
| typingIndicatorWrapper.style.display = 'none'; | |
| typingIndicator.style.display = 'none'; | |
| fullResponse = event.data[0]; | |
| botContentDiv.innerHTML = marked.parse(fullResponse || ""); | |
| scrollToBottom(); | |
| } | |
| if (event.type === "status") { | |
| if (event.stage === "error") { | |
| throw new Error(event.message); | |
| } | |
| if (event.stage === "complete") { | |
| streamDone = true; | |
| } | |
| } | |
| } | |
| // Update history | |
| history.push({ "role": "user", "content": text }); | |
| history.push({ "role": "assistant", "content": fullResponse }); | |
| } catch (err) { | |
| console.error("Error during generation:", err); | |
| botContentDiv.innerHTML = `<span style="color: #ef4444;">Error: ${err.message || 'Something went wrong.'}</span>`; | |
| } | |
| // Always runs — not inside finally (which depends on iterator cleanup) | |
| // but sequentially after try/catch, which is guaranteed to complete | |
| // thanks to Promise.race timeout. | |
| finishGeneration(); | |
| } | |
| // Run init | |
| initClient(); | |
| </script> | |
| </body> | |
| </html> |