feat: add knowledge graph component and persistent handbook sync state collection
This commit is contained in:
@@ -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