| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Hy3 · Tencent</title> |
| <style> |
| :root { |
| |
| --brand: #0052D9; |
| --brand-700: #003CAB; |
| --brand-600: #004FD9; |
| --brand-500: #266FE8; |
| --brand-300: #8FB4F4; |
| --brand-50: #E8F0FF; |
| --brand-25: #F2F7FF; |
| |
| --success: #00A870; |
| --reasoning: #8F6B1F; |
| --reasoning-bg: #FBF6E8; |
| --reasoning-brd:#EAD9A0; |
| |
| --danger: #D54941; |
| --danger-bg: #FFF1F0; |
| --danger-brd: #FFCCC7; |
| |
| --text: #1D2129; |
| --text-dim: #4E5969; |
| --text-muted: #86909C; |
| --text-faint: #C9CDD4; |
| |
| --bg: #F3F4F8; |
| --surface: #FFFFFF; |
| --surface-2: #F7F8FA; |
| --border: #E5E6EB; |
| --border-strong:#DCDCDC; |
| |
| --shadow-sm: 0 1px 2px rgba(0,0,0,0.04), 0 1px 6px -1px rgba(0,0,0,0.02), 0 2px 4px rgba(0,0,0,0.02); |
| --shadow-md: 0 4px 16px rgba(29,33,41,0.08); |
| |
| --radius: 10px; |
| --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; |
| --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif; |
| } |
| |
| * { box-sizing: border-box; } |
| html, body { height: 100%; margin: 0; } |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: var(--sans); |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| -webkit-font-smoothing: antialiased; |
| text-rendering: optimizeLegibility; |
| } |
| |
| |
| header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 0 24px; |
| height: 60px; |
| background: var(--surface); |
| border-bottom: 1px solid var(--border); |
| flex-shrink: 0; |
| } |
| .brand { display: flex; align-items: center; gap: 12px; } |
| .brand .logo { |
| height: 26px; |
| width: auto; |
| display: block; |
| user-select: none; |
| -webkit-user-drag: none; |
| } |
| .brand .divider { |
| width: 1px; height: 22px; background: var(--border); |
| } |
| .brand .tag { |
| font-size: 12.5px; |
| color: var(--text-muted); |
| font-weight: 500; |
| letter-spacing: 0.01em; |
| } |
| .brand .tag b { color: var(--text-dim); font-weight: 600; } |
| .badge { |
| font-family: var(--mono); |
| font-size: 11px; |
| color: var(--brand); |
| background: var(--brand-50); |
| padding: 3px 9px; |
| border-radius: 6px; |
| font-weight: 600; |
| } |
| .actions { display: flex; gap: 8px; } |
| button.icon-btn { |
| background: var(--surface); |
| color: var(--text-dim); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 7px 14px; |
| font-size: 13px; |
| cursor: pointer; |
| font-family: var(--sans); |
| font-weight: 500; |
| transition: all 0.15s ease; |
| display: inline-flex; align-items: center; gap: 6px; |
| } |
| button.icon-btn:hover { |
| color: var(--brand); |
| border-color: var(--brand-300); |
| background: var(--brand-25); |
| } |
| button.icon-btn svg { width: 14px; height: 14px; } |
| |
| |
| main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| min-height: 0; |
| } |
| #transcript { |
| flex: 1; |
| overflow-y: auto; |
| padding: 32px 0 16px; |
| scroll-behavior: smooth; |
| } |
| #transcript::-webkit-scrollbar { width: 8px; } |
| #transcript::-webkit-scrollbar-thumb { background: var(--text-faint); border-radius: 8px; border: 2px solid var(--bg); } |
| |
| .msg-row { |
| max-width: 860px; |
| margin: 0 auto 24px; |
| padding: 0 24px; |
| display: flex; |
| gap: 14px; |
| } |
| .msg-row.user { flex-direction: row-reverse; } |
| .avatar { |
| width: 32px; height: 32px; |
| border-radius: 8px; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 13px; font-weight: 600; |
| flex-shrink: 0; |
| box-shadow: var(--shadow-sm); |
| } |
| .avatar.user { |
| background: linear-gradient(135deg, var(--brand-600), var(--brand-700)); |
| color: #fff; |
| } |
| .avatar.ai { |
| background: var(--surface); |
| color: var(--brand); |
| border: 1px solid var(--border); |
| } |
| .avatar.ai svg { width: 18px; height: 18px; } |
| |
| .bubble-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; max-width: calc(100% - 46px); } |
| .msg-row.user .bubble-col { align-items: flex-end; } |
| .bubble { |
| padding: 12px 16px; |
| border-radius: var(--radius); |
| font-size: 14.5px; |
| line-height: 1.65; |
| word-wrap: break-word; |
| overflow-wrap: anywhere; |
| white-space: pre-wrap; |
| max-width: 100%; |
| } |
| .bubble.user { |
| background: var(--brand-50); |
| color: var(--brand-700); |
| border: 1px solid var(--brand-300); |
| border-top-right-radius: 3px; |
| } |
| .bubble.ai { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text); |
| border-top-left-radius: 3px; |
| box-shadow: var(--shadow-sm); |
| width: 100%; |
| } |
| .bubble .empty { color: var(--text-faint); font-style: italic; } |
| |
| details.reasoning { |
| background: var(--reasoning-bg); |
| border: 1px solid var(--reasoning-brd); |
| border-radius: var(--radius); |
| width: 100%; |
| overflow: hidden; |
| } |
| details.reasoning summary { |
| cursor: pointer; |
| padding: 9px 14px; |
| font-size: 12.5px; |
| color: var(--reasoning); |
| list-style: none; |
| display: flex; align-items: center; gap: 8px; |
| user-select: none; |
| font-weight: 600; |
| letter-spacing: 0.01em; |
| } |
| details.reasoning summary::-webkit-details-marker { display: none; } |
| details.reasoning summary::before { |
| content: "▸"; |
| font-size: 9px; |
| transition: transform 0.18s ease; |
| color: var(--reasoning); |
| } |
| details.reasoning[open] summary::before { transform: rotate(90deg); } |
| details.reasoning summary .dot { |
| width: 6px; height: 6px; border-radius: 50%; |
| background: var(--reasoning); |
| margin-left: auto; |
| opacity: 0; |
| } |
| details.reasoning.streaming summary .dot { |
| opacity: 1; |
| animation: pulse 1.1s ease-in-out infinite; |
| } |
| @keyframes pulse { 0%,100% { opacity: 0.25; } 50% { opacity: 1; } } |
| details.reasoning .body { |
| padding: 6px 14px 12px; |
| color: var(--text-dim); |
| font-size: 13px; |
| line-height: 1.65; |
| white-space: pre-wrap; |
| overflow-wrap: anywhere; |
| border-top: 1px solid var(--reasoning-brd); |
| font-family: var(--mono); |
| } |
| |
| .blink::after { |
| content: "▍"; |
| color: var(--brand); |
| animation: blink 1s steps(2) infinite; |
| margin-left: 1px; |
| } |
| @keyframes blink { 50% { opacity: 0; } } |
| |
| .err { |
| background: var(--danger-bg); |
| border: 1px solid var(--danger-brd); |
| color: var(--danger); |
| padding: 10px 14px; |
| border-radius: var(--radius); |
| font-size: 13px; |
| width: 100%; |
| line-height: 1.55; |
| } |
| |
| |
| .welcome { max-width: 760px; margin: 0 auto; padding: 64px 24px 24px; text-align: center; } |
| .welcome .hero-logo { |
| height: 44px; width: auto; margin: 0 auto 18px; display: block; |
| user-select: none; -webkit-user-drag: none; |
| } |
| .welcome h2 { |
| font-size: 24px; margin: 0 0 10px; font-weight: 650; letter-spacing: -0.01em; color: var(--text); |
| } |
| .welcome p { color: var(--text-dim); font-size: 14px; margin: 0 0 28px; line-height: 1.7; } |
| .examples { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; max-width: 680px; margin: 0 auto; } |
| .example-chip { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text-dim); |
| padding: 10px 16px; |
| border-radius: 999px; |
| font-size: 13px; |
| cursor: pointer; |
| font-family: var(--sans); |
| transition: all 0.15s ease; |
| box-shadow: var(--shadow-sm); |
| } |
| .example-chip:hover { |
| color: var(--brand); |
| border-color: var(--brand-300); |
| background: var(--brand-25); |
| } |
| |
| |
| footer { |
| border-top: 1px solid var(--border); |
| background: var(--surface); |
| padding: 14px 24px 18px; |
| flex-shrink: 0; |
| } |
| .composer { max-width: 860px; margin: 0 auto; } |
| .input-wrap { |
| background: var(--surface); |
| border: 1.5px solid var(--border); |
| border-radius: 12px; |
| transition: border-color 0.15s ease, box-shadow 0.15s ease; |
| box-shadow: var(--shadow-sm); |
| } |
| .input-wrap:focus-within { |
| border-color: var(--brand-500); |
| box-shadow: 0 0 0 3px var(--brand-25); |
| } |
| #input { |
| width: 100%; |
| background: transparent; |
| border: none; |
| color: var(--text); |
| border-radius: 12px; |
| padding: 14px 16px 6px; |
| font-size: 14.5px; |
| font-family: var(--sans); |
| resize: none; |
| outline: none; |
| line-height: 1.55; |
| max-height: 180px; |
| display: block; |
| } |
| #input::placeholder { color: var(--text-faint); } |
| #input:disabled { opacity: 0.55; } |
| .composer-bar { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px 6px 10px; } |
| .opts { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; } |
| .opt { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); font-weight: 500; } |
| .opt input[type=number] { |
| background: var(--surface-2); border: 1px solid var(--border); color: var(--text); |
| border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 72px; |
| font-family: var(--mono); outline: none; |
| } |
| .opt input[type=number]:focus { border-color: var(--brand-500); } |
| .opt input[type=range] { |
| -webkit-appearance: none; appearance: none; |
| width: 96px; height: 4px; background: var(--border); border-radius: 999px; outline: none; |
| } |
| .opt input[type=range]::-webkit-slider-thumb { |
| -webkit-appearance: none; appearance: none; |
| width: 14px; height: 14px; border-radius: 50%; background: var(--brand); cursor: pointer; |
| border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.2); |
| } |
| #temp-val { font-family: var(--mono); min-width: 26px; color: var(--text-dim); } |
| .send-row { display: flex; gap: 8px; align-items: center; } |
| #send { |
| background: var(--brand); |
| color: #fff; |
| border: none; |
| border-radius: 8px; |
| padding: 8px 20px; |
| font-size: 13.5px; |
| font-weight: 600; |
| cursor: pointer; |
| font-family: var(--sans); |
| transition: background 0.15s ease, transform 0.05s ease; |
| display: inline-flex; align-items: center; gap: 6px; |
| } |
| #send:hover:not(:disabled) { background: var(--brand-700); } |
| #send:active { transform: translateY(1px); } |
| #send:disabled { background: var(--text-faint); cursor: not-allowed; } |
| #send svg { width: 14px; height: 14px; } |
| #stop { |
| background: var(--surface); |
| color: var(--text-dim); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 8px 16px; |
| font-size: 13.5px; |
| cursor: pointer; |
| font-family: var(--sans); |
| font-weight: 500; |
| display: none; |
| } |
| #stop:hover { color: var(--danger); border-color: var(--danger-brd); background: var(--danger-bg); } |
| .hint { font-size: 11.5px; color: var(--text-faint); } |
| kbd { |
| font-family: var(--mono); font-size: 11px; |
| background: var(--surface-2); border: 1px solid var(--border); |
| border-bottom-width: 2px; border-radius: 5px; padding: 1px 5px; color: var(--text-dim); |
| } |
| </style> |
| </head> |
| <body> |
| <header> |
| <div class="brand"> |
| <img class="logo" src="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" alt="Hy3" /> |
| <span class="divider"></span> |
| <span class="tag"><b>Chat Playground</b> · reasoning-enabled</span> |
| </div> |
| <div class="actions"> |
| <button class="icon-btn" id="clear-btn" title="Clear conversation"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg> |
| New chat |
| </button> |
| </div> |
| </header> |
|
|
| <main> |
| <div id="transcript"> |
| <div class="welcome" id="welcome"> |
| <img class="hero-logo" src="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" alt="Hy3" /> |
| <h2>Hy3, with its thinking visible</h2> |
| <p>Streaming chat over OpenRouter's OpenAI-compatible API.<br/>Reasoning tokens stream live and are preserved across turns, so the model keeps reasoning from where it left off.</p> |
| <div class="examples" id="examples"></div> |
| </div> |
| </div> |
|
|
| <footer> |
| <div class="composer"> |
| <div class="input-wrap"> |
| <textarea id="input" rows="1" placeholder="Ask Hy3 anything… (Enter to send, Shift+Enter for newline)" autofocus></textarea> |
| <div class="composer-bar"> |
| <div class="opts"> |
| <label class="opt">temp |
| <input type="range" id="temp" min="0" max="2" step="0.1" value="0.9" /> |
| <span id="temp-val">0.9</span> |
| </label> |
| <label class="opt">max_tokens |
| <input type="number" id="max-tokens" min="1" max="32000" step="128" value="2048" /> |
| </label> |
| </div> |
| <div class="send-row"> |
| <span class="hint"><kbd>Enter</kbd> send</span> |
| <button id="stop">Stop</button> |
| <button id="send"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> |
| Send |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </footer> |
| </main> |
|
|
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.7.1/dist/index.min.js"; |
| |
| |
| const messages = []; |
| let client = null; |
| let liveController = null; |
| let busy = false; |
| |
| const EXAMPLES = [ |
| "How many r's are in the word 'strawberry'?", |
| "A bat and ball cost $1.10. The bat costs $1 more than the ball. How much is the ball?", |
| "Write a haiku about deep-sea bioluminescence.", |
| "Explain why 0.1 + 0.2 != 0.3 in floating point.", |
| ]; |
| |
| |
| const $ = (id) => document.getElementById(id); |
| const transcript = $("transcript"); |
| const input = $("input"); |
| const sendBtn = $("send"); |
| const stopBtn = $("stop"); |
| const clearBtn = $("clear-btn"); |
| const tempEl = $("temp"); |
| const tempVal = $("temp-val"); |
| const maxTokensEl = $("max-tokens"); |
| let welcome = $("welcome"); |
| |
| |
| const examplesEl = $("examples"); |
| EXAMPLES.forEach((text) => { |
| const chip = document.createElement("button"); |
| chip.className = "example-chip"; |
| chip.textContent = text; |
| chip.addEventListener("click", () => { input.value = text; autoGrow(); input.focus(); }); |
| examplesEl.appendChild(chip); |
| }); |
| |
| tempEl.addEventListener("input", () => { tempVal.textContent = parseFloat(tempEl.value).toFixed(1); }); |
| |
| |
| function autoGrow() { |
| input.style.height = "auto"; |
| input.style.height = Math.min(input.scrollHeight, 180) + "px"; |
| } |
| input.addEventListener("input", autoGrow); |
| |
| |
| async function ensureClient() { |
| if (!client) { |
| client = await Client.connect(window.location.origin); |
| } |
| return client; |
| } |
| |
| |
| function removeWelcome() { |
| if (welcome && welcome.parentNode) welcome.parentNode.removeChild(welcome); |
| } |
| |
| const AI_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="4"/></svg>'; |
| |
| function newRow(role) { |
| const row = document.createElement("div"); |
| row.className = "msg-row " + role; |
| |
| const avatar = document.createElement("div"); |
| avatar.className = "avatar " + (role === "user" ? "user" : "ai"); |
| if (role === "user") { |
| avatar.textContent = "You"; |
| } else { |
| avatar.innerHTML = AI_ICON; |
| } |
| |
| const col = document.createElement("div"); |
| col.className = "bubble-col"; |
| |
| const bubble = document.createElement("div"); |
| bubble.className = "bubble " + (role === "user" ? "user" : "ai"); |
| |
| row.appendChild(avatar); |
| row.appendChild(col); |
| col.appendChild(bubble); |
| transcript.appendChild(row); |
| transcript.scrollTop = transcript.scrollHeight; |
| return { row, col, bubble }; |
| } |
| |
| function ensureReasoning(col) { |
| let d = col.querySelector("details.reasoning"); |
| if (!d) { |
| d = document.createElement("details"); |
| d.className = "reasoning streaming"; |
| d.open = true; |
| const s = document.createElement("summary"); |
| const label = document.createElement("span"); |
| label.textContent = "Reasoning"; |
| const dot = document.createElement("span"); |
| dot.className = "dot"; |
| s.appendChild(label); |
| s.appendChild(dot); |
| const body = document.createElement("div"); |
| body.className = "body"; |
| d.appendChild(s); |
| d.appendChild(body); |
| |
| col.insertBefore(d, col.querySelector(".bubble")); |
| } |
| return d; |
| } |
| |
| function setBusy(state) { |
| busy = state; |
| input.disabled = state; |
| sendBtn.disabled = state; |
| stopBtn.style.display = state ? "inline-block" : "none"; |
| if (!state) input.focus(); |
| } |
| |
| |
| async function send() { |
| const text = input.value.trim(); |
| if (!text || busy) return; |
| |
| removeWelcome(); |
| input.value = ""; |
| autoGrow(); |
| |
| |
| messages.push({ role: "user", content: text }); |
| const userCell = newRow("user"); |
| userCell.bubble.textContent = text; |
| |
| |
| messages.push({ role: "assistant", content: "", reasoning: "", reasoning_details: null }); |
| const aiCell = newRow("ai"); |
| let reasoningBody = null; |
| let reasoningEl = null; |
| const blinkSpan = document.createElement("span"); |
| blinkSpan.className = "blink"; |
| aiCell.bubble.appendChild(blinkSpan); |
| aiCell.bubble.classList.add("empty"); |
| |
| setBusy(true); |
| liveController = { cancelled: false }; |
| |
| const payload = { |
| messages: messages.map(m => ({ |
| role: m.role, |
| content: m.content, |
| reasoning_details: m.reasoning_details || null, |
| })), |
| model: "tencent/hy3:free", |
| temperature: parseFloat(tempEl.value), |
| max_tokens: parseInt(maxTokensEl.value, 10) || 2048, |
| }; |
| |
| try { |
| const c = await ensureClient(); |
| const result = await c.predict("/chat", payload); |
| |
| for await (const raw of result.data) { |
| if (liveController && liveController.cancelled) break; |
| let evt; |
| try { evt = JSON.parse(raw); } catch { continue; } |
| |
| if (evt.type === "reasoning") { |
| if (!reasoningEl) { |
| reasoningEl = ensureReasoning(aiCell.col); |
| reasoningBody = reasoningEl.querySelector(".body"); |
| } |
| reasoningBody.textContent += evt.text; |
| transcript.scrollTop = transcript.scrollHeight; |
| } else if (evt.type === "content") { |
| if (!aiCell.bubble.classList.contains("populated")) { |
| aiCell.bubble.innerHTML = ""; |
| aiCell.bubble.classList.add("populated"); |
| aiCell.bubble.classList.remove("empty"); |
| } |
| aiCell.bubble.appendChild(document.createTextNode(evt.text)); |
| const blink = aiCell.bubble.querySelector(".blink"); |
| if (blink) blink.remove(); |
| transcript.scrollTop = transcript.scrollHeight; |
| } else if (evt.type === "error") { |
| const e = document.createElement("div"); |
| e.className = "err"; |
| e.textContent = evt.text; |
| aiCell.col.appendChild(e); |
| } else if (evt.type === "done") { |
| const blink = aiCell.bubble.querySelector(".blink"); |
| if (blink) blink.remove(); |
| if (evt.content) { |
| if (!aiCell.bubble.classList.contains("populated")) { |
| aiCell.bubble.innerHTML = ""; |
| aiCell.bubble.classList.add("populated"); |
| aiCell.bubble.classList.remove("empty"); |
| } |
| aiCell.bubble.textContent = evt.content; |
| } else { |
| aiCell.bubble.classList.add("empty"); |
| const em = document.createElement("span"); |
| em.className = "empty"; |
| em.textContent = "(no content)"; |
| aiCell.bubble.appendChild(em); |
| } |
| |
| const last = messages[messages.length - 1]; |
| if (last && last.role === "assistant") { |
| last.content = evt.content || ""; |
| last.reasoning = evt.reasoning || ""; |
| last.reasoning_details = evt.reasoning_details || null; |
| } |
| |
| if (reasoningEl) { |
| reasoningEl.classList.remove("streaming"); |
| if (evt.content) reasoningEl.open = false; |
| } |
| } |
| } |
| } catch (e) { |
| const errEl = document.createElement("div"); |
| errEl.className = "err"; |
| errEl.textContent = "Client error: " + (e?.message || String(e)); |
| aiCell.col.appendChild(errEl); |
| |
| messages.pop(); |
| } finally { |
| const blink = aiCell.bubble.querySelector(".blink"); |
| if (blink) blink.remove(); |
| liveController = null; |
| setBusy(false); |
| } |
| } |
| |
| function stop() { |
| if (liveController) liveController.cancelled = true; |
| } |
| |
| sendBtn.addEventListener("click", send); |
| stopBtn.addEventListener("click", stop); |
| input.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| send(); |
| } |
| }); |
| clearBtn.addEventListener("click", () => { |
| if (busy) return; |
| messages.length = 0; |
| transcript.innerHTML = ""; |
| |
| transcript.appendChild(welcome); |
| input.focus(); |
| }); |
| |
| autoGrow(); |
| </script> |
| </body> |
| </html> |