local-ai-chat-frontend 1.0.0 → 1.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.
- package/README.md +14 -9
- package/bin/security-headers.json +8 -0
- package/bin/start.js +47 -20
- package/dist/assets/index-CNhcUeJ6.js +72 -0
- package/dist/assets/index-CeikZEYv.css +1 -0
- package/dist/index.html +5 -2
- package/package.json +12 -5
- package/dist/assets/index-CB64_40G.js +0 -71
- package/dist/assets/index-ccdrjPVC.css +0 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Local LLM providers lack easy-to-deploy, cross-platform GUIs. local-ai-chat-fron
|
|
|
40
40
|
|
|
41
41
|
### User Experience
|
|
42
42
|
|
|
43
|
-
- **Client-side storage** —
|
|
43
|
+
- **Client-side storage** — Settings and API keys are kept in this browser's IndexedDB (not encrypted). A key is sent only to the endpoint of its own provider; you can turn off sending it per provider, or turn off saving keys entirely on shared devices.
|
|
44
44
|
- **Markdown rendering** — AI responses are rendered as Markdown via `marked`, sanitized with DOMPurify before display, supporting code blocks, lists, and formatting.
|
|
45
45
|
- **Performance metrics** — See tokens per second and timestamp for each AI response.
|
|
46
46
|
- **Conversation minimap** — A compact minimap beside the scrollbar shows all messages color-coded by sender. Click any block to jump to that message instantly.
|
|
@@ -53,7 +53,7 @@ Local LLM providers lack easy-to-deploy, cross-platform GUIs. local-ai-chat-fron
|
|
|
53
53
|
|
|
54
54
|
### Deployment
|
|
55
55
|
|
|
56
|
-
- **npx one-liner** — Run instantly from GitHub without cloning: `npx
|
|
56
|
+
- **npx one-liner** — Run instantly from GitHub without cloning: `npx local-ai-chat-frontend`.
|
|
57
57
|
- **Docker-ready** — Production image serves the static build via nginx; a dev compose file is included for local development.
|
|
58
58
|
|
|
59
59
|
## Tech Stack
|
|
@@ -67,7 +67,7 @@ Local LLM providers lack easy-to-deploy, cross-platform GUIs. local-ai-chat-fron
|
|
|
67
67
|
| Markdown | marked + DOMPurify (sanitization) |
|
|
68
68
|
| Storage | IndexedDB (native) |
|
|
69
69
|
| i18n | i18next / react-i18next |
|
|
70
|
-
| Server | sirv
|
|
70
|
+
| Server | Node `http` + sirv (with security headers) / nginx (Docker) |
|
|
71
71
|
|
|
72
72
|
### Key Technologies
|
|
73
73
|
|
|
@@ -82,7 +82,7 @@ Local LLM providers lack easy-to-deploy, cross-platform GUIs. local-ai-chat-fron
|
|
|
82
82
|
### Run instantly (no install)
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
npx
|
|
85
|
+
npx local-ai-chat-frontend
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
> `bunx` does not reliably build `devDependencies` from a raw git ref, so `npx` is the recommended way to run this project directly from GitHub. If you use Bun, clone the repo and run `bun install && bun run build && bun start` instead.
|
|
@@ -90,8 +90,8 @@ npx github:hidao80/local-ai-chat-frontend
|
|
|
90
90
|
### Custom port / LAN access
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
PORT=8080 npx
|
|
94
|
-
HOST=0.0.0.0 PORT=3000 npx
|
|
93
|
+
PORT=8080 npx local-ai-chat-frontend
|
|
94
|
+
HOST=0.0.0.0 PORT=3000 npx local-ai-chat-frontend
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
### Run with Docker
|
|
@@ -145,7 +145,7 @@ The application supports multiple LLM providers with automatic model discovery:
|
|
|
145
145
|
|
|
146
146
|
### CORS Configuration
|
|
147
147
|
|
|
148
|
-
**GPT4ALL**:
|
|
148
|
+
**GPT4ALL**: GPT4ALL has no CORS support, so the app reaches it through the Vite dev proxy (`bun run dev`). The production server (`npx` / `bun start`) and the Docker image do not proxy GPT4ALL.
|
|
149
149
|
|
|
150
150
|
**Ollama**: Set environment variable before starting:
|
|
151
151
|
```bash
|
|
@@ -170,7 +170,7 @@ bun run build
|
|
|
170
170
|
### Type Check
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
bunx tsc
|
|
173
|
+
bunx tsc -b
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
### Project Structure
|
|
@@ -178,7 +178,12 @@ bunx tsc --noEmit
|
|
|
178
178
|
```
|
|
179
179
|
src/
|
|
180
180
|
├── components/
|
|
181
|
-
│
|
|
181
|
+
│ ├── Chat.tsx # Chat view (send / stream loop)
|
|
182
|
+
│ ├── ChatSidebar.tsx # Session history sidebar
|
|
183
|
+
│ ├── ConfirmModal.tsx # Confirmation dialog
|
|
184
|
+
│ ├── Minimap.tsx # Chat minimap
|
|
185
|
+
│ └── Settings.tsx # Connection / model settings
|
|
186
|
+
├── lib/ # Non-UI helpers (API calls, storage, rendering) + unit tests
|
|
182
187
|
├── locales/
|
|
183
188
|
│ ├── en.json # English translations
|
|
184
189
|
│ └── ja.json # Japanese translations
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src *; media-src 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'",
|
|
3
|
+
"X-Frame-Options": "DENY",
|
|
4
|
+
"X-Content-Type-Options": "nosniff",
|
|
5
|
+
"Referrer-Policy": "no-referrer",
|
|
6
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
|
7
|
+
"Cross-Origin-Opener-Policy": "same-origin"
|
|
8
|
+
}
|
package/bin/start.js
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
-
import { existsSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import { isIP } from 'net';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import sirv from 'sirv';
|
|
6
10
|
|
|
7
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
12
|
const rootDir = join(__dirname, '..');
|
|
9
13
|
const distDir = join(rootDir, 'dist');
|
|
10
14
|
|
|
15
|
+
// Validate environment variables before use (exit before starting on invalid values)
|
|
16
|
+
const PORT = process.env.PORT || '3000';
|
|
17
|
+
const HOST = process.env.HOST || 'localhost';
|
|
18
|
+
const HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
|
|
19
|
+
|
|
20
|
+
if (!/^\d{1,5}$/.test(PORT) || Number(PORT) < 1 || Number(PORT) > 65535) {
|
|
21
|
+
console.error(`Invalid PORT: ${JSON.stringify(PORT)} (expected 1-65535)`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (!isIP(HOST) && !HOSTNAME.test(HOST)) {
|
|
25
|
+
console.error(`Invalid HOST: ${JSON.stringify(HOST)} (expected a hostname or IP address)`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
if (!existsSync(join(distDir, 'index.html'))) {
|
|
12
30
|
console.log(' dist/ not found, building (first run)...');
|
|
13
|
-
|
|
31
|
+
// Run the bundled vite package directly with node, without a shell
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const viteBin = join(dirname(require.resolve('vite/package.json')), 'bin', 'vite.js');
|
|
34
|
+
const build = spawnSync(process.execPath, [viteBin, 'build'], { cwd: rootDir, stdio: 'inherit' });
|
|
14
35
|
if (build.status !== 0) {
|
|
15
36
|
console.error('Build failed.');
|
|
16
37
|
process.exit(build.status || 1);
|
|
17
38
|
}
|
|
18
39
|
}
|
|
19
40
|
|
|
20
|
-
const PORT = process.env.PORT || '3000';
|
|
21
|
-
const HOST = process.env.HOST || 'localhost';
|
|
22
|
-
|
|
23
41
|
console.log('');
|
|
24
42
|
console.log(' local-ai-chat-frontend');
|
|
25
43
|
console.log(' ====================================');
|
|
@@ -35,24 +53,33 @@ console.log(' PORT=8080 npx https://github.com/hidao80/local-ai-chat-frontend
|
|
|
35
53
|
console.log(' HOST=0.0.0.0 PORT=3000 npx https://github.com/hidao80/local-ai-chat-frontend (LAN access)');
|
|
36
54
|
console.log('');
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{ stdio: 'inherit', shell: true }
|
|
56
|
+
// Add security headers to every response (same values as nginx.conf)
|
|
57
|
+
const securityHeaders = JSON.parse(
|
|
58
|
+
readFileSync(join(__dirname, 'security-headers.json'), 'utf8')
|
|
42
59
|
);
|
|
60
|
+
// single: unknown paths return index.html because this is an SPA
|
|
61
|
+
const serve = sirv(distDir, { single: true, etag: true });
|
|
62
|
+
|
|
63
|
+
const server = createServer((req, res) => {
|
|
64
|
+
for (const [name, value] of Object.entries(securityHeaders)) {
|
|
65
|
+
res.setHeader(name, value);
|
|
66
|
+
}
|
|
67
|
+
serve(req, res, () => {
|
|
68
|
+
res.statusCode = 404;
|
|
69
|
+
res.end('Not Found');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
43
72
|
|
|
44
|
-
|
|
73
|
+
server.on('error', (err) => {
|
|
45
74
|
console.error('Failed to start server:', err.message);
|
|
46
75
|
process.exit(1);
|
|
47
76
|
});
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
console.log('\n Shutting down...');
|
|
51
|
-
sirv.kill('SIGINT');
|
|
52
|
-
process.exit(0);
|
|
53
|
-
});
|
|
78
|
+
server.listen(Number(PORT), HOST);
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
80
|
+
const shutdown = () => {
|
|
81
|
+
console.log('\n Shutting down...');
|
|
82
|
+
server.close(() => process.exit(0));
|
|
83
|
+
};
|
|
84
|
+
process.on('SIGINT', shutdown);
|
|
85
|
+
process.on('SIGTERM', shutdown);
|