kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
// src/components/KnowledgePanel.tsx
import { useState, useEffect } from 'react';
import apiClient from '../services/api';
import TileCard from './TileCard'; // Assuming TileCard is in the same directory
// Define the TypeScript interfaces matching the backend schema
interface VerificationMark {
verification_type: string;
is_expert_verified: boolean;
expert_orcid_id?: string;
expert_name?: string;
verification_date?: string;
verification_count: number;
}
interface KnowledgeTile {
tile_id: string;
domain_id: string;
topic: string;
content_preview: string;
created_at: string;
updated_at: string;
verification_mark: VerificationMark;
contributor_id?: string;
confidence_score: number;
tags: string[];
}
interface DomainConfig { // Also defined in DBManager, but local here for clarity
domain_id: string;
name: string;
}
const KnowledgePanel = () => {
const [tiles, setTiles] = useState<KnowledgeTile[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
// Filter states
const [searchTerm, setSearchTerm] = useState('');
const [filterDomain, setFilterDomain] = useState('');
const [availableDomains, setAvailableDomains] = useState<DomainConfig[]>([]);
const [isExporting, setIsExporting] = useState(false);
useEffect(() => {
const fetchDomains = async () => {
try {
const response = await apiClient.get('/config/domains');
setAvailableDomains(response.data);
} catch (err) {
console.error("Failed to fetch domains:", err);
setError("Failed to load domain filters.");
}
};
fetchDomains();
}, []);
useEffect(() => {
const fetchTiles = async () => {
setIsLoading(true);
setError(null);
try {
const response = await apiClient.get('/knowledge/', {
params: {
page: page,
page_size: 20,
domain_id: filterDomain || undefined, // Only send if not empty
search: searchTerm || undefined, // Only send if not empty
},
});
setTiles(response.data.tiles);
setHasMore(response.data.has_more);
} catch (err) {
setError('Failed to fetch knowledge tiles.');
console.error(err);
} finally {
setIsLoading(false);
}
};
fetchTiles();
}, [page, filterDomain, searchTerm]); // Re-fetch tiles when page, filterDomain, or searchTerm changes
const handleExportIath = async () => {
setIsExporting(true);
try {
const response = await apiClient.get('/knowledge/export/iath', {
params: {
domain_id: filterDomain || undefined,
precision: 'standard' // List View uses standard precision
},
responseType: 'blob' // Important for binary data
});
// Create download link
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
// Extract filename from Content-Disposition header or use default
const contentDisposition = response.headers['content-disposition'];
let filename = 'knowledge_base.iath';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Export failed:', err);
setError('Failed to export .iath file.');
} finally {
setIsExporting(false);
}
};
const handleDomainUpdate = async (tileId: string, newDomainId: string) => {
try {
await apiClient.patch(`/knowledge/${tileId}/domain`, null, {
params: { domain_id: newDomainId }
});
// Update the tile in the local state
setTiles(prev => prev.map(tile =>
tile.tile_id === tileId ? { ...tile, domain_id: newDomainId } : tile
));
} catch (err) {
console.error('Failed to update domain:', err);
setError('Failed to update domain.');
}
};
return (
<div>
<div className="panel-section">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h3 style={{ margin: 0 }}>Knowledge Browser</h3>
<button
onClick={handleExportIath}
disabled={isExporting}
style={{
padding: '8px 16px',
fontSize: '14px',
backgroundColor: isExporting ? '#555' : '#007acc',
cursor: isExporting ? 'not-allowed' : 'pointer'
}}
>
{isExporting ? 'Exporting...' : '📦 Export .iath'}
</button>
</div>
<div className="flex flex-col gap-2 mb-4">
<input
type="text"
placeholder="Search tiles by topic or content..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setPage(1); // Reset to first page on search
}}
/>
<select
value={filterDomain}
onChange={(e) => {
setFilterDomain(e.target.value);
setPage(1); // Reset to first page on filter change
}}
>
<option value="">All Domains</option>
{availableDomains.map(domain => (
<option key={domain.domain_id} value={domain.domain_id}>
{domain.name}
</option>
))}
</select>
</div>
</div>
{isLoading && <p>Loading knowledge base... <span className="loading-dots"><span>.</span><span>.</span><span>.</span></span></p>}
{error && <p style={{ color: 'red', marginBottom: '1rem', padding: '0.5rem', backgroundColor: 'rgba(255,0,0,0.1)', borderRadius: '5px' }}>{error}</p>}
{!isLoading && !error && (
<>
<div className="space-y-4">
{tiles.map(tile => (
<TileCard
key={tile.tile_id}
tile={tile}
availableDomains={availableDomains}
onDomainChange={handleDomainUpdate}
/>
))}
</div>
<div className="flex justify-between mt-4">
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)} disabled={!hasMore}>
Next
</button>
</div>
</>
)}
</div>
);
};
export default KnowledgePanel;