278 lines
13 KiB
TypeScript
Executable File
278 lines
13 KiB
TypeScript
Executable File
import { Link, useParams } from "react-router-dom";
|
||
import { Button } from "../../components/ui/button";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCaption,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow
|
||
} from "../../components/ui/table";
|
||
import {
|
||
Tabs,
|
||
TabsContent,
|
||
TabsList,
|
||
TabsTrigger,
|
||
} from "../../components/ui/tabs";
|
||
|
||
import { ArrowLeft, Filter, RotateCw, Search, X } from "lucide-react";
|
||
import { useContext, useEffect, useRef, useState } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
import { bsconfirm } from "../../alerts/confirm";
|
||
import PaginationComponent from "../../components/PaginationComponent";
|
||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||
import { Input } from "../../components/ui/input";
|
||
import { Select, SelectContent, SelectGroup, SelectIconTrigger, SelectItem } from "../../components/ui/select1";
|
||
import { locationContext } from "../../contexts/locationContext";
|
||
import { deleteFile, readFileByLibDatabase, retryKnowledgeFileApi } from "../../controllers/API";
|
||
import { captureAndAlertRequestErrorHoc } from "../../controllers/request";
|
||
import UploadModal from "../../modals/UploadModal";
|
||
import { useTable } from "../../util/hook";
|
||
import del from "../../assets/npc/del.png"
|
||
|
||
export default function FilesPage() {
|
||
const { t } = useTranslation()
|
||
|
||
const { id } = useParams()
|
||
// 上传 上传成功添加到列表
|
||
const [open, setOpen] = useState(false)
|
||
const [title, setTitle] = useState('')
|
||
|
||
const { page, pageSize, data: datalist, total, setPage, search, reload, filterData, refreshData,loadData } = useTable({}, (param) =>
|
||
readFileByLibDatabase({ ...param, id, name: param.keyword }).then(res => {
|
||
setHasPermission(res.writeable)
|
||
return res
|
||
})
|
||
)
|
||
// loadData();
|
||
setTimeout(() => reload(), 5000);
|
||
|
||
const [hasPermission, setHasPermission] = useState(true)
|
||
const { appConfig } = useContext(locationContext)
|
||
|
||
// filter
|
||
const [filter, setFilter] = useState(999)
|
||
useEffect(() => {
|
||
filterData({ status: filter })
|
||
}, [filter])
|
||
|
||
useEffect(() => {
|
||
// @ts-ignore
|
||
const libname = window.libname // 临时记忆
|
||
if (libname) {
|
||
localStorage.setItem('libname', window.libname)
|
||
}
|
||
setTitle(window.libname || localStorage.getItem('libname'))
|
||
}, [])
|
||
|
||
const handleOpen = (e) => {
|
||
setOpen(e)
|
||
reload()
|
||
}
|
||
|
||
// 删除
|
||
const { delShow, idRef, close, delConfim } = useDelete()
|
||
|
||
const handleDelete = () => {
|
||
captureAndAlertRequestErrorHoc(deleteFile(idRef.current).then(res => {
|
||
reload()
|
||
close()
|
||
}))
|
||
}
|
||
|
||
const [repeatFiles, setRepeatFiles] = useState([])
|
||
// 上传结果展示
|
||
const handleUploadResult = (fileCount, failFiles, res) => {
|
||
const _repeatFiles = res.filter(e => e.status === 3)
|
||
if (_repeatFiles.length) {
|
||
setRepeatFiles(_repeatFiles)
|
||
} else {
|
||
failFiles.length && bsconfirm({
|
||
desc: <div>
|
||
<p>{t('lib.fileUploadResult', { total: fileCount, failed: failFiles.length })}</p>
|
||
<div className="max-h-[160px] overflow-y-auto no-scrollbar">
|
||
{failFiles.map(str => <p className=" text-red-400" key={str}>{str}</p>)}
|
||
</div>
|
||
</div>,
|
||
onOk(next) {
|
||
next()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 重试解析
|
||
const [retryLoad, setRetryLoad] = useState(false)
|
||
const handleRetry = (objs) => {
|
||
setRetryLoad(true)
|
||
captureAndAlertRequestErrorHoc(retryKnowledgeFileApi(objs).then(res => {
|
||
// 乐观更新
|
||
// refreshData(
|
||
// (item) => ids.includes(item.id),
|
||
// { status: 1 }
|
||
// )
|
||
reload()
|
||
setRepeatFiles([])
|
||
setRetryLoad(false)
|
||
}))
|
||
}
|
||
|
||
const selectChange = (id) => {
|
||
setFilter(Number(id))
|
||
}
|
||
|
||
return <div className="w-full h-screen p-6 relative overflow-y-auto">
|
||
{/* {loading && <div className="absolute w-full h-full top-0 left-0 flex justify-center items-center z-10 bg-[rgba(255,255,255,0.6)] dark:bg-blur-shared">
|
||
<span className="loading loading-infinity loading-lg"></span>
|
||
</div>} */}
|
||
<ShadTooltip content="back" side="top">
|
||
<Link to='/filelib'>
|
||
<button className="extra-side-bar-buttons w-[36px] absolute top-[26px]" onClick={() => { }} >
|
||
<ArrowLeft className="side-bar-button-size" />
|
||
</button>
|
||
</Link>
|
||
</ShadTooltip>
|
||
<Tabs defaultValue="account" className="w-full">
|
||
{/* <TabsList className="ml-12">
|
||
<TabsTrigger value="account" className="roundedrounded-xl">{t('lib.fileList')}</TabsTrigger>
|
||
<TabsTrigger disabled value="password">{t('lib.systemIntegration')}</TabsTrigger>
|
||
</TabsList> */}
|
||
<div className="flex justify-between">
|
||
<p className="text-[16px] ml-[40px]" style={{color:"#FFF"}}>{t('lib.fileData')}</p>
|
||
<div className="flex justify-center items-center w-[74px] h-[27px] cursor-pointer" onClick={() => setOpen(true)} style={{background: "#FFD025",borderRadius: "7px"}}>
|
||
{/* <img src={jia1} className="w-[14px] mr-[5px]" alt=""/> */}
|
||
<span className="text-[12px]" style={{color:"#333333"}}>上 传</span>
|
||
</div>
|
||
</div>
|
||
<TabsContent value="account">
|
||
{/* <div className="flex justify-between items-center">
|
||
<span className=" text-gray-800">{title}</span>
|
||
<div className="flex gap-4 items-center">
|
||
<div className="w-[180px] relative">
|
||
<Input placeholder={t('lib.fileName')} onChange={(e) => search(e.target.value)}></Input>
|
||
<Search className="absolute right-4 top-2 text-gray-300 pointer-events-none"></Search>
|
||
</div>
|
||
{hasPermission && <Button className="h-8 rounded-full" onClick={() => setOpen(true)}>{t('lib.upload')}</Button>}
|
||
</div>
|
||
</div> */}
|
||
<Table>
|
||
<TableCaption>
|
||
<div className="join grid grid-cols-2 w-[200px]">
|
||
<PaginationComponent
|
||
page={page}
|
||
pageSize={pageSize}
|
||
total={total}
|
||
onChange={(newPage) => setPage(newPage)}
|
||
/>
|
||
</div>
|
||
</TableCaption>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-[600px] dialogueLog-header">{t('lib.fileName')}</TableHead>
|
||
{/* 状态 */}
|
||
<TableHead className="flex items-center gap-4 dialogueLog-header">{t('lib.status')}
|
||
{/* Select component */}
|
||
<Select onValueChange={selectChange}>
|
||
<SelectIconTrigger className="">
|
||
<Filter size={16} className={`cursor-pointer ${filter === 999 ? '' : 'text-gray-950'}`} />
|
||
</SelectIconTrigger>
|
||
<SelectContent className="">
|
||
<SelectGroup>
|
||
<SelectItem value={'999'}>{t('all')}</SelectItem>
|
||
<SelectItem value={'1'}>{t('lib.parsing')}</SelectItem>
|
||
<SelectItem value={'2'}>{t('lib.completed')}</SelectItem>
|
||
<SelectItem value={'3'}>{t('lib.parseFailed')}</SelectItem>
|
||
</SelectGroup>
|
||
</SelectContent>
|
||
</Select>
|
||
</TableHead>
|
||
<TableHead className=" dialogueLog-header">{t('lib.uploadTime')}</TableHead>
|
||
<TableHead className=" dialogueLog-header">{t('operations')}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{datalist.map(el => (
|
||
<TableRow key={el.id}>
|
||
<TableCell className="dialogueLog-body">{el.file_name}</TableCell>
|
||
<TableCell className="dialogueLog-body">
|
||
{el.status === 3 ? <div className="flex items-center">
|
||
<div className="tooltip" data-tip={el.remark}>
|
||
<span className='text-red-500'>{t('lib.parseFailed')}</span>
|
||
</div>
|
||
<Button variant="link"><RotateCw size={16} onClick={() => handleRetry([el])} /></Button>
|
||
</div> :
|
||
<span className={el.status === 3 && 'text-red-500'}>{[t('lib.parseFailed'), t('lib.parsing'), t('lib.completed'), t('lib.parseFailed')][el.status]}</span>
|
||
}
|
||
</TableCell>
|
||
<TableCell className="dialogueLog-body">{el.update_time.replace('T', ' ')}</TableCell>
|
||
<TableCell className="text-right dialogueLog-body">
|
||
{hasPermission ?
|
||
// <a href="javascript:;" onClick={() => delConfim(el.id)} className="underline ml-4">{t('delete')}</a>
|
||
<img src={del} onClick={() => delConfim(el.id)} className="w-[20px] cursor-pointer" alt=""/>
|
||
:
|
||
// <a href="javascript:;" className="underline ml-4 text-gray-400">{t('delete')}</a>
|
||
<img src={del} className="w-[20px] cursor-pointer" alt=""/>
|
||
}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
{/* Pagination */}
|
||
</TabsContent>
|
||
<TabsContent value="password"></TabsContent>
|
||
</Tabs>
|
||
{/* upload modal */}
|
||
<UploadModal id={id} accept={appConfig.libAccepts} open={open} setOpen={handleOpen} onResult={handleUploadResult}></UploadModal>
|
||
{/* 重复文件提醒 */}
|
||
<dialog className={`modal ${repeatFiles.length && 'modal-open'}`}>
|
||
<div className="modal-box w-[560px] bg-[#262626] shadow-lg">
|
||
<h3 className="font-bold text-lg relative text-[#fff]">文件重复提示
|
||
<X className="absolute right-0 top-0 text-[#fff] cursor-pointer" size={20} onClick={() => setRepeatFiles([])}></X>
|
||
</h3>
|
||
<p className="py-4 text-[#fff]">以下文件在知识库中已存在,继续上传将会覆盖原有文件以及处理策略,是否覆盖?</p>
|
||
<ul className="overflow-y-auto max-h-[400px]">
|
||
{repeatFiles.map(el => (
|
||
<li key={el.id} className="py-2 text-red-500">{el.remark}</li>
|
||
))}
|
||
</ul>
|
||
<div className="modal-action">
|
||
<Button className="h-8 rounded-full" variant="outline" onClick={() => setRepeatFiles([])}>不覆盖,保留原文件</Button>
|
||
<Button className="h-8 rounded-full bg-[#FFD025] hover:bg-[#FFD025]" disabled={retryLoad} onClick={() => handleRetry(repeatFiles)}>
|
||
{retryLoad && <span className="loading loading-spinner loading-xs"></span>}覆盖
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
{/* Delete confirmation */}
|
||
<dialog className={`modal ${delShow && 'modal-open'}`}>
|
||
<form method="dialog" className="modal-box w-[400px] bg-[#262626] shadow-lg">
|
||
<h3 className="text-[16px] font-bold text-center" style={{color:"#FFFFFF"}}>{t('prompt')}</h3>
|
||
<p className="text-[12px] text-center mt-[18px]" style={{color:"#FFFFFF"}}>{t('lib.confirmDeleteFile')}</p>
|
||
<div className="flex justify-center mt-[27px]">
|
||
<Button className="baogao-btn" variant="outline" onClick={close}>{t('cancel')}</Button>
|
||
<Button className="baogao-btn ml-[27px]" variant="destructive" onClick={handleDelete}>{t('delete')}</Button>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
</div>
|
||
};
|
||
|
||
|
||
const useDelete = () => {
|
||
const [delShow, setDelShow] = useState(false)
|
||
const idRef = useRef<any>(null)
|
||
|
||
return {
|
||
delShow,
|
||
idRef,
|
||
close: () => {
|
||
setDelShow(false)
|
||
},
|
||
delConfim: (id) => {
|
||
idRef.current = id
|
||
setDelShow(true)
|
||
}
|
||
}
|
||
} |