Spaces:
Running
Running
| // 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; | |