feat: enhance KnowledgeGraph with edge styles, node navigation, and AI analysis scopes; update GraphControls and NodeDetailPanel for improved relation management

This commit is contained in:
RaymondVerhoef
2026-05-27 21:09:05 +02:00
parent 47a738fde3
commit ce276f0296
3 changed files with 296 additions and 117 deletions

View File

@@ -1,13 +1,25 @@
import { RotateCcw, RefreshCw, AlertCircle } from 'lucide-react';
import { useState } from 'react';
import { RotateCcw, RefreshCw, AlertCircle, ChevronDown } from 'lucide-react';
import Button from '../../ui/Button';
import SuggestionsQueue from '../SuggestionsQueue';
const SCOPE_OPTIONS = [
{ value: 'full', label: 'Full Analysis', tier: 'Opus', hint: 'Merges · Deletions · Relations · Relevance' },
{ value: 'relations', label: 'Relations Only', tier: 'Sonnet', hint: 'Suggest missing logical connections' },
{ value: 'relevance', label: 'Relevance Only', tier: 'Sonnet', hint: 'Re-score learning_relevance for all unlocked topics' },
];
/**
* Sidebar controls panel: the exclude-nodes toggle, the AI analysis button,
* the snapshot restore row, and the R42 suggestions queue.
* Sidebar controls panel: the exclude-nodes toggle, the AI analysis button
* (with scope selector), the snapshot restore row, and the R42 suggestions queue.
*
* Kept intentionally thin — all async logic lives in the KnowledgeGraph
* orchestrator and is injected via props.
*
* The scope selector is local state here; it produces a scope string that is
* forwarded to onAnalyze(scope). Scope options map to different AI tiers:
* full → reasoning (Opus) — expensive full quality pass
* relations / relevance → standard (Sonnet) — targeted cheaper sub-task
*/
export default function GraphControls({
showExcludeNodes,
@@ -21,6 +33,10 @@ export default function GraphControls({
onRestore,
isRestoring,
}) {
const [scope, setScope] = useState('full');
const selectedOption = SCOPE_OPTIONS.find(o => o.value === scope) ?? SCOPE_OPTIONS[0];
const snapshotLabel = snapshotMeta
? new Date(snapshotMeta.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: null;
@@ -38,13 +54,35 @@ export default function GraphControls({
</label>
<div className="space-y-2">
{/* Scope selector */}
<div className="relative">
<select
value={scope}
onChange={e => setScope(e.target.value)}
disabled={isAnalyzing || isRestoring || disabled}
className="w-full border border-bg-warm rounded-[var(--r-sm)] pl-3 pr-8 py-1.5 text-xs bg-paper appearance-none disabled:opacity-40 disabled:cursor-not-allowed"
title={selectedOption.hint}
>
{SCOPE_OPTIONS.map(o => (
<option key={o.value} value={o.value}>
{o.label} {o.tier}
</option>
))}
</select>
<ChevronDown
size={12}
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-fg-muted"
/>
</div>
<p className="text-xs text-fg-muted leading-snug">{selectedOption.hint}</p>
<Button
onClick={onAnalyze}
onClick={() => onAnalyze(scope)}
disabled={isAnalyzing || isRestoring || disabled}
className="w-full flex justify-center items-center gap-2"
>
<RefreshCw size={16} className={isAnalyzing ? 'animate-spin' : ''} />
{isAnalyzing ? 'Analyzing Graph…' : 'Analyze & Optimize Graph'}
{isAnalyzing ? 'Analyzing…' : 'Analyze & Optimize Graph'}
</Button>
{/* Restore row — only visible after a bulkSave has written a snapshot */}
@@ -58,7 +96,7 @@ export default function GraphControls({
<RotateCcw size={12} className={isRestoring ? 'animate-spin' : ''} />
{isRestoring
? 'Restoring…'
: `Restore to ${snapshotLabel} · ${snapshotMeta.topicCount}t ${snapshotMeta.relationCount}r`}
: `Restore to ${snapshotLabel} · ${snapshotMeta.topicCount}t ${snapshotMeta.relationCount}r`}
</button>
)}