Implemented backend support for a compendium feature, including database schema updates and frontend integration.
This commit is contained in:
parent
45fadb2734
commit
d4a14fd6f2
4 changed files with 162 additions and 0 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
90
src/compendium.js
Normal 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
64
tests/unit-compendium.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue