Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | |
| <title>Hy3 · Tencent</title> | |
| <meta name="color-scheme" content="light dark" /> | |
| <link rel="icon" href="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" /> | |
| <link rel="apple-touch-icon" href="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" /> | |
| <!-- Tencent logo --> | |
| <link rel="preload" as="image" href="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" /> | |
| <!-- marked + KaTeX + highlight.js from CDN --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" /> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css" id="hljs-light-css" /> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css" id="hljs-dark-css" disabled /> | |
| <script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/highlight.min.js"></script> | |
| <style> | |
| /* ───────────────────────────────────────────────────────────────────── | |
| Tencent design tokens | |
| Tencent brand blue : #0052D9 (primary) | |
| Tencent red accent : #E53935 (secondary / highlights) | |
| Tencent teal : #00A98F (success accents) | |
| ───────────────────────────────────────────────────────────────────── */ | |
| :root { | |
| --tc-blue: #0052D9; | |
| --tc-blue-hover: #003CAB; | |
| --tc-blue-soft: #E8F0FF; | |
| --tc-blue-060: rgba(0, 82, 217, 0.06); | |
| --tc-blue-120: rgba(0, 82, 217, 0.12); | |
| --tc-red: #E53935; | |
| --tc-teal: #00A98F; | |
| --bg: #f7f6f3; /* warm canvas, Claude-like */ | |
| --bg-soft: #ffffff; /* surfaces / bubbles / sidebar */ | |
| --bg-muted: #efece6; | |
| --bg-elev: #ffffff; | |
| --sidebar-bg: #ffffff; | |
| --text: #2b2b2b; | |
| --text-strong: #1a1a1a; | |
| --text-muted: #8a8580; | |
| --border: #e7e3dc; | |
| --border-strong: #d8d3cb; | |
| --accent: #c96442; /* warm terracotta accent (Claude) */ | |
| --accent-soft: #f4ece7; | |
| --shadow-sm: 0 1px 2px rgba(20, 18, 14, 0.04); | |
| --shadow-md: 0 4px 14px rgba(20, 18, 14, 0.06); | |
| --shadow-lg: 0 16px 48px rgba(20, 18, 14, 0.14); | |
| --ring: 0 0 0 3px rgba(0, 82, 217, 0.16); | |
| --radius-sm: 8px; | |
| --radius: 14px; | |
| --radius-lg: 20px; | |
| --radius-xl: 26px; | |
| --col-max: 768px; /* centered conversation width */ | |
| --font-sans: "Söhne", "Inter", -apple-system, BlinkMacSystemFont, | |
| "SF Pro Text", "PingFang SC", "Hiragino Sans GB", | |
| "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; | |
| --font-mono: "SF Mono", "JetBrains Mono", "Fira Code", Consolas, Monaco, monospace; | |
| } | |
| html.dark { | |
| --bg: #1a1a1a; /* warm-tinted dark canvas */ | |
| --bg-soft: #232323; | |
| --bg-muted: #2c2c2c; | |
| --bg-elev: #232323; | |
| --sidebar-bg: #202020; | |
| --text: #ece9e4; | |
| --text-strong: #ffffff; | |
| --text-muted: #9a948d; | |
| --border: #34312d; | |
| --border-strong: #423e39; | |
| --accent: #d97757; | |
| --accent-soft: rgba(217, 119, 87, 0.16); | |
| --tc-blue-soft: rgba(0, 82, 217, 0.16); | |
| --tc-blue-060: rgba(0, 82, 217, 0.12); | |
| --tc-blue-120: rgba(0, 82, 217, 0.24); | |
| --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); | |
| --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4); | |
| --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6); | |
| --ring: 0 0 0 3px rgba(0, 82, 217, 0.3); | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| margin: 0; padding: 0; height: 100%; overflow: hidden; | |
| background: var(--bg); color: var(--text); | |
| font-family: var(--font-sans); | |
| -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; | |
| } | |
| button, input, textarea, select { font-family: inherit; } | |
| /* ── Layout shell ─────────────────────────────────────────────────────── | |
| The chat surface (#main) IS the warm canvas and fills everything below | |
| the header; the composer floats at the bottom of it, centered, so the | |
| conversation scrolls underneath it (Claude-style) rather than living | |
| in a separate hard-bordered bar. */ | |
| #app { | |
| display: grid; | |
| grid-template-columns: 300px 1fr; | |
| grid-template-rows: 60px 1fr; | |
| grid-template-areas: | |
| "header header" | |
| "sidebar main"; | |
| height: 100vh; width: 100vw; | |
| } | |
| #app.sidebar-closed { grid-template-columns: 0 1fr; } | |
| #app.sidebar-closed #sidebar { transform: translateX(-100%); } | |
| /* ── Header / brand bar ──────────────────────────────────────────────── | |
| Transparent over the canvas with only a hairline divider — feels like | |
| part of the page, not a chrome bar. */ | |
| #header { | |
| grid-area: header; | |
| display: flex; align-items: center; gap: 12px; | |
| padding: 0 20px; | |
| background: var(--bg-soft); | |
| border-bottom: 1px solid var(--border); | |
| position: relative; z-index: 20; | |
| } | |
| .brand { display: flex; align-items: center; gap: 12px; cursor: default; } | |
| /* The logo is a wide horizontal wordmark (Tencent + Hy3). Size it by | |
| height and let width follow the natural aspect ratio so it never | |
| gets smushed into a square. */ | |
| .brand-logo { | |
| height: 26px; width: auto; | |
| object-fit: contain; | |
| display: block; | |
| border-radius: 4px; | |
| } | |
| .brand-name { font-size: 16px; font-weight: 650; color: var(--text-strong); letter-spacing: -0.01em; } | |
| .brand-sep { color: var(--border-strong); font-weight: 300; } | |
| .brand-tag { font-size: 13px; color: var(--text-muted); font-weight: 500; } | |
| .brand-badge { | |
| margin-left: 4px; font-size: 11px; font-weight: 600; | |
| color: var(--tc-blue); background: var(--tc-blue-soft); | |
| padding: 2px 9px; border-radius: 999px; letter-spacing: 0.02em; | |
| } | |
| .header-spacer { flex: 1; } | |
| .icon-btn { | |
| width: 36px; height: 36px; border-radius: 9px; | |
| border: 1px solid transparent; background: transparent; color: var(--text-muted); | |
| display: inline-flex; align-items: center; justify-content: center; | |
| cursor: pointer; transition: background 0.15s ease, color 0.15s ease; padding: 0; | |
| } | |
| .icon-btn:hover { background: var(--bg-muted); color: var(--text); } | |
| .icon-btn svg { width: 18px; height: 18px; } | |
| /* ── Sidebar ──────────────────────────────────────────────────────────── */ | |
| #sidebar { | |
| grid-area: sidebar; | |
| background: var(--sidebar-bg); | |
| border-right: 1px solid var(--border); | |
| overflow-y: auto; overflow-x: hidden; | |
| transition: transform 0.22s ease; | |
| padding: 18px 18px 28px; | |
| display: flex; flex-direction: column; gap: 16px; | |
| } | |
| .sidebar-notice { | |
| font-size: 12px; line-height: 1.55; color: var(--text-muted); | |
| background: var(--accent-soft); border: 1px solid transparent; | |
| border-radius: var(--radius-sm); padding: 11px 13px; | |
| } | |
| .field { display: flex; flex-direction: column; gap: 7px; } | |
| .field > label { | |
| font-size: 11.5px; font-weight: 600; color: var(--text-muted); | |
| text-transform: none; letter-spacing: 0.01em; | |
| display: flex; align-items: center; justify-content: space-between; gap: 8px; | |
| } | |
| .field .info { | |
| font-size: 11px; font-weight: 400; color: var(--text-muted); | |
| margin-top: -3px; line-height: 1.45; opacity: 0.85; | |
| } | |
| .field input[type="text"], .field textarea, .field select { | |
| width: 100%; padding: 9px 12px; | |
| border: 1px solid var(--border); border-radius: var(--radius-sm); | |
| background: var(--bg-soft); color: var(--text); font-size: 14px; | |
| transition: border-color 0.15s ease, box-shadow 0.15s ease; | |
| } | |
| .field textarea { resize: vertical; min-height: 64px; font-family: inherit; line-height: 1.5; } | |
| .field input:focus, .field textarea:focus, .field select:focus { | |
| outline: none; border-color: var(--tc-blue); box-shadow: var(--ring); | |
| } | |
| .field select { appearance: none; cursor: pointer; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a8580' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; | |
| } | |
| .range-row { display: flex; align-items: center; gap: 10px; } | |
| .range-row input[type="range"] { flex: 1; accent-color: var(--tc-blue); } | |
| .range-num { | |
| width: 58px; padding: 5px 8px; text-align: center; | |
| border: 1px solid var(--border); border-radius: 7px; | |
| background: var(--bg-soft); color: var(--text); font-size: 12px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .range-num:focus { outline: none; border-color: var(--tc-blue); box-shadow: var(--ring); } | |
| .range-row input[type="range"] { -webkit-appearance: none; appearance: none; height: 4px; background: var(--border); border-radius: 2px; } | |
| .range-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--tc-blue); cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } | |
| .range-row input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border: none; border-radius: 50%; background: var(--tc-blue); cursor: pointer; } | |
| .check-row { | |
| display: flex; align-items: center; gap: 8px; cursor: pointer; | |
| font-size: 12px; color: var(--text-muted); user-select: none; margin-top: 2px; | |
| } | |
| .check-row input { accent-color: var(--tc-blue); width: 14px; height: 14px; } | |
| .accordion { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-soft); } | |
| .accordion > summary { | |
| list-style: none; cursor: pointer; padding: 11px 13px; | |
| font-size: 13px; font-weight: 600; color: var(--text); | |
| display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .accordion > summary::-webkit-details-marker { display: none; } | |
| .accordion > summary::after { content: "⌄"; color: var(--text-muted); transition: transform 0.15s ease; } | |
| .accordion[open] > summary::after { transform: rotate(180deg); } | |
| .accordion > .accordion-body { padding: 13px; display: flex; flex-direction: column; gap: 10px; border-top: 1px solid var(--border); } | |
| .accordion textarea { width: 100%; min-height: 180px; font-family: var(--font-mono); font-size: 12px; line-height: 1.5; resize: vertical; } | |
| .tiny-btn { | |
| align-self: flex-start; padding: 7px 15px; font-size: 12px; font-weight: 500; | |
| border: 1px solid var(--border); border-radius: 8px; background: var(--bg-soft); | |
| color: var(--text); cursor: pointer; transition: all 0.15s ease; | |
| } | |
| .tiny-btn:hover { border-color: var(--tc-blue); color: var(--tc-blue); background: var(--tc-blue-060); } | |
| .fn-msg { font-size: 12px; line-height: 1.5; min-height: 18px; } | |
| .fn-msg.ok { color: var(--tc-teal); } | |
| .fn-msg.err { color: var(--tc-red); } | |
| /* ── Main chat surface ────────────────────────────────────────────────── | |
| The warm canvas. Conversation is centered in a narrow column with | |
| generous vertical rhythm; assistant turns are plain text (no bubble), | |
| user turns are soft pills. The composer floats at the bottom on a | |
| gradient fade so messages scroll under it gracefully. */ | |
| #main { | |
| grid-area: main; position: relative; background: var(--bg); | |
| overflow: hidden; display: flex; flex-direction: column; | |
| } | |
| #chat { | |
| flex: 1; overflow-y: auto; overflow-x: hidden; | |
| padding: 28px clamp(16px, 5vw, 48px) 220px; /* bottom room for floating composer */ | |
| scroll-behavior: auto; | |
| } | |
| /* Center the conversation in a narrow column. */ | |
| #chat > .col, | |
| #chat > .message-row { max-width: var(--col-max); margin-left: auto; margin-right: auto; width: 100%; } | |
| .message-row { display: block; width: 100%; margin: 0; } | |
| .message-row.user-row { display: flex; justify-content: flex-end; margin: 22px 0 4px; } | |
| .message-row.bot-row { margin: 0 0 30px; } | |
| .message { max-width: 100%; } | |
| .message.user { | |
| background: var(--bg-elev); color: var(--text-strong); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); padding: 11px 18px; | |
| max-width: min(560px, 82%); box-shadow: var(--shadow-sm); | |
| font-weight: 450; | |
| } | |
| .message.user .hy-user-text { font-size: 15px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; } | |
| .message.bot { background: transparent; padding: 0; max-width: 100%; } | |
| .message.bot .markdown { font-size: 16px; line-height: 1.7; color: var(--text); letter-spacing: 0.005em; } | |
| .message.bot p { margin: 0.85em 0; } | |
| .message.bot p:first-child { margin-top: 0; } | |
| .message.bot p:last-child { margin-bottom: 0; } | |
| .message.bot strong, .message.bot b { font-weight: 650; color: var(--text-strong); } | |
| .message.bot ul, .message.bot ol { padding-left: 1.5em; margin: 0.6em 0; } | |
| .message.bot li { margin: 0.35em 0; line-height: 1.7; } | |
| .message.bot h1 { font-size: 23px; font-weight: 700; color: var(--text-strong); margin: 1.1em 0 0.5em; letter-spacing: -0.01em; } | |
| .message.bot h2 { font-size: 20px; font-weight: 700; color: var(--text-strong); margin: 1em 0 0.45em; letter-spacing: -0.01em; } | |
| .message.bot h3 { font-size: 17px; font-weight: 650; color: var(--text-strong); margin: 0.9em 0 0.4em; } | |
| .message.bot h4 { font-size: 15px; font-weight: 650; color: var(--text); margin: 0.8em 0 0.35em; } | |
| .message.bot a { color: var(--tc-blue); text-underline-offset: 2px; } | |
| .message.bot blockquote { border-left: 3px solid var(--border-strong); margin: 0.9em 0; padding: 2px 16px; color: var(--text-muted); } | |
| .message.bot hr { border: none; border-top: 1px solid var(--border); margin: 1.4em 0; } | |
| .message.bot table { display: block; width: max-content; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0.9em 0; font-size: 14px; } | |
| .message.bot th, .message.bot td { border: 1px solid var(--border); padding: 8px 14px; text-align: left; vertical-align: top; } | |
| .message.bot th { background: var(--bg-muted); font-weight: 600; } | |
| .message.bot code { font-family: var(--font-mono); font-size: 0.88em; } | |
| .message.bot :not(pre) > code { background: var(--bg-muted); padding: 2px 6px; border-radius: 5px; color: var(--text); } | |
| .hy-codeblock { margin: 1em 0; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--bg-soft); } | |
| .hy-codeblock-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 9px 7px 15px; background: var(--bg-muted); border-bottom: 1px solid var(--border); font-size: 12px; line-height: 1; color: var(--text-muted); user-select: none; } | |
| .hy-codeblock-lang { text-transform: lowercase; letter-spacing: 0.02em; font-weight: 500; } | |
| .hy-codeblock-lang:empty::before { content: "text"; opacity: 0.7; } | |
| .hy-codeblock pre { margin: 0; border: none; border-radius: 0; background: transparent; padding: 13px 16px; overflow-x: auto; font-size: 13px; line-height: 1.65; } | |
| .code-copy-btn { flex-shrink: 0; width: 26px; height: 26px; padding: 0; display: inline-flex; align-items: center; justify-content: center; color: inherit; background: transparent; border: none; border-radius: 7px; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; } | |
| .code-copy-btn svg { display: block; width: 14px; height: 14px; } | |
| .code-copy-btn:hover { background: var(--tc-blue-060); color: var(--tc-blue); } | |
| .code-copy-btn.copied, .code-copy-btn.copied:hover { color: var(--tc-teal); background: transparent; } | |
| .html-preview-btn { display: inline-flex; align-items: center; background: none; border: none; padding: 2px 0; margin: 2px 0 6px; cursor: pointer; font-size: 0; line-height: 1; transition: opacity 0.15s ease; } | |
| .html-preview-btn::before { content: "▶ html preview"; font-size: 12px; color: var(--tc-blue); font-weight: 400; font-family: var(--font-sans); } | |
| .html-preview-btn:hover { opacity: 0.7; } | |
| /* ── Reasoning (thinking) block ───────────────────────────────────────── */ | |
| .thinking-block { background: none; border: none; padding: 0; margin: 6px 0 12px; } | |
| .thinking-block summary { cursor: pointer; font-size: 13px; font-weight: 400; color: var(--text-muted); padding: 0; list-style: none; display: inline-flex; align-items: center; gap: 4px; } | |
| .thinking-block summary:hover { color: var(--text); } | |
| .thinking-block summary::-webkit-details-marker { display: none; } | |
| .thinking-block summary::after { content: " ›"; font-size: 13px; color: var(--border-strong); } | |
| .thinking-block[open] summary::after { content: " ⌄"; } | |
| .thinking-block[open] summary { margin-bottom: 8px; } | |
| .thinking-block[open] > :not(summary) { font-size: 13px; color: var(--text-muted); line-height: 1.7; padding: 12px 16px; background: var(--bg-soft); border-radius: 8px; max-height: 300px; overflow-y: auto; border-left: 2px solid var(--tc-blue); white-space: pre-wrap; word-break: break-word; } | |
| /* ── Tool-call display block ─────────────────────────────────────────── */ | |
| .hy-tool-calls { margin-top: 10px; } | |
| .hy-tool-call + .hy-tool-call { margin-top: 8px; } | |
| /* ── Typing indicator ─────────────────────────────────────────────────── */ | |
| .typing-indicator { display: flex; align-items: center; gap: 5px; padding: 4px 0; height: 24px; } | |
| .typing-indicator span { width: 8px; height: 8px; background: var(--text-muted); border-radius: 50%; animation: typing-bounce 1.4s infinite ease-in-out both; } | |
| .typing-indicator span:nth-child(1) { animation-delay: 0s; } | |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes typing-bounce { 0%, 80%, 100% { transform: scale(0.5); opacity: 0.35; } 40% { transform: scale(1); opacity: 1; } } | |
| /* ── Empty-state examples overlay ─────────────────────────────────────── */ | |
| #examples-overlay { | |
| position: absolute; inset: 0; z-index: 5; | |
| display: flex; align-items: center; justify-content: center; | |
| background: var(--bg); overflow-y: auto; padding: 40px 20px 160px; | |
| transition: opacity 0.25s ease; | |
| } | |
| #examples-overlay.hidden { opacity: 0; pointer-events: none; } | |
| .examples-wrapper { display: flex; flex-direction: column; align-items: center; max-width: 1020px; } | |
| .examples-hero { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 22px; } | |
| .examples-hero img { width: auto; height: 56px; border-radius: 8px; object-fit: contain; } | |
| .examples-heading { font-size: 28px; font-weight: 650; color: var(--text-strong); margin: 0 0 30px; letter-spacing: -0.02em; text-align: center; } | |
| .examples-grid { display: grid; grid-template-columns: minmax(0, 460px) minmax(0, 460px); column-gap: 20px; row-gap: 14px; justify-content: center; width: min(1020px, 100%); } | |
| .example-btn:nth-child(1), .example-btn:nth-child(4) { grid-column: 1 / span 2; justify-self: center; } | |
| .example-btn:nth-child(2) { grid-column: 1; justify-self: end; } | |
| .example-btn:nth-child(3) { grid-column: 2; justify-self: start; } | |
| .example-btn { | |
| display: flex; align-items: center; justify-content: center; | |
| width: 460px; height: 72px; overflow: hidden; | |
| background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--radius); | |
| padding: 14px 20px; font-size: 14px; line-height: 1.5; color: var(--text); | |
| cursor: pointer; user-select: none; text-align: center; | |
| transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease, background 0.2s ease; | |
| } | |
| .example-btn:hover { border-color: var(--border-strong); color: var(--text-strong); box-shadow: var(--shadow-md); transform: translateY(-1px); background: var(--bg-elev); } | |
| .example-btn-text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; width: 100%; } | |
| @media (max-width: 1020px) { | |
| .examples-grid { grid-template-columns: minmax(0, 1fr); } | |
| .example-btn:nth-child(n) { grid-column: 1; justify-self: center; } | |
| .example-btn { width: 100%; max-width: 520px; } | |
| } | |
| /* ── Composer (floating, centered pill) ──────────────────────────────── | |
| Sits at the bottom of the chat canvas, centered to the conversation | |
| column, with a gradient fade above it so content scrolls under it | |
| gracefully — the Claude.ai feel. */ | |
| #composer { | |
| position: absolute; left: 0; right: 0; bottom: 0; | |
| display: flex; align-items: flex-end; justify-content: center; gap: 10px; | |
| padding: 0 clamp(16px, 5vw, 48px) 20px; | |
| z-index: 10; | |
| background: linear-gradient(to top, var(--bg) 52%, rgba(247,246,243,0)); | |
| pointer-events: none; /* let the fade be click-through … */ | |
| } | |
| #composer > * { pointer-events: auto; } /* … but not the controls */ | |
| html.dark #composer { background: linear-gradient(to top, var(--bg) 52%, rgba(26,26,26,0)); } | |
| .composer-inner { | |
| display: flex; align-items: flex-end; gap: 8px; width: 100%; max-width: var(--col-max); | |
| background: var(--bg-elev); border: 1px solid var(--border); | |
| border-radius: var(--radius-xl); padding: 8px 8px 8px 18px; | |
| box-shadow: var(--shadow-md); transition: border-color 0.15s ease, box-shadow 0.15s ease; | |
| } | |
| .composer-inner:focus-within { border-color: var(--border-strong); box-shadow: var(--shadow-md), var(--ring); } | |
| #composer textarea { | |
| flex: 1; min-height: 44px; max-height: 200px; | |
| resize: none; overflow-y: auto; | |
| border: none; border-radius: 0; | |
| padding: 12px 0; font-size: 15.5px; line-height: 1.55; background: transparent; | |
| color: var(--text); box-shadow: none; transition: none; | |
| } | |
| #composer textarea:focus { outline: none; box-shadow: none; } | |
| #new-chat-btn { | |
| width: 40px; height: 40px; border-radius: 50%; padding: 0; | |
| display: flex; align-items: center; justify-content: center; flex-shrink: 0; | |
| cursor: pointer; border: 1px solid transparent; background: transparent; color: var(--text-muted); | |
| transition: background 0.15s ease, color 0.15s ease; | |
| } | |
| #new-chat-btn:hover { background: var(--bg-muted); color: var(--text); } | |
| #new-chat-btn svg { width: 19px; height: 19px; } | |
| #send-btn { | |
| width: 40px; height: 40px; border-radius: 50%; padding: 0; | |
| display: flex; align-items: center; justify-content: center; flex-shrink: 0; | |
| cursor: pointer; border: none; background: var(--tc-blue); color: #fff; | |
| transition: background 0.15s ease, transform 0.1s ease; | |
| } | |
| #send-btn:hover { background: var(--tc-blue-hover); } | |
| #send-btn:active { transform: scale(0.95); } | |
| #send-btn:disabled { background: var(--border-strong); cursor: not-allowed; opacity: 0.6; } | |
| #send-btn svg { width: 18px; height: 18px; } | |
| /* ── Tool-call input panel ───────────────────────────────────────────── */ | |
| #tool-area { | |
| position: absolute; bottom: 104px; left: 50%; transform: translateX(-50%); | |
| width: min(640px, 90%); z-index: 30; | |
| background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); | |
| padding: 16px; box-shadow: var(--shadow-lg); display: none; flex-direction: column; gap: 10px; | |
| } | |
| #tool-area.visible { display: flex; } | |
| #tool-area .tool-header { font-size: 13px; font-weight: 600; color: var(--text); } | |
| #tool-area .tool-markdown { font-size: 13px; line-height: 1.6; color: var(--text); max-height: 160px; overflow-y: auto; background: var(--bg-soft); border-radius: 8px; padding: 10px 12px; } | |
| #tool-area .tool-markdown p { margin: 0.4em 0; } | |
| #tool-area .tool-markdown code { background: var(--bg-muted); padding: 1px 5px; border-radius: 4px; font-family: var(--font-mono); font-size: 12px; } | |
| #tool-area .tool-markdown pre { background: var(--bg-muted); border-radius: 6px; padding: 8px 10px; overflow-x: auto; margin: 0.4em 0; } | |
| #tool-area .tool-markdown pre code { background: none; padding: 0; } | |
| #tool-area .tool-markdown strong { font-weight: 600; } | |
| #tool-result-input { | |
| width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; | |
| background: var(--bg); color: var(--text); font-size: 14px; font-family: var(--font-mono); | |
| } | |
| #tool-result-input:focus { outline: none; border-color: var(--tc-blue); box-shadow: 0 0 0 3px var(--tc-blue-120); } | |
| #tool-submit-btn { align-self: flex-end; padding: 8px 18px; border: none; border-radius: 8px; background: var(--tc-blue); color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; } | |
| #tool-submit-btn:hover { background: var(--tc-blue-hover); } | |
| /* ── HTML preview modal ───────────────────────────────────────────────── */ | |
| .html-preview-overlay { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.2s ease; } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| .html-preview-modal { width: 90vw; height: 85vh; background: var(--bg); border-radius: var(--radius); box-shadow: var(--shadow-lg); display: flex; flex-direction: column; overflow: hidden; animation: modalSlideUp 0.25s ease; } | |
| @keyframes modalSlideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } | |
| .html-preview-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--bg-soft); flex-shrink: 0; } | |
| .html-preview-title { font-size: 14px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; } | |
| .html-preview-title::before { content: "⬡"; color: var(--tc-blue); } | |
| .html-preview-close { background: none; border: none; font-size: 20px; color: var(--text-muted); cursor: pointer; padding: 4px 8px; border-radius: 6px; line-height: 1; transition: all 0.15s ease; } | |
| .html-preview-close:hover { background: var(--tc-blue-060); color: var(--tc-red); } | |
| .html-preview-iframe { flex: 1; border: none; width: 100%; background: var(--bg); } | |
| /* ── Toast ────────────────────────────────────────────────────────────── */ | |
| #toast-host { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); z-index: 9999; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } | |
| .toast { background: #1f2937; color: #fff; padding: 10px 16px; border-radius: 8px; box-shadow: var(--shadow-lg); font-size: 14px; max-width: min(92vw, 520px); line-height: 1.4; opacity: 0; transition: opacity 180ms ease; } | |
| .toast.show { opacity: 1; } | |
| .toast.error { background: var(--tc-red); } | |
| /* ── Sidebar toggle ───────────────────────────────────────────────────── | |
| One class (``#app.sidebar-closed``) drives collapse on every breakpoint: | |
| desktop collapses the grid column to 0; mobile slides the fixed overlay | |
| out. The header toggle is always visible so users can re-open it. */ | |
| #sidebar-toggle { display: inline-flex; } | |
| @media (max-width: 860px) { | |
| #app { grid-template-columns: 1fr; grid-template-areas: "header" "main"; } | |
| #sidebar { | |
| position: fixed; top: 60px; bottom: 0; left: 0; width: 86%; max-width: 360px; | |
| z-index: 50; box-shadow: var(--shadow-lg); | |
| } | |
| /* On mobile the sidebar is an overlay; show it unless explicitly closed. */ | |
| #app.sidebar-closed #sidebar { transform: translateX(-100%); } | |
| .examples-hero img { height: 46px; } | |
| .examples-heading { font-size: 22px; } | |
| } | |
| /* ── Scrollbars (slim & subtle) ──────────────────────────────────────── */ | |
| ::-webkit-scrollbar { width: 10px; height: 10px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; border: 3px solid transparent; background-clip: padding-box; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: padding-box; } | |
| * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } | |
| /* KaTeX: keep its own math fonts and reset inherited typography so SVG | |
| stretchy operators don't get distorted by the bubble's line-height. */ | |
| .message.bot .katex { font: normal 1.21em KaTeX_Main, "Times New Roman", serif ; text-indent: 0 ; } | |
| .message.bot .katex, .message.bot .katex *, .message.bot .katex-display, .message.bot .katex-display * { letter-spacing: normal ; word-spacing: normal ; line-height: 1.2 ; } | |
| .message.bot .katex, .message.bot .katex-display { color: var(--text) ; } | |
| .message.bot .katex svg { fill: currentColor; stroke: none; } | |
| .message.bot .katex-display { margin: 1em 0; padding: 0.4em 0; overflow-x: auto; overflow-y: hidden; } | |
| .message.bot .katex-display > .katex { text-align: center; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app" class="sidebar-closed"> | |
| <!-- ── Header ────────────────────────────────────────────────────── --> | |
| <header id="header"> | |
| <button id="sidebar-toggle" class="icon-btn" title="Menu" aria-label="Toggle sidebar"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg> | |
| </button> | |
| <div class="brand"> | |
| <img class="brand-logo" src="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" alt="Tencent Hy3 logo" /> | |
| <span class="brand-name">Hy3</span> | |
| <span class="brand-sep">·</span> | |
| <span class="brand-tag">Chat Demo</span> | |
| <span class="brand-badge">Tencent</span> | |
| </div> | |
| <div class="header-spacer"></div> | |
| <button id="theme-btn" class="icon-btn" title="Toggle theme" aria-label="Toggle theme"> | |
| <svg id="theme-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg> | |
| </button> | |
| </header> | |
| <!-- ── Sidebar ──────────────────────────────────────────────────── --> | |
| <aside id="sidebar"> | |
| <p class="sidebar-notice" id="sidebar-notice"></p> | |
| <div class="field"> | |
| <label data-i18n="sidebar.think_level"></label> | |
| <select id="think-level"> | |
| <option value="high">high</option> | |
| <option value="low">low</option> | |
| <option value="no_think">no_think</option> | |
| </select> | |
| <span class="info" data-i18n="sidebar.think_level.info"></span> | |
| </div> | |
| <div class="field"> | |
| <label data-i18n="sidebar.system_prompt"></label> | |
| <textarea id="system-prompt" rows="3" data-i18n-ph="sidebar.system_prompt.placeholder"></textarea> | |
| </div> | |
| <div class="field"> | |
| <label data-i18n="sidebar.temperature"></label> | |
| <div class="range-row"> | |
| <input type="range" id="temperature" min="0" max="1" step="0.01" value="0.9" disabled /> | |
| <input type="number" class="range-num" id="temperature-num" value="0.9" min="0" max="1" step="0.01" disabled /> | |
| </div> | |
| <label class="check-row"><input type="checkbox" id="temperature-use-default" checked /><span data-i18n="sidebar.temperature.use_default"></span></label> | |
| <span class="info" data-i18n="sidebar.temperature.info"></span> | |
| </div> | |
| <div class="field"> | |
| <label data-i18n="sidebar.max_tokens"></label> | |
| <div class="range-row"> | |
| <input type="range" id="max-tokens" min="0" max="65536" step="1" value="0" /> | |
| <input type="number" class="range-num" id="max-tokens-num" value="0" min="0" max="65536" step="1" /> | |
| </div> | |
| <span class="info" data-i18n="sidebar.max_tokens.info"></span> | |
| </div> | |
| <div class="field"> | |
| <label data-i18n="sidebar.top_p"></label> | |
| <div class="range-row"> | |
| <input type="range" id="top-p" min="0" max="1" step="0.01" value="0" /> | |
| <input type="number" class="range-num" id="top-p-num" value="0" min="0" max="1" step="0.01" /> | |
| </div> | |
| <span class="info" data-i18n="sidebar.top_p.info"></span> | |
| </div> | |
| <div class="field"> | |
| <label data-i18n="sidebar.preserved_thinking"></label> | |
| <label class="check-row"><input type="checkbox" id="preserved-thinking-use-default" checked /><span data-i18n="sidebar.preserved_thinking.use_default"></span></label> | |
| <label class="check-row"><input type="checkbox" id="preserved-thinking" /><span data-i18n="sidebar.preserved_thinking" style="font-weight:500;color:var(--text)"></span></label> | |
| <span class="info" data-i18n="sidebar.preserved_thinking.use_default.info"></span> | |
| </div> | |
| <details class="accordion"> | |
| <summary data-i18n="sidebar.functions"></summary> | |
| <div class="accordion-body"> | |
| <textarea id="functions-json" spellcheck="false" placeholder=""></textarea> | |
| <button class="tiny-btn" id="validate-fn-btn" data-i18n="sidebar.validate_btn"></button> | |
| <div class="fn-msg" id="fn-msg"></div> | |
| </div> | |
| </details> | |
| </aside> | |
| <!-- ── Main chat ────────────────────────────────────────────────── --> | |
| <main id="main"> | |
| <div id="chat" role="log" aria-live="polite"></div> | |
| <div id="examples-overlay"> | |
| <div class="examples-wrapper"> | |
| <div class="examples-hero"> | |
| <img src="https://huggingface.co/tencent/Hy3/resolve/main/assets/logo-en.png" alt="" /> | |
| </div> | |
| <p class="examples-heading" data-i18n="examples_heading"></p> | |
| <div class="examples-grid" id="examples-grid"></div> | |
| </div> | |
| </div> | |
| <div id="tool-area"> | |
| <div class="tool-header" id="tool-header"></div> | |
| <div class="tool-markdown" id="tool-markdown"></div> | |
| <input type="text" id="tool-result-input" /> | |
| <button id="tool-submit-btn" data-i18n="tool.submit"></button> | |
| </div> | |
| </main> | |
| <!-- ── Composer (floating pill) ─────────────────────────────────── --> | |
| <div id="composer"> | |
| <div class="composer-inner"> | |
| <button id="new-chat-btn" title="New chat" aria-label="New chat"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg> | |
| </button> | |
| <textarea id="msg-input" rows="1" data-i18n-ph="msg_placeholder"></textarea> | |
| <button id="send-btn" title="Send" aria-label="Send"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toast-host"></div> | |
| <script type="module"> | |
| /* ─────────────────────────────────────────────────────────────────── | |
| Hy3 Tencent-branded frontend. | |
| Goal: only the UI + transport changed from the gr.Blocks app. The chat | |
| behaviour is identical, so this file is a faithful port of the old | |
| static/chat.js (the delta-op renderer: append-only bubbles, frozen / | |
| streaming split so KaTeX+marked+hljs run once per closed block, seq | |
| de-dup, epoch staleness, preprocess_latex'd content deltas, tool-call | |
| markdown) plus static/app.js (the busy-marker watchdog, follow-the-tail | |
| scroll lock with user-gesture pause, examples overlay, code-block | |
| chrome with copy + html preview). | |
| The ONLY adaptation is the transport: instead of a MutationObserver | |
| watching a hidden #hy-chat-delta gr.HTML component, we consume the | |
| @gradio/client async-iterator frames from server.py and feed each JSON | |
| delta payload straight into the same applyDelta() the old renderer used. | |
| The busy marker is no longer a hidden gr.HTML span; it's a client-side | |
| flag toggled by the stream's first frame + completion + the watchdog. | |
| ─────────────────────────────────────────────────────────────────── */ | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.7.1/dist/index.min.js"; | |
| (() => { | |
| "use strict"; | |
| if (window.__HY_APP_INIT) return; | |
| window.__HY_APP_INIT = true; | |
| // ─── i18n + examples (loaded from /config endpoint) ────────────── | |
| // Full English default table so every label is populated on first paint | |
| // — before /config round-trips. /config then overrides with the active | |
| // locale (en/zh). Keep these in sync with i18n.py. | |
| const I18N = { | |
| "title": "Hy3 Chat Demo", | |
| "title.notice_html": "The current demo version is offline; full-featured capabilities will be available on the Tencent HY official website.", | |
| "examples_heading": "What can I help you with?", | |
| "msg_placeholder": "Type a message...", | |
| "sidebar.think_level": "Think level", | |
| "sidebar.think_level.info": "Control reasoning depth", | |
| "sidebar.system_prompt": "System prompt", | |
| "sidebar.system_prompt.placeholder": "You are a helpful AI assistant...", | |
| "sidebar.temperature": "Temperature", | |
| "sidebar.temperature.info": "Higher values produce more random outputs", | |
| "sidebar.temperature.use_default": "Use model default", | |
| "sidebar.max_tokens": "Max output tokens", | |
| "sidebar.max_tokens.info": "Maximum tokens per response. Set to 0 to use the model's default.", | |
| "sidebar.top_p": "Top P", | |
| "sidebar.top_p.info": "Nucleus sampling probability threshold. Set to 0 to use the model's default.", | |
| "sidebar.preserved_thinking": "Preserved thinking", | |
| "sidebar.preserved_thinking.info": "When enabled, the model can retain reasoning content from previous assistant turns in the context, helping maintain reasoning continuity and conversation integrity while improving model performance.", | |
| "sidebar.preserved_thinking.use_default": "Preserved thinking — Use model default", | |
| "sidebar.preserved_thinking.use_default.info": "We recommend using \"Use model default\", which automatically enables this option for tool call workflows and disables it for pure text conversations.", | |
| "sidebar.functions": "🔧 Functions", | |
| "sidebar.functions.label": "Function definitions (JSON array)", | |
| "sidebar.validate_btn": "Validate & Format", | |
| "tool.result_label": "Function result", | |
| "tool.result_placeholder": "Enter function return value... (Enter to submit)", | |
| "tool.submit": "Submit", | |
| "tool.call_header": "Function call ({i}/{n})", | |
| "tool.call_label": "🔧 Call function", | |
| "display.thinking": "Thinking...", | |
| "display.thinking_done": "Thinking complete", | |
| "display.code_copy": "Copy", | |
| "display.code_copied": "Copied", | |
| "warn.empty_msg": "Please enter a message", | |
| "warn.invalid_fn_json": "Invalid function JSON, please fix or clear before sending: {err}", | |
| "warn.request_failed": "Request failed, please retry or adjust parameters", | |
| "warn.model_busy": "Model is still responding. Please wait for it to finish before sending a new message.", | |
| "info.new_chat": "Start a new chat", | |
| // short aliases used directly in code below | |
| "thinking": "Thinking...", | |
| "thinking_done": "Thinking complete", | |
| "code_copy": "Copy", | |
| "code_copied": "Copied", | |
| "model_busy": "Model is still responding. Please wait for it to finish before sending a new message.", | |
| "empty_msg": "Please enter a message", | |
| "request_failed": "Request failed, please retry or adjust parameters", | |
| "new_chat": "Start a new chat", | |
| }; | |
| let EXAMPLES = []; | |
| let TOOL_CALL_LABEL = "🔧 Call function"; | |
| // ─── session id (fresh per page load) ───────────────────────────── | |
| // Minting a NEW id on every load (rather than reusing one from | |
| // sessionStorage) makes a browser refresh start a blank conversation: | |
| // the server keys its in-memory ChatState off this id, so a fresh id | |
| // means a fresh, empty state and the prior turn's context never leaks | |
| // into the next message. sessionStorage used to make the id survive F5 | |
| // — which is exactly what surfaced stale history after a refresh. The | |
| // variable persists for the lifetime of this loaded page (across | |
| // @gradio/client reconnects), so no storage is needed. | |
| let SESSION_ID = "s-" + Math.random().toString(36).slice(2) + Date.now().toString(36); | |
| // ─── DOM refs ───────────────────────────────────────────────────── | |
| const $ = (id) => document.getElementById(id); | |
| const chatEl = $("chat"); | |
| const overlayEl = $("examples-overlay"); | |
| const msgInput = $("msg-input"); | |
| const sendBtn = $("send-btn"); | |
| const newChatBtn = $("new-chat-btn"); | |
| const toolArea = $("tool-area"); | |
| const toolMarkdown = $("tool-markdown"); | |
| const toolResultInput = $("tool-result-input"); | |
| const toolSubmitBtn = $("tool-submit-btn"); | |
| // ─── streaming state ────────────────────────────────────────────── | |
| let isStreaming = false; | |
| let currentJob = null; // the active async-iterator job | |
| let pendingToolCalls = []; // tool calls awaiting user results | |
| let currentEpoch = null; // staleness guard for in-flight frames | |
| // How many tool results the user has submitted in the current tool-call | |
| // round, so the panel header shows "Function call (2/3)" correctly. | |
| let toolResultsSubmitted = 0; | |
| let toolRoundTotal = 0; | |
| /* ─── apply i18n to the DOM ──────────────────────────────────────── */ | |
| function applyI18n(table) { | |
| Object.assign(I18N, table || {}); | |
| document.querySelectorAll("[data-i18n]").forEach((el) => { | |
| const key = el.getAttribute("data-i18n"); | |
| if (I18N[key] != null) el.textContent = I18N[key]; | |
| }); | |
| document.querySelectorAll("[data-i18n-ph]").forEach((el) => { | |
| const key = el.getAttribute("data-i18n-ph"); | |
| if (I18N[key] != null) el.placeholder = I18N[key]; | |
| }); | |
| document.title = (I18N["title"] || "Hy3") + " · Tencent"; | |
| $("sidebar-notice").textContent = I18N["title.notice_html"] || ""; | |
| $("msg-input").placeholder = I18N["msg_placeholder"] || "Type a message..."; | |
| if (I18N["tool.call_label"]) TOOL_CALL_LABEL = I18N["tool.call_label"]; | |
| } | |
| /* ─── theme toggle ───────────────────────────────────────────────── */ | |
| function setTheme(dark) { | |
| document.documentElement.classList.toggle("dark", dark); | |
| localStorage.setItem("hy3-theme", dark ? "dark" : "light"); | |
| const lightCss = document.getElementById("hljs-light-css"); | |
| const darkCss = document.getElementById("hljs-dark-css"); | |
| if (lightCss && darkCss) { | |
| lightCss.disabled = dark; | |
| darkCss.disabled = !dark; | |
| } | |
| } | |
| const savedTheme = localStorage.getItem("hy3-theme"); | |
| setTheme(savedTheme ? savedTheme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches); | |
| $("theme-btn").addEventListener("click", () => | |
| setTheme(!document.documentElement.classList.contains("dark")) | |
| ); | |
| /* ─── sidebar toggle ─────────────────────────────────────────────── | |
| One class (``sidebar-closed``) drives collapse on every breakpoint; | |
| the header button flips it, so the same control opens/closes the | |
| sidebar on both desktop and mobile. It starts closed by default | |
| (see the ``sidebar-closed`` class on #app). */ | |
| $("sidebar-toggle").addEventListener("click", () => { | |
| $("app").classList.toggle("sidebar-closed"); | |
| }); | |
| /* ─── markdown + math rendering ──────────────────────────────────── | |
| Ported from static/chat.js: two-stage pipeline so marked doesn't | |
| eat the LaTeX before KaTeX can see it. */ | |
| const MARKED_OPTS = { gfm: true, breaks: false }; | |
| const KATEX_DELIMS = [ | |
| { left: "$$", right: "$$", display: true }, | |
| { left: "$", right: "$", display: false }, | |
| ]; | |
| function normalizeMathDelims(text) { | |
| if (!text) return text; | |
| let out = "", i = 0; const n = text.length; | |
| while (i < n) { | |
| const ch = text[i]; | |
| if ((ch === "`" && text.startsWith("```", i)) || (ch === "~" && text.startsWith("~~~", i))) { | |
| const marker = text.substr(i, 3); out += marker; i += 3; | |
| const close = text.indexOf(marker, i); | |
| if (close < 0) { out += text.slice(i); return out; } | |
| out += text.slice(i, close + 3); i = close + 3; continue; | |
| } | |
| if (ch === "`") { | |
| let runLen = 1; while (i + runLen < n && text[i + runLen] === "`") runLen++; | |
| const open = text.substr(i, runLen); out += open; i += runLen; | |
| const close = text.indexOf(open, i); | |
| if (close < 0) { out += text.slice(i); return out; } | |
| out += text.slice(i, close + runLen); i = close + runLen; continue; | |
| } | |
| if (ch === "\\") { | |
| if (text[i + 1] === "[") { out += "$$"; i += 2; continue; } | |
| if (text[i + 1] === "]") { out += "$$"; i += 2; continue; } | |
| if (text[i + 1] === "(") { out += "$"; i += 2; continue; } | |
| if (text[i + 1] === ")") { out += "$"; i += 2; continue; } | |
| } | |
| out += ch; i++; | |
| } | |
| return out; | |
| } | |
| const MATH_RE = /\$\$[\s\S]+?\$\$|\$(?=[^\s$])[^\n$]*?(?<=[^\s$])\$/g; | |
| function protectMath(text) { | |
| const stash = []; | |
| const escaped = text.replace(MATH_RE, (m) => { | |
| const idx = stash.length; stash.push(m); return " |