ryanhlewis commited on
Commit
5ce089c
·
verified ·
1 Parent(s): 3abb74c

Add Gemma thinking toggle

Browse files
dist/assets/index-Bqz15WNm.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-COohfmVW.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;color:#f4f0e8;font-synthesis:none;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;background:#10100f;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;line-height:1.4}*{box-sizing:border-box}body{background:linear-gradient(#39453a38,#0000 360px),#10100f;min-width:320px;min-height:100vh;margin:0}button,input,textarea,select{font:inherit}button{border:0}.app{width:min(1180px,100vw - 32px);margin:0 auto;padding:24px 0 36px}.topbar{justify-content:space-between;align-items:center;gap:16px;margin-bottom:20px;display:flex}.brand{align-items:center;gap:12px;display:flex}.mark{color:#15170f;background:#d7ff7a;border:1px solid #f4f0e829;border-radius:8px;place-items:center;width:38px;height:38px;font-weight:800;display:grid}h1,h2,h3,p{margin:0}h1{letter-spacing:0;font-size:19px;font-weight:720}.subtle{color:#a7a093;font-size:13px}.actions{align-items:center;gap:8px;display:flex;position:relative}.icon-button,.button{color:#f4f0e8;cursor:pointer;background:#24231f;border:1px solid #f4f0e81f;border-radius:8px;justify-content:center;align-items:center;gap:8px;min-height:38px;padding:0 14px;display:inline-flex}.icon-button{width:38px;padding:0}.icon-button svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.8px;width:18px;height:18px}.settings-popover{z-index:10;width:min(430px,100vw - 32px);padding:16px;position:absolute;top:46px;right:0;box-shadow:0 18px 42px #00000061}.settings-popover h2{margin-bottom:8px;font-size:15px;font-weight:720}.button.primary{color:#15170f;background:#d7ff7a;border-color:#d7ff7a;font-weight:740}.button.danger{color:#ffd9d9;background:#3a2525;border-color:#6e3737}.button:disabled,.icon-button:disabled{cursor:not-allowed;opacity:.55}.shell{grid-template-columns:minmax(0,1fr) 340px;gap:18px;display:grid}.panel{background:#1a1916f0;border:1px solid #f4f0e81f;border-radius:8px}.chat-panel{grid-template-rows:auto minmax(0,1fr) auto;min-height:690px;display:grid}.status-strip{background:#f4f0e814;border-bottom:1px solid #f4f0e81a;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;display:grid}.metric{background:#1a1916;min-width:0;padding:12px 14px}.metric span{color:#a7a093;text-transform:uppercase;font-size:11px;display:block}.metric strong{text-overflow:ellipsis;white-space:nowrap;margin-top:2px;font-size:14px;font-weight:680;display:block;overflow:hidden}.messages{padding:18px;overflow:auto}.message-list{flex-direction:column;gap:14px;display:flex}.message{white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid #f4f0e81a;border-radius:8px;max-width:86%;padding:13px 14px}.message.user{background:#243124;align-self:flex-end}.message.assistant{background:#22211d;align-self:flex-start}.message.thinking-message{padding:8px}.answer-text{white-space:pre-wrap}.thinking-block{flex-direction:column;align-items:flex-start;gap:6px;margin:0 0 8px;display:flex}.thinking-message .thinking-block{margin-bottom:0}.thinking-toggle{color:#c9c1b2;cursor:pointer;background:#171714;border-radius:6px;align-items:center;gap:7px;min-height:26px;padding:0 7px;font-size:12px;display:inline-flex}.thinking-arrow{color:#d7ff7a;justify-content:center;align-items:center;width:14px;height:14px;display:inline-flex}.thinking-arrow svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.2px;width:14px;height:14px}.thinking-content{color:#a7a093;white-space:pre-wrap;background:#141411;border-radius:6px;max-height:240px;padding:8px;font-size:12px;overflow:auto}.empty{color:#a7a093;text-align:center;place-items:center;min-height:340px;display:grid}.composer{border-top:1px solid #f4f0e81a;gap:10px;padding:14px;display:grid}textarea,input,select{color:#f4f0e8;background:#121210;border:1px solid #f4f0e81f;border-radius:8px;outline:none;width:100%}textarea:focus,input:focus,select:focus{border-color:#d7ff7acc;box-shadow:0 0 0 3px #d7ff7a1f}textarea{resize:vertical;min-height:96px;padding:12px}input,select{min-height:38px;padding:0 10px}.composer-row{justify-content:space-between;align-items:center;gap:10px;display:flex}.file-chip{color:#a7a093;flex:1;align-items:center;gap:8px;min-width:0;font-size:13px;display:flex}.file-chip input{display:none}.chip-button{color:#f4f0e8;cursor:pointer;background:#24231f;border-radius:8px;align-items:center;gap:7px;min-height:34px;padding:0 10px;display:inline-flex}.file-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.side{flex-direction:column;gap:14px;display:flex}.card{padding:16px}.card h2{margin-bottom:10px;font-size:15px;font-weight:720}.field{gap:6px;margin-top:12px;display:grid}.field label{color:#c9c1b2;font-size:12px;font-weight:650}.field-grid{grid-template-columns:1fr 1fr;gap:10px;display:grid}.progress{background:#11110f;border-radius:999px;height:10px;overflow:hidden}.bar{border-radius:inherit;background:#d7ff7a;width:0%;height:100%;transition:width .18s}.indeterminate .bar{background:linear-gradient(90deg,#d7ff7a59,#d7ff7a,#d7ff7a59) 0 0/200% 100%;animation:1.1s linear infinite load-sweep}@keyframes load-sweep{0%{background-position:200% 0}to{background-position:0 0}}.log{color:#c9c1b2;white-space:pre-wrap;background:#11110f;border-radius:8px;min-height:106px;max-height:190px;padding:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:auto}.pill-row{flex-wrap:wrap;gap:8px;margin-top:12px;display:flex}.pill{color:#c9c1b2;border:1px solid #f4f0e81f;border-radius:999px;padding:5px 9px;font-size:12px}.source{overflow-wrap:anywhere}@media (max-width:900px){.app{width:min(100vw - 20px,720px);padding-top:14px}.shell{grid-template-columns:1fr}.status-strip{grid-template-columns:1fr 1fr}.message{max-width:94%}.topbar{flex-direction:column;align-items:flex-start}.actions{justify-content:flex-end;align-self:stretch}}
dist/assets/wllama-ByHMPmd7.wasm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3e827b9fc3536fd1b061eee8b130e46f6366d25b062438a791d8b17b2a72a30
3
+ size 7308965
dist/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Gemma 4 WebGPU</title>
7
+ <link rel="preconnect" href="https://huggingface.co" />
8
+ <script type="module" crossorigin src="./assets/index-Bqz15WNm.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-COohfmVW.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>
src/main.ts CHANGED
@@ -75,7 +75,7 @@ const state = {
75
  maxTokens: Number(params.get("maxTokens") || 384),
76
  temperature: Number(params.get("temperature") || 0.7),
77
  topP: Number(params.get("topP") || 0.95),
78
- thinking: params.get("thinking") === "hidden" ? "hidden" : "collapsed",
79
  seed: Number(params.get("seed") || -1),
80
  prompt:
81
  params.get("prompt") ||
@@ -159,7 +159,7 @@ function webgpuStatus(): string {
159
  return modelLoaded && wllama?.isSupportWebGPU() ? "ready" : "available";
160
  }
161
 
162
- function iconSvg(name: "settings" | "newChat"): string {
163
  if (name === "settings") {
164
  return `
165
  <svg viewBox="0 0 24 24" aria-hidden="true">
@@ -168,6 +168,20 @@ function iconSvg(name: "settings" | "newChat"): string {
168
  </svg>
169
  `;
170
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  return `
172
  <svg viewBox="0 0 24 24" aria-hidden="true">
173
  <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
@@ -196,8 +210,8 @@ function renderMessage(item: ChatEntry, index: number): string {
196
  }
197
 
198
  const thinking = item.thinking?.trim() || "";
199
- const shouldShowThinking = state.thinking === "collapsed" && thinking.length > 0;
200
- const arrow = item.thinkingOpen ? "^" : "v";
201
  const thinkingBlock = shouldShowThinking
202
  ? `<div class="thinking-block">
203
  <button class="thinking-toggle" type="button" data-thinking-index="${index}">
@@ -211,11 +225,10 @@ function renderMessage(item: ChatEntry, index: number): string {
211
  }
212
  </div>`
213
  : "";
214
- const answer = item.content.trim()
215
- ? escapeHtml(item.content)
216
- : `<span class="subtle">Waiting for response...</span>`;
217
 
218
- return `<div class="message assistant">${thinkingBlock}<div class="answer-text">${answer}</div></div>`;
219
  }
220
 
221
  function render(): void {
@@ -329,10 +342,10 @@ function render(): void {
329
  </div>
330
  </div>
331
  <div class="field">
332
- <label for="thinking">Thinking</label>
333
  <select id="thinking">
334
- <option value="collapsed" ${state.thinking === "collapsed" ? "selected" : ""}>Show collapsed</option>
335
- <option value="hidden" ${state.thinking === "hidden" ? "selected" : ""}>Hide</option>
336
  </select>
337
  </div>
338
  </section>
@@ -403,7 +416,7 @@ function syncInputs(): void {
403
  state.maxTokens = getNumberValue("maxTokens", state.maxTokens);
404
  state.temperature = getNumberValue("temperature", state.temperature);
405
  state.topP = getNumberValue("topP", state.topP);
406
- state.thinking = getInputValue("thinking", state.thinking) === "hidden" ? "hidden" : "collapsed";
407
  state.seed = getNumberValue("seed", state.seed);
408
  }
409
 
@@ -624,7 +637,7 @@ async function sendMessage(): Promise<void> {
624
  content,
625
  } as ChatCompletionMessage;
626
  const thinkingInstruction =
627
- state.thinking === "hidden"
628
  ? ([
629
  {
630
  role: "system",
@@ -644,6 +657,9 @@ async function sendMessage(): Promise<void> {
644
  seed: state.seed >= 0 ? state.seed : undefined,
645
  cache_prompt: true,
646
  timings_per_token: true,
 
 
 
647
  };
648
 
649
  await wllama.createChatCompletion({
@@ -666,7 +682,7 @@ async function sendMessage(): Promise<void> {
666
  currentLog = [
667
  `Generating ${formatSeconds(elapsed)}`,
668
  assistant?.content ? `Answer ${assistant.content.length} chars` : "Waiting for answer text...",
669
- assistant?.thinking ? `Thinking ${assistant.thinking.length} chars` : "",
670
  lastTimings ? `Prompt ${lastTimings.prompt_per_second.toFixed(1)} tok/s` : "",
671
  lastTimings ? `Output ${lastTimings.predicted_per_second.toFixed(1)} tok/s` : "",
672
  ]
 
75
  maxTokens: Number(params.get("maxTokens") || 384),
76
  temperature: Number(params.get("temperature") || 0.7),
77
  topP: Number(params.get("topP") || 0.95),
78
+ thinking: params.get("thinking") === "enabled" ? "enabled" : "disabled",
79
  seed: Number(params.get("seed") || -1),
80
  prompt:
81
  params.get("prompt") ||
 
159
  return modelLoaded && wllama?.isSupportWebGPU() ? "ready" : "available";
160
  }
161
 
162
+ function iconSvg(name: "settings" | "newChat" | "chevronDown" | "chevronUp"): string {
163
  if (name === "settings") {
164
  return `
165
  <svg viewBox="0 0 24 24" aria-hidden="true">
 
168
  </svg>
169
  `;
170
  }
171
+ if (name === "chevronUp") {
172
+ return `
173
+ <svg viewBox="0 0 24 24" aria-hidden="true">
174
+ <path d="m6 15 6-6 6 6"></path>
175
+ </svg>
176
+ `;
177
+ }
178
+ if (name === "chevronDown") {
179
+ return `
180
+ <svg viewBox="0 0 24 24" aria-hidden="true">
181
+ <path d="m6 9 6 6 6-6"></path>
182
+ </svg>
183
+ `;
184
+ }
185
  return `
186
  <svg viewBox="0 0 24 24" aria-hidden="true">
187
  <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
 
210
  }
211
 
212
  const thinking = item.thinking?.trim() || "";
213
+ const shouldShowThinking = state.thinking === "enabled" && thinking.length > 0;
214
+ const arrow = item.thinkingOpen ? iconSvg("chevronUp") : iconSvg("chevronDown");
215
  const thinkingBlock = shouldShowThinking
216
  ? `<div class="thinking-block">
217
  <button class="thinking-toggle" type="button" data-thinking-index="${index}">
 
225
  }
226
  </div>`
227
  : "";
228
+ const answer = item.content.trim() ? `<div class="answer-text">${escapeHtml(item.content)}</div>` : "";
229
+ const waiting = !answer && !thinkingBlock ? `<div class="answer-text"><span class="subtle">Waiting for response...</span></div>` : "";
 
230
 
231
+ return `<div class="message assistant${thinkingBlock && !answer ? " thinking-message" : ""}">${thinkingBlock}${answer}${waiting}</div>`;
232
  }
233
 
234
  function render(): void {
 
342
  </div>
343
  </div>
344
  <div class="field">
345
+ <label for="thinking">Model thinking</label>
346
  <select id="thinking">
347
+ <option value="disabled" ${state.thinking === "disabled" ? "selected" : ""}>Disabled</option>
348
+ <option value="enabled" ${state.thinking === "enabled" ? "selected" : ""}>Enabled</option>
349
  </select>
350
  </div>
351
  </section>
 
416
  state.maxTokens = getNumberValue("maxTokens", state.maxTokens);
417
  state.temperature = getNumberValue("temperature", state.temperature);
418
  state.topP = getNumberValue("topP", state.topP);
419
+ state.thinking = getInputValue("thinking", state.thinking) === "enabled" ? "enabled" : "disabled";
420
  state.seed = getNumberValue("seed", state.seed);
421
  }
422
 
 
637
  content,
638
  } as ChatCompletionMessage;
639
  const thinkingInstruction =
640
+ state.thinking === "disabled"
641
  ? ([
642
  {
643
  role: "system",
 
657
  seed: state.seed >= 0 ? state.seed : undefined,
658
  cache_prompt: true,
659
  timings_per_token: true,
660
+ chat_template_kwargs: {
661
+ enable_thinking: state.thinking === "enabled",
662
+ },
663
  };
664
 
665
  await wllama.createChatCompletion({
 
682
  currentLog = [
683
  `Generating ${formatSeconds(elapsed)}`,
684
  assistant?.content ? `Answer ${assistant.content.length} chars` : "Waiting for answer text...",
685
+ state.thinking === "enabled" && assistant?.thinking ? `Thinking ${assistant.thinking.length} chars` : "",
686
  lastTimings ? `Prompt ${lastTimings.prompt_per_second.toFixed(1)} tok/s` : "",
687
  lastTimings ? `Output ${lastTimings.predicted_per_second.toFixed(1)} tok/s` : "",
688
  ]
src/style.css CHANGED
@@ -236,40 +236,63 @@ h1 {
236
  background: #22211d;
237
  }
238
 
 
 
 
 
239
  .answer-text {
240
  white-space: pre-wrap;
241
  }
242
 
243
  .thinking-block {
244
- margin-bottom: 10px;
245
- border-bottom: 1px solid rgba(244, 240, 232, 0.08);
246
- padding-bottom: 10px;
 
 
 
 
 
 
247
  }
248
 
249
  .thinking-toggle {
250
  display: inline-flex;
251
- min-height: 28px;
252
  align-items: center;
253
  gap: 7px;
254
- border-radius: 7px;
255
- padding: 0 8px;
256
- background: #181815;
257
  color: #c9c1b2;
258
  cursor: pointer;
259
  font-size: 12px;
260
  }
261
 
262
  .thinking-arrow {
 
 
 
 
 
263
  color: #d7ff7a;
264
- font-weight: 800;
 
 
 
 
 
 
 
 
 
265
  }
266
 
267
  .thinking-content {
268
- margin-top: 8px;
269
  max-height: 240px;
270
  overflow: auto;
271
- border-radius: 7px;
272
- padding: 10px;
273
  background: #141411;
274
  color: #a7a093;
275
  font-size: 12px;
 
236
  background: #22211d;
237
  }
238
 
239
+ .message.thinking-message {
240
+ padding: 8px;
241
+ }
242
+
243
  .answer-text {
244
  white-space: pre-wrap;
245
  }
246
 
247
  .thinking-block {
248
+ display: flex;
249
+ align-items: flex-start;
250
+ flex-direction: column;
251
+ gap: 6px;
252
+ margin: 0 0 8px;
253
+ }
254
+
255
+ .thinking-message .thinking-block {
256
+ margin-bottom: 0;
257
  }
258
 
259
  .thinking-toggle {
260
  display: inline-flex;
261
+ min-height: 26px;
262
  align-items: center;
263
  gap: 7px;
264
+ border-radius: 6px;
265
+ padding: 0 7px;
266
+ background: #171714;
267
  color: #c9c1b2;
268
  cursor: pointer;
269
  font-size: 12px;
270
  }
271
 
272
  .thinking-arrow {
273
+ display: inline-flex;
274
+ width: 14px;
275
+ height: 14px;
276
+ align-items: center;
277
+ justify-content: center;
278
  color: #d7ff7a;
279
+ }
280
+
281
+ .thinking-arrow svg {
282
+ width: 14px;
283
+ height: 14px;
284
+ fill: none;
285
+ stroke: currentColor;
286
+ stroke-linecap: round;
287
+ stroke-linejoin: round;
288
+ stroke-width: 2.2;
289
  }
290
 
291
  .thinking-content {
 
292
  max-height: 240px;
293
  overflow: auto;
294
+ border-radius: 6px;
295
+ padding: 8px;
296
  background: #141411;
297
  color: #a7a093;
298
  font-size: 12px;