kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
// src/components/NurseLogPanel.tsx
import { useState, useEffect } from 'react';
import apiClient from '../services/api';
interface SuccessionStatus {
status: string;
should_trigger: boolean;
high_quality_count: number;
threshold: number;
message: string;
}
interface SuccessionStats {
total_inferences: number;
high_quality_inferences: number;
succession_count: number;
domain_stats: Record<string, {
total: number;
high_quality: number;
avg_confidence: number;
}>;
next_succession_at: number | null;
}
interface InferenceRecord {
timestamp: string;
domain_id: string;
prompt: string;
response: string;
confidence: number;
source_type: string;
}
interface SuccessionHistoryRecord {
timestamp: string;
generation: number;
training_data_count: number;
domain_id: string | null;
exports: Record<string, string>;
}
interface ExportedFile {
filename: string;
size_mb: number;
created_at: string;
modified_at: string;
}
const NurseLogPanel = () => {
// State
const [activeTab, setActiveTab] = useState<'status' | 'history' | 'exports'>('status');
const [successionStatus, setSuccessionStatus] = useState<SuccessionStatus | null>(null);
const [stats, setStats] = useState<SuccessionStats | null>(null);
const [inferenceHistory, setInferenceHistory] = useState<InferenceRecord[]>([]);
const [successionHistory, setSuccessionHistory] = useState<SuccessionHistoryRecord[]>([]);
const [exportedFiles, setExportedFiles] = useState<ExportedFile[]>([]);
// Filters
const [domainFilter, setDomainFilter] = useState<string>('');
const [minConfidence, setMinConfidence] = useState<number>(0.8);
const [historyLimit, setHistoryLimit] = useState<number>(50);
// UI State
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
// Fetch succession status
const fetchStatus = async () => {
try {
const response = await apiClient.get('/succession/status');
setSuccessionStatus(response.data);
} catch (err: any) {
console.error('Failed to fetch succession status:', err);
}
};
// Fetch stats
const fetchStats = async () => {
try {
const response = await apiClient.get('/succession/stats');
setStats(response.data);
} catch (err: any) {
console.error('Failed to fetch stats:', err);
}
};
// Fetch inference history
const fetchInferenceHistory = async () => {
try {
const params: any = {
limit: historyLimit,
min_confidence: minConfidence
};
if (domainFilter) {
params.domain_id = domainFilter;
}
const response = await apiClient.get('/succession/history/inference', { params });
setInferenceHistory(response.data.records);
} catch (err: any) {
console.error('Failed to fetch inference history:', err);
}
};
// Fetch succession history
const fetchSuccessionHistory = async () => {
try {
const response = await apiClient.get('/succession/history/succession');
setSuccessionHistory(response.data.history);
} catch (err: any) {
console.error('Failed to fetch succession history:', err);
}
};
// Fetch exported files
const fetchExportedFiles = async () => {
try {
const response = await apiClient.get('/succession/exports');
setExportedFiles(response.data.files);
} catch (err: any) {
console.error('Failed to fetch exported files:', err);
}
};
// Initial data fetch
useEffect(() => {
fetchStatus();
fetchStats();
fetchInferenceHistory();
fetchSuccessionHistory();
fetchExportedFiles();
}, []);
// Trigger succession
const handleTriggerSuccession = async () => {
setIsLoading(true);
setError('');
setSuccessMessage('');
try {
const requestData = {
domain_id: domainFilter || null,
min_confidence: minConfidence
};
const response = await apiClient.post('/succession/trigger', requestData);
setSuccessMessage(
`世代交代成功!${response.data.count}件のトレーニングデータをエクスポートしました。`
);
// Refresh data
fetchStatus();
fetchStats();
fetchSuccessionHistory();
fetchExportedFiles();
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`世代交代失敗: ${errorMessage}`);
console.error(err);
} finally {
setIsLoading(false);
}
};
// Create standalone package
const handleCreateStandalonePackage = async () => {
setIsLoading(true);
setError('');
setSuccessMessage('');
try {
const requestData = {
domain_id: domainFilter || null,
min_confidence: minConfidence
};
const response = await apiClient.post('/succession/create-standalone-package', requestData);
setSuccessMessage(
`スタンドアロンパッケージ生成成功!\nパス: ${response.data.package_path}\nサイズ: ${response.data.size_mb} MB`
);
fetchExportedFiles();
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
setError(`パッケージ生成失敗: ${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">🏥 NurseLog System (世代交代管理)</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 whitespace-pre-line">
{successMessage}
</div>
)}
{/* Succession Status Card */}
{successionStatus && (
<div className={`mb-6 p-4 rounded border ${
successionStatus.should_trigger
? 'bg-green-900 border-green-500'
: 'bg-gray-700 border-gray-600'
}`}>
<h3 className="text-lg font-semibold mb-2">📊 世代交代ステータス</h3>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-gray-400">高品質推論数</p>
<p className="text-2xl font-bold">{successionStatus.high_quality_count}</p>
</div>
<div>
<p className="text-gray-400">閾値</p>
<p className="text-2xl font-bold">{successionStatus.threshold}</p>
</div>
<div>
<p className="text-gray-400">状態</p>
<p className={`text-lg font-semibold ${
successionStatus.should_trigger ? 'text-green-400' : 'text-yellow-400'
}`}>
{successionStatus.should_trigger ? '✓ 準備完了' : '⏳ データ蓄積中'}
</p>
</div>
</div>
<p className="text-sm text-gray-300 mt-3">{successionStatus.message}</p>
</div>
)}
{/* Stats Card */}
{stats && (
<div className="mb-6 p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">📈 統計情報</h3>
<div className="grid grid-cols-4 gap-4 text-sm mb-4">
<div>
<p className="text-gray-400">総推論数</p>
<p className="text-xl font-bold">{stats.total_inferences}</p>
</div>
<div>
<p className="text-gray-400">高品質推論</p>
<p className="text-xl font-bold text-green-400">{stats.high_quality_inferences}</p>
</div>
<div>
<p className="text-gray-400">世代交代回数</p>
<p className="text-xl font-bold text-blue-400">{stats.succession_count}</p>
</div>
<div>
<p className="text-gray-400">次回閾値</p>
<p className="text-xl font-bold">{stats.next_succession_at || 'N/A'}</p>
</div>
</div>
{/* Domain Stats */}
<div className="mt-4">
<p className="text-sm text-gray-400 mb-2">ドメイン別統計:</p>
<div className="space-y-2">
{Object.entries(stats.domain_stats).map(([domain, domainStats]) => (
<div key={domain} className="bg-gray-600 p-2 rounded text-xs">
<span className="font-semibold">{domain}</span>:
<span className="ml-2">総数 {domainStats.total}</span>
<span className="ml-2 text-green-400">高品質 {domainStats.high_quality}</span>
<span className="ml-2 text-blue-400">平均信頼度 {domainStats.avg_confidence.toFixed(2)}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className="mb-6 flex space-x-2">
<button
onClick={handleTriggerSuccession}
disabled={isLoading}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded font-semibold disabled:opacity-50"
>
{isLoading ? '処理中...' : '🔄 世代交代トリガー'}
</button>
<button
onClick={handleCreateStandalonePackage}
disabled={isLoading}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded font-semibold disabled:opacity-50"
>
📦 スタンドアロンパッケージ生成
</button>
</div>
{/* Filters */}
<div className="mb-4 p-4 bg-gray-700 rounded">
<h4 className="font-semibold mb-2">🔍 フィルター</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm mb-1">ドメイン</label>
<select
value={domainFilter}
onChange={(e) => setDomainFilter(e.target.value)}
className="w-full p-2 bg-gray-600 rounded text-white text-sm"
>
<option value="">全ドメイン</option>
<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">最小信頼度</label>
<input
type="number"
value={minConfidence}
onChange={(e) => setMinConfidence(parseFloat(e.target.value))}
min="0"
max="1"
step="0.1"
className="w-full p-2 bg-gray-600 rounded text-white text-sm"
/>
</div>
<div>
<label className="block text-sm mb-1">表示件数</label>
<input
type="number"
value={historyLimit}
onChange={(e) => setHistoryLimit(parseInt(e.target.value))}
min="10"
max="1000"
className="w-full p-2 bg-gray-600 rounded text-white text-sm"
/>
</div>
</div>
<button
onClick={() => {
fetchInferenceHistory();
fetchSuccessionHistory();
}}
className="mt-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm"
>
🔄 フィルター適用
</button>
</div>
{/* Tab Selection */}
<div className="mb-4 flex space-x-2">
<button
onClick={() => setActiveTab('status')}
className={`px-4 py-2 rounded ${
activeTab === 'status' ? 'bg-blue-600' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
📊 ステータス
</button>
<button
onClick={() => setActiveTab('history')}
className={`px-4 py-2 rounded ${
activeTab === 'history' ? 'bg-blue-600' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
📜 履歴
</button>
<button
onClick={() => setActiveTab('exports')}
className={`px-4 py-2 rounded ${
activeTab === 'exports' ? 'bg-blue-600' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
📁 エクスポート
</button>
</div>
{/* Status Tab */}
{activeTab === 'status' && (
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">📊 現在のステータス</h3>
<p className="text-sm text-gray-300">
上記の統計情報とステータスカードを参照してください。
</p>
</div>
)}
{/* History Tab */}
{activeTab === 'history' && (
<div className="space-y-4">
{/* Inference History */}
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">🧠 推論履歴 ({inferenceHistory.length}件)</h3>
<div className="max-h-96 overflow-y-auto space-y-2">
{inferenceHistory.length === 0 ? (
<p className="text-gray-400 text-sm">履歴がありません</p>
) : (
inferenceHistory.map((record, idx) => (
<div key={idx} className="bg-gray-600 p-3 rounded text-sm">
<div className="flex justify-between mb-1">
<span className="font-semibold">{record.domain_id}</span>
<span className="text-xs text-gray-400">
{new Date(record.timestamp).toLocaleString('ja-JP')}
</span>
</div>
<p className="text-xs text-gray-300 mb-1">
<strong>Q:</strong> {record.prompt.substring(0, 100)}...
</p>
<div className="flex justify-between text-xs">
<span className={`font-semibold ${
record.confidence >= 0.8 ? 'text-green-400' : 'text-yellow-400'
}`}>
信頼度: {record.confidence.toFixed(2)}
</span>
<span className="text-gray-400">{record.source_type}</span>
</div>
</div>
))
)}
</div>
</div>
{/* Succession History */}
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">🔄 世代交代履歴 ({successionHistory.length}回)</h3>
<div className="max-h-64 overflow-y-auto space-y-2">
{successionHistory.length === 0 ? (
<p className="text-gray-400 text-sm">世代交代履歴がありません</p>
) : (
successionHistory.map((record, idx) => (
<div key={idx} className="bg-gray-600 p-3 rounded text-sm">
<div className="flex justify-between mb-1">
<span className="font-semibold">第{record.generation}世代</span>
<span className="text-xs text-gray-400">
{new Date(record.timestamp).toLocaleString('ja-JP')}
</span>
</div>
<p className="text-xs text-gray-300">
訓練データ: {record.training_data_count}件
{record.domain_id && ` | ドメイン: ${record.domain_id}`}
</p>
<p className="text-xs text-gray-400 mt-1">
エクスポート: {Object.keys(record.exports).join(', ')}
</p>
</div>
))
)}
</div>
</div>
</div>
)}
{/* Exports Tab */}
{activeTab === 'exports' && (
<div className="p-4 bg-gray-700 rounded">
<h3 className="text-lg font-semibold mb-3">📁 エクスポートファイル ({exportedFiles.length}件)</h3>
<div className="max-h-96 overflow-y-auto space-y-2">
{exportedFiles.length === 0 ? (
<p className="text-gray-400 text-sm">エクスポートファイルがありません</p>
) : (
exportedFiles.map((file, idx) => (
<div key={idx} className="bg-gray-600 p-3 rounded text-sm flex justify-between items-center">
<div>
<p className="font-semibold">{file.filename}</p>
<p className="text-xs text-gray-400">
{file.size_mb} MB | 作成: {new Date(file.created_at).toLocaleString('ja-JP')}
</p>
</div>
<a
href={`/api/succession/exports/${file.filename}`}
download
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs"
>
⬇️ ダウンロード
</a>
</div>
))
)}
</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">ℹ️ NurseLog Systemとは</h4>
<ul className="text-sm text-gray-300 space-y-1">
<li>• 師匠モデルの高品質な推論を自動記録</li>
<li>• 閾値到達で世代交代トリガー(訓練データ自動エクスポート)</li>
<li>• JSONL, CSV, Parquet, HuggingFace Datasets形式に対応</li>
<li>• スタンドアロンパッケージで完全な推論エンジンを配布可能</li>
<li>• 推論履歴と世代交代履歴を完全に追跡</li>
</ul>
</div>
</div>
);
};
export default NurseLogPanel;