Hy3 / index.html
akhaliq's picture
akhaliq HF Staff
Restyle chat UI with Tencent palette + Hy3 logo
3ac9634
Raw
History Blame Contribute Delete
22.5 kB
<!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 {
/* Tencent palette */
--brand: #0052D9; /* Tencent Blue */
--brand-700: #003CAB;
--brand-600: #004FD9;
--brand-500: #266FE8;
--brand-300: #8FB4F4;
--brand-50: #E8F0FF;
--brand-25: #F2F7FF;
--success: #00A870; /* Tencent green */
--reasoning: #8F6B1F; /* dark amber, readable on light */
--reasoning-bg: #FBF6E8;
--reasoning-brd:#EAD9A0;
--danger: #D54941;
--danger-bg: #FFF1F0;
--danger-brd: #FFCCC7;
--text: #1D2129; /* Tencent "font-gray-1" */
--text-dim: #4E5969; /* "font-gray-2" */
--text-muted: #86909C; /* "font-gray-3" */
--text-faint: #C9CDD4; /* "font-gray-4" */
--bg: #F3F4F8; /* page */
--surface: #FFFFFF; /* cards / bubbles */
--surface-2: #F7F8FA; /* alt row, inputs */
--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 ---------- */
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 ---------- */
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 ---------- */
.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);
}
/* ---------- composer ---------- */
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> &middot; 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";
// ----- state -----
const messages = []; // {role, content, reasoning, reasoning_details}
let client = null;
let liveController = null; // controls the current generation
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.",
];
// ----- dom -----
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");
// ----- examples -----
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); });
// ----- textarea autosize -----
function autoGrow() {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 180) + "px";
}
input.addEventListener("input", autoGrow);
// ----- connect to the gradio.Server backend -----
async function ensureClient() {
if (!client) {
client = await Client.connect(window.location.origin);
}
return client;
}
// ----- rendering helpers -----
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);
// insert reasoning before the bubble
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();
}
// ----- send a turn -----
async function send() {
const text = input.value.trim();
if (!text || busy) return;
removeWelcome();
input.value = "";
autoGrow();
// user bubble
messages.push({ role: "user", content: text });
const userCell = newRow("user");
userCell.bubble.textContent = text;
// assistant bubble (streaming)
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);
}
// update history for continuation
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;
}
// stop the reasoning pulse; collapse once answer is present
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);
// pop the empty assistant turn so history stays consistent
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 = "";
// restore welcome
transcript.appendChild(welcome);
input.focus();
});
autoGrow();
</script>
</body>
</html>