|
@@ -1,6 +1,6 @@
|
|
|
-import { useState, useEffect, useRef, memo } from 'react'
|
|
|
|
|
-import api, { DatasetInfo } from '../api/client'
|
|
|
|
|
-import { Database, Upload, Loader2 } from 'lucide-react'
|
|
|
|
|
|
|
+import { useState, useEffect, useRef, memo, useCallback } from 'react'
|
|
|
|
|
+import api, { DatasetInfo, KnowledgeBaseItem } from '../api/client'
|
|
|
|
|
+import { Database, Upload, Loader2, FolderOpen } from 'lucide-react'
|
|
|
|
|
|
|
|
const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
|
|
const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
|
|
|
d: DatasetInfo
|
|
d: DatasetInfo
|
|
@@ -56,6 +56,15 @@ export function Datasets() {
|
|
|
const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(null)
|
|
const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(null)
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
|
|
|
|
|
+ // Sample center modal state
|
|
|
|
|
+ const [showSampleCenter, setShowSampleCenter] = useState(false)
|
|
|
|
|
+ const [kbList, setKbList] = useState<KnowledgeBaseItem[]>([])
|
|
|
|
|
+ const [kbLoading, setKbLoading] = useState(false)
|
|
|
|
|
+ const [kbImporting, setKbImporting] = useState<string | null>(null)
|
|
|
|
|
+ const [kbStatus, setKbStatus] = useState('')
|
|
|
|
|
+ const [kbPage, setKbPage] = useState(1)
|
|
|
|
|
+ const [kbTotal, setKbTotal] = useState(0)
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
fetchDatasets()
|
|
fetchDatasets()
|
|
|
}, [])
|
|
}, [])
|
|
@@ -117,6 +126,34 @@ export function Datasets() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const fetchKnowledgeBases = useCallback((page = 1) => {
|
|
|
|
|
+ setKbLoading(true)
|
|
|
|
|
+ api.sampleCenter.listKnowledgeBases(page, 20)
|
|
|
|
|
+ .then(res => {
|
|
|
|
|
+ setKbList(res.items)
|
|
|
|
|
+ setKbTotal(res.total)
|
|
|
|
|
+ setKbPage(res.page)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => setKbStatus(`获取知识库列表失败: ${err.message}`))
|
|
|
|
|
+ .finally(() => setKbLoading(false))
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
|
|
+ const handleImportFromKB = async (kb: KnowledgeBaseItem) => {
|
|
|
|
|
+ setKbImporting(kb.id)
|
|
|
|
|
+ setKbStatus(`正在导入 "${kb.name}" ...`)
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.sampleCenter.importFromKnowledgeBase(kb.id, kb.name)
|
|
|
|
|
+ setKbStatus(`"${kb.name}" 导入请求已提交,可在样本中心查看入库进度`)
|
|
|
|
|
+ // 刷新本地数据集列表
|
|
|
|
|
+ fetchDatasets()
|
|
|
|
|
+ } catch (err: unknown) {
|
|
|
|
|
+ const msg = err instanceof Error ? err.message : '导入失败'
|
|
|
|
|
+ setKbStatus(`导入失败: ${msg}`)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setKbImporting(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div>
|
|
<div>
|
|
|
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
|
|
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
|
|
@@ -200,6 +237,165 @@ export function Datasets() {
|
|
|
}}>{dlStatus}</p>}
|
|
}}>{dlStatus}</p>}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* Sample Center section */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
|
|
|
|
|
+ boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>样本中心</h2>
|
|
|
|
|
+ <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 12px' }}>
|
|
|
|
|
+ 从样本中心导入知识库数据作为训练数据集
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => { setShowSampleCenter(true); fetchKnowledgeBases(1); }}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '10px 20px', borderRadius: 8, border: 'none',
|
|
|
|
|
+ background: '#8b5cf6', color: '#fff', cursor: 'pointer', fontSize: 14, fontWeight: 600,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <FolderOpen size={16} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />
|
|
|
|
|
+ 从样本中心导入
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Sample Center Modal */}
|
|
|
|
|
+ {showSampleCenter && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
|
|
|
|
+ display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
|
|
|
|
|
+ }}
|
|
|
|
|
+ onClick={() => setShowSampleCenter(false)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ onClick={e => e.stopPropagation()}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 700,
|
|
|
|
|
+ maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
|
|
|
|
+ <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>样本中心 - 知识库列表</h2>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setShowSampleCenter(false)}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 20,
|
|
|
|
|
+ color: '#64748b', padding: '4px 8px', borderRadius: 4,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 16px' }}>
|
|
|
|
|
+ 选择要导入的知识库,数据将转为训练格式
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ {/* KB list */}
|
|
|
|
|
+ {kbLoading && (
|
|
|
|
|
+ <div style={{ textAlign: 'center', padding: 20, color: '#94a3b8' }}>
|
|
|
|
|
+ <Loader2 size={24} style={{ animation: 'lucide-spin 1s linear infinite' }} />
|
|
|
|
|
+ <div style={{ marginTop: 8, fontSize: 13 }}>加载中...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {!kbLoading && kbList.length === 0 && (
|
|
|
|
|
+ <div style={{ padding: 20, textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
|
|
|
|
|
+ 暂无可用的知识库
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {!kbLoading && kbList.length > 0 && (
|
|
|
|
|
+ <div style={{ border: '1px solid #e2e8f0', borderRadius: 8, overflow: 'hidden' }}>
|
|
|
|
|
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr style={{ background: '#f5f3ff', borderBottom: '2px solid #e2e8f0', textAlign: 'left' }}>
|
|
|
|
|
+ <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
|
|
|
|
|
+ <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>文档数</th>
|
|
|
|
|
+ <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>状态</th>
|
|
|
|
|
+ <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>字段</th>
|
|
|
|
|
+ <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ {kbList.map(kb => (
|
|
|
|
|
+ <tr key={kb.id} style={{ borderBottom: '1px solid #f1f5f9' }}>
|
|
|
|
|
+ <td style={{ padding: '10px 12px', fontWeight: 500 }}>{kb.name}</td>
|
|
|
|
|
+ <td style={{ padding: '10px 12px', fontSize: 13 }}>{kb.document_count}</td>
|
|
|
|
|
+ <td style={{ padding: '10px 12px', fontSize: 13 }}>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
|
|
|
|
|
+ background: kb.status === 1 ? '#dcfce7' : '#f1f5f9',
|
|
|
|
|
+ color: kb.status === 1 ? '#16a34a' : '#64748b',
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {kb.status === 1 ? '启用' : '禁用'}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', maxWidth: 200 }}>
|
|
|
|
|
+ {kb.metadata_schema.slice(0, 3).map(f => f.field_name_cn).join('、')}
|
|
|
|
|
+ {kb.metadata_schema.length > 3 ? '...' : ''}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td style={{ padding: '10px 12px' }}>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleImportFromKB(kb)}
|
|
|
|
|
+ disabled={kbImporting === kb.id}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
|
|
|
|
|
+ borderRadius: 6, background: kbImporting === kb.id ? '#f5f3ff' : 'transparent',
|
|
|
|
|
+ cursor: kbImporting === kb.id ? 'not-allowed' : 'pointer',
|
|
|
|
|
+ fontSize: 12, fontWeight: 500, opacity: kbImporting === kb.id ? 0.7 : 1,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {kbImporting === kb.id ? (
|
|
|
|
|
+ <><Loader2 size={12} style={{ animation: 'lucide-spin 1s linear infinite', display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />导入中</>
|
|
|
|
|
+ ) : '导入'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Pagination */}
|
|
|
|
|
+ {!kbLoading && kbTotal > 20 && (
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16, alignItems: 'center' }}>
|
|
|
|
|
+ <button
|
|
|
|
|
+ disabled={kbPage <= 1}
|
|
|
|
|
+ onClick={() => fetchKnowledgeBases(kbPage - 1)}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
|
|
|
|
|
+ background: '#fff', cursor: kbPage <= 1 ? 'not-allowed' : 'pointer',
|
|
|
|
|
+ opacity: kbPage <= 1 ? 0.5 : 1, fontSize: 13,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >上一页</button>
|
|
|
|
|
+ <span style={{ fontSize: 13, color: '#64748b' }}>
|
|
|
|
|
+ 第 {kbPage} 页 / 共 {Math.ceil(kbTotal / 20)} 页
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ disabled={kbPage * 20 >= kbTotal}
|
|
|
|
|
+ onClick={() => fetchKnowledgeBases(kbPage + 1)}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
|
|
|
|
|
+ background: '#fff', cursor: kbPage * 20 >= kbTotal ? 'not-allowed' : 'pointer',
|
|
|
|
|
+ opacity: kbPage * 20 >= kbTotal ? 0.5 : 1, fontSize: 13,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >下一页</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Status */}
|
|
|
|
|
+ {kbStatus && (
|
|
|
|
|
+ <p style={{
|
|
|
|
|
+ marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 13,
|
|
|
|
|
+ background: kbStatus.includes('失败') ? '#fff1f2' : '#f0fdf4',
|
|
|
|
|
+ color: kbStatus.includes('失败') ? '#e11d48' : '#16a34a',
|
|
|
|
|
+ border: `1px solid ${kbStatus.includes('失败') ? '#fecdd3' : '#bbf7d0'}`,
|
|
|
|
|
+ }}>{kbStatus}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{/* Dataset list */}
|
|
{/* Dataset list */}
|
|
|
<div style={{ marginTop: 24 }}>
|
|
<div style={{ marginTop: 24 }}>
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|