UI enhancements, i18n, history drawer, and requested features
This commit is contained in:
+102
-4
@@ -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/')) {
|
||||
|
||||
Reference in New Issue
Block a user