From d71caa41f697b01a5b99fbaacf56dfa7a480755b Mon Sep 17 00:00:00 2001 From: RaymondVerhoef Date: Mon, 18 May 2026 21:30:05 +0200 Subject: [PATCH] feat: implement interactive Knowledge Graph visualization with AI-driven content analysis and handbook synchronization tools --- src/components/admin/KnowledgeGraph.jsx | 37 ++++++++++++++++-- src/lib/extractionPipeline.js | 51 ++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/components/admin/KnowledgeGraph.jsx b/src/components/admin/KnowledgeGraph.jsx index 2142098..200b624 100644 --- a/src/components/admin/KnowledgeGraph.jsx +++ b/src/components/admin/KnowledgeGraph.jsx @@ -3,7 +3,8 @@ import * as d3 from 'd3'; import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon } from 'lucide-react'; import * as db from '../../lib/db'; import { anthropicApi } from '../../lib/api'; -import { getRepoFolder } from '../../lib/giteaService'; +import { analyzeHandbookDelta } from '../../lib/extractionPipeline'; +import { getRepoFolder, getFileContent } from '../../lib/giteaService'; import Button from '../ui/Button'; import SuggestionsQueue from './SuggestionsQueue'; @@ -21,6 +22,7 @@ const KnowledgeGraph = () => { const [isSyncing, setIsSyncing] = useState(false); const [syncResult, setSyncResult] = useState(null); const [syncError, setSyncError] = useState(null); + const [syncProgress, setSyncProgress] = useState(null); const [topics, setTopics] = useState([]); const [relations, setRelations] = useState([]); @@ -221,10 +223,32 @@ const KnowledgeGraph = () => { }); setSyncResult({ added, modified, unchanged }); + + const filesToProcess = [...added, ...modified]; + if (filesToProcess.length > 0) { + setSyncProgress(`Processing 0 of ${filesToProcess.length} files...`); + let count = 0; + for (const file of filesToProcess) { + count++; + setSyncProgress(`Processing ${count} of ${filesToProcess.length}: ${file.name}...`); + try { + const rawContent = await getFileContent('respellion', 'employee-handbook', file.path); + await analyzeHandbookDelta(rawContent, file.path); + await db.updateHandbookSyncState(file.path, file.sha); + } catch (err) { + console.error('Failed to process file:', file.path, err); + // We continue processing other files even if one fails + } + } + setSyncProgress('Sync Complete!'); + reloadKb(); + } + } catch (e) { setSyncError(e.message || 'Failed to check GitHub for updates.'); } finally { setIsSyncing(false); + setTimeout(() => setSyncProgress(null), 5000); // Clear progress after 5s } }; @@ -358,9 +382,14 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str

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.

- )} + + )} + {syncProgress && ( +
+

+ {syncProgress !== 'Sync Complete!' && } + {syncProgress} +

)} diff --git a/src/lib/extractionPipeline.js b/src/lib/extractionPipeline.js index bb16818..f2026c3 100644 --- a/src/lib/extractionPipeline.js +++ b/src/lib/extractionPipeline.js @@ -24,8 +24,48 @@ ALWAYS return a valid JSON object in the following format: } ] } +} Return JSON only. No markdown blocks or other text.`; +const HANDBOOK_SYSTEM_PROMPT = `You are analyzing an update to the Respellion Employee Handbook. +Your task is to identify changes and extract structural knowledge. + +CRITICAL INSTRUCTION: +You must explicitly identify and create relations between Roles, Processes, and Concepts. +Every Process must have a Role attached (who does it). +Every Concept must have a relation to a Process or Role. + +Return a JSON object: +{ + "topics": [ + { "id": "...", "label": "...", "type": "role | process | concept", "description": "...", "metadata": { "source": "github_handbook" } } + ], + "relations": [ + { "source": "role-id", "target": "process-id", "type": "executes | related_to | depends_on | part_of", "description": "Brief metadata about this specific relation" } + ] +} +Return JSON only. No markdown blocks or other text.`; + +export async function analyzeHandbookDelta(fileContent, filePath) { + try { + const responseText = await anthropicApi.generateContent(HANDBOOK_SYSTEM_PROMPT, `Analyze the following handbook file update (${filePath}):\n\n${fileContent}`); + + let extractedData; + try { + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + const jsonStr = jsonMatch ? jsonMatch[0] : responseText; + extractedData = JSON.parse(jsonStr); + } catch (e) { + console.error('[Pipeline] AI returned non-JSON response for handbook delta:', responseText?.substring(0, 500)); + throw new Error(`AI response was not valid JSON. The model responded with: "${responseText?.substring(0, 120)}..."`); + } + + await mergeKnowledgeGraph(extractedData); + return { success: true, data: extractedData }; + } catch (error) { + throw error; + } +} export async function processSourceText(textContent, sourceName) { // Deduplicate: skip if a source with the same name was already successfully processed const existing = await db.getSources(); @@ -71,7 +111,16 @@ async function mergeKnowledgeGraph(newData) { if (newData.topics && Array.isArray(newData.topics)) { for (const t of newData.topics) { - if (!topicsMap.has(t.id)) { + if (topicsMap.has(t.id)) { + // Upsert: merge new data into existing topic + const existing = topicsMap.get(t.id); + topicsMap.set(t.id, { + ...existing, + ...t, + // Keep existing description if new one is empty, or combine them if needed. Here we prefer the new one. + description: t.description || existing.description + }); + } else { topicsMap.set(t.id, t); } }