Revert "refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization"

This reverts commit edeb9f84f9.
This commit is contained in:
Teo 2025-06-14 15:48:47 +08:00
parent f1804bc3a0
commit 20b3db0c01
10 changed files with 169 additions and 458 deletions

View File

@ -470,7 +470,7 @@
"count": "files",
"created_at": "Created At",
"delete": "Delete",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete {{count}} files?",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
"delete.paintings.warning": "Image contains this file, deletion is not possible",
"delete.title": "Delete File",
"document": "Document",

View File

@ -470,7 +470,7 @@
"count": "ファイル",
"created_at": "作成日",
"delete": "削除",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
"delete.paintings.warning": "画像に含まれているため、削除できません",
"delete.title": "ファイルを削除",
"document": "ドキュメント",

View File

@ -470,7 +470,7 @@
"count": "файлов",
"created_at": "Дата создания",
"delete": "Удалить",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
"delete.title": "Удалить файл",
"document": "Документ",

View File

@ -470,7 +470,7 @@
"count": "个文件",
"created_at": "创建时间",
"delete": "删除",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除文件吗?",
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
"delete.title": "删除文件",
"document": "文档",

View File

@ -470,7 +470,7 @@
"count": "個檔案",
"created_at": "建立時間",
"delete": "刪除",
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?",
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除檔案嗎?",
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
"delete.title": "刪除檔案",
"document": "文件",

View File

@ -12,7 +12,7 @@ import {
GlobalOutlined,
LinkOutlined
} from '@ant-design/icons'
import { t } from 'i18next'
import { Flex } from 'antd'
import React, { memo } from 'react'
import styled from 'styled-components'
@ -21,14 +21,10 @@ interface FileItemProps {
icon?: React.ReactNode
name: React.ReactNode | string
ext: string
size: string
created_at: string
count?: number
checkbox?: React.ReactNode
extra?: React.ReactNode | string
actions: React.ReactNode
}
style?: React.CSSProperties
gridTemplate?: string
}
const getFileIcon = (type?: string) => {
@ -79,30 +75,19 @@ const getFileIcon = (type?: string) => {
return <FileUnknownFilled />
}
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style, gridTemplate = '' }) => {
const { name, ext, size, created_at, count, actions, icon, checkbox } = fileInfo
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style }) => {
const { name, ext, extra, actions, icon } = fileInfo
return (
<FileItemCard style={style}>
<FileGrid style={{ gridTemplateColumns: gridTemplate }}>
{checkbox && <FileCell>{checkbox}</FileCell>}
<FileCell>
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
</FileCell>
<FileCell>
<FileNameColumn>
<FileName>{name}</FileName>
{count && (
<FileCount>
{count} {t('files.count')}
</FileCount>
)}
</FileNameColumn>
</FileCell>
<FileCell style={{ textAlign: 'right' }}>{size}</FileCell>
<FileCell style={{ textAlign: 'right' }}>{created_at}</FileCell>
<FileCell style={{ justifyContent: 'center' }}>{actions}</FileCell>
</FileGrid>
<CardContent>
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
<Flex vertical justify="center" gap={0} flex={1} style={{ width: '0px' }}>
<FileName>{name}</FileName>
{extra && <FileInfo>{extra}</FileInfo>}
</Flex>
<FileActions>{actions}</FileActions>
</CardContent>
</FileItemCard>
)
}
@ -126,36 +111,20 @@ const FileItemCard = styled.div`
}
`
const FileGrid = styled.div`
display: grid;
gap: 8px;
const CardContent = styled.div`
padding: 8px 8px 8px 16px;
align-items: center;
`
const FileCell = styled.div`
display: flex;
align-items: center;
min-width: 0;
align-items: stretch;
gap: 16px;
`
const FileIcon = styled.div`
max-height: 44px;
width: 100%;
color: var(--color-text-3);
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`
const FileNameColumn = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
width: 100%;
`
const FileName = styled.div`
@ -173,9 +142,16 @@ const FileName = styled.div`
}
`
const FileCount = styled.div`
const FileInfo = styled.div`
font-size: 13px;
color: var(--color-text-2);
`
const FileActions = styled.div`
max-height: 44px;
display: flex;
align-items: center;
justify-content: center;
`
export default memo(FileItem)

View File

@ -1,6 +1,11 @@
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'
@ -14,47 +19,43 @@ interface FileItemProps {
size: string
ext: string
created_at: string
checkbox?: React.ReactNode
actions: React.ReactNode
}[]
files?: FileType[]
selectedFileIds?: string[]
onFileSelect?: (fileId: string, checked: boolean) => void
columnWidths?: string
}
const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
// if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
// return (
// <div style={{ padding: 16, overflowY: 'auto' }}>
// <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>
// )
// }
const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
return (
<div style={{ padding: 16, overflowY: 'auto' }}>
<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>
)
}
return (
<VirtualList
@ -62,7 +63,7 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
height={window.innerHeight - 100}
itemHeight={75}
itemKey="key"
style={{ padding: '0 0 16px 0' }}
style={{ padding: '0 16px 16px 16px' }}
styles={{
verticalScrollBar: {
width: 6
@ -75,21 +76,16 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
<div
style={{
height: '75px',
paddingTop: '8px',
margin: '0 16px'
paddingTop: '12px'
}}>
<FileItem
key={item.key}
fileInfo={{
name: item.file,
ext: item.ext,
size: item.size,
created_at: item.created_at,
count: item.count,
checkbox: item.checkbox,
extra: `${item.created_at} · ${item.count}${t('files.count')} · ${item.size}`,
actions: item.actions
}}
gridTemplate={columnWidths}
/>
</div>
)}
@ -97,70 +93,70 @@ const FileList: React.FC<FileItemProps> = ({ list, columnWidths }) => {
)
}
// 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);
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;
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
// &.loaded {
// opacity: 1;
// }
// }
&.loaded {
opacity: 1;
}
}
// &:hover {
// .ant-image.loaded {
// transform: scale(1.05);
// }
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
// div:last-child {
// opacity: 1;
// }
// }
// `
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 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;
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;
// }
// `
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
export default memo(FileList)

View File

@ -15,7 +15,7 @@ import store from '@renderer/store'
import { FileType, FileTypes } from '@renderer/types'
import { Message } from '@renderer/types/newMessage'
import { formatFileSize } from '@renderer/utils'
import { Button, Checkbox, Empty, Flex, Popconfirm } from 'antd'
import { Button, Empty, Flex, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
@ -25,8 +25,6 @@ import styled from 'styled-components'
import FileList from './FileList'
const GRID_TEMPLATE = 'auto 60px 1fr 120px 140px 100px'
type SortField = 'created_at' | 'size' | 'name'
type SortOrder = 'asc' | 'desc'
@ -35,7 +33,6 @@ const FilesPage: FC = () => {
const [fileType, setFileType] = useState<string>('document')
const [sortField, setSortField] = useState<SortField>('created_at')
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
const tempFilesSort = (files: FileType[]) => {
return files.sort((a, b) => {
@ -173,38 +170,6 @@ const FilesPage: FC = () => {
}
}
// 全选/取消全选
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedFileIds(sortedFiles.map((file) => file.id))
} else {
setSelectedFileIds([])
}
}
// 单个文件选择
const handleFileSelect = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds((prev) => [...prev, fileId])
} else {
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedFileIds.length === 0) return
try {
for (const fileId of selectedFileIds) {
await handleDelete(fileId)
}
setSelectedFileIds([])
} catch (error) {
Logger.error('Batch delete error:', error)
}
}
const dataSource = sortedFiles?.map((file) => {
return {
key: file.id,
@ -216,18 +181,12 @@ const FilesPage: FC = () => {
ext: file.ext,
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
created_at_unix: dayjs(file.created_at).unix(),
checkbox: (
<Checkbox
checked={selectedFileIds.includes(file.id)}
onChange={(e) => handleFileSelect(file.id, e.target.checked)}
/>
),
actions: (
<Flex align="center" gap={0} style={{ opacity: 0.7 }}>
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
<Popconfirm
title={t('files.delete.title')}
description={t('files.delete.content', { count: 1 })}
description={t('files.delete.content')}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={() => handleDelete(file.id)}
@ -264,90 +223,26 @@ const FilesPage: FC = () => {
))}
</SideNav>
<MainContent>
<TableHeader>
<TableGrid style={{ gridTemplateColumns: GRID_TEMPLATE }}>
<TableCell>
<Checkbox
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
checked={selectedFileIds.length === sortedFiles.length && sortedFiles.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
disabled={sortedFiles.length === 0}
/>
</TableCell>
<TableCell>{/* 图标列 */}</TableCell>
<TableCell>
<SortButton
active={sortField === 'name'}
onClick={() => {
if (sortField === 'name') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField('name')
setSortOrder('desc')
}
}}>
{t('files.name')}
{sortField === 'name' &&
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
</SortButton>
</TableCell>
<TableCell>
<SortButton
active={sortField === 'size'}
onClick={() => {
if (sortField === 'size') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField('size')
setSortOrder('desc')
}
}}>
{t('files.size')}
{sortField === 'size' &&
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
</SortButton>
</TableCell>
<TableCell>
<SortButton
active={sortField === 'created_at'}
onClick={() => {
if (sortField === 'created_at') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField('created_at')
setSortOrder('desc')
}
}}>
{t('files.created_at')}
{sortField === 'created_at' &&
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
</SortButton>
</TableCell>
<TableCell style={{ justifyContent: 'center' }}>
{selectedFileIds.length > 0 && (
<Popconfirm
title={t('files.delete.title')}
description={t('files.delete.content', { count: selectedFileIds.length })}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={handleBatchDelete}
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
)}
</TableCell>
</TableGrid>
</TableHeader>
<SortContainer>
{['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}
selectedFileIds={selectedFileIds}
onFileSelect={handleFileSelect}
columnWidths={GRID_TEMPLATE}
/>
<FileList id={fileType} list={dataSource} files={sortedFiles} />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
@ -370,6 +265,14 @@ const MainContent = styled.div`
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`
display: flex;
flex: 1;
@ -412,39 +315,20 @@ const SideNav = styled.div`
}
`
const SortButton = styled.div<{ active?: boolean }>`
position: relative;
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)')};
font-weight: ${(props) => (props.active ? '500' : '400')};
cursor: pointer;
z-index: 1;
&::before {
content: '';
position: absolute;
top: -4px;
left: -8px;
right: -8px;
bottom: -4px;
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')};
transition: all 0.2s ease;
z-index: -1;
}
&:hover::before {
&:hover {
background-color: var(--color-background-soft);
border-color: var(--color-border);
}
> * {
position: relative;
z-index: 2;
color: var(--color-text);
}
.anticon {
@ -452,25 +336,4 @@ const SortButton = styled.div<{ active?: boolean }>`
}
`
const TableGrid = styled.div`
display: grid;
gap: 8px;
padding: 8px 8px 0px 16px;
align-items: center;
`
const TableCell = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
&:last-child {
justify-content: center;
}
`
const TableHeader = styled.div`
margin: 0 16px;
`
export default FilesPage

View File

@ -21,8 +21,8 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import CustomCollapse from '../../components/CustomCollapse'
import FileItem from '../files/FileItem'
import { NavbarIcon } from '../home/ChatNavbar'
import KnowledgeFileItem from './components/KnowledgeFileItem'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
import StatusIcon from './components/StatusIcon'
@ -323,8 +323,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
{(item) => {
const file = item.content as FileType
return (
<div style={{ height: '75px', marginTop: '12px' }}>
<KnowledgeFileItem
<div style={{ height: '75px', paddingTop: '12px' }}>
<FileItem
key={item.id}
fileInfo={{
name: (
@ -381,7 +381,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<FlexColumn>
{directoryItems.length === 0 && <EmptyView />}
{directoryItems.reverse().map((item) => (
<KnowledgeFileItem
<FileItem
key={item.id}
fileInfo={{
name: (
@ -433,7 +433,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<FlexColumn>
{urlItems.length === 0 && <EmptyView />}
{urlItems.reverse().map((item) => (
<KnowledgeFileItem
<FileItem
key={item.id}
fileInfo={{
name: (
@ -510,7 +510,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<FlexColumn>
{sitemapItems.length === 0 && <EmptyView />}
{sitemapItems.reverse().map((item) => (
<KnowledgeFileItem
<FileItem
key={item.id}
fileInfo={{
name: (
@ -565,7 +565,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<FlexColumn>
{noteItems.length === 0 && <EmptyView />}
{noteItems.reverse().map((note) => (
<KnowledgeFileItem
<FileItem
key={note.id}
fileInfo={{
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,

View File

@ -1,124 +0,0 @@
import { getFileIcon } from '@renderer/pages/home/Inputbar/AttachmentPreview'
import React, { memo } from 'react'
import styled from 'styled-components'
interface KnowledgeFileItemProps {
fileInfo: {
icon?: React.ReactNode
name: React.ReactNode | string
ext: string
extra?: string
actions: React.ReactNode
}
style?: React.CSSProperties
}
const KnowledgeFileItem: React.FC<KnowledgeFileItemProps> = ({ fileInfo, style }) => {
const { name, ext, extra, actions, icon } = fileInfo
return (
<FileItemCard style={style}>
<FileContainer>
<FileIconContainer>
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
</FileIconContainer>
<FileContent>
<FileNameContainer>
<FileName>{name}</FileName>
{extra && <FileExtra>{extra}</FileExtra>}
</FileNameContainer>
</FileContent>
<FileActions>{actions}</FileActions>
</FileContainer>
</FileItemCard>
)
}
const FileItemCard = styled.div`
border-radius: 8px;
overflow: hidden;
border: 0.5px solid var(--color-border);
flex-shrink: 0;
transition:
box-shadow 0.2s ease,
background-color 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 FileContainer = styled.div`
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
min-height: 63px;
`
const FileIconContainer = styled.div`
flex-shrink: 0;
`
const FileIcon = styled.div`
width: 40px;
height: 40px;
color: var(--color-text-3);
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
`
const FileContent = styled.div`
flex: 1;
min-width: 0;
`
const FileNameContainer = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
`
const FileName = styled.div`
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: color 0.2s ease;
span {
font-size: 15px;
}
&:hover {
color: var(--color-primary);
}
`
const FileExtra = styled.div`
font-size: 13px;
color: var(--color-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const FileActions = styled.div`
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
`
export default memo(KnowledgeFileItem)