| <!doctype html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <link rel="icon" href="data:," /> |
| <title>DiffusionGemma Chat · 中文界面</title> |
| <style> |
| :root { |
| color-scheme: light; |
| --bg: #fafafa; |
| --surface: #ffffff; |
| --surface-soft: #f5f5f5; |
| --surface-raised: #fbfbfb; |
| --ink: #171717; |
| --muted: #737373; |
| --line: #e5e5e5; |
| --line-strong: #d4d4d4; |
| --accent: #171717; |
| --accent-strong: #000000; |
| --accent-soft: #f3f4f6; |
| --indigo: #525252; |
| --warn: #a16207; |
| --bad: #dc2626; |
| --good: #2563eb; |
| --code: #f5f5f5; |
| --ring: rgba(23, 23, 23, 0.18); |
| --shadow: 0 1px 2px rgba(0, 0, 0, 0.06); |
| --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.04); |
| --header-bg: rgba(255, 255, 255, 0.92); |
| --link: #404040; |
| --subtle-ink: #525252; |
| --primary-ink: #ffffff; |
| --badge-bg: rgba(255, 255, 255, 0.78); |
| --good-bg: #eff6ff; |
| --good-border: rgba(37, 99, 235, 0.22); |
| --warn-bg: #fffbeb; |
| --warn-border: rgba(161, 98, 7, 0.22); |
| --bad-bg: #fef2f2; |
| --bad-border: rgba(220, 38, 38, 0.22); |
| --user-bubble: #f4f4f5; |
| --user-bubble-border: #e4e4e7; |
| --assistant-bubble: #ffffff; |
| --composer-bg: rgba(255, 255, 255, 0.96); |
| --composer-panel-bg: rgba(245, 245, 245, 0.82); |
| --composer-panel-border: rgba(212, 212, 212, 0.72); |
| --pill-bg: rgba(255, 255, 255, 0.7); |
| --pill-text: #404040; |
| } |
| |
| :root[data-theme="dark"] { |
| color-scheme: dark; |
| --bg: #141414; |
| --surface: #1f1f1f; |
| --surface-soft: #2a2a2a; |
| --surface-raised: #232323; |
| --ink: #f5f5f5; |
| --muted: #a3a3a3; |
| --line: rgba(255, 255, 255, 0.14); |
| --line-strong: rgba(255, 255, 255, 0.26); |
| --accent: #ededed; |
| --accent-strong: #ffffff; |
| --accent-soft: #262626; |
| --indigo: #737373; |
| --warn: #fbbf24; |
| --bad: #f87171; |
| --good: #93c5fd; |
| --code: #242424; |
| --ring: rgba(245, 245, 245, 0.22); |
| --shadow: 0 1px 2px rgba(0, 0, 0, 0.28); |
| --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.22); |
| --header-bg: rgba(20, 20, 20, 0.88); |
| --link: #d4d4d4; |
| --subtle-ink: #d4d4d4; |
| --primary-ink: #171717; |
| --badge-bg: rgba(31, 31, 31, 0.78); |
| --good-bg: rgba(59, 130, 246, 0.14); |
| --good-border: rgba(147, 197, 253, 0.28); |
| --warn-bg: rgba(245, 158, 11, 0.12); |
| --warn-border: rgba(251, 191, 36, 0.26); |
| --bad-bg: rgba(239, 68, 68, 0.13); |
| --bad-border: rgba(248, 113, 113, 0.28); |
| --user-bubble: #2a2a2a; |
| --user-bubble-border: rgba(255, 255, 255, 0.12); |
| --assistant-bubble: #1f1f1f; |
| --composer-bg: rgba(31, 31, 31, 0.94); |
| --composer-panel-bg: rgba(38, 38, 38, 0.86); |
| --composer-panel-border: rgba(255, 255, 255, 0.16); |
| --pill-bg: rgba(20, 20, 20, 0.7); |
| --pill-text: #d4d4d4; |
| } |
| |
| * { box-sizing: border-box; } |
| |
| html, |
| body { |
| margin: 0; |
| min-height: 100%; |
| width: 100%; |
| overflow-x: hidden; |
| background: var(--bg); |
| color: var(--ink); |
| font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| line-height: 1.5; |
| } |
| |
| button, |
| textarea, |
| input, |
| select { |
| font: inherit; |
| } |
| |
| button { |
| border: 1px solid var(--line); |
| border-radius: 8px; |
| background: var(--surface); |
| color: var(--ink); |
| min-height: 36px; |
| padding: 8px 12px; |
| font-weight: 650; |
| cursor: pointer; |
| transition: background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease, color 150ms ease; |
| } |
| |
| button:hover { |
| background: var(--surface-soft); |
| border-color: var(--line-strong); |
| box-shadow: var(--shadow); |
| } |
| |
| button:focus-visible, |
| textarea:focus-visible, |
| input:focus-visible, |
| select:focus-visible { |
| outline: 2px solid var(--ring); |
| outline-offset: 2px; |
| } |
| |
| button:disabled { |
| opacity: 0.55; |
| cursor: not-allowed; |
| box-shadow: none; |
| } |
| |
| button.primary { |
| background: var(--accent); |
| border-color: var(--accent); |
| color: var(--primary-ink); |
| box-shadow: var(--shadow); |
| } |
| |
| button.primary:hover { background: var(--accent-strong); } |
| |
| header { |
| border-bottom: 1px solid var(--line); |
| background: var(--header-bg); |
| backdrop-filter: blur(16px); |
| position: sticky; |
| top: 0; |
| z-index: 20; |
| } |
| |
| .topbar { |
| width: min(1440px, calc(100vw - 32px)); |
| margin: 0 auto; |
| display: grid; |
| grid-template-columns: minmax(0, 1fr) minmax(300px, auto); |
| gap: 24px; |
| align-items: center; |
| padding: 16px 0; |
| min-width: 0; |
| } |
| |
| h1 { |
| margin: 0; |
| font-size: 21px; |
| line-height: 1.2; |
| letter-spacing: 0; |
| font-weight: 700; |
| } |
| |
| h2 { |
| margin: 0; |
| font-size: 15px; |
| line-height: 1.2; |
| letter-spacing: 0; |
| } |
| |
| .brand-copy { |
| min-width: 0; |
| } |
| |
| .eyebrow { |
| display: inline-flex; |
| align-items: center; |
| border: 1px solid var(--line); |
| border-radius: 999px; |
| background: var(--surface-soft); |
| color: var(--subtle-ink); |
| font-size: 12px; |
| font-weight: 700; |
| letter-spacing: 0; |
| margin-bottom: 8px; |
| padding: 3px 8px; |
| text-transform: uppercase; |
| } |
| |
| .model-line { |
| margin-top: 5px; |
| color: var(--muted); |
| font-size: 13px; |
| overflow-wrap: anywhere; |
| } |
| |
| .source-row { |
| display: grid; |
| grid-template-columns: auto minmax(0, 1fr); |
| gap: 8px; |
| align-items: baseline; |
| margin-top: 7px; |
| max-width: 920px; |
| } |
| |
| .source-label { |
| color: var(--muted); |
| font-size: 12px; |
| font-weight: 800; |
| white-space: nowrap; |
| } |
| |
| .model-source { |
| color: var(--link); |
| font-size: 12px; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| text-decoration: none; |
| min-width: 0; |
| } |
| |
| .model-source:hover { |
| text-decoration: underline; |
| } |
| |
| .top-actions { |
| display: grid; |
| gap: 10px; |
| justify-items: end; |
| min-width: 0; |
| } |
| |
| .language-switch, |
| .theme-switch { |
| border: 1px solid var(--line); |
| border-radius: 8px; |
| background: var(--surface-soft); |
| display: inline-grid; |
| padding: 3px; |
| gap: 3px; |
| } |
| |
| .language-switch { |
| grid-template-columns: 1fr 1fr; |
| } |
| |
| .theme-switch { |
| grid-template-columns: repeat(3, 1fr); |
| } |
| |
| .panel-toggle { |
| justify-self: end; |
| min-width: 108px; |
| } |
| |
| .language-switch button, |
| .theme-switch button { |
| border: 0; |
| min-height: 30px; |
| padding: 5px 10px; |
| background: transparent; |
| color: var(--muted); |
| } |
| |
| .language-switch button.active, |
| .theme-switch button.active { |
| background: var(--surface); |
| color: var(--ink); |
| box-shadow: var(--shadow-soft); |
| } |
| |
| .badge-row { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| justify-content: flex-end; |
| min-width: 0; |
| max-width: 100%; |
| } |
| |
| .badge { |
| border: 1px solid var(--line); |
| border-radius: 999px; |
| background: var(--badge-bg); |
| padding: 5px 9px; |
| font-size: 12px; |
| color: var(--muted); |
| white-space: nowrap; |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| max-width: 100%; |
| } |
| |
| .badge::before { |
| content: ""; |
| width: 6px; |
| height: 6px; |
| border-radius: 50%; |
| background: currentColor; |
| opacity: 0.72; |
| } |
| |
| .badge.good { |
| color: var(--good); |
| border-color: var(--good-border); |
| background: var(--good-bg); |
| } |
| |
| .badge.warn { |
| color: var(--warn); |
| border-color: var(--warn-border); |
| background: var(--warn-bg); |
| } |
| |
| .badge.bad { |
| color: var(--bad); |
| border-color: var(--bad-border); |
| background: var(--bad-bg); |
| } |
| |
| main { |
| width: min(1440px, calc(100vw - 32px)); |
| margin: 0 auto; |
| padding: 20px 0 32px; |
| } |
| |
| .app-grid { |
| display: grid; |
| grid-template-columns: minmax(0, 1fr) minmax(330px, 368px); |
| gap: 16px; |
| align-items: start; |
| } |
| |
| .app-grid.panel-collapsed { |
| grid-template-columns: minmax(0, 1fr); |
| } |
| |
| .app-grid.panel-collapsed .side-panel { |
| display: none; |
| } |
| |
| .chat-shell { |
| background: var(--surface); |
| border: 1px solid var(--line); |
| border-radius: 10px; |
| box-shadow: var(--shadow); |
| min-width: 0; |
| } |
| |
| .chat-shell { |
| min-height: calc(100vh - 118px); |
| display: grid; |
| grid-template-rows: auto minmax(360px, 1fr) auto; |
| overflow: hidden; |
| } |
| |
| .chat-head { |
| border-bottom: 1px solid var(--line); |
| padding: 15px 17px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 12px; |
| background: var(--surface); |
| } |
| |
| .chat-head p { |
| display: block; |
| margin: 4px 0 0; |
| color: var(--muted); |
| font-size: 13px; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .chat-actions { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| justify-content: flex-end; |
| } |
| |
| .timeline { |
| padding: 18px; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 14px; |
| background: var(--bg); |
| } |
| |
| .empty-state { |
| max-width: 640px; |
| margin: auto; |
| padding: 0 20px; |
| text-align: center; |
| } |
| |
| .empty-state strong { |
| display: block; |
| font-size: 24px; |
| line-height: 1.2; |
| margin-bottom: 10px; |
| } |
| |
| .empty-state span { |
| display: block; |
| color: var(--muted); |
| font-size: 14px; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .prompt-chips { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| margin-top: 18px; |
| justify-content: center; |
| } |
| |
| .prompt-chip { |
| min-height: 34px; |
| border-color: var(--line); |
| background: var(--surface-soft); |
| color: var(--ink); |
| font-size: 13px; |
| font-weight: 650; |
| } |
| |
| .message-row { |
| display: flex; |
| gap: 10px; |
| align-items: flex-start; |
| } |
| |
| .message-row.user { |
| justify-content: flex-end; |
| } |
| |
| .avatar { |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| display: grid; |
| place-items: center; |
| flex: 0 0 auto; |
| font-size: 12px; |
| font-weight: 700; |
| color: var(--primary-ink); |
| background: var(--indigo); |
| } |
| |
| .message-row.assistant .avatar { background: var(--accent); } |
| .message-row.error .avatar { background: var(--bad); } |
| .message-row.user .avatar { order: 2; background: var(--subtle-ink); } |
| |
| .bubble-wrap { |
| min-width: 0; |
| max-width: min(760px, 82%); |
| } |
| |
| .role-label { |
| color: var(--muted); |
| font-size: 12px; |
| margin: 0 0 4px; |
| } |
| |
| .message-row.user .role-label { |
| text-align: right; |
| } |
| |
| .bubble { |
| border: 1px solid var(--line); |
| border-radius: 12px; |
| background: var(--surface); |
| box-shadow: var(--shadow-soft); |
| padding: 12px 13px; |
| white-space: pre-wrap; |
| overflow-wrap: anywhere; |
| font-size: 14px; |
| } |
| |
| .message-row.user .bubble { |
| background: var(--user-bubble); |
| border-color: var(--user-bubble-border); |
| } |
| |
| .message-row.assistant .bubble { |
| background: var(--assistant-bubble); |
| } |
| |
| .message-row.error .bubble { |
| color: var(--bad); |
| border-color: var(--bad-border); |
| background: var(--bad-bg); |
| } |
| |
| .typing { |
| color: var(--muted); |
| font-style: italic; |
| } |
| |
| .composer { |
| border-top: 1px solid var(--line); |
| background: var(--composer-bg); |
| padding: 12px; |
| } |
| |
| .composer-box { |
| display: grid; |
| grid-template-columns: minmax(0, 1fr) auto; |
| gap: 8px; |
| align-items: center; |
| border: 1px solid var(--composer-panel-border); |
| border-radius: 18px; |
| background: var(--composer-panel-bg); |
| box-shadow: var(--shadow); |
| padding: 8px; |
| } |
| |
| textarea, |
| input, |
| select { |
| width: 100%; |
| border: 1px solid var(--line); |
| border-radius: 8px; |
| background: var(--surface); |
| color: var(--ink); |
| outline: none; |
| padding: 10px 11px; |
| font-size: 14px; |
| box-shadow: var(--shadow-soft); |
| } |
| |
| textarea:focus, |
| input:focus, |
| select:focus { |
| border-color: var(--line-strong); |
| box-shadow: 0 0 0 3px var(--ring); |
| } |
| |
| textarea { |
| min-height: 52px; |
| max-height: 180px; |
| resize: none; |
| line-height: 1.45; |
| } |
| |
| .composer textarea { |
| border: 0; |
| background: transparent; |
| box-shadow: none; |
| padding: 9px 10px; |
| } |
| |
| .composer textarea:focus { |
| border-color: transparent; |
| box-shadow: none; |
| } |
| |
| .send-stack { |
| display: grid; |
| gap: 6px; |
| width: 92px; |
| } |
| |
| .composer-input-area { |
| min-width: 0; |
| display: grid; |
| gap: 7px; |
| } |
| |
| .composer-meta { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 6px; |
| padding: 0 10px 2px; |
| } |
| |
| .model-pill { |
| display: inline-flex; |
| align-items: center; |
| min-height: 22px; |
| border-radius: 6px; |
| background: var(--pill-bg); |
| border: 1px solid var(--line); |
| color: var(--pill-text); |
| font-size: 11px; |
| font-weight: 650; |
| padding: 2px 7px; |
| max-width: 100%; |
| overflow-wrap: anywhere; |
| } |
| |
| .model-pill.strong { |
| color: var(--ink); |
| } |
| |
| .status-line { |
| margin-top: 8px; |
| min-height: 20px; |
| color: var(--muted); |
| font-size: 13px; |
| overflow-wrap: anywhere; |
| } |
| |
| .side-panel { |
| background: transparent; |
| border: 0; |
| border-radius: 0; |
| box-shadow: none; |
| padding: 0; |
| display: grid; |
| gap: 10px; |
| position: sticky; |
| top: 94px; |
| max-height: calc(100vh - 116px); |
| overflow: auto; |
| min-width: 0; |
| } |
| |
| details { |
| border: 1px solid var(--line); |
| border-radius: 10px; |
| background: var(--surface); |
| box-shadow: var(--shadow); |
| overflow: hidden; |
| } |
| |
| summary { |
| cursor: pointer; |
| padding: 11px 12px; |
| font-weight: 700; |
| background: var(--surface); |
| border-bottom: 1px solid transparent; |
| } |
| |
| summary:hover { |
| background: var(--surface-soft); |
| } |
| |
| details[open] summary { |
| border-bottom-color: var(--line); |
| } |
| |
| .details-body { |
| padding: 12px; |
| display: grid; |
| gap: 12px; |
| } |
| |
| .field-grid { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 10px; |
| } |
| |
| label { |
| display: block; |
| color: var(--muted); |
| font-size: 12px; |
| font-weight: 700; |
| margin-bottom: 5px; |
| } |
| |
| .toggle-row { |
| display: flex; |
| gap: 12px; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| |
| .toggle-row label { |
| display: inline-flex; |
| align-items: center; |
| gap: 7px; |
| margin: 0; |
| color: var(--ink); |
| font-size: 13px; |
| } |
| |
| input[type="checkbox"] { |
| width: 16px; |
| height: 16px; |
| accent-color: var(--accent); |
| box-shadow: none; |
| } |
| |
| .side-actions { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| } |
| |
| .preset-actions { |
| margin-bottom: 18px; |
| } |
| |
| .api-note { |
| border: 1px solid var(--line); |
| border-radius: 8px; |
| padding: 10px 12px; |
| background: var(--surface-soft); |
| color: var(--muted); |
| font-size: 13px; |
| line-height: 1.45; |
| } |
| |
| .control-note { |
| margin-top: 6px; |
| color: var(--muted); |
| font-size: 12px; |
| overflow-wrap: anywhere; |
| } |
| |
| .help-list { |
| margin: 0; |
| padding-left: 18px; |
| color: var(--muted); |
| font-size: 13px; |
| } |
| |
| .help-list li + li { |
| margin-top: 6px; |
| } |
| |
| .kv { |
| display: grid; |
| grid-template-columns: 118px minmax(0, 1fr); |
| gap: 7px 10px; |
| font-size: 13px; |
| } |
| |
| .kv span:nth-child(odd) { |
| color: var(--muted); |
| } |
| |
| .mono { |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| } |
| |
| .log-list { |
| display: grid; |
| gap: 8px; |
| max-height: 360px; |
| overflow: auto; |
| } |
| |
| .log-item { |
| border: 1px solid var(--line); |
| border-radius: 8px; |
| padding: 8px; |
| background: var(--surface-raised); |
| font-size: 12px; |
| } |
| |
| .log-item strong { |
| display: block; |
| margin-bottom: 2px; |
| } |
| |
| pre { |
| margin: 0; |
| padding: 12px; |
| border-radius: 8px; |
| background: var(--code); |
| border: 1px solid var(--line); |
| overflow-x: auto; |
| font-size: 12px; |
| line-height: 1.45; |
| } |
| |
| code { |
| background: var(--code); |
| border: 1px solid var(--line); |
| border-radius: 5px; |
| padding: 2px 5px; |
| overflow-wrap: anywhere; |
| } |
| |
| .api-list { |
| display: grid; |
| gap: 8px; |
| font-size: 13px; |
| } |
| |
| .api-row { |
| display: grid; |
| grid-template-columns: 52px minmax(0, 1fr); |
| gap: 8px; |
| align-items: start; |
| } |
| |
| @media (max-width: 1040px) { |
| .app-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .chat-shell { |
| min-height: 76vh; |
| } |
| |
| .side-panel { |
| grid-template-columns: 1fr 1fr; |
| position: static; |
| max-height: none; |
| } |
| } |
| |
| @media (max-width: 720px) { |
| .topbar { |
| grid-template-columns: 1fr; |
| align-items: start; |
| gap: 12px; |
| } |
| |
| .badge-row { |
| justify-content: flex-start; |
| } |
| |
| .top-actions { |
| justify-items: start; |
| width: 100%; |
| } |
| |
| .language-switch { |
| width: min(100%, 164px); |
| } |
| |
| .theme-switch { |
| width: min(100%, 220px); |
| } |
| |
| .panel-toggle { |
| justify-self: start; |
| min-width: 0; |
| } |
| |
| .badge-row { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| width: 100%; |
| justify-content: flex-start; |
| } |
| |
| .badge { |
| justify-content: flex-start; |
| white-space: normal; |
| } |
| |
| .source-label { |
| white-space: normal; |
| } |
| |
| .model-source { |
| display: block; |
| max-width: 100%; |
| } |
| |
| main, |
| .topbar { |
| width: min(100vw - 20px, 1440px); |
| } |
| |
| .chat-head { |
| align-items: flex-start; |
| flex-direction: column; |
| } |
| |
| .bubble-wrap { |
| max-width: 88%; |
| } |
| |
| .empty-state { |
| max-width: 100%; |
| } |
| |
| .composer-box { |
| grid-template-columns: 1fr; |
| } |
| |
| .send-stack { |
| width: 100%; |
| grid-template-columns: 1fr 1fr; |
| } |
| |
| .composer-box { |
| border-radius: 14px; |
| } |
| |
| .composer-meta { |
| padding-inline: 8px; |
| } |
| |
| .field-grid, |
| .side-panel { |
| grid-template-columns: 1fr; |
| } |
| |
| .kv, |
| .api-row, |
| .source-row { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .sr-only { |
| position: absolute; |
| width: 1px; |
| height: 1px; |
| padding: 0; |
| margin: -1px; |
| overflow: hidden; |
| clip: rect(0, 0, 0, 0); |
| white-space: nowrap; |
| border: 0; |
| } |
| |
| :root { |
| --target-bg: #ffffff; |
| --target-text: #0a0a0a; |
| --target-muted: #666666; |
| --target-faint: #eeeeee; |
| --target-soft: #f4f4f5; |
| --target-border: #dddddd; |
| --target-sidebar: 288px; |
| --target-radius: 24px; |
| --target-shadow: 0 2px 7px rgba(0, 0, 0, 0.12); |
| --target-shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.08); |
| } |
| |
| :root[data-theme="dark"] { |
| --target-bg: #111111; |
| --target-text: #f5f5f5; |
| --target-muted: #a3a3a3; |
| --target-faint: rgba(255, 255, 255, 0.12); |
| --target-soft: #1f1f1f; |
| --target-border: rgba(255, 255, 255, 0.16); |
| --target-shadow: 0 2px 9px rgba(0, 0, 0, 0.34); |
| --target-shadow-soft: 0 1px 4px rgba(0, 0, 0, 0.28); |
| } |
| |
| html, |
| body { |
| height: 100%; |
| background: var(--target-bg); |
| } |
| |
| body { |
| color: var(--target-text); |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| line-height: 1.45; |
| } |
| |
| .app-shell { |
| min-height: 100vh; |
| background: var(--target-bg); |
| } |
| |
| .sidebar { |
| position: fixed; |
| inset: 0 auto 0 0; |
| z-index: 30; |
| width: var(--target-sidebar); |
| padding: 20px 24px; |
| display: grid; |
| grid-template-rows: auto auto 1fr; |
| gap: 28px; |
| background: var(--target-bg); |
| color: var(--target-text); |
| overflow-y: auto; |
| border: 0; |
| box-shadow: none; |
| transform: translateX(0); |
| transition: transform 170ms ease; |
| } |
| |
| .app-shell.panel-collapsed .sidebar { |
| transform: translateX(calc(-1 * var(--target-sidebar))); |
| } |
| |
| .sidebar[hidden] { |
| display: block; |
| visibility: hidden; |
| pointer-events: none; |
| } |
| |
| .sidebar-brand { |
| display: flex; |
| align-items: center; |
| min-height: 30px; |
| } |
| |
| .sidebar-brand h1 { |
| margin: 0; |
| font-size: 20px; |
| line-height: 1.15; |
| letter-spacing: 0; |
| font-weight: 750; |
| } |
| |
| .sidebar-nav { |
| display: grid; |
| gap: 17px; |
| } |
| |
| .sidebar-action { |
| border: 0; |
| border-radius: 8px; |
| min-height: 24px; |
| width: max-content; |
| padding: 0; |
| display: inline-flex; |
| align-items: center; |
| gap: 10px; |
| background: transparent; |
| color: var(--target-text); |
| font-size: 14px; |
| font-weight: 650; |
| box-shadow: none; |
| } |
| |
| .sidebar-action:hover { |
| background: transparent; |
| border-color: transparent; |
| box-shadow: none; |
| color: var(--target-text); |
| } |
| |
| .sidebar-action .shortcut { |
| display: inline-flex; |
| align-items: center; |
| gap: 4px; |
| margin-left: 8px; |
| color: var(--target-muted); |
| font-size: 12px; |
| font-weight: 500; |
| } |
| |
| .conversation-section { |
| display: grid; |
| align-content: start; |
| gap: 44px; |
| color: var(--target-muted); |
| } |
| |
| .conversation-title { |
| font-size: 13px; |
| font-weight: 550; |
| color: #484848; |
| } |
| |
| :root[data-theme="dark"] .conversation-title { |
| color: var(--target-muted); |
| } |
| |
| .conversation-empty { |
| margin-left: 52px; |
| font-size: 15px; |
| } |
| |
| .icon-button { |
| width: 32px; |
| height: 32px; |
| min-height: 32px; |
| padding: 0; |
| display: inline-grid; |
| place-items: center; |
| border: 0; |
| border-radius: 9px; |
| background: transparent; |
| color: var(--target-text); |
| box-shadow: none; |
| } |
| |
| .icon-button:hover { |
| border-color: transparent; |
| background: var(--target-soft); |
| box-shadow: none; |
| } |
| |
| .icon { |
| width: 18px; |
| height: 18px; |
| display: inline-block; |
| color: currentColor; |
| flex: 0 0 auto; |
| } |
| |
| .icon svg { |
| width: 100%; |
| height: 100%; |
| display: block; |
| stroke: currentColor; |
| stroke-width: 2; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| fill: none; |
| } |
| |
| .sidebar-toggle { |
| position: fixed; |
| left: 24px; |
| top: 21px; |
| z-index: 45; |
| transition: left 170ms ease; |
| } |
| |
| .app-shell:not(.panel-collapsed) .sidebar-toggle { |
| left: 316px; |
| } |
| |
| .settings-button { |
| position: fixed; |
| top: 21px; |
| right: 24px; |
| z-index: 45; |
| } |
| |
| .chat-main { |
| width: auto; |
| margin: 0; |
| min-height: 100vh; |
| padding: 0 24px 44px; |
| transition: padding-left 170ms ease; |
| background: var(--target-bg); |
| } |
| |
| .app-shell:not(.panel-collapsed) .chat-main { |
| padding-left: calc(var(--target-sidebar) + 24px); |
| } |
| |
| .chat-shell { |
| width: 100%; |
| min-height: 100vh; |
| display: grid; |
| grid-template-rows: minmax(420px, 47vh) auto minmax(120px, 1fr); |
| background: transparent; |
| border: 0; |
| border-radius: 0; |
| box-shadow: none; |
| overflow: visible; |
| } |
| |
| .chat-head, |
| .chat-actions, |
| .prompt-chips, |
| .send-stack { |
| display: none !important; |
| } |
| |
| .timeline { |
| grid-row: 1; |
| width: 100%; |
| max-width: 920px; |
| margin: 0 auto; |
| padding: 112px 0 38px; |
| display: flex; |
| flex-direction: column; |
| align-items: end; |
| justify-content: flex-end; |
| gap: 22px; |
| overflow: visible; |
| background: transparent; |
| } |
| |
| .timeline:has(.message-row) { |
| height: calc(100vh - 178px); |
| padding: 72px 0 24px; |
| justify-content: flex-start; |
| align-items: stretch; |
| overflow-y: auto; |
| } |
| |
| .empty-state { |
| width: min(100%, 760px); |
| margin: 0 auto; |
| padding: 0; |
| text-align: center; |
| } |
| |
| .empty-state strong { |
| display: block; |
| color: var(--target-text); |
| font-size: 28px; |
| line-height: 1.16; |
| letter-spacing: 0; |
| font-weight: 800; |
| margin: 0 0 8px; |
| } |
| |
| .empty-state span { |
| display: block; |
| color: var(--target-muted); |
| font-size: 18px; |
| line-height: 1.35; |
| } |
| |
| .composer { |
| grid-row: 2; |
| width: min(736px, calc(100vw - 32px)); |
| margin: 0 auto; |
| padding: 0; |
| border: 0; |
| background: transparent; |
| } |
| |
| .app-shell:not(.panel-collapsed) .composer { |
| width: min(736px, calc(100vw - var(--target-sidebar) - 64px)); |
| } |
| |
| .composer-box { |
| min-height: 118px; |
| padding: 15px 14px 12px; |
| display: flex; |
| flex-direction: column; |
| align-items: stretch; |
| justify-content: space-between; |
| gap: 18px; |
| border: 1px solid var(--target-border); |
| border-radius: var(--target-radius); |
| background: #fbfbfb; |
| box-shadow: var(--target-shadow); |
| } |
| |
| :root[data-theme="dark"] .composer-box { |
| background: #161616; |
| } |
| |
| .composer-input-area { |
| min-width: 0; |
| display: block; |
| } |
| |
| .composer textarea { |
| width: 100%; |
| min-height: 34px; |
| max-height: 180px; |
| padding: 0 6px; |
| border: 0; |
| border-radius: 0; |
| background: transparent; |
| color: var(--target-text); |
| box-shadow: none; |
| font-size: 16px; |
| line-height: 1.55; |
| resize: none; |
| } |
| |
| .composer textarea::placeholder { |
| color: #707070; |
| opacity: 1; |
| } |
| |
| .composer textarea:focus { |
| border-color: transparent; |
| box-shadow: none; |
| } |
| |
| .composer-toolbar { |
| display: grid; |
| grid-template-columns: auto minmax(0, 1fr) auto; |
| align-items: center; |
| gap: 12px; |
| } |
| |
| .round-button, |
| .send-button { |
| width: 32px; |
| height: 32px; |
| min-height: 32px; |
| padding: 0; |
| border-radius: 999px; |
| display: inline-grid; |
| place-items: center; |
| box-shadow: var(--target-shadow-soft); |
| } |
| |
| .round-button { |
| border: 1px solid var(--target-faint); |
| background: var(--target-bg); |
| color: var(--target-text); |
| font-size: 25px; |
| font-weight: 300; |
| line-height: 1; |
| } |
| |
| .send-button { |
| border: 0; |
| background: #8c8c8c; |
| color: #ffffff; |
| font-size: 20px; |
| font-weight: 700; |
| } |
| |
| .send-button:disabled { |
| opacity: 1; |
| background: #8f8f8f; |
| color: #ffffff; |
| } |
| |
| .stop-button { |
| display: inline-grid; |
| place-items: center; |
| min-width: 64px; |
| min-height: 36px; |
| border-color: var(--target-accent); |
| background: var(--target-accent-soft); |
| color: var(--target-accent-strong); |
| font-weight: 760; |
| } |
| |
| .stop-button:disabled { |
| display: none; |
| } |
| |
| .model-button { |
| justify-self: end; |
| min-width: min(430px, 100%); |
| max-width: 100%; |
| min-height: 26px; |
| padding: 0 7px; |
| border: 0; |
| border-radius: 7px; |
| background: #ededed; |
| display: inline-flex; |
| align-items: center; |
| justify-content: flex-start; |
| gap: 7px; |
| color: var(--target-text); |
| box-shadow: none; |
| } |
| |
| .model-button:hover { |
| background: #e8e8e8; |
| border-color: transparent; |
| box-shadow: none; |
| } |
| |
| :root[data-theme="dark"] .model-button { |
| background: #2b2b2b; |
| } |
| |
| .composer-meta { |
| min-width: 0; |
| display: inline-flex; |
| align-items: center; |
| gap: 5px; |
| padding: 0; |
| overflow: hidden; |
| } |
| |
| .model-pill { |
| min-height: 18px; |
| max-width: 160px; |
| padding: 1px 5px; |
| border: 0; |
| border-radius: 5px; |
| background: #e7e7e7; |
| color: var(--target-text); |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| font-size: 11px; |
| font-weight: 650; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| display: inline-block; |
| } |
| |
| :root[data-theme="dark"] .model-pill { |
| background: #383838; |
| } |
| |
| .model-pill.strong { |
| max-width: 88px; |
| font-family: inherit; |
| font-size: 12px; |
| font-weight: 750; |
| background: transparent; |
| } |
| |
| .status-line { |
| min-height: 18px; |
| margin-top: 22px; |
| text-align: center; |
| color: var(--target-muted); |
| font-size: 12px; |
| } |
| |
| .status-line kbd { |
| padding: 1px 5px; |
| border-radius: 4px; |
| background: var(--target-soft); |
| color: var(--target-muted); |
| font-family: inherit; |
| font-size: 12px; |
| font-weight: 650; |
| } |
| |
| .message-row { |
| display: flex; |
| gap: 10px; |
| align-items: flex-start; |
| width: 100%; |
| } |
| |
| .message-row.user { |
| justify-content: flex-end; |
| } |
| |
| .avatar { |
| display: none; |
| } |
| |
| .bubble-wrap { |
| max-width: min(760px, 86%); |
| min-width: 0; |
| } |
| |
| .role-label { |
| margin-bottom: 6px; |
| color: var(--target-muted); |
| font-size: 12px; |
| } |
| |
| .message-row.user .role-label { |
| text-align: right; |
| } |
| |
| .bubble { |
| border: 1px solid var(--target-border); |
| border-radius: 18px; |
| background: var(--target-bg); |
| box-shadow: var(--target-shadow-soft); |
| padding: 12px 14px; |
| color: var(--target-text); |
| font-size: 14px; |
| line-height: 1.55; |
| } |
| |
| .message-row.user .bubble { |
| background: var(--target-soft); |
| border-color: var(--target-soft); |
| } |
| |
| .message-row.error .bubble { |
| color: var(--bad); |
| border-color: var(--bad-border); |
| background: var(--bad-bg); |
| } |
| |
| .settings-overlay { |
| position: fixed; |
| inset: 0; |
| z-index: 80; |
| display: grid; |
| place-items: center; |
| padding: 24px; |
| background: rgba(0, 0, 0, 0.46); |
| } |
| |
| .settings-overlay[hidden] { |
| display: none; |
| } |
| |
| .settings-dialog { |
| position: relative; |
| width: min(896px, calc(100vw - 48px)); |
| height: min(640px, calc(100vh - 72px)); |
| display: grid; |
| grid-template-columns: 256px minmax(0, 1fr); |
| grid-template-rows: minmax(0, 1fr) 86px; |
| overflow: hidden; |
| border: 1px solid var(--target-border); |
| border-radius: 8px; |
| background: var(--target-bg); |
| color: var(--target-text); |
| box-shadow: 0 20px 80px rgba(0, 0, 0, 0.25); |
| } |
| |
| .settings-tabs { |
| grid-row: 1 / 3; |
| padding: 32px 24px; |
| border-right: 1px solid var(--target-faint); |
| display: grid; |
| align-content: start; |
| gap: 8px; |
| } |
| |
| .settings-tab { |
| min-height: 36px; |
| padding: 8px 12px; |
| border: 0; |
| border-radius: 9px; |
| background: transparent; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| color: var(--target-muted); |
| font-size: 14px; |
| font-weight: 550; |
| box-shadow: none; |
| } |
| |
| .settings-tab.active, |
| .settings-tab:hover { |
| background: var(--target-soft); |
| color: var(--target-text); |
| border-color: transparent; |
| box-shadow: none; |
| } |
| |
| .settings-content { |
| min-width: 0; |
| padding: 26px 24px 0; |
| overflow: hidden; |
| display: grid; |
| } |
| |
| .settings-panel { |
| display: none; |
| min-height: 0; |
| overflow-y: auto; |
| padding-right: 2px; |
| } |
| |
| .settings-panel.active { |
| display: block; |
| } |
| |
| .settings-heading { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| padding-bottom: 26px; |
| border-bottom: 1px solid var(--target-faint); |
| margin-bottom: 24px; |
| } |
| |
| .settings-heading h2 { |
| margin: 0; |
| font-size: 20px; |
| font-weight: 750; |
| line-height: 1.2; |
| } |
| |
| .settings-close { |
| position: absolute; |
| top: 16px; |
| right: 14px; |
| z-index: 2; |
| } |
| |
| .settings-footer { |
| grid-column: 2; |
| grid-row: 2; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 16px 24px 24px; |
| border-top: 1px solid var(--target-faint); |
| background: var(--target-bg); |
| } |
| |
| .settings-footer .primary { |
| background: #171717; |
| border-color: #171717; |
| color: #ffffff; |
| min-width: 122px; |
| min-height: 36px; |
| border-radius: 8px; |
| } |
| |
| .settings-footer .ghost { |
| min-height: 36px; |
| border-radius: 8px; |
| background: var(--target-bg); |
| color: var(--target-text); |
| } |
| |
| .setting-block { |
| display: grid; |
| gap: 8px; |
| margin-bottom: 24px; |
| } |
| |
| .setting-block > label, |
| .setting-label { |
| margin: 0; |
| color: var(--target-text); |
| font-size: 14px; |
| font-weight: 750; |
| } |
| |
| .setting-help { |
| margin: 0; |
| color: var(--target-muted); |
| font-size: 13px; |
| line-height: 1.35; |
| } |
| |
| .language-switch, |
| .theme-switch { |
| width: min(100%, 448px); |
| border: 1px solid var(--target-border); |
| border-radius: 8px; |
| background: var(--target-bg); |
| display: inline-grid; |
| padding: 3px; |
| gap: 3px; |
| } |
| |
| .language-switch { |
| grid-template-columns: 1fr 1fr; |
| } |
| |
| .theme-switch { |
| grid-template-columns: repeat(3, 1fr); |
| } |
| |
| .language-switch button, |
| .theme-switch button { |
| border: 0; |
| border-radius: 6px; |
| min-height: 30px; |
| padding: 5px 10px; |
| background: transparent; |
| color: var(--target-muted); |
| box-shadow: none; |
| } |
| |
| .language-switch button.active, |
| .theme-switch button.active { |
| background: var(--target-soft); |
| color: var(--target-text); |
| box-shadow: none; |
| } |
| |
| .badge-row { |
| justify-content: flex-start; |
| } |
| |
| .badge { |
| border-radius: 999px; |
| background: var(--target-soft); |
| } |
| |
| .field-grid { |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 14px; |
| } |
| |
| textarea, |
| input, |
| select { |
| border: 1px solid var(--target-border); |
| border-radius: 8px; |
| background: var(--target-bg); |
| color: var(--target-text); |
| box-shadow: var(--target-shadow-soft); |
| } |
| |
| .kv { |
| grid-template-columns: 140px minmax(0, 1fr); |
| } |
| |
| .log-list { |
| max-height: 400px; |
| } |
| |
| .log-item, |
| pre, |
| code { |
| background: var(--target-soft); |
| border-color: var(--target-faint); |
| } |
| |
| @media (max-width: 900px) { |
| .sidebar { |
| width: 260px; |
| } |
| |
| .app-shell:not(.panel-collapsed) .sidebar-toggle { |
| left: 280px; |
| } |
| |
| .app-shell:not(.panel-collapsed) .chat-main { |
| padding-left: 24px; |
| } |
| |
| .settings-dialog { |
| grid-template-columns: 1fr; |
| grid-template-rows: auto minmax(0, 1fr) 76px; |
| height: min(720px, calc(100vh - 40px)); |
| } |
| |
| .settings-tabs { |
| grid-row: 1; |
| grid-column: 1; |
| display: flex; |
| gap: 6px; |
| overflow-x: auto; |
| padding: 16px 18px 10px; |
| border-right: 0; |
| border-bottom: 1px solid var(--target-faint); |
| } |
| |
| .settings-tab { |
| flex: 0 0 auto; |
| } |
| |
| .settings-content { |
| grid-row: 2; |
| padding: 18px 18px 0; |
| } |
| |
| .settings-footer { |
| grid-column: 1; |
| grid-row: 3; |
| padding: 14px 18px 18px; |
| } |
| } |
| |
| @media (max-width: 720px) { |
| .app-shell { |
| --target-sidebar: 100vw; |
| } |
| |
| .sidebar { |
| width: 100vw; |
| } |
| |
| .chat-main, |
| .app-shell:not(.panel-collapsed) .chat-main { |
| padding: 0 16px 34px; |
| } |
| |
| .sidebar-toggle { |
| left: 20px; |
| top: 20px; |
| } |
| |
| .app-shell:not(.panel-collapsed) .sidebar-toggle { |
| left: 20px; |
| } |
| |
| .app-shell:not(.panel-collapsed) .sidebar-brand { |
| padding-left: 44px; |
| } |
| |
| .settings-button { |
| top: 20px; |
| right: 18px; |
| } |
| |
| .chat-shell { |
| grid-template-rows: minmax(390px, 49vh) auto minmax(120px, 1fr); |
| } |
| |
| .timeline { |
| padding: 88px 0 36px; |
| } |
| |
| .empty-state strong { |
| font-size: 24px; |
| } |
| |
| .empty-state span { |
| font-size: 17px; |
| } |
| |
| .composer { |
| width: 100%; |
| } |
| |
| .composer-box { |
| min-height: 116px; |
| border-radius: 23px; |
| padding: 15px 12px 12px; |
| } |
| |
| .composer-toolbar { |
| gap: 10px; |
| } |
| |
| .model-button { |
| justify-self: stretch; |
| min-width: 0; |
| } |
| |
| .model-pill:not(.strong) { |
| display: none; |
| } |
| |
| .model-pill.strong { |
| max-width: 100%; |
| } |
| |
| .status-line { |
| margin-top: 20px; |
| } |
| |
| .settings-overlay { |
| padding: 20px 12px; |
| } |
| |
| .settings-dialog { |
| width: 100%; |
| } |
| |
| .field-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .settings-footer { |
| gap: 10px; |
| } |
| } |
| |
| |
| :root { |
| --target-bg: #f7f8fb; |
| --target-bg-end: #eceff4; |
| --target-panel: #ffffff; |
| --target-text: #17181d; |
| --target-muted: #68707c; |
| --target-faint: #e3e6ec; |
| --target-soft: #f0f2f6; |
| --target-border: #d6dbe4; |
| --target-accent: #c21f3a; |
| --target-accent-strong: #9f1630; |
| --target-accent-soft: #fbe8ed; |
| --target-warning-soft: #fff5df; |
| --target-sidebar: 292px; |
| --target-radius: 22px; |
| --target-shadow: 0 18px 52px rgba(24, 27, 35, 0.13); |
| --target-shadow-soft: 0 8px 22px rgba(24, 27, 35, 0.08); |
| } |
| |
| :root[data-theme="dark"] { |
| --target-bg: #111115; |
| --target-bg-end: #18181f; |
| --target-panel: #1c1c23; |
| --target-text: #f6f7fb; |
| --target-muted: #a6acb8; |
| --target-faint: rgba(255, 255, 255, 0.11); |
| --target-soft: #262832; |
| --target-border: rgba(255, 255, 255, 0.17); |
| --target-accent: #ff5a76; |
| --target-accent-strong: #ff7c93; |
| --target-accent-soft: rgba(255, 90, 118, 0.16); |
| --target-warning-soft: rgba(245, 158, 11, 0.14); |
| --target-shadow: 0 22px 62px rgba(0, 0, 0, 0.42); |
| --target-shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.32); |
| } |
| |
| body { |
| font-family: Inter, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| background: var(--target-bg); |
| } |
| |
| .app-shell { |
| background: linear-gradient(180deg, var(--target-bg) 0%, var(--target-bg-end) 100%); |
| } |
| |
| .sidebar { |
| background: var(--target-panel); |
| border-right: 1px solid var(--target-faint); |
| box-shadow: 18px 0 42px rgba(24, 27, 35, 0.06); |
| } |
| |
| :root[data-theme="dark"] .sidebar { |
| box-shadow: 18px 0 44px rgba(0, 0, 0, 0.28); |
| } |
| |
| .sidebar-brand { |
| min-height: 38px; |
| border-bottom: 1px solid var(--target-faint); |
| padding-bottom: 18px; |
| } |
| |
| .sidebar-brand h1 { |
| color: var(--target-text); |
| font-size: 19px; |
| font-weight: 780; |
| } |
| |
| .sidebar-nav { |
| gap: 10px; |
| } |
| |
| .sidebar-action { |
| width: 100%; |
| min-height: 40px; |
| padding: 0 10px; |
| border-radius: 10px; |
| justify-content: flex-start; |
| } |
| |
| .sidebar-action:hover, |
| .sidebar-action:focus-visible { |
| background: var(--target-soft); |
| } |
| |
| .sidebar-action .shortcut { |
| margin-left: auto; |
| padding: 2px 5px; |
| border: 1px solid var(--target-faint); |
| border-radius: 5px; |
| background: var(--target-bg); |
| } |
| |
| .conversation-section { |
| gap: 10px; |
| padding-top: 12px; |
| border-top: 1px solid var(--target-faint); |
| } |
| |
| .conversation-title { |
| color: var(--target-text); |
| font-weight: 720; |
| } |
| |
| .conversation-empty { |
| margin-left: 0; |
| padding: 12px; |
| border: 1px dashed var(--target-border); |
| border-radius: 10px; |
| background: var(--target-soft); |
| color: var(--target-muted); |
| font-size: 13px; |
| line-height: 1.45; |
| } |
| |
| .icon-button { |
| border: 1px solid var(--target-faint); |
| background: var(--target-panel); |
| box-shadow: var(--target-shadow-soft); |
| } |
| |
| .icon-button:hover { |
| background: var(--target-soft); |
| border-color: var(--target-border); |
| } |
| |
| .chat-main, |
| .chat-shell, |
| .timeline { |
| background: transparent; |
| } |
| |
| .timeline { |
| max-width: 900px; |
| padding-top: 116px; |
| } |
| |
| .timeline:has(.message-row) { |
| scrollbar-gutter: stable; |
| } |
| |
| .empty-state { |
| width: min(100%, 780px); |
| } |
| |
| .empty-state strong { |
| color: var(--target-text); |
| font-size: 34px; |
| font-weight: 820; |
| } |
| |
| .empty-state span { |
| max-width: 560px; |
| margin: 0 auto; |
| color: var(--target-muted); |
| font-size: 16px; |
| } |
| |
| .empty-state .prompt-chips { |
| display: flex !important; |
| margin-top: 24px; |
| justify-content: center; |
| } |
| |
| .empty-state .prompt-chip { |
| min-height: 40px; |
| border-color: var(--target-border); |
| border-radius: 12px; |
| background: var(--target-panel); |
| color: var(--target-text); |
| box-shadow: var(--target-shadow-soft); |
| } |
| |
| .empty-state .prompt-chip:hover { |
| border-color: var(--target-accent); |
| background: var(--target-accent-soft); |
| box-shadow: var(--target-shadow); |
| } |
| |
| .composer { |
| width: min(780px, calc(100vw - 32px)); |
| } |
| |
| .app-shell:not(.panel-collapsed) .composer { |
| width: min(780px, calc(100vw - var(--target-sidebar) - 68px)); |
| } |
| |
| .composer-box { |
| border-color: var(--target-border); |
| background: rgba(255, 255, 255, 0.92); |
| box-shadow: var(--target-shadow); |
| backdrop-filter: blur(18px); |
| } |
| |
| :root[data-theme="dark"] .composer-box { |
| background: rgba(28, 28, 35, 0.92); |
| } |
| |
| .composer textarea { |
| color: var(--target-text); |
| } |
| |
| .composer textarea::placeholder { |
| color: var(--target-muted); |
| } |
| |
| .round-button, |
| .model-button { |
| border: 1px solid var(--target-faint); |
| background: var(--target-panel); |
| } |
| |
| .model-button { |
| background: var(--target-soft); |
| } |
| |
| .model-button:hover, |
| .round-button:hover { |
| border-color: var(--target-border); |
| background: var(--target-accent-soft); |
| } |
| |
| .model-pill { |
| background: rgba(255, 255, 255, 0.72); |
| color: var(--target-text); |
| } |
| |
| :root[data-theme="dark"] .model-pill { |
| background: rgba(255, 255, 255, 0.08); |
| } |
| |
| .send-button:not(:disabled) { |
| background: var(--target-accent); |
| color: #ffffff; |
| } |
| |
| .send-button:not(:disabled):hover { |
| background: var(--target-accent-strong); |
| } |
| |
| .status-line { |
| color: var(--target-muted); |
| } |
| |
| .message-row.assistant .bubble { |
| border-left: 3px solid var(--target-accent); |
| background: var(--target-panel); |
| } |
| |
| .message-row.user .bubble { |
| background: var(--target-accent-soft); |
| border-color: transparent; |
| } |
| |
| .bubble { |
| box-shadow: var(--target-shadow-soft); |
| } |
| |
| .settings-overlay { |
| background: rgba(17, 17, 21, 0.5); |
| backdrop-filter: blur(10px); |
| } |
| |
| .settings-dialog { |
| border-color: var(--target-border); |
| border-radius: 16px; |
| background: var(--target-panel); |
| box-shadow: 0 28px 90px rgba(17, 17, 21, 0.28); |
| } |
| |
| .settings-tabs { |
| background: var(--target-bg); |
| } |
| |
| .settings-tab.active, |
| .settings-tab:hover { |
| background: var(--target-accent-soft); |
| color: var(--target-text); |
| } |
| |
| .settings-heading { |
| border-bottom-color: var(--target-faint); |
| } |
| |
| .settings-heading .icon { |
| color: var(--target-accent); |
| } |
| |
| .settings-footer .primary { |
| background: var(--target-accent); |
| border-color: var(--target-accent); |
| } |
| |
| .settings-footer .primary:hover { |
| background: var(--target-accent-strong); |
| } |
| |
| input[type="checkbox"] { |
| accent-color: var(--target-accent); |
| } |
| |
| .badge.good { |
| color: var(--good); |
| border-color: var(--good-border); |
| background: var(--good-bg); |
| } |
| |
| .badge.warn { |
| color: var(--warn); |
| border-color: var(--warn-border); |
| background: var(--warn-bg); |
| } |
| |
| .badge.bad { |
| color: var(--bad); |
| border-color: var(--bad-border); |
| background: var(--bad-bg); |
| } |
| |
| @media (max-width: 720px) { |
| .empty-state strong { |
| font-size: 27px; |
| } |
| |
| .empty-state span { |
| font-size: 15px; |
| } |
| |
| .empty-state .prompt-chips { |
| display: grid !important; |
| grid-template-columns: 1fr; |
| } |
| |
| .empty-state .prompt-chip { |
| width: 100%; |
| justify-content: center; |
| } |
| |
| .settings-dialog { |
| border-radius: 14px; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="appGrid" class="app-shell panel-collapsed"> |
| <aside id="sidePanel" class="sidebar" aria-label="对话" hidden> |
| <div class="sidebar-brand"> |
| <h1>Qwen GGUF</h1> |
| </div> |
| <nav class="sidebar-nav" aria-label="Primary"> |
| <button type="button" id="newChatBtn" class="sidebar-action"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></span> |
| <span data-i18n="newChat">New chat</span> |
| </button> |
| <button type="button" class="sidebar-action" data-settings-open="api"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m16 6-8 12"/><path d="m10 5-6 6 6 6"/><path d="m14 5 6 6-6 6"/></svg></span> |
| <span data-i18n="toolServers">API 与工具</span> |
| </button> |
| <button type="button" id="retryBtn" class="sidebar-action" disabled> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v6h6"/></svg></span> |
| <span data-i18n="retry">Retry</span> |
| </button> |
| </nav> |
| <div class="conversation-section"> |
| <div class="conversation-title" data-i18n="conversations">对话记录</div> |
| <div class="conversation-empty" data-i18n="noConversations">当前会话仅保存在浏览器内存中。</div> |
| </div> |
| </aside> |
|
|
| <button type="button" id="panelToggle" class="icon-button sidebar-toggle" aria-expanded="false" aria-controls="sidePanel" title="显示侧栏"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M9 5v14"/></svg></span> |
| <span class="sr-only">显示侧栏</span> |
| </button> |
| <button type="button" id="settingsBtn" class="icon-button settings-button" title="设置" aria-label="设置" data-i18n-title="settingsLabel"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.05A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.05A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.05A1.7 1.7 0 0 0 15 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.33.6.95 1 1.55 1H21a2 2 0 1 1 0 4h-.05A1.7 1.7 0 0 0 19.4 15Z"/></svg></span> |
| <span class="sr-only">设置</span> |
| </button> |
|
|
| <main class="chat-main"> |
| <section class="chat-shell" aria-label="Chat"> |
| <div id="timeline" class="timeline" aria-live="polite"></div> |
|
|
| <form id="composer" class="composer"> |
| <div class="composer-box"> |
| <div class="composer-input-area"> |
| <textarea id="userInput" rows="1" placeholder="输入消息..." data-i18n-placeholder="inputPlaceholder" autocomplete="off"></textarea> |
| </div> |
| <div class="composer-toolbar"> |
| <button type="button" class="round-button" title="打开设置" aria-label="打开设置" data-i18n-title="addContext" data-settings-open="general">+</button> |
| <button type="button" id="modelPillButton" class="model-button" title="模型与参数" aria-label="模型与参数" data-i18n-title="modelParams" data-settings-open="parameters"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m12 3 8 4.5v9L12 21l-8-4.5v-9L12 3Z"/><path d="M12 12 4 7.5"/><path d="m12 12 8-4.5"/><path d="M12 12v9"/></svg></span> |
| <span class="composer-meta" aria-label="Current model"> |
| <span id="activeModelPill" class="model-pill strong">unsloth/diffusiongemma-26B-A4B-it-GGUF:Q4_K_M</span> |
| <span id="activeQuantPill" class="model-pill">Q4_K_M</span> |
| <span id="activeBackendPill" class="model-pill">GPU</span> |
| </span> |
| </button> |
| <button id="sendBtn" class="send-button" type="submit" disabled aria-label="发送">↑</button> |
| <button id="stopBtn" class="stop-button" type="button" disabled data-i18n="stop">Stop</button> |
| </div> |
| </div> |
| <div id="chatStatus" class="status-line">按 <kbd>Enter</kbd> 发送,<kbd>Shift + Enter</kbd> 换行</div> |
| </form> |
| </section> |
| </main> |
| </div> |
|
|
| <div id="settingsOverlay" class="settings-overlay" hidden> |
| <div class="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settingsTitle"> |
| <button type="button" id="settingsClose" class="icon-button settings-close" title="关闭" aria-label="关闭" data-i18n-title="close"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></span> |
| <span class="sr-only">关闭</span> |
| </button> |
| <nav class="settings-tabs" aria-label="设置"> |
| <button type="button" class="settings-tab active" data-settings-tab="general"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.05A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.05A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.05A1.7 1.7 0 0 0 15 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.33.6.95 1 1.55 1H21a2 2 0 1 1 0 4h-.05A1.7 1.7 0 0 0 19.4 15Z"/></svg></span> |
| <span id="settingsTitle" data-i18n="generalSummary">通用</span> |
| </button> |
| <button type="button" class="settings-tab" data-settings-tab="parameters"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg></span> |
| <span data-i18n="paramsSummary">Sampling</span> |
| </button> |
| <button type="button" class="settings-tab" data-settings-tab="runtime"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M8 20h8"/><path d="M12 18v2"/></svg></span> |
| <span data-i18n="runtimeSummary">Runtime</span> |
| </button> |
| <button type="button" class="settings-tab" data-settings-tab="logs"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h10"/></svg></span> |
| <span data-i18n="logsSummary">Logs</span> |
| </button> |
| <button type="button" class="settings-tab" data-settings-tab="api"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg></span> |
| <span data-i18n="apiSummary">Developer</span> |
| </button> |
| </nav> |
|
|
| <div class="settings-content"> |
| <section class="settings-panel active" data-settings-panel="general"> |
| <div class="settings-heading"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.05A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.05A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.05A1.7 1.7 0 0 0 15 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.33.6.95 1 1.55 1H21a2 2 0 1 1 0 4h-.05A1.7 1.7 0 0 0 19.4 15Z"/></svg></span> |
| <h2 data-i18n="generalSummary">通用</h2> |
| </div> |
| <div class="setting-block"> |
| <div class="setting-label" data-i18n="themeLabel">主题</div> |
| <div class="theme-switch" aria-label="主题"> |
| <button type="button" id="themeSystem" class="active" data-i18n="themeSystem">System</button> |
| <button type="button" id="themeLight" data-i18n="themeLight">Light</button> |
| <button type="button" id="themeDark" data-i18n="themeDark">Dark</button> |
| </div> |
| <p class="setting-help" data-i18n="themeHelp">按系统主题显示,也可以固定浅色或深色。</p> |
| </div> |
| <div class="setting-block"> |
| <div class="setting-label" data-i18n="languageLabel">语言</div> |
| <div class="language-switch" aria-label="语言"> |
| <button type="button" id="langZh" class="active">中文</button> |
| <button type="button" id="langEn">English</button> |
| </div> |
| </div> |
| <div class="setting-block"> |
| <div class="setting-label" data-i18n="sourceLabel">Original model source</div> |
| <div class="model-line" id="modelLine">unsloth/diffusiongemma-26B-A4B-it-GGUF:Q4_K_M</div> |
| <a class="model-source" id="modelSource" href="https://huggingface.co/unsloth/diffusiongemma-26B-A4B-it-GGUF" target="_blank" rel="noreferrer">https://huggingface.co/unsloth/diffusiongemma-26B-A4B-it-GGUF</a> |
| </div> |
| <div class="setting-block"> |
| <div class="setting-label" data-i18n="statusLabel">状态</div> |
| <div class="badge-row" aria-label="Runtime status"> |
| <span id="runtimeBadge" class="badge warn" data-i18n="badgeRuntimeUnknown">runtime unknown</span> |
| <span id="modelBadge" class="badge warn" data-i18n="badgeModelUnknown">model unknown</span> |
| <span id="streamBadge" class="badge warn" data-i18n="badgeStreamUnknown">stream unknown</span> |
| <span id="gpuBadge" class="badge warn" data-i18n="badgeGpuNotProbed">GPU not probed</span> |
| </div> |
| </div> |
| </section> |
|
|
| <section class="settings-panel" data-settings-panel="parameters"> |
| <div class="settings-heading"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg></span> |
| <h2 data-i18n="paramsSummary">Sampling</h2> |
| </div> |
| <div class="setting-block"> |
| <label for="model" data-i18n="modelLabel">Model</label> |
| <select id="model"> |
| <option value="unsloth/diffusiongemma-26B-A4B-it-GGUF:Q4_K_M">unsloth/diffusiongemma-26B-A4B-it-GGUF:Q4_K_M</option> |
| </select> |
| <p id="precisionNote" class="setting-help"></p> |
| </div> |
| <div class="side-actions preset-actions" aria-label="参数预设"> |
| <button type="button" id="stablePresetBtn" data-i18n="stablePreset">稳定验证</button> |
| <button type="button" id="longPresetBtn" data-i18n="longPreset">长上下文</button> |
| </div> |
| <div class="field-grid"> |
| <div class="setting-block"> |
| <label for="maxTokens" data-i18n="maxTokens">Max tokens</label> |
| <input id="maxTokens" type="number" min="1" max="2048" step="1" value="512" /> |
| </div> |
| <div class="setting-block"> |
| <label for="ctxSize" data-i18n="context">Context</label> |
| <input id="ctxSize" type="number" min="1024" max="4096" step="1024" value="4096" /> |
| </div> |
| <div class="setting-block"> |
| <label for="temperature" data-i18n="temperature">Temperature</label> |
| <input id="temperature" type="number" min="0" max="3" step="0.1" value="0" /> |
| </div> |
| <div class="setting-block"> |
| <label for="topP" data-i18n="topP">Top p</label> |
| <input id="topP" type="number" min="0" max="1" step="0.01" value="1" /> |
| </div> |
| <div class="setting-block"> |
| <label for="topK" data-i18n="topK">Top k</label> |
| <input id="topK" type="number" min="0" step="1" value="0" /> |
| </div> |
| <div class="setting-block"> |
| <label for="minP" data-i18n="minP">Min p</label> |
| <input id="minP" type="number" min="0" max="1" step="0.01" value="0" /> |
| </div> |
| <div class="setting-block"> |
| <label for="presencePenalty" data-i18n="presencePenalty">Presence penalty</label> |
| <input id="presencePenalty" type="number" min="-2" max="2" step="0.1" value="0" /> |
| </div> |
| <div class="setting-block"> |
| <label for="repetitionPenalty" data-i18n="repetitionPenalty">Repetition penalty</label> |
| <input id="repetitionPenalty" type="number" min="0" max="2" step="0.1" value="1" /> |
| </div> |
| </div> |
| <div class="toggle-row"> |
| <label><input id="stream" type="checkbox" checked /> <span data-i18n="streamLabel">Stream</span></label> |
| <label><input id="includeUsage" type="checkbox" checked /> <span data-i18n="usageLabel">Usage</span></label> |
| </div> |
| </section> |
|
|
| <section class="settings-panel" data-settings-panel="runtime"> |
| <div class="settings-heading"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M8 20h8"/><path d="M12 18v2"/></svg></span> |
| <h2 data-i18n="runtimeSummary">Runtime</h2> |
| </div> |
| <div class="side-actions"> |
| <button type="button" id="refreshRuntimeBtn" data-i18n="refresh">Refresh</button> |
| <button type="button" id="probeGpuBtn" data-i18n="gpuProbe">GPU Probe</button> |
| </div> |
| <div id="runtimeKv" class="kv"></div> |
| </section> |
|
|
| <section class="settings-panel" data-settings-panel="logs"> |
| <div class="settings-heading"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h10"/></svg></span> |
| <h2 data-i18n="logsSummary">Logs</h2> |
| </div> |
| <div class="field-grid"> |
| <div class="setting-block"> |
| <label for="logLimit" data-i18n="limit">Limit</label> |
| <input id="logLimit" type="number" min="1" max="200" step="1" value="40" /> |
| </div> |
| <div class="setting-block"> |
| <label for="logEvent" data-i18n="eventFilter">Event filter</label> |
| <input id="logEvent" type="text" placeholder="chat.request" data-i18n-placeholder="eventPlaceholder" /> |
| </div> |
| <div class="setting-block"> |
| <label for="logRequestId" data-i18n="requestFilter">Request ID</label> |
| <input id="logRequestId" type="text" placeholder="chatcmpl-..." data-i18n-placeholder="requestPlaceholder" /> |
| </div> |
| </div> |
| <div class="side-actions"> |
| <button type="button" id="refreshLogsBtn" data-i18n="refreshLogs">Refresh logs</button> |
| </div> |
| <div id="logs" class="log-list"></div> |
| </section> |
|
|
| <section class="settings-panel" data-settings-panel="api"> |
| <div class="settings-heading"> |
| <span class="icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg></span> |
| <h2 data-i18n="apiSummary">Developer</h2> |
| </div> |
| <div id="apiList" class="api-list"></div> |
| <div class="side-actions"> |
| <button type="button" id="copyCurlBtn" data-i18n="copyCurl">Copy curl</button> |
| </div> |
| <pre id="curlBox"></pre> |
| </section> |
| </div> |
|
|
| <div class="settings-footer"> |
| <button type="button" id="settingsReset" class="ghost"> |
| <span data-i18n="resetSettings">Reset to default</span> |
| </button> |
| <button type="button" id="settingsSave" class="primary" data-i18n="saveSettings">Save settings</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const fallbackConfig = { |
| model: 'unsloth/diffusiongemma-26B-A4B-it-GGUF:Q4_K_M', |
| repoId: 'unsloth/diffusiongemma-26B-A4B-it-GGUF', |
| filename: 'diffusiongemma-26B-A4B-it-Q4_K_M.gguf', |
| modelSourceUrl: 'https://huggingface.co/unsloth/diffusiongemma-26B-A4B-it-GGUF', |
| zeroGpuSize: 'large', |
| defaultMaxTokens: 512, |
| maxMaxTokens: 2048, |
| diffusionMaxSteps: 48, |
| nGpuLayers: 99, |
| thinkingDefault: false, |
| diffusionVisualDefault: false, |
| diffusionKvCache: 'auto' |
| }; |
| |
| function readConfig() { |
| const node = document.getElementById('app-config'); |
| try { |
| const parsed = JSON.parse(node.textContent); |
| return {...fallbackConfig, ...parsed}; |
| } catch (_) { |
| return fallbackConfig; |
| } |
| } |
| |
| const APP = readConfig(); |
| const DEFAULT_MODEL = APP.model; |
| const CPU_MODEL = ''; |
| const DEFAULT_QUANT = (APP.model && APP.model.includes(':') ? APP.model.split(':').pop() : 'Q4_K_M'); |
| const BUILD_RUNTIME_DEFAULTS = { |
| model: APP.model, |
| maxTokens: Number(APP.defaultMaxTokens || 512), |
| maxMaxTokens: Number(APP.maxMaxTokens || APP.defaultMaxTokens || 2048), |
| ctxSize: 4096, |
| maxCtxSize: 4096, |
| temperature: 0, |
| topP: 1, |
| topK: 0, |
| minP: 0, |
| presencePenalty: 0, |
| repetitionPenalty: 1, |
| backendLimits: { |
| gpu: { |
| default_max_tokens: Number(APP.defaultMaxTokens || 512), |
| max_max_tokens: Number(APP.maxMaxTokens || APP.defaultMaxTokens || 2048), |
| ctx_size: 4096, |
| max_ctx_size: 4096 |
| } |
| }, |
| stream: true, |
| includeUsage: true |
| }; |
| let runtimeDefaults = {...BUILD_RUNTIME_DEFAULTS}; |
| let modelBackends = {[DEFAULT_MODEL]: 'gpu'}; |
| let messages = []; |
| let activeController = null; |
| let lastUserText = ''; |
| let lastRawEvents = []; |
| let lastRuntimeData = null; |
| let currentStatus = {key: 'statusReady', args: {}}; |
| let uiRunning = false; |
| const LANG_KEY = 'diffusiongemma-ui-lang'; |
| const savedLang = localStorage.getItem(LANG_KEY); |
| let currentLang = savedLang === 'en' ? 'en' : 'zh'; |
| const THEME_KEY = 'diffusiongemma-ui-theme'; |
| const THEME_VALUES = ['system', 'light', 'dark']; |
| const savedTheme = localStorage.getItem(THEME_KEY); |
| let currentTheme = THEME_VALUES.includes(savedTheme) ? savedTheme : 'system'; |
| const themeMedia = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; |
| const PANEL_KEY = 'diffusiongemma-ui-panel-collapsed'; |
| const savedPanelState = localStorage.getItem(PANEL_KEY); |
| let panelCollapsed = savedPanelState === null ? true : savedPanelState === '1'; |
| const SETTINGS_KEY = 'diffusiongemma-ui-settings-v2'; |
| let hasSavedParameterSettings = false; |
| |
| const I18N = { |
| zh: { |
| documentTitle: 'DiffusionGemma Chat · 中文界面', |
| eyebrow: 'ZeroGPU GGUF API', |
| appTitle: 'DiffusionGemma ChatUI', |
| settingsLabel: '设置', |
| close: '关闭', |
| addContext: '打开设置', |
| modelParams: '模型与参数', |
| sourceLabel: '原始模型来源', |
| toolServers: 'API 兼容性', |
| conversations: '对话记录', |
| noConversations: '当前会话仅保存在浏览器内存中。', |
| badgeRuntimeUnknown: 'runtime 未知', |
| badgeModelUnknown: 'model 未知', |
| badgeStreamUnknown: 'stream 未知', |
| badgeGpuNotProbed: 'GPU 未探测', |
| chatTitle: '对话', |
| chatSubtitle: '流式响应;首次请求可能等待 ZeroGPU 分配。', |
| themeSystem: '系统', |
| themeLight: '浅色', |
| themeDark: '深色', |
| panelShow: '显示面板', |
| panelHide: '隐藏面板', |
| newChat: '新对话', |
| retry: '重试', |
| emptyTitle: '和 DiffusionGemma 开始对话', |
| emptyBody: '保留 qwen 风格外壳,但当前交互绑定的是 DiffusionGemma ZeroGPU 运行态和扩散式生成参数。', |
| inputPlaceholder: '输入消息...', |
| send: '发送', |
| stop: '停止', |
| generalSummary: '通用', |
| themeLabel: '主题', |
| themeHelp: '按系统主题显示,也可以固定浅色或深色。', |
| languageLabel: '语言', |
| statusLabel: '状态', |
| paramsSummary: '采样', |
| modelLabel: '模型', |
| maxTokens: '最大输出 tokens', |
| context: '上下文', |
| temperature: '温度', |
| topP: 'Top p', |
| topK: 'Top k', |
| minP: 'Min p', |
| presencePenalty: '存在惩罚', |
| repetitionPenalty: '重复惩罚', |
| streamLabel: '流式', |
| usageLabel: 'Usage 统计', |
| precisionNote: '当前模型为 {model};量化固定为 {quant};页面参数会映射到 DiffusionGemma 的 /v1 接口。', |
| runtimeSummary: '运行状态', |
| refresh: '刷新', |
| gpuProbe: 'GPU Probe', |
| helpSummary: '使用说明', |
| helpModel: '当前模型为 DiffusionGemma GGUF;前端会调用 /v1/chat/completions、/v1/models 与 /v2/runtime。', |
| stablePreset: '稳定验证', |
| longPreset: '长上下文', |
| helpDefaults: '参数来自当前 runtime;稳定验证适合首轮测试,长上下文适合压力实验。', |
| helpEmpty: '空输出会按服务异常处理;用 /v2/logs 的 request_id 串起一次请求。', |
| helpApi: 'API 支持 OpenAI-style Chat Completions 和客户端工具调用协议。', |
| apiCompatNote: 'OpenAI-style /v1/chat/completions:支持 messages 与 stream 最终块兼容模式。运行态与日志继续走 /v2/runtime、/v2/logs、/v2/zerogpu/probe。', |
| logsSummary: '日志', |
| limit: '条数', |
| eventFilter: 'Event 筛选', |
| requestFilter: 'Request ID', |
| eventPlaceholder: 'chat.request', |
| requestPlaceholder: 'chatcmpl-...', |
| refreshLogs: '刷新日志', |
| apiSummary: '开发者', |
| copyCurl: '复制 curl', |
| resetSettings: '恢复默认', |
| saveSettings: '保存设置', |
| settingsSaved: '设置已保存', |
| statusReady: '按 Enter 发送,Shift + Enter 换行', |
| statusSent: '请求已发送,等待 ZeroGPU / llama-server', |
| statusGenerating: '生成中...', |
| statusStreamFallback: '流式返回为空,正在改用非流式重试', |
| statusStableFallback: '返回为空,正在用稳定参数重试', |
| statusDone: '完成', |
| statusStopped: '已停止', |
| statusFailed: '请求失败', |
| statusCurlCopied: 'curl 已复制', |
| statusGpuDone: 'GPU probe 完成', |
| statusGpuFailed: 'GPU probe 失败', |
| roleUser: '你', |
| roleAssistant: 'DiffusionGemma', |
| roleError: '错误', |
| typing: '正在生成...', |
| samplePrompt: '用五句话解释 block diffusion 和 autoregressive decoding 的差别。', |
| emptyOutputMessage: '模型返回空内容;这会被按服务异常处理,请查看 request_id 日志。', |
| emptyOutputFinal: '模型返回空内容,请重试。', |
| requestStopped: '请求已停止。', |
| logsEmpty: '暂无日志', |
| logsFailed: '日志请求失败:{message}', |
| modelLine: '模型 {model} · 精度 {quant}', |
| runtimeReady: 'runtime 就绪', |
| runtimePending: 'runtime 等待中', |
| runtimeError: 'runtime 错误', |
| modelCached: 'model 已缓存', |
| modelMissing: 'model 未缓存', |
| streamOn: 'server stream 开启', |
| streamOff: 'server stream 关闭', |
| gpuProbing: '正在探测 GPU', |
| gpuAllocated: 'GPU 已分配', |
| gpuUnavailable: 'GPU 不可用', |
| gpuProbeFailed: 'probe 失败', |
| kvZeroGPU: 'ZeroGPU', |
| kvRunner: 'Runner', |
| kvStream: 'Stream', |
| kvDefaultQuant: '默认精度', |
| kvSource: '来源', |
| kvDefaultMax: '默认 max tokens', |
| kvDefaultCtx: '默认 context', |
| kvTemperature: '温度', |
| kvTopP: 'Top p', |
| kvTopK: 'Top k', |
| kvMinP: 'Min p', |
| kvPresence: '存在惩罚', |
| kvRepetition: '重复惩罚', |
| kvModelCached: 'Model 缓存', |
| kvModelGB: 'Model GB', |
| kvLastQuant: '上次精度', |
| kvLastOutput: '上次输出', |
| kvLastDuration: '上次耗时', |
| kvDataDir: 'Data dir', |
| kvCpuModel: 'CPU model', |
| kvCpuServer: 'CPU server', |
| kvCpuRAM: 'CPU RAM', |
| kvBackend: '后端', |
| backendGpu: 'GPU', |
| backendCpu: 'CPU' |
| }, |
| en: { |
| documentTitle: 'DiffusionGemma Chat', |
| eyebrow: 'ZeroGPU GGUF API', |
| appTitle: 'DiffusionGemma ChatUI', |
| settingsLabel: 'Settings', |
| close: 'Close', |
| addContext: 'Open settings', |
| modelParams: 'Model and parameters', |
| sourceLabel: 'Original model source', |
| toolServers: 'API compatibility', |
| conversations: 'Conversations', |
| noConversations: 'This session is stored only in browser memory.', |
| badgeRuntimeUnknown: 'runtime unknown', |
| badgeModelUnknown: 'model unknown', |
| badgeStreamUnknown: 'stream unknown', |
| badgeGpuNotProbed: 'GPU not probed', |
| chatTitle: 'Chat', |
| chatSubtitle: 'Streaming is on; first requests may wait for ZeroGPU allocation.', |
| themeSystem: 'System', |
| themeLight: 'Light', |
| themeDark: 'Dark', |
| panelShow: 'Show panel', |
| panelHide: 'Hide panel', |
| newChat: 'New chat', |
| retry: 'Retry', |
| emptyTitle: 'Start with DiffusionGemma', |
| emptyBody: 'This keeps the qwen-style shell, but the actual interaction is wired to DiffusionGemma ZeroGPU runtime and diffusion-specific defaults.', |
| inputPlaceholder: 'Type a message...', |
| send: 'Send', |
| stop: 'Stop', |
| generalSummary: 'General', |
| themeLabel: 'Theme', |
| themeHelp: 'Follow the system theme or pin light or dark mode.', |
| languageLabel: 'Language', |
| statusLabel: 'Status', |
| paramsSummary: 'Sampling', |
| modelLabel: 'Model', |
| maxTokens: 'Max tokens', |
| context: 'Context', |
| temperature: 'Temperature', |
| topP: 'Top p', |
| topK: 'Top k', |
| minP: 'Min p', |
| presencePenalty: 'Presence penalty', |
| repetitionPenalty: 'Repetition penalty', |
| streamLabel: 'Stream', |
| usageLabel: 'Usage', |
| precisionNote: 'Current model is {model}; quant is fixed to {quant}; this UI is mapped to the DiffusionGemma /v1 API.', |
| runtimeSummary: 'Runtime', |
| refresh: 'Refresh', |
| gpuProbe: 'GPU Probe', |
| helpSummary: 'Guide', |
| helpModel: 'This Space serves DiffusionGemma GGUF. The frontend calls /v1/chat/completions, /v1/models, and /v2 runtime endpoints.', |
| stablePreset: 'Stable check', |
| longPreset: 'Long context', |
| helpDefaults: 'Parameters come from the current runtime. Stable check is for first validation; long context is for stress tests.', |
| helpEmpty: 'Empty output is treated as a service error; use request_id in /v2/logs to trace a request.', |
| helpApi: 'The API supports OpenAI-style Chat Completions and client-side tool-calling protocol fields.', |
| apiCompatNote: 'OpenAI-style /v1/chat/completions supports messages and a final-delta stream compatibility mode. Runtime and logs remain under /v2/runtime, /v2/logs, and /v2/zerogpu/probe.', |
| logsSummary: 'Logs', |
| limit: 'Limit', |
| eventFilter: 'Event filter', |
| requestFilter: 'Request ID', |
| eventPlaceholder: 'chat.request', |
| requestPlaceholder: 'chatcmpl-...', |
| refreshLogs: 'Refresh logs', |
| apiSummary: 'Developer', |
| copyCurl: 'Copy curl', |
| resetSettings: 'Reset to default', |
| saveSettings: 'Save settings', |
| settingsSaved: 'Settings saved', |
| statusReady: 'Press Enter to send, Shift + Enter for new line', |
| statusSent: 'Request sent; waiting for ZeroGPU / llama-server', |
| statusGenerating: 'Generating...', |
| statusStreamFallback: 'Stream returned empty; retrying without stream', |
| statusStableFallback: 'Empty response; retrying with stable parameters', |
| statusDone: 'Done', |
| statusStopped: 'Stopped', |
| statusFailed: 'Request failed', |
| statusCurlCopied: 'curl copied', |
| statusGpuDone: 'GPU probe complete', |
| statusGpuFailed: 'GPU probe failed', |
| roleUser: 'You', |
| roleAssistant: 'DiffusionGemma', |
| roleError: 'Error', |
| typing: 'Generating...', |
| samplePrompt: 'Explain the difference between block diffusion and autoregressive decoding in five sentences.', |
| emptyOutputMessage: 'The model returned empty content; this is treated as a service error. Check logs by request_id.', |
| emptyOutputFinal: 'The model returned empty content. Try again.', |
| requestStopped: 'Request stopped.', |
| logsEmpty: 'No logs', |
| logsFailed: 'Log request failed: {message}', |
| modelLine: 'Model {model} · Precision {quant}', |
| runtimeReady: 'runtime ready', |
| runtimePending: 'runtime pending', |
| runtimeError: 'runtime error', |
| modelCached: 'model cached', |
| modelMissing: 'model missing', |
| streamOn: 'server stream on', |
| streamOff: 'server stream off', |
| gpuProbing: 'probing GPU', |
| gpuAllocated: 'GPU allocated', |
| gpuUnavailable: 'GPU unavailable', |
| gpuProbeFailed: 'probe failed', |
| kvZeroGPU: 'ZeroGPU', |
| kvRunner: 'Runner', |
| kvStream: 'Stream', |
| kvDefaultQuant: 'Default precision', |
| kvSource: 'Source', |
| kvDefaultMax: 'Default max tokens', |
| kvDefaultCtx: 'Default context', |
| kvTemperature: 'Temperature', |
| kvTopP: 'Top p', |
| kvTopK: 'Top k', |
| kvMinP: 'Min p', |
| kvPresence: 'Presence penalty', |
| kvRepetition: 'Repetition penalty', |
| kvModelCached: 'Model cached', |
| kvModelGB: 'Model GB', |
| kvLastQuant: 'Last precision', |
| kvLastOutput: 'Last output', |
| kvLastDuration: 'Last duration', |
| kvDataDir: 'Data dir', |
| kvCpuModel: 'CPU model', |
| kvCpuServer: 'CPU server', |
| kvCpuRAM: 'CPU RAM', |
| kvBackend: 'Backend', |
| backendGpu: 'GPU', |
| backendCpu: 'CPU' |
| } |
| }; |
| |
| const QUICK_PROMPTS = { |
| zh: [ |
| ['快速自检', '用五句话解释 block diffusion 和 autoregressive decoding 的差别。'], |
| ['结构化总结', '请把下面材料整理成结构化摘要,并列出关键风险:'], |
| ['API 调用', '给我一个最小可用的 curl 示例,调用 /v1/chat/completions。'] |
| ], |
| en: [ |
| ['Quick check', 'Explain the difference between block diffusion and autoregressive decoding in five sentences.'], |
| ['Structured summary', 'Turn the material below into a structured summary and list key risks:'], |
| ['API call', 'Give me a minimal curl example for /v1/chat/completions.'] |
| ] |
| }; |
| |
| const els = {}; |
| const ids = [ |
| 'modelLine', 'modelSource', 'langZh', 'langEn', 'themeSystem', 'themeLight', 'themeDark', |
| 'panelToggle', 'settingsBtn', 'settingsOverlay', 'settingsClose', 'settingsSave', 'settingsReset', |
| 'modelPillButton', 'activeModelPill', 'activeQuantPill', 'activeBackendPill', 'appGrid', 'sidePanel', |
| 'runtimeBadge', 'modelBadge', 'streamBadge', 'gpuBadge', |
| 'timeline', 'composer', 'userInput', 'sendBtn', 'stopBtn', 'newChatBtn', |
| 'retryBtn', 'chatStatus', 'model', 'stablePresetBtn', 'longPresetBtn', 'maxTokens', 'ctxSize', 'temperature', |
| 'topP', 'topK', 'minP', 'presencePenalty', 'repetitionPenalty', |
| 'precisionNote', |
| 'stream', 'includeUsage', 'refreshRuntimeBtn', 'probeGpuBtn', |
| 'runtimeKv', 'logLimit', 'logEvent', 'logRequestId', 'refreshLogsBtn', 'logs', 'apiList', 'copyCurlBtn', |
| 'curlBox' |
| ]; |
| |
| for (const id of ids) { |
| els[id] = document.getElementById(id); |
| } |
| |
| function t(key, args = {}) { |
| let text = (I18N[currentLang] && I18N[currentLang][key]) || I18N.zh[key] || key; |
| for (const [name, value] of Object.entries(args)) { |
| text = text.replaceAll('{' + name + '}', String(value ?? '')); |
| } |
| return text; |
| } |
| |
| function resolvedTheme() { |
| if (currentTheme === 'dark' || currentTheme === 'light') return currentTheme; |
| return themeMedia && themeMedia.matches ? 'dark' : 'light'; |
| } |
| |
| function applyTheme() { |
| const theme = resolvedTheme(); |
| document.documentElement.dataset.theme = theme; |
| document.documentElement.style.colorScheme = theme; |
| els.themeSystem.classList.toggle('active', currentTheme === 'system'); |
| els.themeLight.classList.toggle('active', currentTheme === 'light'); |
| els.themeDark.classList.toggle('active', currentTheme === 'dark'); |
| } |
| |
| function setTheme(theme) { |
| currentTheme = THEME_VALUES.includes(theme) ? theme : 'system'; |
| localStorage.setItem(THEME_KEY, currentTheme); |
| applyTheme(); |
| } |
| |
| function setButtonLabel(node, text) { |
| if (!node) return; |
| const label = node.querySelector('.sr-only'); |
| if (label) { |
| label.textContent = text; |
| } else { |
| node.textContent = text; |
| } |
| node.title = text; |
| node.setAttribute('aria-label', text); |
| } |
| |
| function applyPanelState() { |
| els.appGrid.classList.toggle('panel-collapsed', panelCollapsed); |
| els.sidePanel.hidden = panelCollapsed; |
| els.panelToggle.setAttribute('aria-expanded', String(!panelCollapsed)); |
| setButtonLabel(els.panelToggle, t(panelCollapsed ? 'panelShow' : 'panelHide')); |
| } |
| |
| function togglePanel() { |
| panelCollapsed = !panelCollapsed; |
| localStorage.setItem(PANEL_KEY, panelCollapsed ? '1' : '0'); |
| applyPanelState(); |
| } |
| |
| function applyI18n() { |
| document.documentElement.lang = currentLang === 'zh' ? 'zh-CN' : 'en'; |
| document.title = t('documentTitle'); |
| document.querySelectorAll('[data-i18n]').forEach((node) => { |
| node.textContent = t(node.dataset.i18n); |
| }); |
| document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { |
| node.placeholder = t(node.dataset.i18nPlaceholder); |
| }); |
| document.querySelectorAll('[data-i18n-title]').forEach((node) => { |
| const label = t(node.dataset.i18nTitle); |
| node.title = label; |
| node.setAttribute('aria-label', label); |
| const sr = node.querySelector('.sr-only'); |
| if (sr) sr.textContent = label; |
| }); |
| els.langZh.classList.toggle('active', currentLang === 'zh'); |
| els.langEn.classList.toggle('active', currentLang === 'en'); |
| applyTheme(); |
| applyPanelState(); |
| els.precisionNote.textContent = t('precisionNote', {model: DEFAULT_MODEL, cpuModel: CPU_MODEL, quant: DEFAULT_QUANT}); |
| syncModelPill(); |
| setStatusKey(currentStatus.key, currentStatus.args); |
| if (lastRuntimeData) { |
| renderRuntime(lastRuntimeData, false); |
| } else { |
| setBadge(els.runtimeBadge, t('badgeRuntimeUnknown'), 'warn'); |
| setBadge(els.modelBadge, t('badgeModelUnknown'), 'warn'); |
| setBadge(els.streamBadge, t('badgeStreamUnknown'), 'warn'); |
| setBadge(els.gpuBadge, t('badgeGpuNotProbed'), 'warn'); |
| } |
| renderMessages(); |
| updateCurl(); |
| } |
| |
| function setLanguage(lang) { |
| currentLang = lang === 'en' ? 'en' : 'zh'; |
| localStorage.setItem(LANG_KEY, currentLang); |
| applyI18n(); |
| } |
| |
| function setBadge(node, text, state) { |
| node.textContent = text; |
| node.className = 'badge ' + (state || 'warn'); |
| } |
| |
| function setStatusKey(key, args = {}) { |
| currentStatus = {key, args}; |
| if (key === 'statusReady') { |
| els.chatStatus.innerHTML = currentLang === 'zh' |
| ? '按 <kbd>Enter</kbd> 发送,<kbd>Shift + Enter</kbd> 换行' |
| : 'Press <kbd>Enter</kbd> to send, <kbd>Shift + Enter</kbd> for new line'; |
| } else { |
| els.chatStatus.textContent = t(key, args); |
| } |
| } |
| |
| function shortModelLabel(model) { |
| if (!model) return 'unknown'; |
| const parts = model.split(':'); |
| return parts.length > 1 ? parts[parts.length - 1] : model.split('/').pop(); |
| } |
| |
| function backendForModel(model) { |
| if (modelBackends[model]) return modelBackends[model]; |
| return String(model || '').endsWith('-cpu') ? 'cpu' : 'gpu'; |
| } |
| |
| function currentBackend() { |
| return backendForModel(els.model.value || DEFAULT_MODEL); |
| } |
| |
| function limitsForBackend(backend) { |
| const limits = lastRuntimeData?.backend_limits?.[backend] || BUILD_RUNTIME_DEFAULTS.backendLimits?.[backend] || {}; |
| return { |
| maxTokens: Number(limits.default_max_tokens ?? BUILD_RUNTIME_DEFAULTS.maxTokens), |
| maxMaxTokens: Number(limits.max_max_tokens ?? limits.default_max_tokens ?? BUILD_RUNTIME_DEFAULTS.maxMaxTokens), |
| ctxSize: Number(limits.ctx_size ?? BUILD_RUNTIME_DEFAULTS.ctxSize), |
| maxCtxSize: Number(limits.max_ctx_size ?? limits.ctx_size ?? BUILD_RUNTIME_DEFAULTS.ctxSize) |
| }; |
| } |
| |
| function syncModelPill() { |
| const model = els.model.value || DEFAULT_MODEL; |
| const backend = currentBackend(); |
| if (els.activeModelPill) els.activeModelPill.textContent = shortModelLabel(model); |
| if (els.activeQuantPill) els.activeQuantPill.textContent = DEFAULT_QUANT; |
| if (els.activeBackendPill) els.activeBackendPill.textContent = backend === 'cpu' ? t('backendCpu') : t('backendGpu'); |
| } |
| |
| function createId() { |
| if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); |
| return 'msg-' + Date.now() + '-' + Math.random().toString(16).slice(2); |
| } |
| |
| function roleName(role) { |
| if (role === 'user') return t('roleUser'); |
| if (role === 'assistant') return t('roleAssistant'); |
| return t('roleError'); |
| } |
| |
| function fillPrompt(text) { |
| els.userInput.value = text; |
| resizeInput(); |
| updateCurl(); |
| updateSendState(); |
| els.userInput.focus(); |
| } |
| |
| function renderEmptyState() { |
| const empty = document.createElement('div'); |
| empty.className = 'empty-state'; |
| |
| const title = document.createElement('strong'); |
| title.textContent = t('emptyTitle'); |
| const body = document.createElement('span'); |
| body.textContent = t('emptyBody'); |
| const chips = document.createElement('div'); |
| chips.className = 'prompt-chips'; |
| for (const [label, prompt] of QUICK_PROMPTS[currentLang] || QUICK_PROMPTS.zh) { |
| const chip = document.createElement('button'); |
| chip.type = 'button'; |
| chip.className = 'prompt-chip'; |
| chip.textContent = label; |
| chip.addEventListener('click', () => fillPrompt(prompt)); |
| chips.appendChild(chip); |
| } |
| empty.append(title, body, chips); |
| return empty; |
| } |
| |
| function renderMessages() { |
| els.timeline.textContent = ''; |
| if (!messages.length) { |
| els.timeline.appendChild(renderEmptyState()); |
| return; |
| } |
| |
| for (const message of messages) { |
| const row = document.createElement('div'); |
| row.className = 'message-row ' + message.role; |
| |
| const avatar = document.createElement('div'); |
| avatar.className = 'avatar'; |
| avatar.textContent = message.role === 'user' ? 'U' : (message.role === 'assistant' ? 'A' : '!'); |
| |
| const wrap = document.createElement('div'); |
| wrap.className = 'bubble-wrap'; |
| |
| const label = document.createElement('div'); |
| label.className = 'role-label'; |
| label.textContent = roleName(message.role); |
| |
| const bubble = document.createElement('div'); |
| bubble.className = 'bubble'; |
| if (message.pending && !message.content) { |
| const typing = document.createElement('span'); |
| typing.className = 'typing'; |
| typing.textContent = t('typing'); |
| bubble.appendChild(typing); |
| } else { |
| bubble.textContent = message.content || ''; |
| } |
| |
| wrap.append(label, bubble); |
| row.append(avatar, wrap); |
| els.timeline.appendChild(row); |
| } |
| els.timeline.scrollTop = els.timeline.scrollHeight; |
| } |
| |
| function updateSendState() { |
| els.sendBtn.disabled = uiRunning || !els.userInput.value.trim(); |
| } |
| |
| function setRunning(isRunning) { |
| uiRunning = isRunning; |
| updateSendState(); |
| els.stopBtn.disabled = !isRunning; |
| els.retryBtn.disabled = isRunning || !lastUserText; |
| els.userInput.disabled = isRunning; |
| } |
| |
| function numericValue(id, fallback) { |
| const value = Number(els[id].value); |
| return Number.isFinite(value) ? value : fallback; |
| } |
| |
| function normalizedDefaults(source = runtimeDefaults) { |
| return { |
| model: source.model || DEFAULT_MODEL, |
| maxTokens: Number(source.maxTokens ?? BUILD_RUNTIME_DEFAULTS.maxTokens), |
| maxMaxTokens: Number(source.maxMaxTokens ?? source.maxTokens ?? BUILD_RUNTIME_DEFAULTS.maxMaxTokens), |
| ctxSize: Number(source.ctxSize ?? BUILD_RUNTIME_DEFAULTS.ctxSize), |
| maxCtxSize: Number(source.maxCtxSize ?? source.ctxSize ?? BUILD_RUNTIME_DEFAULTS.ctxSize), |
| temperature: Number(source.temperature ?? BUILD_RUNTIME_DEFAULTS.temperature), |
| topP: Number(source.topP ?? BUILD_RUNTIME_DEFAULTS.topP), |
| topK: Number(source.topK ?? BUILD_RUNTIME_DEFAULTS.topK), |
| minP: Number(source.minP ?? BUILD_RUNTIME_DEFAULTS.minP), |
| presencePenalty: Number(source.presencePenalty ?? BUILD_RUNTIME_DEFAULTS.presencePenalty), |
| repetitionPenalty: Number(source.repetitionPenalty ?? BUILD_RUNTIME_DEFAULTS.repetitionPenalty), |
| backendLimits: source.backendLimits || BUILD_RUNTIME_DEFAULTS.backendLimits || {}, |
| stream: source.stream !== false, |
| includeUsage: source.includeUsage !== false |
| }; |
| } |
| |
| function setControlValues(values) { |
| const next = normalizedDefaults({...runtimeDefaults, ...values}); |
| if (next.model && Array.from(els.model.options).some((option) => option.value === next.model)) { |
| els.model.value = next.model; |
| } |
| const backendLimits = limitsForBackend(backendForModel(els.model.value || next.model || DEFAULT_MODEL)); |
| const maxMaxTokens = backendLimits.maxMaxTokens || next.maxMaxTokens || next.maxTokens || 32000; |
| const maxCtxSize = Math.max(1024, backendLimits.maxCtxSize || backendLimits.ctxSize || next.maxCtxSize || next.ctxSize || 1024); |
| els.maxTokens.max = String(maxMaxTokens); |
| els.ctxSize.max = String(maxCtxSize); |
| els.maxTokens.value = Math.min(next.maxTokens, maxMaxTokens); |
| els.ctxSize.value = Math.min(next.ctxSize, maxCtxSize); |
| els.temperature.value = next.temperature; |
| els.topP.value = next.topP; |
| els.topK.value = next.topK; |
| els.minP.value = next.minP; |
| els.presencePenalty.value = next.presencePenalty; |
| els.repetitionPenalty.value = next.repetitionPenalty; |
| els.stream.checked = Boolean(next.stream); |
| els.includeUsage.checked = Boolean(next.includeUsage); |
| syncModelPill(); |
| updateCurl(); |
| } |
| |
| function currentParameterSettings() { |
| return { |
| model: els.model.value || DEFAULT_MODEL, |
| maxTokens: numericValue('maxTokens', runtimeDefaults.maxTokens), |
| ctxSize: numericValue('ctxSize', runtimeDefaults.ctxSize), |
| temperature: numericValue('temperature', runtimeDefaults.temperature), |
| topP: numericValue('topP', runtimeDefaults.topP), |
| topK: numericValue('topK', runtimeDefaults.topK), |
| minP: numericValue('minP', runtimeDefaults.minP), |
| presencePenalty: numericValue('presencePenalty', runtimeDefaults.presencePenalty), |
| repetitionPenalty: numericValue('repetitionPenalty', runtimeDefaults.repetitionPenalty), |
| stream: els.stream.checked, |
| includeUsage: els.includeUsage.checked |
| }; |
| } |
| |
| function saveCurrentSettings() { |
| localStorage.setItem(SETTINGS_KEY, JSON.stringify(currentParameterSettings())); |
| hasSavedParameterSettings = true; |
| setStatusKey('settingsSaved'); |
| closeSettings(); |
| } |
| |
| function loadSavedSettings() { |
| const raw = localStorage.getItem(SETTINGS_KEY); |
| if (!raw) return false; |
| try { |
| const parsed = JSON.parse(raw); |
| if (!parsed || typeof parsed !== 'object') return false; |
| hasSavedParameterSettings = true; |
| setControlValues(parsed); |
| return true; |
| } catch (error) { |
| localStorage.removeItem(SETTINGS_KEY); |
| return false; |
| } |
| } |
| |
| function applyStablePreset() { |
| const limits = limitsForBackend(currentBackend()); |
| setControlValues({ |
| ...runtimeDefaults, |
| model: els.model.value || DEFAULT_MODEL, |
| maxTokens: Math.min(limits.maxTokens || runtimeDefaults.maxTokens || 256, 256), |
| ctxSize: Math.min(limits.ctxSize || runtimeDefaults.ctxSize || 4096, 4096), |
| temperature: Math.min(runtimeDefaults.temperature ?? 0.2, 0.2), |
| stream: true, |
| includeUsage: true |
| }); |
| } |
| |
| function applyLongPreset() { |
| const limits = limitsForBackend(currentBackend()); |
| setControlValues({ |
| ...runtimeDefaults, |
| model: els.model.value || DEFAULT_MODEL, |
| maxTokens: limits.maxTokens || runtimeDefaults.maxTokens, |
| maxMaxTokens: limits.maxMaxTokens || runtimeDefaults.maxMaxTokens, |
| ctxSize: limits.ctxSize || runtimeDefaults.ctxSize, |
| maxCtxSize: limits.maxCtxSize || runtimeDefaults.maxCtxSize |
| }); |
| } |
| |
| function handleModelChange() { |
| const limits = limitsForBackend(currentBackend()); |
| setControlValues({ |
| ...currentParameterSettings(), |
| model: els.model.value || DEFAULT_MODEL, |
| maxTokens: Math.min(numericValue('maxTokens', limits.maxTokens || runtimeDefaults.maxTokens), limits.maxMaxTokens || runtimeDefaults.maxMaxTokens || 32000), |
| ctxSize: Math.min(numericValue('ctxSize', limits.ctxSize || runtimeDefaults.ctxSize), limits.maxCtxSize || limits.ctxSize || runtimeDefaults.ctxSize || 1024) |
| }); |
| } |
| |
| function chatMessagesForRequest(nextUserText) { |
| const result = messages |
| .filter((item) => item.role === 'user' || item.role === 'assistant') |
| .filter((item) => item.content && !item.pending) |
| .map((item) => ({role: item.role, content: item.content})); |
| if (nextUserText) result.push({role: 'user', content: nextUserText}); |
| return result; |
| } |
| |
| function requestBody(nextUserText) { |
| const backend = currentBackend(); |
| const limits = limitsForBackend(backend); |
| const maxTokens = Math.min( |
| numericValue('maxTokens', limits.maxTokens || runtimeDefaults.maxTokens), |
| limits.maxMaxTokens || runtimeDefaults.maxMaxTokens || Number.MAX_SAFE_INTEGER |
| ); |
| const body = { |
| model: els.model.value || DEFAULT_MODEL, |
| messages: chatMessagesForRequest(nextUserText), |
| max_tokens: maxTokens, |
| temperature: numericValue('temperature', runtimeDefaults.temperature), |
| top_p: numericValue('topP', runtimeDefaults.topP), |
| top_k: numericValue('topK', runtimeDefaults.topK), |
| min_p: numericValue('minP', runtimeDefaults.minP), |
| presence_penalty: numericValue('presencePenalty', runtimeDefaults.presencePenalty), |
| repetition_penalty: numericValue('repetitionPenalty', runtimeDefaults.repetitionPenalty), |
| stream: els.stream.checked, |
| extra_body: { |
| thinking: false, |
| diffusion_visual: false, |
| max_denoising_steps: Number(APP.diffusionMaxSteps || 48), |
| diffusion_kv_cache: APP.diffusionKvCache || 'auto', |
| n_gpu_layers: Number(APP.nGpuLayers || 99) |
| } |
| }; |
| if (body.stream && els.includeUsage.checked) { |
| body.stream_options = {include_usage: true}; |
| } |
| return body; |
| } |
| |
| function updateCurl() { |
| const draft = els.userInput.value.trim(); |
| const body = requestBody(draft || t('samplePrompt')); |
| els.curlBox.textContent = [ |
| 'curl -sS ' + (body.stream ? '-N ' : '') + '--max-time 900 ' + location.origin + '/v1/chat/completions \\', |
| " -H 'Content-Type: application/json' \\", |
| " -d '" + JSON.stringify(body).replaceAll("'", "'\\''") + "'" |
| ].join('\n'); |
| updateSendState(); |
| } |
| |
| function errorMessageFromPayload(data) { |
| if (data?.error?.type === 'empty_output') return t('emptyOutputMessage'); |
| return data?.error?.message || JSON.stringify(data); |
| } |
| |
| function formatToolCalls(toolCalls) { |
| if (!Array.isArray(toolCalls) || !toolCalls.length) return ''; |
| return JSON.stringify({tool_calls: toolCalls}, null, 2); |
| } |
| |
| async function copyCurl() { |
| updateCurl(); |
| await navigator.clipboard.writeText(els.curlBox.textContent); |
| setStatusKey('statusCurlCopied'); |
| } |
| |
| async function sendMessage(textOverride) { |
| const text = (textOverride || els.userInput.value).trim(); |
| if (!text || activeController) return; |
| |
| const payload = requestBody(text); |
| lastUserText = text; |
| messages.push({id: createId(), role: 'user', content: text}); |
| const assistant = {id: createId(), role: 'assistant', content: '', pending: true}; |
| messages.push(assistant); |
| els.userInput.value = ''; |
| resizeInput(); |
| renderMessages(); |
| updateCurl(); |
| updateSendState(); |
| |
| activeController = new AbortController(); |
| setRunning(true); |
| setStatusKey('statusSent'); |
| |
| try { |
| if (payload.stream) { |
| const streamedText = await runStream(payload, assistant, activeController.signal); |
| if (!streamedText.trim()) { |
| setStatusKey('statusStreamFallback'); |
| assistant.pending = true; |
| assistant.content = ''; |
| renderMessages(); |
| await runNonStream( |
| {...payload, stream: false, temperature: Math.min(payload.temperature, 0.2)}, |
| assistant, |
| activeController.signal |
| ); |
| } |
| } else { |
| await runNonStream(payload, assistant, activeController.signal); |
| } |
| if (!assistant.content.trim() && payload.temperature > 0.2) { |
| setStatusKey('statusStableFallback'); |
| assistant.pending = true; |
| assistant.content = ''; |
| renderMessages(); |
| await runNonStream({...payload, stream: false, temperature: 0.2}, assistant, activeController.signal); |
| } |
| if (!assistant.content.trim()) { |
| throw new Error(t('emptyOutputFinal')); |
| } |
| assistant.pending = false; |
| setStatusKey('statusDone'); |
| } catch (error) { |
| assistant.pending = false; |
| assistant.role = 'error'; |
| assistant.content = error.name === 'AbortError' ? t('requestStopped') : String(error.message || error); |
| setStatusKey(error.name === 'AbortError' ? 'statusStopped' : 'statusFailed'); |
| } finally { |
| activeController = null; |
| setRunning(false); |
| renderMessages(); |
| loadRuntime(); |
| loadLogs(); |
| } |
| } |
| |
| async function runNonStream(body, assistant, signal) { |
| const res = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({...body, stream: false}), |
| signal |
| }); |
| const data = await res.json(); |
| lastRawEvents = [data]; |
| if (!res.ok) { |
| throw new Error(errorMessageFromPayload(data)); |
| } |
| const message = data.choices?.[0]?.message || {}; |
| assistant.content = message.content || formatToolCalls(message.tool_calls); |
| assistant.pending = false; |
| renderMessages(); |
| return assistant.content; |
| } |
| |
| async function runStream(body, assistant, signal) { |
| const res = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({...body, stream: true}), |
| signal |
| }); |
| if (!res.ok) { |
| let data = {}; |
| try { |
| data = await res.json(); |
| } catch (_) { |
| data = {error: {message: await res.text()}}; |
| } |
| throw new Error(errorMessageFromPayload(data)); |
| } |
| |
| const reader = res.body.getReader(); |
| const decoder = new TextDecoder(); |
| let buffer = ''; |
| let text = ''; |
| lastRawEvents = []; |
| |
| while (true) { |
| const chunk = await reader.read(); |
| if (chunk.done) break; |
| buffer += decoder.decode(chunk.value, {stream: true}); |
| const parts = buffer.split('\n\n'); |
| buffer = parts.pop() || ''; |
| for (const part of parts) { |
| const lines = part.split('\n').filter((line) => line.startsWith('data:')); |
| for (const line of lines) { |
| const payload = line.slice(5).trim(); |
| if (!payload) continue; |
| if (payload === '[DONE]') return text; |
| const event = JSON.parse(payload); |
| lastRawEvents.push(event); |
| if (event.error) { |
| throw new Error(errorMessageFromPayload(event)); |
| } |
| const delta = event.choices?.[0]?.delta?.content || ''; |
| const toolCalls = event.choices?.[0]?.delta?.tool_calls; |
| if (Array.isArray(toolCalls) && toolCalls.length) { |
| text = formatToolCalls(toolCalls); |
| assistant.content = text; |
| assistant.pending = false; |
| setStatusKey('statusGenerating'); |
| renderMessages(); |
| } |
| if (delta) { |
| text += delta; |
| assistant.content = text; |
| assistant.pending = false; |
| setStatusKey('statusGenerating'); |
| renderMessages(); |
| } |
| } |
| } |
| } |
| return text; |
| } |
| |
| function stopRun() { |
| if (activeController) activeController.abort(); |
| } |
| |
| function clearChat() { |
| if (activeController) activeController.abort(); |
| messages = []; |
| lastUserText = ''; |
| lastRawEvents = []; |
| els.userInput.value = ''; |
| resizeInput(); |
| renderMessages(); |
| setStatusKey('statusReady'); |
| updateCurl(); |
| setRunning(false); |
| } |
| |
| function retryLast() { |
| if (lastUserText && !activeController) { |
| sendMessage(lastUserText); |
| } |
| } |
| |
| function addKv(container, rows) { |
| container.textContent = ''; |
| for (const [key, value] of rows) { |
| const k = document.createElement('span'); |
| k.textContent = key; |
| const v = document.createElement('span'); |
| v.className = 'mono'; |
| v.textContent = String(value ?? 'n/a'); |
| container.append(k, v); |
| } |
| } |
| |
| async function loadModels() { |
| try { |
| const res = await fetch('/v1/models'); |
| const data = await res.json(); |
| const selected = els.model.value || DEFAULT_MODEL; |
| els.model.textContent = ''; |
| modelBackends = {}; |
| for (const item of data.data || []) { |
| const option = document.createElement('option'); |
| option.value = item.id; |
| option.textContent = item.id; |
| option.title = item.id; |
| modelBackends[item.id] = item.inference_backend || item.backend || backendForModel(item.id); |
| if (item.id === selected || (!selected && item.id === DEFAULT_MODEL)) option.selected = true; |
| els.model.appendChild(option); |
| } |
| if (!els.model.value && Array.from(els.model.options).some((option) => option.value === DEFAULT_MODEL)) { |
| els.model.value = DEFAULT_MODEL; |
| } |
| syncModelPill(); |
| } catch (error) { |
| console.warn(error); |
| } |
| } |
| |
| function renderRuntime(data, syncControls) { |
| const runtime = data.runtime || {}; |
| const runtimeReady = Boolean(data.ok && runtime.bin_exists && runtime.model_file_exists); |
| const modelCached = Boolean(runtime.model_file_exists); |
| const streamReady = Boolean(data.ok); |
| setBadge(els.runtimeBadge, runtimeReady ? t('runtimeReady') : t('runtimePending'), runtimeReady ? 'good' : 'warn'); |
| setBadge(els.modelBadge, modelCached ? t('modelCached') : t('modelMissing'), modelCached ? 'good' : 'warn'); |
| setBadge(els.streamBadge, streamReady ? t('streamOn') : t('streamOff'), streamReady ? 'good' : 'warn'); |
| runtimeDefaults = normalizedDefaults({ |
| model: els.model.value || DEFAULT_MODEL, |
| maxTokens: data.limits?.default_max_tokens, |
| maxMaxTokens: data.limits?.max_max_tokens, |
| ctxSize: 4096, |
| maxCtxSize: 4096, |
| temperature: runtimeDefaults.temperature, |
| topP: runtimeDefaults.topP, |
| topK: runtimeDefaults.topK, |
| minP: runtimeDefaults.minP, |
| presencePenalty: runtimeDefaults.presencePenalty, |
| repetitionPenalty: runtimeDefaults.repetitionPenalty, |
| backendLimits: { |
| gpu: { |
| default_max_tokens: data.limits?.default_max_tokens, |
| max_max_tokens: data.limits?.max_max_tokens, |
| ctx_size: 4096, |
| max_ctx_size: 4096 |
| } |
| }, |
| stream: true, |
| includeUsage: true |
| }); |
| if (syncControls) { |
| els.maxTokens.max = runtimeDefaults.maxMaxTokens || runtimeDefaults.maxTokens; |
| els.ctxSize.max = Math.max(1024, runtimeDefaults.maxCtxSize || runtimeDefaults.ctxSize || 4096); |
| if (!hasSavedParameterSettings) { |
| setControlValues(runtimeDefaults); |
| } |
| } |
| els.modelLine.textContent = t('modelLine', { |
| model: data.default_model || DEFAULT_MODEL, |
| quant: DEFAULT_QUANT |
| }); |
| if (els.modelSource) { |
| els.modelSource.href = APP.modelSourceUrl; |
| els.modelSource.textContent = APP.modelSourceUrl; |
| } |
| addKv(els.runtimeKv, [ |
| [t('kvZeroGPU'), data.zero_gpu_size], |
| [t('kvRunner'), runtime.bin_exists ? runtime.bin : 'missing'], |
| [t('kvStream'), 'final delta'], |
| [t('kvDefaultQuant'), DEFAULT_QUANT], |
| [t('kvSource'), APP.modelSourceUrl], |
| [t('kvDefaultMax'), data.limits?.default_max_tokens], |
| [t('kvDefaultCtx'), 4096], |
| [t('kvTemperature'), runtimeDefaults.temperature], |
| [t('kvTopP'), runtimeDefaults.topP], |
| [t('kvTopK'), runtimeDefaults.topK], |
| [t('kvMinP'), runtimeDefaults.minP], |
| [t('kvPresence'), runtimeDefaults.presencePenalty], |
| [t('kvRepetition'), runtimeDefaults.repetitionPenalty], |
| [t('kvModelCached'), runtime.model_file_exists], |
| [t('kvModelGB'), runtime.model_file], |
| [t('kvBackend'), 'GPU'], |
| [t('kvLastQuant'), DEFAULT_QUANT], |
| [t('kvLastOutput'), data.service], |
| [t('kvLastDuration'), data.limits?.cli_timeout_seconds ? data.limits.cli_timeout_seconds + 's timeout' : 'n/a'], |
| [t('kvDataDir'), data.paths?.data_dir] |
| ]); |
| } |
| |
| async function loadRuntime() { |
| try { |
| const res = await fetch('/v2/runtime'); |
| const data = await res.json(); |
| lastRuntimeData = data; |
| renderRuntime(data, true); |
| } catch (error) { |
| setBadge(els.runtimeBadge, t('runtimeError'), 'bad'); |
| } |
| } |
| |
| async function probeGpu() { |
| setBadge(els.gpuBadge, t('gpuProbing'), 'warn'); |
| try { |
| const res = await fetch('/v2/zerogpu/probe?cli=1'); |
| const data = await res.json(); |
| setBadge(els.gpuBadge, data.ok ? t('gpuAllocated') : t('gpuUnavailable'), data.ok ? 'good' : 'bad'); |
| setStatusKey('statusGpuDone'); |
| if (data.cuda_visible_devices) { |
| els.chatStatus.textContent = 'GPU: ' + data.cuda_visible_devices; |
| } |
| loadLogs(); |
| } catch (error) { |
| setBadge(els.gpuBadge, t('gpuProbeFailed'), 'bad'); |
| setStatusKey('statusGpuFailed'); |
| } |
| } |
| |
| async function loadLogs() { |
| try { |
| const limit = encodeURIComponent(els.logLimit.value || '40'); |
| const event = els.logEvent.value.trim(); |
| const requestId = els.logRequestId.value.trim(); |
| const url = '/v2/logs?limit=' + limit |
| + (event ? '&event=' + encodeURIComponent(event) : '') |
| + (requestId ? '&request_id=' + encodeURIComponent(requestId) : ''); |
| const res = await fetch(url); |
| const data = await res.json(); |
| els.logs.textContent = ''; |
| const rows = data.data || []; |
| if (!rows.length) { |
| const empty = document.createElement('div'); |
| empty.className = 'log-item'; |
| empty.textContent = t('logsEmpty'); |
| els.logs.appendChild(empty); |
| return; |
| } |
| for (const item of rows.slice().reverse()) { |
| const row = document.createElement('div'); |
| row.className = 'log-item'; |
| const title = document.createElement('strong'); |
| title.textContent = item.event || 'event'; |
| const body = document.createElement('div'); |
| body.className = 'mono'; |
| body.textContent = (item.ts || '') + ' ' + JSON.stringify(item); |
| row.append(title, body); |
| els.logs.appendChild(row); |
| } |
| } catch (error) { |
| els.logs.textContent = t('logsFailed', {message: String(error)}); |
| } |
| } |
| |
| async function loadCatalog() { |
| try { |
| const res = await fetch('/v2'); |
| const data = await res.json(); |
| els.apiList.textContent = ''; |
| const note = document.createElement('div'); |
| note.className = 'api-note'; |
| note.textContent = t('apiCompatNote'); |
| els.apiList.appendChild(note); |
| for (const item of data.endpoints || []) { |
| const row = document.createElement('div'); |
| row.className = 'api-row'; |
| const method = document.createElement('strong'); |
| method.textContent = item.method; |
| const path = document.createElement('code'); |
| path.textContent = item.path; |
| row.append(method, path); |
| els.apiList.appendChild(row); |
| } |
| } catch (error) { |
| console.warn(error); |
| } |
| } |
| |
| function resizeInput() { |
| els.userInput.style.height = 'auto'; |
| els.userInput.style.height = Math.min(180, Math.max(46, els.userInput.scrollHeight)) + 'px'; |
| } |
| |
| function setSettingsTab(tabName) { |
| const name = tabName || 'general'; |
| document.querySelectorAll('[data-settings-tab]').forEach((button) => { |
| button.classList.toggle('active', button.dataset.settingsTab === name); |
| }); |
| document.querySelectorAll('[data-settings-panel]').forEach((panel) => { |
| panel.classList.toggle('active', panel.dataset.settingsPanel === name); |
| }); |
| } |
| |
| function openSettings(tabName) { |
| setSettingsTab(tabName || 'general'); |
| els.settingsOverlay.hidden = false; |
| document.body.style.overflow = 'hidden'; |
| } |
| |
| function closeSettings() { |
| els.settingsOverlay.hidden = true; |
| document.body.style.overflow = ''; |
| els.settingsBtn.focus(); |
| } |
| |
| function resetSettings() { |
| setTheme('system'); |
| setLanguage('zh'); |
| localStorage.removeItem(SETTINGS_KEY); |
| hasSavedParameterSettings = false; |
| setControlValues(runtimeDefaults); |
| loadRuntime(); |
| } |
| |
| els.composer.addEventListener('submit', (event) => { |
| event.preventDefault(); |
| sendMessage(); |
| }); |
| |
| els.userInput.addEventListener('keydown', (event) => { |
| if (event.key === 'Enter' && !event.shiftKey) { |
| event.preventDefault(); |
| els.composer.requestSubmit(); |
| } |
| }); |
| |
| els.userInput.addEventListener('input', () => { |
| resizeInput(); |
| updateCurl(); |
| }); |
| |
| for (const id of ['model', 'maxTokens', 'ctxSize', 'temperature', 'topP', 'topK', 'minP', 'presencePenalty', 'repetitionPenalty', 'stream', 'includeUsage']) { |
| els[id].addEventListener('change', updateCurl); |
| els[id].addEventListener('input', updateCurl); |
| } |
| els.model.addEventListener('change', handleModelChange); |
| |
| els.langZh.addEventListener('click', () => setLanguage('zh')); |
| els.langEn.addEventListener('click', () => setLanguage('en')); |
| els.themeSystem.addEventListener('click', () => setTheme('system')); |
| els.themeLight.addEventListener('click', () => setTheme('light')); |
| els.themeDark.addEventListener('click', () => setTheme('dark')); |
| els.panelToggle.addEventListener('click', togglePanel); |
| els.stablePresetBtn.addEventListener('click', applyStablePreset); |
| els.longPresetBtn.addEventListener('click', applyLongPreset); |
| els.stopBtn.addEventListener('click', stopRun); |
| els.newChatBtn.addEventListener('click', clearChat); |
| els.retryBtn.addEventListener('click', retryLast); |
| els.refreshRuntimeBtn.addEventListener('click', loadRuntime); |
| els.probeGpuBtn.addEventListener('click', probeGpu); |
| els.refreshLogsBtn.addEventListener('click', loadLogs); |
| els.logRequestId.addEventListener('keydown', (event) => { |
| if (event.key === 'Enter') loadLogs(); |
| }); |
| els.logEvent.addEventListener('keydown', (event) => { |
| if (event.key === 'Enter') loadLogs(); |
| }); |
| els.copyCurlBtn.addEventListener('click', copyCurl); |
| els.settingsBtn.addEventListener('click', () => openSettings('general')); |
| els.settingsClose.addEventListener('click', closeSettings); |
| els.settingsSave.addEventListener('click', saveCurrentSettings); |
| els.settingsReset.addEventListener('click', resetSettings); |
| els.settingsOverlay.addEventListener('click', (event) => { |
| if (event.target === els.settingsOverlay) closeSettings(); |
| }); |
| document.addEventListener('keydown', (event) => { |
| if (event.key === 'Escape' && !els.settingsOverlay.hidden) closeSettings(); |
| }); |
| document.querySelectorAll('[data-settings-tab]').forEach((button) => { |
| button.addEventListener('click', () => setSettingsTab(button.dataset.settingsTab)); |
| }); |
| document.querySelectorAll('[data-settings-open]').forEach((button) => { |
| button.addEventListener('click', () => openSettings(button.dataset.settingsOpen || 'general')); |
| }); |
| if (themeMedia) { |
| const onThemeMediaChange = () => { |
| if (currentTheme === 'system') applyTheme(); |
| }; |
| if (themeMedia.addEventListener) { |
| themeMedia.addEventListener('change', onThemeMediaChange); |
| } else if (themeMedia.addListener) { |
| themeMedia.addListener(onThemeMediaChange); |
| } |
| } |
| |
| setControlValues(runtimeDefaults); |
| loadSavedSettings(); |
| applyI18n(); |
| renderMessages(); |
| resizeInput(); |
| updateCurl(); |
| updateSendState(); |
| loadModels().then(updateCurl); |
| loadCatalog(); |
| loadRuntime(); |
| loadLogs(); |
| </script> |
| </body> |
| </html> |
|
|