Software_Engineer_Agent / frontend /src /components /KnowledgeExplorer.jsx
G-Madhuri's picture
deploy to hf space
7e2f74d
Raw
History Blame Contribute Delete
19.9 kB
import React, { useState, useEffect } from 'react';
import {
Search, Database, MessageSquare, Zap, FileText,
ChevronRight, Clock, RefreshCw, Terminal,
Activity, BookOpen, Play, Layers, Shield, Globe
} from 'lucide-react';
import { apiUrl } from '../api';
const CATEGORY_OPTIONS = [
{ value: '', label: 'All Categories' },
{ value: 'report', label: 'Report' },
{ value: 'summary', label: 'Summary' },
{ value: 'architecture', label: 'Architecture' },
{ value: 'authentication', label: 'Authentication' },
{ value: 'api', label: 'API Endpoints' },
{ value: 'dependency', label: 'Dependencies' },
{ value: 'business_flow', label: 'Business Flows' },
{ value: 'concept', label: 'Concepts' },
{ value: 'file', label: 'Source Files' },
];
const TOOLS = [
{ name: 'repository_search', icon: <Search size={14} />, color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8', desc: 'Semantic search over indexed repo chunks' },
{ name: 'graph_query', icon: <Layers size={14} />, color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD', desc: 'Query architecture graph, entry points, and flows' },
{ name: 'dependency_lookup', icon: <Play size={14} />, color: '#D97706', bg: '#FFFBEB', border: '#FDE68A', desc: 'Lookup packages, frameworks, and databases' },
{ name: 'file_reader', icon: <FileText size={14} />, color: '#4338CA', bg: '#EEF2FF', border: '#C7D2FE', desc: 'Retrieve specific source file content' },
{ name: 'architecture_lookup', icon: <Activity size={14} />, color: '#059669', bg: '#ECFDF5', border: '#A7F3D0', desc: 'Query architecture pattern and key modules' },
{ name: 'api_lookup', icon: <Terminal size={14} />, color: '#E11D48', bg: '#FFF1F2', border: '#FECDD3', desc: 'Lookup HTTP routes and authentication methods' },
];
const CATEGORY_COLORS = {
report: { color: '#4338CA', bg: '#EEF2FF', border: '#C7D2FE' },
summary: { color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8' },
architecture: { color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD' },
authentication: { color: '#E11D48', bg: '#FFF1F2', border: '#FECDD3' },
api: { color: '#059669', bg: '#ECFDF5', border: '#A7F3D0' },
dependency: { color: '#D97706', bg: '#FFFBEB', border: '#FDE68A' },
business_flow: { color: '#2E9E9E', bg: '#E6F7F7', border: '#A8D8D8' },
concept: { color: '#7C3AED', bg: '#F5F3FF', border: '#C4B5FD' },
file: { color: '#475569', bg: '#F8FAFC', border: '#CBD5E1' },
};
const getCatStyle = (cat) => CATEGORY_COLORS[cat] || { color: '#64748B', bg: '#F8FAFC', border: '#CBD5E1' };
const TABS = [
{ id: 'search', icon: <Search size={14} />, label: 'Semantic Search' },
{ id: 'memory', icon: <Database size={14} />, label: 'Memory Inspector' },
{ id: 'conversations', icon: <MessageSquare size={14} />, label: 'Conversations' },
{ id: 'tools', icon: <Zap size={14} />, label: 'Tool Catalog' },
];
export default function KnowledgeExplorer({ repo_id, apiKey }) {
const [activeTab, setActiveTab] = useState('search');
const [searchQuery, setSearchQuery] = useState('');
const [searchCategory, setSearchCategory] = useState('');
const [searchTopK, setSearchTopK] = useState(5);
const [searchResults, setSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const [searchLatency, setSearchLatency] = useState(null);
const [searchError, setSearchError] = useState(null);
const [memoryInfo, setMemoryInfo] = useState(null);
const [memoryLoading, setMemoryLoading] = useState(false);
const [conversations, setConversations] = useState([]);
const [convsLoading, setConvsLoading] = useState(false);
const [selectedSession, setSelectedSession] = useState(null);
const [sessionHistory, setSessionHistory] = useState([]);
const [historyLoading, setHistoryLoading] = useState(false);
const headers = () => {
const h = { 'Content-Type': 'application/json' };
if (apiKey) h['x-gemini-key'] = apiKey;
return h;
};
useEffect(() => {
if (activeTab === 'memory') loadMemory();
if (activeTab === 'conversations') loadConversations();
}, [activeTab]);
const loadMemory = async () => {
setMemoryLoading(true);
try {
const r = await fetch(apiUrl(`/api/memory?repo_id=${repo_id}`));
setMemoryInfo(await r.json());
} catch { setMemoryInfo(null); }
setMemoryLoading(false);
};
const loadConversations = async () => {
setConvsLoading(true);
try {
const r = await fetch(apiUrl(`/api/conversations?repo_id=${repo_id}`));
const d = await r.json();
setConversations(d.sessions || []);
} catch { setConversations([]); }
setConvsLoading(false);
};
const loadSessionHistory = async (sessionId) => {
setHistoryLoading(true);
try {
const r = await fetch(apiUrl(`/api/conversations/${sessionId}`));
const data = await r.json();
if (!r.ok) throw new Error(data.detail);
setSessionHistory(data.history || []);
} catch {
setSessionHistory([]);
} finally {
setHistoryLoading(false);
}
};
const handleSelectSession = (sessionId) => {
if (selectedSession === sessionId) {
setSelectedSession(null);
setSessionHistory([]);
return;
}
setSelectedSession(sessionId);
loadSessionHistory(sessionId);
};
const handleSearch = async (e) => {
e.preventDefault();
if (!searchQuery.trim()) return;
setSearchLoading(true);
setSearchError(null);
setSearchResults([]);
setSearchLatency(null);
try {
const body = { repo_id, query: searchQuery.trim(), top_k: searchTopK };
if (searchCategory) body.category = searchCategory;
const r = await fetch(apiUrl('/api/search'), { method: 'POST', headers: headers(), body: JSON.stringify(body) });
const d = await r.json();
if (!r.ok) throw new Error(d.detail || 'Search failed');
setSearchResults(d.results || []);
setSearchLatency(d.latency_ms);
} catch (e) {
setSearchError(e.message);
}
setSearchLoading(false);
};
return (
<div className="ke-container">
{/* Sub-tabs */}
<div className="ke-tabs">
{TABS.map(t => (
<button
key={t.id}
className={`ke-tab-btn ${activeTab === t.id ? 'active' : ''}`}
onClick={() => setActiveTab(t.id)}
id={`ke-tab-${t.id}`}
>
{t.icon} {t.label}
</button>
))}
</div>
{/* ── SEMANTIC SEARCH ── */}
{activeTab === 'search' && (
<div className="ke-panel">
<div className="ke-panel-title">
<Search size={16} style={{ color: 'var(--accent-teal)' }} />
Semantic Knowledge Search
</div>
<p className="ke-panel-desc">
Search across all indexed repository knowledge using natural language.
Results are ranked by cosine similarity score.
</p>
<form onSubmit={handleSearch} className="search-form-row">
<input
type="text"
className="ke-input"
placeholder="e.g. 'JWT authentication middleware' or 'database connection'"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
id="semantic-search-input"
/>
<select className="ke-select" value={searchCategory} onChange={e => setSearchCategory(e.target.value)}>
{CATEGORY_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<select className="ke-select ke-select-sm" value={searchTopK} onChange={e => setSearchTopK(Number(e.target.value))}>
{[3, 5, 8, 10].map(n => <option key={n} value={n}>Top {n}</option>)}
</select>
<button
type="submit"
className="ke-search-btn"
disabled={searchLoading || !searchQuery.trim()}
id="semantic-search-btn"
>
{searchLoading ? <RefreshCw size={14} className="spin-slow" /> : <Search size={14} />}
{searchLoading ? 'Searching…' : 'Search'}
</button>
</form>
{searchError && <div className="ke-error-box">{searchError}</div>}
{searchLatency != null && !searchLoading && (
<div className="ke-latency-badge">
<Clock size={11} />
{searchResults.length} results · {searchLatency}ms
</div>
)}
<div className="search-results-list">
{searchResults.map((r, i) => {
const cs = getCatStyle(r.metadata?.category);
const sim = r.similarity;
const fillColor = sim > 0.8 ? 'var(--accent-green)' : sim > 0.6 ? 'var(--accent-teal)' : 'var(--accent-amber)';
return (
<div key={i} className="search-result-card">
<div className="src-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', flexWrap: 'wrap' }}>
<span className="src-category-tag" style={{ color: cs.color, background: cs.bg, borderColor: cs.border }}>
{r.metadata?.category || 'general'}
</span>
{r.metadata?.path && <span className="src-path-tag">{r.metadata.path}</span>}
</div>
<div className="src-score-bar-wrap">
<span className="src-score-label">{Math.round(sim * 100)}%</span>
<div className="src-score-bg">
<div className="src-score-fill" style={{ width: `${sim * 100}%`, background: fillColor }} />
</div>
</div>
</div>
<pre className="src-content">{r.content}</pre>
</div>
);
})}
{!searchLoading && searchResults.length === 0 && searchLatency != null && (
<div className="ke-empty">No results found. Try different search terms or select a different category.</div>
)}
</div>
</div>
)}
{/* ── MEMORY INSPECTOR ── */}
{activeTab === 'memory' && (
<div className="ke-panel">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<div className="ke-panel-title" style={{ marginBottom: 0 }}>
<Database size={16} style={{ color: 'var(--accent-indigo)' }} />
Vector Memory Inspector
</div>
<button className="ke-refresh-btn" onClick={loadMemory} disabled={memoryLoading}>
<RefreshCw size={13} className={memoryLoading ? 'spin-slow' : ''} /> Refresh
</button>
</div>
<p className="ke-panel-desc">
Inspect the repository's semantic knowledge stored in ChromaDB. Each chunk is
tagged with category metadata for precise retrieval.
</p>
{memoryLoading && (
<div className="ke-spinner-row">
<RefreshCw size={18} className="spin-slow" style={{ color: 'var(--accent-teal)' }} />
Loading memory info…
</div>
)}
{memoryInfo && !memoryLoading && (
<div className="memory-stats-grid">
<div className="memory-stat-card">
<div className="msc-label">Indexed Chunks</div>
<div className="msc-value">{memoryInfo.indexed_chunks?.toLocaleString()}</div>
<div className="msc-sub">ChromaDB documents</div>
</div>
<div className="memory-stat-card">
<div className="msc-label">Repository ID</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--text-muted)', wordBreak: 'break-all', marginTop: '0.25rem' }}>
{memoryInfo.repo_id}
</div>
</div>
<div className="memory-stat-card">
<div className="msc-label">Storage Path</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--text-secondary)', wordBreak: 'break-all', marginTop: '0.25rem' }}>
{memoryInfo.storage_path}
</div>
</div>
</div>
)}
<div className="ke-info-box" style={{ marginBottom: '1rem' }}>
<BookOpen size={14} style={{ flexShrink: 0, marginTop: 1 }} />
<span>
The knowledge index contains chunked embeddings of the intelligence report, source files,
architecture concepts, API endpoints, dependencies, business flows, and concepts.
Embeddings are generated using Gemini <code>text-embedding-004</code>.
</span>
</div>
<div className="ke-category-legend">
<div className="ke-legend-title">Indexed Categories</div>
<div className="ke-legend-grid">
{CATEGORY_OPTIONS.filter(o => o.value).map(o => {
const cs = getCatStyle(o.value);
return (
<div key={o.value} className="ke-legend-item">
<span className="ke-legend-dot" style={{ background: cs.color }} />
{o.label}
</div>
);
})}
</div>
</div>
</div>
)}
{/* ── CONVERSATIONS ── */}
{activeTab === 'conversations' && (
<div className="ke-panel">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<div className="ke-panel-title" style={{ marginBottom: 0 }}>
<MessageSquare size={16} style={{ color: 'var(--accent-green)' }} />
Conversation History
</div>
<button className="ke-refresh-btn" onClick={loadConversations} disabled={convsLoading}>
<RefreshCw size={13} className={convsLoading ? 'spin-slow' : ''} /> Refresh
</button>
</div>
<p className="ke-panel-desc">
All AI Assistant chat sessions for this repository, stored in conversation memory.
</p>
{convsLoading && (
<div className="ke-spinner-row">
<RefreshCw size={18} className="spin-slow" style={{ color: 'var(--accent-teal)' }} />
Loading conversations…
</div>
)}
{!convsLoading && conversations.length === 0 && (
<div className="ke-empty">
No conversations yet. Start chatting in the AI Assistant tab to see sessions here.
</div>
)}
<div className="conv-list">
{conversations.map((s, idx) => (
<div
key={s.session_id}
className={`conv-item ${selectedSession === s.session_id ? 'selected' : ''}`}
onClick={() => handleSelectSession(s.session_id)}
id={`conv-item-${idx}`}
>
<div className="conv-item-header">
<span className="conv-idx">#{idx + 1}</span>
<span className="conv-summary">{s.summary || 'Untitled Session'}</span>
<span className="conv-count">{s.message_count} msgs</span>
<ChevronRight
size={14}
style={{
transition: 'transform 0.2s',
transform: selectedSession === s.session_id ? 'rotate(90deg)' : 'none',
color: 'var(--text-muted)',
flexShrink: 0,
}}
/>
</div>
<div className="conv-item-meta">
<span className="conv-id">{s.session_id?.substring(0, 8)}…</span>
<span className="conv-time">{new Date(s.last_updated * 1000).toLocaleString()}</span>
</div>
</div>
))}
</div>
{selectedSession && (
<div style={{ marginTop: '1.25rem' }}>
<div className="ke-panel-title" style={{ marginBottom: '0.75rem' }}>
<MessageSquare size={14} style={{ color: 'var(--accent-teal)' }} />
Session Messages
</div>
{historyLoading && (
<div className="ke-spinner-row">
<RefreshCw size={16} className="spin-slow" style={{ color: 'var(--accent-teal)' }} />
Loading message history…
</div>
)}
{!historyLoading && sessionHistory.length === 0 && (
<div className="ke-empty">No messages in this session.</div>
)}
{!historyLoading && sessionHistory.length > 0 && (
<div className="conv-list" style={{ maxHeight: 360 }}>
{sessionHistory.map((msg, i) => (
<div key={i} className="search-result-card">
<div className="src-header">
<span className={`src-category-tag`} style={{
color: msg.role === 'user' ? 'var(--accent-teal-dk)' : 'var(--accent-indigo)',
background: msg.role === 'user' ? '#E6F7F7' : '#EEF2FF',
borderColor: msg.role === 'user' ? '#A8D8D8' : '#C7D2FE',
}}>
{msg.role}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
{msg.content}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
{/* ── TOOL CATALOG ── */}
{activeTab === 'tools' && (
<div className="ke-panel">
<div className="ke-panel-title">
<Zap size={16} style={{ color: 'var(--accent-amber)' }} />
Tool Catalog (MCP-Ready)
</div>
<p className="ke-panel-desc">
These tools are available to the Planner Agent during orchestration.
Interfaces are compatible with Model Context Protocol (MCP) and Google ADK.
</p>
<div className="tool-catalog-grid">
{TOOLS.map(t => (
<div key={t.name} className="tool-card" id={`tool-${t.name}`}>
<div
className="tc-icon-wrap"
style={{ background: t.bg, borderColor: t.border, color: t.color }}
>
{t.icon}
</div>
<div className="tc-body">
<div className="tc-name">{t.name}</div>
<div className="tc-desc">{t.desc}</div>
</div>
<span className="tc-badge">execute()</span>
</div>
))}
</div>
<div className="ke-info-box" style={{ marginTop: '1.25rem' }}>
<Globe size={14} style={{ flexShrink: 0, marginTop: 1 }} />
<span>
Each tool implements a <code>BaseTool</code> interface with <code>name</code>,
<code>description</code>, and <code>execute(**kwargs)</code>. This design is
forward-compatible with Google ADK, LangGraph, CrewAI, and MCP server registration.
</span>
</div>
</div>
)}
</div>
);
}