enCoder commited on
Commit
9dfe9bd
·
1 Parent(s): 39fa862

Enhance web client and server for improved demo functionality

Browse files

- Updated `settings.local.json` to include new web fetch commands for demo resources.
- Modified `server.py` to serve static assets directly from the root, improving compatibility with GitHub Pages.
- Enhanced `index.html` with updated metadata, navigation links, and a refined layout for better user experience.
- Revamped `style.css` to align with the new design, incorporating a cohesive color scheme and improved styling for UI elements.
- Added new routes in the server to serve additional static files, ensuring all necessary resources are accessible for the demo.

Files changed (4) hide show
  1. .claude/settings.local.json +8 -1
  2. tiny_vllm/server.py +15 -0
  3. web/index.html +29 -9
  4. web/style.css +301 -112
.claude/settings.local.json CHANGED
@@ -8,7 +8,14 @@
8
  "Bash(python -m pytest tests/ -v)",
9
  "Bash(python -c ' *)",
10
  "Bash(python scripts/make_demo_recording.py)",
11
- "Bash(python *)"
 
 
 
 
 
 
 
12
  ]
13
  }
14
  }
 
8
  "Bash(python -m pytest tests/ -v)",
9
  "Bash(python -c ' *)",
10
  "Bash(python scripts/make_demo_recording.py)",
11
+ "Bash(python *)",
12
+ "WebFetch(domain:surajsharan.github.io)",
13
+ "Bash(curl -sIL \"https://surajsharan.github.io/tiny_vLLM/events.jsonl\")",
14
+ "Bash(curl -sL \"https://surajsharan.github.io/tiny_vLLM/\")",
15
+ "Bash(curl -sL \"https://surajsharan.github.io/\")",
16
+ "Bash(curl -sL \"https://surajsharan.github.io/_next/static/css/9d0d2df30f6cb73d.css\")",
17
+ "Bash(curl -sL \"https://surajsharan.github.io/_next/static/css/0f8dc0fb86554783.css\")",
18
+ "Read(//tmp/**)"
19
  ]
20
  }
21
  }
tiny_vllm/server.py CHANGED
@@ -86,11 +86,26 @@ def build_app(config: EngineConfig) -> FastAPI:
86
 
87
  static_dir = Path(__file__).parent.parent / "web"
88
  if static_dir.exists():
 
 
 
89
  app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
90
 
91
  @app.get("/")
92
  async def root() -> FileResponse:
93
  return FileResponse(str(static_dir / "index.html"))
 
 
 
 
 
 
 
 
 
 
 
 
94
  else:
95
  @app.get("/")
96
  async def root() -> dict:
 
86
 
87
  static_dir = Path(__file__).parent.parent / "web"
88
  if static_dir.exists():
89
+ # Keep the legacy /static prefix working for anyone bookmarking it,
90
+ # but also serve assets at root so the SAME HTML works on GH Pages
91
+ # (which has no Python backend and just serves files from web/).
92
  app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
93
 
94
  @app.get("/")
95
  async def root() -> FileResponse:
96
  return FileResponse(str(static_dir / "index.html"))
97
+
98
+ @app.get("/style.css")
99
+ async def _css() -> FileResponse:
100
+ return FileResponse(str(static_dir / "style.css"))
101
+
102
+ @app.get("/app.js")
103
+ async def _js() -> FileResponse:
104
+ return FileResponse(str(static_dir / "app.js"))
105
+
106
+ @app.get("/events.jsonl")
107
+ async def _jsonl() -> FileResponse:
108
+ return FileResponse(str(static_dir / "events.jsonl"))
109
  else:
110
  @app.get("/")
111
  async def root() -> dict:
web/index.html CHANGED
@@ -3,12 +3,32 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>tiny_vllm — engine internals</title>
7
- <link rel="stylesheet" href="/static/style.css">
 
 
 
 
8
  </head>
9
  <body>
10
- <header>
11
- <h1>tiny_vllm <span class="muted">— minimal continuous-batching engine</span></h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  <div class="status">
13
  <span id="connection" class="badge offline">connecting…</span>
14
  <span id="model" class="muted"></span>
@@ -56,12 +76,12 @@
56
  <section class="card">
57
  <h2>Scheduler <span class="muted" id="sched-step"></span></h2>
58
  <div class="stats">
59
- <div class="stat"><div class="stat-label">tokens this step</div><div class="stat-value" id="stat-tokens">0</div></div>
60
  <div class="stat"><div class="stat-label">prefill / decode</div><div class="stat-value" id="stat-pfdec">0 / 0</div></div>
61
  <div class="stat"><div class="stat-label">step (ms)</div><div class="stat-value" id="stat-ms">0</div></div>
62
- <div class="stat"><div class="stat-label">prefix cache hit-rate</div><div class="stat-value" id="stat-cache">0%</div></div>
63
  <div class="stat"><div class="stat-label">free blocks</div><div class="stat-value" id="stat-free">0</div></div>
64
- <div class="stat"><div class="stat-label">preemptions (total)</div><div class="stat-value" id="stat-pre">0</div></div>
65
  </div>
66
  <h3>step log</h3>
67
  <pre id="log" class="log"></pre>
@@ -74,9 +94,9 @@
74
  </main>
75
 
76
  <footer>
77
- <span class="muted">Subscribed to <code>/engine/events</code>. Source: <a href="https://github.com/yourname/tiny_vllm" target="_blank">github</a>.</span>
78
  </footer>
79
 
80
- <script src="/static/app.js"></script>
81
  </body>
82
  </html>
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>tiny_vllm — Suraj Sharan</title>
7
+ <meta name="description" content="Minimal continuous-batching LLM engine: paged KV-cache, prefix caching, chunked prefill, SSE streaming. Live visualization of engine internals.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
11
+ <link rel="stylesheet" href="style.css">
12
  </head>
13
  <body>
14
+
15
+ <nav class="topbar">
16
+ <div class="crumb">
17
+ <a href="https://surajsharan.github.io/">suraj sharan</a>
18
+ <span class="sep">/</span>
19
+ <a href="https://surajsharan.github.io/#projects">projects</a>
20
+ <span class="sep">/</span>
21
+ <span>tiny_vllm</span>
22
+ </div>
23
+ <div class="right">
24
+ <a href="https://github.com/surajsharan/tiny_vLLM" target="_blank" rel="noopener">github ↗</a>
25
+ <a href="https://surajsharan.github.io/" title="Back to portfolio">← back</a>
26
+ </div>
27
+ </nav>
28
+
29
+ <header class="hero">
30
+ <h1>tiny_vllm<span class="muted"> --watch</span></h1>
31
+ <p class="subtitle">A minimal continuous-batching LLM engine — paged KV-cache, automatic prefix caching, chunked prefill, SSE streaming. Built to be read end-to-end. Below is a live visualization of the scheduler and memory pool.</p>
32
  <div class="status">
33
  <span id="connection" class="badge offline">connecting…</span>
34
  <span id="model" class="muted"></span>
 
76
  <section class="card">
77
  <h2>Scheduler <span class="muted" id="sched-step"></span></h2>
78
  <div class="stats">
79
+ <div class="stat"><div class="stat-label">tokens / step</div><div class="stat-value" id="stat-tokens">0</div></div>
80
  <div class="stat"><div class="stat-label">prefill / decode</div><div class="stat-value" id="stat-pfdec">0 / 0</div></div>
81
  <div class="stat"><div class="stat-label">step (ms)</div><div class="stat-value" id="stat-ms">0</div></div>
82
+ <div class="stat"><div class="stat-label">prefix cache</div><div class="stat-value" id="stat-cache"></div></div>
83
  <div class="stat"><div class="stat-label">free blocks</div><div class="stat-value" id="stat-free">0</div></div>
84
+ <div class="stat"><div class="stat-label">preemptions</div><div class="stat-value" id="stat-pre">0</div></div>
85
  </div>
86
  <h3>step log</h3>
87
  <pre id="log" class="log"></pre>
 
94
  </main>
95
 
96
  <footer>
97
+ Subscribed to <code>/engine/events</code> · Source on <a href="https://github.com/surajsharan/tiny_vLLM" target="_blank" rel="noopener">github.com/surajsharan/tiny_vLLM</a> · Designed to match <a href="https://surajsharan.github.io/">surajsharan.github.io</a>.
98
  </footer>
99
 
100
+ <script src="app.js"></script>
101
  </body>
102
  </html>
web/style.css CHANGED
@@ -1,59 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
1
  :root {
2
- --bg: #0e1116;
3
- --bg-elev: #161b22;
4
- --bg-elev2: #1f2630;
5
- --fg: #e6edf3;
6
- --muted: #8b949e;
7
- --accent: #58a6ff;
8
- --green: #3fb950;
9
- --purple: #a371f7;
10
- --orange: #f0883e;
11
- --red: #f85149;
12
- --border: #30363d;
13
- --mono: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
 
 
 
 
 
 
14
  }
15
 
16
  * { box-sizing: border-box; }
17
 
18
- body {
19
- margin: 0;
20
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif;
21
  background: var(--bg);
22
  color: var(--fg);
 
 
 
 
 
23
  font-size: 14px;
 
 
 
 
 
 
 
24
  }
25
 
26
- header {
 
27
  display: flex; align-items: center; justify-content: space-between;
28
- padding: 14px 20px;
29
  border-bottom: 1px solid var(--border);
30
- background: var(--bg-elev);
 
 
 
 
31
  }
32
- header h1 { font-size: 16px; margin: 0; font-weight: 600; }
33
- .muted { color: var(--muted); font-weight: 400; }
34
- .badge {
35
- display: inline-block;
36
- padding: 2px 8px;
37
- border-radius: 10px;
38
- font-size: 11px;
 
 
 
 
 
 
 
39
  font-family: var(--mono);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
- .badge.online { background: rgba(63, 185, 80, 0.15); color: var(--green); }
42
- .badge.offline { background: rgba(248, 81, 73, 0.15); color: var(--red); }
43
- .badge.replay { background: rgba(163, 113, 247, 0.15); color: var(--purple); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
 
45
  .banner {
46
- padding: 8px 20px;
 
47
  font-size: 12px;
48
- background: rgba(163, 113, 247, 0.12);
49
  color: var(--purple);
50
- border-bottom: 1px solid rgba(163, 113, 247, 0.3);
 
 
 
 
 
51
  }
52
- .banner.error { background: rgba(248, 81, 73, 0.12); color: var(--red); border-bottom-color: rgba(248, 81, 73, 0.3); }
53
- .banner.replay-banner.error { background: rgba(248, 81, 73, 0.12); color: var(--red); }
54
 
 
55
  .prompt-box {
56
- padding: 12px 20px;
57
  border-bottom: 1px solid var(--border);
58
  background: var(--bg-elev);
59
  display: flex; flex-direction: column; gap: 10px;
@@ -63,176 +144,284 @@ header h1 { font-size: 16px; margin: 0; font-weight: 600; }
63
  background: var(--bg);
64
  color: var(--fg);
65
  border: 1px solid var(--border);
66
- border-radius: 6px;
67
- padding: 8px;
68
  font-family: var(--mono);
 
69
  resize: vertical;
70
  }
 
 
 
 
 
71
  .controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
72
- .controls label { display: flex; gap: 6px; align-items: center; font-size: 12px; color: var(--muted); }
 
 
 
 
 
73
  .controls input {
74
- width: 70px; background: var(--bg); color: var(--fg);
75
- border: 1px solid var(--border); border-radius: 4px; padding: 3px 6px;
76
- font-family: var(--mono);
 
 
 
 
 
77
  }
 
78
  button {
79
- background: var(--accent); color: white;
80
- border: none; border-radius: 4px;
81
- padding: 6px 14px; font-weight: 500; cursor: pointer;
 
 
 
 
82
  }
83
- button:hover { filter: brightness(1.1); }
84
- button:disabled { opacity: 0.4; cursor: not-allowed; }
85
  button.ghost {
86
  background: transparent;
87
- border: 1px solid var(--border);
88
  color: var(--fg);
89
  }
90
- #send-twice { background: var(--purple); }
 
 
 
 
 
91
 
92
  textarea:disabled { opacity: 0.5; }
93
 
94
  .replay-controls { margin-left: auto; display: flex; gap: 6px; align-items: center; }
95
  .replay-controls select {
96
- background: var(--bg); color: var(--fg); border: 1px solid var(--border);
97
- border-radius: 4px; padding: 4px 6px; font-family: var(--mono);
 
 
98
  }
99
 
 
100
  main {
101
  display: grid;
102
  grid-template-columns: 1fr 1fr;
103
  grid-template-areas: "pool sched" "seqs seqs";
104
  gap: 16px;
105
- padding: 16px 20px;
106
  }
107
  .card {
108
  background: var(--bg-elev);
109
  border: 1px solid var(--border);
110
- border-radius: 8px;
111
- padding: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
- .card h2 { font-size: 14px; margin: 0 0 10px; font-weight: 600; }
114
- .card h3 { font-size: 12px; margin: 14px 0 6px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
115
  .card.grow { grid-area: seqs; }
116
- .card:nth-child(1) { grid-area: pool; }
117
- .card:nth-child(2) { grid-area: sched; }
118
 
119
- /* ---- block pool ---- */
120
  .block-pool {
121
  display: grid;
122
  grid-template-columns: repeat(auto-fill, 16px);
123
  gap: 3px;
124
- padding: 8px;
125
  background: var(--bg);
126
- border-radius: 6px;
127
- max-height: 280px; overflow-y: auto;
 
128
  }
129
  .block {
130
- width: 16px; height: 16px; border-radius: 3px;
131
  background: var(--bg-elev2);
132
- position: relative;
133
  cursor: help;
134
  border: 1px solid transparent;
 
135
  }
136
- .block.free { background: #2a3140; }
137
- .block.cached { background: #1f3b5c; } /* free but in prefix cache */
138
- .block.used { background: var(--green); }
 
139
  .block.shared { background: var(--purple); }
140
- .block.hashed { border-color: var(--orange); }
141
 
142
- .legend { display: flex; gap: 14px; margin-top: 10px; font-size: 11px; color: var(--muted); flex-wrap: wrap; }
143
  .legend-item { display: flex; align-items: center; gap: 5px; }
144
- .swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
145
- .swatch-free { background: #2a3140; }
146
- .swatch-cached { background: #1f3b5c; }
147
- .swatch-used { background: var(--green); }
148
  .swatch-shared { background: var(--purple); }
149
- .swatch-hashed-edge { background: var(--bg-elev2); border: 1px solid var(--orange); }
150
 
151
- /* ---- stats ---- */
152
  .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
153
  .stat {
154
  background: var(--bg);
155
- border-radius: 6px;
156
- padding: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
- .stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
159
- .stat-value { font-family: var(--mono); font-size: 18px; margin-top: 3px; }
160
 
161
- /* ---- log ---- */
162
  .log {
163
  background: var(--bg);
164
- border-radius: 6px;
165
- padding: 8px;
166
- height: 140px; overflow-y: auto;
 
167
  font-family: var(--mono); font-size: 11px;
168
  margin: 0;
169
  white-space: pre-wrap; word-break: break-word;
 
170
  }
171
  .log .ev-step { color: var(--muted); }
172
  .log .ev-admit { color: var(--accent); }
173
- .log .ev-finish { color: var(--green); }
174
- .log .ev-preempt { color: var(--red); }
175
 
176
- /* ---- sequences ---- */
177
  #seqs { display: flex; flex-direction: column; gap: 10px; }
178
  .seq {
179
  background: var(--bg);
180
  border: 1px solid var(--border);
181
- border-radius: 6px;
182
- padding: 10px;
183
  }
184
- .seq-header { display: flex; gap: 10px; align-items: center; }
185
- .seq-id { font-family: var(--mono); color: var(--muted); font-size: 12px; }
186
  .seq-status {
187
- font-size: 10px; text-transform: uppercase; padding: 2px 6px; border-radius: 3px;
188
- font-family: var(--mono); letter-spacing: 0.05em;
189
- }
190
- .seq-status.waiting { background: rgba(139, 148, 158, 0.2); color: var(--muted); }
191
- .seq-status.prefilling { background: rgba(88, 166, 255, 0.15); color: var(--accent); }
192
- .seq-status.running { background: rgba(63, 185, 80, 0.15); color: var(--green); }
193
- .seq-status.finished { background: rgba(163, 113, 247, 0.15); color: var(--purple); }
194
- .seq-status.preempted { background: rgba(240, 136, 62, 0.2); color: var(--orange); }
195
- .seq-meta { color: var(--muted); font-size: 11px; font-family: var(--mono); margin-left: auto; }
 
 
 
 
 
 
 
 
 
196
  .seq-blocks {
197
- margin-top: 8px;
198
- display: flex; gap: 2px; flex-wrap: wrap;
199
  }
200
  .seq-block {
201
- width: 22px; height: 14px;
202
  background: var(--bg-elev2);
203
- font-size: 9px; line-height: 14px; text-align: center;
204
- font-family: var(--mono);
205
  border-radius: 2px;
206
  color: var(--muted);
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
- .seq-block.cached-hit { background: #1f3b5c; color: var(--accent); }
209
- .seq-block.shared { background: var(--purple); color: white; }
210
  .seq-text {
211
- margin-top: 8px;
212
  font-family: var(--mono); font-size: 12px;
213
  background: var(--bg-elev2);
214
- border-radius: 4px; padding: 6px;
215
- min-height: 24px;
216
- max-height: 180px;
 
 
217
  overflow-y: auto;
218
  white-space: pre-wrap; word-break: break-word;
 
219
  }
220
  .seq-text .prompt { color: var(--muted); }
221
  .seq-text .gen { color: var(--fg); }
222
  .seq-text .cursor {
223
- display: inline-block; width: 6px; background: var(--accent);
 
 
 
224
  animation: blink 1s steps(2, start) infinite;
225
  }
226
- @keyframes blink { to { visibility: hidden; } }
227
 
228
  footer {
229
- padding: 10px 20px;
230
  border-top: 1px solid var(--border);
231
- color: var(--muted); font-size: 11px;
 
 
232
  }
233
  footer a { color: var(--accent); text-decoration: none; }
 
 
 
 
 
 
 
234
 
235
  @media (max-width: 900px) {
236
  main { grid-template-columns: 1fr; grid-template-areas: "pool" "sched" "seqs"; }
237
  .stats { grid-template-columns: repeat(2, 1fr); }
 
 
238
  }
 
1
+ /* tiny_vllm demo — visual language matched to surajsharan.github.io
2
+ *
3
+ * Tokens lifted from the portfolio CSS:
4
+ * bg: #0a0e14 (215 30% 5%) foreground: #fafafa
5
+ * accent: #c6ff3e (lime) accent-dim: #8ac91f
6
+ * card: #0f131a border: #212833
7
+ * muted text: ~hsl(215 12% 60%)
8
+ * fonts: Inter (sans), JetBrains Mono (mono)
9
+ * radius: 0.5rem
10
+ */
11
+
12
  :root {
13
+ --bg: #0a0e14;
14
+ --bg-elev: #0f131a;
15
+ --bg-elev2: #141926;
16
+ --fg: #fafafa;
17
+ --muted: #8d96a3;
18
+ --accent: #c6ff3e;
19
+ --accent-dim: #8ac91f;
20
+ --accent-bg: rgba(198, 255, 62, 0.08);
21
+ --green: #c6ff3e; /* reuse lime for success */
22
+ --purple: #b78cff;
23
+ --orange: #f0a23e;
24
+ --red: #ff6b6b;
25
+ --border: #212833;
26
+ --border-strong: #2d3645;
27
+ --radius: 0.5rem;
28
+ --sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
29
+ --mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
30
+ --grid: rgba(255, 255, 255, 0.045);
31
  }
32
 
33
  * { box-sizing: border-box; }
34
 
35
+ html, body {
 
 
36
  background: var(--bg);
37
  color: var(--fg);
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ font-family: var(--sans);
43
  font-size: 14px;
44
+ line-height: 1.5;
45
+ background-image:
46
+ linear-gradient(var(--grid) 1px, transparent 1px),
47
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
48
+ background-size: 48px 48px;
49
+ background-position: -1px -1px;
50
+ background-attachment: fixed;
51
  }
52
 
53
+ /* ---------- topbar ---------- */
54
+ .topbar {
55
  display: flex; align-items: center; justify-content: space-between;
56
+ padding: 10px 24px;
57
  border-bottom: 1px solid var(--border);
58
+ background: rgba(10, 14, 20, 0.85);
59
+ backdrop-filter: blur(8px);
60
+ font-family: var(--mono);
61
+ font-size: 12px;
62
+ position: sticky; top: 0; z-index: 10;
63
  }
64
+ .topbar a { color: var(--fg); text-decoration: none; }
65
+ .topbar a:hover { color: var(--accent); }
66
+ .topbar .crumb { color: var(--muted); }
67
+ .topbar .crumb .sep { margin: 0 6px; }
68
+ .topbar .right { display: flex; gap: 16px; align-items: center; }
69
+ .topbar .right a { color: var(--muted); }
70
+ .topbar .right a:hover { color: var(--accent); }
71
+
72
+ header.hero {
73
+ padding: 28px 24px 20px;
74
+ border-bottom: 1px solid var(--border);
75
+ }
76
+ header.hero h1 {
77
+ margin: 0 0 4px;
78
  font-family: var(--mono);
79
+ font-size: 22px;
80
+ font-weight: 600;
81
+ letter-spacing: -0.01em;
82
+ }
83
+ header.hero h1::before {
84
+ content: "$ ";
85
+ color: var(--accent);
86
+ margin-right: 4px;
87
+ }
88
+ header.hero .subtitle {
89
+ color: var(--muted);
90
+ font-size: 13px;
91
+ margin: 0;
92
+ max-width: 760px;
93
  }
94
+ header.hero .status {
95
+ display: flex; gap: 10px; align-items: center;
96
+ margin-top: 10px;
97
+ font-family: var(--mono); font-size: 11px;
98
+ }
99
+ .badge {
100
+ display: inline-flex; align-items: center; gap: 6px;
101
+ padding: 3px 10px;
102
+ border-radius: 999px;
103
+ font-family: var(--mono); font-size: 11px;
104
+ border: 1px solid var(--border-strong);
105
+ background: var(--bg-elev);
106
+ }
107
+ .badge::before {
108
+ content: ""; width: 6px; height: 6px; border-radius: 50%;
109
+ background: var(--muted);
110
+ }
111
+ .badge.online { color: var(--accent); border-color: var(--accent); }
112
+ .badge.online::before { background: var(--accent); box-shadow: 0 0 8px var(--accent); }
113
+ .badge.offline { color: var(--red); border-color: rgba(255, 107, 107, 0.4); }
114
+ .badge.offline::before { background: var(--red); }
115
+ .badge.replay { color: var(--purple); border-color: rgba(183, 140, 255, 0.4); }
116
+ .badge.replay::before { background: var(--purple); }
117
+
118
+ .muted { color: var(--muted); font-weight: 400; }
119
 
120
+ /* ---------- banner ---------- */
121
  .banner {
122
+ padding: 10px 24px;
123
+ font-family: var(--mono);
124
  font-size: 12px;
125
+ background: rgba(183, 140, 255, 0.08);
126
  color: var(--purple);
127
+ border-bottom: 1px solid rgba(183, 140, 255, 0.25);
128
+ }
129
+ .banner.error, .banner.replay-banner.error {
130
+ background: rgba(255, 107, 107, 0.08);
131
+ color: var(--red);
132
+ border-bottom-color: rgba(255, 107, 107, 0.3);
133
  }
 
 
134
 
135
+ /* ---------- prompt box ---------- */
136
  .prompt-box {
137
+ padding: 14px 24px;
138
  border-bottom: 1px solid var(--border);
139
  background: var(--bg-elev);
140
  display: flex; flex-direction: column; gap: 10px;
 
144
  background: var(--bg);
145
  color: var(--fg);
146
  border: 1px solid var(--border);
147
+ border-radius: var(--radius);
148
+ padding: 10px 12px;
149
  font-family: var(--mono);
150
+ font-size: 13px;
151
  resize: vertical;
152
  }
153
+ .prompt-box textarea:focus {
154
+ outline: none;
155
+ border-color: var(--accent);
156
+ box-shadow: 0 0 0 3px var(--accent-bg);
157
+ }
158
  .controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
159
+ .controls label {
160
+ display: flex; gap: 6px; align-items: center;
161
+ font-family: var(--mono); font-size: 11px;
162
+ color: var(--muted);
163
+ text-transform: lowercase;
164
+ }
165
  .controls input {
166
+ width: 70px;
167
+ background: var(--bg); color: var(--fg);
168
+ border: 1px solid var(--border); border-radius: var(--radius);
169
+ padding: 4px 8px;
170
+ font-family: var(--mono); font-size: 12px;
171
+ }
172
+ .controls input:focus {
173
+ outline: none; border-color: var(--accent);
174
  }
175
+
176
  button {
177
+ background: var(--accent); color: var(--bg);
178
+ border: 1px solid var(--accent);
179
+ border-radius: var(--radius);
180
+ padding: 6px 14px;
181
+ font-family: var(--mono); font-size: 12px; font-weight: 600;
182
+ cursor: pointer;
183
+ transition: all 0.12s ease;
184
  }
185
+ button:hover { background: var(--accent-dim); border-color: var(--accent-dim); }
186
+ button:disabled { opacity: 0.35; cursor: not-allowed; background: var(--bg-elev); color: var(--muted); border-color: var(--border); }
187
  button.ghost {
188
  background: transparent;
189
+ border: 1px solid var(--border-strong);
190
  color: var(--fg);
191
  }
192
+ button.ghost:hover { border-color: var(--accent); color: var(--accent); }
193
+ #send-twice {
194
+ background: transparent; color: var(--accent);
195
+ border: 1px solid var(--accent);
196
+ }
197
+ #send-twice:hover { background: var(--accent-bg); }
198
 
199
  textarea:disabled { opacity: 0.5; }
200
 
201
  .replay-controls { margin-left: auto; display: flex; gap: 6px; align-items: center; }
202
  .replay-controls select {
203
+ background: var(--bg); color: var(--fg);
204
+ border: 1px solid var(--border); border-radius: var(--radius);
205
+ padding: 4px 8px;
206
+ font-family: var(--mono); font-size: 12px;
207
  }
208
 
209
+ /* ---------- layout ---------- */
210
  main {
211
  display: grid;
212
  grid-template-columns: 1fr 1fr;
213
  grid-template-areas: "pool sched" "seqs seqs";
214
  gap: 16px;
215
+ padding: 16px 24px;
216
  }
217
  .card {
218
  background: var(--bg-elev);
219
  border: 1px solid var(--border);
220
+ border-radius: var(--radius);
221
+ padding: 16px;
222
+ position: relative;
223
+ }
224
+ .card::before {
225
+ content: "";
226
+ position: absolute;
227
+ top: -1px; bottom: -1px; left: -1px;
228
+ width: 2px;
229
+ background: var(--accent);
230
+ border-top-left-radius: var(--radius);
231
+ border-bottom-left-radius: var(--radius);
232
+ opacity: 0;
233
+ transition: opacity 0.18s ease;
234
+ }
235
+ .card:hover::before { opacity: 1; }
236
+ .card h2 {
237
+ font-family: var(--mono);
238
+ font-size: 12px;
239
+ font-weight: 600;
240
+ margin: 0 0 12px;
241
+ text-transform: uppercase;
242
+ letter-spacing: 0.08em;
243
+ color: var(--fg);
244
+ }
245
+ .card h2 .muted { font-weight: 400; text-transform: none; letter-spacing: 0; }
246
+ .card h3 {
247
+ font-family: var(--mono); font-size: 10px;
248
+ margin: 14px 0 6px;
249
+ color: var(--muted);
250
+ text-transform: uppercase; letter-spacing: 0.1em;
251
  }
 
 
252
  .card.grow { grid-area: seqs; }
253
+ .card:nth-of-type(1) { grid-area: pool; }
254
+ .card:nth-of-type(2) { grid-area: sched; }
255
 
256
+ /* ---------- block pool ---------- */
257
  .block-pool {
258
  display: grid;
259
  grid-template-columns: repeat(auto-fill, 16px);
260
  gap: 3px;
261
+ padding: 10px;
262
  background: var(--bg);
263
+ border-radius: var(--radius);
264
+ border: 1px solid var(--border);
265
+ max-height: 320px; overflow-y: auto;
266
  }
267
  .block {
268
+ width: 16px; height: 16px; border-radius: 2px;
269
  background: var(--bg-elev2);
 
270
  cursor: help;
271
  border: 1px solid transparent;
272
+ transition: transform 0.1s ease;
273
  }
274
+ .block:hover { transform: scale(1.25); z-index: 2; position: relative; }
275
+ .block.free { background: #1a2030; }
276
+ .block.cached { background: #2a3d2a; }
277
+ .block.used { background: var(--accent); }
278
  .block.shared { background: var(--purple); }
279
+ .block.hashed { border-color: var(--accent); }
280
 
281
+ .legend { display: flex; gap: 14px; margin-top: 10px; font-family: var(--mono); font-size: 10px; color: var(--muted); flex-wrap: wrap; }
282
  .legend-item { display: flex; align-items: center; gap: 5px; }
283
+ .swatch { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
284
+ .swatch-free { background: #1a2030; }
285
+ .swatch-cached { background: #2a3d2a; }
286
+ .swatch-used { background: var(--accent); }
287
  .swatch-shared { background: var(--purple); }
288
+ .swatch-hashed-edge { background: var(--bg-elev2); border: 1px solid var(--accent); }
289
 
290
+ /* ---------- stats ---------- */
291
  .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
292
  .stat {
293
  background: var(--bg);
294
+ border: 1px solid var(--border);
295
+ border-radius: var(--radius);
296
+ padding: 10px;
297
+ }
298
+ .stat-label {
299
+ font-family: var(--mono);
300
+ font-size: 9px;
301
+ color: var(--muted);
302
+ text-transform: uppercase;
303
+ letter-spacing: 0.1em;
304
+ }
305
+ .stat-value {
306
+ font-family: var(--mono);
307
+ font-size: 20px;
308
+ margin-top: 4px;
309
+ color: var(--fg);
310
+ font-weight: 500;
311
  }
 
 
312
 
313
+ /* ---------- log ---------- */
314
  .log {
315
  background: var(--bg);
316
+ border: 1px solid var(--border);
317
+ border-radius: var(--radius);
318
+ padding: 10px;
319
+ height: 160px; overflow-y: auto;
320
  font-family: var(--mono); font-size: 11px;
321
  margin: 0;
322
  white-space: pre-wrap; word-break: break-word;
323
+ color: var(--muted);
324
  }
325
  .log .ev-step { color: var(--muted); }
326
  .log .ev-admit { color: var(--accent); }
327
+ .log .ev-finish { color: var(--accent-dim); }
328
+ .log .ev-preempt { color: var(--orange); }
329
 
330
+ /* ---------- sequences ---------- */
331
  #seqs { display: flex; flex-direction: column; gap: 10px; }
332
  .seq {
333
  background: var(--bg);
334
  border: 1px solid var(--border);
335
+ border-radius: var(--radius);
336
+ padding: 12px;
337
  }
338
+ .seq-header { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
339
+ .seq-id { font-family: var(--mono); color: var(--muted); font-size: 11px; }
340
  .seq-status {
341
+ font-family: var(--mono);
342
+ font-size: 9px;
343
+ text-transform: uppercase;
344
+ padding: 2px 8px;
345
+ border-radius: 999px;
346
+ letter-spacing: 0.08em;
347
+ border: 1px solid;
348
+ }
349
+ .seq-status.waiting { color: var(--muted); border-color: var(--border-strong); }
350
+ .seq-status.prefilling { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
351
+ .seq-status.running { color: var(--accent); border-color: var(--accent); }
352
+ .seq-status.finished { color: var(--purple); border-color: var(--purple); }
353
+ .seq-status.preempted { color: var(--orange); border-color: var(--orange); }
354
+ .seq-meta {
355
+ color: var(--muted); font-size: 11px;
356
+ font-family: var(--mono);
357
+ margin-left: auto;
358
+ }
359
  .seq-blocks {
360
+ margin-top: 10px;
361
+ display: flex; gap: 3px; flex-wrap: wrap;
362
  }
363
  .seq-block {
364
+ width: 26px; height: 16px;
365
  background: var(--bg-elev2);
366
+ font-family: var(--mono); font-size: 9px;
367
+ line-height: 16px; text-align: center;
368
  border-radius: 2px;
369
  color: var(--muted);
370
+ border: 1px solid var(--border);
371
+ }
372
+ .seq-block.cached-hit {
373
+ background: rgba(198, 255, 62, 0.12);
374
+ color: var(--accent);
375
+ border-color: var(--accent);
376
+ }
377
+ .seq-block.shared {
378
+ background: rgba(183, 140, 255, 0.18);
379
+ color: var(--purple);
380
+ border-color: var(--purple);
381
  }
 
 
382
  .seq-text {
383
+ margin-top: 10px;
384
  font-family: var(--mono); font-size: 12px;
385
  background: var(--bg-elev2);
386
+ border: 1px solid var(--border);
387
+ border-radius: var(--radius);
388
+ padding: 10px 12px;
389
+ min-height: 28px;
390
+ max-height: 200px;
391
  overflow-y: auto;
392
  white-space: pre-wrap; word-break: break-word;
393
+ line-height: 1.55;
394
  }
395
  .seq-text .prompt { color: var(--muted); }
396
  .seq-text .gen { color: var(--fg); }
397
  .seq-text .cursor {
398
+ display: inline-block; width: 7px; height: 14px;
399
+ background: var(--accent);
400
+ vertical-align: text-bottom;
401
+ margin-left: 1px;
402
  animation: blink 1s steps(2, start) infinite;
403
  }
404
+ @keyframes blink { to { opacity: 0; } }
405
 
406
  footer {
407
+ padding: 14px 24px;
408
  border-top: 1px solid var(--border);
409
+ color: var(--muted);
410
+ font-family: var(--mono);
411
+ font-size: 11px;
412
  }
413
  footer a { color: var(--accent); text-decoration: none; }
414
+ footer a:hover { text-decoration: underline; }
415
+
416
+ /* ---------- scrollbar ---------- */
417
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
418
+ ::-webkit-scrollbar-track { background: transparent; }
419
+ ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
420
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
421
 
422
  @media (max-width: 900px) {
423
  main { grid-template-columns: 1fr; grid-template-areas: "pool" "sched" "seqs"; }
424
  .stats { grid-template-columns: repeat(2, 1fr); }
425
+ .topbar { padding: 8px 16px; font-size: 11px; }
426
+ header.hero, .prompt-box, main, footer { padding-left: 16px; padding-right: 16px; }
427
  }