feat: add knowledge graph component and persistent handbook sync state collection

This commit is contained in:
RaymondVerhoef
2026-05-18 21:13:17 +02:00
parent 9f3e1113a6
commit f35550f270
3 changed files with 191 additions and 16 deletions

View File

@@ -0,0 +1,90 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_path123",
"max": 0,
"min": 0,
"name": "path",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_sha123",
"max": 0,
"min": 0,
"name": "sha",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_handbooksync123",
"indexes": [
"CREATE UNIQUE INDEX `idx_handbook_path` ON `handbook_sync_state` (`path`)"
],
"listRule": "",
"name": "handbook_sync_state",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_handbooksync123");
return app.delete(collection);
})

View File

@@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon } from 'lucide-react';
import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon, Github } from 'lucide-react';
import * as db from '../../lib/db';
import { anthropicApi } from '../../lib/api';
import { getRepoFolder } from '../../lib/giteaService';
import Button from '../ui/Button';
import SuggestionsQueue from './SuggestionsQueue';
@@ -17,6 +18,10 @@ const KnowledgeGraph = () => {
const [analyzeError, setAnalyzeError] = useState(null);
const [newRelData, setNewRelData] = useState({ target: '', type: 'related_to' });
const [isSyncing, setIsSyncing] = useState(false);
const [syncResult, setSyncResult] = useState(null);
const [syncError, setSyncError] = useState(null);
const [topics, setTopics] = useState([]);
const [relations, setRelations] = useState([]);
@@ -191,6 +196,38 @@ const KnowledgeGraph = () => {
)));
};
const syncHandbook = async () => {
setIsSyncing(true);
setSyncError(null);
setSyncResult(null);
try {
const files = await getRepoFolder('respellion', 'employee-handbook', 'docs');
const syncStates = await db.getHandbookSyncStates();
const added = [];
const modified = [];
const unchanged = [];
files.forEach(file => {
const state = syncStates.find(s => s.path === file.path);
if (!state) {
added.push(file);
} else if (state.sha !== file.sha) {
modified.push(file);
} else {
unchanged.push(file);
}
});
setSyncResult({ added, modified, unchanged });
} catch (e) {
setSyncError(e.message || 'Failed to check GitHub for updates.');
} finally {
setIsSyncing(false);
}
};
const analyzeGraph = async () => {
if (!confirm('This will send the entire graph to the AI to evaluate content, merge duplicates, and create new logical relations. This may take a moment. Proceed?')) return;
@@ -281,7 +318,8 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
</div>
<div className="w-full md:w-80 bg-paper p-6 overflow-y-auto border-t md:border-t-0 border-bg-warm flex flex-col">
<div className="mb-6 pb-4 border-b border-bg-warm">
<div className="mb-6 pb-4 border-b border-bg-warm space-y-3">
<div>
<Button
onClick={analyzeGraph}
disabled={isAnalyzing || topics.length === 0}
@@ -298,6 +336,36 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
)}
</div>
<div>
<Button
onClick={syncHandbook}
disabled={isSyncing}
variant="outline"
className="w-full flex justify-center items-center gap-2"
>
<Github size={16} className={isSyncing ? "animate-pulse" : ""} />
{isSyncing ? 'Checking for updates...' : 'Sync Employee Handbook'}
</Button>
{syncError && (
<p className="mt-2 text-xs text-red-600 flex items-start gap-1">
<AlertCircle size={14} className="shrink-0 mt-0.5" />
{syncError}
</p>
)}
{syncResult && (
<div className="mt-2 text-xs text-fg-muted space-y-1 p-2 bg-bg rounded">
<p className="font-medium text-fg">GitHub Sync Status:</p>
<p>Added files: {syncResult.added.length}</p>
<p>Modified files: {syncResult.modified.length}</p>
<p>Unchanged: {syncResult.unchanged.length}</p>
{(syncResult.added.length > 0 || syncResult.modified.length > 0) && (
<p className="mt-2 italic text-teal">Ready for Phase 2 implementation.</p>
)}
</div>
)}
</div>
</div>
{/* R42 chatbot suggestions queue */}
<div className="mb-6 pb-4 border-b border-bg-warm">
<SuggestionsQueue onApplied={reloadKb} />

View File

@@ -308,3 +308,20 @@ export async function bulkSetCurriculum(year, weeks) {
);
}
// ── Handbook Sync State ───────────────────────────────────────────────────────
export async function getHandbookSyncStates() {
try {
return await pb.collection('handbook_sync_state').getFullList();
} catch { return []; }
}
export async function updateHandbookSyncState(path, sha) {
try {
const r = await pb.collection('handbook_sync_state').getFirstListItem(`path="${path}"`);
return await pb.collection('handbook_sync_state').update(r.id, { sha });
} catch {
return await pb.collection('handbook_sync_state').create({ path, sha });
}
}