kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
// src/components/EnrichmentPanel.tsx
import { useState, useEffect } from 'react';
import apiClient from '../services/api';
interface ModelInfo {
id: string;
name: string;
provider: string;
}
interface EnrichmentConfig {
ai_enrichment: {
available: boolean;
available_models: ModelInfo[];
master_model_id: string | null;
};
web_enrichment: {
available: boolean;
search_engine: string;
api_key_configured: boolean;
};
}
interface EnrichmentStatus {
is_running: boolean;
progress: number;
current_question: number;
total_questions: number;
generated_tiles: number;
start_time: string | null;
domain_id: string | null;
}
interface QuestionPreview {
questions: string[];
domain_id: string;
count: number;
}
interface KnowledgeTile {
tile_id: string;
domain_id: string;
topic: string;
content_preview: string;
created_at: string;
verification_mark: string;
confidence_score: number;
tags: string[];
coordinates?: number[];
}
const EnrichmentPanel = () => {
// Form state
const [activeTab, setActiveTab] = useState<'ai' | 'web'>('ai');
const [domainId, setDomainId] = useState('medical');
const [numQuestions, setNumQuestions] = useState(10);
const [focusAreas, setFocusAreas] = useState('');
const [promptModelId, setPromptModelId] = useState<string>('');
const [answerModelId, setAnswerModelId] = useState<string>('master');
const [webQuery, setWebQuery] = useState('');
const [maxResults, setMaxResults] = useState(5);
// Enrichment state
const [enrichmentStatus, setEnrichmentStatus] = useState<EnrichmentStatus | null>(null);
const [config, setConfig] = useState<EnrichmentConfig | null>(null);
const [questionPreview, setQuestionPreview] = useState<QuestionPreview | null>(null);
const [generatedTiles, setGeneratedTiles] = useState<KnowledgeTile[]>([]);
const [showGeneratedTiles, setShowGeneratedTiles] = useState(false);
// UI state
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [showPreview, setShowPreview] = useState(false);
// Polling interval for status updates
const [statusPollingInterval, setStatusPollingInterval] = useState<NodeJS.Timeout | null>(null);
const [previouslyRunning, setPreviouslyRunning] = useState(false);
// Fetch enrichment configuration
const fetchConfig = async (retries = 3, delay = 2000) => {
try {
const response = await apiClient.get('/enrichment/config');
setConfig(response.data);
} catch (err: any) {
console.error(`Failed to fetch enrichment config (retries left: ${retries}):`, err);
if (retries > 0) {
setTimeout(() => fetchConfig(retries - 1, delay), delay);
}
}
};
// Fetch generated tiles
const fetchGeneratedTiles = async (domain: string) => {
try {
const response = await apiClient.get(`/knowledge/?domain_id=${domain}&page=1&page_size=50`);
const tiles = response.data.tiles || [];
// Sort by created_at descending (newest first)
const sortedTiles = tiles.sort((a: KnowledgeTile, b: KnowledgeTile) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
setGeneratedTiles(sortedTiles);
setShowGeneratedTiles(true);
} catch (err: any) {
console.error('Failed to fetch generated tiles:', err);
}
};
// Fetch enrichment status
const fetchStatus = async () => {
try {
const response = await apiClient.get('/enrichment/status');
const currentStatus = response.data;
setEnrichmentStatus(currentStatus);
// Detect completion: was running, now stopped
if (previouslyRunning && !currentStatus.is_running) {
// Enrichment just completed!
if (currentStatus.generated_tiles > 0 && currentStatus.domain_id) {
setSuccessMessage(
`✅ Enrichment completed! Generated ${currentStatus.generated_tiles} tiles. Loading results...`
);
// Fetch the generated tiles
setTimeout(() => {
fetchGeneratedTiles(currentStatus.domain_id);
}, 1000);
}
}
// Track previous running state
setPreviouslyRunning(currentStatus.is_running);
// If enrichment is active, start polling
if (currentStatus.is_running && !statusPollingInterval) {
const interval = setInterval(fetchStatus, 2000); // Poll every 2 seconds
setStatusPollingInterval(interval);
}
// If enrichment finished, stop polling
if (!currentStatus.is_running && statusPollingInterval) {
clearInterval(statusPollingInterval);
setStatusPollingInterval(null);
}
} catch (err: any) {
console.error('Failed to fetch enrichment status:', err);
}
};
// Initial data fetch
useEffect(() => {
fetchConfig();
fetchStatus();
// Cleanup polling on unmount
return () => {
if (statusPollingInterval) {
clearInterval(statusPollingInterval);
}
};
}, []);
// Start AI enrichment
const handleStartAIEnrichment = async () => {
setIsLoading(true);
setError('');
setSuccessMessage('');
try {
const requestData = {
domain_id: domainId,
num_questions: numQuestions,
focus_areas: focusAreas ? focusAreas.split(',').map(s => s.trim()) : null,
prompt_model_id: promptModelId || null,
answer_model_id: answerModelId || null
};
await apiClient.post('/enrichment/ai/start', requestData);
setSuccessMessage('AI enrichment started successfully! Check the progress below.');
setShowPreview(false);
// Start polling for status
fetchStatus();
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`Failed to start AI enrichment: ${errorMessage}`);
console.error(err);
} finally {
setIsLoading(false);
}
};
// Start Web enrichment
const handleStartWebEnrichment = async () => {
setIsLoading(true);
setError('');
setSuccessMessage('');
try {
const requestData = {
query: webQuery,
domain_id: domainId,
max_results: maxResults
};
const response = await apiClient.post('/enrichment/web/start', requestData);
setSuccessMessage(
`Web enrichment completed! Created ${response.data.tiles_created} tiles from ${response.data.search_results} search results.`
);
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`Failed to start web enrichment: ${errorMessage}`);
console.error(err);
} finally {
setIsLoading(false);
}
};
// Stop enrichment
const handleStopEnrichment = async () => {
try {
await apiClient.post('/enrichment/stop');
setSuccessMessage('Enrichment stop requested. This may take a moment...');
fetchStatus();
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`Failed to stop enrichment: ${errorMessage}`);
console.error(err);
}
};
// Preview questions
const handlePreviewQuestions = async () => {
setIsLoading(true);
setError('');
setQuestionPreview(null);
try {
const requestData = {
domain_id: domainId,
num_questions: numQuestions,
focus_areas: focusAreas ? focusAreas.split(',').map(s => s.trim()) : null,
prompt_model_id: promptModelId || null,
answer_model_id: answerModelId || null
};
const response = await apiClient.post('/enrichment/ai/generate-questions', requestData);
setQuestionPreview(response.data);
setShowPreview(true);
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`Failed to preview questions: ${errorMessage}`);
console.error(err);
} finally {
setIsLoading(false);
}
};
return (
<div className="bg-gray-800 text-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-4">🌱 Knowledge Base Enrichment</h2>
{/* Error/Success Messages */}
{error && (
<div className="bg-red-600 text-white p-3 rounded mb-4">
{error}
</div>
)}
{successMessage && (
<div className="bg-green-600 text-white p-3 rounded mb-4">
{successMessage}
</div>
)}
{/* Configuration Status */}
{config && (
<div className="mb-6 p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-2">⚙️ Configuration Status</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-400">AI Enrichment</p>
<p className={config.ai_enrichment.available ? 'text-green-400' : 'text-red-400'}>
{config.ai_enrichment.available ? '✓ Available' : '✗ Unavailable'}
</p>
{config.ai_enrichment.available && (
<p className="text-xs text-gray-500">
{config.ai_enrichment.available_models.length} models available
{config.ai_enrichment.master_model_id && ` (Master: ${config.ai_enrichment.master_model_id})`}
</p>
)}
</div>
<div>
<p className="text-gray-400">Web Enrichment</p>
<p className={config.web_enrichment.available ? 'text-green-400' : 'text-red-400'}>
{config.web_enrichment.available ? '✓ Available' : '✗ Unavailable'}
</p>
{!config.web_enrichment.api_key_configured && (
<p className="text-xs text-yellow-400">⚠️ BRAVE_SEARCH_API_KEY not configured</p>
)}
</div>
</div>
</div>
)}
{/* Enrichment Status */}
{enrichmentStatus?.is_running && (
<div className="mb-6 p-4 bg-blue-900 rounded border border-blue-500">
<h3 className="text-lg font-semibold mb-3">🔄 Enrichment in Progress</h3>
<div className="space-y-2">
<div>
<p className="text-sm text-gray-300">Domain: {enrichmentStatus.domain_id}</p>
<p className="text-sm text-gray-300">
Question: {enrichmentStatus.current_question} / {enrichmentStatus.total_questions}
</p>
<p className="text-sm text-gray-300">
Tiles Generated: {enrichmentStatus.generated_tiles}
</p>
</div>
<div>
<div className="w-full bg-gray-600 rounded-full h-4">
<div
className="bg-blue-500 h-4 rounded-full transition-all duration-500"
style={{ width: `${enrichmentStatus.progress}%` }}
></div>
</div>
<p className="text-sm text-gray-300 mt-1">
Progress: {enrichmentStatus.progress.toFixed(1)}%
</p>
</div>
<button
onClick={handleStopEnrichment}
className="mt-3 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
>
⛔ Stop Enrichment
</button>
</div>
</div>
)}
{/* Tab Selection */}
<div className="mb-4 flex space-x-2">
<button
onClick={() => setActiveTab('ai')}
className={`px-4 py-2 rounded ${
activeTab === 'ai' ? 'bg-blue-600' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
🤖 AI Enrichment
</button>
<button
onClick={() => setActiveTab('web')}
className={`px-4 py-2 rounded ${
activeTab === 'web' ? 'bg-blue-600' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
🌐 Web Enrichment
</button>
</div>
{/* AI Enrichment Form */}
{activeTab === 'ai' && !enrichmentStatus?.is_running && (
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">🤖 AI-Driven Enrichment</h3>
<p className="text-sm text-gray-400 mb-4">
指定したモデルで質問生成 → 回答生成 → 座標推定 → .iath保存
</p>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">🧠 Prompt Generation Model</label>
<select
value={promptModelId}
onChange={(e) => setPromptModelId(e.target.value)}
className="w-full p-2 bg-gray-600 rounded text-white"
>
<option value="">Auto (Any available)</option>
{config?.ai_enrichment.available_models.map((model) => (
<option key={model.id} value={model.id}>
{model.id} ({model.provider})
</option>
))}
</select>
<p className="text-xs text-gray-400 mt-1">質問を生成するモデル</p>
</div>
<div>
<label className="block text-sm mb-1">💬 Answer Generation Model</label>
<select
value={answerModelId}
onChange={(e) => setAnswerModelId(e.target.value)}
className="w-full p-2 bg-gray-600 rounded text-white"
>
<option value="master">Master Model</option>
{config?.ai_enrichment.available_models.map((model) => (
<option key={model.id} value={model.id}>
{model.id} ({model.provider})
</option>
))}
</select>
<p className="text-xs text-gray-400 mt-1">回答を生成するモデル</p>
</div>
</div>
<div>
<label className="block text-sm mb-1">Domain ID</label>
<select
value={domainId}
onChange={(e) => setDomainId(e.target.value)}
className="w-full p-2 bg-gray-600 rounded text-white"
>
<option value="medical">Medical</option>
<option value="general">General</option>
<option value="legal">Legal</option>
<option value="technology">Technology</option>
</select>
</div>
<div>
<label className="block text-sm mb-1">Number of Questions</label>
<input
type="number"
value={numQuestions}
onChange={(e) => setNumQuestions(parseInt(e.target.value))}
min="1"
max="100"
className="w-full p-2 bg-gray-600 rounded text-white"
/>
</div>
<div>
<label className="block text-sm mb-1">Focus Areas (optional, comma-separated)</label>
<input
type="text"
value={focusAreas}
onChange={(e) => setFocusAreas(e.target.value)}
placeholder="e.g., 基礎理論, 臨床応用, 最新研究"
className="w-full p-2 bg-gray-600 rounded text-white"
/>
<p className="text-xs text-gray-400 mt-1">Leave empty for broad coverage</p>
</div>
<div className="flex space-x-2">
<button
onClick={handlePreviewQuestions}
disabled={isLoading}
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded disabled:opacity-50"
>
👁️ Preview Questions
</button>
<button
onClick={handleStartAIEnrichment}
disabled={isLoading || !config?.ai_enrichment.available}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded font-semibold disabled:opacity-50"
>
{isLoading ? 'Starting...' : '▶️ Start AI Enrichment'}
</button>
</div>
</div>
{/* Question Preview */}
{showPreview && questionPreview && (
<div className="mt-4 p-4 bg-gray-600 rounded">
<h4 className="font-semibold mb-2">📝 Question Preview ({questionPreview.count} questions)</h4>
<div className="max-h-64 overflow-y-auto space-y-2">
{questionPreview.questions.map((question, idx) => (
<div key={idx} className="text-sm bg-gray-700 p-2 rounded">
{idx + 1}. {question}
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Web Enrichment Form */}
{activeTab === 'web' && !enrichmentStatus?.is_running && (
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">🌐 Web Search Enrichment</h3>
<p className="text-sm text-gray-400 mb-4">
Brave Search API → Extract snippets → Convert to tiles → Save to .iath
</p>
<div className="space-y-4">
<div>
<label className="block text-sm mb-1">Search Query</label>
<input
type="text"
value={webQuery}
onChange={(e) => setWebQuery(e.target.value)}
placeholder="e.g., 最新の量子コンピュータ技術"
className="w-full p-2 bg-gray-600 rounded text-white"
/>
</div>
<div>
<label className="block text-sm mb-1">Domain ID</label>
<select
value={domainId}
onChange={(e) => setDomainId(e.target.value)}
className="w-full p-2 bg-gray-600 rounded text-white"
>
<option value="medical">Medical</option>
<option value="general">General</option>
<option value="legal">Legal</option>
<option value="technology">Technology</option>
</select>
</div>
<div>
<label className="block text-sm mb-1">Max Results</label>
<input
type="number"
value={maxResults}
onChange={(e) => setMaxResults(parseInt(e.target.value))}
min="1"
max="20"
className="w-full p-2 bg-gray-600 rounded text-white"
/>
</div>
<button
onClick={handleStartWebEnrichment}
disabled={isLoading || !webQuery || !config?.web_enrichment.available}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded font-semibold disabled:opacity-50"
>
{isLoading ? 'Searching...' : '🔍 Start Web Enrichment'}
</button>
{!config?.web_enrichment.api_key_configured && (
<p className="text-yellow-400 text-sm">
⚠️ Set BRAVE_SEARCH_API_KEY environment variable to enable web enrichment
</p>
)}
</div>
</div>
)}
{/* Generated Tiles Display */}
{showGeneratedTiles && generatedTiles.length > 0 && (
<div className="mt-6 p-4 bg-green-900 bg-opacity-30 rounded border border-green-500">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold">✨ Generated Knowledge Tiles ({generatedTiles.length})</h3>
<button
onClick={() => setShowGeneratedTiles(false)}
className="text-sm text-gray-400 hover:text-white"
>
✕ Close
</button>
</div>
<div className="max-h-[600px] overflow-y-auto space-y-3">
{generatedTiles.slice(0, 20).map((tile) => (
<div
key={tile.tile_id}
className="bg-gray-800 p-4 rounded-lg border border-gray-600 hover:border-green-500 transition-colors"
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-semibold text-green-400 flex-1">{tile.topic}</h4>
<span className="text-xs bg-gray-700 px-2 py-1 rounded ml-2">
{tile.domain_id}
</span>
</div>
<p className="text-sm text-gray-300 mb-2 line-clamp-3">
{tile.content_preview}
</p>
<div className="flex flex-wrap gap-2 text-xs text-gray-400">
<span>🎯 Confidence: {(tile.confidence_score * 100).toFixed(1)}%</span>
<span>✓ {tile.verification_mark}</span>
{tile.coordinates && (
<span title="6D Coordinates">
📍 [{tile.coordinates.map(c => c.toFixed(2)).join(', ')}]
</span>
)}
<span>🕐 {new Date(tile.created_at).toLocaleString()}</span>
</div>
{tile.tags && tile.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{tile.tags.map((tag, idx) => (
<span key={idx} className="text-xs bg-blue-900 text-blue-300 px-2 py-0.5 rounded">
{tag}
</span>
))}
</div>
)}
</div>
))}
</div>
{generatedTiles.length > 20 && (
<p className="text-sm text-gray-400 mt-3 text-center">
Showing first 20 of {generatedTiles.length} tiles
</p>
)}
<div className="mt-4 text-sm text-gray-400">
💡 Tip: Navigate to <strong>Knowledge Panel</strong> to view all tiles with full details
</div>
</div>
)}
{/* Info Box */}
<div className="mt-6 p-4 bg-blue-900 bg-opacity-30 rounded border border-blue-500">
<h4 className="font-semibold mb-2">ℹ️ How It Works</h4>
<ul className="text-sm text-gray-300 space-y-1">
<li><strong>AI Enrichment:</strong> 任意のモデルで質問生成 → 任意のモデルで回答 → 自動保存</li>
<li><strong>Model Selection:</strong> プロンプト生成と回答生成で異なるモデルを使い分け可能</li>
<li><strong>Web Enrichment:</strong> Brave APIで検索 → Knowledge Tile形式に変換</li>
<li>• すべてのタイルに6次元座標が自動割り当てされ.iathに保存されます</li>
<li>• バックグラウンド実行で、いつでも停止可能</li>
<li><strong className="text-green-400">完了時に自動的にタイルが表示されます</strong></li>
</ul>
</div>
</div>
);
};
export default EnrichmentPanel;