GraphRAG Studio — initial commit: multimodal RAG system with KG visualization

Full-stack application for document-to-knowledge-graph pipeline:
- Backend: FastAPI + LangGraph ReAct agent + DeepSeek + MinerU parsing
- Frontend: React 19 + Vite + D3.js + shadcn/ui
- Pipeline: MinerU parsing → LangExtract entity extraction → KG building

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
plf
2026-06-07 17:30:04 +08:00
commit b02d3378fc
127 changed files with 37218 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { Menu, Search, X } from 'lucide-react';
import { useAppState, type KGNode } from '../../store';
import { api } from '../../api';
import { TYPE_COLORS } from '../../mock-data';
export function Header() {
const { sidebarCollapsed, setSidebarCollapsed, health } = useAppState();
const [query, setQuery] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const [suggestions, setSuggestions] = useState<KGNode[]>([]);
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
if (query.length >= 2) {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(async () => {
try {
const res = await api.searchEntities(query, undefined, 5);
setSuggestions(res.items.map(n => ({
id: n.id, name: n.name, type: n.type as KGNode['type'],
page: n.page, confidence: n.confidence as KGNode['confidence'],
degree: n.degree, centrality: 0, doc_id: n.doc_id,
})));
setShowSuggestions(true);
} catch {
setSuggestions([]);
}
}, 300);
} else {
setSuggestions([]);
setShowSuggestions(false);
}
return () => clearTimeout(timerRef.current);
}, [query]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
setShowSuggestions(false);
navigate(`/search?q=${encodeURIComponent(query)}`);
}
};
const allOk = Object.values(health).every(v => v === 'ok');
return (
<header
className="flex items-center px-4 gap-4"
style={{
gridArea: 'header',
height: 56,
background: 'var(--bg-s1)',
borderBottom: '1px solid var(--border-main)',
position: 'sticky',
top: 0,
zIndex: 100,
}}
>
{/* Left */}
<button
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1.5 rounded-md hover:opacity-80 cursor-pointer"
style={{ background: 'var(--bg-s2)', color: 'var(--text-3)' }}
aria-label="Toggle sidebar"
>
<Menu size={18} />
</button>
<span style={{ color: 'var(--blue)', fontSize: 16, fontWeight: 600, whiteSpace: 'nowrap' }}>
GraphRAG Studio
</span>
{/* Center - Search */}
<form onSubmit={handleSubmit} className="flex-1 flex justify-center relative" style={{ maxWidth: 400, margin: '0 auto' }}>
<div className="relative w-full">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--text-4)' }} />
<input
ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
onFocus={() => query.length >= 3 && setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
placeholder="搜索实体..."
className="w-full pl-9 pr-8 py-1.5 rounded-md outline-none"
style={{
background: 'var(--bg-s2)',
border: '1px solid var(--border-main)',
color: 'var(--text-1)',
fontSize: 13,
}}
/>
{query && (
<button type="button" onClick={() => { setQuery(''); setShowSuggestions(false); }} className="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer" style={{ color: 'var(--text-4)' }}>
<X size={14} />
</button>
)}
</div>
{showSuggestions && suggestions.length > 0 && (
<div
className="absolute top-full mt-1 w-full rounded-md overflow-hidden"
style={{ background: 'var(--bg-s3)', border: '1px solid var(--border-main)', boxShadow: 'var(--shadow-md)', zIndex: 200 }}
>
{suggestions.map(s => (
<button
key={s.id}
type="button"
className="w-full flex items-center gap-2 px-3 py-2 hover:opacity-80 cursor-pointer text-left"
style={{ background: 'transparent', borderBottom: '1px solid var(--border-muted)' }}
onMouseDown={() => {
setShowSuggestions(false);
setQuery('');
navigate(`/graph?node=${s.id}`);
}}
>
<span style={{ color: 'var(--text-1)', fontSize: 13 }}>{s.name}</span>
<span
className="px-1.5 py-0.5 rounded"
style={{
fontSize: 10, fontWeight: 600,
background: `${TYPE_COLORS[s.type]}20`,
color: TYPE_COLORS[s.type],
}}
>
{s.type}
</span>
</button>
))}
</div>
)}
</form>
{/* Right */}
<div className="flex items-center gap-2" style={{ whiteSpace: 'nowrap' }}>
<span
className="inline-block w-2 h-2 rounded-full"
style={{ background: allOk ? 'var(--green)' : 'var(--red)' }}
/>
<span style={{ color: 'var(--text-3)', fontSize: 12 }}>API: localhost:8000</span>
</div>
</header>
);
}