diff --git a/pb_migrations/1780000000_created_handbook_sync_state.js b/pb_migrations/1780000000_created_handbook_sync_state.js new file mode 100644 index 0000000..23f7d0e --- /dev/null +++ b/pb_migrations/1780000000_created_handbook_sync_state.js @@ -0,0 +1,90 @@ +/// +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); +}) diff --git a/src/components/admin/KnowledgeGraph.jsx b/src/components/admin/KnowledgeGraph.jsx index 99a3488..cbdc98a 100644 --- a/src/components/admin/KnowledgeGraph.jsx +++ b/src/components/admin/KnowledgeGraph.jsx @@ -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
-
- - {analyzeError && ( -

- - {analyzeError} -

- )} +
+
+ + {analyzeError && ( +

+ + {analyzeError} +

+ )} +
+ +
+ + {syncError && ( +

+ + {syncError} +

+ )} + {syncResult && ( +
+

GitHub Sync Status:

+

Added files: {syncResult.added.length}

+

Modified files: {syncResult.modified.length}

+

Unchanged: {syncResult.unchanged.length}

+ {(syncResult.added.length > 0 || syncResult.modified.length > 0) && ( +

Ready for Phase 2 implementation.

+ )} +
+ )} +
{/* R42 chatbot suggestions queue */} diff --git a/src/lib/db.js b/src/lib/db.js index 74cc1e5..92ceff6 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -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 }); + } +} +