kvyb commited on
Commit
20af531
·
verified ·
1 Parent(s): a2929ff

Use standard Gradio chat UI

Browse files
Files changed (3) hide show
  1. README.md +3 -3
  2. app.py +55 -0
  3. index.html +0 -361
README.md CHANGED
@@ -2,8 +2,8 @@
2
  title: Qwen3.8 27B Humanlike Chat
3
  colorFrom: indigo
4
  colorTo: pink
5
- sdk: static
6
- app_file: index.html
7
  fullWidth: true
8
  short_description: Chat with the Q8_0 Humanlike model through its public API.
9
  models:
@@ -12,4 +12,4 @@ models:
12
 
13
  # Qwen3.8 27B Humanlike Chat
14
 
15
- Static chat client for the public OpenAI-compatible inference endpoint. Prompts and model responses are retained privately for evaluation.
 
2
  title: Qwen3.8 27B Humanlike Chat
3
  colorFrom: indigo
4
  colorTo: pink
5
+ sdk: gradio
6
+ app_file: app.py
7
  fullWidth: true
8
  short_description: Chat with the Q8_0 Humanlike model through its public API.
9
  models:
 
12
 
13
  # Qwen3.8 27B Humanlike Chat
14
 
15
+ Gradio chat client for the public OpenAI-compatible inference endpoint.
app.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from urllib.error import HTTPError, URLError
3
+ from urllib.request import Request, urlopen
4
+
5
+ import gradio as gr
6
+
7
+
8
+ API_BASE = "https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1"
9
+ MODEL = "LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-Q8_0-GGUF"
10
+
11
+
12
+ def chat(message, history, max_tokens):
13
+ request = Request(
14
+ f"{API_BASE}/chat/completions",
15
+ data=json.dumps(
16
+ {
17
+ "model": MODEL,
18
+ "messages": [*history, {"role": "user", "content": message}],
19
+ "max_tokens": int(max_tokens),
20
+ "stream": True,
21
+ }
22
+ ).encode(),
23
+ headers={"Content-Type": "application/json"},
24
+ method="POST",
25
+ )
26
+
27
+ answer = ""
28
+ try:
29
+ with urlopen(request, timeout=900) as response:
30
+ for raw_line in response:
31
+ line = raw_line.decode().strip()
32
+ if not line.startswith("data:"):
33
+ continue
34
+ payload = line[5:].strip()
35
+ if not payload or payload == "[DONE]":
36
+ continue
37
+ event = json.loads(payload)
38
+ content = event.get("choices", [{}])[0].get("delta", {}).get("content", "")
39
+ if content:
40
+ answer += content
41
+ yield answer
42
+ except HTTPError as error:
43
+ raise gr.Error(error.read().decode() or f"Inference request failed: HTTP {error.code}") from error
44
+ except URLError as error:
45
+ raise gr.Error(f"Inference endpoint unavailable: {error.reason}") from error
46
+
47
+
48
+ with gr.Blocks(fill_height=True) as demo:
49
+ gr.Markdown(f"OpenAI-compatible endpoint: `{API_BASE}`")
50
+ max_tokens = gr.Slider(1, 20_000, value=1_024, step=1, label="Max output tokens")
51
+ gr.ChatInterface(chat, type="messages", additional_inputs=[max_tokens])
52
+
53
+
54
+ if __name__ == "__main__":
55
+ demo.launch()
index.html DELETED
@@ -1,361 +0,0 @@
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">
6
- <meta name="description" content="Chat with Qwen3.8 27B Humanlike through its public OpenAI-compatible API.">
7
- <title>Qwen3.8 27B Humanlike Chat</title>
8
- <style>
9
- :root {
10
- color-scheme: light;
11
- --paper: #f4efe4;
12
- --ink: #18231e;
13
- --muted: #657068;
14
- --line: #c9c2b3;
15
- --accent: #d9513d;
16
- --accent-dark: #a63225;
17
- --panel: rgba(255, 253, 247, 0.78);
18
- --user: #18231e;
19
- --assistant: #fffdf7;
20
- }
21
-
22
- * { box-sizing: border-box; }
23
-
24
- body {
25
- margin: 0;
26
- min-height: 100vh;
27
- color: var(--ink);
28
- background:
29
- radial-gradient(circle at 9% 12%, rgba(217, 81, 61, 0.13), transparent 28rem),
30
- linear-gradient(120deg, transparent 0 49.7%, rgba(24, 35, 30, 0.045) 50%, transparent 50.3%),
31
- var(--paper);
32
- font-family: Georgia, "Times New Roman", serif;
33
- }
34
-
35
- button, input, textarea { font: inherit; }
36
-
37
- .shell {
38
- width: min(1180px, calc(100% - 32px));
39
- margin: 0 auto;
40
- padding: 32px 0;
41
- }
42
-
43
- header {
44
- display: flex;
45
- align-items: end;
46
- justify-content: space-between;
47
- gap: 24px;
48
- padding-bottom: 20px;
49
- border-bottom: 1px solid var(--ink);
50
- }
51
-
52
- .eyebrow, label, .label, code, pre, .status {
53
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
54
- }
55
-
56
- .eyebrow {
57
- margin: 0 0 8px;
58
- color: var(--accent-dark);
59
- font-size: 0.72rem;
60
- font-weight: 700;
61
- letter-spacing: 0.16em;
62
- text-transform: uppercase;
63
- }
64
-
65
- h1 {
66
- max-width: 760px;
67
- margin: 0;
68
- font-size: clamp(2.2rem, 6vw, 5.4rem);
69
- font-weight: 400;
70
- line-height: 0.92;
71
- letter-spacing: -0.045em;
72
- }
73
-
74
- .model-link {
75
- color: var(--ink);
76
- text-decoration-thickness: 1px;
77
- text-underline-offset: 4px;
78
- white-space: nowrap;
79
- }
80
-
81
- main {
82
- display: grid;
83
- grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.75fr);
84
- gap: 24px;
85
- padding-top: 24px;
86
- }
87
-
88
- .chat, aside section {
89
- border: 1px solid var(--line);
90
- background: var(--panel);
91
- box-shadow: 6px 6px 0 rgba(24, 35, 30, 0.08);
92
- }
93
-
94
- .transcript {
95
- height: min(58vh, 600px);
96
- min-height: 360px;
97
- overflow-y: auto;
98
- padding: 22px;
99
- }
100
-
101
- .message {
102
- width: fit-content;
103
- max-width: 88%;
104
- margin: 0 0 16px;
105
- padding: 14px 16px;
106
- border: 1px solid var(--line);
107
- white-space: pre-wrap;
108
- overflow-wrap: anywhere;
109
- line-height: 1.5;
110
- }
111
-
112
- .message.user {
113
- margin-left: auto;
114
- color: var(--paper);
115
- background: var(--user);
116
- border-color: var(--user);
117
- }
118
-
119
- .message.assistant { background: var(--assistant); }
120
- .message.error { color: #8c241a; border-color: #c77a70; }
121
-
122
- form {
123
- display: grid;
124
- grid-template-columns: 1fr auto;
125
- gap: 12px;
126
- padding: 16px;
127
- border-top: 1px solid var(--line);
128
- }
129
-
130
- textarea {
131
- width: 100%;
132
- min-height: 94px;
133
- resize: vertical;
134
- padding: 12px;
135
- color: var(--ink);
136
- background: #fffdf7;
137
- border: 1px solid var(--line);
138
- border-radius: 0;
139
- }
140
-
141
- textarea:focus, input:focus, button:focus-visible {
142
- outline: 2px solid var(--accent);
143
- outline-offset: 2px;
144
- }
145
-
146
- .controls {
147
- display: flex;
148
- flex-direction: column;
149
- justify-content: space-between;
150
- gap: 10px;
151
- }
152
-
153
- input[type="number"] {
154
- width: 112px;
155
- padding: 8px;
156
- color: var(--ink);
157
- background: #fffdf7;
158
- border: 1px solid var(--line);
159
- }
160
-
161
- button {
162
- padding: 11px 16px;
163
- color: white;
164
- background: var(--accent);
165
- border: 1px solid var(--accent-dark);
166
- cursor: pointer;
167
- }
168
-
169
- button:hover { background: var(--accent-dark); }
170
- button:disabled { cursor: wait; opacity: 0.6; }
171
-
172
- aside {
173
- display: flex;
174
- flex-direction: column;
175
- gap: 18px;
176
- }
177
-
178
- aside section { padding: 18px; }
179
- aside h2 { margin: 0 0 12px; font-size: 1.1rem; }
180
- aside p { margin: 0; color: var(--muted); line-height: 1.5; }
181
-
182
- .endpoint {
183
- display: block;
184
- margin: 10px 0;
185
- padding: 10px;
186
- overflow-x: auto;
187
- color: var(--ink);
188
- background: #e9e2d5;
189
- border: 1px solid var(--line);
190
- font-size: 0.78rem;
191
- }
192
-
193
- pre {
194
- margin: 10px 0 0;
195
- padding: 12px;
196
- overflow-x: auto;
197
- color: #eaf0eb;
198
- background: #18231e;
199
- font-size: 0.7rem;
200
- line-height: 1.45;
201
- white-space: pre-wrap;
202
- }
203
-
204
- .notice { border-left: 5px solid var(--accent); }
205
- .status { min-height: 1.2em; margin: 0; padding: 0 16px 14px; color: var(--muted); font-size: 0.75rem; }
206
-
207
- @media (max-width: 780px) {
208
- .shell { width: min(100% - 20px, 680px); padding: 20px 0; }
209
- header { align-items: start; flex-direction: column; }
210
- h1 { font-size: clamp(2.4rem, 15vw, 4.4rem); }
211
- main { grid-template-columns: 1fr; }
212
- .transcript { height: 52vh; min-height: 320px; }
213
- form { grid-template-columns: 1fr; }
214
- .controls { flex-direction: row; align-items: end; }
215
- .message { max-width: 94%; }
216
- }
217
- </style>
218
- </head>
219
- <body>
220
- <div class="shell">
221
- <header>
222
- <div>
223
- <p class="eyebrow">Public research preview</p>
224
- <h1>Qwen3.8 27B Humanlike Chat</h1>
225
- </div>
226
- <a class="model-link" href="https://huggingface.co/LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-Q8_0-GGUF">View model files</a>
227
- </header>
228
-
229
- <main>
230
- <section class="chat" aria-label="Chat interface">
231
- <div id="transcript" class="transcript" aria-live="polite">
232
- <div class="message assistant">Ask anything. The first response can take longer while the inference worker starts.</div>
233
- </div>
234
- <form id="chat-form">
235
- <label>
236
- Message
237
- <textarea id="prompt" required placeholder="Write a message..." aria-label="Message"></textarea>
238
- </label>
239
- <div class="controls">
240
- <label>
241
- Max tokens
242
- <input id="max-tokens" type="number" min="1" max="20000" value="1024">
243
- </label>
244
- <button id="send" type="submit">Send</button>
245
- </div>
246
- </form>
247
- <p id="status" class="status" aria-live="polite"></p>
248
- </section>
249
-
250
- <aside>
251
- <section class="notice">
252
- <h2>Retention notice</h2>
253
- <p>Prompts and model responses are retained privately for evaluation. Authorization headers and IP addresses are not stored in conversation records.</p>
254
- </section>
255
-
256
- <section>
257
- <p class="label">OPENAI-COMPATIBLE ENDPOINT</p>
258
- <code class="endpoint">https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1</code>
259
- <button id="copy-endpoint" type="button">Copy endpoint</button>
260
- <pre>curl https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1/chat/completions \
261
- -H 'Content-Type: application/json' \
262
- -d '{"model":"LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-Q8_0-GGUF","messages":[{"role":"user","content":"Hello"}],"max_tokens":1024,"stream":true}'</pre>
263
- </section>
264
- </aside>
265
- </main>
266
- </div>
267
-
268
- <script>
269
- const API = "https://qwen38-humanlike-api.qwen38-public-api.workers.dev/v1";
270
- const MODEL = "LessThanThreeAI/Qwen3.8-27B-Humanlike-Chat-Q8_0-GGUF";
271
- const messages = [];
272
- const form = document.querySelector("#chat-form");
273
- const prompt = document.querySelector("#prompt");
274
- const send = document.querySelector("#send");
275
- const status = document.querySelector("#status");
276
- const transcript = document.querySelector("#transcript");
277
-
278
- function addMessage(role, text, extraClass = "") {
279
- const element = document.createElement("div");
280
- element.className = `message ${role} ${extraClass}`;
281
- element.textContent = text;
282
- transcript.append(element);
283
- transcript.scrollTop = transcript.scrollHeight;
284
- return element;
285
- }
286
-
287
- function applyEvent(line, output) {
288
- if (!line.startsWith("data:")) return;
289
- const payload = line.slice(5).trim();
290
- if (!payload || payload === "[DONE]") return;
291
- const event = JSON.parse(payload);
292
- const content = event.choices?.[0]?.delta?.content || event.choices?.[0]?.text || "";
293
- if (content) {
294
- output.textContent += content;
295
- transcript.scrollTop = transcript.scrollHeight;
296
- }
297
- }
298
-
299
- form.addEventListener("submit", async (event) => {
300
- event.preventDefault();
301
- const text = prompt.value.trim();
302
- if (!text) return;
303
-
304
- messages.push({ role: "user", content: text });
305
- addMessage("user", text);
306
- prompt.value = "";
307
- send.disabled = true;
308
- status.textContent = "Waiting for model...";
309
- const output = addMessage("assistant", "");
310
-
311
- try {
312
- const response = await fetch(`${API}/chat/completions`, {
313
- method: "POST",
314
- headers: { "content-type": "application/json" },
315
- body: JSON.stringify({
316
- model: MODEL,
317
- messages,
318
- max_tokens: Number(document.querySelector("#max-tokens").value),
319
- stream: true,
320
- }),
321
- });
322
- if (!response.ok) throw new Error((await response.text()) || `HTTP ${response.status}`);
323
-
324
- if (!response.headers.get("content-type")?.includes("text/event-stream")) {
325
- const result = await response.json();
326
- output.textContent = result.choices?.[0]?.message?.content || "No response content.";
327
- } else {
328
- const reader = response.body.getReader();
329
- const decoder = new TextDecoder();
330
- let buffer = "";
331
- while (true) {
332
- const { value, done } = await reader.read();
333
- buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
334
- const lines = buffer.split("\n");
335
- buffer = lines.pop() || "";
336
- for (const line of lines) applyEvent(line.trim(), output);
337
- if (done) break;
338
- }
339
- if (buffer.trim()) applyEvent(buffer.trim(), output);
340
- }
341
-
342
- messages.push({ role: "assistant", content: output.textContent });
343
- status.textContent = "Complete";
344
- } catch (error) {
345
- output.classList.add("error");
346
- output.textContent = `Request failed: ${error.message}`;
347
- status.textContent = "The inference service did not complete the request.";
348
- } finally {
349
- send.disabled = false;
350
- prompt.focus();
351
- }
352
- });
353
-
354
- document.querySelector("#copy-endpoint").addEventListener("click", async (event) => {
355
- await navigator.clipboard.writeText(API);
356
- event.currentTarget.textContent = "Copied";
357
- setTimeout(() => { event.currentTarget.textContent = "Copy endpoint"; }, 1500);
358
- });
359
- </script>
360
- </body>
361
- </html>