{errorMessage ? (
)
}
diff --git a/src/renderer/pages/knowledge/components/navigator/KnowledgeBaseRow.tsx b/src/renderer/pages/knowledge/components/navigator/KnowledgeBaseRow.tsx
index d96b914185..67bc1c251f 100644
--- a/src/renderer/pages/knowledge/components/navigator/KnowledgeBaseRow.tsx
+++ b/src/renderer/pages/knowledge/components/navigator/KnowledgeBaseRow.tsx
@@ -49,7 +49,7 @@ const KnowledgeBaseRow = ({
type: 'item',
id: 'rename',
label: t('knowledge.context.rename'),
- icon:
,
+ icon:
,
onSelect: handleRenameBase
}
]
@@ -59,7 +59,7 @@ const KnowledgeBaseRow = ({
type: 'submenu',
id: 'move',
label: t('knowledge.context.move_to'),
- icon:
,
+ icon:
,
children: [
...(canMoveToUngrouped
? ([
@@ -86,7 +86,7 @@ const KnowledgeBaseRow = ({
type: 'item',
id: 'delete',
label: t('knowledge.context.delete'),
- icon:
,
+ icon:
,
destructive: true,
onSelect: handleRequestDelete
})
@@ -126,7 +126,7 @@ const KnowledgeBaseRow = ({
size="icon-sm"
aria-label={t('common.more')}
className={cn(
- 'text-foreground-muted hover:bg-accent group-focus-within/kb:opacity-100 group-focus-within:opacity-100 group-hover/kb:opacity-100 group-hover:opacity-100',
+ 'text-foreground/80 hover:bg-accent group-focus-within/kb:opacity-100 group-focus-within:opacity-100 group-hover/kb:opacity-100 group-hover:opacity-100 [&_svg]:[stroke-width:1.6]',
moreMenuOpen ? 'opacity-100' : 'opacity-0'
)}>
diff --git a/src/renderer/pages/knowledge/components/navigator/KnowledgeGroupRow.tsx b/src/renderer/pages/knowledge/components/navigator/KnowledgeGroupRow.tsx
index a076297dc6..7aa4b24c73 100644
--- a/src/renderer/pages/knowledge/components/navigator/KnowledgeGroupRow.tsx
+++ b/src/renderer/pages/knowledge/components/navigator/KnowledgeGroupRow.tsx
@@ -87,7 +87,7 @@ const KnowledgeGroupRow = ({
size="icon-sm"
aria-label={t('common.more')}
className={cn(
- 'size-6 min-h-6 min-w-6 rounded-md p-0 text-foreground-muted hover:bg-accent hover:text-foreground group-focus-within/grp:opacity-100 group-hover/grp:opacity-100 [&_svg]:size-3.5',
+ 'size-6 min-h-6 min-w-6 rounded-md p-0 text-foreground/80 hover:bg-accent hover:text-foreground group-focus-within/grp:opacity-100 group-hover/grp:opacity-100 [&_svg]:size-3.5 [&_svg]:[stroke-width:1.6]',
moreMenuOpen ? 'opacity-100' : 'opacity-0'
)}>
diff --git a/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanel.tsx b/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanel.tsx
index 7915d51a9e..ecc574d36c 100644
--- a/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanel.tsx
+++ b/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanel.tsx
@@ -20,7 +20,6 @@ export interface DataSourcePanelProps {
hasMore?: boolean
isLoadingMore?: boolean
onLoadMore?: () => void
- updatedAt: string
onAdd: (source?: KnowledgeItemType, files?: File[]) => void
onItemClick?: (itemId: string) => void
onDelete: (item: KnowledgeItem) => void | Promise
@@ -47,9 +46,9 @@ const DataSourceEmptyState = ({ onAddSource }: { onAddSource: (source: Knowledge
type="button"
variant="outline"
size="lg"
- className="h-9 w-24 rounded-lg px-3 font-medium"
+ className="h-8 w-20 rounded-lg px-3 font-medium"
onClick={() => onAddSource(source.value)}>
-
+
{t(source.labelKey)}
)
@@ -67,7 +66,6 @@ const DataSourcePanel = ({
hasMore = false,
isLoadingMore = false,
onLoadMore = () => undefined,
- updatedAt,
onAdd,
onItemClick,
onDelete,
@@ -153,17 +151,17 @@ const DataSourcePanel = ({
- setIsBulkDeleteOpen(true)}
- onAdd={handleAddSource}
- />
-
{!isLoading && items.length === 0 ? (
diff --git a/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanelHeader.tsx b/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanelHeader.tsx
index 33eccf2fae..2aefc8fa27 100644
--- a/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanelHeader.tsx
+++ b/src/renderer/pages/knowledge/panels/dataSource/DataSourcePanelHeader.tsx
@@ -1,114 +1,49 @@
-import { Button, MenuItem, MenuList, Popover, PopoverContent, PopoverTrigger } from '@cherrystudio/ui'
-import { formatRelativeTime } from '@renderer/utils/time'
-import type { KnowledgeItemType } from '@shared/data/types/knowledge'
-import { Plus, RefreshCw, Trash2 } from 'lucide-react'
-import { useCallback, useState } from 'react'
+import { Button } from '@cherrystudio/ui'
+import { RefreshCw, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
-import { KNOWLEDGE_DATA_SOURCE_TYPES } from '../../components/addKnowledgeItemDialog/constants'
-
interface DataSourcePanelHeaderProps {
/** Server-side total across all pages. */
total: number
/** Rows currently loaded in the renderer (≤ total when pages remain). */
loadedCount: number
selectedCount: number
- updatedAt: string
onBulkReindex: () => void
onBulkDelete: () => void
- onAdd: (source: KnowledgeItemType) => void
}
const DataSourcePanelHeader = ({
total,
loadedCount,
selectedCount,
- updatedAt,
onBulkReindex,
- onBulkDelete,
- onAdd
+ onBulkDelete
}: DataSourcePanelHeaderProps) => {
- const { t, i18n } = useTranslation()
- const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false)
-
- const handleSourceSelect = useCallback(
- (source: KnowledgeItemType) => {
- setIsSourceMenuOpen(false)
- onAdd(source)
- },
- [onAdd]
- )
-
- if (selectedCount > 0) {
- return (
-
-
-
- {t('knowledge.data_source.bulk.selected_count', { count: selectedCount })}
-
- {/* Selection only covers loaded rows; warn when unloaded pages remain so the
- checked-all state doesn't read as "all rows in the base". */}
- {total > loadedCount ? (
-
- {t('knowledge.data_source.bulk.loaded_only_hint', { total })}
-
- ) : null}
-
-
-
-
-
-
- )
- }
+ const { t } = useTranslation()
return (
-
-
- {t('knowledge.meta.updated_at', { time: formatRelativeTime(updatedAt, i18n.language) })}
+
+
+
+ {t('knowledge.data_source.bulk.selected_count', { count: selectedCount })}
+
+ {/* Selection only covers loaded rows; warn when unloaded pages remain so the
+ checked-all state doesn't read as "all rows in the base". */}
+ {total > loadedCount ? (
+
+ {t('knowledge.data_source.bulk.loaded_only_hint', { total })}
+
+ ) : null}
-
-
-
-
- event.preventDefault()}
- onCloseAutoFocus={(event) => event.preventDefault()}>
-
- {KNOWLEDGE_DATA_SOURCE_TYPES.map((source) => (
-
-
-
+
+
)
diff --git a/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemChunkDetailPanel.tsx b/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemChunkDetailPanel.tsx
index c0b3a6026b..2dcb902ca6 100644
--- a/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemChunkDetailPanel.tsx
+++ b/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemChunkDetailPanel.tsx
@@ -27,7 +27,7 @@ const KnowledgeItemChunkCard = ({ chunk }: { chunk: KnowledgeItemChunk }) => {
return (
-
+
{chunk.metadata.chunkIndex + 1}
@@ -35,7 +35,7 @@ const KnowledgeItemChunkCard = ({ chunk }: { chunk: KnowledgeItemChunk }) => {
-
{chunk.content}
+
{chunk.content}
)
diff --git a/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemRow.tsx b/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemRow.tsx
index ad431cc139..403e08f385 100644
--- a/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemRow.tsx
+++ b/src/renderer/pages/knowledge/panels/dataSource/KnowledgeItemRow.tsx
@@ -105,7 +105,7 @@ const KnowledgeItemRow = ({
type: 'item',
id: 'preview-source',
label: t('knowledge.data_source.actions.preview_source'),
- icon: ,
+ icon: ,
onSelect: () => {
void Promise.resolve(onPreviewSource()).catch((error) => {
window.toast.error(formatErrorMessageWithPrefix(error, t('knowledge.data_source.preview.failed')))
@@ -119,7 +119,7 @@ const KnowledgeItemRow = ({
type: 'item',
id: 'view-chunks',
label: t('knowledge.data_source.actions.view_chunks'),
- icon: ,
+ icon: ,
onSelect: onViewChunks
})
}
@@ -129,7 +129,7 @@ const KnowledgeItemRow = ({
type: 'item',
id: 'reindex',
label: t('knowledge.data_source.actions.reindex'),
- icon: ,
+ icon: ,
onSelect: () => {
void Promise.resolve(onReindex()).catch((error) => {
window.toast.error(formatErrorMessageWithPrefix(error, t('knowledge.data_source.reindex_failed')))
@@ -143,7 +143,7 @@ const KnowledgeItemRow = ({
type: 'item',
id: 'delete',
label: t('knowledge.data_source.actions.delete'),
- icon: ,
+ icon: ,
destructive: true,
onSelect: () => {
void Promise.resolve(onDelete()).catch((error) => {
diff --git a/src/renderer/pages/knowledge/panels/dataSource/__tests__/DataSourcePanel.test.tsx b/src/renderer/pages/knowledge/panels/dataSource/__tests__/DataSourcePanel.test.tsx
index 20e5d6ba04..67e8877cec 100644
--- a/src/renderer/pages/knowledge/panels/dataSource/__tests__/DataSourcePanel.test.tsx
+++ b/src/renderer/pages/knowledge/panels/dataSource/__tests__/DataSourcePanel.test.tsx
@@ -213,10 +213,6 @@ vi.mock('react-i18next', () => ({
return `确认删除选中的 ${options?.count} 个数据源`
}
- if (key === 'knowledge.meta.updated_at') {
- return `更新于 ${options?.time ?? ''}`
- }
-
return (
(
{
@@ -296,28 +292,12 @@ describe('DataSourcePanel', () => {
it('renders loading and empty states through the list composition without changing panel behavior', () => {
const { rerender } = render(
-
+
)
expect(screen.getByText('加载中...')).toBeInTheDocument()
- rerender(
-
- )
+ rerender()
expect(screen.getByText('上传第一个数据源')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '文件' })).toBeInTheDocument()
@@ -331,14 +311,7 @@ describe('DataSourcePanel', () => {
const onAdd = vi.fn()
const { rerender } = render(
-
+
)
expect(screen.getByText('暂无数据源')).toBeInTheDocument()
@@ -355,16 +328,7 @@ describe('DataSourcePanel', () => {
expect(onAdd).toHaveBeenCalledWith('file')
- rerender(
-
- )
+ rerender()
fireEvent.click(screen.getByRole('button', { name: '链接' }))
expect(onAdd).toHaveBeenCalledWith('url')
@@ -373,7 +337,6 @@ describe('DataSourcePanel', () => {
it('uses the first non-empty note line as the title and leaves blank notes without the old fallback label', () => {
render(
{
it('renders url and directory items from their required source fields', () => {
render(
{
const directoryTitle = screen.getByText('本地资料夹')
expect(directoryTitle).toBeInTheDocument()
expect(directoryTitle).toHaveAttribute('title', '/Users/eeee/本地资料夹')
- expect(screen.getByText('更新于 刚刚')).toBeInTheDocument()
})
it('renders processing directory rows as processing when no phase is available', () => {
render(
{
// migration-failed tooltip so the user knows to delete and re-upload.
render(
{
expect(screen.getByLabelText('该文件夹内容迁移失败,请删除后重新上传。')).toBeInTheDocument()
})
- it('does not open the add source dialog from the header button before a source is selected', () => {
- const onAdd = vi.fn()
-
- render(
-
- )
-
- fireEvent.click(screen.getByRole('button', { name: '添加数据源' }))
-
- expect(onAdd).not.toHaveBeenCalled()
- expect(screen.getByText('季度报告.pdf')).toBeInTheDocument()
- })
-
- it('opens the add dialog when selecting the file source from the header menu', () => {
- const onAdd = vi.fn()
-
- render(
-
- )
-
- expect(document.querySelector('input[type="file"]')).toBeNull()
-
- fireEvent.mouseEnter(screen.getByRole('button', { name: '添加数据源' }))
- fireEvent.click(screen.getByRole('menuitem', { name: '文件' }))
-
- expect(onAdd).toHaveBeenCalledWith('file')
- })
-
- it('shows source choices on header add hover and forwards the selected source', () => {
- const onAdd = vi.fn()
-
- render(
-
- )
-
- fireEvent.mouseEnter(screen.getByRole('button', { name: '添加数据源' }))
- fireEvent.click(screen.getByRole('menuitem', { name: '目录' }))
-
- expect(onAdd).toHaveBeenCalledWith('directory')
- })
-
it('prunes selected item ids when items are removed', async () => {
const onDelete = vi.fn().mockResolvedValue(undefined)
const { rerender } = render(
{
rerender(
{
render(
{
render(
{
render(
{
render(
{
render(
{
render(
{
render(
{
render(
{
render(
{
it('selects all rows from the header checkbox and clears selection when toggled again from all selected', async () => {
render(
{
it('warns that select-all only covers loaded rows when more pages remain on the server', () => {
render(
{
it('shows the header select-all checkbox as partially selected after deselecting one selected row', () => {
render(
{
it('prunes selected item ids when the backing item list changes', async () => {
const { rerender } = render(
{
rerender(
{
render(
({
- formatRelativeTime: () => '刚刚'
-}))
-
vi.mock('@cherrystudio/ui', () => ({
Button: ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => (
-
- ),
- MenuItem: ({ label, ...props }: { label: string; [key: string]: unknown }) => ,
- MenuList: ({ children }: { children: ReactNode }) => {children}
,
- Popover: ({ children }: { children: ReactNode }) => {children}
,
- PopoverContent: ({ children }: { children: ReactNode }) => {children}
,
- PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}>
+
+ )
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
- i18n: { language: 'zh-CN' },
t: (key: string, opts?: Record) => {
if (key === 'knowledge.data_source.bulk.selected_count') return `已选 ${opts?.count}`
- if (key === 'knowledge.meta.updated_at') return `更新于 ${opts?.time}`
if (key === 'knowledge.data_source.bulk.loaded_only_hint') return `仅已加载,共 ${opts?.total} 项`
return (
(
{
- 'knowledge.data_source.bulk.cancel': '取消',
'knowledge.data_source.bulk.reindex': '重新索引',
- 'knowledge.data_source.bulk.delete': '删除',
- 'knowledge.data_source.toolbar.add': '添加'
+ 'knowledge.data_source.bulk.delete': '删除'
} as Record
)[key] ?? key
)
@@ -45,66 +34,36 @@ vi.mock('react-i18next', () => ({
const baseProps = {
total: 5,
loadedCount: 5,
- selectedCount: 0,
- updatedAt: '2026-06-16T00:00:00.000Z',
+ selectedCount: 2,
onBulkReindex: vi.fn(),
- onBulkDelete: vi.fn(),
- onAdd: vi.fn()
+ onBulkDelete: vi.fn()
}
describe('DataSourcePanelHeader', () => {
- it('renders the updated time and add button in the default state', () => {
- render()
-
- expect(screen.getByText('更新于 刚刚')).toBeInTheDocument()
- expect(screen.getByRole('button', { name: '添加' })).toBeInTheDocument()
- })
-
- it('switches to the bulk toolbar when rows are selected', () => {
- render()
+ it('renders the bulk toolbar with the selected count', () => {
+ render()
expect(screen.getByText('已选 2')).toBeInTheDocument()
- expect(screen.queryByRole('button', { name: '取消' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: '重新索引' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '删除' })).toBeInTheDocument()
})
it('warns that a selection only covers loaded rows when unloaded pages remain', () => {
- const { rerender } = render(
-
- )
+ const { rerender } = render()
expect(screen.getByText('仅已加载,共 200 项')).toBeInTheDocument()
// Fully loaded (total === loadedCount): no hint.
- rerender()
+ rerender()
expect(screen.queryByText('仅已加载,共 50 项')).not.toBeInTheDocument()
})
- // Regression for the QA issue "选中文件后列表轻微上移": the default toolbar
- // (32px add button) and the bulk toolbar (28px sm buttons) differed by 4px,
- // shifting the list on selection. Both states must keep the same min height.
- it('keeps the same min height across default and selected states', () => {
- const { container: defaultContainer } = render()
- const { container: selectedContainer } = render()
-
- expect(defaultContainer.firstChild).toHaveClass('min-h-8')
- expect(selectedContainer.firstChild).toHaveClass('min-h-8')
- })
-
- it('invokes bulk callbacks from the selected-state toolbar', () => {
+ it('invokes bulk callbacks from the toolbar', () => {
const onBulkReindex = vi.fn()
const onBulkDelete = vi.fn()
- render(
-
- )
+ render()
fireEvent.click(screen.getByRole('button', { name: '重新索引' }))
fireEvent.click(screen.getByRole('button', { name: '删除' }))
diff --git a/src/renderer/pages/knowledge/panels/ragConfig/RagConfigPanel.tsx b/src/renderer/pages/knowledge/panels/ragConfig/RagConfigPanel.tsx
index 6880f7462e..1b56d485db 100644
--- a/src/renderer/pages/knowledge/panels/ragConfig/RagConfigPanel.tsx
+++ b/src/renderer/pages/knowledge/panels/ragConfig/RagConfigPanel.tsx
@@ -163,7 +163,7 @@ const ActiveRagConfigPanel = ({ base, onRestoreBase }: RagConfigPanelProps) => {
{t('knowledge.rag.reset_action')}
-