mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
refactor: new knowledge base ui layout (#7748)
This commit is contained in:
parent
38497597b9
commit
7ed6e58f8e
@ -1,233 +1,90 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import { RedoOutlined } from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import Logger from '@renderer/config/logger'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { NavbarIcon } from '@renderer/pages/home/Navbar'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||
import { Alert, Button, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { ChevronsDown, ChevronsUp, Plus, Search, Settings2 } from 'lucide-react'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { Button, Empty, Tabs, Tag, Tooltip } from 'antd'
|
||||
import { Book, Folder, Globe, Link, Notebook, Search, Settings } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import CustomCollapse from '../../components/CustomCollapse'
|
||||
import FileItem from '../files/FileItem'
|
||||
import { NavbarIcon } from '../home/Navbar'
|
||||
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
|
||||
import StatusIcon from './components/StatusIcon'
|
||||
|
||||
const { Dragger } = Upload
|
||||
import KnowledgeDirectories from './items/KnowledgeDirectories'
|
||||
import KnowledgeFiles from './items/KnowledgeFiles'
|
||||
import KnowledgeNotes from './items/KnowledgeNotes'
|
||||
import KnowledgeSitemaps from './items/KnowledgeSitemaps'
|
||||
import KnowledgeUrls from './items/KnowledgeUrls'
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandAll, setExpandAll] = useState(false)
|
||||
|
||||
const {
|
||||
base,
|
||||
noteItems,
|
||||
fileItems,
|
||||
urlItems,
|
||||
sitemapItems,
|
||||
directoryItems,
|
||||
addFiles,
|
||||
updateNoteContent,
|
||||
refreshItem,
|
||||
addUrl,
|
||||
addSitemap,
|
||||
removeItem,
|
||||
getProcessingStatus,
|
||||
getDirectoryProcessingPercent,
|
||||
addNote,
|
||||
addDirectory,
|
||||
updateItem
|
||||
} = useKnowledge(selectedBase.id || '')
|
||||
const { base, urlItems, fileItems, directoryItems, noteItems, sitemapItems } = useKnowledge(selectedBase.id || '')
|
||||
const [activeKey, setActiveKey] = useState('files')
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
const knowledgeItems = [
|
||||
{
|
||||
key: 'files',
|
||||
title: t('files.title'),
|
||||
icon: activeKey === 'files' ? <Book size={16} color="var(--color-primary)" /> : <Book size={16} />,
|
||||
items: fileItems,
|
||||
content: <KnowledgeFiles selectedBase={selectedBase} />
|
||||
},
|
||||
{
|
||||
key: 'notes',
|
||||
title: t('knowledge.notes'),
|
||||
icon: activeKey === 'notes' ? <Notebook size={16} color="var(--color-primary)" /> : <Notebook size={16} />,
|
||||
items: noteItems,
|
||||
content: <KnowledgeNotes selectedBase={selectedBase} />
|
||||
},
|
||||
{
|
||||
key: 'directories',
|
||||
title: t('knowledge.directories'),
|
||||
icon: activeKey === 'directories' ? <Folder size={16} color="var(--color-primary)" /> : <Folder size={16} />,
|
||||
items: directoryItems,
|
||||
content: <KnowledgeDirectories selectedBase={selectedBase} />
|
||||
},
|
||||
{
|
||||
key: 'urls',
|
||||
title: t('knowledge.urls'),
|
||||
icon: activeKey === 'urls' ? <Link size={16} color="var(--color-primary)" /> : <Link size={16} />,
|
||||
items: urlItems,
|
||||
content: <KnowledgeUrls selectedBase={selectedBase} />
|
||||
},
|
||||
{
|
||||
key: 'sitemaps',
|
||||
title: t('knowledge.sitemaps'),
|
||||
icon: activeKey === 'sitemaps' ? <Globe size={16} color="var(--color-primary)" /> : <Globe size={16} />,
|
||||
items: sitemapItems,
|
||||
content: <KnowledgeSitemaps selectedBase={selectedBase} />
|
||||
}
|
||||
]
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getProgressingPercentForItem = (itemId: string) => getDirectoryProcessingPercent(itemId)
|
||||
|
||||
const handleAddFile = () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.accept = fileTypes.join(',')
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
files && handleDrop(Array.from(files))
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleDrop = async (files: File[]) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (files) {
|
||||
const _files: FileType[] = files
|
||||
.map((file) => ({
|
||||
id: file.name,
|
||||
name: file.name,
|
||||
path: window.api.file.getPathForFile(file),
|
||||
size: file.size,
|
||||
ext: `.${file.name.split('.').pop()}`.toLowerCase(),
|
||||
count: 1,
|
||||
origin_name: file.name,
|
||||
type: file.type as FileTypes,
|
||||
created_at: new Date().toISOString()
|
||||
}))
|
||||
.filter(({ ext }) => fileTypes.includes(ext))
|
||||
const uploadedFiles = await FileManager.uploadFiles(_files)
|
||||
addFiles(uploadedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddUrl = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const urlInput = await PromptPopup.show({
|
||||
title: t('knowledge.add_url'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.url_placeholder'),
|
||||
inputProps: {
|
||||
rows: 10,
|
||||
onPressEnter: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
if (urlInput) {
|
||||
// Split input by newlines and filter out empty lines
|
||||
const urls = urlInput.split('\n').filter((url) => url.trim())
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
new URL(url.trim())
|
||||
if (!urlItems.find((item) => item.content === url.trim())) {
|
||||
addUrl(url.trim())
|
||||
} else {
|
||||
message.success(t('knowledge.url_added'))
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid URLs silently
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddSitemap = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = await PromptPopup.show({
|
||||
title: t('knowledge.add_sitemap'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.sitemap_placeholder'),
|
||||
inputProps: {
|
||||
maxLength: 1000,
|
||||
rows: 1
|
||||
}
|
||||
})
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
new URL(url)
|
||||
if (sitemapItems.find((item) => item.content === url)) {
|
||||
message.success(t('knowledge.sitemap_added'))
|
||||
return
|
||||
}
|
||||
addSitemap(url)
|
||||
} catch (e) {
|
||||
console.error('Invalid Sitemap URL:', url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddNote = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
||||
note && addNote(note)
|
||||
}
|
||||
|
||||
const handleEditNote = async (note: any) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
||||
editedText && updateNoteContent(note.id, editedText)
|
||||
}
|
||||
|
||||
const handleAddDirectory = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = await window.api.file.selectFolder()
|
||||
Logger.log('[KnowledgeContent] Selected directory:', path)
|
||||
path && addDirectory(path)
|
||||
}
|
||||
|
||||
const handleEditRemark = async (item: KnowledgeItem) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const editedRemark: string | undefined = await PromptPopup.show({
|
||||
title: t('knowledge.edit_remark'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.edit_remark_placeholder'),
|
||||
defaultValue: item.remark || '',
|
||||
inputProps: {
|
||||
maxLength: 100,
|
||||
rows: 1
|
||||
}
|
||||
})
|
||||
|
||||
if (editedRemark !== undefined && editedRemark !== null) {
|
||||
updateItem({
|
||||
...item,
|
||||
remark: editedRemark,
|
||||
updated_at: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
const tabItems = knowledgeItems.map((item) => ({
|
||||
key: item.key,
|
||||
label: (
|
||||
<TabLabel>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
<CustomTag size={10} color={item.items.length > 0 ? '#00b96b' : '#cccccc'}>
|
||||
{item.items.length}
|
||||
</CustomTag>
|
||||
</TabLabel>
|
||||
),
|
||||
children: <TabContent>{item.content}</TabContent>
|
||||
}))
|
||||
|
||||
return (
|
||||
<MainContainer>
|
||||
@ -235,7 +92,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<ModelInfo>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Settings2 size={18} color="var(--color-icon)" />}
|
||||
icon={<Settings size={18} color="var(--color-icon)" />}
|
||||
onClick={() => KnowledgeSettingsPopup.show({ base })}
|
||||
size="small"
|
||||
/>
|
||||
@ -245,16 +102,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
</div>
|
||||
<Tooltip title={providerName} placement="bottom">
|
||||
<div className="tag-column">
|
||||
<Tag color="green" style={{ borderRadius: 20, margin: 0 }}>
|
||||
{base.model.name}
|
||||
</Tag>
|
||||
<Tag style={{ borderRadius: 20, margin: 0 }}>{base.model.name}</Tag>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{base.rerankModel && (
|
||||
<Tag color="cyan" style={{ borderRadius: 20, margin: 0 }}>
|
||||
{base.rerankModel.name}
|
||||
</Tag>
|
||||
)}
|
||||
{base.rerankModel && <Tag style={{ borderRadius: 20, margin: 0 }}>{base.rerankModel.name}</Tag>}
|
||||
</div>
|
||||
</ModelInfo>
|
||||
<HStack gap={8} alignItems="center">
|
||||
@ -262,353 +113,19 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<NarrowIcon onClick={() => base && KnowledgeSearchPopup.show({ base: base })}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
<Tooltip title={expandAll ? t('common.collapse') : t('common.expand')}>
|
||||
<Button
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={() => setExpandAll(!expandAll)}
|
||||
icon={expandAll ? <ChevronsUp size={14} /> : <ChevronsDown size={14} />}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</HeaderContainer>
|
||||
<MainContent>
|
||||
{!base?.version && (
|
||||
<Alert message={t('knowledge.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||
)}
|
||||
{!providerName && (
|
||||
<Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||
)}
|
||||
<CustomCollapse
|
||||
label={<CollapseLabel label={t('files.title')} count={fileItems.length} />}
|
||||
defaultActiveKey={['1']}
|
||||
activeKey={expandAll ? ['1'] : undefined}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddFile()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_file')}
|
||||
</Button>
|
||||
}>
|
||||
<Dragger
|
||||
showUploadList={false}
|
||||
customRequest={({ file }) => handleDrop([file as File])}
|
||||
multiple={true}
|
||||
accept={fileTypes.join(',')}
|
||||
style={{ marginTop: 10, background: 'transparent' }}>
|
||||
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
||||
<p className="ant-upload-hint">
|
||||
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
<FlexColumn>
|
||||
{fileItems.length === 0 ? (
|
||||
<EmptyView />
|
||||
) : (
|
||||
<VirtualList
|
||||
data={fileItems.reverse()}
|
||||
height={fileItems.length > 5 ? 400 : fileItems.length * 75}
|
||||
itemHeight={75}
|
||||
itemKey="id"
|
||||
styles={{
|
||||
verticalScrollBar: {
|
||||
width: 6
|
||||
},
|
||||
verticalScrollBarThumb: {
|
||||
background: 'var(--color-scrollbar-thumb)'
|
||||
}
|
||||
}}>
|
||||
{(item) => {
|
||||
const file = item.content as FileType
|
||||
return (
|
||||
<div style={{ height: '75px', paddingTop: '12px' }}>
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan onClick={() => window.api.file.openPath(FileManager.getFilePath(file))}>
|
||||
<Ellipsis>
|
||||
<Tooltip title={file.origin_name}>{file.origin_name}</Tooltip>
|
||||
</Ellipsis>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: file.ext,
|
||||
extra: `${getDisplayTime(item)} · ${formatFileSize(file.size)}`,
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && (
|
||||
<Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />
|
||||
)}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="file"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</VirtualList>
|
||||
)}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
|
||||
<CustomCollapse
|
||||
label={<CollapseLabel label={t('knowledge.directories')} count={directoryItems.length} />}
|
||||
defaultActiveKey={[]}
|
||||
activeKey={expandAll ? ['1'] : undefined}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddDirectory()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_directory')}
|
||||
</Button>
|
||||
}>
|
||||
<FlexColumn>
|
||||
{directoryItems.length === 0 && <EmptyView />}
|
||||
{directoryItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan onClick={() => window.api.file.openPath(item.content as string)}>
|
||||
<Ellipsis>
|
||||
<Tooltip title={item.content as string}>{item.content as string}</Tooltip>
|
||||
</Ellipsis>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: '.folder',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
getProcessingPercent={getProgressingPercentForItem}
|
||||
type="directory"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
|
||||
<CustomCollapse
|
||||
label={<CollapseLabel label={t('knowledge.urls')} count={urlItems.length} />}
|
||||
defaultActiveKey={[]}
|
||||
activeKey={expandAll ? ['1'] : undefined}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddUrl()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_url')}
|
||||
</Button>
|
||||
}>
|
||||
<FlexColumn>
|
||||
{urlItems.length === 0 && <EmptyView />}
|
||||
{urlItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
label: t('knowledge.edit_remark'),
|
||||
onClick: () => handleEditRemark(item)
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <CopyOutlined />,
|
||||
label: t('common.copy'),
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(item.content as string)
|
||||
message.success(t('message.copied'))
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['contextMenu']}>
|
||||
<ClickableSpan>
|
||||
<Tooltip title={item.content as string}>
|
||||
<Ellipsis>
|
||||
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||
{item.remark || (item.content as string)}
|
||||
</a>
|
||||
</Ellipsis>
|
||||
</Tooltip>
|
||||
</ClickableSpan>
|
||||
</Dropdown>
|
||||
),
|
||||
ext: '.url',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="url"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
|
||||
<CustomCollapse
|
||||
label={<CollapseLabel label={t('knowledge.sitemaps')} count={sitemapItems.length} />}
|
||||
defaultActiveKey={[]}
|
||||
activeKey={expandAll ? ['1'] : undefined}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddSitemap()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_sitemap')}
|
||||
</Button>
|
||||
}>
|
||||
<FlexColumn>
|
||||
{sitemapItems.length === 0 && <EmptyView />}
|
||||
{sitemapItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan>
|
||||
<Tooltip title={item.content as string}>
|
||||
<Ellipsis>
|
||||
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||
{item.content as string}
|
||||
</a>
|
||||
</Ellipsis>
|
||||
</Tooltip>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: '.sitemap',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="sitemap"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
|
||||
<CustomCollapse
|
||||
label={<CollapseLabel label={t('knowledge.notes')} count={noteItems.length} />}
|
||||
defaultActiveKey={[]}
|
||||
activeKey={expandAll ? ['1'] : undefined}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddNote()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_note')}
|
||||
</Button>
|
||||
}>
|
||||
<FlexColumn>
|
||||
{noteItems.length === 0 && <EmptyView />}
|
||||
{noteItems.reverse().map((note) => (
|
||||
<FileItem
|
||||
key={note.id}
|
||||
fileInfo={{
|
||||
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,
|
||||
ext: '.txt',
|
||||
extra: getDisplayTime(note),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={note.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="note"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
</MainContent>
|
||||
<StyledTabs activeKey={activeKey} onChange={setActiveKey} items={tabItems} type="line" size="small" />
|
||||
</MainContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const EmptyView = () => <Empty style={{ margin: 0 }} styles={{ image: { display: 'none' } }} />
|
||||
export const KnowledgeEmptyView = () => <Empty style={{ margin: 20 }} styles={{ image: { display: 'none' } }} />
|
||||
|
||||
const CollapseLabel = ({ label, count }: { label: string; count: number }) => {
|
||||
export const ItemHeaderLabel = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<label style={{ fontWeight: 600 }}>{label}</label>
|
||||
<CustomTag size={12} color={count ? '#008001' : '#cccccc'}>
|
||||
{count}
|
||||
</CustomTag>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@ -620,14 +137,55 @@ const MainContainer = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const MainContent = styled(Scrollbar)`
|
||||
padding: 15px 20px;
|
||||
const TabLabel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 4px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const TabContent = styled.div``
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
flex: 1;
|
||||
gap: 20px;
|
||||
padding-bottom: 50px;
|
||||
padding-right: 12px;
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 12px 12px;
|
||||
margin-right: 0;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
position: initial !important;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
height: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
@ -645,7 +203,7 @@ const ModelInfo = styled.div`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 50px;
|
||||
height: 45px;
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
@ -675,26 +233,32 @@ const ModelInfo = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const FlexColumn = styled.div`
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const ItemContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const FlexAlignCenter = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const ClickableSpan = styled.span`
|
||||
cursor: pointer;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
`
|
||||
|
||||
const StatusIconWrapper = styled.div`
|
||||
export const ItemHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: calc(var(--navbar-height) + 14px);
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
`
|
||||
|
||||
export const StatusIconWrapper = styled.div`
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
@ -703,15 +267,21 @@ const StatusIconWrapper = styled.div`
|
||||
padding-top: 2px;
|
||||
`
|
||||
|
||||
const RefreshIcon = styled(RedoOutlined)`
|
||||
export const RefreshIcon = styled(RedoOutlined)`
|
||||
font-size: 15px !important;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
export const ClickableSpan = styled.span`
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
`
|
||||
|
||||
export const FlexAlignCenter = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export default KnowledgeContent
|
||||
|
||||
@ -96,7 +96,7 @@ const KnowledgePage: FC = () => {
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SideNav>
|
||||
<KnowledgeSideNav>
|
||||
<ScrollContainer>
|
||||
<DragableList
|
||||
list={bases}
|
||||
@ -127,7 +127,7 @@ const KnowledgePage: FC = () => {
|
||||
)}
|
||||
<div style={{ minHeight: '10px' }}></div>
|
||||
</ScrollContainer>
|
||||
</SideNav>
|
||||
</KnowledgeSideNav>
|
||||
{bases.length === 0 ? (
|
||||
<MainContent>
|
||||
<Empty description={t('knowledge.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
@ -162,7 +162,7 @@ const MainContent = styled(Scrollbar)`
|
||||
padding-bottom: 50px;
|
||||
`
|
||||
|
||||
const SideNav = styled.div`
|
||||
export const KnowledgeSideNav = styled.div`
|
||||
min-width: var(--settings-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 12px 10px;
|
||||
|
||||
128
src/renderer/src/pages/knowledge/items/KnowledgeDirectories.tsx
Normal file
128
src/renderer/src/pages/knowledge/items/KnowledgeDirectories.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import Logger from '@renderer/config/logger'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import StatusIcon from '../components/StatusIcon'
|
||||
import {
|
||||
ClickableSpan,
|
||||
FlexAlignCenter,
|
||||
ItemContainer,
|
||||
ItemHeader,
|
||||
KnowledgeEmptyView,
|
||||
RefreshIcon,
|
||||
StatusIconWrapper
|
||||
} from '../KnowledgeContent'
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeDirectories: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
base,
|
||||
directoryItems,
|
||||
refreshItem,
|
||||
removeItem,
|
||||
getProcessingStatus,
|
||||
getDirectoryProcessingPercent,
|
||||
addDirectory
|
||||
} = useKnowledge(selectedBase.id || '')
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getProgressingPercentForItem = (itemId: string) => getDirectoryProcessingPercent(itemId)
|
||||
|
||||
const handleAddDirectory = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = await window.api.file.selectFolder()
|
||||
Logger.log('[KnowledgeContent] Selected directory:', path)
|
||||
path && addDirectory(path)
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemContainer>
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddDirectory()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_directory')}
|
||||
</Button>
|
||||
</ItemHeader>
|
||||
<ItemFlexColumn>
|
||||
{directoryItems.length === 0 && <KnowledgeEmptyView />}
|
||||
{directoryItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan onClick={() => window.api.file.openPath(item.content as string)}>
|
||||
<Ellipsis>
|
||||
<Tooltip title={item.content as string}>{item.content as string}</Tooltip>
|
||||
</Ellipsis>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: '.folder',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
getProcessingPercent={getProgressingPercentForItem}
|
||||
type="directory"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ItemFlexColumn>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemFlexColumn = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 20px 16px;
|
||||
height: calc(100vh - 135px);
|
||||
`
|
||||
|
||||
export default KnowledgeDirectories
|
||||
194
src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx
Normal file
194
src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import StatusIcon from '@renderer/pages/knowledge/components/StatusIcon'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||
import { Button, Tooltip, Upload } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
ClickableSpan,
|
||||
FlexAlignCenter,
|
||||
ItemContainer,
|
||||
ItemHeader,
|
||||
KnowledgeEmptyView,
|
||||
RefreshIcon,
|
||||
StatusIconWrapper
|
||||
} from '../KnowledgeContent'
|
||||
|
||||
const { Dragger } = Upload
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
const [windowHeight, setWindowHeight] = useState(window.innerHeight)
|
||||
|
||||
const { base, fileItems, addFiles, refreshItem, removeItem, getProcessingStatus } = useKnowledge(
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowHeight(window.innerHeight)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleAddFile = () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.accept = fileTypes.join(',')
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
files && handleDrop(Array.from(files))
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleDrop = async (files: File[]) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (files) {
|
||||
const _files: FileType[] = files
|
||||
.map((file) => ({
|
||||
id: file.name,
|
||||
name: file.name,
|
||||
path: window.api.file.getPathForFile(file),
|
||||
size: file.size,
|
||||
ext: `.${file.name.split('.').pop()}`.toLowerCase(),
|
||||
count: 1,
|
||||
origin_name: file.name,
|
||||
type: file.type as FileTypes,
|
||||
created_at: new Date().toISOString()
|
||||
}))
|
||||
.filter(({ ext }) => fileTypes.includes(ext))
|
||||
const uploadedFiles = await FileManager.uploadFiles(_files)
|
||||
addFiles(uploadedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemContainer>
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddFile()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_file')}
|
||||
</Button>
|
||||
</ItemHeader>
|
||||
|
||||
<ItemFlexColumn>
|
||||
<Dragger
|
||||
showUploadList={false}
|
||||
customRequest={({ file }) => handleDrop([file as File])}
|
||||
multiple={true}
|
||||
accept={fileTypes.join(',')}>
|
||||
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
||||
<p className="ant-upload-hint">
|
||||
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
||||
</p>
|
||||
</Dragger>
|
||||
{fileItems.length === 0 ? (
|
||||
<KnowledgeEmptyView />
|
||||
) : (
|
||||
<VirtualList
|
||||
data={fileItems.reverse()}
|
||||
height={windowHeight - 270}
|
||||
itemHeight={75}
|
||||
itemKey="id"
|
||||
styles={{
|
||||
verticalScrollBar: { width: 6 },
|
||||
verticalScrollBarThumb: { background: 'var(--color-scrollbar-thumb)' }
|
||||
}}>
|
||||
{(item) => {
|
||||
const file = item.content as FileType
|
||||
return (
|
||||
<div style={{ height: '75px', paddingTop: '12px' }}>
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan onClick={() => window.api.file.openPath(FileManager.getFilePath(file))}>
|
||||
<Ellipsis>
|
||||
<Tooltip title={file.origin_name}>{file.origin_name}</Tooltip>
|
||||
</Ellipsis>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: file.ext,
|
||||
extra: `${getDisplayTime(item)} · ${formatFileSize(file.size)}`,
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && (
|
||||
<Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />
|
||||
)}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="file"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</VirtualList>
|
||||
)}
|
||||
</ItemFlexColumn>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemFlexColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 16px;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
export default KnowledgeFiles
|
||||
107
src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx
Normal file
107
src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import StatusIcon from '../components/StatusIcon'
|
||||
import { FlexAlignCenter, ItemContainer, ItemHeader, KnowledgeEmptyView, StatusIconWrapper } from '../KnowledgeContent'
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeNotes: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { base, noteItems, updateNoteContent, removeItem, getProcessingStatus, addNote } = useKnowledge(
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleAddNote = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
||||
note && addNote(note)
|
||||
}
|
||||
|
||||
const handleEditNote = async (note: any) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
||||
editedText && updateNoteContent(note.id, editedText)
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemContainer>
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddNote()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_note')}
|
||||
</Button>
|
||||
</ItemHeader>
|
||||
<ItemFlexColumn>
|
||||
{noteItems.length === 0 && <KnowledgeEmptyView />}
|
||||
{noteItems.reverse().map((note) => (
|
||||
<FileItem
|
||||
key={note.id}
|
||||
fileInfo={{
|
||||
name: <span onClick={() => handleEditNote(note)}>{(note.content as string).slice(0, 50)}...</span>,
|
||||
ext: '.txt',
|
||||
extra: getDisplayTime(note),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} type="note" />
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ItemFlexColumn>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemFlexColumn = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 20px 16px;
|
||||
height: calc(100vh - 135px);
|
||||
`
|
||||
|
||||
export default KnowledgeNotes
|
||||
142
src/renderer/src/pages/knowledge/items/KnowledgeSitemaps.tsx
Normal file
142
src/renderer/src/pages/knowledge/items/KnowledgeSitemaps.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, message, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import StatusIcon from '../components/StatusIcon'
|
||||
import {
|
||||
ClickableSpan,
|
||||
FlexAlignCenter,
|
||||
ItemContainer,
|
||||
ItemHeader,
|
||||
KnowledgeEmptyView,
|
||||
RefreshIcon,
|
||||
StatusIconWrapper
|
||||
} from '../KnowledgeContent'
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeSitemaps: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { base, sitemapItems, refreshItem, addSitemap, removeItem, getProcessingStatus } = useKnowledge(
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleAddSitemap = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = await PromptPopup.show({
|
||||
title: t('knowledge.add_sitemap'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.sitemap_placeholder'),
|
||||
inputProps: {
|
||||
maxLength: 1000,
|
||||
rows: 1
|
||||
}
|
||||
})
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
new URL(url)
|
||||
if (sitemapItems.find((item) => item.content === url)) {
|
||||
message.success(t('knowledge.sitemap_added'))
|
||||
return
|
||||
}
|
||||
addSitemap(url)
|
||||
} catch (e) {
|
||||
console.error('Invalid Sitemap URL:', url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemContainer>
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddSitemap()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_sitemap')}
|
||||
</Button>
|
||||
</ItemHeader>
|
||||
<ItemFlexColumn>
|
||||
{sitemapItems.length === 0 && <KnowledgeEmptyView />}
|
||||
{sitemapItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<ClickableSpan>
|
||||
<Tooltip title={item.content as string}>
|
||||
<Ellipsis>
|
||||
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||
{item.content as string}
|
||||
</a>
|
||||
</Ellipsis>
|
||||
</Tooltip>
|
||||
</ClickableSpan>
|
||||
),
|
||||
ext: '.sitemap',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={item.id}
|
||||
base={base}
|
||||
getProcessingStatus={getProcessingStatus}
|
||||
type="sitemap"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ItemFlexColumn>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemFlexColumn = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 20px 16px;
|
||||
height: calc(100vh - 135px);
|
||||
`
|
||||
|
||||
export default KnowledgeSitemaps
|
||||
190
src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx
Normal file
190
src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, Dropdown, message, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import StatusIcon from '../components/StatusIcon'
|
||||
import {
|
||||
ClickableSpan,
|
||||
FlexAlignCenter,
|
||||
ItemContainer,
|
||||
ItemHeader,
|
||||
KnowledgeEmptyView,
|
||||
RefreshIcon,
|
||||
StatusIconWrapper
|
||||
} from '../KnowledgeContent'
|
||||
|
||||
interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const getDisplayTime = (item: KnowledgeItem) => {
|
||||
const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at
|
||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { base, urlItems, refreshItem, addUrl, removeItem, getProcessingStatus, updateItem } = useKnowledge(
|
||||
selectedBase.id || ''
|
||||
)
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleAddUrl = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const urlInput = await PromptPopup.show({
|
||||
title: t('knowledge.add_url'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.url_placeholder'),
|
||||
inputProps: {
|
||||
rows: 10,
|
||||
onPressEnter: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
if (urlInput) {
|
||||
// Split input by newlines and filter out empty lines
|
||||
const urls = urlInput.split('\n').filter((url) => url.trim())
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
new URL(url.trim())
|
||||
if (!urlItems.find((item) => item.content === url.trim())) {
|
||||
addUrl(url.trim())
|
||||
} else {
|
||||
message.success(t('knowledge.url_added'))
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid URLs silently
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditRemark = async (item: KnowledgeItem) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const editedRemark: string | undefined = await PromptPopup.show({
|
||||
title: t('knowledge.edit_remark'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.edit_remark_placeholder'),
|
||||
defaultValue: item.remark || '',
|
||||
inputProps: {
|
||||
maxLength: 100,
|
||||
rows: 1
|
||||
}
|
||||
})
|
||||
|
||||
if (editedRemark !== undefined && editedRemark !== null) {
|
||||
updateItem({
|
||||
...item,
|
||||
remark: editedRemark,
|
||||
updated_at: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemContainer>
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddUrl()
|
||||
}}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.add_url')}
|
||||
</Button>
|
||||
</ItemHeader>
|
||||
<ItemFlexColumn>
|
||||
{urlItems.length === 0 && <KnowledgeEmptyView />}
|
||||
{urlItems.reverse().map((item) => (
|
||||
<FileItem
|
||||
key={item.id}
|
||||
fileInfo={{
|
||||
name: (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
label: t('knowledge.edit_remark'),
|
||||
onClick: () => handleEditRemark(item)
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <CopyOutlined />,
|
||||
label: t('common.copy'),
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(item.content as string)
|
||||
message.success(t('message.copied'))
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['contextMenu']}>
|
||||
<ClickableSpan>
|
||||
<Tooltip title={item.content as string}>
|
||||
<Ellipsis>
|
||||
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||
{item.remark || (item.content as string)}
|
||||
</a>
|
||||
</Ellipsis>
|
||||
</Tooltip>
|
||||
</ClickableSpan>
|
||||
</Dropdown>
|
||||
),
|
||||
ext: '.url',
|
||||
extra: getDisplayTime(item),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} type="url" />
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ItemFlexColumn>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemFlexColumn = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 20px 16px;
|
||||
height: calc(100vh - 135px);
|
||||
`
|
||||
|
||||
export default KnowledgeUrls
|
||||
Loading…
Reference in New Issue
Block a user