kvyb commited on
Commit
dc52501
·
verified ·
1 Parent(s): 275db1e

Point the demo at the hosted vLLM endpoint

Browse files
Files changed (1) hide show
  1. index.html +117 -50
index.html CHANGED
@@ -3,35 +3,77 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Qwen3.8 27B Humanlike Chat</title>
7
  <style>
8
  * { box-sizing: border-box; }
9
- body { margin: 0; color: #111827; background: #fff; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
10
- main { display: grid; grid-template-rows: auto 1fr auto auto; width: min(900px, 100%); height: 100vh; margin: auto; }
11
- .endpoint { padding: 12px 16px; color: #6b7280; background: #f9fafb; border-bottom: 1px solid #e5e7eb; font-size: 13px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  code { color: #374151; overflow-wrap: anywhere; }
13
- #chat { padding: 24px 16px; overflow-y: auto; }
14
- .message { max-width: 80%; margin: 0 0 16px; padding: 12px 16px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
 
 
 
 
 
 
 
 
15
  .assistant { background: #f3f4f6; }
16
- .user { margin-left: auto; color: white; background: #4f46e5; }
17
  .error { color: #991b1b; background: #fee2e2; }
18
- form { display: grid; grid-template-columns: 1fr auto; gap: 10px; padding: 16px; border-top: 1px solid #e5e7eb; }
19
- textarea, input { padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font: inherit; }
20
- textarea { min-height: 84px; resize: vertical; }
21
- .controls { display: flex; flex-direction: column; gap: 8px; }
22
- label { color: #6b7280; font-size: 12px; }
23
- input { display: block; width: 120px; margin-top: 4px; }
24
- button { padding: 10px 18px; color: white; background: #4f46e5; border: 0; border-radius: 8px; font-weight: 600; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  button:disabled { opacity: 0.5; cursor: wait; }
26
- textarea:focus, input:focus, button:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
27
- #status { min-height: 34px; margin: 0; padding: 0 16px 12px; color: #6b7280; font-size: 12px; }
28
- #status.loading::before {
29
  content: "";
30
  display: inline-block;
31
- width: 12px;
32
- height: 12px;
33
  margin-right: 8px;
34
- vertical-align: -2px;
35
  border: 2px solid #d1d5db;
36
  border-top-color: #4f46e5;
37
  border-radius: 50%;
@@ -41,42 +83,46 @@
41
  @media (prefers-color-scheme: dark) {
42
  :root { color-scheme: dark; }
43
  body { color: #f3f4f6; background: #111827; }
44
- .endpoint { color: #9ca3af; background: #0f172a; border-color: #374151; }
 
45
  code { color: #d1d5db; }
46
  .assistant { background: #1f2937; }
47
  .error { color: #fecaca; background: #450a0a; }
48
- form { border-color: #374151; }
49
- textarea, input { color: #f3f4f6; background: #1f2937; border-color: #4b5563; }
50
- label, #status { color: #9ca3af; }
51
- textarea:focus, input:focus, button:focus-visible { outline-color: #818cf8; }
52
- #status.loading::before { border-color: #4b5563; border-top-color: #818cf8; }
53
  }
54
- @media (max-width: 600px) {
55
- form { grid-template-columns: 1fr; }
56
- .controls { flex-direction: row; align-items: end; justify-content: space-between; }
57
  .message { max-width: 92%; }
 
 
58
  }
59
  </style>
60
  </head>
61
  <body>
62
  <main>
63
- <div class="endpoint">OpenAI-compatible endpoint: <code>https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1</code></div>
 
 
 
 
 
 
 
64
  <div id="chat" aria-live="polite">
65
- <div class="message assistant">Ask anything. The first response can take longer while the inference worker starts.</div>
66
  </div>
 
67
  <form id="form">
68
- <textarea id="prompt" required aria-label="Message" placeholder="Type a message..."></textarea>
69
- <div class="controls">
70
- <label>Max output tokens<input id="max-tokens" type="number" min="1" max="20000" value="1024"></label>
71
- <button id="send" type="submit">Send</button>
72
- </div>
73
  </form>
 
74
  <p id="status" aria-live="polite"></p>
75
  </main>
76
 
77
  <script>
78
- const API = "https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1";
79
- const MODEL = "LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-Q8_0-GGUF";
80
  const messages = [];
81
  const chat = document.querySelector("#chat");
82
  const form = document.querySelector("#form");
@@ -93,13 +139,28 @@
93
  return element;
94
  }
95
 
 
 
96
  function applyEvent(line, output) {
 
 
 
 
 
 
 
 
97
  if (!line.startsWith("data:")) return;
98
  const payload = line.slice(5).trim();
99
  if (!payload || payload === "[DONE]") return;
100
  const event = JSON.parse(payload);
101
  const content = event.choices?.[0]?.delta?.content || event.choices?.[0]?.text || "";
102
  if (content) {
 
 
 
 
 
103
  output.textContent += content;
104
  chat.scrollTop = chat.scrollHeight;
105
  }
@@ -109,16 +170,17 @@
109
  event.preventDefault();
110
  const text = prompt.value.trim();
111
  if (!text) return;
 
112
  messages.push({ role: "user", content: text });
113
  addMessage("user", text);
114
  prompt.value = "";
115
  send.disabled = true;
116
- status.classList.add("loading");
117
- status.textContent = "Starting model. Cold starts can take a few minutes...";
118
- const slowNotice = setTimeout(() => {
119
- status.textContent = "Still waiting for the inference worker to become ready...";
120
- }, 45_000);
121
  const output = addMessage("assistant", "");
 
 
122
 
123
  try {
124
  const response = await fetch(`${API}/chat/completions`, {
@@ -127,20 +189,22 @@
127
  body: JSON.stringify({
128
  model: MODEL,
129
  messages,
130
- max_tokens: Number(document.querySelector("#max-tokens").value),
131
  stream: true,
 
132
  }),
133
  });
 
134
  if (!response.ok) {
135
- const errorBody = await response.text();
136
- let message = errorBody;
137
- try { message = JSON.parse(errorBody).error?.message || errorBody; } catch {}
138
  throw new Error(message || `HTTP ${response.status}`);
139
  }
140
 
141
  const reader = response.body.getReader();
142
  const decoder = new TextDecoder();
143
  let buffer = "";
 
144
  while (true) {
145
  const { value, done } = await reader.read();
146
  buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
@@ -150,15 +214,18 @@
150
  if (done) break;
151
  }
152
  if (buffer.trim()) applyEvent(buffer.trim(), output);
 
 
 
153
  messages.push({ role: "assistant", content: output.textContent });
154
- status.textContent = "Complete";
155
  } catch (error) {
 
156
  output.classList.add("error");
157
  output.textContent = `Request failed: ${error.message}`;
158
  status.textContent = "The inference service did not complete the request.";
159
  } finally {
160
- clearTimeout(slowNotice);
161
- status.classList.remove("loading");
162
  send.disabled = false;
163
  prompt.focus();
164
  }
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Qwen3.8-27B Humanlike Chat</title>
7
  <style>
8
  * { box-sizing: border-box; }
9
+ body {
10
+ margin: 0;
11
+ color: #111827;
12
+ background: #fff;
13
+ font: 15px/1.5 Inter, ui-sans-serif, system-ui, sans-serif;
14
+ }
15
+ main {
16
+ display: grid;
17
+ grid-template-rows: auto 1fr auto auto;
18
+ width: min(880px, 100%);
19
+ height: 100dvh;
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; }
26
  code { color: #374151; overflow-wrap: anywhere; }
27
+ #chat { padding: 20px 16px; overflow-y: auto; }
28
+ .message {
29
+ max-width: 80%;
30
+ margin: 0 0 14px;
31
+ padding: 10px 14px;
32
+ border-radius: 12px;
33
+ line-height: 1.55;
34
+ white-space: pre-wrap;
35
+ overflow-wrap: anywhere;
36
+ }
37
  .assistant { background: #f3f4f6; }
38
+ .user { margin-left: auto; color: #fff; background: #4f46e5; }
39
  .error { color: #991b1b; background: #fee2e2; }
40
+ .pending { color: #6b7280; font-style: italic; }
41
+ form {
42
+ display: grid;
43
+ grid-template-columns: 1fr auto;
44
+ gap: 10px;
45
+ padding: 14px 16px;
46
+ border-top: 1px solid #e5e7eb;
47
+ }
48
+ textarea {
49
+ min-height: 60px;
50
+ padding: 10px 12px;
51
+ border: 1px solid #d1d5db;
52
+ border-radius: 8px;
53
+ font: inherit;
54
+ resize: vertical;
55
+ }
56
+ button {
57
+ padding: 10px 20px;
58
+ color: #fff;
59
+ background: #4f46e5;
60
+ border: 0;
61
+ border-radius: 8px;
62
+ font: inherit;
63
+ font-weight: 600;
64
+ cursor: pointer;
65
+ align-self: end;
66
+ }
67
  button:disabled { opacity: 0.5; cursor: wait; }
68
+ textarea:focus, button:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
69
+ #status { min-height: 32px; margin: 0; padding: 0 16px 12px; color: #6b7280; font-size: 12px; }
70
+ #status.busy::before {
71
  content: "";
72
  display: inline-block;
73
+ width: 10px;
74
+ height: 10px;
75
  margin-right: 8px;
76
+ vertical-align: -1px;
77
  border: 2px solid #d1d5db;
78
  border-top-color: #4f46e5;
79
  border-radius: 50%;
 
83
  @media (prefers-color-scheme: dark) {
84
  :root { color-scheme: dark; }
85
  body { color: #f3f4f6; background: #111827; }
86
+ header, form { border-color: #374151; }
87
+ header p, #status, .pending { color: #9ca3af; }
88
  code { color: #d1d5db; }
89
  .assistant { background: #1f2937; }
90
  .error { color: #fecaca; background: #450a0a; }
91
+ textarea { color: #f3f4f6; background: #1f2937; border-color: #4b5563; }
92
+ #status.busy::before { border-color: #4b5563; border-top-color: #818cf8; }
 
 
 
93
  }
94
+ @media (max-width: 620px) {
 
 
95
  .message { max-width: 92%; }
96
+ form { grid-template-columns: 1fr; }
97
+ button { width: 100%; }
98
  }
99
  </style>
100
  </head>
101
  <body>
102
  <main>
103
+ <header>
104
+ <h1>Qwen3.8-27B Humanlike Chat</h1>
105
+ <p>
106
+ Live demo of the hosted endpoint <code>https://api.lessthanthreeai.com/v1</code>.
107
+ <a href="https://huggingface.co/LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-GGUF">Model card</a>
108
+ </p>
109
+ </header>
110
+
111
  <div id="chat" aria-live="polite">
112
+ <div class="message assistant">Say something. The first reply can take a couple of minutes while the inference worker starts.</div>
113
  </div>
114
+
115
  <form id="form">
116
+ <textarea id="prompt" required aria-label="Message" placeholder="Type a message"></textarea>
117
+ <button id="send" type="submit">Send</button>
 
 
 
118
  </form>
119
+
120
  <p id="status" aria-live="polite"></p>
121
  </main>
122
 
123
  <script>
124
+ const API = "https://api.lessthanthreeai.com/v1";
125
+ const MODEL = "qwen3.8-27b-humanlike-chat";
126
  const messages = [];
127
  const chat = document.querySelector("#chat");
128
  const form = document.querySelector("#form");
 
139
  return element;
140
  }
141
 
142
+ // Server-sent events arrive as "data: {...}" chunks. The endpoint may also send
143
+ // ": status={...}" comment lines while a cold worker starts.
144
  function applyEvent(line, output) {
145
+ if (line.startsWith(":")) {
146
+ const payload = line.slice(1).trim().replace(/^status=/, "");
147
+ try {
148
+ const state = JSON.parse(payload);
149
+ if (state.message) status.textContent = state.message;
150
+ } catch {}
151
+ return;
152
+ }
153
  if (!line.startsWith("data:")) return;
154
  const payload = line.slice(5).trim();
155
  if (!payload || payload === "[DONE]") return;
156
  const event = JSON.parse(payload);
157
  const content = event.choices?.[0]?.delta?.content || event.choices?.[0]?.text || "";
158
  if (content) {
159
+ if (output.classList.contains("pending")) {
160
+ output.classList.remove("pending");
161
+ output.textContent = "";
162
+ status.textContent = "";
163
+ }
164
  output.textContent += content;
165
  chat.scrollTop = chat.scrollHeight;
166
  }
 
170
  event.preventDefault();
171
  const text = prompt.value.trim();
172
  if (!text) return;
173
+
174
  messages.push({ role: "user", content: text });
175
  addMessage("user", text);
176
  prompt.value = "";
177
  send.disabled = true;
178
+ status.classList.add("busy");
179
+ status.textContent = "Starting the model. A cold start takes about two to three minutes.";
180
+
 
 
181
  const output = addMessage("assistant", "");
182
+ output.classList.add("pending");
183
+ output.textContent = "waiting for the first token";
184
 
185
  try {
186
  const response = await fetch(`${API}/chat/completions`, {
 
189
  body: JSON.stringify({
190
  model: MODEL,
191
  messages,
 
192
  stream: true,
193
+ max_tokens: 512,
194
  }),
195
  });
196
+
197
  if (!response.ok) {
198
+ const body = await response.text();
199
+ let message = body;
200
+ try { message = JSON.parse(body).error?.message || body; } catch {}
201
  throw new Error(message || `HTTP ${response.status}`);
202
  }
203
 
204
  const reader = response.body.getReader();
205
  const decoder = new TextDecoder();
206
  let buffer = "";
207
+
208
  while (true) {
209
  const { value, done } = await reader.read();
210
  buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
 
214
  if (done) break;
215
  }
216
  if (buffer.trim()) applyEvent(buffer.trim(), output);
217
+
218
+ output.classList.remove("pending");
219
+ if (!output.textContent) output.textContent = "(empty response)";
220
  messages.push({ role: "assistant", content: output.textContent });
221
+ status.textContent = "Complete. Later replies are fast while the worker stays warm.";
222
  } catch (error) {
223
+ output.classList.remove("pending");
224
  output.classList.add("error");
225
  output.textContent = `Request failed: ${error.message}`;
226
  status.textContent = "The inference service did not complete the request.";
227
  } finally {
228
+ status.classList.remove("busy");
 
229
  send.disabled = false;
230
  prompt.focus();
231
  }