Implemented backend support for a compendium feature, including database schema updates and frontend integration.

This commit is contained in:
aomukai 2025-11-08 01:45:12 +09:00
parent 45fadb2734
commit d4a14fd6f2
4 changed files with 162 additions and 0 deletions

View file

@ -12,6 +12,7 @@
<script defer src="src/ai.js"></script>
<script defer src="src/save-utils.js"></script>
<script defer src="src/app.js"></script>
<script defer src="src/compendium.js"></script>
<!-- Local vendor Alpine (use a real alpine.min.js here for offline runs) -->
<script defer src="src/vendor/alpine.min.js"></script>

View file

@ -133,6 +133,13 @@ db.version(3).stores({
// noop migration for now; existing installs will get empty prompts/codex
});
// Add compendium table (v4)
db.version(4).stores({
compendium: 'id, projectId, category, title, modified, tags'
}).upgrade(async tx => {
// noop migration; new installs will get empty compendium
});
// Alpine.js App
document.addEventListener('alpine:init', () => {
Alpine.data('app', () => ({

90
src/compendium.js Normal file
View file

@ -0,0 +1,90 @@
// Simple Compendium module backed by Dexie `db` defined in app.js
(function () {
try {
const Compendium = {
async createEntry(projectId, { category = 'lore', title = '', body = '', tags = [] } = {}) {
const id = Date.now().toString() + '-' + Math.random().toString(36).slice(2, 8);
const now = new Date();
const entry = { id, projectId, category, title, body, summary: '', tags: (tags || []).slice(0, 10), created: now, modified: now, order: 0 };
await db.compendium.add(entry);
return entry;
},
async updateEntry(id, updates) {
updates.modified = new Date();
await db.compendium.update(id, updates);
return db.compendium.get(id);
},
async deleteEntry(id) {
await db.compendium.delete(id);
},
async getEntry(id) {
return db.compendium.get(id);
},
async listByCategory(projectId, category) {
return db.compendium.where({ projectId, category }).sortBy('order');
},
async search(projectId, q, options = {}) {
q = (q || '').trim().toLowerCase();
const limit = options.limit || 20;
if (!q) {
return (await db.compendium.where('projectId').equals(projectId).limit(limit).toArray()) || [];
}
const all = await db.compendium.where('projectId').equals(projectId).toArray();
const results = all.filter(e => {
const hay = ((e.title || '') + '\n' + (e.tags || []).join(' ') + '\n' + (e.body || '')).toLowerCase();
return hay.indexOf(q) !== -1;
}).slice(0, limit);
return results;
},
async summaries(projectId, ids = []) {
const out = {};
for (const id of ids) {
const e = await db.compendium.get(id);
if (!e) continue;
if (e.summary && e.summary.length > 10) out[id] = e.summary;
else out[id] = (e.body || '').slice(0, 300) + ((e.body || '').length > 300 ? '…' : '');
}
return out;
},
async export(projectId) {
return db.compendium.where('projectId').equals(projectId).toArray();
},
async import(projectId, items = [], opts = { merge: true }) {
const added = [];
for (const it of items) {
const entry = Object.assign({}, it);
entry.projectId = projectId;
entry.id = entry.id || (Date.now().toString() + '-' + Math.random().toString(36).slice(2, 6));
entry.created = entry.created ? new Date(entry.created) : new Date();
entry.modified = new Date();
await db.compendium.put(entry);
added.push(entry);
}
return added;
}
};
window.Compendium = Compendium;
// Test helper
window.__test = window.__test || {};
window.__test.seedCompendium = async function (projectId, entries) {
const created = [];
for (const e of (entries || [])) {
const en = await Compendium.createEntry(projectId, e);
created.push(en);
}
return created;
};
} catch (e) {
console.warn('Failed to attach Compendium module:', e && e.message ? e.message : e);
}
})();

64
tests/unit-compendium.js Normal file
View file

@ -0,0 +1,64 @@
const { chromium } = require('playwright');
const path = require('path');
(async () => {
const projectRoot = path.resolve(__dirname, '..');
const fileUrl = process.env.APP_URL || ('file:///' + path.join(projectRoot, 'main.html').replace(/\\/g, '/'));
console.log('Opening:', fileUrl);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
page.on('console', msg => { try { console.log(`PAGE LOG [${msg.type()}] ${msg.text()}`); } catch (e) { } });
page.on('pageerror', err => { console.error('PAGE ERROR', err && err.message ? err.message : err); });
try {
await page.goto(fileUrl, { waitUntil: 'load', timeout: 15000 });
await page.waitForSelector('.app-container, .welcome-screen', { timeout: 10000 });
// Seed project
const proj = await page.evaluate(async () => await window.__test.seedProject('CompTest'));
// Seed compendium entries via API
const entries = await page.evaluate(async (pid) => {
return await window.__test.seedCompendium(pid, [
{ category: 'character', title: 'Alice', body: 'Alice is a cautious detective.' },
{ category: 'location', title: 'Old Harbor', body: 'The Old Harbor smells of salt and diesel.' }
]);
}, proj.id);
if (!Array.isArray(entries) || entries.length < 2) {
console.error('Compendium seed failed:', entries);
await browser.close();
process.exit(2);
}
// Test search
const search = await page.evaluate(async (pid) => {
return await window.Compendium.search(pid, 'Alice');
}, proj.id);
if (!(Array.isArray(search) && search.length >= 1 && search[0].title === 'Alice')) {
console.error('Compendium search failed:', search);
await browser.close();
process.exit(3);
}
// Test getEntry
const fetched = await page.evaluate(async (id) => await window.Compendium.getEntry(id), entries[0].id);
if (!fetched || fetched.title !== 'Alice') {
console.error('Compendium getEntry failed:', fetched);
await browser.close();
process.exit(4);
}
console.log('unit-compendium test passed.');
await browser.close();
process.exit(0);
} catch (err) {
console.error('unit-compendium test failed:', err && err.message ? err.message : err);
await browser.close();
process.exit(1);
}
})();