diff --git a/app/management/person/dashboard/components/PersonalProjectTable.tsx b/app/management/person/dashboard/components/PersonalProjectTable.tsx index 132ad59..97e8c36 100644 --- a/app/management/person/dashboard/components/PersonalProjectTable.tsx +++ b/app/management/person/dashboard/components/PersonalProjectTable.tsx @@ -186,6 +186,7 @@ export default function PersonalProjectTable() { if (record.project_id === currentRowId) return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))" }} + idAccessor="project_id" scrollAreaProps={{ type: "auto" }} records={pageRecords} onRecordsPerPageChange={setPageSize} diff --git a/app/management/project/audit/components/NewProjectModal.tsx b/app/management/project/audit/components/NewProjectModal.tsx new file mode 100644 index 0000000..051dd07 --- /dev/null +++ b/app/management/project/audit/components/NewProjectModal.tsx @@ -0,0 +1,459 @@ +"use client" + +import { + checkLabelDataSource, + getProjectAdminList, + getProjectTypeList, + projectAdd, +} from "@/components/label/api/project" +import { + getAllVersionByName, + getLabelSchemeList, +} from "@/components/label/api/scheme" +import { usePermissionStore } from "@/components/label/store/auth" +import { + Alert, + Button, + Group, + Modal, + MultiSelect, + Radio, + Select, + Stack, + Switch, + Text, + TextInput, + Textarea, +} from "@mantine/core" +import { useForm } from "@mantine/form" +import { notifications } from "@mantine/notifications" +import { IconAlertTriangle } from "@tabler/icons-react" +import { useEffect, useMemo, useRef, useState } from "react" + +export default function NewProjectModal(props: { + opened: boolean + onCloseAction: (refresh?: boolean) => void +}) { + const { opened, onCloseAction } = props + const userName = usePermissionStore((s) => s.user_name) + + const [typeOpts, setTypeOpts] = useState< + Array<{ label: string; value: string }> + >([]) + const [labelSchemeOpts, setLabelSchemeOpts] = useState< + Array<{ label: string; value: string }> + >([]) + const [versionOpts, setVersionOpts] = useState< + Array<{ label: string; value: string }> + >([]) + const [adminUserOpts, setAdminUserOpts] = useState< + Array<{ label: string; value: string }> + >([]) + const [dataSize, setDataSize] = useState(0) + + const debounceTimerRef = useRef | null>(null) + + const form = useForm({ + initialValues: { + project_name: "", + project_type: "", + is_dynamic: false, + data_source_radio: "" as "" | "0" | "1", + data_source: "", + label_schema_name: "", + label_schema_version: "", + is_embedding: false, + admin_user: [] as string[], + remark: "", + }, + validate: { + project_name: (v) => { + if (!v) return "必填" + if (/^\s*$/.test(v)) return "项目名称不能全为空格" + return null + }, + project_type: (v) => (!v ? "必填" : null), + label_schema_name: (v) => (!v ? "必填" : null), + label_schema_version: (v) => (!v ? "必填" : null), + admin_user: (v) => (v && v.length ? null : "必填"), + }, + }) + + const isDynamic = form.values.is_dynamic + const projectType = form.values.project_type + const labelSchema = form.values.label_schema_name + + useEffect(() => { + if (!opened) return + const initFormOpts = async () => { + try { + const [schemeRes, adminRes, typeRes] = await Promise.all([ + getLabelSchemeList({ page_number: 1, page_size: 5000 } as any), + getProjectAdminList(), + getProjectTypeList(), + ]) + + const schemes = (schemeRes as any)?.label_schema_list ?? [] + setLabelSchemeOpts( + schemes.map((item: any) => ({ label: item.name, value: item.name })) + ) + + setAdminUserOpts( + (adminRes ?? []).map((item: any) => ({ + label: item.label, + value: String(item.value), + })) + ) + + setTypeOpts( + (typeRes ?? []).map((name) => ({ label: name, value: name })) + ) + } catch { + setLabelSchemeOpts([]) + setAdminUserOpts([]) + setTypeOpts([]) + } + } + initFormOpts() + }, [opened]) + + useEffect(() => { + if (!opened) return + const run = async () => { + form.setFieldValue("label_schema_version", "") + setVersionOpts([]) + if (!labelSchema) return + try { + const res = await getAllVersionByName(labelSchema) + const list = (res as any)?.label_schema_version_list ?? [] + setVersionOpts( + list.map((item: any) => ({ + label: item.version, + value: item.version, + })) + ) + } catch { + setVersionOpts([]) + } + } + run() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [labelSchema, opened]) + + useEffect(() => { + if (!opened) return + if (isDynamic) { + form.setFieldValue("data_source_radio", "") + form.setFieldValue("data_source", "") + queueMicrotask(() => setDataSize(0)) + form.clearFieldError("data_source") + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDynamic, opened]) + + const triggerCheckDataSource = (value: string) => { + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) + if (!value) { + setDataSize(0) + form.clearFieldError("data_source") + return + } + const radio = form.values.data_source_radio + if (!projectType) { + setDataSize(0) + form.setFieldError("data_source", "请先选择所属业务") + return + } + if (radio !== "0" && radio !== "1") { + setDataSize(0) + form.setFieldError("data_source", "请先选择标注数据源对应来源类型") + return + } + + debounceTimerRef.current = setTimeout(async () => { + try { + const res = await checkLabelDataSource({ + project_type: projectType, + nfs_path: value, + flag: Number(radio), + }) + const errorCode = (res as any)?.error_code + const size = (res as any)?.data_size ?? 0 + + const errorMap = new Map([ + [1, "nfs路径不存在"], + [2, "nfs路径下不存在data目录"], + [3, "nfs路径下不存在tag目录"], + [4, "tag文件数量小于data文件数量"], + [5, "pattern.json文件不存在"], + [6, "clip.json文件不合法"], + [7, "tag.json文件不合法"], + [8, "pattern.json文件不合法"], + ]) + + if (errorCode === 0) { + setDataSize(size) + form.clearFieldError("data_source") + } else if (errorMap.has(errorCode)) { + setDataSize(0) + form.setFieldError( + "data_source", + errorMap.get(errorCode) || "标注数据源填写有误" + ) + } else { + setDataSize(0) + form.setFieldError("data_source", "标注数据源填写有误") + } + } catch (e) { + setDataSize(0) + form.setFieldError( + "data_source", + e instanceof Error ? e.message : "标注数据源校验失败" + ) + } + }, 1000) + } + + const canSubmit = useMemo(() => { + if (!opened) return false + if (!form.values.project_name || /^\s*$/.test(form.values.project_name)) + return false + if (!form.values.project_type) return false + if (!form.values.label_schema_name || !form.values.label_schema_version) + return false + if (!form.values.admin_user.length) return false + if (!form.values.is_dynamic) { + if (!form.values.data_source_radio) return false + if (!form.values.data_source) return false + if (form.errors.data_source) return false + } + return true + }, [form.errors.data_source, form.values, opened]) + + return ( + onCloseAction()} + title="新增标注任务" + centered + size="50%" + closeOnClickOutside={false}> + + } + title="提示"> + 如标注数据需预标或辅助标注,请先前往数据中心MDS模块进行数据处理 + +
{ + if (!canSubmit) return + const data: any = { ...values } + data.is_dynamic = values.is_dynamic ? 1 : 0 + data.is_embedding = values.is_embedding ? 1 : 0 + data.data_source = values.data_source ?? "" + data.create_user = userName ?? "" + data.data_size = dataSize + data.admin_user = values.admin_user + .map((v) => Number(v)) + .filter((n) => Number.isFinite(n)) + data.remarks = values.remark || undefined + delete data.remark + delete data.data_source_radio + try { + await projectAdd(data) + notifications.show({ + color: "green", + title: "项目创建成功", + message: "已创建项目", + }) + form.reset() + setDataSize(0) + onCloseAction(true) + } catch (e) { + notifications.show({ + color: "red", + title: "创建失败", + message: e instanceof Error ? e.message : "请求失败", + }) + } + })}> +
+ + + + + form.setFieldValue("label_schema_name", v ?? "") + } + error={form.errors.label_schema_name} + /> + +