Download index.html from Viraj2307/my-llm: direct link, hf CLI and curl.
- Browser
- Download file 29.8 kB
-
https://huggingface.co/spaces/Viraj2307/my-llm/resolve/main/index.html
- Command line
-
hf download hf://spaces/Viraj2307/my-llm/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Viraj2307/my-llm/resolve/main/index.html
29.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>MY-LLM</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #050508; | |
| --bg2: #08080e; | |
| --surface: #0c0c15; | |
| --surface2: #10101c; | |
| --border: rgba(255,255,255,0.06); | |
| --border2: rgba(255,255,255,0.1); | |
| --accent: #00ff88; | |
| --accent2: #00cc6a; | |
| --accent-dim: rgba(0,255,136,0.08); | |
| --accent-glow:rgba(0,255,136,0.15); | |
| --amber: #ffaa00; | |
| --red: #ff4466; | |
| --blue: #4488ff; | |
| --text: #f0f0f0; | |
| --text2: #888; | |
| --text3: #444; | |
| --mono: 'Space Mono', monospace; | |
| --sans: 'Syne', sans-serif; | |
| --radius: 4px; | |
| } | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| overflow: hidden; | |
| } | |
| /* Animated grid background */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0,255,136,0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,255,136,0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| body::after { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(0,255,136,0.04) 0%, transparent 70%); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* ββββββββββββββ LAYOUT ββββββββββββββ */ | |
| .shell { | |
| position: relative; | |
| z-index: 1; | |
| display: grid; | |
| grid-template-rows: 48px 1fr; | |
| grid-template-columns: 260px 1fr; | |
| height: 100vh; | |
| } | |
| /* ββββββββββββββ TOPBAR ββββββββββββββ */ | |
| .topbar { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(5,5,8,0.95); | |
| backdrop-filter: blur(20px); | |
| position: relative; | |
| } | |
| .topbar::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; left: 0; right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--accent), transparent); | |
| opacity: 0.3; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .brand-mark { | |
| width: 24px; | |
| height: 24px; | |
| position: relative; | |
| } | |
| .brand-mark svg { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .brand-name { | |
| font-family: var(--sans); | |
| font-size: 14px; | |
| font-weight: 800; | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| color: var(--text); | |
| } | |
| .brand-slash { | |
| color: var(--accent); | |
| margin: 0 2px; | |
| } | |
| .topbar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text2); | |
| padding: 4px 10px; | |
| border: 1px solid var(--border2); | |
| border-radius: 2px; | |
| letter-spacing: 0.05em; | |
| } | |
| .pulse { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| position: relative; | |
| } | |
| .pulse::after { | |
| content: ''; | |
| position: absolute; | |
| inset: -3px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| animation: ripple 2s ease-out infinite; | |
| } | |
| @keyframes ripple { | |
| 0% { transform: scale(1); opacity: 0.4; } | |
| 100% { transform: scale(2.5); opacity: 0; } | |
| } | |
| .pulse.offline { background: var(--red); } | |
| .pulse.offline::after { background: var(--red); animation: none; } | |
| .topbar-btn { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text3); | |
| background: none; | |
| border: 1px solid var(--border); | |
| border-radius: 2px; | |
| padding: 4px 10px; | |
| cursor: pointer; | |
| letter-spacing: 0.05em; | |
| transition: all 0.15s; | |
| text-transform: uppercase; | |
| } | |
| .topbar-btn:hover { | |
| color: var(--text2); | |
| border-color: var(--border2); | |
| } | |
| /* ββββββββββββββ SIDEBAR ββββββββββββββ */ | |
| .sidebar { | |
| background: var(--bg2); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .sidebar-section { | |
| padding: 20px 18px 16px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .sidebar-section:last-child { border-bottom: none; } | |
| .section-title { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--text3); | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .section-title::after { | |
| content: ''; | |
| flex: 1; | |
| height: 1px; | |
| background: var(--border); | |
| } | |
| /* Model card */ | |
| .model-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius); | |
| padding: 12px 14px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .model-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, var(--accent), transparent); | |
| } | |
| .model-id { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--accent); | |
| margin-bottom: 10px; | |
| letter-spacing: 0.05em; | |
| } | |
| .model-rows { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .model-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .model-row-key { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text3); | |
| } | |
| .model-row-val { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text2); | |
| } | |
| .model-row-val.accent { color: var(--accent); } | |
| /* Params */ | |
| .param-block { | |
| margin-bottom: 14px; | |
| } | |
| .param-block:last-child { margin-bottom: 0; } | |
| .param-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 6px; | |
| } | |
| .param-name { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text2); | |
| letter-spacing: 0.05em; | |
| } | |
| .param-value { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--accent); | |
| } | |
| .track { | |
| height: 3px; | |
| background: var(--surface2); | |
| border-radius: 0; | |
| position: relative; | |
| cursor: pointer; | |
| } | |
| .track-fill { | |
| position: absolute; | |
| left: 0; top: 0; bottom: 0; | |
| background: var(--accent); | |
| border-radius: 0; | |
| transition: width 0.1s; | |
| } | |
| .track input[type=range] { | |
| position: absolute; | |
| inset: -6px 0; | |
| width: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| height: 16px; | |
| } | |
| /* Endpoints */ | |
| .ep-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .ep-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 10px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| } | |
| .ep-item:hover { border-color: var(--border2); } | |
| .ep-item.active { | |
| border-color: rgba(0,255,136,0.3); | |
| background: var(--accent-dim); | |
| } | |
| .ep-method { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--accent); | |
| background: rgba(0,255,136,0.1); | |
| padding: 2px 5px; | |
| border-radius: 2px; | |
| } | |
| .ep-path { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text2); | |
| letter-spacing: 0.03em; | |
| } | |
| .ep-dot { | |
| margin-left: auto; | |
| width: 5px; height: 5px; | |
| border-radius: 50%; | |
| border: 1px solid var(--border2); | |
| } | |
| .ep-item.active .ep-dot { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| /* Stats in sidebar bottom */ | |
| .sidebar-stats { | |
| margin-top: auto; | |
| padding: 14px 18px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .stat-cell { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 8px 10px; | |
| } | |
| .stat-label { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--text3); | |
| letter-spacing: 0.05em; | |
| margin-bottom: 4px; | |
| text-transform: uppercase; | |
| } | |
| .stat-val { | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| color: var(--text); | |
| font-weight: 700; | |
| } | |
| .stat-val.accent { color: var(--accent); } | |
| /* ββββββββββββββ CHAT ββββββββββββββ */ | |
| .chat { | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| background: var(--bg); | |
| } | |
| /* Messages */ | |
| .messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 32px 32px 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0; | |
| scroll-behavior: smooth; | |
| } | |
| .messages::-webkit-scrollbar { width: 3px; } | |
| .messages::-webkit-scrollbar-track { background: transparent; } | |
| .messages::-webkit-scrollbar-thumb { background: var(--border2); } | |
| /* Empty */ | |
| .empty { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0; | |
| padding-bottom: 40px; | |
| } | |
| .empty-glyph { | |
| font-family: var(--mono); | |
| font-size: 80px; | |
| line-height: 1; | |
| color: var(--accent); | |
| opacity: 0.15; | |
| margin-bottom: 16px; | |
| animation: float 4s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-8px); } | |
| } | |
| .empty-headline { | |
| font-family: var(--sans); | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: var(--text); | |
| margin-bottom: 8px; | |
| letter-spacing: -0.02em; | |
| opacity: 0.5; | |
| } | |
| .empty-sub { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--text3); | |
| letter-spacing: 0.08em; | |
| margin-bottom: 32px; | |
| } | |
| .chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| justify-content: center; | |
| max-width: 480px; | |
| } | |
| .chip { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--text2); | |
| padding: 7px 14px; | |
| border: 1px solid var(--border2); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| letter-spacing: 0.03em; | |
| } | |
| .chip:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| background: var(--accent-dim); | |
| transform: translateY(-1px); | |
| } | |
| /* Message blocks */ | |
| .msg-group { | |
| margin-bottom: 28px; | |
| animation: msgIn 0.25s cubic-bezier(0.16,1,0.3,1); | |
| } | |
| @keyframes msgIn { | |
| from { opacity: 0; transform: translateY(12px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .msg-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .msg-role-tag { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| padding: 2px 8px; | |
| border-radius: 2px; | |
| } | |
| .msg-group.user .msg-role-tag { | |
| color: var(--blue); | |
| background: rgba(68,136,255,0.1); | |
| border: 1px solid rgba(68,136,255,0.2); | |
| } | |
| .msg-group.assistant .msg-role-tag { | |
| color: var(--accent); | |
| background: var(--accent-dim); | |
| border: 1px solid rgba(0,255,136,0.2); | |
| } | |
| .msg-time { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text3); | |
| } | |
| .msg-perf { | |
| margin-left: auto; | |
| display: flex; | |
| gap: 12px; | |
| } | |
| .perf-item { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text3); | |
| } | |
| .perf-item span { color: var(--accent); } | |
| .msg-bubble { | |
| padding: 14px 18px; | |
| border-radius: var(--radius); | |
| font-size: 14px; | |
| line-height: 1.75; | |
| position: relative; | |
| } | |
| .msg-group.user .msg-bubble { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| border-left: 2px solid rgba(68,136,255,0.4); | |
| } | |
| .msg-group.assistant .msg-bubble { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| border-left: 2px solid rgba(0,255,136,0.4); | |
| } | |
| .msg-text { | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| /* Thinking */ | |
| .thinking-dots { | |
| display: flex; | |
| gap: 5px; | |
| align-items: center; | |
| padding: 4px 0; | |
| } | |
| .thinking-dots span { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| animation: dot 1.4s ease-in-out infinite; | |
| opacity: 0.3; | |
| } | |
| .thinking-dots span:nth-child(2) { animation-delay: 0.2s; } | |
| .thinking-dots span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes dot { | |
| 0%, 60%, 100% { transform: scale(0.8); opacity: 0.3; } | |
| 30% { transform: scale(1.2); opacity: 1; } | |
| } | |
| /* Streaming cursor */ | |
| .cursor { | |
| display: inline-block; | |
| width: 2px; | |
| height: 1em; | |
| background: var(--accent); | |
| margin-left: 2px; | |
| vertical-align: text-bottom; | |
| animation: blink 1s step-end infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| /* ββββββββββββββ INPUT ββββββββββββββ */ | |
| .input-zone { | |
| padding: 16px 32px 20px; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg2); | |
| position: relative; | |
| } | |
| .input-zone::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--accent), transparent); | |
| opacity: 0.2; | |
| } | |
| .input-shell { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| } | |
| .input-wrapper { | |
| flex: 1; | |
| background: var(--surface); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius); | |
| transition: border-color 0.15s, box-shadow 0.15s; | |
| position: relative; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: rgba(0,255,136,0.4); | |
| box-shadow: 0 0 0 3px rgba(0,255,136,0.05), 0 0 20px rgba(0,255,136,0.05); | |
| } | |
| .input-prefix { | |
| position: absolute; | |
| left: 14px; | |
| top: 14px; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--accent); | |
| pointer-events: none; | |
| opacity: 0.6; | |
| } | |
| textarea { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| line-height: 1.6; | |
| padding: 13px 14px 13px 30px; | |
| resize: none; | |
| min-height: 46px; | |
| max-height: 160px; | |
| letter-spacing: 0.02em; | |
| } | |
| textarea::placeholder { color: var(--text3); } | |
| .input-footer { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 14px 8px 30px; | |
| } | |
| .input-hint-text { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--text3); | |
| letter-spacing: 0.06em; | |
| } | |
| .char-count { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--text3); | |
| } | |
| .send-btn { | |
| width: 46px; height: 46px; | |
| background: var(--accent); | |
| border: none; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.15s; | |
| flex-shrink: 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .send-btn::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent); | |
| } | |
| .send-btn:hover { | |
| background: var(--accent2); | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(0,255,136,0.3); | |
| } | |
| .send-btn:active { transform: translateY(0); } | |
| .send-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; box-shadow: none; } | |
| .send-btn svg { width: 18px; height: 18px; fill: var(--bg); } | |
| /* Loading bar */ | |
| .loading-bar { | |
| position: fixed; | |
| top: 48px; | |
| left: 0; right: 0; | |
| height: 2px; | |
| z-index: 100; | |
| overflow: hidden; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .loading-bar.active { opacity: 1; } | |
| .loading-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, var(--accent), transparent); | |
| animation: loading 1.5s ease-in-out infinite; | |
| width: 60%; | |
| } | |
| @keyframes loading { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(250%); } | |
| } | |
| /* Error toast */ | |
| .toast { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| background: rgba(255,68,102,0.1); | |
| border: 1px solid rgba(255,68,102,0.3); | |
| color: var(--red); | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 10px 16px; | |
| border-radius: var(--radius); | |
| z-index: 200; | |
| transform: translateY(20px); | |
| opacity: 0; | |
| transition: all 0.2s; | |
| letter-spacing: 0.03em; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 3px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border2); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="loading-bar" id="loading-bar"> | |
| <div class="loading-bar-fill"></div> | |
| </div> | |
| <div class="shell"> | |
| <!-- TOPBAR --> | |
| <header class="topbar"> | |
| <div class="brand"> | |
| <div class="brand-mark"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect x="2" y="2" width="20" height="20" rx="2" stroke="#00ff88" stroke-width="1.5"/> | |
| <path d="M6 12h12M12 6v12" stroke="#00ff88" stroke-width="1.5" stroke-linecap="round"/> | |
| <circle cx="12" cy="12" r="3" fill="none" stroke="#00ff88" stroke-width="1"/> | |
| </svg> | |
| </div> | |
| <div class="brand-name">MY<span class="brand-slash">-</span>LLM</div> | |
| </div> | |
| <div class="topbar-right"> | |
| <div class="status-badge"> | |
| <div class="pulse" id="pulse"></div> | |
| <span id="status-text">connecting</span> | |
| </div> | |
| <button class="topbar-btn" onclick="clearChat()">clear</button> | |
| </div> | |
| </header> | |
| <!-- SIDEBAR --> | |
| <aside class="sidebar"> | |
| <div class="sidebar-section"> | |
| <div class="section-title">model</div> | |
| <div class="model-card"> | |
| <div class="model-id" id="model-id">my-llm v1.0</div> | |
| <div class="model-rows"> | |
| <div class="model-row"> | |
| <div class="model-row-key">params</div> | |
| <div class="model-row-val accent" id="stat-params">β</div> | |
| </div> | |
| <div class="model-row"> | |
| <div class="model-row-key">arch</div> | |
| <div class="model-row-val">decoder</div> | |
| </div> | |
| <div class="model-row"> | |
| <div class="model-row-key">quant</div> | |
| <div class="model-row-val">int8</div> | |
| </div> | |
| <div class="model-row"> | |
| <div class="model-row-key">device</div> | |
| <div class="model-row-val">cpu</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="section-title">parameters</div> | |
| <div class="param-block"> | |
| <div class="param-header"> | |
| <div class="param-name">temperature</div> | |
| <div class="param-value" id="temp-val">0.80</div> | |
| </div> | |
| <div class="track"> | |
| <div class="track-fill" id="temp-fill" style="width:35%"></div> | |
| <input type="range" min="0.1" max="2.0" step="0.05" value="0.8" | |
| oninput="updateParam('temp',this.value);updateFill('temp',this.value,0.1,2.0)"> | |
| </div> | |
| </div> | |
| <div class="param-block"> | |
| <div class="param-header"> | |
| <div class="param-name">top_k</div> | |
| <div class="param-value" id="topk-val">40</div> | |
| </div> | |
| <div class="track"> | |
| <div class="track-fill" id="topk-fill" style="width:39%"></div> | |
| <input type="range" min="1" max="100" step="1" value="40" | |
| oninput="updateParam('topk',this.value);updateFill('topk',this.value,1,100)"> | |
| </div> | |
| </div> | |
| <div class="param-block"> | |
| <div class="param-header"> | |
| <div class="param-name">max_tokens</div> | |
| <div class="param-value" id="maxtok-val">256</div> | |
| </div> | |
| <div class="track"> | |
| <div class="track-fill" id="maxtok-fill" style="width:49%"></div> | |
| <input type="range" min="16" max="512" step="16" value="256" | |
| oninput="updateParam('maxtok',this.value);updateFill('maxtok',this.value,16,512)"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="section-title">endpoint</div> | |
| <div class="ep-list"> | |
| <div class="ep-item active" id="ep-chat" onclick="setEndpoint('chat')"> | |
| <div class="ep-method">POST</div> | |
| <div class="ep-path">/chat</div> | |
| <div class="ep-dot"></div> | |
| </div> | |
| <div class="ep-item" id="ep-generate" onclick="setEndpoint('generate')"> | |
| <div class="ep-method">POST</div> | |
| <div class="ep-path">/generate</div> | |
| <div class="ep-dot"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-stats"> | |
| <div class="section-title">last run</div> | |
| <div class="stats-grid"> | |
| <div class="stat-cell"> | |
| <div class="stat-label">time</div> | |
| <div class="stat-val" id="stat-time">β</div> | |
| </div> | |
| <div class="stat-cell"> | |
| <div class="stat-label">tok/s</div> | |
| <div class="stat-val accent" id="stat-speed">β</div> | |
| </div> | |
| <div class="stat-cell"> | |
| <div class="stat-label">tokens</div> | |
| <div class="stat-val" id="stat-tokens">β</div> | |
| </div> | |
| <div class="stat-cell"> | |
| <div class="stat-label">port</div> | |
| <div class="stat-val">3001</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- CHAT --> | |
| <main class="chat"> | |
| <div class="messages" id="messages"> | |
| <div class="empty" id="empty-state"> | |
| <div class="empty-glyph">⬑</div> | |
| <div class="empty-headline">Ready to inference</div> | |
| <div class="empty-sub">model loaded Β· awaiting prompt</div> | |
| <div class="chips"> | |
| <div class="chip" onclick="suggest('Explain how transformers work')">transformers</div> | |
| <div class="chip" onclick="suggest('Once upon a time in a digital world')">story</div> | |
| <div class="chip" onclick="suggest('Write a Python function to sort a list')">code</div> | |
| <div class="chip" onclick="suggest('What is the future of AI?')">future of AI</div> | |
| <div class="chip" onclick="suggest('Write a haiku about neural networks')">haiku</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-zone"> | |
| <div class="input-shell"> | |
| <div class="input-wrapper"> | |
| <div class="input-prefix">></div> | |
| <textarea | |
| id="prompt-input" | |
| placeholder="enter prompt..." | |
| rows="1" | |
| onkeydown="handleKey(event)" | |
| oninput="autoResize(this);updateCharCount(this)" | |
| ></textarea> | |
| <div class="input-footer"> | |
| <div class="input-hint-text">β΅ send Β· shift+β΅ newline</div> | |
| <div class="char-count" id="char-count">0</div> | |
| </div> | |
| </div> | |
| <button class="send-btn" id="send-btn" onclick="sendMessage()" title="Send"> | |
| <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M22 2L11 13M22 2L15 22L11 13M22 2L2 9L11 13"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| const API = window.location.origin; | |
| let endpoint = 'chat'; | |
| let params = { temperature: 0.8, top_k: 40, max_tokens: 256 }; | |
| let isLoading = false; | |
| // βββ Health βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkHealth() { | |
| try { | |
| const r = await fetch(`${API}/health`, { signal: AbortSignal.timeout(3000) }); | |
| const d = await r.json(); | |
| document.getElementById('pulse').className = 'pulse'; | |
| document.getElementById('status-text').textContent = 'online'; | |
| if (d.params) { | |
| document.getElementById('stat-params').textContent = | |
| (d.params / 1e6).toFixed(1) + 'M'; | |
| } | |
| } catch { | |
| document.getElementById('pulse').className = 'pulse offline'; | |
| document.getElementById('status-text').textContent = 'offline'; | |
| } | |
| } | |
| checkHealth(); | |
| setInterval(checkHealth, 10000); | |
| // βββ Param sliders ββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateFill(key, val, min, max) { | |
| const pct = ((val - min) / (max - min)) * 100; | |
| document.getElementById(`${key}-fill`).style.width = pct + '%'; | |
| } | |
| function updateParam(key, val) { | |
| const v = parseFloat(val); | |
| if (key === 'temp') { params.temperature = v; document.getElementById('temp-val').textContent = v.toFixed(2); } | |
| if (key === 'topk') { params.top_k = parseInt(val); document.getElementById('topk-val').textContent = val; } | |
| if (key === 'maxtok') { params.max_tokens = parseInt(val); document.getElementById('maxtok-val').textContent = val; } | |
| } | |
| // βββ Endpoint βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setEndpoint(ep) { | |
| endpoint = ep; | |
| document.getElementById('ep-chat').classList.toggle('active', ep === 'chat'); | |
| document.getElementById('ep-generate').classList.toggle('active', ep === 'generate'); | |
| } | |
| // βββ Suggest βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function suggest(text) { | |
| const el = document.getElementById('prompt-input'); | |
| el.value = text; | |
| autoResize(el); | |
| updateCharCount(el); | |
| sendMessage(); | |
| } | |
| // βββ Send βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function sendMessage() { | |
| const input = document.getElementById('prompt-input'); | |
| const prompt = input.value.trim(); | |
| if (!prompt || isLoading) return; | |
| document.getElementById('empty-state').style.display = 'none'; | |
| addMessage('user', prompt); | |
| input.value = ''; | |
| autoResize(input); | |
| updateCharCount(input); | |
| const thinkId = addThinking(); | |
| setLoading(true); | |
| try { | |
| const res = await fetch(`${API}/${endpoint}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt, ...params }), | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| removeThinking(thinkId); | |
| addMessage('assistant', data.text, { | |
| time: data.time_seconds + 's', | |
| speed: data.tokens_per_sec, | |
| tokens: data.tokens_generated, | |
| }); | |
| document.getElementById('stat-time').textContent = data.time_seconds + 's'; | |
| document.getElementById('stat-speed').textContent = data.tokens_per_sec; | |
| document.getElementById('stat-tokens').textContent = data.tokens_generated; | |
| } catch (err) { | |
| removeThinking(thinkId); | |
| showToast('connection failed β is serve.py running?'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // βββ Message helpers ββββββββββββββββββββββββββββββββββββββββββ | |
| function addMessage(role, text, meta = null) { | |
| const container = document.getElementById('messages'); | |
| const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| const group = document.createElement('div'); | |
| group.className = `msg-group ${role}`; | |
| const perfHtml = meta ? ` | |
| <div class="msg-perf"> | |
| <div class="perf-item">${meta.time}</div> | |
| <div class="perf-item"><span>${meta.speed}</span> tok/s</div> | |
| <div class="perf-item"><span>${meta.tokens}</span> tok</div> | |
| </div>` : ''; | |
| group.innerHTML = ` | |
| <div class="msg-meta"> | |
| <div class="msg-role-tag">${role === 'user' ? 'you' : 'llm'}</div> | |
| <div class="msg-time">${now}</div> | |
| ${perfHtml} | |
| </div> | |
| <div class="msg-bubble"> | |
| <div class="msg-text">${esc(text)}</div> | |
| </div> | |
| `; | |
| container.appendChild(group); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function addThinking() { | |
| const container = document.getElementById('messages'); | |
| const id = 'think-' + Date.now(); | |
| const el = document.createElement('div'); | |
| el.id = id; | |
| el.className = 'msg-group assistant'; | |
| el.innerHTML = ` | |
| <div class="msg-meta"> | |
| <div class="msg-role-tag">llm</div> | |
| </div> | |
| <div class="msg-bubble"> | |
| <div class="thinking-dots"> | |
| <span></span><span></span><span></span> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(el); | |
| container.scrollTop = container.scrollHeight; | |
| return id; | |
| } | |
| function removeThinking(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.remove(); | |
| } | |
| function esc(t) { | |
| return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| // βββ UI helpers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setLoading(v) { | |
| isLoading = v; | |
| document.getElementById('send-btn').disabled = v; | |
| document.getElementById('loading-bar').classList.toggle('active', v); | |
| } | |
| function clearChat() { | |
| const c = document.getElementById('messages'); | |
| c.innerHTML = ` | |
| <div class="empty" id="empty-state"> | |
| <div class="empty-glyph">⬑</div> | |
| <div class="empty-headline">Ready to inference</div> | |
| <div class="empty-sub">model loaded Β· awaiting prompt</div> | |
| <div class="chips"> | |
| <div class="chip" onclick="suggest('Explain how transformers work')">transformers</div> | |
| <div class="chip" onclick="suggest('Once upon a time in a digital world')">story</div> | |
| <div class="chip" onclick="suggest('Write a Python function to sort a list')">code</div> | |
| <div class="chip" onclick="suggest('What is the future of AI?')">future of AI</div> | |
| <div class="chip" onclick="suggest('Write a haiku about neural networks')">haiku</div> | |
| </div> | |
| </div> | |
| `; | |
| ['stat-time','stat-speed','stat-tokens'].forEach(id => { | |
| document.getElementById(id).textContent = 'β'; | |
| }); | |
| } | |
| function showToast(msg) { | |
| const t = document.getElementById('toast'); | |
| t.textContent = msg; | |
| t.classList.add('show'); | |
| setTimeout(() => t.classList.remove('show'), 4000); | |
| } | |
| function handleKey(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function autoResize(el) { | |
| el.style.height = 'auto'; | |
| el.style.height = Math.min(el.scrollHeight, 160) + 'px'; | |
| } | |
| function updateCharCount(el) { | |
| document.getElementById('char-count').textContent = el.value.length; | |
| } | |
| </script> | |
| </body> | |
| </html> |