feat: implement R42 chat infrastructure with Anthropic API integration and custom design system
This commit is contained in:
117
src/lib/kbStore.js
Normal file
117
src/lib/kbStore.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user