feat: add knowledge graph component and persistent handbook sync state collection
This commit is contained in:
90
pb_migrations/1780000000_created_handbook_sync_state.js
Normal file
90
pb_migrations/1780000000_created_handbook_sync_state.js
Normal 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);
|
||||
})
|
||||
@@ -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,21 +318,52 @@ 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">
|
||||
<Button
|
||||
onClick={analyzeGraph}
|
||||
disabled={isAnalyzing || topics.length === 0}
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
>
|
||||
<RefreshCw size={16} className={isAnalyzing ? "animate-spin" : ""} />
|
||||
{isAnalyzing ? 'Analyzing Graph...' : 'Analyze & Optimize Graph'}
|
||||
</Button>
|
||||
{analyzeError && (
|
||||
<p className="mt-2 text-xs text-red-600 flex items-start gap-1">
|
||||
<AlertCircle size={14} className="shrink-0 mt-0.5" />
|
||||
{analyzeError}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-6 pb-4 border-b border-bg-warm space-y-3">
|
||||
<div>
|
||||
<Button
|
||||
onClick={analyzeGraph}
|
||||
disabled={isAnalyzing || topics.length === 0}
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
>
|
||||
<RefreshCw size={16} className={isAnalyzing ? "animate-spin" : ""} />
|
||||
{isAnalyzing ? 'Analyzing Graph...' : 'Analyze & Optimize Graph'}
|
||||
</Button>
|
||||
{analyzeError && (
|
||||
<p className="mt-2 text-xs text-red-600 flex items-start gap-1">
|
||||
<AlertCircle size={14} className="shrink-0 mt-0.5" />
|
||||
{analyzeError}
|
||||
</p>
|
||||
)}
|
||||
</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 */}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user