UI enhancements, i18n, history drawer, and requested features

This commit is contained in:
2026-06-04 00:01:32 +02:00
parent c3e167bde9
commit 3a2c738bd0
14 changed files with 598 additions and 288 deletions
+102 -4
View File
@@ -18,7 +18,7 @@
// (default: "*", fine for single-user personal use)
import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { readFile, writeFile, stat, mkdir } from 'node:fs/promises';
import { extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -36,6 +36,9 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
const DIST = join(__dirname, 'dist');
const PORT = parseInt(process.env.PORT || '3000', 10);
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
const DATA_DIR = process.env.DATA_DIR || join(__dirname, 'data');
const HISTORY_FILE = join(DATA_DIR, 'history.json');
const MAX_HISTORY = 500;
const LLM = {
endpoint: (process.env.LLM_ENDPOINT || '').replace(/\/+$/, ''),
@@ -52,10 +55,32 @@ if (!LLM.endpoint || !LLM.apiKey) {
process.exit(1);
}
// Ensure data directory exists
await mkdir(DATA_DIR, { recursive: true });
console.log(`[startup] LLM endpoint: ${LLM.endpoint}`);
console.log(`[startup] LLM model: ${LLM.model}`);
console.log(`[startup] Data dir: ${DATA_DIR}`);
console.log(`[startup] Listening on: http://0.0.0.0:${PORT}`);
// ──────────────────────────────────────────────────────────────────────────────
// History helpers
// ──────────────────────────────────────────────────────────────────────────────
async function readHistory() {
try {
const raw = await readFile(HISTORY_FILE, 'utf-8');
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
async function writeHistory(entries) {
await writeFile(HISTORY_FILE, JSON.stringify(entries, null, 2), 'utf-8');
}
// ──────────────────────────────────────────────────────────────────────────────
// JSON extraction + shape validation
// ──────────────────────────────────────────────────────────────────────────────
@@ -330,13 +355,18 @@ async function handleStyleRandom(req, res) {
catch (err) { return json(res, { error: err.message }, 400); }
const mode = body?.mode === 'crazy' ? 'crazy' : 'normal';
const idea = typeof body?.idea === 'string' ? body.idea.trim() : '';
const signal = combinedSignal(req);
const userMsg = idea
? `Generate one style description now. (User's idea: "${idea}")`
: 'Generate one style description now.';
try {
const content = await callProvider(
[
{ role: 'system', content: buildStylePrompt(mode) },
{ role: 'user', content: 'Generate one style description now.' },
{ role: 'system', content: buildStylePrompt(mode, idea) },
{ role: 'user', content: userMsg },
],
{ max_tokens: 120 },
signal,
@@ -346,13 +376,67 @@ async function handleStyleRandom(req, res) {
json(res, { style, mode });
} catch (err) {
if (err && (err.name === 'AbortError' || /client disconnected/i.test(err.message))) {
return; // Client gave up; no point responding.
return;
}
console.error('[style/random]', err);
json(res, { error: err.message || 'Failed to generate style' }, 500);
}
}
async function handleGetHistory(req, res) {
try {
const entries = await readHistory();
json(res, entries);
} catch (err) {
console.error('[history/get]', err);
json(res, { error: 'Failed to read history' }, 500);
}
}
async function handleAddHistory(req, res) {
let body;
try { body = await readJsonBody(req); }
catch (err) { return json(res, { error: err.message }, 400); }
if (!body || typeof body.id !== 'string' || !body.input || !body.assets) {
return json(res, { error: 'Invalid history entry' }, 400);
}
try {
const entries = await readHistory();
// Deduplicate by id
const filtered = entries.filter(e => e.id !== body.id);
const next = [body, ...filtered].slice(0, MAX_HISTORY);
await writeHistory(next);
json(res, { ok: true });
} catch (err) {
console.error('[history/add]', err);
json(res, { error: 'Failed to save history' }, 500);
}
}
async function handleDeleteHistory(req, res, id) {
try {
const entries = await readHistory();
const next = entries.filter(e => e.id !== id);
await writeHistory(next);
json(res, { ok: true });
} catch (err) {
console.error('[history/delete]', err);
json(res, { error: 'Failed to delete entry' }, 500);
}
}
async function handleClearHistory(req, res) {
try {
await writeHistory([]);
json(res, { ok: true });
} catch (err) {
console.error('[history/clear]', err);
json(res, { error: 'Failed to clear history' }, 500);
}
}
async function handleGenerate(req, res) {
let body;
try { body = await readJsonBody(req); }
@@ -430,6 +514,20 @@ const server = createServer(async (req, res) => {
if (req.method === 'POST' && req.url.split('?')[0] === '/api/style/random') {
return handleStyleRandom(req, res);
}
// History endpoints
if (req.method === 'GET' && req.url.split('?')[0] === '/api/history') {
return handleGetHistory(req, res);
}
if (req.method === 'POST' && req.url.split('?')[0] === '/api/history') {
return handleAddHistory(req, res);
}
if (req.method === 'DELETE' && req.url.split('?')[0] === '/api/history') {
return handleClearHistory(req, res);
}
const deleteMatch = req.method === 'DELETE' && req.url.match(/^\/api\/history\/([^/?]+)/);
if (deleteMatch) {
return handleDeleteHistory(req, res, decodeURIComponent(deleteMatch[1]));
}
// Anything under /api that didn't match is a 404 (don't fall through to the SPA).
if (req.url.startsWith('/api/')) {