diff --git a/src/frontend/platform/src/components/LinSight/SopFormDrawer.tsx b/src/frontend/platform/src/components/LinSight/SopFormDrawer.tsx index 412784136..caa7446b2 100644 --- a/src/frontend/platform/src/components/LinSight/SopFormDrawer.tsx +++ b/src/frontend/platform/src/components/LinSight/SopFormDrawer.tsx @@ -146,7 +146,7 @@ const SopFormDrawer = ({ ref={nameInputRef} value={sopForm.name} onChange={(e) => handleInputChange('name', e.target.value)} - className={`mt-1 block w-full border ${errors.name ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-lg`} + className={`mt-1 block w-full border ${errors.name ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-[16px]`} placeholder="请输入SOP名称" />
@@ -166,7 +166,7 @@ const SopFormDrawer = ({ rows={3} value={sopForm.description} onChange={(e) => handleInputChange('description', e.target.value)} - className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-lg" + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-[16px]" placeholder="请输入SOP描述" />
diff --git a/src/frontend/platform/src/components/LinSight/SopFromRecord.tsx b/src/frontend/platform/src/components/LinSight/SopFromRecord.tsx index c44187c8f..05ca4b0e8 100644 --- a/src/frontend/platform/src/components/LinSight/SopFromRecord.tsx +++ b/src/frontend/platform/src/components/LinSight/SopFromRecord.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { LoadIcon } from '../bs-icons/loading'; import AutoPagination from '../bs-ui/pagination/autoPagination'; import SopMarkdown from './SopMarkdown'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../bs-ui/tooltip'; interface SopRecord { id: number; @@ -24,7 +25,7 @@ interface SopRecord { user_name: string; } -export default function ImportFromRecordsDialog({ open, tools, onOpenChange, onSuccess }) { +export default function ImportFromRecordsDialog({ open, tools, onOpenChange, onSuccess, setDuplicateNames, duplicateNames, duplicateDialogOpen, setDuplicateDialogOpen, importFormData }) { const { toast } = useToast(); const [searchTerm, setSearchTerm] = useState(''); const [records, setRecords] = useState([]); @@ -37,45 +38,34 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on key: 'create_time', direction: 'desc' as 'asc' | 'desc' }); - const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false); - const [duplicateNames, setDuplicateNames] = useState([]); + const [pageInputValue, setPageInputValue] = useState(page.toString()); const [selectedRecordIds, setSelectedRecordIds] = useState([]); - + const [allRecords, setAllRecords] = useState([]); + const [selectedRecords, setSelectedRecords] = useState([]); + const [isSavingAsNew, setIsSavingAsNew] = useState(false); + const [isOverwriting, setIsOverwriting] = useState(false); // 获取SOP记录 - const fetchRecords = async () => { + const fetchRecords = async (isSearch = false) => { setLoading(true); try { - const res = await sopApi.GetSopRecord({ + const params = { keyword: searchTerm, - page, - page_size: pageSize, - sort: sortConfig.direction - }); + ...(isSearch ? {} : { page, page_size: pageSize }), + sort: sortConfig.direction, + }; + + const res = await sopApi.GetSopRecord(params); - console.log('API响应:', res); if (Array.isArray(res)) { - setRecords(res); + setAllRecords(res); // 存储所有数据 + setRecords(res); // 当前页数据 setTotal(res.length); - setCurrentRecord(res[0] || null); + } else if (res?.list) { + setAllRecords(res.list); // 存储所有数据 + setRecords(res.list); // 当前页数据 + setTotal(res.total); } - else if (res && Array.isArray(res.list)) { - setRecords(res.list); - setTotal(res.total || res.list.length); - setCurrentRecord(res.list[0] || null); - } - else { - throw new Error('API返回的数据结构不符合预期'); - } - } catch (error) { - console.error('获取数据失败:', error); - toast({ - variant: 'error', - description: error.message || '获取数据失败,请检查API响应结构' - }); - setRecords([]); - setTotal(0); - setCurrentRecord(null); } finally { setLoading(false); } @@ -92,14 +82,27 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on setPage(1); } }, [open]); - - // 搜索和分页变化时重新获取数据 useEffect(() => { const timer = setTimeout(() => { - if (open) fetchRecords(); + if (open) { + if (searchTerm) { + // 执行搜索时,重置页码为1并使用全局搜索 + setPage(1); + fetchRecords(true); // 传入true表示是搜索请求 + } else { + // 没有搜索词时,恢复普通分页模式 + fetchRecords(); + } + } }, 500); return () => clearTimeout(timer); - }, [searchTerm, page, pageSize, sortConfig]); + }, [searchTerm]); + // 搜索和分页变化时重新获取数据 + useEffect(() => { + if (open && !searchTerm) { + fetchRecords(); + } + }, [page, pageSize, sortConfig]); // 排序处理 const sortedRecords = useMemo(() => { @@ -156,11 +159,20 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on const handleToggleSelect = (record: SopRecord, e: React.MouseEvent) => { e.stopPropagation(); - setSelectedRecordIds(prev => - prev.includes(record.id) + setSelectedRecordIds(prev => { + const newIds = prev.includes(record.id) ? prev.filter(id => id !== record.id) - : [...prev, record.id] - ); + : [...prev, record.id]; + + // 同步更新selectedRecords + setSelectedRecords(prevRecords => + prev.includes(record.id) + ? prevRecords.filter(r => r.id !== record.id) + : [...prevRecords, record] + ); + + return newIds; + }); setCurrentRecord(record); }; // 全选/取消全选当前页 @@ -173,6 +185,16 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on ? prev.filter(id => !currentPageIds.includes(id)) : [...new Set([...prev, ...currentPageIds])] ); + + // 同步更新selectedRecords + setSelectedRecords(prev => { + if (allSelected) { + return prev.filter(r => !currentPageIds.includes(r.id)); + } else { + const newRecords = records.filter(r => !prev.some(p => p.id === r.id)); + return [...prev, ...newRecords]; + } + }); }; const importSops = async (recordsToImport: SopRecord[], overwrite = false, saveNew = false) => { @@ -196,13 +218,15 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on toast({ variant: 'success', description: '导入成功' }); onOpenChange(false); - onSuccess?.(); // 导入成功后调用回调 + + // 导入成功后清空所有选择状态 + setSelectedRecordIds([]); + setSelectedRecords([]); } finally { setLoading(false); } }; - const markdownRef = useRef(null); useEffect(() => { markdownRef.current?.setValue(currentRecord?.content || ''); @@ -210,10 +234,10 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on return ( - +
e.stopPropagation()}> {/* 左侧记录列表 */} -
+
从运行记录中导入SOP @@ -231,8 +255,10 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
{loading ? ( -
- +
+
+ +
) : records.length === 0 ? (
@@ -341,8 +367,8 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
{records.length > 0 && (
-
- +
+ 共 {total} 条记录
@@ -380,8 +406,7 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on 已选择 {selectedRecordIds.length} 项
diff --git a/src/frontend/platform/src/components/LinSight/ToolSelector.tsx b/src/frontend/platform/src/components/LinSight/ToolSelector.tsx index d8779fd55..6e83cb6c3 100644 --- a/src/frontend/platform/src/components/LinSight/ToolSelector.tsx +++ b/src/frontend/platform/src/components/LinSight/ToolSelector.tsx @@ -230,7 +230,7 @@ const ToolSelector = ({ className="w-1/3 flex border rounded-lg bg-white" >
-

已选工具

+

已选工具

{selectedTools.length === 0 ? (
diff --git a/src/frontend/platform/src/controllers/API/linsight.ts b/src/frontend/platform/src/controllers/API/linsight.ts index 27b59fee6..68b2cf0ea 100644 --- a/src/frontend/platform/src/controllers/API/linsight.ts +++ b/src/frontend/platform/src/controllers/API/linsight.ts @@ -44,4 +44,7 @@ export const sopApi = { SyncSopRecord:(data) => { return axios.post(`/api/v1/linsight/sop/record/sync`, data); }, + UploadSopRecord:(data) => { + return axios.post(`/api/v1/linsight/sop/upload`, data); + }, } \ No newline at end of file diff --git a/src/frontend/platform/src/controllers/request.ts b/src/frontend/platform/src/controllers/request.ts index 9c061a1b1..19567eb63 100644 --- a/src/frontend/platform/src/controllers/request.ts +++ b/src/frontend/platform/src/controllers/request.ts @@ -23,6 +23,9 @@ customAxios.interceptors.request.use(function (config) { customAxios.interceptors.response.use(function (response) { if (response.data.status_code === 200) { return response.data.data; + } + if (response.data.status_code === 11010) { + return response.data; } const i18Msg = i18next.t(`errors.${response.data.status_code}`) const errorMessage = i18Msg === `errors.${response.data.status_code}` ? response.data.status_message : i18Msg diff --git a/src/frontend/platform/src/pages/BuildPage/bench/LingSiWork.tsx b/src/frontend/platform/src/pages/BuildPage/bench/LingSiWork.tsx index 6626ad00e..a5f406f1e 100644 --- a/src/frontend/platform/src/pages/BuildPage/bench/LingSiWork.tsx +++ b/src/frontend/platform/src/pages/BuildPage/bench/LingSiWork.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/bs-ui/button"; import { Card, CardContent } from "@/components/bs-ui/card"; import { toast, useToast } from "@/components/bs-ui/toast/use-toast"; - +import { AlertTriangle } from "lucide-react"; import SopFormDrawer from "@/components/LinSight/SopFormDrawer"; import ImportFromRecordsDialog from "@/components/LinSight/SopFromRecord"; import SopTable from "@/components/LinSight/SopTable"; @@ -21,6 +21,12 @@ import { useNavigate } from "react-router-dom"; import { FormInput } from "./FormInput"; import { Model } from "./ModelManagement"; import Preview from "./Preview"; +import { useDropzone } from "react-dropzone"; +import { useTranslation } from "react-i18next"; +import { locationContext } from "@/contexts/locationContext"; +import { UploadIcon } from "@/components/bs-icons"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/bs-ui/dialog"; +import { captureAndAlertRequestErrorHoc } from "@/controllers/request"; export interface FormErrors { @@ -101,6 +107,17 @@ export default function index({ formData: parentFormData, setFormData: parentSet const [manuallyExpandedItems, setManuallyExpandedItems] = useState([]); const [initialized, setInitialized] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false); + const [localFileDialogOpen, setLocalFileDialogOpen] = useState(false); +const [importFile, setImportFile] = useState(null); +const [importFiles, setImportFiles] = useState([]); +const [isImporting, setIsImporting] = useState(false); +const [validationDialog, setValidationDialog] = useState({ + open: false, + status_message: "共计划导入0条SOP,格式正确0条,错误0条" +}); + const [importFilesData, setImportFilesData] = useState([]); + const [duplicateNames, setDuplicateNames] = useState([]); + const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false); const [deleteConfirmModal, setDeleteConfirmModal] = useState({ open: false, title: '确认删除', @@ -156,6 +173,8 @@ export default function index({ formData: parentFormData, setFormData: parentSet api: [], mcp: [] }); +const [importFormData, setImportFormData] = useState(null); + const fetchTools = async (type: 'builtin' | 'api' | 'mcp') => { setLoading(true); try { @@ -225,13 +244,13 @@ export default function index({ formData: parentFormData, setFormData: parentSet .filter(Boolean); }, [toolsData, activeToolTab, toolSearchTerm]); - const refreshSopList = () => { - fetchData({ - page: page, - pageSize: 10, - keyword: keywords - }); -}; +// const refreshSopList = () => { +// fetchData({ +// page: page, +// pageSize: 10, +// keyword: keywords +// }); +// }; const fetchData = async (params: { page: number; pageSize: number; @@ -243,7 +262,7 @@ export default function index({ formData: parentFormData, setFormData: parentSet const res = await sopApi.getSopList({ page_size: params.pageSize, page: params.page, - keywords: params.keyword || keywords, + keywords: params.keyword || '', sort: params.sort, }); @@ -384,23 +403,25 @@ export default function index({ formData: parentFormData, setFormData: parentSet const [selectedItems, setSelectedItems] = useState([]); const [sortConfig, setSortConfig] = useState({ key: null, direction: '' }); - const handleSearch = (keyword: string, resetPage: boolean = false) => { - const newKeywords = keyword; - const newPage = resetPage || newKeywords.trim() === '' ? 1 : page; + const { t } = useTranslation() + const { appConfig } = useContext(locationContext) + const handleSearch = (keyword: string, resetPage: boolean = false) => { + const newKeywords = keyword; + const newPage = resetPage || newKeywords.trim() === '' ? 1 : page; - fetchData({ - keyword: newKeywords, - page: newPage, - pageSize - }); + fetchData({ + keyword: newKeywords, + page: newPage, + pageSize + }); - // 更新状态 - setKeywords(newKeywords); - setPageInputValue(newPage.toString()); - if (newPage !== page) { - setPage(newPage); - } - }; + // 更新状态 + setKeywords(newKeywords); + setPageInputValue(newPage.toString()); + if (newPage !== page) { + setPage(newPage); + } +}; // 关闭弹窗刷新下数据 useEffect(() => { @@ -687,6 +708,80 @@ export default function index({ formData: parentFormData, setFormData: parentSet return [...tools]; }); }, []); +const { getRootProps: getLocalFileRootProps, getInputProps: getLocalFileInputProps } = useDropzone({ + accept: { + 'application/*': appConfig.enableEtl4lm ? + ['.PDF', '.TXT', '.DOCX', '.PPT', '.PPTX', '.MD', '.HTML', '.XLS', '.XLSX', '.CSV', '.DOC', '.PNG', '.JPG', '.JPEG', '.BMP'] + : ['.PDF', '.TXT', '.DOCX', '.DOC', '.PPT', '.PPTX', '.MD', '.HTML', '.XLS', '.XLSX', '.CSV'] + }, + multiple: true, // 允许选择多个文件 + onDrop: (acceptedFiles) => { + setImportFiles(prev => [...prev, ...acceptedFiles]); // 追加新文件 + } +}); +const handleLocalFileImport = async () => { + setIsImporting(true); + try { + const formData = new FormData(); + formData.append('file', importFiles[0]); + + setImportFilesData([importFiles[0]]); // 只保存一个文件 + const res = await sopApi.UploadSopRecord(formData); + console.log('API Response:', res); // 调试用 + if(res.status_code === 11010){ + setValidationDialog({ + open: true, + status_message: res.status_message + }); + } + if(res.status_code === 200){ + const formData = new FormData(); + importFilesData.forEach(file => { + formData.append('file', file); + }); + formData.append('ignore_error', 'false'); + formData.append('override', 'false'); + formData.append('save_new', 'false'); + const res = await sopApi.UploadSopRecord(formData); + if(res?.repeat_name){ + setImportDialogOpen(true) + setDuplicateNames(res.repeat_name); + setDuplicateDialogOpen(true); + setImportFormData(formData); + }else{ + toast({ variant: 'success', description: '提交成功' }); + } + } + + } finally { + setIsImporting(false); + } +}; +const handleValidationDialogConfirm = async () => { + console.log(111); + const formData = new FormData(); + importFilesData.forEach(file => { + formData.append('file', file); + }); + formData.append('ignore_error', 'true'); + formData.append('override', 'false'); + formData.append('save_new', 'false'); + + setValidationDialog(prev => ({...prev, open: false})); + + + + const res = await sopApi.UploadSopRecord(formData); + if(res?.repeat_name){ + setImportDialogOpen(true) + setDuplicateNames(res.repeat_name); + setDuplicateDialogOpen(true); + setImportFormData(formData); + return + }else{ + toast({ variant: 'success', description: '提交成功' }); + } +}; return (
{loading && ( @@ -744,7 +839,7 @@ export default function index({ formData: parentFormData, setFormData: parentSet onChange={(e) => { const newValue = e.target.value; setKeywords(newValue); - handleSearch(newValue, newValue.trim() === ''); + handleSearch(newValue); }} onKeyDown={(e) => { if (e.key === 'Enter') { @@ -760,12 +855,20 @@ export default function index({ formData: parentFormData, setFormData: parentSet + +
+ + + + 导入SOP + + +
+
+ * + 请上传文件 + +
+ +
+ + +

{t('code.clickOrDragHere')}

+ {importFiles.length > 0 && ( +
+ {importFiles.slice(0, 1).map((file, index) => ( // 只显示一个文件 +
+ + {file.name} + + +
+ ))} +
+ )} +
+
+ +
+ + +
+
+
+ setValidationDialog(prev => ({...prev, open}))}> + + +
+
+ +
+ 文件导入结果 +
+
+ +
+ {validationDialog.status_message && ( + <> +

+ {validationDialog.status_message.split(":")[0]} +

+ {validationDialog.status_message.includes(":") && ( +
+ {validationDialog.status_message.split(":")[1].split("\n").map((line, index) => ( +

{line.trim()}

+ ))} +
+ )} + + )} +
+ +
+ +
+
+
); }