import { storage } from './storage'; import * as db from './db'; /** * Knowledge-base mutations for the R42 chatbot path. * * Topics and relations live in PocketBase (via db.js). The suggestions queue * (pending / approved / rejected) is ephemeral and stored in localStorage * since there is no PocketBase collection for it. * * Every write dispatches `respellion:kb-updated` so the D3 graph and the * suggestions panel refresh without a page reload. */ function dispatchUpdated() { try { window.dispatchEvent(new CustomEvent('respellion:kb-updated')); } catch { // non-browser environment — no-op } } export const kbStore = { /** * Apply a validated delta directly to PocketBase topics + relations. * Skips topics/relations that already exist. */ async applyDelta(delta) { if (!delta) return { addedTopics: 0, addedRelations: 0 }; const [existingTopics, existingRelations] = await Promise.all([ db.getTopics(), db.getRelations(), ]); const existingIds = new Set(existingTopics.map(t => t.id)); const existingLabels = new Set(existingTopics.map(t => (t.label || '').toLowerCase())); let addedTopics = 0; for (const t of delta.topics || []) { if (existingIds.has(t.id) || existingLabels.has((t.label || '').toLowerCase())) continue; await db.upsertTopic({ id: t.id, label: t.label, type: t.type || 'concept', description: t.description || '', }); existingIds.add(t.id); existingLabels.add((t.label || '').toLowerCase()); addedTopics++; } let addedRelations = 0; const allIds = new Set([...existingTopics.map(t => t.id), ...(delta.topics || []).map(t => t.id)]); for (const r of delta.relations || []) { if (!allIds.has(r.source) || !allIds.has(r.target) || r.source === r.target) continue; const dup = existingRelations.some(x => { const sx = typeof x.source === 'object' ? x.source.id : x.source; const tx = typeof x.target === 'object' ? x.target.id : x.target; return sx === r.source && tx === r.target && x.type === r.type; }); if (dup) continue; await db.addRelation({ source: r.source, target: r.target, type: r.type }); addedRelations++; } dispatchUpdated(); return { addedTopics, addedRelations }; }, /** Queue a suggestion for admin review (stored in localStorage). */ appendSuggestion(suggestion) { const list = storage.get('kb:suggestions', []) || []; const entry = { id: `sug_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, ts: Date.now(), status: 'pending', reason: suggestion.reason || '', proposedBy: suggestion.proposedBy || null, proposedByName: suggestion.proposedByName || null, topics: suggestion.topics || [], relations: suggestion.relations || [], }; storage.set('kb:suggestions', [...list, entry]); dispatchUpdated(); return entry; }, listSuggestions(status) { const all = storage.get('kb:suggestions', []) || []; if (!status) return all; return all.filter(s => s.status === status); }, async approveSuggestion(id) { const all = storage.get('kb:suggestions', []) || []; const target = all.find(s => s.id === id); if (!target || target.status !== 'pending') return null; const result = await this.applyDelta(target); target.status = 'approved'; target.appliedAt = Date.now(); storage.set('kb:suggestions', all); dispatchUpdated(); return result; }, rejectSuggestion(id) { const all = storage.get('kb:suggestions', []) || []; const target = all.find(s => s.id === id); if (!target || target.status !== 'pending') return null; target.status = 'rejected'; target.rejectedAt = Date.now(); storage.set('kb:suggestions', all); dispatchUpdated(); return target; }, };