refactor(files): Reconstruct file system UI (#4100)
* refactor(files): Reconstruct file system UI * refactor(knowledge): replace Card components with CustomCollapse for better UI structure * refactor(files): update folder icon from FolderOpenOutlined to FolderOpenFilled * feat(components): add CustomCollapse component for enhanced collapsible UI * refactor(files): implement virtual scrolling in FileList and KnowledgeContent components
This commit is contained in:
parent
22b0bd54b4
commit
00de616958
@ -1,4 +1,4 @@
|
|||||||
@keyframes pulse {
|
@keyframes animation-pulse {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0.5);
|
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0.5);
|
||||||
}
|
}
|
||||||
@ -14,5 +14,5 @@
|
|||||||
.animation-pulse {
|
.animation-pulse {
|
||||||
--pulse-color: 59, 130, 246;
|
--pulse-color: 59, 130, 246;
|
||||||
--pulse-size: 8px;
|
--pulse-size: 8px;
|
||||||
animation: pulse 1.5s infinite;
|
animation: animation-pulse 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/renderer/src/components/CustomCollapse.tsx
Normal file
44
src/renderer/src/components/CustomCollapse.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Collapse } from 'antd'
|
||||||
|
import { FC, memo } from 'react'
|
||||||
|
|
||||||
|
interface CustomCollapseProps {
|
||||||
|
label: string
|
||||||
|
extra: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomCollapse: FC<CustomCollapseProps> = ({ label, extra, children }) => {
|
||||||
|
const CollapseStyle = {
|
||||||
|
background: 'transparent',
|
||||||
|
border: '0.5px solid var(--color-border)'
|
||||||
|
}
|
||||||
|
const CollapseItemStyles = {
|
||||||
|
header: {
|
||||||
|
padding: '8px 16px',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
borderTop: '0.5px solid var(--color-border)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
collapsible="header"
|
||||||
|
bordered={false}
|
||||||
|
style={CollapseStyle}
|
||||||
|
defaultActiveKey={['1']}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
styles: CollapseItemStyles,
|
||||||
|
key: '1',
|
||||||
|
label,
|
||||||
|
extra,
|
||||||
|
children
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CustomCollapse)
|
||||||
145
src/renderer/src/pages/files/FileItem.tsx
Normal file
145
src/renderer/src/pages/files/FileItem.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
FileExcelFilled,
|
||||||
|
FileImageFilled,
|
||||||
|
FileMarkdownFilled,
|
||||||
|
FilePdfFilled,
|
||||||
|
FilePptFilled,
|
||||||
|
FileTextFilled,
|
||||||
|
FileUnknownFilled,
|
||||||
|
FileWordFilled,
|
||||||
|
FileZipFilled,
|
||||||
|
FolderOpenFilled,
|
||||||
|
GlobalOutlined,
|
||||||
|
LinkOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { Flex } from 'antd'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface FileItemProps {
|
||||||
|
fileInfo: {
|
||||||
|
name: React.ReactNode | string
|
||||||
|
ext: string
|
||||||
|
extra?: React.ReactNode | string
|
||||||
|
actions: React.ReactNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileIcon = (type?: string) => {
|
||||||
|
if (!type) return <FileUnknownFilled />
|
||||||
|
|
||||||
|
const ext = type.toLowerCase()
|
||||||
|
|
||||||
|
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
|
||||||
|
return <FileImageFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.doc', '.docx'].includes(ext)) {
|
||||||
|
return <FileWordFilled />
|
||||||
|
}
|
||||||
|
if (['.xls', '.xlsx'].includes(ext)) {
|
||||||
|
return <FileExcelFilled />
|
||||||
|
}
|
||||||
|
if (['.ppt', '.pptx'].includes(ext)) {
|
||||||
|
return <FilePptFilled />
|
||||||
|
}
|
||||||
|
if (ext === '.pdf') {
|
||||||
|
return <FilePdfFilled />
|
||||||
|
}
|
||||||
|
if (['.md', '.markdown'].includes(ext)) {
|
||||||
|
return <FileMarkdownFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
|
||||||
|
return <FileZipFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
|
||||||
|
return <FileTextFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.url'].includes(ext)) {
|
||||||
|
return <LinkOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.sitemap'].includes(ext)) {
|
||||||
|
return <GlobalOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.folder'].includes(ext)) {
|
||||||
|
return <FolderOpenFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FileUnknownFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileItem: React.FC<FileItemProps> = ({ fileInfo }) => {
|
||||||
|
const { name, ext, extra, actions } = fileInfo
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileItemCard>
|
||||||
|
<CardContent>
|
||||||
|
<FileIcon>{getFileIcon(ext)}</FileIcon>
|
||||||
|
<Flex vertical gap={0} flex={1} style={{ width: '0px' }}>
|
||||||
|
<FileName>{name}</FileName>
|
||||||
|
{extra && <FileInfo>{extra}</FileInfo>}
|
||||||
|
</Flex>
|
||||||
|
{actions}
|
||||||
|
</CardContent>
|
||||||
|
</FileItemCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileItemCard = styled.div`
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.05);
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px var(--shadow-color),
|
||||||
|
0 4px 6px -4px var(--shadow-color);
|
||||||
|
}
|
||||||
|
body[theme-mode='dark'] & {
|
||||||
|
--shadow-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardContent = styled.div`
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileIcon = styled.div`
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 32px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileName = styled.div`
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
span {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileInfo = styled.div`
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(FileItem)
|
||||||
167
src/renderer/src/pages/files/FileList.tsx
Normal file
167
src/renderer/src/pages/files/FileList.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
|
import { formatFileSize } from '@renderer/utils'
|
||||||
|
import { Col, Image, Row, Spin } from 'antd'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import VirtualList from 'rc-virtual-list'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import FileItem from './FileItem'
|
||||||
|
import GeminiFiles from './GeminiFiles'
|
||||||
|
|
||||||
|
interface FileItemProps {
|
||||||
|
id: FileTypes | 'all' | string
|
||||||
|
list: {
|
||||||
|
key: FileTypes | 'all' | string
|
||||||
|
file: React.ReactNode
|
||||||
|
files?: FileType[]
|
||||||
|
count?: number
|
||||||
|
size: string
|
||||||
|
ext: string
|
||||||
|
created_at: string
|
||||||
|
actions: React.ReactNode
|
||||||
|
}[]
|
||||||
|
files?: FileType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
||||||
|
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16 }}>
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{files?.map((file) => (
|
||||||
|
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
|
||||||
|
<ImageWrapper>
|
||||||
|
<LoadingWrapper>
|
||||||
|
<Spin />
|
||||||
|
</LoadingWrapper>
|
||||||
|
<Image
|
||||||
|
src={FileManager.getFileUrl(file)}
|
||||||
|
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
|
||||||
|
preview={{ mask: false }}
|
||||||
|
onLoad={(e) => {
|
||||||
|
const img = e.target as HTMLImageElement
|
||||||
|
img.parentElement?.classList.add('loaded')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ImageInfo>
|
||||||
|
<div>{formatFileSize(file.size)}</div>
|
||||||
|
</ImageInfo>
|
||||||
|
</ImageWrapper>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.startsWith('gemini_')) {
|
||||||
|
return <GeminiFiles id={id.replace('gemini_', '') as string} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualList
|
||||||
|
data={list}
|
||||||
|
height={window.innerHeight - 100}
|
||||||
|
itemHeight={80}
|
||||||
|
itemKey="key"
|
||||||
|
style={{ padding: '0 16px 16px 16px' }}
|
||||||
|
styles={{
|
||||||
|
verticalScrollBar: {
|
||||||
|
width: 6
|
||||||
|
},
|
||||||
|
verticalScrollBarThumb: {
|
||||||
|
background: 'var(--color-scrollbar-thumb)'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{(item) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '80px',
|
||||||
|
paddingTop: '12px'
|
||||||
|
}}>
|
||||||
|
<FileItem
|
||||||
|
key={item.key}
|
||||||
|
fileInfo={{
|
||||||
|
name: item.file,
|
||||||
|
ext: item.ext,
|
||||||
|
extra: `${item.created_at} · ${t('files.count')} ${item.count} · ${item.size}`,
|
||||||
|
actions: item.actions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</VirtualList>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
|
||||||
|
.ant-image {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
|
|
||||||
|
&.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.ant-image.loaded {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
div:last-child {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingWrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImageInfo = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 8px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(FileList)
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
EllipsisOutlined,
|
ExclamationCircleOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
FilePdfOutlined,
|
FilePdfOutlined,
|
||||||
FileTextOutlined
|
FileTextOutlined,
|
||||||
|
SortAscendingOutlined,
|
||||||
|
SortDescendingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
@ -16,18 +17,23 @@ import store from '@renderer/store'
|
|||||||
import { FileType, FileTypes } from '@renderer/types'
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
import { formatFileSize } from '@renderer/utils'
|
import { formatFileSize } from '@renderer/utils'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Button, Dropdown, Menu } from 'antd'
|
import { Button, Empty, Flex, Menu, Popconfirm } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { FC, useMemo, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import ContentView from './ContentView'
|
import FileList from './FileList'
|
||||||
|
|
||||||
|
type SortField = 'created_at' | 'size' | 'name'
|
||||||
|
type SortOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
const FilesPage: FC = () => {
|
const FilesPage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [fileType, setFileType] = useState<string>('document')
|
const [fileType, setFileType] = useState<string>('document')
|
||||||
|
const [sortField, setSortField] = useState<SortField>('created_at')
|
||||||
|
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
|
|
||||||
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
|
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
|
||||||
@ -42,6 +48,24 @@ const FilesPage: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortFiles = (files: FileType[]) => {
|
||||||
|
return [...files].sort((a, b) => {
|
||||||
|
let comparison = 0
|
||||||
|
switch (sortField) {
|
||||||
|
case 'created_at':
|
||||||
|
comparison = dayjs(a.created_at).unix() - dayjs(b.created_at).unix()
|
||||||
|
break
|
||||||
|
case 'size':
|
||||||
|
comparison = a.size - b.size
|
||||||
|
break
|
||||||
|
case 'name':
|
||||||
|
comparison = a.origin_name.localeCompare(b.origin_name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return sortOrder === 'asc' ? comparison : -comparison
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const files = useLiveQuery<FileType[]>(() => {
|
const files = useLiveQuery<FileType[]>(() => {
|
||||||
if (fileType === 'all') {
|
if (fileType === 'all') {
|
||||||
return db.files.orderBy('count').toArray().then(tempFilesSort)
|
return db.files.orderBy('count').toArray().then(tempFilesSort)
|
||||||
@ -49,6 +73,8 @@ const FilesPage: FC = () => {
|
|||||||
return db.files.where('type').equals(fileType).sortBy('count').then(tempFilesSort)
|
return db.files.where('type').equals(fileType).sortBy('count').then(tempFilesSort)
|
||||||
}, [fileType])
|
}, [fileType])
|
||||||
|
|
||||||
|
const sortedFiles = files ? sortFiles(files) : []
|
||||||
|
|
||||||
const handleDelete = async (fileId: string) => {
|
const handleDelete = async (fileId: string) => {
|
||||||
const file = await FileManager.getFile(fileId)
|
const file = await FileManager.getFile(fileId)
|
||||||
|
|
||||||
@ -89,95 +115,34 @@ const FilesPage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActionMenu = (fileId: string): MenuProps['items'] => [
|
const dataSource = sortedFiles?.map((file) => {
|
||||||
{
|
|
||||||
key: 'rename',
|
|
||||||
icon: <EditOutlined />,
|
|
||||||
label: t('files.edit'),
|
|
||||||
onClick: () => handleRename(fileId)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
label: t('files.delete'),
|
|
||||||
danger: true,
|
|
||||||
onClick: () => {
|
|
||||||
window.modal.confirm({
|
|
||||||
title: t('files.delete.title'),
|
|
||||||
content: t('files.delete.content'),
|
|
||||||
centered: true,
|
|
||||||
okButtonProps: { danger: true },
|
|
||||||
onOk: () => handleDelete(fileId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const dataSource = files?.map((file) => {
|
|
||||||
return {
|
return {
|
||||||
key: file.id,
|
key: file.id,
|
||||||
file: (
|
file: <span onClick={() => window.api.file.openPath(file.path)}>{FileManager.formatFileName(file)}</span>,
|
||||||
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
|
|
||||||
{FileManager.formatFileName(file)}
|
|
||||||
</FileNameText>
|
|
||||||
),
|
|
||||||
size: formatFileSize(file.size),
|
size: formatFileSize(file.size),
|
||||||
size_bytes: file.size,
|
size_bytes: file.size,
|
||||||
count: file.count,
|
count: file.count,
|
||||||
|
path: file.path,
|
||||||
|
ext: file.ext,
|
||||||
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
||||||
created_at_unix: dayjs(file.created_at).unix(),
|
created_at_unix: dayjs(file.created_at).unix(),
|
||||||
actions: (
|
actions: (
|
||||||
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
|
<Flex align="center" gap={0} style={{ opacity: 0.7 }}>
|
||||||
<Button type="text" size="small" icon={<EllipsisOutlined />} />
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
||||||
</Dropdown>
|
<Popconfirm
|
||||||
|
title={t('files.delete.title')}
|
||||||
|
description={t('files.delete.content')}
|
||||||
|
okText={t('common.confirm')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
onConfirm={() => handleDelete(file.id)}
|
||||||
|
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
title: t('files.name'),
|
|
||||||
dataIndex: 'file',
|
|
||||||
key: 'file',
|
|
||||||
width: '300px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.size'),
|
|
||||||
dataIndex: 'size',
|
|
||||||
key: 'size',
|
|
||||||
width: '80px',
|
|
||||||
sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.count'),
|
|
||||||
dataIndex: 'count',
|
|
||||||
key: 'count',
|
|
||||||
width: '60px',
|
|
||||||
sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.created_at'),
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
key: 'created_at',
|
|
||||||
width: '120px',
|
|
||||||
align: 'center',
|
|
||||||
sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) =>
|
|
||||||
b.created_at_unix - a.created_at_unix
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.actions'),
|
|
||||||
dataIndex: 'actions',
|
|
||||||
key: 'actions',
|
|
||||||
width: '80px',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
|
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
|
||||||
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
|
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
|
||||||
@ -199,9 +164,31 @@ const FilesPage: FC = () => {
|
|||||||
<SideNav>
|
<SideNav>
|
||||||
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
|
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<TableContainer right>
|
<MainContent>
|
||||||
<ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
|
<SortContainer>
|
||||||
</TableContainer>
|
{['created_at', 'size', 'name'].map((field) => (
|
||||||
|
<SortButton
|
||||||
|
key={field}
|
||||||
|
active={sortField === field}
|
||||||
|
onClick={() => {
|
||||||
|
if (sortField === field) {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortField(field as 'created_at' | 'size' | 'name')
|
||||||
|
setSortOrder('desc')
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{t(`files.${field}`)}
|
||||||
|
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||||
|
</SortButton>
|
||||||
|
))}
|
||||||
|
</SortContainer>
|
||||||
|
{dataSource && dataSource?.length > 0 ? (
|
||||||
|
<FileList id={fileType} list={dataSource} files={sortedFiles} />
|
||||||
|
) : (
|
||||||
|
<Empty />
|
||||||
|
)}
|
||||||
|
</MainContent>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@ -214,6 +201,20 @@ const Container = styled.div`
|
|||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const MainContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SortContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -221,19 +222,6 @@ const ContentContainer = styled.div`
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TableContainer = styled(Scrollbar)`
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileNameText = styled.div`
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-text);
|
|
||||||
cursor: pointer;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SideNav = styled.div`
|
const SideNav = styled.div`
|
||||||
width: var(--assistants-width);
|
width: var(--assistants-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
@ -266,4 +254,25 @@ const SideNav = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const SortButton = styled(Button)<{ active?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: var(--list-item-border-radius);
|
||||||
|
border: 0.5px solid ${(props) => (props.active ? 'var(--color-border)' : 'transparent')};
|
||||||
|
background-color: ${(props) => (props.active ? 'var(--color-background-soft)' : 'transparent')};
|
||||||
|
color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default FilesPage
|
export default FilesPage
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import { DeleteOutlined } from '@ant-design/icons'
|
|||||||
import type { FileMetadataResponse } from '@google/generative-ai/server'
|
import type { FileMetadataResponse } from '@google/generative-ai/server'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { Table } from 'antd'
|
import { Spin } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import dayjs from 'dayjs'
|
||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import FileItem from './FileItem'
|
||||||
|
|
||||||
interface GeminiFilesProps {
|
interface GeminiFilesProps {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
@ -15,7 +16,6 @@ interface GeminiFilesProps {
|
|||||||
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
||||||
const { provider } = useProvider(id)
|
const { provider } = useProvider(id)
|
||||||
const [files, setFiles] = useState<FileMetadataResponse[]>([])
|
const [files, setFiles] = useState<FileMetadataResponse[]>([])
|
||||||
const { t } = useTranslation()
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const fetchFiles = useCallback(async () => {
|
const fetchFiles = useCallback(async () => {
|
||||||
@ -23,51 +23,6 @@ const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
|||||||
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
|
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
|
||||||
}, [provider])
|
}, [provider])
|
||||||
|
|
||||||
const columns: ColumnsType<FileMetadataResponse> = [
|
|
||||||
{
|
|
||||||
title: t('files.name'),
|
|
||||||
dataIndex: 'displayName',
|
|
||||||
key: 'displayName'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.type'),
|
|
||||||
dataIndex: 'mimeType',
|
|
||||||
key: 'mimeType'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.size'),
|
|
||||||
dataIndex: 'sizeBytes',
|
|
||||||
key: 'sizeBytes',
|
|
||||||
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.created_at'),
|
|
||||||
dataIndex: 'createTime',
|
|
||||||
key: 'createTime',
|
|
||||||
render: (time: string) => new Date(time).toLocaleString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('files.actions'),
|
|
||||||
dataIndex: 'actions',
|
|
||||||
key: 'actions',
|
|
||||||
align: 'center',
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<DeleteOutlined
|
|
||||||
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
|
|
||||||
onClick={() => {
|
|
||||||
setFiles(files.filter((file) => file.name !== record.name))
|
|
||||||
window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => {
|
|
||||||
console.error('Failed to delete file:', error)
|
|
||||||
setFiles((prev) => [...prev, record])
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
try {
|
try {
|
||||||
@ -86,13 +41,61 @@ const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
|||||||
setFiles([])
|
setFiles([])
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
|
<LoadingWrapper>
|
||||||
|
<Spin />
|
||||||
|
</LoadingWrapper>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div``
|
return (
|
||||||
|
<Container>
|
||||||
|
<FileListContainer>
|
||||||
|
{files.map((file) => (
|
||||||
|
<FileItem
|
||||||
|
key={file.name}
|
||||||
|
fileInfo={{
|
||||||
|
name: file.displayName,
|
||||||
|
ext: `.${file.name.split('.').pop()}`,
|
||||||
|
extra: `${dayjs(file.createTime).format('MM-DD HH:mm')} · ${(parseInt(file.sizeBytes) / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
actions: (
|
||||||
|
<DeleteOutlined
|
||||||
|
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
|
||||||
|
onClick={() => {
|
||||||
|
setFiles(files.filter((f) => f.name !== file.name))
|
||||||
|
window.api.gemini.deleteFile(provider.apiKey, file.name).catch((error) => {
|
||||||
|
console.error('Failed to delete file:', error)
|
||||||
|
setFiles((prev) => [...prev, file])
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FileListContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileListContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
`
|
||||||
|
|
||||||
export default GeminiFiles
|
export default GeminiFiles
|
||||||
|
|||||||
@ -2,10 +2,6 @@ import {
|
|||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileTextOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
GlobalOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
RedoOutlined,
|
RedoOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
@ -19,24 +15,29 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
|||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { getProviderName } from '@renderer/services/ProviderService'
|
import { getProviderName } from '@renderer/services/ProviderService'
|
||||||
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||||
|
import { formatFileSize } from '@renderer/utils'
|
||||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||||
import { Alert, Button, Card, Divider, Dropdown, message, Tag, Tooltip, Typography, Upload } from 'antd'
|
import { Alert, Button, Divider, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import VirtualList from 'rc-virtual-list'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import CustomCollapse from '../../components/CustomCollapse'
|
||||||
|
import FileItem from '../files/FileItem'
|
||||||
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||||
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
||||||
import StatusIcon from './components/StatusIcon'
|
import StatusIcon from './components/StatusIcon'
|
||||||
|
|
||||||
const { Dragger } = Upload
|
const { Dragger } = Upload
|
||||||
const { Title } = Typography
|
|
||||||
|
|
||||||
interface KnowledgeContentProps {
|
interface KnowledgeContentProps {
|
||||||
selectedBase: KnowledgeBase
|
selectedBase: KnowledgeBase
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
||||||
|
|
||||||
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -234,13 +235,14 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
{!providerName && (
|
{!providerName && (
|
||||||
<Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
|
<Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||||
)}
|
)}
|
||||||
<FileSection>
|
|
||||||
<TitleWrapper>
|
<CustomCollapse
|
||||||
<Title level={5}>{t('files.title')}</Title>
|
label={t('files.title')}
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
|
extra={
|
||||||
|
<Button type="text" icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
|
||||||
{t('knowledge.add_file')}
|
{t('knowledge.add_file')}
|
||||||
</Button>
|
</Button>
|
||||||
</TitleWrapper>
|
}>
|
||||||
<Dragger
|
<Dragger
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
customRequest={({ file }) => handleDrop([file as File])}
|
customRequest={({ file }) => handleDrop([file as File])}
|
||||||
@ -252,54 +254,89 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
||||||
</p>
|
</p>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
</FileSection>
|
|
||||||
|
|
||||||
<FileListSection>
|
<FlexColumn>
|
||||||
{fileItems.reverse().map((item) => {
|
{fileItems.length === 0 ? (
|
||||||
|
<Empty />
|
||||||
|
) : (
|
||||||
|
<VirtualList
|
||||||
|
data={fileItems.reverse()}
|
||||||
|
height={window.innerHeight - 310}
|
||||||
|
itemHeight={80}
|
||||||
|
itemKey="id"
|
||||||
|
styles={{
|
||||||
|
verticalScrollBar: {
|
||||||
|
width: 6
|
||||||
|
},
|
||||||
|
verticalScrollBarThumb: {
|
||||||
|
background: 'var(--color-scrollbar-thumb)'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{(item) => {
|
||||||
const file = item.content as FileType
|
const file = item.content as FileType
|
||||||
return (
|
return (
|
||||||
<ItemCard key={item.id}>
|
<div style={{ height: '80px', paddingTop: '12px' }}>
|
||||||
<ItemContent>
|
<FileItem
|
||||||
<ItemInfo>
|
key={item.id}
|
||||||
<FileIcon />
|
fileInfo={{
|
||||||
|
name: (
|
||||||
<ClickableSpan onClick={() => window.api.file.openPath(file.path)}>
|
<ClickableSpan onClick={() => window.api.file.openPath(file.path)}>
|
||||||
<Ellipsis>
|
<Ellipsis>
|
||||||
<Tooltip title={file.origin_name}>{file.origin_name}</Tooltip>
|
<Tooltip title={file.origin_name}>{file.origin_name}</Tooltip>
|
||||||
</Ellipsis>
|
</Ellipsis>
|
||||||
</ClickableSpan>
|
</ClickableSpan>
|
||||||
</ItemInfo>
|
),
|
||||||
|
ext: file.ext,
|
||||||
|
extra: `${dayjs(file.created_at).format('MM-DD HH:mm')} · ${formatFileSize(file.size)}`,
|
||||||
|
actions: (
|
||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && (
|
||||||
|
<Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />
|
||||||
|
)}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} type="file" />
|
<StatusIcon
|
||||||
|
sourceId={item.id}
|
||||||
|
base={base}
|
||||||
|
getProcessingStatus={getProcessingStatus}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
</ItemContent>
|
|
||||||
</ItemCard>
|
|
||||||
)
|
)
|
||||||
})}
|
}}
|
||||||
</FileListSection>
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</VirtualList>
|
||||||
|
)}
|
||||||
|
</FlexColumn>
|
||||||
|
</CustomCollapse>
|
||||||
|
|
||||||
<ContentSection>
|
<CustomCollapse
|
||||||
<TitleWrapper>
|
label={t('knowledge.directories')}
|
||||||
<Title level={5}>{t('knowledge.directories')}</Title>
|
extra={
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
|
<Button type="text" icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
|
||||||
{t('knowledge.add_directory')}
|
{t('knowledge.add_directory')}
|
||||||
</Button>
|
</Button>
|
||||||
</TitleWrapper>
|
}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
|
{directoryItems.length === 0 && <Empty />}
|
||||||
{directoryItems.reverse().map((item) => (
|
{directoryItems.reverse().map((item) => (
|
||||||
<ItemCard key={item.id}>
|
<FileItem
|
||||||
<ItemContent>
|
key={item.id}
|
||||||
<ItemInfo>
|
fileInfo={{
|
||||||
<FolderOutlined />
|
name: (
|
||||||
<ClickableSpan onClick={() => window.api.file.openPath(item.content as string)}>
|
<ClickableSpan onClick={() => window.api.file.openPath(item.content as string)}>
|
||||||
<Ellipsis>
|
<Ellipsis>
|
||||||
<Tooltip title={item.content as string}>{item.content as string}</Tooltip>
|
<Tooltip title={item.content as string}>{item.content as string}</Tooltip>
|
||||||
</Ellipsis>
|
</Ellipsis>
|
||||||
</ClickableSpan>
|
</ClickableSpan>
|
||||||
</ItemInfo>
|
),
|
||||||
|
ext: '.folder',
|
||||||
|
extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`,
|
||||||
|
actions: (
|
||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
@ -313,25 +350,27 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
</ItemContent>
|
)
|
||||||
</ItemCard>
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</ContentSection>
|
</CustomCollapse>
|
||||||
|
|
||||||
<ContentSection>
|
<CustomCollapse
|
||||||
<TitleWrapper>
|
label={t('knowledge.urls')}
|
||||||
<Title level={5}>{t('knowledge.urls')}</Title>
|
extra={
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
|
<Button type="text" icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
|
||||||
{t('knowledge.add_url')}
|
{t('knowledge.add_url')}
|
||||||
</Button>
|
</Button>
|
||||||
</TitleWrapper>
|
}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
|
{urlItems.length === 0 && <Empty />}
|
||||||
{urlItems.reverse().map((item) => (
|
{urlItems.reverse().map((item) => (
|
||||||
<ItemCard key={item.id}>
|
<FileItem
|
||||||
<ItemContent>
|
key={item.id}
|
||||||
<ItemInfo>
|
fileInfo={{
|
||||||
<LinkOutlined />
|
name: (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
@ -363,7 +402,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ClickableSpan>
|
</ClickableSpan>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</ItemInfo>
|
),
|
||||||
|
ext: '.url',
|
||||||
|
extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`,
|
||||||
|
actions: (
|
||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
@ -371,25 +413,27 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
</ItemContent>
|
)
|
||||||
</ItemCard>
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</ContentSection>
|
</CustomCollapse>
|
||||||
|
|
||||||
<ContentSection>
|
<CustomCollapse
|
||||||
<TitleWrapper>
|
label={t('knowledge.sitemaps')}
|
||||||
<Title level={5}>{t('knowledge.sitemaps')}</Title>
|
extra={
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
|
<Button type="text" icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
|
||||||
{t('knowledge.add_sitemap')}
|
{t('knowledge.add_sitemap')}
|
||||||
</Button>
|
</Button>
|
||||||
</TitleWrapper>
|
}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
|
{sitemapItems.length === 0 && <Empty />}
|
||||||
{sitemapItems.reverse().map((item) => (
|
{sitemapItems.reverse().map((item) => (
|
||||||
<ItemCard key={item.id}>
|
<FileItem
|
||||||
<ItemContent>
|
key={item.id}
|
||||||
<ItemInfo>
|
fileInfo={{
|
||||||
<GlobalOutlined />
|
name: (
|
||||||
<ClickableSpan>
|
<ClickableSpan>
|
||||||
<Tooltip title={item.content as string}>
|
<Tooltip title={item.content as string}>
|
||||||
<Ellipsis>
|
<Ellipsis>
|
||||||
@ -399,7 +443,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</Ellipsis>
|
</Ellipsis>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ClickableSpan>
|
</ClickableSpan>
|
||||||
</ItemInfo>
|
),
|
||||||
|
ext: '.sitemap',
|
||||||
|
extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`,
|
||||||
|
actions: (
|
||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
@ -412,38 +459,48 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
</ItemContent>
|
)
|
||||||
</ItemCard>
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</ContentSection>
|
</CustomCollapse>
|
||||||
|
|
||||||
<ContentSection>
|
<CustomCollapse
|
||||||
<TitleWrapper>
|
label={t('knowledge.notes')}
|
||||||
<Title level={5}>{t('knowledge.notes')}</Title>
|
extra={
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
|
<Button type="text" icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
|
||||||
{t('knowledge.add_note')}
|
{t('knowledge.add_note')}
|
||||||
</Button>
|
</Button>
|
||||||
</TitleWrapper>
|
}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
|
{noteItems.length === 0 && <Empty />}
|
||||||
{noteItems.reverse().map((note) => (
|
{noteItems.reverse().map((note) => (
|
||||||
<ItemCard key={note.id}>
|
<FileItem
|
||||||
<ItemContent>
|
key={note.id}
|
||||||
<ItemInfo onClick={() => handleEditNote(note)} style={{ cursor: 'pointer' }}>
|
fileInfo={{
|
||||||
<span>{(note.content as string).slice(0, 50)}...</span>
|
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,
|
||||||
</ItemInfo>
|
ext: '.txt',
|
||||||
|
extra: `${dayjs(note.created_at).format('MM-DD HH:mm')}`,
|
||||||
|
actions: (
|
||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} type="note" />
|
<StatusIcon
|
||||||
|
sourceId={note.id}
|
||||||
|
base={base}
|
||||||
|
getProcessingStatus={getProcessingStatus}
|
||||||
|
type="note"
|
||||||
|
/>
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
</ItemContent>
|
)
|
||||||
</ItemCard>
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</ContentSection>
|
</CustomCollapse>
|
||||||
|
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<ModelInfo>
|
<ModelInfo>
|
||||||
@ -498,69 +555,9 @@ const MainContent = styled(Scrollbar)`
|
|||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
`
|
|
||||||
|
|
||||||
const FileSection = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ContentSection = styled.div`
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.ant-input-textarea {
|
|
||||||
background: var(--color-background-soft);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const TitleWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
padding: 5px 20px;
|
|
||||||
min-height: 45px;
|
|
||||||
border-radius: 6px;
|
|
||||||
.ant-typography {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const FileListSection = styled.div`
|
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ItemCard = styled(Card)`
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const ItemContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ItemInfo = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const IndexSection = styled.div`
|
const IndexSection = styled.div`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -602,10 +599,12 @@ const ModelInfo = styled.div`
|
|||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FlexColumn = styled.div`
|
const FlexColumn = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FlexAlignCenter = styled.div`
|
const FlexAlignCenter = styled.div`
|
||||||
@ -620,10 +619,6 @@ const ClickableSpan = styled.span`
|
|||||||
width: 0;
|
width: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileIcon = styled(FileTextOutlined)`
|
|
||||||
font-size: 16px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const BottomSpacer = styled.div`
|
const BottomSpacer = styled.div`
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
`
|
`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user