tinywebui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +586 -0
  3. package/bin/tinywebui.js +105 -0
  4. package/package.json +49 -0
  5. package/public/app.js +81 -0
  6. package/public/chat/attachments.js +304 -0
  7. package/public/chat/chat.js +282 -0
  8. package/public/chat/composer.js +289 -0
  9. package/public/chat/library.js +76 -0
  10. package/public/chat/outline.js +39 -0
  11. package/public/chat/queue.js +124 -0
  12. package/public/chat/sidebar.js +485 -0
  13. package/public/chat/stream.js +162 -0
  14. package/public/chat/tasks.js +36 -0
  15. package/public/chat/transcript.js +691 -0
  16. package/public/core/access.js +42 -0
  17. package/public/core/api.js +27 -0
  18. package/public/core/dom.js +13 -0
  19. package/public/core/id.js +7 -0
  20. package/public/core/md.js +131 -0
  21. package/public/core/state.js +31 -0
  22. package/public/favicon.svg +9 -0
  23. package/public/index.html +261 -0
  24. package/public/panels/admin.js +199 -0
  25. package/public/panels/automations.js +464 -0
  26. package/public/panels/panels.js +52 -0
  27. package/public/panels/settings.js +365 -0
  28. package/public/panels/usage.js +474 -0
  29. package/public/shared/attachment_note.js +23 -0
  30. package/public/shared/events.js +27 -0
  31. package/public/shared/features.js +21 -0
  32. package/public/shared/origins.js +8 -0
  33. package/public/shared/usage.js +59 -0
  34. package/public/styles.css +1455 -0
  35. package/public/themes/README.md +178 -0
  36. package/public/themes/cyber-grid/cyber-grid.css +59 -0
  37. package/public/themes/drivers/scene-kit.js +422 -0
  38. package/public/themes/fall-fairy/fall-fairy.css +221 -0
  39. package/public/themes/fall-fairy/fall-fairy.js +859 -0
  40. package/public/themes/manifest.json +6 -0
  41. package/public/themes/theme-loader.js +123 -0
  42. package/src/access/auth.js +228 -0
  43. package/src/access/auth_gate.js +67 -0
  44. package/src/access/policy.js +180 -0
  45. package/src/audit.js +7 -0
  46. package/src/automations/automation.js +136 -0
  47. package/src/automations/scheduler.js +147 -0
  48. package/src/chat/compact.js +242 -0
  49. package/src/chat/llm.js +448 -0
  50. package/src/chat/runs.js +180 -0
  51. package/src/chat/tool_executor.js +239 -0
  52. package/src/cli.js +478 -0
  53. package/src/config/approval.js +54 -0
  54. package/src/config/config.js +479 -0
  55. package/src/config/models.js +112 -0
  56. package/src/files/documents.js +91 -0
  57. package/src/files/images.js +62 -0
  58. package/src/harness/agent.js +115 -0
  59. package/src/harness/attribution.js +75 -0
  60. package/src/harness/checkpoint.js +96 -0
  61. package/src/harness/history.js +40 -0
  62. package/src/harness/prompt.js +93 -0
  63. package/src/harness/provider.js +430 -0
  64. package/src/http.js +95 -0
  65. package/src/log.js +24 -0
  66. package/src/mcp.js +405 -0
  67. package/src/retrieval/embedding.js +298 -0
  68. package/src/retrieval/retrieval.js +250 -0
  69. package/src/routes/admin.js +112 -0
  70. package/src/routes/auth.js +97 -0
  71. package/src/routes/automations.js +92 -0
  72. package/src/routes/chats.js +232 -0
  73. package/src/routes/documents.js +133 -0
  74. package/src/routes/library.js +51 -0
  75. package/src/routes/mcp.js +59 -0
  76. package/src/routes/settings.js +101 -0
  77. package/src/server.js +397 -0
  78. package/src/set-password.js +32 -0
  79. package/src/store/automations.js +119 -0
  80. package/src/store/chats.js +254 -0
  81. package/src/store/documents.js +256 -0
  82. package/src/store/embeddings.js +67 -0
  83. package/src/store/index.js +90 -0
  84. package/src/store/messages.js +93 -0
  85. package/src/store/schema.js +480 -0
  86. package/src/store/scope.js +43 -0
  87. package/src/store/search.js +56 -0
  88. package/src/store/turns.js +78 -0
  89. package/src/store/usage.js +327 -0
  90. package/src/store/users.js +102 -0
  91. package/src/store/wire.js +55 -0
  92. package/src/tools/ask_tool.js +93 -0
  93. package/src/tools/chat_search_tool.js +63 -0
  94. package/src/tools/context_tool.js +183 -0
  95. package/src/tools/document_tool.js +102 -0
  96. package/src/tools/task_tool.js +63 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * What the signed-in person may use, from /api/auth/me. The server enforces
3
+ * every feature on its own routes; this only keeps the UI from offering
4
+ * what would be refused. Hidden, not greyed out: a chat-only user should see
5
+ * a chat app, not a settings app with the settings locked.
6
+ */
7
+ import { $ } from './dom.js';
8
+ import { FEATURE } from '../shared/features.js';
9
+ import { api } from './api.js';
10
+
11
+ let me = { features: [], models: '*' };
12
+ let features = new Set();
13
+
14
+ export const can = (feature) => features.has(feature);
15
+ export const whoami = () => me;
16
+
17
+ // Nav items and controls that exist only for one feature.
18
+ const GATED = {
19
+ [FEATURE.SETTINGS]: ['toggle-settings'],
20
+ [FEATURE.MCP]: ['toggle-mcp'],
21
+ [FEATURE.AUTOMATIONS]: ['toggle-automations'],
22
+ [FEATURE.STATISTICS]: ['toggle-statistics'],
23
+ [FEATURE.ADMIN]: ['toggle-admin'],
24
+ [FEATURE.FOLDERS]: ['newFolder']
25
+ };
26
+
27
+ export async function loadAccess() {
28
+ try { me = await api.get('/api/auth/me'); } catch { /* offline: show nothing extra */ }
29
+ features = new Set(me.features || []);
30
+ for (const [feature, ids] of Object.entries(GATED)) {
31
+ for (const id of ids) { const node = $(id); if (node) node.hidden = !can(feature); }
32
+ }
33
+ const search = $('chatSearch')?.closest('.search-row');
34
+ if (search) search.hidden = !can(FEATURE.SEARCH);
35
+ // The + button opens attach and tools; with neither there is nothing in it.
36
+ $('attach').hidden = !(can(FEATURE.ATTACHMENTS) || can(FEATURE.IMAGES) || can(FEATURE.TOOLS));
37
+ // The model pill still says which model answers; it just stops being a menu.
38
+ const pill = $('modelBtn');
39
+ pill.disabled = !can(FEATURE.MODEL_PICKER);
40
+ pill.classList.toggle('static', !can(FEATURE.MODEL_PICKER));
41
+ return me;
42
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The page's one way to call the JSON API. A non-2xx reply throws an Error
3
+ * carrying the server's `error` text (or the bare status), plus `.status` for
4
+ * the few callers that treat a particular code as fine.
5
+ *
6
+ * Streaming endpoints (/api/chat, /api/chats/:id/stream) are read as streams
7
+ * and still use fetch directly.
8
+ */
9
+
10
+ async function request(method, url, body, { signal } = {}) {
11
+ const init = { method, signal };
12
+ if (body !== undefined) {
13
+ init.headers = { 'content-type': 'application/json' };
14
+ init.body = JSON.stringify(body);
15
+ }
16
+ const res = await fetch(url, init);
17
+ const out = await res.json().catch(() => ({}));
18
+ if (!res.ok) throw Object.assign(new Error(out.error || String(res.status)), { status: res.status });
19
+ return out;
20
+ }
21
+
22
+ export const api = {
23
+ get: (url, opts) => request('GET', url, undefined, opts),
24
+ post: (url, body, opts) => request('POST', url, body, opts),
25
+ patch: (url, body, opts) => request('PATCH', url, body, opts),
26
+ del: (url, opts) => request('DELETE', url, undefined, opts)
27
+ };
@@ -0,0 +1,13 @@
1
+ /** Tiny DOM helpers shared by every module. */
2
+
3
+ export const $ = (id) => document.getElementById(id);
4
+ export const el = (tag, cls) => { const n = document.createElement(tag); if (cls) n.className = cls; return n; };
5
+ export const num = (v) => (v.trim() === '' ? null : Number(v));
6
+
7
+ /** "Install uv" for a server whose launcher is missing (`install` from /api/tools). */
8
+ export const installLink = ({ name, url }) => Object.assign(el('a', 'install-link'), {
9
+ textContent: `Install ${name}`,
10
+ href: /^https:\/\//.test(url) ? url : '#',
11
+ target: '_blank',
12
+ rel: 'noopener noreferrer'
13
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A random id for things the page names before the server has seen them (a
3
+ * new chat, a queued message). randomUUID needs a secure context (https or
4
+ * localhost); getRandomValues works on plain-http LAN installs too.
5
+ */
6
+ export const newId = () => crypto.randomUUID?.()
7
+ ?? [...crypto.getRandomValues(new Uint8Array(16))].map((b) => b.toString(16).padStart(2, '0')).join('');
@@ -0,0 +1,131 @@
1
+ // The page's only producers of HTML from outside text. A small, dependency-free
2
+ // Markdown renderer that covers what chat models actually emit -- headings,
3
+ // emphasis, code, lists, tables, quotes, links -- and nothing else, plus the
4
+ // search-snippet highlighter. Everything is HTML-escaped before any tag is
5
+ // produced, so model output can never inject markup.
6
+
7
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => (
8
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
9
+ ));
10
+
11
+ // A private-use character: stands in for a stashed span while the inline rules run.
12
+ const MARK = '\uE000';
13
+
14
+ function inline(src) {
15
+ // A MARK typed into the text would otherwise read as a reference to a stash slot.
16
+ let out = esc(src).replaceAll(MARK, '');
17
+ const spans = [];
18
+ const stash = (html) => `${MARK}${spans.push(html) - 1}${MARK}`;
19
+
20
+ // Code spans first: nothing inside them should be interpreted further.
21
+ out = out.replace(/`([^`\n]+)`/g, (_, code) => stash(`<code>${code}</code>`));
22
+
23
+ // Only http(s)/mailto/anchor/same-site links become anchors; anything else
24
+ // (javascript:, data:, a protocol-relative //host) degrades to its link text.
25
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, (_, text, href) =>
26
+ /^(https?:|mailto:|#|\/(?!\/))/i.test(href)
27
+ ? stash(`<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`)
28
+ : text);
29
+ out = out.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, (_, pre, href) =>
30
+ pre + stash(`<a href="${href}" target="_blank" rel="noopener noreferrer">${href}</a>`));
31
+
32
+ out = out
33
+ .replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>')
34
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
35
+ .replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, '$1<em>$2</em>')
36
+ .replace(/(^|\W)_([^_\n]+)_(?=\W|$)/g, '$1<em>$2</em>')
37
+ .replace(/~~([^~]+)~~/g, '<del>$1</del>');
38
+
39
+ return out.replace(new RegExp(`${MARK}(\\d+)${MARK}`, 'g'), (_, i) => spans[i]);
40
+ }
41
+
42
+ const cells = (line) => line.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
43
+
44
+ export function renderMarkdown(src) {
45
+ const lines = String(src).replace(/\r\n/g, '\n').split('\n');
46
+ const html = [];
47
+ let i = 0;
48
+
49
+ const listBlock = (ordered) => {
50
+ const items = [];
51
+ const marker = ordered ? /^\s*\d+[.)]\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/;
52
+ while (i < lines.length) {
53
+ const m = lines[i].match(marker);
54
+ if (m) { items.push(m[1]); i++; continue; }
55
+ // A wrapped continuation line belongs to the item above it.
56
+ if (items.length && /^\s+\S/.test(lines[i])) {
57
+ items[items.length - 1] += ' ' + lines[i].trim();
58
+ i++;
59
+ continue;
60
+ }
61
+ break;
62
+ }
63
+ const tag = ordered ? 'ol' : 'ul';
64
+ html.push(`<${tag}>${items.map((t) => `<li>${inline(t)}</li>`).join('')}</${tag}>`);
65
+ };
66
+
67
+ while (i < lines.length) {
68
+ const line = lines[i];
69
+
70
+ if (/^\s*```/.test(line)) {
71
+ const lang = line.replace(/^\s*```/, '').trim();
72
+ const body = [];
73
+ i++;
74
+ while (i < lines.length && !/^\s*```/.test(lines[i])) body.push(lines[i++]);
75
+ i++; // closing fence, or the end of a stream still being written
76
+ html.push(`<pre data-lang="${esc(lang)}"><code>${esc(body.join('\n'))}</code></pre>`);
77
+ continue;
78
+ }
79
+
80
+ if (!line.trim()) { i++; continue; }
81
+
82
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
83
+ if (heading) {
84
+ html.push(`<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>`);
85
+ i++;
86
+ continue;
87
+ }
88
+
89
+ if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(line)) { html.push('<hr>'); i++; continue; }
90
+
91
+ if (/^\s*>/.test(line)) {
92
+ const body = [];
93
+ while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ''));
94
+ html.push(`<blockquote>${renderMarkdown(body.join('\n'))}</blockquote>`);
95
+ continue;
96
+ }
97
+
98
+ if (/^\s*[-*+]\s+/.test(line)) { listBlock(false); continue; }
99
+ if (/^\s*\d+[.)]\s+/.test(line)) { listBlock(true); continue; }
100
+
101
+ if (line.includes('|') && /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1] || '')) {
102
+ const head = cells(line);
103
+ i += 2;
104
+ const rows = [];
105
+ while (i < lines.length && lines[i].includes('|')) rows.push(cells(lines[i++]));
106
+ html.push(
107
+ `<table><thead><tr>${head.map((c) => `<th>${inline(c)}</th>`).join('')}</tr></thead>` +
108
+ `<tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</tbody></table>`
109
+ );
110
+ continue;
111
+ }
112
+
113
+ const para = [];
114
+ while (i < lines.length && lines[i].trim() && !/^\s*(#{1,6}\s|>|```|[-*+]\s|\d+[.)]\s)/.test(lines[i])) {
115
+ para.push(lines[i++]);
116
+ }
117
+ html.push(`<p>${inline(para.join('\n')).replace(/\n/g, '<br>')}</p>`);
118
+ }
119
+
120
+ return html.join('\n');
121
+ }
122
+
123
+ /** Turns the search index's ‹...› markers into <mark>, escaping everything else. */
124
+ export function markSnippet(raw) {
125
+ return String(raw).split('‹').map((chunk, i) => {
126
+ if (i === 0) return esc(chunk);
127
+ const at = chunk.indexOf('›');
128
+ if (at === -1) return esc('‹' + chunk); // an unmatched marker -- show it plainly rather than eat it
129
+ return `<mark>${esc(chunk.slice(0, at))}</mark>${esc(chunk.slice(at + 1))}`;
130
+ }).join('');
131
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * State shared across the split-up UI modules.
3
+ *
4
+ * A single mutable object rather than several exported `let`s: every module
5
+ * that needs to read or write `state.chat` (or `.busy`, `.viewCtrl`, ...) just
6
+ * imports this one object, and since its identity never changes, plain
7
+ * property assignment from any module is visible to all the others -- no
8
+ * setter functions or live-binding subtleties to keep track of.
9
+ */
10
+ export const state = {
11
+ chat: null, // { id, title } -- the conversation currently on screen
12
+ busy: false, // a turn is streaming into the view right now
13
+ // The fetch reading a run's stream belongs to whichever view started it.
14
+ // Switching chats aborts it -- see leaveView() in chat.js.
15
+ viewCtrl: null,
16
+ chats: [], // the sidebar's chronological chat list
17
+ queue: [], // input queued on the running turn: [{id, kind, content}]
18
+ folders: [], // every known folder name, including empty ones
19
+
20
+ // Uploaded before the message that references them is sent, so a slow
21
+ // upload doesn't block typing. Cleared once that turn is sent.
22
+ pendingAttachments: [], // [{id, filename, char_len, pending}]
23
+
24
+ // Every document ever attached to the chat currently open, shown in the
25
+ // artifacts strip above the transcript. Not the same list as
26
+ // pendingAttachments, which is only what hasn't been sent yet.
27
+ chatDocuments: [], // [{id, filename, char_len}]
28
+ // Every image ever attached to the chat currently open, shown in the same
29
+ // strip. Derived from message history rather than stored separately.
30
+ chatImages: [] // [{filename, mime, data}]
31
+ };
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg"
3
+ viewBox="0 0 300 300"
4
+ role="img"
5
+ aria-labelledby="title description">
6
+ <title id="title">TinyWebUI mark</title>
7
+ <desc id="description">TinyWebUI bunny mark with centered chat bubble.</desc>
8
+ <path d="M 119.38 40.83 L 119.10 39.79 L 118.77 38.76 L 118.39 37.75 L 117.96 36.76 L 117.49 35.79 L 116.96 34.84 L 116.39 33.93 L 115.78 33.04 L 115.12 32.18 L 114.42 31.36 L 113.68 30.57 L 112.91 29.82 L 112.10 29.11 L 111.25 28.44 L 110.37 27.81 L 109.46 27.22 L 108.53 26.68 L 107.57 26.19 L 106.58 25.74 L 105.58 25.35 L 104.56 25.00 L 103.52 24.70 L 102.47 24.46 L 101.41 24.27 L 100.34 24.12 L 99.26 24.04 L 98.18 24.00 L 97.10 24.02 L 96.02 24.09 L 94.95 24.21 L 93.88 24.39 L 92.83 24.62 L 91.79 24.90 L 90.76 25.23 L 89.75 25.61 L 88.76 26.04 L 87.79 26.51 L 86.84 27.04 L 85.93 27.61 L 85.04 28.22 L 84.18 28.88 L 83.36 29.58 L 82.57 30.32 L 81.82 31.09 L 81.11 31.90 L 80.44 32.75 L 79.81 33.63 L 79.22 34.54 L 78.68 35.47 L 78.19 36.43 L 77.74 37.42 L 77.35 38.42 L 77.00 39.44 L 76.70 40.48 L 76.46 41.53 L 76.27 42.59 L 76.12 43.66 L 76.04 44.74 L 76.00 45.82 L 76.02 46.90 L 76.09 47.98 L 76.21 49.05 L 85.40 114.68 L 85.24 114.74 L 82.03 116.12 L 78.92 117.65 L 75.93 119.36 L 73.11 121.26 L 70.49 123.36 L 68.09 125.66 L 65.94 128.13 L 64.04 130.75 L 62.37 133.50 L 60.92 136.34 L 59.66 139.24 L 58.54 142.17 L 57.52 145.10 L 56.55 148.00 L 55.59 150.87 L 54.60 153.70 L 53.55 156.49 L 52.41 159.25 L 51.19 161.98 L 49.89 164.72 L 48.53 167.47 L 47.15 170.24 L 45.80 173.07 L 44.52 175.95 L 43.38 178.89 L 42.42 181.88 L 41.70 184.92 L 41.26 188.00 L 41.12 191.09 L 41.29 194.18 L 41.78 197.25 L 42.57 200.28 L 43.63 203.24 L 44.92 206.14 L 46.39 208.97 L 47.99 211.72 L 49.65 214.42 L 51.34 217.07 L 53.00 219.69 L 54.60 222.30 L 56.13 224.92 L 57.57 227.56 L 58.93 230.25 L 60.24 232.98 L 61.52 235.75 L 62.81 238.56 L 64.15 241.39 L 65.60 244.21 L 67.18 246.99 L 68.95 249.69 L 70.92 252.29 L 73.11 254.74 L 75.53 257.01 L 78.17 259.09 L 81.01 260.96 L 84.04 262.61 L 87.20 264.06 L 90.48 265.31 L 93.82 266.41 L 97.19 267.38 L 100.57 268.27 L 103.92 269.11 L 107.22 269.94 L 110.48 270.80 L 113.70 271.71 L 116.87 272.70 L 120.03 273.76 L 123.17 274.89 L 126.34 276.07 L 129.54 277.27 L 132.80 278.44 L 136.11 279.55 L 139.50 280.54 L 142.95 281.37 L 146.45 281.99 L 150.00 282.38 L 153.56 282.50 L 157.13 282.35 L 160.66 281.93 L 164.14 281.24 L 167.56 280.32 L 170.90 279.20 L 174.16 277.92 L 177.33 276.54 L 180.44 275.10 L 183.49 273.63 L 186.51 272.19 L 189.52 270.80 L 192.53 269.47 L 195.58 268.22 L 198.68 267.04 L 201.82 265.90 L 205.02 264.79 L 208.26 263.68 L 211.52 262.51 L 214.76 261.26 L 217.97 259.88 L 221.08 258.35 L 224.07 256.64 L 226.89 254.74 L 229.51 252.64 L 231.91 250.34 L 234.06 247.87 L 235.96 245.25 L 237.63 242.50 L 239.08 239.66 L 240.34 236.76 L 241.46 233.83 L 242.48 230.90 L 243.45 228.00 L 244.41 225.13 L 245.40 222.30 L 246.45 219.51 L 247.59 216.75 L 248.81 214.02 L 250.11 211.28 L 251.47 208.53 L 252.85 205.76 L 254.20 202.93 L 255.48 200.05 L 256.62 197.11 L 257.58 194.12 L 258.30 191.08 L 258.74 188.00 L 258.88 184.91 L 258.71 181.82 L 258.22 178.75 L 257.43 175.72 L 256.37 172.76 L 255.08 169.86 L 253.61 167.03 L 252.01 164.28 L 250.35 161.58 L 248.66 158.93 L 247.00 156.31 L 245.40 153.70 L 243.87 151.08 L 242.43 148.44 L 241.07 145.75 L 239.76 143.02 L 238.48 140.25 L 237.19 137.44 L 235.85 134.61 L 234.40 131.79 L 232.82 129.01 L 231.05 126.31 L 229.08 123.71 L 226.89 121.26 L 224.47 118.99 L 221.83 116.91 L 218.99 115.04 L 215.96 113.39 L 214.85 112.88 L 223.79 49.05 L 223.91 47.98 L 223.98 46.90 L 224.00 45.82 L 223.96 44.74 L 223.88 43.66 L 223.73 42.59 L 223.54 41.53 L 223.30 40.48 L 223.00 39.44 L 222.65 38.42 L 222.26 37.42 L 221.81 36.43 L 221.32 35.47 L 220.78 34.54 L 220.19 33.63 L 219.56 32.75 L 218.89 31.90 L 218.18 31.09 L 217.43 30.32 L 216.64 29.58 L 215.82 28.88 L 214.96 28.22 L 214.07 27.61 L 213.16 27.04 L 212.21 26.51 L 211.24 26.04 L 210.25 25.61 L 209.24 25.23 L 208.21 24.90 L 207.17 24.62 L 206.12 24.39 L 205.05 24.21 L 203.98 24.09 L 202.90 24.02 L 201.82 24.00 L 200.74 24.04 L 199.66 24.12 L 198.59 24.27 L 197.53 24.46 L 196.48 24.70 L 195.44 25.00 L 194.42 25.35 L 193.42 25.74 L 192.43 26.19 L 191.47 26.68 L 190.54 27.22 L 189.63 27.81 L 188.75 28.44 L 187.90 29.11 L 187.09 29.82 L 186.32 30.57 L 185.58 31.36 L 184.88 32.18 L 184.22 33.04 L 183.61 33.93 L 183.04 34.84 L 182.51 35.79 L 182.04 36.76 L 181.61 37.75 L 181.23 38.76 L 180.90 39.79 L 180.62 40.83 L 180.39 41.88 L 180.21 42.95 L 172.31 99.42 L 170.46 98.73 L 167.20 97.56 L 163.89 96.45 L 160.50 95.46 L 157.05 94.63 L 153.55 94.01 L 150.00 93.62 L 146.44 93.50 L 142.87 93.65 L 139.34 94.07 L 135.86 94.76 L 132.44 95.68 L 129.10 96.80 L 127.42 97.46 L 119.79 42.95 L 119.61 41.88 L 119.38 40.83 Z M 106.60 241.86 L 106.62 241.62 L 106.66 241.38 L 109.05 228.23 L 109.10 227.92 L 109.12 227.68 L 109.14 227.43 L 109.14 227.19 L 109.13 226.94 L 109.11 226.69 L 109.07 226.45 L 109.02 226.21 L 108.96 225.97 L 108.89 225.73 L 108.81 225.50 L 108.72 225.27 L 108.61 225.04 L 108.50 224.83 L 108.37 224.61 L 108.24 224.41 L 108.09 224.21 L 107.93 224.02 L 107.77 223.83 L 107.59 223.66 L 107.41 223.49 L 107.22 223.34 L 107.02 223.19 L 106.71 222.98 L 106.71 222.98 L 106.57 222.88 L 105.80 222.35 L 105.61 222.20 L 104.87 221.63 L 104.68 221.47 L 103.98 220.86 L 103.79 220.70 L 103.12 220.06 L 102.94 219.88 L 102.30 219.21 L 102.14 219.02 L 101.53 218.32 L 101.37 218.13 L 100.80 217.39 L 100.65 217.20 L 100.12 216.43 L 99.98 216.23 L 99.48 215.44 L 99.35 215.23 L 98.89 214.42 L 98.78 214.20 L 98.36 213.37 L 98.25 213.15 L 97.88 212.30 L 97.78 212.07 L 97.45 211.20 L 97.36 210.97 L 97.07 210.08 L 97.00 209.85 L 96.75 208.95 L 96.69 208.71 L 96.49 207.80 L 96.44 207.56 L 96.28 206.64 L 96.24 206.40 L 96.13 205.47 L 96.11 205.23 L 96.04 204.30 L 96.03 204.05 L 96.00 203.06 L 96.00 202.94 L 96.00 167.06 L 96.00 166.94 L 96.03 165.95 L 96.04 165.70 L 96.11 164.77 L 96.13 164.53 L 96.24 163.60 L 96.28 163.36 L 96.44 162.44 L 96.49 162.20 L 96.69 161.29 L 96.75 161.05 L 97.00 160.15 L 97.07 159.92 L 97.36 159.03 L 97.45 158.80 L 97.78 157.93 L 97.88 157.70 L 98.25 156.85 L 98.36 156.63 L 98.78 155.80 L 98.89 155.58 L 99.35 154.77 L 99.48 154.56 L 99.98 153.77 L 100.12 153.57 L 100.65 152.80 L 100.80 152.61 L 101.37 151.87 L 101.53 151.68 L 102.14 150.98 L 102.30 150.79 L 102.94 150.12 L 103.12 149.94 L 103.79 149.30 L 103.98 149.14 L 104.68 148.53 L 104.87 148.37 L 105.61 147.80 L 105.80 147.65 L 106.57 147.12 L 106.77 146.98 L 107.56 146.48 L 107.77 146.35 L 108.58 145.89 L 108.80 145.78 L 109.63 145.36 L 109.85 145.25 L 110.70 144.88 L 110.93 144.78 L 111.80 144.45 L 112.03 144.36 L 112.92 144.07 L 113.15 144.00 L 114.05 143.75 L 114.29 143.69 L 115.20 143.49 L 115.44 143.44 L 116.36 143.28 L 116.60 143.24 L 117.53 143.13 L 117.77 143.11 L 118.70 143.04 L 118.95 143.03 L 119.94 143.00 L 120.06 143.00 L 179.94 143.00 L 180.06 143.00 L 181.05 143.03 L 181.30 143.04 L 182.23 143.11 L 182.47 143.13 L 183.40 143.24 L 183.64 143.28 L 184.56 143.44 L 184.80 143.49 L 185.71 143.69 L 185.95 143.75 L 186.85 144.00 L 187.08 144.07 L 187.97 144.36 L 188.20 144.45 L 189.07 144.78 L 189.30 144.88 L 190.15 145.25 L 190.37 145.36 L 191.20 145.78 L 191.42 145.89 L 192.23 146.35 L 192.44 146.48 L 193.23 146.98 L 193.43 147.12 L 194.20 147.65 L 194.39 147.80 L 195.13 148.37 L 195.32 148.53 L 196.02 149.14 L 196.21 149.30 L 196.88 149.94 L 197.06 150.12 L 197.70 150.79 L 197.86 150.98 L 198.47 151.68 L 198.63 151.87 L 199.20 152.61 L 199.35 152.80 L 199.88 153.57 L 200.02 153.77 L 200.52 154.56 L 200.65 154.77 L 201.11 155.58 L 201.22 155.80 L 201.64 156.63 L 201.75 156.85 L 202.12 157.70 L 202.22 157.93 L 202.55 158.80 L 202.64 159.03 L 202.93 159.92 L 203.00 160.15 L 203.25 161.05 L 203.31 161.29 L 203.51 162.20 L 203.56 162.44 L 203.72 163.36 L 203.76 163.60 L 203.87 164.53 L 203.89 164.77 L 203.96 165.70 L 203.97 165.95 L 204.00 166.94 L 204.00 167.06 L 204.00 202.94 L 204.00 203.06 L 203.97 204.05 L 203.96 204.30 L 203.89 205.23 L 203.87 205.47 L 203.76 206.40 L 203.72 206.64 L 203.56 207.56 L 203.51 207.80 L 203.31 208.71 L 203.25 208.95 L 203.00 209.85 L 202.93 210.08 L 202.64 210.97 L 202.55 211.20 L 202.22 212.07 L 202.12 212.30 L 201.75 213.15 L 201.64 213.37 L 201.22 214.20 L 201.11 214.42 L 200.65 215.23 L 200.52 215.44 L 200.02 216.23 L 199.88 216.43 L 199.35 217.20 L 199.20 217.39 L 198.63 218.13 L 198.47 218.32 L 197.86 219.02 L 197.70 219.21 L 197.06 219.88 L 196.88 220.06 L 196.21 220.70 L 196.02 220.86 L 195.32 221.47 L 195.13 221.63 L 194.39 222.20 L 194.20 222.35 L 193.43 222.88 L 193.23 223.02 L 192.44 223.52 L 192.23 223.65 L 191.42 224.11 L 191.20 224.22 L 190.37 224.64 L 190.15 224.75 L 189.30 225.12 L 189.07 225.22 L 188.20 225.55 L 187.97 225.64 L 187.08 225.93 L 186.85 226.00 L 185.95 226.25 L 185.71 226.31 L 184.80 226.51 L 184.56 226.56 L 183.64 226.72 L 183.40 226.76 L 182.47 226.87 L 182.23 226.89 L 181.30 226.96 L 181.05 226.97 L 180.06 227.00 L 179.94 227.00 L 137.64 227.00 L 137.33 227.01 L 137.09 227.03 L 136.84 227.06 L 136.60 227.10 L 136.36 227.15 L 136.12 227.22 L 135.88 227.30 L 135.65 227.39 L 135.43 227.49 L 135.21 227.60 L 134.99 227.72 L 134.79 227.86 L 134.58 228.00 L 134.39 228.15 L 134.16 228.36 L 114.96 245.96 L 114.77 246.12 L 114.58 246.27 L 114.38 246.41 L 114.18 246.54 L 113.96 246.66 L 113.75 246.78 L 113.52 246.88 L 113.30 246.97 L 113.07 247.04 L 112.83 247.11 L 112.59 247.17 L 112.35 247.21 L 112.11 247.24 L 111.87 247.26 L 111.62 247.27 L 111.38 247.27 L 111.13 247.25 L 110.89 247.22 L 110.65 247.18 L 110.41 247.13 L 110.18 247.07 L 109.94 247.00 L 109.71 246.91 L 109.49 246.81 L 109.27 246.71 L 109.06 246.59 L 108.85 246.46 L 108.65 246.32 L 108.45 246.17 L 108.27 246.02 L 108.09 245.85 L 107.92 245.67 L 107.75 245.49 L 107.60 245.30 L 107.46 245.10 L 107.32 244.90 L 107.20 244.69 L 107.09 244.47 L 106.99 244.25 L 106.89 244.02 L 106.81 243.79 L 106.75 243.56 L 106.69 243.32 L 106.64 243.08 L 106.61 242.84 L 106.59 242.59 L 106.58 242.35 L 106.58 242.10 L 106.60 241.86 Z M 123.00 158.00 L 122.41 158.01 L 121.82 158.06 L 121.24 158.13 L 120.66 158.23 L 120.08 158.36 L 119.52 158.52 L 118.96 158.70 L 118.41 158.91 L 117.87 159.15 L 117.34 159.42 L 116.83 159.71 L 116.33 160.02 L 115.85 160.36 L 115.39 160.72 L 114.94 161.11 L 114.51 161.51 L 114.11 161.94 L 113.72 162.39 L 113.36 162.85 L 113.02 163.33 L 112.71 163.83 L 112.42 164.34 L 112.15 164.87 L 111.91 165.41 L 111.70 165.96 L 111.52 166.52 L 111.36 167.08 L 111.23 167.66 L 111.13 168.24 L 111.06 168.82 L 111.01 169.41 L 111.00 170.00 L 111.00 200.00 L 111.01 200.59 L 111.06 201.18 L 111.13 201.76 L 111.23 202.34 L 111.36 202.92 L 111.52 203.48 L 111.70 204.04 L 111.91 204.59 L 112.15 205.13 L 112.42 205.66 L 112.71 206.17 L 113.02 206.67 L 113.36 207.15 L 113.72 207.61 L 114.11 208.06 L 114.51 208.49 L 114.94 208.89 L 115.39 209.28 L 115.85 209.64 L 116.33 209.98 L 116.83 210.29 L 117.34 210.58 L 117.87 210.85 L 118.41 211.09 L 118.96 211.30 L 119.52 211.48 L 120.08 211.64 L 120.66 211.77 L 121.24 211.87 L 121.82 211.94 L 122.41 211.99 L 123.00 212.00 L 177.00 212.00 L 177.59 211.99 L 178.18 211.94 L 178.76 211.87 L 179.34 211.77 L 179.92 211.64 L 180.48 211.48 L 181.04 211.30 L 181.59 211.09 L 182.13 210.85 L 182.66 210.58 L 183.17 210.29 L 183.67 209.98 L 184.15 209.64 L 184.61 209.28 L 185.06 208.89 L 185.49 208.49 L 185.89 208.06 L 186.28 207.61 L 186.64 207.15 L 186.98 206.67 L 187.29 206.17 L 187.58 205.66 L 187.85 205.13 L 188.09 204.59 L 188.30 204.04 L 188.48 203.48 L 188.64 202.92 L 188.77 202.34 L 188.87 201.76 L 188.94 201.18 L 188.99 200.59 L 189.00 200.00 L 189.00 170.00 L 188.99 169.41 L 188.94 168.82 L 188.87 168.24 L 188.77 167.66 L 188.64 167.08 L 188.48 166.52 L 188.30 165.96 L 188.09 165.41 L 187.85 164.87 L 187.58 164.34 L 187.29 163.83 L 186.98 163.33 L 186.64 162.85 L 186.28 162.39 L 185.89 161.94 L 185.49 161.51 L 185.06 161.11 L 184.61 160.72 L 184.15 160.36 L 183.67 160.02 L 183.17 159.71 L 182.66 159.42 L 182.13 159.15 L 181.59 158.91 L 181.04 158.70 L 180.48 158.52 L 179.92 158.36 L 179.34 158.23 L 178.76 158.13 L 178.18 158.06 L 177.59 158.01 L 177.00 158.00 L 123.00 158.00 Z M 134.99 184.61 L 135.00 185.00 L 134.99 185.39 L 134.96 185.78 L 134.91 186.17 L 134.85 186.56 L 134.76 186.94 L 134.66 187.32 L 134.53 187.70 L 134.39 188.06 L 134.23 188.42 L 134.06 188.77 L 133.86 189.11 L 133.65 189.44 L 133.43 189.77 L 133.18 190.08 L 132.93 190.37 L 132.66 190.66 L 132.37 190.93 L 132.08 191.18 L 131.77 191.43 L 131.44 191.65 L 131.11 191.86 L 130.77 192.06 L 130.42 192.23 L 130.06 192.39 L 129.70 192.53 L 129.32 192.66 L 128.94 192.76 L 128.56 192.85 L 128.17 192.91 L 127.78 192.96 L 127.39 192.99 L 127.00 193.00 L 126.61 192.99 L 126.22 192.96 L 125.83 192.91 L 125.44 192.85 L 125.06 192.76 L 124.68 192.66 L 124.30 192.53 L 123.94 192.39 L 123.58 192.23 L 123.23 192.06 L 122.89 191.86 L 122.56 191.65 L 122.23 191.43 L 121.92 191.18 L 121.63 190.93 L 121.34 190.66 L 121.07 190.37 L 120.82 190.08 L 120.57 189.77 L 120.35 189.44 L 120.14 189.11 L 119.94 188.77 L 119.77 188.42 L 119.61 188.06 L 119.47 187.70 L 119.34 187.32 L 119.24 186.94 L 119.15 186.56 L 119.09 186.17 L 119.04 185.78 L 119.01 185.39 L 119.00 185.00 L 119.01 184.61 L 119.04 184.22 L 119.09 183.83 L 119.15 183.44 L 119.24 183.06 L 119.34 182.68 L 119.47 182.30 L 119.61 181.94 L 119.77 181.58 L 119.94 181.23 L 120.14 180.89 L 120.35 180.56 L 120.57 180.23 L 120.82 179.92 L 121.07 179.63 L 121.34 179.34 L 121.63 179.07 L 121.92 178.82 L 122.23 178.57 L 122.56 178.35 L 122.89 178.14 L 123.23 177.94 L 123.58 177.77 L 123.94 177.61 L 124.30 177.47 L 124.68 177.34 L 125.06 177.24 L 125.44 177.15 L 125.83 177.09 L 126.22 177.04 L 126.61 177.01 L 127.00 177.00 L 127.39 177.01 L 127.78 177.04 L 128.17 177.09 L 128.56 177.15 L 128.94 177.24 L 129.32 177.34 L 129.70 177.47 L 130.06 177.61 L 130.42 177.77 L 130.77 177.94 L 131.11 178.14 L 131.44 178.35 L 131.77 178.57 L 132.08 178.82 L 132.37 179.07 L 132.66 179.34 L 132.93 179.63 L 133.18 179.92 L 133.43 180.23 L 133.65 180.56 L 133.86 180.89 L 134.06 181.23 L 134.23 181.58 L 134.39 181.94 L 134.53 182.30 L 134.66 182.68 L 134.76 183.06 L 134.85 183.44 L 134.91 183.83 L 134.96 184.22 L 134.99 184.61 Z M 157.99 184.61 L 158.00 185.00 L 157.99 185.39 L 157.96 185.78 L 157.91 186.17 L 157.85 186.56 L 157.76 186.94 L 157.66 187.32 L 157.53 187.70 L 157.39 188.06 L 157.23 188.42 L 157.06 188.77 L 156.86 189.11 L 156.65 189.44 L 156.43 189.77 L 156.18 190.08 L 155.93 190.37 L 155.66 190.66 L 155.37 190.93 L 155.08 191.18 L 154.77 191.43 L 154.44 191.65 L 154.11 191.86 L 153.77 192.06 L 153.42 192.23 L 153.06 192.39 L 152.70 192.53 L 152.32 192.66 L 151.94 192.76 L 151.56 192.85 L 151.17 192.91 L 150.78 192.96 L 150.39 192.99 L 150.00 193.00 L 149.61 192.99 L 149.22 192.96 L 148.83 192.91 L 148.44 192.85 L 148.06 192.76 L 147.68 192.66 L 147.30 192.53 L 146.94 192.39 L 146.58 192.23 L 146.23 192.06 L 145.89 191.86 L 145.56 191.65 L 145.23 191.43 L 144.92 191.18 L 144.63 190.93 L 144.34 190.66 L 144.07 190.37 L 143.82 190.08 L 143.57 189.77 L 143.35 189.44 L 143.14 189.11 L 142.94 188.77 L 142.77 188.42 L 142.61 188.06 L 142.47 187.70 L 142.34 187.32 L 142.24 186.94 L 142.15 186.56 L 142.09 186.17 L 142.04 185.78 L 142.01 185.39 L 142.00 185.00 L 142.01 184.61 L 142.04 184.22 L 142.09 183.83 L 142.15 183.44 L 142.24 183.06 L 142.34 182.68 L 142.47 182.30 L 142.61 181.94 L 142.77 181.58 L 142.94 181.23 L 143.14 180.89 L 143.35 180.56 L 143.57 180.23 L 143.82 179.92 L 144.07 179.63 L 144.34 179.34 L 144.63 179.07 L 144.92 178.82 L 145.23 178.57 L 145.56 178.35 L 145.89 178.14 L 146.23 177.94 L 146.58 177.77 L 146.94 177.61 L 147.30 177.47 L 147.68 177.34 L 148.06 177.24 L 148.44 177.15 L 148.83 177.09 L 149.22 177.04 L 149.61 177.01 L 150.00 177.00 L 150.39 177.01 L 150.78 177.04 L 151.17 177.09 L 151.56 177.15 L 151.94 177.24 L 152.32 177.34 L 152.70 177.47 L 153.06 177.61 L 153.42 177.77 L 153.77 177.94 L 154.11 178.14 L 154.44 178.35 L 154.77 178.57 L 155.08 178.82 L 155.37 179.07 L 155.66 179.34 L 155.93 179.63 L 156.18 179.92 L 156.43 180.23 L 156.65 180.56 L 156.86 180.89 L 157.06 181.23 L 157.23 181.58 L 157.39 181.94 L 157.53 182.30 L 157.66 182.68 L 157.76 183.06 L 157.85 183.44 L 157.91 183.83 L 157.96 184.22 L 157.99 184.61 Z M 180.99 184.61 L 181.00 185.00 L 180.99 185.39 L 180.96 185.78 L 180.91 186.17 L 180.85 186.56 L 180.76 186.94 L 180.66 187.32 L 180.53 187.70 L 180.39 188.06 L 180.23 188.42 L 180.06 188.77 L 179.86 189.11 L 179.65 189.44 L 179.43 189.77 L 179.18 190.08 L 178.93 190.37 L 178.66 190.66 L 178.37 190.93 L 178.08 191.18 L 177.77 191.43 L 177.44 191.65 L 177.11 191.86 L 176.77 192.06 L 176.42 192.23 L 176.06 192.39 L 175.70 192.53 L 175.32 192.66 L 174.94 192.76 L 174.56 192.85 L 174.17 192.91 L 173.78 192.96 L 173.39 192.99 L 173.00 193.00 L 172.61 192.99 L 172.22 192.96 L 171.83 192.91 L 171.44 192.85 L 171.06 192.76 L 170.68 192.66 L 170.30 192.53 L 169.94 192.39 L 169.58 192.23 L 169.23 192.06 L 168.89 191.86 L 168.56 191.65 L 168.23 191.43 L 167.92 191.18 L 167.63 190.93 L 167.34 190.66 L 167.07 190.37 L 166.82 190.08 L 166.57 189.77 L 166.35 189.44 L 166.14 189.11 L 165.94 188.77 L 165.77 188.42 L 165.61 188.06 L 165.47 187.70 L 165.34 187.32 L 165.24 186.94 L 165.15 186.56 L 165.09 186.17 L 165.04 185.78 L 165.01 185.39 L 165.00 185.00 L 165.01 184.61 L 165.04 184.22 L 165.09 183.83 L 165.15 183.44 L 165.24 183.06 L 165.34 182.68 L 165.47 182.30 L 165.61 181.94 L 165.77 181.58 L 165.94 181.23 L 166.14 180.89 L 166.35 180.56 L 166.57 180.23 L 166.82 179.92 L 167.07 179.63 L 167.34 179.34 L 167.63 179.07 L 167.92 178.82 L 168.23 178.57 L 168.56 178.35 L 168.89 178.14 L 169.23 177.94 L 169.58 177.77 L 169.94 177.61 L 170.30 177.47 L 170.68 177.34 L 171.06 177.24 L 171.44 177.15 L 171.83 177.09 L 172.22 177.04 L 172.61 177.01 L 173.00 177.00 L 173.39 177.01 L 173.78 177.04 L 174.17 177.09 L 174.56 177.15 L 174.94 177.24 L 175.32 177.34 L 175.70 177.47 L 176.06 177.61 L 176.42 177.77 L 176.77 177.94 L 177.11 178.14 L 177.44 178.35 L 177.77 178.57 L 178.08 178.82 L 178.37 179.07 L 178.66 179.34 L 178.93 179.63 L 179.18 179.92 L 179.43 180.23 L 179.65 180.56 L 179.86 180.89 L 180.06 181.23 L 180.23 181.58 L 180.39 181.94 L 180.53 182.30 L 180.66 182.68 L 180.76 183.06 L 180.85 183.44 L 180.91 183.83 L 180.96 184.22 L 180.99 184.61 Z" fill="#8C94A3" fill-rule="evenodd"/>
9
+ </svg>
@@ -0,0 +1,261 @@
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
+ <title>TinyWebUI</title>
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
8
+ <link rel="stylesheet" href="styles.css">
9
+ </head>
10
+ <body>
11
+
12
+ <div id="shell">
13
+
14
+ <div id="side-backdrop"></div>
15
+
16
+ <aside id="side" aria-label="Chat history">
17
+ <div class="brand">
18
+ <button id="brandHome" class="brand-home" type="button" title="New chat" aria-label="TinyWebUI, start a new chat">
19
+ <img class="logo" src="favicon.svg" alt="" aria-hidden="true">
20
+ <span class="brand-name">TinyWebUI</span>
21
+ </button>
22
+ <button id="sideCollapse" class="icon-btn" type="button" aria-label="Hide sidebar" title="Hide sidebar"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg></button>
23
+ </div>
24
+ <div class="side-scroll">
25
+ <nav class="nav">
26
+ <button id="new" class="nav-item" type="button"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.4 2.6a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg><span>New chat</span></button>
27
+ <div class="search-row nav-item">
28
+ <svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
29
+ <input id="chatSearch" type="search" placeholder="Search" aria-label="Search chats" autocomplete="off" spellcheck="false">
30
+ <button id="chatSearchClear" class="x" type="button" aria-label="Clear search" hidden>×</button>
31
+ </div>
32
+ <button id="toggle-automations" class="nav-item" type="button"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4l2.5 1.5"/><path d="M19 3.5 21.5 6"/><path d="M5 3.5 2.5 6"/><circle cx="12" cy="13" r="8"/></svg><span>Automations</span></button>
33
+ <button id="toggle-mcp" class="nav-item" type="button"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg><span>MCP servers</span></button>
34
+ <button id="toggle-settings" class="nav-item" type="button"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg><span>Settings</span></button>
35
+ <button id="toggle-statistics" class="nav-item" type="button"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg><span>Statistics</span></button>
36
+ <button id="toggle-admin" class="nav-item" type="button" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg><span>Admin</span></button>
37
+ <button id="logout" class="nav-item" type="button" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg><span>Log out</span></button>
38
+ </nav>
39
+ <div class="side-label"><span>Recent conversations</span><button id="newFolder" class="icon-btn" type="button" aria-label="New folder" title="New folder"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><path d="M12 11v4M10 13h4"/></svg></button></div>
40
+ <div id="chats"></div>
41
+ </div>
42
+ <div id="side-resize" role="separator" aria-orientation="vertical" tabindex="0"
43
+ aria-label="Resize chat history" title="Drag to resize, double-click to reset"></div>
44
+ </aside>
45
+
46
+ <dialog id="organizeDialog" aria-labelledby="organizeTitle">
47
+ <form id="organizeForm">
48
+ <h2 id="organizeTitle">Chat options</h2>
49
+ <label for="organizeFolder">folder</label>
50
+ <select id="organizeFolder">
51
+ <option value="">(no folder)</option>
52
+ </select>
53
+ <div id="organizeError" class="organize-error" role="alert" hidden></div>
54
+ <div class="dialog-actions">
55
+ <button type="button" id="organizeDelete" class="danger">delete chat</button>
56
+ <button type="button" id="organizeCancel">cancel</button>
57
+ <button type="submit" class="primary">save</button>
58
+ </div>
59
+ </form>
60
+ </dialog>
61
+
62
+ <dialog id="newFolderDialog" aria-labelledby="newFolderTitle">
63
+ <form id="newFolderForm">
64
+ <h2 id="newFolderTitle">New folder</h2>
65
+ <label for="newFolderName">name</label>
66
+ <input id="newFolderName" maxlength="40" autocomplete="off" placeholder="Folder name">
67
+ <div id="newFolderError" class="organize-error" role="alert" hidden></div>
68
+ <div class="dialog-actions">
69
+ <button type="button" id="newFolderCancel">cancel</button>
70
+ <button type="submit" class="primary">create</button>
71
+ </div>
72
+ </form>
73
+ </dialog>
74
+
75
+ <dialog id="libraryDialog" aria-labelledby="libraryTitle">
76
+ <form id="libraryForm">
77
+ <h2 id="libraryTitle">Attach from earlier chats</h2>
78
+ <input id="libraryFilter" type="search" autocomplete="off" placeholder="Filter by file name" aria-label="Filter files">
79
+ <div id="libraryList" class="library-list"></div>
80
+ <div class="dialog-actions">
81
+ <button type="button" id="libraryCancel">cancel</button>
82
+ <button type="submit" id="libraryAttach" class="primary" disabled>attach</button>
83
+ </div>
84
+ </form>
85
+ </dialog>
86
+
87
+ <main>
88
+ <button id="menu" class="icon-btn" type="button" aria-label="Show sidebar" aria-expanded="false" aria-controls="side"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg></button>
89
+ <section id="settings">
90
+ <div class="automation-head">
91
+ <h2>Settings</h2>
92
+ <span id="saveMsg" class="usage"></span>
93
+ <button id="cancel">Close</button>
94
+ <button class="primary" id="save">Save changes</button>
95
+ </div>
96
+ <div class="settings-content">
97
+ <section class="set-group">
98
+ <header><h3>Model</h3><p>Which model answers, and how it samples.</p></header>
99
+ <div class="set-grid">
100
+ <div class="span-2"><label for="model">Model id</label><input id="model" spellcheck="false"><small>Also switchable from the model pill in the message box.</small></div>
101
+ <div><label for="temperature">Temperature</label><input id="temperature" type="number" step="0.1" min="0" max="2" placeholder="Provider default"></div>
102
+ <div><label for="maxTokens">Max output tokens</label><input id="maxTokens" type="number" min="1" placeholder="No limit"></div>
103
+ <div class="span-2"><label for="timezone">Timezone</label><select id="timezone"><option value="">Server's timezone</option></select><small>The zone the model is told today's date in.</small></div>
104
+ </div>
105
+ </section>
106
+
107
+ <section class="set-group">
108
+ <header><h3>Instructions</h3><p>The system prompt sent at the start of every chat.</p></header>
109
+ <textarea id="systemPrompt" rows="10" aria-label="System prompt"></textarea>
110
+ </section>
111
+
112
+ <section class="set-group">
113
+ <header><h3>Tools</h3><p>How far the model can go on its own before handing back.</p></header>
114
+ <div class="set-grid">
115
+ <div><label for="maxToolRounds">Max tool turns</label><input id="maxToolRounds" type="number" min="1" max="50"><small>Tool calls allowed per reply.</small></div>
116
+ <div><label for="askUserTimeoutSeconds">Question timeout (s)</label><input id="askUserTimeoutSeconds" type="number" min="0" max="86400"><small>How long a question from the model waits before it continues on its own. 0: wait until answered.</small></div>
117
+ <div><label for="toolApproval">Ask before running</label><select id="toolApproval"><option value="writes">MCP tools that write</option><option value="all">Every MCP tool call</option><option value="off">Never ask</option></select><small>Built-in tools never ask. Scheduled runs refuse calls that would.</small></div>
118
+ </div>
119
+ </section>
120
+
121
+ <section class="set-group">
122
+ <header><h3>Context &amp; caching</h3><p>Keeps long chats fast and cheap by trimming old tool output.</p></header>
123
+ <div class="set-grid">
124
+ <div><label for="compactThreshold">Compact history above</label><input id="compactThreshold" type="number" min="0" step="1000"><small>Tokens. Old tool results are stubbed past this. 0 turns it off.</small></div>
125
+ <div><label for="keepTurns">Always keep last</label><input id="keepTurns" type="number" min="1" max="20"><small>Recent turns whose tool results stay in full.</small></div>
126
+ <div><label for="maxInlineChars">Stub single results above</label><input id="maxInlineChars" type="number" min="0" step="1000"><small>Characters. The turn that fetched it still reads it whole.</small></div>
127
+ <div><label for="maxTurnChars">Stub even this turn above</label><input id="maxTurnChars" type="number" min="0" step="10000"><small>Characters. A result this big is stubbed right away, even for the turn that fetched it.</small></div>
128
+ <div><label for="compactMinSaved">Only compact if it saves</label><input id="compactMinSaved" type="number" min="0" step="1000"><small>Characters. Compacting costs one cache miss, so small savings are skipped.</small></div>
129
+ <div><label for="maxHistoryTokens">Hard history limit</label><input id="maxHistoryTokens" type="number" min="0" step="10000"><small>Tokens. Past this, the oldest turns stop being sent (they stay visible). 0 turns it off.</small></div>
130
+ <div><label for="cacheTtl">Prompt cache lifetime</label><select id="cacheTtl"><option value="5m">5 minutes</option><option value="1h">1 hour</option></select><small>Anthropic models. 1 hour costs more to write, survives longer pauses.</small></div>
131
+ <div><label for="cacheMode">Cache style</label><select id="cacheMode"><option value="auto">Automatic</option><option value="implicit">Stable prefix only</option><option value="explicit">Anthropic cache_control</option><option value="rolling">OpenRouter automatic</option><option value="off">Off</option></select><small>Automatic is right for OpenRouter and local models; set it for other gateways.</small></div>
132
+ </div>
133
+ </section>
134
+
135
+ <section class="set-group">
136
+ <header><h3>Appearance</h3></header>
137
+ <div class="set-grid">
138
+ <div><label for="theme">Theme</label><select id="theme"><option value="">Default</option></select></div>
139
+ </div>
140
+ </section>
141
+ </div>
142
+ </section>
143
+
144
+ <section id="mcp" aria-label="MCP servers">
145
+ <div class="automation-head"><h2>MCP servers</h2><button id="close-mcp">close</button></div>
146
+ <div class="settings-content">
147
+ <div class="sec">
148
+ <div class="sec-h">config file <span id="mcpPath" class="usage"></span></div>
149
+ <textarea id="mcpText" rows="9" spellcheck="false"></textarea>
150
+ <div class="row">
151
+ <div id="mcpMsg" class="usage"></div>
152
+ <div class="no-shrink"><button id="saveMcp">save &amp; reconnect</button></div>
153
+ </div>
154
+ <div>
155
+ <label>tools <span id="toolCount" class="usage"></span></label>
156
+ <div id="tools" class="tool-list"></div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </section>
161
+
162
+ <section id="statistics" aria-label="Statistics">
163
+ <div class="automation-head"><h2>Statistics</h2><span id="statisticsFilter"></span><button id="close-statistics">close</button></div>
164
+ <div class="statistics-content">
165
+ <section class="statistics-block">
166
+ <div class="sec-h">cost and averages <span class="usage">provider-reported costs only</span></div>
167
+ <div id="statisticsSummary" class="row"></div>
168
+ <div id="statisticsCoverage" class="usage"></div>
169
+ <div id="statisticsCosts" class="statistics-costs"></div>
170
+ </section>
171
+ <section class="statistics-block">
172
+ <div class="sec-h">token usage <span id="usageRange" class="usage"></span></div>
173
+ <div id="usageStats" class="row"></div>
174
+ <div id="usageDistribution"></div>
175
+ <div id="usageModels" class="usage-models"></div>
176
+ <div id="usageBars" class="usage-bars"></div>
177
+ </section>
178
+ <section class="statistics-block">
179
+ <div class="sec-h">agentic <span class="usage">tool calls across all rounds</span></div>
180
+ <div id="statisticsTools" class="statistics-costs"></div>
181
+ </section>
182
+ <section class="statistics-block">
183
+ <div class="sec-h">cache misses <span id="statisticsMissesSummary" class="usage"></span></div>
184
+ <div id="statisticsMisses" class="statistics-costs"></div>
185
+ </section>
186
+ </div>
187
+ </section>
188
+
189
+ <div id="login" hidden>
190
+ <form id="loginForm" class="login-card">
191
+ <h2>TinyWebUI</h2>
192
+ <label for="loginPassword">Password</label>
193
+ <input id="loginPassword" type="password" autocomplete="current-password" required>
194
+ <button class="primary" type="submit">Sign in</button>
195
+ <div id="loginMsg" class="usage" role="alert"></div>
196
+ </form>
197
+ </div>
198
+
199
+ <section id="admin" aria-label="Admin">
200
+ <div class="automation-head"><h2>Admin</h2><button id="close-admin">close</button></div>
201
+ <div class="statistics-content">
202
+ <section class="statistics-block">
203
+ <div class="sec-h">users <span id="adminCount" class="usage"></span></div>
204
+ <div id="adminMsg" class="usage"></div>
205
+ <div id="adminUsers" class="admin-users"></div>
206
+ </section>
207
+ <section id="adminView" class="statistics-block" hidden>
208
+ <div class="sec-h"><button id="adminBack" type="button">← back</button> <span id="adminViewTitle"></span><span id="adminViewMeta" class="usage"></span></div>
209
+ <div class="usage admin-note">Read-only admin view. Every view is recorded in the audit log.</div>
210
+ <div id="adminViewBody"></div>
211
+ </section>
212
+ </div>
213
+ </section>
214
+
215
+ <section id="automations" aria-label="Automations">
216
+ <div class="automation-head"><h2>Automations</h2><button id="new-automation" class="primary" type="button">New automation</button><button id="close-automations">close</button></div>
217
+ <div class="automation-content">
218
+ <div id="automation-create-slot"></div>
219
+ <div id="automation-list" class="automation-list"></div>
220
+ </div>
221
+ </section>
222
+
223
+ <div id="log"><div class="wrap" id="wrap"></div></div>
224
+
225
+ <aside id="rightRail" class="right-rail">
226
+ <div id="chatDocs" class="rail-card" hidden></div>
227
+ </aside>
228
+
229
+ <aside id="chatOutline" class="outline-rail" hidden></aside>
230
+
231
+ <div id="hello" aria-hidden="true">
232
+ <h2>Hello there</h2>
233
+ <p>Type a message or attach files to get started</p>
234
+ </div>
235
+
236
+ <footer>
237
+ <details id="chatTasks" class="task-card" open hidden>
238
+ <summary class="task-heading"><strong>Tasks</strong><span id="taskCount"></span></summary>
239
+ <ol id="taskItems" class="task-items"></ol>
240
+ </details>
241
+ <div id="attachments" class="attachments" hidden></div>
242
+ <form id="form">
243
+ <input type="file" id="fileInput" multiple hidden>
244
+ <div id="queue" class="queue" hidden></div>
245
+ <textarea id="input" rows="1" placeholder="Type a message…" aria-label="Message"></textarea>
246
+ <div class="composer-bar">
247
+ <button type="button" id="attach" class="icon-btn" title="Attach files and tools" aria-label="Attach files and tools" aria-haspopup="menu" aria-expanded="false"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
248
+ <span class="grow"></span>
249
+ <button type="button" id="modelBtn" class="model-chip" aria-haspopup="menu" aria-expanded="false" title="Switch model"><span id="status" role="status" aria-live="polite"></span><svg class="ic chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
250
+ <button class="primary" id="send" type="submit" aria-label="Send"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>
251
+ </div>
252
+ </form>
253
+ </footer>
254
+ </main>
255
+
256
+ </div>
257
+
258
+ <script type="module" src="themes/theme-loader.js"></script>
259
+ <script type="module" src="app.js"></script>
260
+ </body>
261
+ </html>