|
@@ -1,6 +1,6 @@
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
-import api, { DatasetInfo, AnnotationProjectItem, DatasetDownloadTaskResponse } from '../api/client'
|
|
|
|
|
-import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle, Eye, Trash2, FileText } from 'lucide-react'
|
|
|
|
|
|
|
+import api, { DatasetInfo, AnnotationProjectItem, AnnotationProjectDetailResponse, DatasetDownloadTaskResponse } from '../api/client'
|
|
|
|
|
+import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle, Eye, Trash2, FileText, X } from 'lucide-react'
|
|
|
|
|
|
|
|
function formatBadge(format: string) {
|
|
function formatBadge(format: string) {
|
|
|
const map: Record<string, { bg: string; color: string; border: string }> = {
|
|
const map: Record<string, { bg: string; color: string; border: string }> = {
|
|
@@ -122,6 +122,7 @@ export function Datasets() {
|
|
|
const [downloading, setDownloading] = useState(false)
|
|
const [downloading, setDownloading] = useState(false)
|
|
|
const [loading, setLoading] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
|
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 [previewError, setPreviewError] = useState<string | null>(null)
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
|
|
|
// Annotation platform modal state
|
|
// Annotation platform modal state
|
|
@@ -133,6 +134,11 @@ export function Datasets() {
|
|
|
const [projectPage, setProjectPage] = useState(1)
|
|
const [projectPage, setProjectPage] = useState(1)
|
|
|
const [projectTotal, setProjectTotal] = useState(0)
|
|
const [projectTotal, setProjectTotal] = useState(0)
|
|
|
|
|
|
|
|
|
|
+ // Project detail modal
|
|
|
|
|
+ const [projectDetail, setProjectDetail] = useState<AnnotationProjectDetailResponse | null>(null)
|
|
|
|
|
+ const [showDetail, setShowDetail] = useState(false)
|
|
|
|
|
+ const [detailLoading, setDetailLoading] = useState(false)
|
|
|
|
|
+
|
|
|
// Active downloads tracking
|
|
// Active downloads tracking
|
|
|
const [activeDownloads, setActiveDownloads] = useState<Map<string, DatasetDownloadTaskResponse>>(new Map())
|
|
const [activeDownloads, setActiveDownloads] = useState<Map<string, DatasetDownloadTaskResponse>>(new Map())
|
|
|
const downloadPollIntervals = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
|
const downloadPollIntervals = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
|
@@ -233,9 +239,20 @@ export function Datasets() {
|
|
|
}, [])
|
|
}, [])
|
|
|
|
|
|
|
|
const handlePreview = (id: string) => {
|
|
const handlePreview = (id: string) => {
|
|
|
|
|
+ setPreviewError(null)
|
|
|
api.datasets.preview(id, 10)
|
|
api.datasets.preview(id, 10)
|
|
|
- .then(res => setPreviewData({ columns: res.columns, rows: res.preview_rows }))
|
|
|
|
|
- .catch(() => setPreviewData(null))
|
|
|
|
|
|
|
+ .then(res => {
|
|
|
|
|
+ if (!res.columns || res.columns.length === 0) {
|
|
|
|
|
+ setPreviewError('该数据集没有可预览的列,可能格式不受支持')
|
|
|
|
|
+ setPreviewData(null)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ setPreviewData({ columns: res.columns, rows: res.preview_rows })
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => {
|
|
|
|
|
+ setPreviewError(`预览失败: ${err.message || '未知错误'}`)
|
|
|
|
|
+ setPreviewData(null)
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleDelete = async (id: string) => {
|
|
const handleDelete = async (id: string) => {
|
|
@@ -276,6 +293,20 @@ export function Datasets() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const handleViewProjectDetail = async (project: AnnotationProjectItem) => {
|
|
|
|
|
+ setDetailLoading(true)
|
|
|
|
|
+ setShowDetail(true)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await api.annotationPlatform.getProjectDetail(project.project_id)
|
|
|
|
|
+ setProjectDetail(detail)
|
|
|
|
|
+ } catch (err: unknown) {
|
|
|
|
|
+ setProjectStatus(`获取项目详情失败: ${err instanceof Error ? err.message : '未知错误'}`)
|
|
|
|
|
+ setShowDetail(false)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setDetailLoading(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div>
|
|
<div>
|
|
|
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
|
|
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
|
|
@@ -504,13 +535,21 @@ export function Datasets() {
|
|
|
{p.completed_task_count}/{p.task_count}
|
|
{p.completed_task_count}/{p.task_count}
|
|
|
<span style={{ marginLeft: 4, fontSize: 11, color: '#94a3b8' }}>({progress}%)</span>
|
|
<span style={{ marginLeft: 4, fontSize: 11, color: '#94a3b8' }}>({progress}%)</span>
|
|
|
</td>
|
|
</td>
|
|
|
- <td style={{ padding: '10px 12px' }}>
|
|
|
|
|
|
|
+ <td style={{ padding: '10px 12px', display: 'flex', gap: 6 }}>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleViewProjectDetail(p)}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '4px 10px', color: '#0ea5e9', border: '1px solid #0ea5e9',
|
|
|
|
|
+ borderRadius: 6, background: 'transparent', cursor: 'pointer',
|
|
|
|
|
+ fontSize: 12, fontWeight: 500,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >详情</button>
|
|
|
{isText ? (
|
|
{isText ? (
|
|
|
<button
|
|
<button
|
|
|
onClick={() => handleImportProject(p)}
|
|
onClick={() => handleImportProject(p)}
|
|
|
disabled={projectImporting === p.project_id}
|
|
disabled={projectImporting === p.project_id}
|
|
|
style={{
|
|
style={{
|
|
|
- padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
|
|
|
|
|
|
|
+ padding: '4px 10px', color: '#8b5cf6', border: '1px solid #8b5cf6',
|
|
|
borderRadius: 6, background: projectImporting === p.project_id ? '#f5f3ff' : 'transparent',
|
|
borderRadius: 6, background: projectImporting === p.project_id ? '#f5f3ff' : 'transparent',
|
|
|
cursor: projectImporting === p.project_id ? 'not-allowed' : 'pointer',
|
|
cursor: projectImporting === p.project_id ? 'not-allowed' : 'pointer',
|
|
|
fontSize: 12, fontWeight: 500, opacity: projectImporting === p.project_id ? 0.7 : 1,
|
|
fontSize: 12, fontWeight: 500, opacity: projectImporting === p.project_id ? 0.7 : 1,
|
|
@@ -521,7 +560,7 @@ export function Datasets() {
|
|
|
) : '导入'}
|
|
) : '导入'}
|
|
|
</button>
|
|
</button>
|
|
|
) : (
|
|
) : (
|
|
|
- <span style={{ fontSize: 12, color: '#94a3b8' }}>不支持训练</span>
|
|
|
|
|
|
|
+ <span style={{ fontSize: 12, color: '#94a3b8', padding: '4px 0' }}>不支持训练</span>
|
|
|
)}
|
|
)}
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
@@ -570,6 +609,119 @@ export function Datasets() {
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {/* Project Detail Modal */}
|
|
|
|
|
+ {showDetail && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
|
|
|
|
+ display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1001,
|
|
|
|
|
+ }}
|
|
|
|
|
+ onClick={() => { setShowDetail(false); setProjectDetail(null) }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ onClick={e => e.stopPropagation()}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 500,
|
|
|
|
|
+ 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={() => { setShowDetail(false); setProjectDetail(null) }}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 20,
|
|
|
|
|
+ color: '#64748b', padding: '4px 8px', borderRadius: 4,
|
|
|
|
|
+ }}
|
|
|
|
|
+ ><X size={18} /></button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {detailLoading ? (
|
|
|
|
|
+ <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>
|
|
|
|
|
+ ) : projectDetail ? (() => {
|
|
|
|
|
+ const d = projectDetail
|
|
|
|
|
+ const progress = d.task_count > 0 ? Math.round((d.completed_task_count / d.task_count) * 100) : 0
|
|
|
|
|
+ const isText = d.project_type === 'text'
|
|
|
|
|
+ const statusMap: Record<string, { bg: string; color: string }> = {
|
|
|
|
|
+ completed: { bg: '#dcfce7', color: '#16a34a' },
|
|
|
|
|
+ in_progress: { bg: '#dbeafe', color: '#2563eb' },
|
|
|
|
|
+ ready: { bg: '#fef3c7', color: '#d97706' },
|
|
|
|
|
+ configuring: { bg: '#f1f5f9', color: '#64748b' },
|
|
|
|
|
+ draft: { bg: '#f1f5f9', color: '#64748b' },
|
|
|
|
|
+ }
|
|
|
|
|
+ const st = statusMap[d.status] || { bg: '#f1f5f9', color: '#64748b' }
|
|
|
|
|
+
|
|
|
|
|
+ const fields = [
|
|
|
|
|
+ ['项目 ID', d.project_id],
|
|
|
|
|
+ ['项目名称', d.project_name],
|
|
|
|
|
+ ['描述', d.description || '-'],
|
|
|
|
|
+ ['类型', isText ? '文本' : '图片'],
|
|
|
|
|
+ ['任务类型', d.task_type?.replace(/_/g, ' ') || '-'],
|
|
|
|
|
+ ['状态', d.status, true],
|
|
|
|
|
+ ['创建人', d.created_by],
|
|
|
|
|
+ ['创建时间', d.created_at],
|
|
|
|
|
+ ['更新时间', d.updated_at],
|
|
|
|
|
+ ['总任务数', String(d.task_count)],
|
|
|
|
|
+ ['已完成', String(d.completed_task_count)],
|
|
|
|
|
+ ['已分配', String(d.assigned_task_count)],
|
|
|
|
|
+ ['完成率', `${progress}%`],
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
|
|
|
+ {fields.map(([label, value, isStatus], idx) => (
|
|
|
|
|
+ <div key={idx} style={{ display: 'flex', gap: 12, fontSize: 13 }}>
|
|
|
|
|
+ <span style={{ color: '#94a3b8', minWidth: 80, flexShrink: 0 }}>{label}</span>
|
|
|
|
|
+ <span style={{ color: '#1e293b', fontWeight: isStatus ? 500 : 400 }}>
|
|
|
|
|
+ {isStatus ? (
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
|
|
|
|
|
+ background: st.bg, color: st.color,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {value === 'completed' ? '已完成' : value === 'in_progress' ? '进行中' : value}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : value}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ {isText && (
|
|
|
|
|
+ <div style={{ marginTop: 8, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setShowDetail(false)
|
|
|
|
|
+ setProjectDetail(null)
|
|
|
|
|
+ handleImportProject({
|
|
|
|
|
+ project_id: d.project_id,
|
|
|
|
|
+ project_name: d.project_name,
|
|
|
|
|
+ project_type: d.project_type,
|
|
|
|
|
+ task_type: d.task_type,
|
|
|
|
|
+ status: d.status,
|
|
|
|
|
+ task_count: d.task_count,
|
|
|
|
|
+ completed_task_count: d.completed_task_count,
|
|
|
|
|
+ } as AnnotationProjectItem)
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '6px 16px', borderRadius: 6, border: 'none',
|
|
|
|
|
+ background: '#8b5cf6', color: '#fff', cursor: 'pointer',
|
|
|
|
|
+ fontSize: 13, fontWeight: 500,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >导入为训练数据</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ })() : (
|
|
|
|
|
+ <div style={{ padding: 20, textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
|
|
|
|
|
+ 暂无详情
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </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 }}>
|
|
@@ -611,6 +763,14 @@ export function Datasets() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Preview */}
|
|
{/* Preview */}
|
|
|
|
|
+ {previewError && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ marginTop: 24, padding: '12px 16px', borderRadius: 8, fontSize: 13,
|
|
|
|
|
+ background: '#fff1f2', color: '#e11d48', border: '1px solid #fecdd3',
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {previewError}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
{previewData && previewData.rows.length > 0 && (
|
|
{previewData && previewData.rows.length > 0 && (
|
|
|
<div style={{
|
|
<div style={{
|
|
|
marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
|
|
marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
|