|
|
@@ -1,5 +1,5 @@
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
-import api, { DatasetInfo, KnowledgeBaseItem, DatasetDownloadTaskResponse } from '../api/client'
|
|
|
+import api, { DatasetInfo, AnnotationProjectItem, DatasetDownloadTaskResponse } from '../api/client'
|
|
|
import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle, Eye, Trash2, FileText } from 'lucide-react'
|
|
|
|
|
|
function formatBadge(format: string) {
|
|
|
@@ -124,14 +124,14 @@ export function Datasets() {
|
|
|
const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(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)
|
|
|
+ // Annotation platform modal state
|
|
|
+ const [showAnnotationPlatform, setShowAnnotationPlatform] = useState(false)
|
|
|
+ const [projectList, setProjectList] = useState<AnnotationProjectItem[]>([])
|
|
|
+ const [projectLoading, setProjectLoading] = useState(false)
|
|
|
+ const [projectImporting, setProjectImporting] = useState<string | null>(null)
|
|
|
+ const [projectStatus, setProjectStatus] = useState('')
|
|
|
+ const [projectPage, setProjectPage] = useState(1)
|
|
|
+ const [projectTotal, setProjectTotal] = useState(0)
|
|
|
|
|
|
// Active downloads tracking
|
|
|
const [activeDownloads, setActiveDownloads] = useState<Map<string, DatasetDownloadTaskResponse>>(new Map())
|
|
|
@@ -249,30 +249,30 @@ export function Datasets() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const fetchKnowledgeBases = useCallback((page = 1) => {
|
|
|
- setKbLoading(true)
|
|
|
- api.sampleCenter.listKnowledgeBases(page, 20)
|
|
|
+ const fetchProjects = useCallback((page = 1) => {
|
|
|
+ setProjectLoading(true)
|
|
|
+ api.annotationPlatform.listProjects(page, 20)
|
|
|
.then(res => {
|
|
|
- setKbList(res.items)
|
|
|
- setKbTotal(res.total)
|
|
|
- setKbPage(res.page)
|
|
|
+ setProjectList(res.items)
|
|
|
+ setProjectTotal(res.total)
|
|
|
+ setProjectPage(res.page)
|
|
|
})
|
|
|
- .catch(err => setKbStatus(`获取知识库列表失败: ${err.message}`))
|
|
|
- .finally(() => setKbLoading(false))
|
|
|
+ .catch(err => setProjectStatus(`获取项目列表失败: ${err.message}`))
|
|
|
+ .finally(() => setProjectLoading(false))
|
|
|
}, [])
|
|
|
|
|
|
- const handleImportFromKB = async (kb: KnowledgeBaseItem) => {
|
|
|
- setKbImporting(kb.id)
|
|
|
- setKbStatus(`正在导入 "${kb.name}" ...`)
|
|
|
+ const handleImportProject = async (project: AnnotationProjectItem) => {
|
|
|
+ setProjectImporting(project.project_id)
|
|
|
+ setProjectStatus(`正在导入 "${project.project_name}" ...`)
|
|
|
try {
|
|
|
- await api.sampleCenter.importFromKnowledgeBase(kb.id, kb.name)
|
|
|
- setKbStatus(`"${kb.name}" 导入请求已提交,可在样本中心查看入库进度`)
|
|
|
+ const res = await api.annotationPlatform.importProject(project.project_id, project.project_name, 'alpaca')
|
|
|
+ setProjectStatus(`"${res.dataset_name}" 导入成功,共 ${res.total_exported} 条数据`)
|
|
|
fetchDatasets()
|
|
|
} catch (err: unknown) {
|
|
|
const msg = err instanceof Error ? err.message : '导入失败'
|
|
|
- setKbStatus(`导入失败: ${msg}`)
|
|
|
+ setProjectStatus(`导入失败: ${msg}`)
|
|
|
} finally {
|
|
|
- setKbImporting(null)
|
|
|
+ setProjectImporting(null)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -398,47 +398,47 @@ export function Datasets() {
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {/* Sample Center section */}
|
|
|
+ {/* Annotation Platform 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>
|
|
|
+ <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); }}
|
|
|
+ onClick={() => { setShowAnnotationPlatform(true); fetchProjects(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 && (
|
|
|
+ {/* Annotation Platform Modal */}
|
|
|
+ {showAnnotationPlatform && (
|
|
|
<div
|
|
|
style={{
|
|
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
|
|
|
}}
|
|
|
- onClick={() => setShowSampleCenter(false)}
|
|
|
+ onClick={() => setShowAnnotationPlatform(false)}
|
|
|
>
|
|
|
<div
|
|
|
onClick={e => e.stopPropagation()}
|
|
|
style={{
|
|
|
- background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 700,
|
|
|
+ background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 750,
|
|
|
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>
|
|
|
+ <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>标注平台 - 项目列表</h2>
|
|
|
<button
|
|
|
- onClick={() => setShowSampleCenter(false)}
|
|
|
+ onClick={() => setShowAnnotationPlatform(false)}
|
|
|
style={{
|
|
|
border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 20,
|
|
|
color: '#64748b', padding: '4px 8px', borderRadius: 4,
|
|
|
@@ -447,108 +447,124 @@ export function Datasets() {
|
|
|
</div>
|
|
|
|
|
|
<p style={{ fontSize: 13, color: '#64748b', margin: '0 0 16px' }}>
|
|
|
- 选择要导入的知识库,数据将转为训练格式
|
|
|
+ 选择要导入的标注项目,文本项目将转为 Alpaca 格式训练数据
|
|
|
</p>
|
|
|
|
|
|
- {kbLoading && (
|
|
|
+ {projectLoading && (
|
|
|
<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 && (
|
|
|
+ {!projectLoading && projectList.length === 0 && (
|
|
|
<div style={{ padding: 20, textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
|
|
|
- 暂无可用的知识库
|
|
|
+ 暂无可用的标注项目
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {!kbLoading && kbList.length > 0 && (
|
|
|
+ {!projectLoading && projectList.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>
|
|
|
- <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>
|
|
|
- ))}
|
|
|
+ {projectList.map(p => {
|
|
|
+ const isText = p.project_type === 'text'
|
|
|
+ const progress = p.task_count > 0 ? Math.round((p.completed_task_count / p.task_count) * 100) : 0
|
|
|
+ return (
|
|
|
+ <tr key={p.project_id} style={{ borderBottom: '1px solid #f1f5f9' }}>
|
|
|
+ <td style={{ padding: '10px 12px', fontWeight: 500 }}>{p.project_name}</td>
|
|
|
+ <td style={{ padding: '10px 12px' }}>
|
|
|
+ <span style={{
|
|
|
+ display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
|
|
|
+ background: isText ? '#dbeafe' : '#fef3c7',
|
|
|
+ color: isText ? '#2563eb' : '#d97706',
|
|
|
+ }}>
|
|
|
+ {isText ? '文本' : '图片'}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td style={{ padding: '10px 12px', fontSize: 13 }}>
|
|
|
+ <span style={{
|
|
|
+ display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
|
|
|
+ background: p.status === 'completed' ? '#dcfce7' : p.status === 'in_progress' ? '#dbeafe' : '#f1f5f9',
|
|
|
+ color: p.status === 'completed' ? '#16a34a' : p.status === 'in_progress' ? '#2563eb' : '#64748b',
|
|
|
+ }}>
|
|
|
+ {p.status === 'completed' ? '已完成' : p.status === 'in_progress' ? '进行中' : p.status}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td style={{ padding: '10px 12px', fontSize: 13, color: '#64748b' }}>
|
|
|
+ {p.completed_task_count}/{p.task_count}
|
|
|
+ <span style={{ marginLeft: 4, fontSize: 11, color: '#94a3b8' }}>({progress}%)</span>
|
|
|
+ </td>
|
|
|
+ <td style={{ padding: '10px 12px' }}>
|
|
|
+ {isText ? (
|
|
|
+ <button
|
|
|
+ onClick={() => handleImportProject(p)}
|
|
|
+ disabled={projectImporting === p.project_id}
|
|
|
+ style={{
|
|
|
+ padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
|
|
|
+ borderRadius: 6, background: projectImporting === p.project_id ? '#f5f3ff' : 'transparent',
|
|
|
+ cursor: projectImporting === p.project_id ? 'not-allowed' : 'pointer',
|
|
|
+ fontSize: 12, fontWeight: 500, opacity: projectImporting === p.project_id ? 0.7 : 1,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {projectImporting === p.project_id ? (
|
|
|
+ <><Loader2 size={12} style={{ animation: 'lucide-spin 1s linear infinite', display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />导入中</>
|
|
|
+ ) : '导入'}
|
|
|
+ </button>
|
|
|
+ ) : (
|
|
|
+ <span style={{ fontSize: 12, color: '#94a3b8' }}>不支持训练</span>
|
|
|
+ )}
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ )
|
|
|
+ })}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {!kbLoading && kbTotal > 20 && (
|
|
|
+ {!projectLoading && projectTotal > 20 && (
|
|
|
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16, alignItems: 'center' }}>
|
|
|
<button
|
|
|
- disabled={kbPage <= 1}
|
|
|
- onClick={() => fetchKnowledgeBases(kbPage - 1)}
|
|
|
+ disabled={projectPage <= 1}
|
|
|
+ onClick={() => fetchProjects(projectPage - 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,
|
|
|
+ background: '#fff', cursor: projectPage <= 1 ? 'not-allowed' : 'pointer',
|
|
|
+ opacity: projectPage <= 1 ? 0.5 : 1, fontSize: 13,
|
|
|
}}
|
|
|
>上一页</button>
|
|
|
<span style={{ fontSize: 13, color: '#64748b' }}>
|
|
|
- 第 {kbPage} 页 / 共 {Math.ceil(kbTotal / 20)} 页
|
|
|
+ 第 {projectPage} 页 / 共 {Math.ceil(projectTotal / 20)} 页
|
|
|
</span>
|
|
|
<button
|
|
|
- disabled={kbPage * 20 >= kbTotal}
|
|
|
- onClick={() => fetchKnowledgeBases(kbPage + 1)}
|
|
|
+ disabled={projectPage * 20 >= projectTotal}
|
|
|
+ onClick={() => fetchProjects(projectPage + 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,
|
|
|
+ background: '#fff', cursor: projectPage * 20 >= projectTotal ? 'not-allowed' : 'pointer',
|
|
|
+ opacity: projectPage * 20 >= projectTotal ? 0.5 : 1, fontSize: 13,
|
|
|
}}
|
|
|
>下一页</button>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {kbStatus && (
|
|
|
+ {projectStatus && (
|
|
|
<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>
|
|
|
+ background: projectStatus.includes('失败') ? '#fff1f2' : '#f0fdf4',
|
|
|
+ color: projectStatus.includes('失败') ? '#e11d48' : '#16a34a',
|
|
|
+ border: `1px solid ${projectStatus.includes('失败') ? '#fecdd3' : '#bbf7d0'}`,
|
|
|
+ }}>{projectStatus}</p>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|