Initial commit: Character sandbox with React+Express frontend/backend, SvelteKit foundation

This commit is contained in:
2026-06-30 16:57:28 +02:00
commit 85454f9737
48 changed files with 4662 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { authenticateToken } from '../middleware/auth.js';
import { processTrainingPrompt, generateCharacterSuggestion, DEFAULT_KNOWLEDGE } from '../ai/trainer.js';
const router = Router();
router.post('/suggest', authenticateToken, async (req, res) => {
const { prompt } = req.body;
if (!prompt) return res.status(400).json({ error: 'Prompt is required' });
const suggestion = await processTrainingPrompt(prompt, req.userId);
res.json({ suggestion });
});
router.post('/generate-name', authenticateToken, (req, res) => {
const { style } = req.body;
const name = generateCharacterSuggestion('generate name', req.userId).name;
res.json({ name });
});
router.post('/train', authenticateToken, (req, res) => {
const { prompt, response, category } = req.body;
if (!prompt || !response) return res.status(400).json({ error: 'Prompt and response are required' });
const id = uuidv4();
db.prepare('INSERT INTO ai_training_data (id, user_id, prompt, response, category) VALUES (?, ?, ?, ?, ?)')
.run(id, req.userId, prompt, response, category || 'general');
res.status(201).json({ message: 'Training data added', id });
});
router.get('/training-data', authenticateToken, (req, res) => {
const data = db.prepare('SELECT id, prompt, response, category, created_at FROM ai_training_data WHERE user_id = ? ORDER BY created_at DESC').all(req.userId);
res.json({ training_data: data });
});
router.delete('/training-data/:id', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM ai_training_data WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!existing) return res.status(404).json({ error: 'Training data not found' });
db.prepare('DELETE FROM ai_training_data WHERE id = ?').run(req.params.id);
res.json({ message: 'Training data deleted' });
});
router.get('/knowledge', authenticateToken, (req, res) => {
const archetypes = DEFAULT_KNOWLEDGE.character_archetypes.map(a => ({
name: a.name,
traits: a.traits,
needs_priority: a.needs_priority
}));
res.json({ archetypes, name_styles: Object.keys(DEFAULT_KNOWLEDGE.name_generators) });
});
export default router;
+49
View File
@@ -0,0 +1,49 @@
import { Router } from 'express';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { generateToken, authenticateToken } from '../middleware/auth.js';
const router = Router();
router.post('/register', (req, res) => {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ error: 'Username, email, and password are required' });
}
const existing = db.prepare('SELECT id FROM users WHERE username = ? OR email = ?').get(username, email);
if (existing) {
return res.status(409).json({ error: 'Username or email already exists' });
}
const id = uuidv4();
const passwordHash = bcrypt.hashSync(password, 10);
db.prepare('INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)').run(id, username, email, passwordHash);
const token = generateToken(id);
res.status(201).json({ token, user: { id, username, email } });
});
router.post('/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user.id);
res.json({ token, user: { id: user.id, username: user.username, email: user.email } });
});
router.get('/me', authenticateToken, (req, res) => {
const user = db.prepare('SELECT id, username, email, created_at FROM users WHERE id = ?').get(req.userId);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json({ user });
});
export default router;
+238
View File
@@ -0,0 +1,238 @@
import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { authenticateToken } from '../middleware/auth.js';
const router = Router();
router.get('/', authenticateToken, (req, res) => {
const characters = db.prepare('SELECT * FROM characters WHERE user_id = ? ORDER BY updated_at DESC').all(req.userId);
res.json({ characters });
});
router.post('/', authenticateToken, (req, res) => {
const { name, description, personality_traits, backstory } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const id = uuidv4();
const traits = JSON.stringify(personality_traits || []);
db.prepare('INSERT INTO characters (id, user_id, name, description, personality_traits, backstory) VALUES (?, ?, ?, ?, ?, ?)')
.run(id, req.userId, name, description || '', traits, backstory || '');
const character = db.prepare('SELECT * FROM characters WHERE id = ?').get(id);
res.status(201).json({ character });
});
router.get('/:id', authenticateToken, (req, res) => {
const character = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!character) return res.status(404).json({ error: 'Character not found' });
const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ?').all(req.params.id);
const uiElements = db.prepare('SELECT * FROM character_ui_elements WHERE character_id = ?').all(req.params.id);
const brainRules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ?').all(req.params.id);
res.json({ character, needs, ui_elements: uiElements, brain_rules: brainRules });
});
router.put('/:id', authenticateToken, (req, res) => {
const { name, description, personality_traits, backstory, avatar_url } = req.body;
const existing = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!existing) return res.status(404).json({ error: 'Character not found' });
db.prepare('UPDATE characters SET name = ?, description = ?, personality_traits = ?, backstory = ?, avatar_url = ?, updated_at = datetime(\'now\') WHERE id = ?')
.run(
name || existing.name,
description !== undefined ? description : existing.description,
personality_traits ? JSON.stringify(personality_traits) : existing.personality_traits,
backstory !== undefined ? backstory : existing.backstory,
avatar_url !== undefined ? avatar_url : existing.avatar_url,
req.params.id
);
const character = db.prepare('SELECT * FROM characters WHERE id = ?').get(req.params.id);
res.json({ character });
});
router.delete('/:id', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!existing) return res.status(404).json({ error: 'Character not found' });
db.prepare('DELETE FROM characters WHERE id = ?').run(req.params.id);
res.json({ message: 'Character deleted' });
});
// --- Needs ---
router.get('/:id/needs', authenticateToken, (req, res) => {
const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ?').all(req.params.id);
res.json({ needs });
});
router.post('/:id/needs', authenticateToken, (req, res) => {
const { name, enabled, initial_value, min_value, max_value, decay_rate, priority } = req.body;
if (!name) return res.status(400).json({ error: 'Need name is required' });
const needId = uuidv4();
db.prepare('INSERT INTO character_needs (id, character_id, name, enabled, initial_value, min_value, max_value, decay_rate, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
.run(needId, req.params.id, name, enabled !== false ? 1 : 0, initial_value || 50, min_value || 0, max_value || 100, decay_rate || 1, priority || 0);
const need = db.prepare('SELECT * FROM character_needs WHERE id = ?').get(needId);
res.status(201).json({ need });
});
router.put('/:id/needs/:needId', authenticateToken, (req, res) => {
const { name, enabled, initial_value, min_value, max_value, decay_rate, priority } = req.body;
const existing = db.prepare('SELECT * FROM character_needs WHERE id = ? AND character_id = ?').get(req.params.needId, req.params.id);
if (!existing) return res.status(404).json({ error: 'Need not found' });
db.prepare('UPDATE character_needs SET name = ?, enabled = ?, initial_value = ?, min_value = ?, max_value = ?, decay_rate = ?, priority = ? WHERE id = ?')
.run(name || existing.name, enabled !== undefined ? (enabled ? 1 : 0) : existing.enabled, initial_value ?? existing.initial_value, min_value ?? existing.min_value, max_value ?? existing.max_value, decay_rate ?? existing.decay_rate, priority ?? existing.priority, req.params.needId);
const need = db.prepare('SELECT * FROM character_needs WHERE id = ?').get(req.params.needId);
res.json({ need });
});
router.delete('/:id/needs/:needId', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM character_needs WHERE id = ? AND character_id = ?').get(req.params.needId, req.params.id);
if (!existing) return res.status(404).json({ error: 'Need not found' });
db.prepare('DELETE FROM character_needs WHERE id = ?').run(req.params.needId);
res.json({ message: 'Need deleted' });
});
// --- UI Elements ---
router.get('/:id/ui-elements', authenticateToken, (req, res) => {
const elements = db.prepare('SELECT * FROM character_ui_elements WHERE character_id = ?').all(req.params.id);
res.json({ ui_elements: elements });
});
router.post('/:id/ui-elements', authenticateToken, (req, res) => {
const { need_id, element_type, config } = req.body;
if (!element_type) return res.status(400).json({ error: 'Element type is required' });
const elementId = uuidv4();
db.prepare('INSERT INTO character_ui_elements (id, character_id, need_id, element_type, config) VALUES (?, ?, ?, ?, ?)')
.run(elementId, req.params.id, need_id || null, element_type, JSON.stringify(config || {}));
const element = db.prepare('SELECT * FROM character_ui_elements WHERE id = ?').get(elementId);
res.status(201).json({ ui_element: element });
});
router.put('/:id/ui-elements/:elementId', authenticateToken, (req, res) => {
const { need_id, element_type, config } = req.body;
const existing = db.prepare('SELECT * FROM character_ui_elements WHERE id = ? AND character_id = ?').get(req.params.elementId, req.params.id);
if (!existing) return res.status(404).json({ error: 'UI element not found' });
db.prepare('UPDATE character_ui_elements SET need_id = ?, element_type = ?, config = ? WHERE id = ?')
.run(need_id !== undefined ? need_id : existing.need_id, element_type || existing.element_type, config ? JSON.stringify(config) : existing.config, req.params.elementId);
const element = db.prepare('SELECT * FROM character_ui_elements WHERE id = ?').get(req.params.elementId);
res.json({ ui_element: element });
});
router.delete('/:id/ui-elements/:elementId', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM character_ui_elements WHERE id = ? AND character_id = ?').get(req.params.elementId, req.params.id);
if (!existing) return res.status(404).json({ error: 'UI element not found' });
db.prepare('DELETE FROM character_ui_elements WHERE id = ?').run(req.params.elementId);
res.json({ message: 'UI element deleted' });
});
// --- Brain Rules ---
router.get('/:id/brain-rules', authenticateToken, (req, res) => {
const rules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ?').all(req.params.id);
res.json({ brain_rules: rules });
});
router.post('/:id/brain-rules', authenticateToken, (req, res) => {
const { condition, action, priority, enabled } = req.body;
if (!condition || !action) return res.status(400).json({ error: 'Condition and action are required' });
const ruleId = uuidv4();
db.prepare('INSERT INTO character_brain_rules (id, character_id, condition, action, priority, enabled) VALUES (?, ?, ?, ?, ?, ?)')
.run(ruleId, req.params.id, JSON.stringify(condition), JSON.stringify(action), priority || 0, enabled !== false ? 1 : 0);
const rule = db.prepare('SELECT * FROM character_brain_rules WHERE id = ?').get(ruleId);
res.status(201).json({ brain_rule: rule });
});
router.put('/:id/brain-rules/:ruleId', authenticateToken, (req, res) => {
const { condition, action, priority, enabled } = req.body;
const existing = db.prepare('SELECT * FROM character_brain_rules WHERE id = ? AND character_id = ?').get(req.params.ruleId, req.params.id);
if (!existing) return res.status(404).json({ error: 'Brain rule not found' });
db.prepare('UPDATE character_brain_rules SET condition = ?, action = ?, priority = ?, enabled = ? WHERE id = ?')
.run(condition ? JSON.stringify(condition) : existing.condition, action ? JSON.stringify(action) : existing.action, priority ?? existing.priority, enabled !== undefined ? (enabled ? 1 : 0) : existing.enabled, req.params.ruleId);
const rule = db.prepare('SELECT * FROM character_brain_rules WHERE id = ?').get(req.params.ruleId);
res.json({ brain_rule: rule });
});
router.delete('/:id/brain-rules/:ruleId', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM character_brain_rules WHERE id = ? AND character_id = ?').get(req.params.ruleId, req.params.id);
if (!existing) return res.status(404).json({ error: 'Brain rule not found' });
db.prepare('DELETE FROM character_brain_rules WHERE id = ?').run(req.params.ruleId);
res.json({ message: 'Brain rule deleted' });
});
// --- Simulation ---
router.post('/:id/simulate', authenticateToken, (req, res) => {
const { steps = 10, events = [] } = req.body;
const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ? AND enabled = 1').all(req.params.id);
const rules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ? AND enabled = 1 ORDER BY priority DESC').all(req.params.id);
const simulation = [];
let currentValues = {};
needs.forEach(n => { currentValues[n.name] = { ...n }; });
for (let step = 0; step < steps; step++) {
for (const need of needs) {
if (currentValues[need.name]) {
let newVal = currentValues[need.name].current_value || need.initial_value;
newVal -= need.decay_rate;
newVal = Math.max(need.min_value, Math.min(need.max_value, newVal));
currentValues[need.name].current_value = newVal;
}
}
for (const event of events) {
for (const need of needs) {
if (event[need.name]) {
let val = currentValues[need.name].current_value || need.initial_value;
val += event[need.name];
val = Math.max(need.min_value, Math.min(need.max_value, val));
currentValues[need.name].current_value = val;
}
}
}
const triggeredRules = [];
for (const rule of rules) {
const cond = JSON.parse(rule.condition);
const needVal = currentValues[cond.need]?.current_value ?? need.initial_value;
if (cond.operator === 'lt' && needVal < cond.value) {
triggeredRules.push(rule);
} else if (cond.operator === 'gt' && needVal > cond.value) {
triggeredRules.push(rule);
} else if (cond.operator === 'eq' && needVal === cond.value) {
triggeredRules.push(rule);
} else if (cond.operator === 'lte' && needVal <= cond.value) {
triggeredRules.push(rule);
} else if (cond.operator === 'gte' && needVal >= cond.value) {
triggeredRules.push(rule);
}
}
const snapshot = {};
for (const need of needs) {
snapshot[need.name] = Math.round((currentValues[need.name].current_value || need.initial_value) * 100) / 100;
}
simulation.push({ step, values: snapshot, triggered_rules: triggeredRules.map(r => ({ id: r.id, action: JSON.parse(r.action) })) });
}
res.json({ simulation, final_values: simulation[simulation.length - 1]?.values });
});
export default router;
+84
View File
@@ -0,0 +1,84 @@
import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { authenticateToken } from '../middleware/auth.js';
const router = Router();
router.get('/search', authenticateToken, (req, res) => {
const { q, tag } = req.query;
let query = `SELECT f.*, l.name as lorebook_name FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id WHERE l.user_id = ?`;
const params = [req.userId];
if (q) {
query += ` AND (f.title LIKE ? OR f.content LIKE ?)`;
params.push(`%${q}%`, `%${q}%`);
}
if (tag) {
query += ` AND f.tags LIKE ?`;
params.push(`%"${tag}"%`);
}
query += ` ORDER BY f.updated_at DESC LIMIT 50`;
const fragments = db.prepare(query).all(...params);
res.json({ fragments });
});
router.get('/:id', authenticateToken, (req, res) => {
const fragment = db.prepare(`
SELECT f.*, l.name as lorebook_name, l.user_id
FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id
WHERE f.id = ? AND l.user_id = ?
`).get(req.params.id, req.userId);
if (!fragment) return res.status(404).json({ error: 'Fragment not found' });
res.json({ fragment });
});
router.post('/lorebook/:lorebookId', authenticateToken, (req, res) => {
const { title, content, tags, linked_characters } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.lorebookId, req.userId);
if (!lorebook) return res.status(404).json({ error: 'Lorebook not found' });
const id = uuidv4();
db.prepare('INSERT INTO fragments (id, lorebook_id, title, content, tags, linked_characters) VALUES (?, ?, ?, ?, ?, ?)')
.run(id, req.params.lorebookId, title, content || '', JSON.stringify(tags || []), JSON.stringify(linked_characters || []));
const fragment = db.prepare('SELECT * FROM fragments WHERE id = ?').get(id);
res.status(201).json({ fragment });
});
router.put('/:id', authenticateToken, (req, res) => {
const { title, content, tags, linked_characters } = req.body;
const fragment = db.prepare(`
SELECT f.* FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id
WHERE f.id = ? AND l.user_id = ?
`).get(req.params.id, req.userId);
if (!fragment) return res.status(404).json({ error: 'Fragment not found' });
db.prepare('UPDATE fragments SET title = ?, content = ?, tags = ?, linked_characters = ?, updated_at = datetime(\'now\') WHERE id = ?')
.run(
title || fragment.title,
content !== undefined ? content : fragment.content,
tags ? JSON.stringify(tags) : fragment.tags,
linked_characters ? JSON.stringify(linked_characters) : fragment.linked_characters,
req.params.id
);
const updated = db.prepare('SELECT * FROM fragments WHERE id = ?').get(req.params.id);
res.json({ fragment: updated });
});
router.delete('/:id', authenticateToken, (req, res) => {
const fragment = db.prepare(`
SELECT f.* FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id
WHERE f.id = ? AND l.user_id = ?
`).get(req.params.id, req.userId);
if (!fragment) return res.status(404).json({ error: 'Fragment not found' });
db.prepare('DELETE FROM fragments WHERE id = ?').run(req.params.id);
res.json({ message: 'Fragment deleted' });
});
export default router;
+51
View File
@@ -0,0 +1,51 @@
import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { authenticateToken } from '../middleware/auth.js';
const router = Router();
router.get('/', authenticateToken, (req, res) => {
const lorebooks = db.prepare('SELECT * FROM lorebooks WHERE user_id = ? ORDER BY updated_at DESC').all(req.userId);
res.json({ lorebooks });
});
router.post('/', authenticateToken, (req, res) => {
const { name, description } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const id = uuidv4();
db.prepare('INSERT INTO lorebooks (id, user_id, name, description) VALUES (?, ?, ?, ?)').run(id, req.userId, name, description || '');
const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ?').get(id);
res.status(201).json({ lorebook });
});
router.get('/:id', authenticateToken, (req, res) => {
const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!lorebook) return res.status(404).json({ error: 'Lorebook not found' });
const fragments = db.prepare('SELECT * FROM fragments WHERE lorebook_id = ? ORDER BY created_at DESC').all(req.params.id);
res.json({ lorebook, fragments });
});
router.put('/:id', authenticateToken, (req, res) => {
const { name, description } = req.body;
const existing = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!existing) return res.status(404).json({ error: 'Lorebook not found' });
db.prepare('UPDATE lorebooks SET name = ?, description = ?, updated_at = datetime(\'now\') WHERE id = ?')
.run(name || existing.name, description !== undefined ? description : existing.description, req.params.id);
const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ?').get(req.params.id);
res.json({ lorebook });
});
router.delete('/:id', authenticateToken, (req, res) => {
const existing = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
if (!existing) return res.status(404).json({ error: 'Lorebook not found' });
db.prepare('DELETE FROM lorebooks WHERE id = ?').run(req.params.id);
res.json({ message: 'Lorebook deleted' });
});
export default router;