460 lines
14 KiB
TypeScript
460 lines
14 KiB
TypeScript
"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<number>(0)
|
||
|
||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | 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<number, string>([
|
||
[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 (
|
||
<Modal
|
||
opened={opened}
|
||
onClose={() => onCloseAction()}
|
||
title="新增标注任务"
|
||
centered
|
||
size="50%"
|
||
closeOnClickOutside={false}>
|
||
<Stack gap="sm">
|
||
<Alert
|
||
color="yellow"
|
||
radius="xs"
|
||
p="xs"
|
||
icon={<IconAlertTriangle size={16} />}
|
||
title="提示">
|
||
如标注数据需预标或辅助标注,请先前往数据中心MDS模块进行数据处理
|
||
</Alert>
|
||
<form
|
||
onSubmit={form.onSubmit(async (values) => {
|
||
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 : "请求失败",
|
||
})
|
||
}
|
||
})}>
|
||
<div
|
||
style={{ maxHeight: "58vh", overflowY: "auto", paddingRight: 6 }}>
|
||
<Stack gap="sm">
|
||
<TextInput
|
||
label="项目名称"
|
||
radius="xs"
|
||
size="xs"
|
||
placeholder="请输入项目名称"
|
||
withAsterisk
|
||
{...form.getInputProps("project_name")}
|
||
/>
|
||
|
||
<Select
|
||
label="所属业务"
|
||
radius="xs"
|
||
size="xs"
|
||
placeholder="请选择所属业务"
|
||
data={typeOpts}
|
||
searchable
|
||
clearable
|
||
withAsterisk
|
||
value={form.values.project_type || null}
|
||
onChange={(v) => form.setFieldValue("project_type", v ?? "")}
|
||
error={form.errors.project_type}
|
||
/>
|
||
|
||
<Switch
|
||
label="是否为动态任务"
|
||
size="sm"
|
||
checked={form.values.is_dynamic}
|
||
onChange={(e) =>
|
||
form.setFieldValue("is_dynamic", e.currentTarget.checked)
|
||
}
|
||
/>
|
||
|
||
{!isDynamic ? (
|
||
<>
|
||
<Radio.Group
|
||
label="标注数据源"
|
||
size="xs"
|
||
withAsterisk
|
||
value={form.values.data_source_radio}
|
||
onChange={(v) => {
|
||
const next = v as "0" | "1" | ""
|
||
form.setFieldValue("data_source_radio", next)
|
||
if (form.values.data_source)
|
||
triggerCheckDataSource(form.values.data_source)
|
||
}}>
|
||
<Group gap="md" mt={6}>
|
||
<Radio
|
||
value="0"
|
||
label="新建数据源"
|
||
size="xs"
|
||
disabled={!projectType}
|
||
/>
|
||
<Radio
|
||
value="1"
|
||
label="现存数据源"
|
||
size="xs"
|
||
disabled={!projectType}
|
||
/>
|
||
</Group>
|
||
</Radio.Group>
|
||
|
||
<TextInput
|
||
label="标注数据源"
|
||
size="xs"
|
||
radius="xs"
|
||
placeholder="请输入标注数据源"
|
||
withAsterisk
|
||
disabled={!projectType}
|
||
value={form.values.data_source}
|
||
onChange={(e) => {
|
||
const v = e.currentTarget.value
|
||
form.setFieldValue("data_source", v)
|
||
triggerCheckDataSource(v)
|
||
}}
|
||
error={form.errors.data_source}
|
||
rightSection={
|
||
dataSize > 0 && form.values.data_source ? (
|
||
<Text size="xs" c="blue" style={{ paddingRight: 6 }}>
|
||
{dataSize}
|
||
</Text>
|
||
) : undefined
|
||
}
|
||
/>
|
||
</>
|
||
) : null}
|
||
|
||
<Select
|
||
label="标注方案"
|
||
size="xs"
|
||
radius="xs"
|
||
placeholder="请选择标注方案"
|
||
data={labelSchemeOpts}
|
||
searchable
|
||
clearable
|
||
withAsterisk
|
||
value={form.values.label_schema_name || null}
|
||
onChange={(v) =>
|
||
form.setFieldValue("label_schema_name", v ?? "")
|
||
}
|
||
error={form.errors.label_schema_name}
|
||
/>
|
||
|
||
<Select
|
||
label="方案版本"
|
||
size="xs"
|
||
radius="xs"
|
||
placeholder="请选择标注方案版本号"
|
||
data={versionOpts}
|
||
searchable
|
||
clearable
|
||
withAsterisk
|
||
value={form.values.label_schema_version || null}
|
||
onChange={(v) =>
|
||
form.setFieldValue("label_schema_version", v ?? "")
|
||
}
|
||
error={form.errors.label_schema_version}
|
||
/>
|
||
|
||
<Switch
|
||
label="是否生成embedding"
|
||
size="sm"
|
||
checked={form.values.is_embedding}
|
||
onChange={(e) =>
|
||
form.setFieldValue("is_embedding", e.currentTarget.checked)
|
||
}
|
||
/>
|
||
|
||
<MultiSelect
|
||
label="标注管理员"
|
||
size="xs"
|
||
radius="xs"
|
||
placeholder="请选择标注管理员"
|
||
data={adminUserOpts}
|
||
searchable
|
||
clearable
|
||
withAsterisk
|
||
value={form.values.admin_user}
|
||
onChange={(v) => form.setFieldValue("admin_user", v)}
|
||
error={form.errors.admin_user}
|
||
/>
|
||
|
||
<Textarea
|
||
label="备注"
|
||
size="xs"
|
||
radius="xs"
|
||
placeholder="请输入"
|
||
maxLength={50}
|
||
autosize
|
||
minRows={3}
|
||
value={form.values.remark}
|
||
onChange={(e) =>
|
||
form.setFieldValue("remark", e.currentTarget.value)
|
||
}
|
||
/>
|
||
</Stack>
|
||
</div>
|
||
|
||
<Group justify="flex-end" mt="md" gap="sm">
|
||
<Button
|
||
variant="default"
|
||
size="xs"
|
||
radius="xs"
|
||
onClick={() => onCloseAction()}>
|
||
取消
|
||
</Button>
|
||
<Button type="submit" size="xs" radius="xs" disabled={!canSubmit}>
|
||
确定
|
||
</Button>
|
||
</Group>
|
||
</form>
|
||
</Stack>
|
||
</Modal>
|
||
)
|
||
}
|