kvyb commited on
Commit
eaddeff
·
verified ·
1 Parent(s): bc8a1d5

Add reasoning level selector, single session per page load

Browse files
Files changed (1) hide show
  1. index.html +90 -62
index.html CHANGED
@@ -20,7 +20,6 @@
20
  margin: auto;
21
  }
22
  header { padding: 14px 16px; border-bottom: 1px solid #e5e7eb; }
23
- .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
24
  h1 { margin: 0; font-size: 15px; font-weight: 600; }
25
  header p { margin: 4px 0 0; color: #6b7280; font-size: 12px; }
26
  header a { color: #4f46e5; }
@@ -28,10 +27,11 @@
28
  details { margin-top: 10px; }
29
  summary { color: #6b7280; font-size: 12px; cursor: pointer; }
30
  summary:hover { color: #4f46e5; }
 
 
31
  #system {
32
  width: 100%;
33
  min-height: 76px;
34
- margin-top: 8px;
35
  padding: 8px 10px;
36
  border: 1px solid #d1d5db;
37
  border-radius: 8px;
@@ -39,6 +39,14 @@
39
  font-size: 13px;
40
  resize: vertical;
41
  }
 
 
 
 
 
 
 
 
42
  #chat { padding: 20px 16px; overflow-y: auto; }
43
  .message {
44
  max-width: 80%;
@@ -53,6 +61,14 @@
53
  .user { margin-left: auto; color: #fff; background: #4f46e5; }
54
  .error { color: #991b1b; background: #fee2e2; }
55
  .pending { color: #6b7280; font-style: italic; }
 
 
 
 
 
 
 
 
56
  form {
57
  display: grid;
58
  grid-template-columns: 1fr auto;
@@ -77,20 +93,10 @@
77
  font: inherit;
78
  font-weight: 600;
79
  cursor: pointer;
 
80
  }
81
  button:disabled { opacity: 0.5; cursor: wait; }
82
- #new {
83
- padding: 6px 12px;
84
- color: #4f46e5;
85
- background: transparent;
86
- border: 1px solid #d1d5db;
87
- border-radius: 8px;
88
- font-size: 13px;
89
- font-weight: 500;
90
- }
91
- #new:hover { background: #f3f4f6; }
92
- #send { align-self: end; }
93
- textarea:focus, button:focus-visible, summary:focus-visible {
94
  outline: 2px solid #6366f1;
95
  outline-offset: 2px;
96
  }
@@ -112,38 +118,45 @@
112
  :root { color-scheme: dark; }
113
  body { color: #f3f4f6; background: #111827; }
114
  header, form { border-color: #374151; }
115
- header p, #status, .pending, summary { color: #9ca3af; }
116
  summary:hover { color: #818cf8; }
117
  code { color: #d1d5db; }
118
- #system, textarea { color: #f3f4f6; background: #1f2937; border-color: #4b5563; }
119
  .assistant { background: #1f2937; }
120
  .error { color: #fecaca; background: #450a0a; }
121
- #new { color: #a5b4fc; border-color: #4b5563; }
122
- #new:hover { background: #1f2937; }
123
  #status.busy::before { border-color: #4b5563; border-top-color: #818cf8; }
124
  }
125
  @media (max-width: 620px) {
126
  .message { max-width: 92%; }
127
  form { grid-template-columns: 1fr; }
128
- #send { width: 100%; }
129
  }
130
  </style>
131
  </head>
132
  <body>
133
  <main>
134
  <header>
135
- <div class="row">
136
- <h1>Qwen3.8-27B Humanlike Chat</h1>
137
- <button id="new" type="button">New chat</button>
138
- </div>
139
  <p>
140
  Live demo of the hosted endpoint <code>https://api.lessthanthreeai.com/v1</code>.
141
  <a href="https://huggingface.co/LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-GGUF">Model card</a>
142
  </p>
143
  <details>
144
- <summary>System prompt</summary>
145
- <textarea id="system" aria-label="System prompt"
146
- placeholder="Optional. Sets identity, setting, or relationship for the whole conversation."></textarea>
 
 
 
 
 
 
 
 
 
 
 
147
  </details>
148
  </header>
149
 
@@ -161,6 +174,7 @@
161
  const API = "https://api.lessthanthreeai.com/v1";
162
  const MODEL = "qwen3.8-27b-humanlike-chat";
163
  const INTRO = "Say something. The first reply can take a couple of minutes while the inference worker starts.";
 
164
 
165
  const chat = document.querySelector("#chat");
166
  const form = document.querySelector("#form");
@@ -168,14 +182,15 @@
168
  const send = document.querySelector("#send");
169
  const status = document.querySelector("#status");
170
  const system = document.querySelector("#system");
171
- const newChat = document.querySelector("#new");
172
 
173
- let messages = [];
 
174
 
175
  system.value = localStorage.getItem("systemPrompt") || "";
176
- system.addEventListener("input", () => {
177
- localStorage.setItem("systemPrompt", system.value);
178
- });
179
 
180
  function addMessage(role, text) {
181
  const element = document.createElement("div");
@@ -186,19 +201,24 @@
186
  return element;
187
  }
188
 
189
- function startSession(message) {
190
- messages = [];
191
- chat.replaceChildren();
192
- addMessage("assistant", message);
193
- status.textContent = "";
194
- status.classList.remove("busy");
195
- prompt.value = "";
196
- prompt.focus();
 
 
 
 
 
197
  }
198
 
199
  // Server-sent events arrive as "data: {...}" chunks. The endpoint may also send
200
  // ": status={...}" comment lines while a cold worker starts.
201
- function applyEvent(line, output) {
202
  if (line.startsWith(":")) {
203
  const payload = line.slice(1).trim().replace(/^status=/, "");
204
  try {
@@ -210,21 +230,24 @@
210
  if (!line.startsWith("data:")) return;
211
  const payload = line.slice(5).trim();
212
  if (!payload || payload === "[DONE]") return;
213
- const event = JSON.parse(payload);
214
- const content = event.choices?.[0]?.delta?.content || event.choices?.[0]?.text || "";
215
- if (content) {
216
- if (output.classList.contains("pending")) {
217
- output.classList.remove("pending");
218
- output.textContent = "";
 
 
 
 
 
219
  status.textContent = "";
220
  }
221
- output.textContent += content;
222
  chat.scrollTop = chat.scrollHeight;
223
  }
224
  }
225
 
226
- newChat.addEventListener("click", () => startSession(INTRO));
227
-
228
  form.addEventListener("submit", async (event) => {
229
  event.preventDefault();
230
  const text = prompt.value.trim();
@@ -237,15 +260,18 @@
237
  status.classList.add("busy");
238
  status.textContent = "Starting the model. A cold start takes about two to three minutes.";
239
 
240
- const output = addMessage("assistant", "");
241
- output.classList.add("pending");
242
- output.textContent = "waiting for the first token";
243
 
244
  const systemPrompt = system.value.trim();
245
  const request = systemPrompt
246
  ? [{ role: "system", content: systemPrompt }, ...messages]
247
  : messages;
248
 
 
 
 
 
249
  try {
250
  const response = await fetch(`${API}/chat/completions`, {
251
  method: "POST",
@@ -254,7 +280,8 @@
254
  model: MODEL,
255
  messages: request,
256
  stream: true,
257
- max_tokens: 512,
 
258
  }),
259
  });
260
 
@@ -274,19 +301,19 @@
274
  buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
275
  const lines = buffer.split("\n");
276
  buffer = lines.pop() || "";
277
- for (const line of lines) applyEvent(line.trim(), output);
278
  if (done) break;
279
  }
280
- if (buffer.trim()) applyEvent(buffer.trim(), output);
281
 
282
- output.classList.remove("pending");
283
- if (!output.textContent) output.textContent = "(empty response)";
284
- messages.push({ role: "assistant", content: output.textContent });
285
  status.textContent = "Complete. Later replies are fast while the worker stays warm.";
286
  } catch (error) {
287
- output.classList.remove("pending");
288
- output.classList.add("error");
289
- output.textContent = `Request failed: ${error.message}`;
290
  status.textContent = "The inference service did not complete the request.";
291
  } finally {
292
  status.classList.remove("busy");
@@ -295,7 +322,8 @@
295
  }
296
  });
297
 
298
- startSession(INTRO);
 
299
  </script>
300
  </body>
301
  </html>
 
20
  margin: auto;
21
  }
22
  header { padding: 14px 16px; border-bottom: 1px solid #e5e7eb; }
 
23
  h1 { margin: 0; font-size: 15px; font-weight: 600; }
24
  header p { margin: 4px 0 0; color: #6b7280; font-size: 12px; }
25
  header a { color: #4f46e5; }
 
27
  details { margin-top: 10px; }
28
  summary { color: #6b7280; font-size: 12px; cursor: pointer; }
29
  summary:hover { color: #4f46e5; }
30
+ .field { margin-top: 10px; }
31
+ .field label { display: block; margin-bottom: 4px; color: #6b7280; font-size: 12px; }
32
  #system {
33
  width: 100%;
34
  min-height: 76px;
 
35
  padding: 8px 10px;
36
  border: 1px solid #d1d5db;
37
  border-radius: 8px;
 
39
  font-size: 13px;
40
  resize: vertical;
41
  }
42
+ #reasoning {
43
+ padding: 6px 8px;
44
+ border: 1px solid #d1d5db;
45
+ border-radius: 8px;
46
+ background: #fff;
47
+ font: inherit;
48
+ font-size: 13px;
49
+ }
50
  #chat { padding: 20px 16px; overflow-y: auto; }
51
  .message {
52
  max-width: 80%;
 
61
  .user { margin-left: auto; color: #fff; background: #4f46e5; }
62
  .error { color: #991b1b; background: #fee2e2; }
63
  .pending { color: #6b7280; font-style: italic; }
64
+ .reasoning {
65
+ margin-bottom: 8px;
66
+ padding-left: 9px;
67
+ border-left: 2px solid #d1d5db;
68
+ color: #6b7280;
69
+ font-size: 13px;
70
+ font-style: italic;
71
+ }
72
  form {
73
  display: grid;
74
  grid-template-columns: 1fr auto;
 
93
  font: inherit;
94
  font-weight: 600;
95
  cursor: pointer;
96
+ align-self: end;
97
  }
98
  button:disabled { opacity: 0.5; cursor: wait; }
99
+ textarea:focus, button:focus-visible, select:focus-visible, summary:focus-visible {
 
 
 
 
 
 
 
 
 
 
 
100
  outline: 2px solid #6366f1;
101
  outline-offset: 2px;
102
  }
 
118
  :root { color-scheme: dark; }
119
  body { color: #f3f4f6; background: #111827; }
120
  header, form { border-color: #374151; }
121
+ header p, #status, .pending, summary, .field label, .reasoning { color: #9ca3af; }
122
  summary:hover { color: #818cf8; }
123
  code { color: #d1d5db; }
124
+ #system, textarea, #reasoning { color: #f3f4f6; background: #1f2937; border-color: #4b5563; }
125
  .assistant { background: #1f2937; }
126
  .error { color: #fecaca; background: #450a0a; }
127
+ .reasoning { border-color: #4b5563; }
 
128
  #status.busy::before { border-color: #4b5563; border-top-color: #818cf8; }
129
  }
130
  @media (max-width: 620px) {
131
  .message { max-width: 92%; }
132
  form { grid-template-columns: 1fr; }
133
+ button { width: 100%; }
134
  }
135
  </style>
136
  </head>
137
  <body>
138
  <main>
139
  <header>
140
+ <h1>Qwen3.8-27B Humanlike Chat</h1>
 
 
 
141
  <p>
142
  Live demo of the hosted endpoint <code>https://api.lessthanthreeai.com/v1</code>.
143
  <a href="https://huggingface.co/LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-GGUF">Model card</a>
144
  </p>
145
  <details>
146
+ <summary>Settings</summary>
147
+ <div class="field">
148
+ <label for="system">System prompt</label>
149
+ <textarea id="system"
150
+ placeholder="Optional. Sets identity, setting, or relationship for the conversation."></textarea>
151
+ </div>
152
+ <div class="field">
153
+ <label for="reasoning">Reasoning</label>
154
+ <select id="reasoning">
155
+ <option value="off">Off (fastest, the default)</option>
156
+ <option value="low">Low</option>
157
+ <option value="medium">Medium</option>
158
+ </select>
159
+ </div>
160
  </details>
161
  </header>
162
 
 
174
  const API = "https://api.lessthanthreeai.com/v1";
175
  const MODEL = "qwen3.8-27b-humanlike-chat";
176
  const INTRO = "Say something. The first reply can take a couple of minutes while the inference worker starts.";
177
+ const MAX_TOKENS = { off: 512, low: 4096, medium: 4096 };
178
 
179
  const chat = document.querySelector("#chat");
180
  const form = document.querySelector("#form");
 
182
  const send = document.querySelector("#send");
183
  const status = document.querySelector("#status");
184
  const system = document.querySelector("#system");
185
+ const reasoning = document.querySelector("#reasoning");
186
 
187
+ // One conversation per page load. Reloading starts a fresh session.
188
+ const messages = [];
189
 
190
  system.value = localStorage.getItem("systemPrompt") || "";
191
+ reasoning.value = localStorage.getItem("reasoningEffort") || "off";
192
+ system.addEventListener("input", () => localStorage.setItem("systemPrompt", system.value));
193
+ reasoning.addEventListener("change", () => localStorage.setItem("reasoningEffort", reasoning.value));
194
 
195
  function addMessage(role, text) {
196
  const element = document.createElement("div");
 
201
  return element;
202
  }
203
 
204
+ function addAssistantMessage() {
205
+ const element = document.createElement("div");
206
+ element.className = "message assistant";
207
+ const thoughts = document.createElement("div");
208
+ thoughts.className = "reasoning";
209
+ thoughts.hidden = true;
210
+ const content = document.createElement("div");
211
+ content.className = "pending";
212
+ content.textContent = "waiting for the first token";
213
+ element.append(thoughts, content);
214
+ chat.append(element);
215
+ chat.scrollTop = chat.scrollHeight;
216
+ return { element, thoughts, content };
217
  }
218
 
219
  // Server-sent events arrive as "data: {...}" chunks. The endpoint may also send
220
  // ": status={...}" comment lines while a cold worker starts.
221
+ function applyEvent(line, target) {
222
  if (line.startsWith(":")) {
223
  const payload = line.slice(1).trim().replace(/^status=/, "");
224
  try {
 
230
  if (!line.startsWith("data:")) return;
231
  const payload = line.slice(5).trim();
232
  if (!payload || payload === "[DONE]") return;
233
+
234
+ const delta = (JSON.parse(payload).choices || [{}])[0].delta || {};
235
+ if (delta.reasoning) {
236
+ target.thoughts.hidden = false;
237
+ target.thoughts.textContent += delta.reasoning;
238
+ chat.scrollTop = chat.scrollHeight;
239
+ }
240
+ if (delta.content) {
241
+ if (target.content.classList.contains("pending")) {
242
+ target.content.classList.remove("pending");
243
+ target.content.textContent = "";
244
  status.textContent = "";
245
  }
246
+ target.content.textContent += delta.content;
247
  chat.scrollTop = chat.scrollHeight;
248
  }
249
  }
250
 
 
 
251
  form.addEventListener("submit", async (event) => {
252
  event.preventDefault();
253
  const text = prompt.value.trim();
 
260
  status.classList.add("busy");
261
  status.textContent = "Starting the model. A cold start takes about two to three minutes.";
262
 
263
+ const target = addAssistantMessage();
264
+ const effort = reasoning.value;
 
265
 
266
  const systemPrompt = system.value.trim();
267
  const request = systemPrompt
268
  ? [{ role: "system", content: systemPrompt }, ...messages]
269
  : messages;
270
 
271
+ const chatTemplateKwargs = effort === "off"
272
+ ? { enable_thinking: false, preserve_thinking: false }
273
+ : { enable_thinking: true, reasoning_effort: effort, preserve_thinking: false };
274
+
275
  try {
276
  const response = await fetch(`${API}/chat/completions`, {
277
  method: "POST",
 
280
  model: MODEL,
281
  messages: request,
282
  stream: true,
283
+ max_tokens: MAX_TOKENS[effort],
284
+ chat_template_kwargs: chatTemplateKwargs,
285
  }),
286
  });
287
 
 
301
  buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
302
  const lines = buffer.split("\n");
303
  buffer = lines.pop() || "";
304
+ for (const line of lines) applyEvent(line.trim(), target);
305
  if (done) break;
306
  }
307
+ if (buffer.trim()) applyEvent(buffer.trim(), target);
308
 
309
+ target.content.classList.remove("pending");
310
+ if (!target.content.textContent) target.content.textContent = "(no final answer returned)";
311
+ messages.push({ role: "assistant", content: target.content.textContent });
312
  status.textContent = "Complete. Later replies are fast while the worker stays warm.";
313
  } catch (error) {
314
+ target.content.classList.remove("pending");
315
+ target.element.classList.add("error");
316
+ target.content.textContent = `Request failed: ${error.message}`;
317
  status.textContent = "The inference service did not complete the request.";
318
  } finally {
319
  status.classList.remove("busy");
 
322
  }
323
  });
324
 
325
+ addMessage("assistant", INTRO);
326
+ prompt.focus();
327
  </script>
328
  </body>
329
  </html>