akhaliq HF Staff GLM-5.2 commited on
Commit
6b5cd6a
·
1 Parent(s): 32c3dcb

Terminal-style UI for Ornith-1.0-9B

Browse files

- Rewrite index.html as a dark terminal-style coding assistant UI.
- Use the Ornith logo from the Hub Xet CDN as the header image.
- Monospace prompts, collapsible thinking block, markdown/code rendering.
- Keep streaming chat, image attachment, temperature/max-token controls.

Co-Authored-By: GLM-5.2 <noreply@z.ai>

Files changed (1) hide show
  1. index.html +150 -132
index.html CHANGED
@@ -3,160 +3,164 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Ornith-1.0-9B · Chat</title>
7
  <style>
8
  :root {
9
- --bg: #0d1117;
10
- --panel: #161b22;
11
- --panel-2: #1c232c;
12
- --border: #30363d;
13
- --text: #e6edf3;
14
- --muted: #9da7b3;
15
- --accent: #ff9d00;
16
- --accent-2: #6f42c1;
17
- --user: #2d333b;
18
- --reason: #14202b;
19
- --ok: #3fb950;
20
- --err: #f85149;
21
  }
 
22
  * { box-sizing: border-box; }
23
  html, body { height: 100%; margin: 0; }
24
  body {
25
  background: var(--bg);
26
  color: var(--text);
27
- font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
 
 
28
  display: flex;
29
  flex-direction: column;
30
  }
 
31
  header {
32
  display: flex;
33
  align-items: center;
34
  gap: 12px;
35
- padding: 12px 18px;
36
- background: linear-gradient(90deg, var(--panel), var(--panel-2));
37
  border-bottom: 1px solid var(--border);
38
  }
39
- .logo { font-size: 26px; line-height: 1; }
40
- header h1 { font-size: 17px; margin: 0; font-weight: 600; }
41
- header .sub { color: var(--muted); font-size: 12px; }
42
  header .spacer { flex: 1; }
43
- .badge {
44
  font-size: 11px; color: var(--muted); border: 1px solid var(--border);
45
- padding: 3px 8px; border-radius: 999px; cursor: pointer; user-select: none;
46
  }
47
- .badge:hover { color: var(--text); border-color: var(--muted); }
48
 
49
  #chat {
50
  flex: 1;
51
  overflow-y: auto;
52
- padding: 20px 18px 8px;
53
- display: flex;
54
- flex-direction: column;
55
- gap: 14px;
56
- }
57
- .msg { max-width: 820px; width: 100%; margin: 0 auto; }
58
- .bubble {
59
- border-radius: 12px; padding: 12px 14px; border: 1px solid var(--border);
60
- white-space: normal; word-wrap: break-word; overflow-wrap: anywhere;
61
- }
62
- .msg.user .bubble { background: var(--user); }
63
- .msg.assistant .bubble { background: var(--panel); }
64
- .role { font-size: 12px; color: var(--muted); margin: 0 4px 5px; }
65
- .msg img.attached {
66
- max-width: 220px; max-height: 220px; border-radius: 8px;
67
- margin-top: 8px; border: 1px solid var(--border); display: block;
68
  }
 
 
69
 
70
- .reasoning {
71
- margin-bottom: 8px; border-left: 3px solid var(--accent-2);
72
- background: var(--reason); border-radius: 6px; overflow: hidden;
73
- }
74
- .reasoning summary {
75
- cursor: pointer; padding: 6px 10px; font-size: 12px; color: var(--muted);
76
- list-style: none;
 
 
 
 
 
 
77
  }
78
- .reasoning summary::-webkit-details-marker { display: none; }
79
- .reasoning summary::before { content: "▸ "; }
80
- .reasoning[open] summary::before { content: "▾ "; }
81
- .reasoning pre {
82
- margin: 0; padding: 8px 12px 10px; white-space: pre-wrap;
83
- word-wrap: break-word; font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
84
- color: #b9c2cf; max-height: 340px; overflow-y: auto;
 
85
  }
86
- .answer { overflow-x: auto; }
 
 
 
87
  .answer pre {
88
- background: #010409; border: 1px solid var(--border); border-radius: 8px;
89
- padding: 10px 12px; overflow-x: auto; margin: 8px 0;
90
- font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
91
  }
92
- .answer code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
93
- .answer :not(pre) > code { background: #010409; padding: 1px 5px; border-radius: 4px; }
94
- .answer p { margin: 6px 0; }
95
  .answer ul, .answer ol { padding-left: 22px; margin: 6px 0; }
96
- .answer h1,.answer h2,.answer h3 { margin: 12px 0 6px; }
97
- .answer a { color: var(--accent); }
98
- .err { color: var(--err); }
 
 
99
 
100
- #status {
101
- max-width: 820px; width: 100%; margin: 0 auto;
102
- color: var(--muted); font-size: 12px; min-height: 16px; padding: 0 4px;
103
- }
104
 
105
  .composer {
106
- border-top: 1px solid var(--border);
107
- background: var(--panel);
108
- padding: 12px 18px 16px;
109
  }
110
- .composer-inner { max-width: 820px; margin: 0 auto; }
 
111
  #settings {
112
- display: none; gap: 18px; flex-wrap: wrap; margin-bottom: 10px;
113
- color: var(--muted); font-size: 12px;
114
  }
115
  #settings.open { display: flex; }
116
  #settings label { display: flex; flex-direction: column; gap: 4px; }
117
- #settings input[type=range] { width: 150px; accent-color: var(--accent); }
 
118
  .previews { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
119
  .previews .thumb { position: relative; }
120
- .previews .thumb img { height: 56px; border-radius: 6px; border: 1px solid var(--border); }
121
  .previews .thumb .x {
122
  position: absolute; top: -6px; right: -6px; cursor: pointer;
123
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 50%;
124
- width: 18px; height: 18px; text-align: center; line-height: 16px; font-size: 12px;
125
  }
126
- .row { display: flex; gap: 8px; align-items: flex-end; }
 
127
  #text {
128
- flex: 1; resize: none; min-height: 44px; max-height: 180px;
129
- background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
130
- border-radius: 10px; padding: 11px 12px; font: inherit; outline: none;
131
  }
132
- #text:focus { border-color: var(--accent); }
133
  button {
134
- background: var(--accent); color: #1a1200; border: none; border-radius: 10px;
135
- padding: 11px 16px; font: 600 14px inherit; cursor: pointer;
136
  }
137
  button:disabled { opacity: .5; cursor: not-allowed; }
138
  #stop { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); display: none; }
139
- .icon-btn {
140
- background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
141
- padding: 11px 12px; font-size: 16px;
142
- }
143
  .hint { color: var(--muted); font-size: 11px; margin-top: 8px; text-align: center; }
 
 
 
 
 
 
144
  </style>
145
  </head>
146
  <body>
147
 
148
  <header>
149
- <div class="logo">🤗</div>
150
- <div>
151
- <h1>Ornith-1.0-9B</h1>
152
- <div class="sub">deepreinforce-ai · multimodal reasoning · gradio.Server</div>
153
- </div>
154
  <div class="spacer"></div>
155
- <div class="badge" id="settings-toggle">⚙ Settings</div>
156
  </header>
157
 
158
- <div id="chat"></div>
159
- <div id="status"></div>
 
 
 
 
 
160
 
161
  <div class="composer">
162
  <div class="composer-inner">
@@ -165,13 +169,13 @@
165
  <label>Max tokens <input type="range" id="maxtok" min="256" max="8192" step="256" value="2048" /><span id="maxtok-v">2048</span></label>
166
  </div>
167
  <div class="previews" id="previews"></div>
168
- <div class="row">
169
  <button class="icon-btn" id="attach" title="Attach image">📎</button>
170
- <textarea id="text" placeholder="Message Ornith… (Shift+Enter for newline, Enter to send)"></textarea>
171
  <button id="send">Send</button>
172
  <button id="stop">Stop</button>
173
  </div>
174
- <div class="hint">Image-text-to-text model. Attach an image and ask about it, or just chat. Reasoning streams in the purple panel.</div>
175
  </div>
176
  </div>
177
 
@@ -197,8 +201,8 @@ const tempV = document.getElementById("temp-v");
197
  const maxEl = document.getElementById("maxtok");
198
  const maxV = document.getElementById("maxtok-v");
199
 
200
- let conversation = []; // [{role, content}]
201
- let pendingImage = null; // File
202
  let client = null;
203
  let job = null;
204
 
@@ -233,7 +237,7 @@ function renderPreviews() {
233
 
234
  textEl.addEventListener("input", () => {
235
  textEl.style.height = "auto";
236
- textEl.style.height = Math.min(textEl.scrollHeight, 180) + "px";
237
  });
238
  textEl.addEventListener("keydown", (e) => {
239
  if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
@@ -246,17 +250,25 @@ function scrollIfNear() {
246
  if (near) chatEl.scrollTop = chatEl.scrollHeight;
247
  }
248
 
249
- function addMsg(role) {
250
- const m = document.createElement("div");
251
- m.className = "msg " + role;
252
- const r = document.createElement("div");
253
- r.className = "role";
254
- r.textContent = role === "user" ? "You" : "Ornith-1.0-9B";
255
- const b = document.createElement("div");
256
- b.className = "bubble";
257
- m.appendChild(r); m.appendChild(b); chatEl.appendChild(m);
 
 
 
 
 
 
 
 
258
  chatEl.scrollTop = chatEl.scrollHeight;
259
- return b;
260
  }
261
 
262
  async function send() {
@@ -264,13 +276,13 @@ async function send() {
264
  if (!message && !pendingImage) return;
265
  if (job) return;
266
 
267
- const userBubble = addMsg("user");
268
- let html = escapeHtml(message);
269
  if (pendingImage) {
270
  const url = URL.createObjectURL(pendingImage);
271
- html += `<img class="attached" src="${url}" alt="attachment"/>`;
272
  }
273
- userBubble.innerHTML = html;
274
 
275
  const sentImage = pendingImage;
276
  pendingImage = null; renderPreviews();
@@ -279,11 +291,11 @@ async function send() {
279
  const history = conversation.slice();
280
  conversation.push({ role: "user", content: message });
281
 
282
- const assistantBubble = addMsg("assistant");
283
- renderAssistant(assistantBubble, "", "");
284
 
285
  setBusy(true);
286
- statusEl.textContent = "Connecting…";
287
 
288
  try {
289
  const c = await connect();
@@ -296,11 +308,15 @@ async function send() {
296
  };
297
  job = c.submit("/generate", payload);
298
 
 
 
 
 
299
  for await (const msg of job) {
300
  if (msg.type === "status") {
301
  const s = msg.status || msg.data?.status;
302
- if (s === "pending") statusEl.textContent = `Queued…${msg.position != null ? " (#" + msg.position + ")" : ""}`;
303
- else if (s === "generating") statusEl.textContent = "Generating…";
304
  else if (s === "complete" || s === "error") statusEl.textContent = "";
305
  continue;
306
  }
@@ -308,19 +324,23 @@ async function send() {
308
  const raw = Array.isArray(msg.data) ? msg.data[0] : msg.data;
309
  let d = raw;
310
  if (typeof raw === "string") { try { d = JSON.parse(raw); } catch { d = { answer: raw }; } }
311
- if (d && d.error) {
312
- renderAssistant(assistantBubble, d.reasoning || "", d.answer || "", d.error);
313
- statusEl.innerHTML = `<span class="err">Error: ${escapeHtml(d.error)}</span>`;
314
  break;
315
  }
316
- renderAssistant(assistantBubble, d?.reasoning || "", d?.answer || "");
317
- if (d && d.status === "complete") {
318
- if (d.answer) conversation.push({ role: "assistant", content: d.answer });
319
- }
320
  scrollIfNear();
321
  }
 
 
 
 
322
  } catch (e) {
323
- renderAssistant(assistantBubble, "", "", String(e));
324
  statusEl.innerHTML = `<span class="err">${escapeHtml(String(e))}</span>`;
325
  } finally {
326
  job = null;
@@ -329,11 +349,10 @@ async function send() {
329
  }
330
  }
331
 
332
- function renderAssistant(bubble, reasoning, answer, error) {
333
  let h = "";
334
  if (reasoning && reasoning.trim()) {
335
- const open = error === undefined ? " open" : ""; // collapse once answer lands
336
- h += `<details class="reasoning"${answer ? "" : " open"}><summary>Reasoning</summary><pre>${escapeHtml(reasoning)}</pre></details>`;
337
  }
338
  if (error) {
339
  h += `<div class="err">⚠ ${escapeHtml(error)}</div>`;
@@ -342,7 +361,7 @@ function renderAssistant(bubble, reasoning, answer, error) {
342
  } else if (!reasoning) {
343
  h += `<span style="color:var(--muted)">…</span>`;
344
  }
345
- bubble.innerHTML = h;
346
  }
347
 
348
  function setBusy(busy) {
@@ -357,7 +376,6 @@ function escapeHtml(s) {
357
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
358
  }
359
 
360
- // Focus the input on load.
361
  textEl.focus();
362
  </script>
363
  </body>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Ornith-1.0-9B · Claude Code</title>
7
  <style>
8
  :root {
9
+ --bg: #0a0a0a;
10
+ --panel: #111111;
11
+ --panel-2: #171717;
12
+ --border: #2a2a2a;
13
+ --text: #e0e0e0;
14
+ --muted: #6b6b6b;
15
+ --green: #4ade80;
16
+ --green-dim: #22c55e;
17
+ --yellow: #facc15;
18
+ --red: #f87171;
19
+ --blue: #60a5fa;
20
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
21
  }
22
+
23
  * { box-sizing: border-box; }
24
  html, body { height: 100%; margin: 0; }
25
  body {
26
  background: var(--bg);
27
  color: var(--text);
28
+ font-family: var(--mono);
29
+ font-size: 14px;
30
+ line-height: 1.6;
31
  display: flex;
32
  flex-direction: column;
33
  }
34
+
35
  header {
36
  display: flex;
37
  align-items: center;
38
  gap: 12px;
39
+ padding: 10px 16px;
40
+ background: var(--panel);
41
  border-bottom: 1px solid var(--border);
42
  }
43
+ header img { height: 28px; width: auto; }
44
+ header .title { font-weight: 600; font-size: 13px; color: var(--text); }
45
+ header .title span { color: var(--muted); font-weight: 400; }
46
  header .spacer { flex: 1; }
47
+ header .badge {
48
  font-size: 11px; color: var(--muted); border: 1px solid var(--border);
49
+ padding: 2px 8px; border-radius: 4px; cursor: pointer; user-select: none;
50
  }
51
+ header .badge:hover { color: var(--text); border-color: var(--muted); }
52
 
53
  #chat {
54
  flex: 1;
55
  overflow-y: auto;
56
+ padding: 0 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
+ .turn { max-width: 900px; margin: 0 auto; padding: 10px 0; border-bottom: 1px solid var(--border); }
59
+ .turn:last-child { border-bottom: none; }
60
 
61
+ .prompt-line { display: flex; align-items: flex-start; gap: 10px; }
62
+ .prompt { color: var(--green); white-space: nowrap; font-weight: 600; user-select: none; }
63
+ .prompt.user::before { content: "❯"; }
64
+ .prompt.assistant::before { content: "◆"; color: var(--green-dim); }
65
+ .content { flex: 1; min-width: 0; }
66
+
67
+ .user-input { color: var(--text); white-space: pre-wrap; }
68
+ .user-image { margin-top: 8px; max-width: 240px; border: 1px solid var(--border); border-radius: 4px; display: block; }
69
+
70
+ .thinking { margin: 6px 0 10px; }
71
+ .thinking summary {
72
+ cursor: pointer; color: var(--muted); font-size: 12px; list-style: none;
73
+ display: flex; align-items: center; gap: 6px;
74
  }
75
+ .thinking summary::-webkit-details-marker { display: none; }
76
+ .thinking summary::before { content: "▸"; color: var(--green-dim); }
77
+ .thinking[open] summary::before { content: "▾"; }
78
+ .thinking pre {
79
+ margin: 6px 0 0; padding: 10px 12px; background: var(--panel-2);
80
+ border-left: 2px solid var(--green-dim); color: var(--muted);
81
+ font: inherit; white-space: pre-wrap; word-wrap: break-word;
82
+ max-height: 300px; overflow-y: auto;
83
  }
84
+
85
+ .answer { color: var(--text); }
86
+ .answer p { margin: 0 0 8px; }
87
+ .answer p:last-child { margin-bottom: 0; }
88
  .answer pre {
89
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px;
90
+ padding: 12px 14px; overflow-x: auto; margin: 10px 0; font: inherit;
 
91
  }
92
+ .answer code { font-family: inherit; }
93
+ .answer :not(pre) > code { background: var(--panel-2); padding: 1px 4px; border-radius: 3px; color: var(--yellow); }
 
94
  .answer ul, .answer ol { padding-left: 22px; margin: 6px 0; }
95
+ .answer a { color: var(--green); }
96
+ .answer blockquote { margin: 8px 0; padding-left: 12px; border-left: 2px solid var(--muted); color: var(--muted); }
97
+ .answer h1,.answer h2,.answer h3 { margin: 14px 0 6px; font-size: inherit; color: var(--green); }
98
+ .answer h1 { font-size: 16px; }
99
+ .answer h2 { font-size: 15px; }
100
 
101
+ .status { max-width: 900px; margin: 0 auto; padding: 4px 0; color: var(--muted); font-size: 12px; min-height: 18px; }
102
+ .status .err { color: var(--red); }
 
 
103
 
104
  .composer {
105
+ border-top: 1px solid var(--border); background: var(--panel); padding: 12px 16px 16px;
 
 
106
  }
107
+ .composer-inner { max-width: 900px; margin: 0 auto; }
108
+
109
  #settings {
110
+ display: none; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; color: var(--muted); font-size: 12px;
 
111
  }
112
  #settings.open { display: flex; }
113
  #settings label { display: flex; flex-direction: column; gap: 4px; }
114
+ #settings input[type=range] { width: 150px; accent-color: var(--green); }
115
+
116
  .previews { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
117
  .previews .thumb { position: relative; }
118
+ .previews .thumb img { height: 48px; border-radius: 4px; border: 1px solid var(--border); }
119
  .previews .thumb .x {
120
  position: absolute; top: -6px; right: -6px; cursor: pointer;
121
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 50%;
122
+ width: 16px; height: 16px; text-align: center; line-height: 14px; font-size: 11px;
123
  }
124
+
125
+ .input-row { display: flex; align-items: flex-end; gap: 8px; }
126
  #text {
127
+ flex: 1; resize: none; min-height: 42px; max-height: 160px;
128
+ background: var(--bg); color: var(--text); border: 1px solid var(--border);
129
+ border-radius: 6px; padding: 10px 12px; font: inherit; outline: none;
130
  }
131
+ #text:focus { border-color: var(--green); }
132
  button {
133
+ background: var(--green); color: #052e16; border: none; border-radius: 6px;
134
+ padding: 10px 16px; font: 600 13px inherit; cursor: pointer;
135
  }
136
  button:disabled { opacity: .5; cursor: not-allowed; }
137
  #stop { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); display: none; }
138
+ .icon-btn { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); padding: 10px 12px; font-size: 14px; }
139
+
 
 
140
  .hint { color: var(--muted); font-size: 11px; margin-top: 8px; text-align: center; }
141
+
142
+ /* Empty state */
143
+ .welcome { max-width: 900px; margin: 24px auto; color: var(--muted); }
144
+ .welcome h2 { color: var(--green); margin: 0 0 8px; font-size: 14px; }
145
+ .welcome p { margin: 0; }
146
+ .welcome .cmd { color: var(--yellow); }
147
  </style>
148
  </head>
149
  <body>
150
 
151
  <header>
152
+ <img src="https://us.aws.cdn.hf.co/xet-bridge-us/6a37681bb57803e616606089/2430ce5755ccaa545def0292cb0f1068d4af99d5093c299b8167c9bfc1128dab?X-Xet-Cas-Uid=60f1abe7544c2adfd699860c&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27ornith_logo.png%3B+filename%3D%22ornith_logo.png%22%3B&response-content-type=image%2Fpng&user_id=60f1abe7544c2adfd699860c&Expires=1782415346&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly91cy5hd3MuY2RuLmhmLmNvL3hldC1icmlkZ2UtdXMvNmEzNzY4MWJiNTc4MDNlNjE2NjA2MDg5LzI0MzBjZTU3NTVjY2FhNTQ1ZGVmMDI5MmNiMGYxMDY4ZDRhZjk5ZDUwOTNjMjk5YjgxNjdjOWJmYzExMjhkYWJcXD9YLVhldC1DYXMtVWlkPTYwZjFhYmU3NTQ0YzJhZGZkNjk5ODYwYyZyZXNwb25zZS1jb250ZW50LWRpc3Bvc2l0aW9uPWlubGluZSUzQitmaWxlbmFtZSUyQSUzRFVURi04JTI3JTI3b3JuaXRoX2xvZ28ucG5nJTNCK2ZpbGVuYW1lJTNEJTIyb3JuaXRoX2xvZ28ucG5nJTIyJTNCJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyZ1c2VyX2lkPTYwZjFhYmU3NTQ0YzJhZGZkNjk5ODYwYyIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiRXBvY2hUaW1lIjoxNzgyNDE1MzQ2fX19XX0_&Signature=MEUCIQCJDHCuf325VtWk4Lr7BrnsQw9LL9RPIuQzcVq7xPY7vAIgXghesfcDb0xuVJm-bq8mkneN1tG5zTgpzIY6NdpDehw_&Key-Pair-Id=01KAYHXK2CBJSW0YZTMNXK9W1M" alt="Ornith" />
 
 
 
 
153
  <div class="spacer"></div>
154
+ <div class="badge" id="settings-toggle">⚙ settings</div>
155
  </header>
156
 
157
+ <div id="chat">
158
+ <div class="welcome">
159
+ <h2>Welcome to Ornith-1.0-9B</h2>
160
+ <p>Agentic coding assistant. Ask me to write, debug, or explain code. You can also attach an image for multimodal questions.</p>
161
+ </div>
162
+ </div>
163
+ <div class="status" id="status"></div>
164
 
165
  <div class="composer">
166
  <div class="composer-inner">
 
169
  <label>Max tokens <input type="range" id="maxtok" min="256" max="8192" step="256" value="2048" /><span id="maxtok-v">2048</span></label>
170
  </div>
171
  <div class="previews" id="previews"></div>
172
+ <div class="input-row">
173
  <button class="icon-btn" id="attach" title="Attach image">📎</button>
174
+ <textarea id="text" placeholder="Ask Ornith to write code, explain, or debug…"></textarea>
175
  <button id="send">Send</button>
176
  <button id="stop">Stop</button>
177
  </div>
178
+ <div class="hint">Shift+Enter for newline · Enter to send · Image-text-to-text supported</div>
179
  </div>
180
  </div>
181
 
 
201
  const maxEl = document.getElementById("maxtok");
202
  const maxV = document.getElementById("maxtok-v");
203
 
204
+ let conversation = [];
205
+ let pendingImage = null;
206
  let client = null;
207
  let job = null;
208
 
 
237
 
238
  textEl.addEventListener("input", () => {
239
  textEl.style.height = "auto";
240
+ textEl.style.height = Math.min(textEl.scrollHeight, 160) + "px";
241
  });
242
  textEl.addEventListener("keydown", (e) => {
243
  if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
 
250
  if (near) chatEl.scrollTop = chatEl.scrollHeight;
251
  }
252
 
253
+ function addTurn(role) {
254
+ const turn = document.createElement("div");
255
+ turn.className = "turn";
256
+ const line = document.createElement("div");
257
+ line.className = "prompt-line";
258
+ const prompt = document.createElement("div");
259
+ prompt.className = "prompt " + role;
260
+ const content = document.createElement("div");
261
+ content.className = "content";
262
+ line.appendChild(prompt); line.appendChild(content);
263
+ turn.appendChild(line);
264
+
265
+ // Remove welcome message on first real turn.
266
+ const welcome = chatEl.querySelector(".welcome");
267
+ if (welcome) welcome.remove();
268
+
269
+ chatEl.appendChild(turn);
270
  chatEl.scrollTop = chatEl.scrollHeight;
271
+ return content;
272
  }
273
 
274
  async function send() {
 
276
  if (!message && !pendingImage) return;
277
  if (job) return;
278
 
279
+ const userContent = addTurn("user");
280
+ let html = `<div class="user-input">${escapeHtml(message)}</div>`;
281
  if (pendingImage) {
282
  const url = URL.createObjectURL(pendingImage);
283
+ html += `<img class="user-image" src="${url}" alt="attachment"/>`;
284
  }
285
+ userContent.innerHTML = html;
286
 
287
  const sentImage = pendingImage;
288
  pendingImage = null; renderPreviews();
 
291
  const history = conversation.slice();
292
  conversation.push({ role: "user", content: message });
293
 
294
+ const assistantContent = addTurn("assistant");
295
+ assistantContent.innerHTML = `<span style="color:var(--muted)">…</span>`;
296
 
297
  setBusy(true);
298
+ statusEl.textContent = "connecting…";
299
 
300
  try {
301
  const c = await connect();
 
308
  };
309
  job = c.submit("/generate", payload);
310
 
311
+ let latestReasoning = "";
312
+ let latestAnswer = "";
313
+ let complete = false;
314
+
315
  for await (const msg of job) {
316
  if (msg.type === "status") {
317
  const s = msg.status || msg.data?.status;
318
+ if (s === "pending") statusEl.textContent = `queued…${msg.position != null ? " (#" + msg.position + ")" : ""}`;
319
+ else if (s === "generating") statusEl.textContent = "generating…";
320
  else if (s === "complete" || s === "error") statusEl.textContent = "";
321
  continue;
322
  }
 
324
  const raw = Array.isArray(msg.data) ? msg.data[0] : msg.data;
325
  let d = raw;
326
  if (typeof raw === "string") { try { d = JSON.parse(raw); } catch { d = { answer: raw }; } }
327
+ if (d?.error) {
328
+ renderAssistant(assistantContent, latestReasoning, latestAnswer, d.error);
329
+ statusEl.innerHTML = `<span class="err">error: ${escapeHtml(d.error)}</span>`;
330
  break;
331
  }
332
+ latestReasoning = d?.reasoning || "";
333
+ latestAnswer = d?.answer || "";
334
+ complete = d?.status === "complete";
335
+ renderAssistant(assistantContent, latestReasoning, latestAnswer);
336
  scrollIfNear();
337
  }
338
+
339
+ if (complete && latestAnswer) {
340
+ conversation.push({ role: "assistant", content: latestAnswer });
341
+ }
342
  } catch (e) {
343
+ renderAssistant(assistantContent, "", "", String(e));
344
  statusEl.innerHTML = `<span class="err">${escapeHtml(String(e))}</span>`;
345
  } finally {
346
  job = null;
 
349
  }
350
  }
351
 
352
+ function renderAssistant(el, reasoning, answer, error) {
353
  let h = "";
354
  if (reasoning && reasoning.trim()) {
355
+ h += `<details class="thinking"${answer ? "" : " open"}><summary>thinking</summary><pre>${escapeHtml(reasoning)}</pre></details>`;
 
356
  }
357
  if (error) {
358
  h += `<div class="err">⚠ ${escapeHtml(error)}</div>`;
 
361
  } else if (!reasoning) {
362
  h += `<span style="color:var(--muted)">…</span>`;
363
  }
364
+ el.innerHTML = h;
365
  }
366
 
367
  function setBusy(busy) {
 
376
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
377
  }
378
 
 
379
  textEl.focus();
380
  </script>
381
  </body>