DeepSeek-V4-Pro / index.html
akhaliq's picture
akhaliq HF Staff
feat: implement dynamic stream timeout to improve response handling responsiveness
0195d91
<!DOCTYPE html>
<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>