tinywebui 0.1.3 → 0.1.5
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.
- package/README.md +20 -13
- package/bin/tinywebui.js +69 -63
- package/package.json +25 -8
- package/public/app.js +40 -14
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -49
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +83 -20
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -138
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -451
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +98 -66
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -100
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +428 -392
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- package/src/set-password.js +0 -43
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ It comes pre-loaded with two TinySuite MCP servers, so a new install can already
|
|
|
47
47
|
- **Hybrid local search** (BM25 plus a small on-device embedding model) over documents and past chats.
|
|
48
48
|
- **Long chats that stay affordable.** Cache-friendly prefixes, one-time compaction of large tool results, and per-round token statistics.
|
|
49
49
|
- **Automations.** Cron-scheduled prompts that run in a chat of your choice.
|
|
50
|
-
- **Private by default.**
|
|
50
|
+
- **Private by default.** The first account becomes the administrator; later local accounts require a single-use invitation. Teams can alternatively put SSO in front.
|
|
51
51
|
- **Managed from files.** Version-controlled config that hot-reloads, refuses typos and has a fingerprint you can check against a running instance.
|
|
52
52
|
- **Themes.** Fall Fairy and Cyber Grid are included. See [theme authoring](public/themes/README.md) to write your own.
|
|
53
53
|
|
|
@@ -59,31 +59,36 @@ Requires **Node.js 22.13** or later. The bundled TinySearch and TinyContext serv
|
|
|
59
59
|
npx tinywebui
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Open <http://127.0.0.1:7777> and create the first account with a password of at least 10 characters. That account becomes the administrator and can invite others from the Admin panel. Settings then opens on its first step: add a connector (any OpenAI-compatible URL, with an API key if it needs one), then the models people can pick from it. Every model, with its prompt, sampling and caching, belongs to a connector. The same in `tinywebui.config.json` (start from `example.tinywebui.config.json`):
|
|
63
63
|
|
|
64
64
|
```json
|
|
65
65
|
{
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
"connectors": [
|
|
67
|
+
{
|
|
68
|
+
"id": "local",
|
|
69
|
+
"baseUrl": "http://localhost:11434/v1",
|
|
70
|
+
"models": [{ "id": "llama", "label": "Llama 3.2", "model": "llama3.2" }]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
69
73
|
}
|
|
70
74
|
```
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
A connector that needs a key takes `"apiKey": "${NAME}"`, filled in from the environment, or the key typed into Settings.
|
|
73
77
|
|
|
74
|
-
`
|
|
78
|
+
`tinywebui.config.json` describes the deployment (endpoints, connectors, models, policy) and holds no secrets. A key is either a `${NAME}` reference, filled in from the environment, or kept in `tinywebui.secrets.json` beside it: what the Settings panel saves, and the generated session-signing secret. A key written straight into the config file moves there the next time the server starts. Accounts, password hashes and conversations live in `tinywebui.db`. TinyWebUI keeps the secrets file and the database readable by their owner only; keep them private and backed up.
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
`npx tinywebui` keeps the config, keys, database and embedding models in one folder per user: `~/Library/Application Support/TinyWebUI` on macOS, `%LOCALAPPDATA%\TinyWebUI` on Windows and `~/.local/share/tinywebui` (or `$XDG_DATA_HOME/tinywebui`) on Linux. Run it in a folder that has its own `tinywebui.config.json`, or pass `--config`, to use that instead. It uses keyword search until you run `npx tinywebui models pull fast` (about 90 MB) to enable hybrid search.
|
|
81
|
+
|
|
82
|
+
From a clone, run `npm install && npm start`. With Docker, start the service and create the administrator with an email address and password in the browser. The email is checked for valid format; TinyWebUI does not send mail or require a mail server for local accounts:
|
|
77
83
|
|
|
78
84
|
```bash
|
|
79
85
|
docker compose build
|
|
80
|
-
docker compose run --rm -it tinywebui set-password
|
|
81
86
|
docker compose up -d
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
The Compose example publishes port 7777 on the host's loopback interface.
|
|
89
|
+
The Compose example publishes port 7777 on the host's loopback interface. Keep the data volume: it contains the accounts, conversations and local password hashes. `npm start` and `npx tinywebui` default to multiuser setup. To run a local workspace without login, explicitly set `"authMode": "local"` in `tinywebui.config.json`; this mode binds only to loopback and has full access to settings and data, without account management. The runtime images contain Node.js but no npm/npx; connect MCP servers over HTTP or include their executables in a custom image.
|
|
85
90
|
|
|
86
|
-
The
|
|
91
|
+
The former password-only `single` mode now fails startup instead of opening without login. To move such an installation to local accounts, set `authMode` to `multiuser`, restart, and create the administrator in the browser. On that first setup, existing unowned chats, files, artifacts, folders and automations move to the new administrator. Remove any `TINYWEBUI_PASSWORD` environment variable; it no longer selects an auth mode.
|
|
87
92
|
|
|
88
93
|
## Documentation
|
|
89
94
|
|
|
@@ -99,7 +104,7 @@ The full reference lives at **[tinysuite.dev/docs/tinywebui](https://tinysuite.d
|
|
|
99
104
|
| [Automations](https://tinysuite.dev/docs/tinywebui/#automations) | Scheduled prompts |
|
|
100
105
|
| [Long chats](https://tinysuite.dev/docs/tinywebui/#long-chats-caching-and-compaction) | Caching and compaction |
|
|
101
106
|
| [Usage statistics](https://tinysuite.dev/docs/tinywebui/#usage-statistics) | Token use and attribution |
|
|
102
|
-
| [Access](https://tinysuite.dev/docs/tinywebui/#access) |
|
|
107
|
+
| [Access](https://tinysuite.dev/docs/tinywebui/#access) | Local invited users, explicit local mode, or a team behind SSO |
|
|
103
108
|
| [Deployment as code](https://tinysuite.dev/docs/tinywebui/#deployment-as-code) | JavaScript config, locked settings, roles and features |
|
|
104
109
|
| [CLI](https://tinysuite.dev/docs/tinywebui/#cli) | Every command |
|
|
105
110
|
| [Security and limits](https://tinysuite.dev/docs/tinywebui/#security-and-limits) | Exposure, MCP trust, what isn't included |
|
|
@@ -109,10 +114,12 @@ In this repository: [docs/deploy.md](docs/deploy.md) is a scripted container dep
|
|
|
109
114
|
## Development
|
|
110
115
|
|
|
111
116
|
```bash
|
|
112
|
-
npm test # the whole suite, about 10 seconds
|
|
117
|
+
npm test # type check, then the whole suite, about 10 seconds
|
|
113
118
|
npm run dev # restart on changes in src/ and bin/
|
|
114
119
|
```
|
|
115
120
|
|
|
121
|
+
The server is TypeScript that Node runs directly, with no build step, so working from a clone needs Node.js 22.18 or later. The npm package ships JavaScript compiled from it (`npm run build`, which `npm pack` runs) and works from 22.13.
|
|
122
|
+
|
|
116
123
|
[AGENTS.md](AGENTS.md) maps the codebase and says where new code goes. `npm run eval` runs model-behaviour evals from `evals/tasks/` and makes real API calls, so it costs money.
|
|
117
124
|
|
|
118
125
|
## License
|
package/bin/tinywebui.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
3
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
4
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
5
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return path;
|
|
9
|
+
};
|
|
10
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { resolve, dirname, join } from 'node:path';
|
|
4
12
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
|
|
13
|
+
import { userDataDir, isInstalled } from '../src/config/data_dir.js';
|
|
6
14
|
const args = process.argv.slice(2);
|
|
7
15
|
const flag = (name, fallback) => {
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
const i = args.indexOf(`--${name}`);
|
|
17
|
+
return i === -1 ? fallback : args[i + 1];
|
|
10
18
|
};
|
|
11
|
-
|
|
12
19
|
// `--config <path>` (or `validate <path>`) points every command at a config
|
|
13
20
|
// file, JS or JSON, exactly like $TINYWEBUI_CONFIG does.
|
|
14
21
|
const configArg = flag('config') ?? (args[0] === 'validate' && args[1] && !args[1].startsWith('--') ? args[1] : undefined);
|
|
15
|
-
if (configArg)
|
|
16
|
-
|
|
22
|
+
if (configArg)
|
|
23
|
+
process.env.TINYWEBUI_CONFIG = resolve(configArg);
|
|
17
24
|
if (args.includes('--help') || args.includes('-h')) {
|
|
18
|
-
|
|
19
|
-
tinywebui set-password create or change the owner password
|
|
25
|
+
console.log(`tinywebui [start] [--port 7777] [--host 127.0.0.1] [--config path]
|
|
20
26
|
tinywebui validate [path] check the config files; prints the fingerprint, exits 1 on problems
|
|
21
27
|
tinywebui migrate [--check]
|
|
22
28
|
bring the database to the current schema (--check: exit 1 if behind)
|
|
@@ -36,7 +42,10 @@ tinywebui schema JSON Schema for the config file
|
|
|
36
42
|
|
|
37
43
|
Config comes from ./tinywebui.config.js if it exists, then
|
|
38
44
|
./tinywebui.config.json ($TINYWEBUI_CONFIG may point at either), and MCP
|
|
39
|
-
servers from mcp.json next to it (or $TINYWEBUI_MCP).
|
|
45
|
+
servers from mcp.json next to it (or $TINYWEBUI_MCP). Installed from npm
|
|
46
|
+
(npx, npm install -g) and run where neither file is, it uses the per-user
|
|
47
|
+
folder instead: ${userDataDir()}
|
|
48
|
+
The keys, database and embedding models live beside the config.
|
|
40
49
|
|
|
41
50
|
tinywebui.config.js exports the options start() takes, as an object or an
|
|
42
51
|
(async) function returning one:
|
|
@@ -49,66 +58,63 @@ tinywebui.config.js exports the options start() takes, as an object or an
|
|
|
49
58
|
Keys under "config" win over the JSON file and are locked in the settings
|
|
50
59
|
panel. See README for the rest.
|
|
51
60
|
`);
|
|
52
|
-
|
|
61
|
+
process.exit(0);
|
|
53
62
|
}
|
|
54
|
-
|
|
55
63
|
// Resolve the JS config before src/config/config.js is imported: that module fixes
|
|
56
64
|
// its JSON path at load time from $TINYWEBUI_CONFIG, so a .js path there has
|
|
57
65
|
// to be swapped for the JSON file beside it first.
|
|
58
66
|
const envPath = process.env.TINYWEBUI_CONFIG && resolve(process.env.TINYWEBUI_CONFIG);
|
|
59
67
|
let jsPath = null;
|
|
60
68
|
if (envPath && /\.[cm]?js$/.test(envPath)) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
jsPath = envPath;
|
|
70
|
+
process.env.TINYWEBUI_CONFIG = resolve(dirname(envPath), 'tinywebui.config.json');
|
|
71
|
+
}
|
|
72
|
+
else if (!envPath) {
|
|
73
|
+
for (const name of ['tinywebui.config.js', 'tinywebui.config.mjs']) {
|
|
74
|
+
const p = resolve(process.cwd(), name);
|
|
75
|
+
if (existsSync(p)) {
|
|
76
|
+
jsPath = p;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Installed (npx, npm install -g) and not run in a folder with a config of
|
|
81
|
+
// its own: the per-user data folder, so keys and chats do not end up in
|
|
82
|
+
// whichever folder the command happened to run in.
|
|
83
|
+
if (!jsPath && !existsSync(resolve(process.cwd(), 'tinywebui.config.json')) && isInstalled(import.meta.url)) {
|
|
84
|
+
const dir = userDataDir();
|
|
85
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
86
|
+
process.env.TINYWEBUI_CONFIG = join(dir, 'tinywebui.config.json');
|
|
87
|
+
}
|
|
68
88
|
}
|
|
69
|
-
|
|
70
89
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (args[0] === 'set-password') {
|
|
81
|
-
const { setPassword } = await import('../src/set-password.js');
|
|
82
|
-
await setPassword(opts);
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
const COMMANDS = ['validate', 'effective', 'fingerprint', 'users', 'schema', 'migrate', 'doctor', 'config', 'models'];
|
|
86
|
-
if (COMMANDS.includes(args[0])) {
|
|
87
|
-
const { runCli } = await import('../src/cli.js');
|
|
88
|
-
process.exit(await runCli(args[0], args.slice(1), opts));
|
|
89
|
-
}
|
|
90
|
-
if (args[0] && !args[0].startsWith('--') && args[0] !== 'start') {
|
|
91
|
-
console.error(`[tinywebui] unknown command "${args[0]}" (see --help)`);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
const { createConfigSource } = await import('../src/config/config.js');
|
|
95
|
-
const initial = createConfigSource(opts).load({ persistSecret: false });
|
|
96
|
-
if (initial.authMode === 'single' && !initial.authPassword && !process.env.TINYWEBUI_PASSWORD) {
|
|
97
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
98
|
-
throw new Error('owner password required: run `tinywebui set-password` in a terminal, or set TINYWEBUI_PASSWORD before starting');
|
|
90
|
+
let opts = {};
|
|
91
|
+
if (jsPath && !existsSync(jsPath))
|
|
92
|
+
throw new Error(`config file not found: ${jsPath}`);
|
|
93
|
+
if (jsPath) {
|
|
94
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(jsPath).href));
|
|
95
|
+
const exported = mod.default ?? mod;
|
|
96
|
+
opts = (typeof exported === 'function' ? await exported() : exported) || {};
|
|
97
|
+
// stderr, so a command's stdout stays pure JSON for scripts.
|
|
98
|
+
console.error(`[tinywebui] options: ${jsPath}`);
|
|
99
99
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
100
|
+
const COMMANDS = ['validate', 'effective', 'fingerprint', 'users', 'schema', 'migrate', 'doctor', 'config', 'models'];
|
|
101
|
+
if (COMMANDS.includes(args[0])) {
|
|
102
|
+
const { runCli } = await import('../src/cli.js');
|
|
103
|
+
process.exit(await runCli(args[0], args.slice(1), opts));
|
|
104
|
+
}
|
|
105
|
+
if (args[0] && !args[0].startsWith('--') && args[0] !== 'start') {
|
|
106
|
+
console.error(`[tinywebui] unknown command "${args[0]}" (see --help)`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const { start } = await import('../src/server.js');
|
|
110
|
+
// Command-line flags beat the file; the file beats the built-in defaults.
|
|
111
|
+
await start({
|
|
112
|
+
...opts,
|
|
113
|
+
port: Number(flag('port', opts.port ?? process.env.PORT ?? 7777)),
|
|
114
|
+
host: flag('host', opts.host ?? '127.0.0.1'),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[tinywebui] ${err.message}`);
|
|
119
|
+
process.exit(1);
|
|
114
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinywebui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A small, local-first chat workspace for any OpenAI-compatible model, with MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -18,18 +18,30 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"src",
|
|
20
20
|
"public",
|
|
21
|
-
"README.md"
|
|
21
|
+
"README.md",
|
|
22
|
+
"!**/*.ts"
|
|
22
23
|
],
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=22.13"
|
|
25
26
|
},
|
|
27
|
+
"devEngines": {
|
|
28
|
+
"runtime": {
|
|
29
|
+
"name": "node",
|
|
30
|
+
"version": ">=22.18",
|
|
31
|
+
"onFail": "warn"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
26
34
|
"scripts": {
|
|
27
|
-
"prestart": "node bin/tinywebui.
|
|
28
|
-
"start": "node bin/tinywebui.
|
|
29
|
-
"predev": "node bin/tinywebui.
|
|
30
|
-
"dev": "node --watch-path=./src --watch-path=./bin bin/tinywebui.
|
|
31
|
-
"stop": "pkill -f 'bin/tinywebui
|
|
32
|
-
"
|
|
35
|
+
"prestart": "node bin/tinywebui.ts models ensure",
|
|
36
|
+
"start": "node bin/tinywebui.ts",
|
|
37
|
+
"predev": "node bin/tinywebui.ts models ensure",
|
|
38
|
+
"dev": "node --watch-path=./src --watch-path=./bin bin/tinywebui.ts",
|
|
39
|
+
"stop": "pkill -f 'bin/tinywebui' || true",
|
|
40
|
+
"typecheck": "tsc -p .",
|
|
41
|
+
"test": "tsc -p . && node --test --test-timeout=30000",
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"clean": "tsc -b --clean tsconfig.build.json",
|
|
44
|
+
"prepack": "npm run build",
|
|
33
45
|
"eval": "node evals/run.js"
|
|
34
46
|
},
|
|
35
47
|
"dependencies": {
|
|
@@ -45,5 +57,10 @@
|
|
|
45
57
|
"optionalDependencies": {
|
|
46
58
|
"@huggingface/tokenizers": "^0.2.0",
|
|
47
59
|
"onnxruntime-node": "^1.30.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/heic-convert": "^2.1.1",
|
|
63
|
+
"@types/node": "^22.20.5",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
48
65
|
}
|
|
49
66
|
}
|
package/public/app.js
CHANGED
|
@@ -11,35 +11,53 @@ import { FEATURE } from './shared/features.js';
|
|
|
11
11
|
import { initChat, newChat, openChat, sendHeld } from './chat/chat.js';
|
|
12
12
|
import { initTranscript } from './chat/transcript.js';
|
|
13
13
|
import { initQueue } from './chat/queue.js';
|
|
14
|
-
import { initSidebar, loadChats } from './chat/sidebar.js';
|
|
14
|
+
import { initSidebar, loadChats, closeSideDrawer } from './chat/sidebar.js';
|
|
15
15
|
import { initAttachments } from './chat/attachments.js';
|
|
16
16
|
import { initLibrary } from './chat/library.js';
|
|
17
17
|
import { initComposer } from './chat/composer.js';
|
|
18
|
-
import { PANEL, initPanels, openPanel } from './panels/panels.js';
|
|
18
|
+
import { PANEL, initPanels, openPanel, closeAllPanels } from './panels/panels.js';
|
|
19
19
|
import {
|
|
20
|
-
initSettings, onSettingsOpen, onSettingsClose,
|
|
21
|
-
loadConfig, loadMcp, loadTools, isLocked, isReadOnly, configuredModel
|
|
20
|
+
initSettings, onSettingsOpen, onSettingsClose, settingsBack, settingsCanClose,
|
|
21
|
+
loadConfig, loadMcp, loadTools, isLocked, isReadOnly, configuredModel, needsSetup
|
|
22
22
|
} from './panels/settings.js';
|
|
23
23
|
import { initAutomations, loadAutomations } from './panels/automations.js';
|
|
24
24
|
import { loadUsage } from './panels/usage.js';
|
|
25
25
|
import { initAdmin, loadAdmin } from './panels/admin.js';
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/** Local auth with no session: sign in, claim a fresh install, or accept an invite. */
|
|
28
28
|
async function needsLogin() {
|
|
29
29
|
const me = await loadAccess();
|
|
30
|
-
if (me.authMode !== '
|
|
30
|
+
if (me.authMode !== 'multiuser' || me.user) return false;
|
|
31
|
+
const invite = new URLSearchParams(location.search).get('invite');
|
|
32
|
+
if (invite) history.replaceState(null, '', location.pathname);
|
|
33
|
+
const setup = me.needsSetup && !invite;
|
|
34
|
+
const joining = Boolean(invite);
|
|
31
35
|
const box = $('login');
|
|
32
36
|
box.hidden = false;
|
|
33
|
-
$('
|
|
37
|
+
$('loginTitle').textContent = setup ? 'Create the administrator' : joining ? 'Join TinyWebUI' : 'TinyWebUI';
|
|
38
|
+
$('loginHelp').textContent = setup ? 'The first account owns this installation and can invite others.'
|
|
39
|
+
: joining ? 'Create your account using this invitation.' : '';
|
|
40
|
+
for (const id of ['loginEmailLabel', 'loginEmail']) $(id).hidden = false;
|
|
41
|
+
$('loginEmail').required = true;
|
|
42
|
+
$('loginPassword').autocomplete = setup || joining ? 'new-password' : 'current-password';
|
|
43
|
+
$('loginSubmit').textContent = setup ? 'Create administrator' : joining ? 'Create account' : 'Sign in';
|
|
44
|
+
$('loginEmail').focus();
|
|
34
45
|
$('loginForm').onsubmit = async (e) => {
|
|
35
46
|
e.preventDefault();
|
|
36
47
|
$('loginMsg').textContent = '';
|
|
37
48
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const account = {
|
|
50
|
+
email: $('loginEmail').value,
|
|
51
|
+
password: $('loginPassword').value
|
|
52
|
+
};
|
|
53
|
+
await api.post(setup ? '/api/auth/setup' : joining ? '/api/auth/accept-invite' : '/api/auth/login',
|
|
54
|
+
setup || joining ? { ...account, ...(joining ? { token: invite } : {}) }
|
|
55
|
+
: { email: account.email, password: account.password });
|
|
56
|
+
if (joining) location.href = location.pathname;
|
|
57
|
+
else location.reload();
|
|
40
58
|
} catch (err) {
|
|
41
59
|
$('loginMsg').textContent = err.message;
|
|
42
|
-
$('loginPassword').select();
|
|
60
|
+
if (!setup && !joining) $('loginPassword').select();
|
|
43
61
|
}
|
|
44
62
|
};
|
|
45
63
|
return true;
|
|
@@ -48,7 +66,12 @@ async function needsLogin() {
|
|
|
48
66
|
function wire() {
|
|
49
67
|
initTranscript({ openChat });
|
|
50
68
|
initQueue({ sendHeld });
|
|
51
|
-
|
|
69
|
+
// Picking a chat or New from the sidebar is a way out of any open panel,
|
|
70
|
+
// unless it holds unsaved edits the person chose to keep.
|
|
71
|
+
initSidebar({
|
|
72
|
+
openChat: (id) => closeAllPanels() && openChat(id),
|
|
73
|
+
newChat: () => closeAllPanels() && newChat()
|
|
74
|
+
});
|
|
52
75
|
initAttachments();
|
|
53
76
|
initLibrary();
|
|
54
77
|
initChat();
|
|
@@ -59,21 +82,24 @@ function wire() {
|
|
|
59
82
|
initSettings();
|
|
60
83
|
initAutomations();
|
|
61
84
|
initPanels({
|
|
62
|
-
[PANEL.SETTINGS]: { onOpen: onSettingsOpen, onClose: onSettingsClose, closeId: 'cancel' },
|
|
85
|
+
[PANEL.SETTINGS]: { onOpen: onSettingsOpen, onClose: onSettingsClose, onEscape: settingsBack, canClose: settingsCanClose, closeId: 'cancel' },
|
|
63
86
|
// The raw editor and the tool list are reloaded on open, so they reflect
|
|
64
87
|
// any toggles made from the composer menu.
|
|
65
88
|
[PANEL.MCP]: { onOpen: () => Promise.all([loadMcp(), loadTools()]) },
|
|
66
89
|
[PANEL.AUTOMATIONS]: { onOpen: loadAutomations },
|
|
67
90
|
[PANEL.STATISTICS]: { onOpen: loadUsage },
|
|
68
91
|
[PANEL.ADMIN]: { onOpen: loadAdmin }
|
|
69
|
-
});
|
|
92
|
+
}, { onAnyOpen: closeSideDrawer });
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
if (!(await needsLogin())) {
|
|
73
96
|
wire();
|
|
74
97
|
newChat();
|
|
75
98
|
// Tools render read-only for non-admins, so they wait on the config.
|
|
76
|
-
loadConfig().then(() =>
|
|
99
|
+
loadConfig().then(() => {
|
|
100
|
+
if (needsSetup()) openPanel(PANEL.SETTINGS);
|
|
101
|
+
return can(FEATURE.TOOLS) && loadTools();
|
|
102
|
+
});
|
|
77
103
|
if (can(FEATURE.MCP)) loadMcp();
|
|
78
104
|
initAdmin();
|
|
79
105
|
loadChats();
|
package/public/chat/composer.js
CHANGED
|
@@ -210,7 +210,7 @@ function renderTools(body, data) {
|
|
|
210
210
|
async function buildModelMenu(pop) {
|
|
211
211
|
// Tier 3: the pill is a personal choice among your role's models, saved
|
|
212
212
|
// per user; it never changes anyone else's model.
|
|
213
|
-
const personal =
|
|
213
|
+
const personal = ['multiuser', 'trusted-header'].includes(whoami().authMode);
|
|
214
214
|
const current = personal ? whoami().model : config.model();
|
|
215
215
|
if (!personal && config.isLocked('model')) {
|
|
216
216
|
pop.appendChild(Object.assign(el('div', 'pop-note'), {
|
package/public/chat/sidebar.js
CHANGED
|
@@ -299,8 +299,9 @@ export function clearSearch() {
|
|
|
299
299
|
* thing that reads as broken on a phone even when it technically isn't.
|
|
300
300
|
*/
|
|
301
301
|
/**
|
|
302
|
-
* Desktop hides the sidebar outright instead of opening it as a drawer
|
|
303
|
-
* same #menu button
|
|
302
|
+
* Desktop hides the sidebar outright instead of opening it as a drawer. The
|
|
303
|
+
* same #menu button does both, and it is on screen at every width -- over the
|
|
304
|
+
* panels too -- so getting back to the chats is always one press away.
|
|
304
305
|
*/
|
|
305
306
|
const COLLAPSE_KEY = 'tinywebui.side.collapsed';
|
|
306
307
|
const narrow = () => matchMedia('(max-width: 720px)').matches;
|
|
@@ -321,7 +322,7 @@ function setSideDrawer(open) {
|
|
|
321
322
|
side.inert = mobile && !open;
|
|
322
323
|
main.inert = mobile && open;
|
|
323
324
|
side.toggleAttribute('aria-hidden', mobile && !open);
|
|
324
|
-
menu.setAttribute('aria-expanded', String(mobile
|
|
325
|
+
menu.setAttribute('aria-expanded', String(mobile ? open : !document.body.classList.contains('side-collapsed')));
|
|
325
326
|
|
|
326
327
|
if (mobile && open) requestAnimationFrame(() => $('new').focus());
|
|
327
328
|
if (mobile && !open && side.contains(document.activeElement)) menu.focus();
|
|
@@ -330,6 +331,7 @@ export const closeSideDrawer = () => setSideDrawer(false);
|
|
|
330
331
|
|
|
331
332
|
function setCollapsed(on) {
|
|
332
333
|
document.body.classList.toggle('side-collapsed', on);
|
|
334
|
+
$('menu').setAttribute('aria-expanded', String(!on));
|
|
333
335
|
try { on ? localStorage.setItem(COLLAPSE_KEY, '1') : localStorage.removeItem(COLLAPSE_KEY); } catch { /* per-viewer nicety only */ }
|
|
334
336
|
}
|
|
335
337
|
|
|
@@ -421,8 +423,9 @@ export function initSidebar(deps) {
|
|
|
421
423
|
});
|
|
422
424
|
|
|
423
425
|
try { if (localStorage.getItem(COLLAPSE_KEY)) document.body.classList.add('side-collapsed'); } catch { /* as above */ }
|
|
424
|
-
$('
|
|
425
|
-
|
|
426
|
+
$('menu').onclick = () => (narrow()
|
|
427
|
+
? setSideDrawer(!$('side').classList.contains('open'))
|
|
428
|
+
: setCollapsed(!document.body.classList.contains('side-collapsed')));
|
|
426
429
|
$('side-backdrop').onclick = closeSideDrawer;
|
|
427
430
|
document.addEventListener('keydown', (e) => {
|
|
428
431
|
if (e.key === 'Escape' && $('side').classList.contains('open')) closeSideDrawer();
|