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 React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import * as d3 from 'd3';
|
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 * as db from '../../lib/db';
|
||||||
import { anthropicApi } from '../../lib/api';
|
import { anthropicApi } from '../../lib/api';
|
||||||
|
import { getRepoFolder } from '../../lib/giteaService';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import SuggestionsQueue from './SuggestionsQueue';
|
import SuggestionsQueue from './SuggestionsQueue';
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ const KnowledgeGraph = () => {
|
|||||||
const [analyzeError, setAnalyzeError] = useState(null);
|
const [analyzeError, setAnalyzeError] = useState(null);
|
||||||
const [newRelData, setNewRelData] = useState({ target: '', type: 'related_to' });
|
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 [topics, setTopics] = useState([]);
|
||||||
const [relations, setRelations] = 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 () => {
|
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;
|
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>
|
||||||
|
|
||||||
<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="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
|
<Button
|
||||||
onClick={analyzeGraph}
|
onClick={analyzeGraph}
|
||||||
disabled={isAnalyzing || topics.length === 0}
|
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>
|
||||||
|
|
||||||
|
<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 */}
|
{/* R42 chatbot suggestions queue */}
|
||||||
<div className="mb-6 pb-4 border-b border-bg-warm">
|
<div className="mb-6 pb-4 border-b border-bg-warm">
|
||||||
<SuggestionsQueue onApplied={reloadKb} />
|
<SuggestionsQueue onApplied={reloadKb} />
|
||||||
|
|||||||
@@ -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