feat: implement interactive Knowledge Graph visualization with AI-driven content analysis and handbook synchronization tools
This commit is contained in:
@@ -3,7 +3,8 @@ 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 } 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 { analyzeHandbookDelta } from '../../lib/extractionPipeline';
|
||||||
|
import { getRepoFolder, getFileContent } from '../../lib/giteaService';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import SuggestionsQueue from './SuggestionsQueue';
|
import SuggestionsQueue from './SuggestionsQueue';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ const KnowledgeGraph = () => {
|
|||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [syncResult, setSyncResult] = useState(null);
|
const [syncResult, setSyncResult] = useState(null);
|
||||||
const [syncError, setSyncError] = useState(null);
|
const [syncError, setSyncError] = useState(null);
|
||||||
|
const [syncProgress, setSyncProgress] = useState(null);
|
||||||
|
|
||||||
const [topics, setTopics] = useState([]);
|
const [topics, setTopics] = useState([]);
|
||||||
const [relations, setRelations] = useState([]);
|
const [relations, setRelations] = useState([]);
|
||||||
@@ -221,10 +223,32 @@ const KnowledgeGraph = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setSyncResult({ added, modified, unchanged });
|
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) {
|
} catch (e) {
|
||||||
setSyncError(e.message || 'Failed to check GitHub for updates.');
|
setSyncError(e.message || 'Failed to check GitHub for updates.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSyncing(false);
|
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
|
|||||||
<p>Added files: {syncResult.added.length}</p>
|
<p>Added files: {syncResult.added.length}</p>
|
||||||
<p>Modified files: {syncResult.modified.length}</p>
|
<p>Modified files: {syncResult.modified.length}</p>
|
||||||
<p>Unchanged: {syncResult.unchanged.length}</p>
|
<p>Unchanged: {syncResult.unchanged.length}</p>
|
||||||
{(syncResult.added.length > 0 || syncResult.modified.length > 0) && (
|
</div>
|
||||||
<p className="mt-2 italic text-teal">Ready for Phase 2 implementation.</p>
|
|
||||||
)}
|
)}
|
||||||
|
{syncProgress && (
|
||||||
|
<div className="mt-2 text-xs text-teal p-2 bg-teal/10 border border-teal/20 rounded">
|
||||||
|
<p className="font-medium flex items-center gap-2">
|
||||||
|
{syncProgress !== 'Sync Complete!' && <RefreshCw size={12} className="animate-spin" />}
|
||||||
|
{syncProgress}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,8 +24,48 @@ ALWAYS return a valid JSON object in the following format:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Return JSON only. No markdown blocks or other text.`;
|
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) {
|
export async function processSourceText(textContent, sourceName) {
|
||||||
// Deduplicate: skip if a source with the same name was already successfully processed
|
// Deduplicate: skip if a source with the same name was already successfully processed
|
||||||
const existing = await db.getSources();
|
const existing = await db.getSources();
|
||||||
@@ -71,7 +111,16 @@ async function mergeKnowledgeGraph(newData) {
|
|||||||
|
|
||||||
if (newData.topics && Array.isArray(newData.topics)) {
|
if (newData.topics && Array.isArray(newData.topics)) {
|
||||||
for (const t of 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);
|
topicsMap.set(t.id, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user