import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react'; import { useRouter } from 'next/router'; import { getModelById, delModelById, postTrainModel, putModelTrainingStatus } from '@/api/model'; import { getChatSiteId } from '@/api/chat'; import type { ModelType } from '@/types/model'; import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; import { useConfirm } from '@/hooks/useConfirm'; import { formatModelStatus, ModelStatusEnum, OpenAiList } from '@/constants/model'; import { useGlobalStore } from '@/store/global'; import { useScreen } from '@/hooks/useScreen'; import ModelEditForm from './components/ModelEditForm'; import Icon from '@/components/Icon'; import dynamic from 'next/dynamic'; const Training = dynamic(() => import('./components/Training')); const ModelDetail = () => { const { toast } = useToast(); const router = useRouter(); const { isPc, media } = useScreen(); const { setLoading } = useGlobalStore(); const { openConfirm, ConfirmChild } = useConfirm({ content: '确认删除该模型?' }); const SelectFileDom = useRef(null); const { modelId } = router.query as { modelId: string }; const [model, setModel] = useState(); const canTrain = useMemo(() => { const openai = OpenAiList.find((item) => item.model === model?.service.modelName); return openai && openai.canTraining === true; }, [model]); /* 加载模型数据 */ const loadModel = useCallback(async () => { if (!modelId) return; setLoading(true); try { const res = await getModelById(modelId as string); res.security.expiredTime /= 60 * 60 * 1000; setModel(res); } catch (err) { console.error(err); } setLoading(false); }, [modelId, setLoading]); useEffect(() => { loadModel(); }, [loadModel, modelId]); /* 点击删除 */ const handleDelModel = useCallback(async () => { if (!model) return; setLoading(true); try { await delModelById(model._id); toast({ title: '删除成功', status: 'success' }); router.replace('/model/list'); } catch (err) { console.error(err); } setLoading(false); }, [setLoading, model, router, toast]); /* 点前往聊天预览页 */ const handlePreviewChat = useCallback(async () => { if (!model) return; setLoading(true); try { const chatId = await getChatSiteId(model._id); router.push(`/chat?chatId=${chatId}`); } catch (err) { console.error(err); } setLoading(false); }, [setLoading, model, router]); /* 上传数据集,触发微调 */ const startTraining = useCallback( async (e: React.ChangeEvent) => { if (!modelId || !e.target.files || e.target.files?.length === 0) return; setLoading(true); try { const file = e.target.files[0]; const formData = new FormData(); formData.append('file', file); await postTrainModel(modelId, formData); toast({ title: '开始训练,大约需要 30 分钟', status: 'success' }); // 重新获取模型 loadModel(); } catch (err) { toast({ title: typeof err === 'string' ? err : '文件格式错误', status: 'error' }); console.error(err); } setLoading(false); }, [setLoading, loadModel, modelId, toast] ); /* 点击更新模型状态 */ const handleClickUpdateStatus = useCallback(async () => { if (!model || model.status !== ModelStatusEnum.training) return; setLoading(true); try { await putModelTrainingStatus(model._id); loadModel(); } catch (error: any) { console.error(error); toast({ title: error.message || '更新失败', status: 'error' }); } setLoading(false); }, [model, setLoading, loadModel, toast]); return ( <> {/* 头部 */} {isPc ? ( {model?.name || '模型'} 配置 {!!model && ( {formatModelStatus[model.status].text} )} ) : ( <> {model?.name || '模型'} 配置 {!!model && ( {formatModelStatus[model.status].text} )} )} {/* 基本信息编辑 */} {/* 其他配置 */} {!!model && } 神奇操作 模型微调: 下载模板 {/* 提示 */} 每行包括一个 prompt 和一个 completion prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt 内容不都是同一个标点结尾,可以加一个空格打断相同性, completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。 删除模型: ); }; export default ModelDetail;