diff --git a/src/renderer/pages/agents/components/sessionItemActions.tsx b/src/renderer/pages/agents/components/sessionItemActions.tsx index aceee7cb74..b97729eb79 100644 --- a/src/renderer/pages/agents/components/sessionItemActions.tsx +++ b/src/renderer/pages/agents/components/sessionItemActions.tsx @@ -56,7 +56,7 @@ sessionActionRegistry.registerAction({ id: 'session.rename', commandId: 'session.rename', label: ({ t }) => t('common.rename'), - icon: () => , + icon: () => , order: 10, surface: 'menu' }) @@ -65,7 +65,8 @@ sessionActionRegistry.registerAction({ id: 'session.toggle-pin', commandId: 'session.toggle-pin', label: ({ pinned, t }) => (pinned ? t('agent.session.unpin.title') : t('agent.session.pin.title')), - icon: ({ pinned }) => (pinned ? : ), + icon: ({ pinned }) => + pinned ? : , order: 20, surface: 'menu' }) @@ -74,7 +75,7 @@ sessionActionRegistry.registerAction({ id: 'session.open-in-new-tab', commandId: 'session.open-in-new-tab', label: ({ t }) => t('common.open_in_new_tab'), - icon: () => , + icon: () => , order: 30, surface: 'menu' }) @@ -83,7 +84,7 @@ sessionActionRegistry.registerAction({ id: 'session.open-in-new-window', commandId: 'session.open-in-new-window', label: ({ t }) => t('tab.open_in_new_window'), - icon: () => , + icon: () => , order: 35, surface: 'menu' }) @@ -92,7 +93,7 @@ sessionActionRegistry.registerAction({ id: 'session.delete', commandId: 'session.delete', label: ({ t }) => t('common.delete'), - icon: () => , + icon: () => , group: 'danger', order: 40, surface: 'menu', diff --git a/src/renderer/pages/knowledge/components/CreateKnowledgeBaseDialog.tsx b/src/renderer/pages/knowledge/components/CreateKnowledgeBaseDialog.tsx index 28e3526199..2033320c0e 100644 --- a/src/renderer/pages/knowledge/components/CreateKnowledgeBaseDialog.tsx +++ b/src/renderer/pages/knowledge/components/CreateKnowledgeBaseDialog.tsx @@ -83,7 +83,7 @@ const CreateKnowledgeBaseDialogActions = ({ - @@ -164,7 +164,9 @@ const CreateKnowledgeBaseDialogRoot = ({ - + ) => void onDeleteBase: (baseId: string) => Promise | void onRebuild: () => void + onAddSource: (source: KnowledgeItemType) => void } const DetailHeader = ({ @@ -30,11 +33,13 @@ const DetailHeader = ({ onOpenRecallTest, onRenameBase, onDeleteBase, - onRebuild + onRebuild, + onAddSource }: DetailHeaderProps) => { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const [isMenuOpen, setIsMenuOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false) const statusLabelKey = `knowledge.status.${base.status}` as const const statusLabel = t(statusLabelKey) @@ -52,36 +57,84 @@ const DetailHeader = ({ setIsDeleteDialogOpen(false) }, [base.id, onDeleteBase]) + const handleSourceSelect = useCallback( + (source: KnowledgeItemType) => { + setIsSourceMenuOpen(false) + onAddSource(source) + }, + [onAddSource] + ) + return ( <>
-
-

{base.name}

- {base.status === 'failed' ? ( - + ) : ( + {statusLabel} - - ) : ( - - {statusLabel} - - )} + )} + + {t('knowledge.meta.updated_at', { time: formatRelativeTime(base.updatedAt, i18n.language) })} + +
+ + + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()}> + + {KNOWLEDGE_DATA_SOURCE_TYPES.map((source) => ( + handleSourceSelect(source.value)} + /> + ))} + + + {base.status !== 'failed' && ( <> )} diff --git a/src/renderer/pages/knowledge/components/KnowledgeEntityNameDialog.tsx b/src/renderer/pages/knowledge/components/KnowledgeEntityNameDialog.tsx index d376416db9..f1b0309950 100644 --- a/src/renderer/pages/knowledge/components/KnowledgeEntityNameDialog.tsx +++ b/src/renderer/pages/knowledge/components/KnowledgeEntityNameDialog.tsx @@ -102,7 +102,7 @@ const KnowledgeEntityNameDialog = ({ - diff --git a/src/renderer/pages/knowledge/components/KnowledgeModelSelect.tsx b/src/renderer/pages/knowledge/components/KnowledgeModelSelect.tsx index 1435507df0..7bf9c5e664 100644 --- a/src/renderer/pages/knowledge/components/KnowledgeModelSelect.tsx +++ b/src/renderer/pages/knowledge/components/KnowledgeModelSelect.tsx @@ -64,8 +64,8 @@ export const KnowledgeModelSelect = ({ aria-label={ariaLabel} aria-invalid={invalid || undefined} className={cn( - 'h-8 w-full justify-between gap-2 rounded-md px-3 font-normal text-sm shadow-none', - 'aria-expanded:border-primary aria-expanded:ring-3 aria-expanded:ring-primary/20', + 'h-7 w-full justify-between gap-2 rounded-lg border-transparent bg-muted/50 px-2.5 font-normal text-sm shadow-none hover:bg-muted', + 'aria-expanded:bg-muted', hasValue ? 'text-foreground' : 'text-muted-foreground', invalid && 'border-destructive aria-expanded:ring-red-600/20' )}> diff --git a/src/renderer/pages/knowledge/components/__tests__/DetailHeader.test.tsx b/src/renderer/pages/knowledge/components/__tests__/DetailHeader.test.tsx index 4bfb20531a..8cac413930 100644 --- a/src/renderer/pages/knowledge/components/__tests__/DetailHeader.test.tsx +++ b/src/renderer/pages/knowledge/components/__tests__/DetailHeader.test.tsx @@ -5,6 +5,10 @@ import { describe, expect, it, vi } from 'vitest' import DetailHeader from '../DetailHeader' +vi.mock('@renderer/utils/time', () => ({ + formatRelativeTime: () => '刚刚' +})) + vi.mock('@cherrystudio/ui', async () => { const React = await import('react') const PopoverContext = React.createContext<{ @@ -92,6 +96,7 @@ vi.mock('@cherrystudio/ui', async () => { onClick?: (event: React.MouseEvent) => void }> + // eslint-disable-next-line @eslint-react/no-clone-element return React.cloneElement(child, { onClick: (event: React.MouseEvent) => { child.props.onClick?.(event) @@ -121,6 +126,11 @@ vi.mock('react-i18next', () => ({ 'common.clear': '清除', 'common.delete': '删除', 'common.more': '更多', + 'knowledge.data_source.toolbar.add': '添加数据源', + 'knowledge.data_source.add_dialog.sources.file': '文件', + 'knowledge.data_source.add_dialog.sources.note': '笔记', + 'knowledge.data_source.add_dialog.sources.directory': '目录', + 'knowledge.data_source.add_dialog.sources.url': '链接', 'knowledge.context.delete': '删除知识库', 'knowledge.context.delete_confirm_description': '删除后无法恢复', 'knowledge.context.delete_confirm_title': '确认删除知识库', @@ -172,6 +182,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={vi.fn()} onRebuild={vi.fn()} + onAddSource={vi.fn()} /> ) @@ -191,6 +202,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={vi.fn()} onRebuild={onRebuild} + onAddSource={vi.fn()} /> ) @@ -224,6 +236,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={vi.fn()} onRebuild={onRebuild} + onAddSource={vi.fn()} /> ) @@ -243,6 +256,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={vi.fn()} onRebuild={vi.fn()} + onAddSource={vi.fn()} /> ) @@ -264,6 +278,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={vi.fn()} onRebuild={vi.fn()} + onAddSource={vi.fn()} /> ) @@ -284,6 +299,7 @@ describe('DetailHeader', () => { onRenameBase={onRenameBase} onDeleteBase={vi.fn()} onRebuild={vi.fn()} + onAddSource={vi.fn()} /> ) @@ -307,6 +323,7 @@ describe('DetailHeader', () => { onRenameBase={vi.fn()} onDeleteBase={onDeleteBase} onRebuild={vi.fn()} + onAddSource={vi.fn()} /> ) @@ -322,4 +339,41 @@ describe('DetailHeader', () => { expect(onDeleteBase).toHaveBeenCalledWith('base-1') }) }) + + it('renders the updated-at time after the title', () => { + render( + + ) + + expect(screen.getByText('更新于 刚刚')).toBeInTheDocument() + }) + + it('opens the add-source menu and forwards the selected source', () => { + const onAddSource = vi.fn() + + render( + + ) + + fireEvent.click(screen.getByRole('button', { name: '添加数据源' })) + fireEvent.click(screen.getByRole('menuitem', { name: '文件' })) + + expect(onAddSource).toHaveBeenCalledWith('file') + }) }) diff --git a/src/renderer/pages/knowledge/components/addKnowledgeItemDialog/AddKnowledgeItemDialogFooter.tsx b/src/renderer/pages/knowledge/components/addKnowledgeItemDialog/AddKnowledgeItemDialogFooter.tsx index 6e67a45642..b12fe69df3 100644 --- a/src/renderer/pages/knowledge/components/addKnowledgeItemDialog/AddKnowledgeItemDialogFooter.tsx +++ b/src/renderer/pages/knowledge/components/addKnowledgeItemDialog/AddKnowledgeItemDialogFooter.tsx @@ -31,7 +31,7 @@ const AddKnowledgeItemDialogFooter = ({ ? t('knowledge.data_source.add_dialog.footer.selected_notes', { count: selectedNoteCount }) : '' return ( -
+
{errorMessage ? (
) @@ -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} - /> -
+ selectedIds.size > 0 ? ( +
+ setIsBulkDeleteOpen(true)} + /> +
+ ) : undefined }>
{!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) => ( - handleSourceSelect(source.value)} - /> - ))} - - - + +
) 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')} - diff --git a/src/renderer/pages/knowledge/panels/ragConfig/__tests__/RagConfigPanel.test.tsx b/src/renderer/pages/knowledge/panels/ragConfig/__tests__/RagConfigPanel.test.tsx index 83b889e272..eeb4f49000 100644 --- a/src/renderer/pages/knowledge/panels/ragConfig/__tests__/RagConfigPanel.test.tsx +++ b/src/renderer/pages/knowledge/panels/ragConfig/__tests__/RagConfigPanel.test.tsx @@ -377,11 +377,11 @@ describe('RagConfigPanel', () => { it('uses the mini-apps style flat field layout', () => { renderRagConfigPanel() - // Each field label is now a strong text-sm font-medium label (mini-apps FieldLabel parity). - expect(screen.getByText('文档处理')).toHaveClass('font-medium', 'text-sm') - expect(screen.getByText('分块大小')).toHaveClass('font-medium', 'text-sm') - expect(screen.getByText('嵌入模型')).toHaveClass('font-medium', 'text-sm') - expect(screen.getByText('请求文档片段数 (Top K)')).toHaveClass('font-medium', 'text-sm') + // Each field label is a small, regular-weight label (mini-apps FieldLabel parity). + expect(screen.getByText('文档处理')).toHaveClass('font-normal', 'text-xs') + expect(screen.getByText('分块大小')).toHaveClass('font-normal', 'text-xs') + expect(screen.getByText('嵌入模型')).toHaveClass('font-normal', 'text-xs') + expect(screen.getByText('请求文档片段数 (Top K)')).toHaveClass('font-normal', 'text-xs') // Section-level small-caps headings are gone — no Chunking / Embedding / Retrieval section title in the DOM. expect(screen.queryByText('Chunking')).not.toBeInTheDocument() expect(screen.queryByText('Embedding')).not.toBeInTheDocument() diff --git a/src/renderer/pages/knowledge/panels/ragConfig/panelPrimitives.tsx b/src/renderer/pages/knowledge/panels/ragConfig/panelPrimitives.tsx index 4c0345ff12..9eea225ea8 100644 --- a/src/renderer/pages/knowledge/panels/ragConfig/panelPrimitives.tsx +++ b/src/renderer/pages/knowledge/panels/ragConfig/panelPrimitives.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react' export const RagFieldLabel = ({ className, label, hint }: { className?: string; label: string; hint?: string }) => { return (
- {label} + {label} {hint ? ( @@ -163,7 +163,7 @@ export const RagSliderField = ({ step={step} size="md" disabled={disabled} - className="w-full **:data-[slot=slider-thumb]:border-primary **:data-[slot=slider-range]:bg-primary **:data-[slot=slider-thumb]:bg-background **:data-[slot=slider-track]:bg-muted **:data-[slot=slider-thumb]:shadow-sm" + className="w-full" />
diff --git a/src/renderer/pages/knowledge/panels/recallTest/RecallHistoryList.tsx b/src/renderer/pages/knowledge/panels/recallTest/RecallHistoryList.tsx index fb5ea0c7cc..86e464609f 100644 --- a/src/renderer/pages/knowledge/panels/recallTest/RecallHistoryList.tsx +++ b/src/renderer/pages/knowledge/panels/recallTest/RecallHistoryList.tsx @@ -18,7 +18,7 @@ const RecallHistoryList = () => { diff --git a/src/renderer/pages/knowledge/panels/recallTest/RecallResultCard.tsx b/src/renderer/pages/knowledge/panels/recallTest/RecallResultCard.tsx index 7873813616..85e5c2cf94 100644 --- a/src/renderer/pages/knowledge/panels/recallTest/RecallResultCard.tsx +++ b/src/renderer/pages/knowledge/panels/recallTest/RecallResultCard.tsx @@ -43,7 +43,7 @@ const RecallResultCard = ({ item, index }: RecallResultCardProps) => { return (
- + {index + 1}
@@ -60,7 +60,7 @@ const RecallResultCard = ({ item, index }: RecallResultCardProps) => { aria-label={t('knowledge.recall.copy')} className={`size-5 min-h-5 shrink-0 rounded p-0 shadow-none transition-all hover:bg-accent hover:text-foreground group-hover/chunk:opacity-100 ${copied ? 'text-success opacity-100' : 'text-foreground-muted opacity-0'}`} onClick={() => void copyContent()}> - {copied ? : } + {copied ? : }

+ className={`wrap-anywhere min-w-0 whitespace-normal text-foreground text-sm leading-relaxed ${isExpanded ? '' : 'line-clamp-2'}`}> {item.content}

diff --git a/src/renderer/pages/knowledge/sections/KnowledgePageDetailSection.tsx b/src/renderer/pages/knowledge/sections/KnowledgePageDetailSection.tsx index f1663919d1..efcbc141ab 100644 --- a/src/renderer/pages/knowledge/sections/KnowledgePageDetailSection.tsx +++ b/src/renderer/pages/knowledge/sections/KnowledgePageDetailSection.tsx @@ -52,6 +52,7 @@ const KnowledgePageDetailSection = () => { onRenameBase={openRenameBaseDialog} onDeleteBase={deleteBase} onRebuild={() => openRestoreBaseDialog(selectedBase)} + onAddSource={openAddSourceDialog} />
@@ -65,7 +66,6 @@ const KnowledgePageDetailSection = () => { hasMore={hasMoreItems} isLoadingMore={isLoadingMoreItems} onLoadMore={loadMoreItems} - updatedAt={selectedBase.updatedAt} onAdd={openAddSourceDialog} onItemClick={openItemChunks} onDelete={deleteItem} diff --git a/src/renderer/pages/library/list/AssistantCatalogTabRail.tsx b/src/renderer/pages/library/list/AssistantCatalogTabRail.tsx index 035cf20a31..0a9e56bc2c 100644 --- a/src/renderer/pages/library/list/AssistantCatalogTabRail.tsx +++ b/src/renderer/pages/library/list/AssistantCatalogTabRail.tsx @@ -26,7 +26,7 @@ export function AssistantCatalogTabRail({ tabs, activeTab, onTabChange }: Assist size="icon-sm" aria-label={t('library.assistant_catalog.scroll_left')} onClick={() => scrollRail(-1)} - className="shrink-0 rounded-full text-foreground-muted hover:text-foreground"> + className="shrink-0 rounded-full text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]">
@@ -55,7 +55,7 @@ export function AssistantCatalogTabRail({ tabs, activeTab, onTabChange }: Assist size="icon-sm" aria-label={t('library.assistant_catalog.scroll_right')} onClick={() => scrollRail(1)} - className="shrink-0 rounded-full text-foreground-muted hover:text-foreground"> + className="shrink-0 rounded-full text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]">
diff --git a/src/renderer/pages/library/list/LibrarySidebar.tsx b/src/renderer/pages/library/list/LibrarySidebar.tsx index 6717f6d5a4..c706699726 100644 --- a/src/renderer/pages/library/list/LibrarySidebar.tsx +++ b/src/renderer/pages/library/list/LibrarySidebar.tsx @@ -13,7 +13,7 @@ interface Props { const ITEM_CLASS = 'h-8 w-full cursor-pointer gap-1.5 rounded-lg border-0 px-1.5 text-[13px] font-normal ' + - 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground [&_svg]:size-4 ' + + 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground [&_svg]:size-4 [&_svg]:text-current ' + 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-foreground ' + 'focus-visible:bg-sidebar-accent focus-visible:text-sidebar-foreground focus-visible:ring-1 focus-visible:ring-sidebar-ring' diff --git a/src/renderer/pages/library/list/ResourceCards.tsx b/src/renderer/pages/library/list/ResourceCards.tsx index afd7f46c82..51cf26fe43 100644 --- a/src/renderer/pages/library/list/ResourceCards.tsx +++ b/src/renderer/pages/library/list/ResourceCards.tsx @@ -268,7 +268,7 @@ export function ResourceCard({ resource: r, allTagNames, onDelete, onDuplicate, size="icon-sm" aria-label={t('common.more')} onClick={(e) => e.stopPropagation()} - className="text-foreground-muted opacity-0 hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 data-[state=open]:opacity-100"> + className="text-foreground/80 opacity-0 hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 data-[state=open]:opacity-100 [&_svg]:[stroke-width:1.6]"> @@ -296,7 +296,7 @@ export function ResourceCard({ resource: r, allTagNames, onDelete, onDuplicate, size="icon-sm" aria-label={r.type === 'skill' ? t('library.action.uninstall') : t('common.delete')} onClick={() => onDelete(r)} - className="text-foreground-muted opacity-0 hover:bg-error-bg hover:text-error-text focus-visible:opacity-100 group-hover:opacity-100"> + className="text-foreground/80 opacity-0 hover:bg-error-bg hover:text-error-text focus-visible:opacity-100 group-hover:opacity-100 [&_svg]:[stroke-width:1.6]"> )} diff --git a/src/renderer/pages/library/list/ResourceGrid.tsx b/src/renderer/pages/library/list/ResourceGrid.tsx index 40b9852a45..1a84875db8 100644 --- a/src/renderer/pages/library/list/ResourceGrid.tsx +++ b/src/renderer/pages/library/list/ResourceGrid.tsx @@ -266,7 +266,11 @@ export const ResourceGrid: FC = ({
- + onSearchChange(e.target.value)} @@ -279,7 +283,7 @@ export const ResourceGrid: FC = ({ size="icon-sm" aria-label={t('common.clear')} onClick={() => onSearchChange('')} - className="-translate-y-1/2 absolute top-1/2 right-1 size-6 text-foreground-muted hover:text-foreground"> + className="-translate-y-1/2 absolute top-1/2 right-1 size-6 text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]"> )} @@ -360,7 +364,7 @@ export const ResourceGrid: FC = ({ aria-label={t('library.toolbar.all_tags')} title={t('library.toolbar.all_tags')} onClick={() => setShowAllTags((value) => !value)} - className="size-6 shrink-0 rounded-full text-foreground-muted hover:bg-accent hover:text-foreground"> + className="size-6 shrink-0 rounded-full text-foreground/80 hover:bg-accent hover:text-foreground [&_svg]:[stroke-width:1.6]"> {showAllTags ? : } )} @@ -391,7 +395,7 @@ export const ResourceGrid: FC = ({ size="icon-sm" onClick={() => void handleAddTag()} disabled={addingTag || !newTagName.trim()} - className="size-6 text-foreground-muted hover:text-foreground"> + className="size-6 text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]">
diff --git a/src/renderer/pages/notes/HeaderNavbar.tsx b/src/renderer/pages/notes/HeaderNavbar.tsx index cb63886cbf..8e78e9b503 100644 --- a/src/renderer/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/pages/notes/HeaderNavbar.tsx @@ -235,14 +235,14 @@ const HeaderNavbar = ({ {showWorkspace && ( - + )} {!showWorkspace && ( - + @@ -274,7 +274,7 @@ const HeaderNavbar = ({ handleBreadcrumbClick(item)}> {item.title} @@ -297,7 +297,7 @@ const HeaderNavbar = ({ {canShowStarButton && (
{activeNode.isStarred ? ( @@ -311,7 +311,7 @@ const HeaderNavbar = ({
- + diff --git a/src/renderer/pages/notes/NotesEditor.tsx b/src/renderer/pages/notes/NotesEditor.tsx index 71b7349637..3a7d942b1d 100644 --- a/src/renderer/pages/notes/NotesEditor.tsx +++ b/src/renderer/pages/notes/NotesEditor.tsx @@ -1,11 +1,10 @@ -import { EmptyState, SpaceBetweenRowFlex, Tooltip } from '@cherrystudio/ui' +import { Combobox, EmptyState, SpaceBetweenRowFlex, Tooltip } from '@cherrystudio/ui' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import ActionIconButton from '@renderer/components/Buttons/ActionIconButton' import CodeEditor, { type CodeEditorHandles } from '@renderer/components/CodeEditor' import RichEditor from '@renderer/components/RichEditor' import type { RichEditorRef } from '@renderer/components/RichEditor/types' -import Selector from '@renderer/components/Selector' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import type { EditorView } from '@renderer/types/app' import { SpellCheck } from 'lucide-react' @@ -144,11 +143,13 @@ const NotesEditor: FC = memo( /> )} - { + onChange={(value) => { userViewModeOverrideRef.current = true - setTmpViewMode(value) + setTmpViewMode(value as EditorView) }} options={[ { label: t('notes.settings.editor.edit_mode.preview_mode'), value: 'preview' }, diff --git a/src/renderer/pages/notes/NotesSettings.tsx b/src/renderer/pages/notes/NotesSettings.tsx index a1b5f4b7f3..310a394b94 100644 --- a/src/renderer/pages/notes/NotesSettings.tsx +++ b/src/renderer/pages/notes/NotesSettings.tsx @@ -1,6 +1,5 @@ -import { Button, Input, Slider, Switch } from '@cherrystudio/ui' +import { Button, Combobox, Input, Slider, Switch } from '@cherrystudio/ui' import { loggerService } from '@logger' -import Selector from '@renderer/components/Selector' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { useTheme } from '@renderer/hooks/useTheme' import { @@ -129,26 +128,28 @@ const NotesSettings: FC = () => { {t('notes.settings.editor.view_mode.title')} - updateSettings({ defaultViewMode: value })} + onChange={(value) => updateSettings({ defaultViewMode: value as 'edit' | 'read' })} /> {t('notes.settings.editor.view_mode.description')} {t('notes.settings.editor.edit_mode.title')} - ) => updateSettings({ defaultEditMode: value })} + onChange={(value) => updateSettings({ defaultEditMode: value as Exclude })} /> {t('notes.settings.editor.edit_mode.description')} diff --git a/src/renderer/pages/notes/NotesSidebar.tsx b/src/renderer/pages/notes/NotesSidebar.tsx index 832bc83f2d..c8a1f1eb99 100644 --- a/src/renderer/pages/notes/NotesSidebar.tsx +++ b/src/renderer/pages/notes/NotesSidebar.tsx @@ -352,7 +352,7 @@ const NotesSidebar: FC = ({ {t('notes.search.searching')} } /> @@ -522,8 +524,8 @@ const OpenClawPage: FC = () => { {/* Tips about OpenClaw */}
+ className="mt-4 rounded-lg bg-muted/50 p-3 text-xs leading-relaxed" + style={{ color: 'var(--color-text-3)' }}>
💡 {t('openclaw.tips.title')}
  • {t('openclaw.tips.permissions')}
  • diff --git a/src/renderer/pages/paintings/components/Artboard.tsx b/src/renderer/pages/paintings/components/Artboard.tsx index 7e9e9761f4..f972244928 100644 --- a/src/renderer/pages/paintings/components/Artboard.tsx +++ b/src/renderer/pages/paintings/components/Artboard.tsx @@ -1,7 +1,6 @@ import { Button, Tooltip } from '@cherrystudio/ui' import ImageViewer from '@renderer/components/ImageViewer' -import { ImageDown, ImageUp, RefreshCcw, RotateCcwSquare, RotateCwSquare, ZoomIn, ZoomOut } from 'lucide-react' -import { motion } from 'motion/react' +import { ImageDown, ImageUp, Loader2, RefreshCcw, RotateCcwSquare, RotateCwSquare, ZoomIn, ZoomOut } from 'lucide-react' import { type FC, type PointerEvent, @@ -46,19 +45,8 @@ const LoadingStateCard: FC<{ text: ReactNode; onCancel: () => void; cancelLabel: cancelLabel }) => { return ( -
    -
    - - -
    +
    +
    {text}
    - +
    = ({ className, pain showPinnedModels={false} showPinActions={false} prioritizedProviderIds={painting.providerId ? [painting.providerId] : undefined} - contentClassName="w-[min(420px,calc(100vw-2rem))] rounded-[8px]" + contentClassName="w-[min(420px,calc(100vw-2rem))]" trigger={ {items.map((painting) => ( diff --git a/src/renderer/pages/paintings/form/fields/SelectField.tsx b/src/renderer/pages/paintings/form/fields/SelectField.tsx index dd254a6a70..ee2c091e4d 100644 --- a/src/renderer/pages/paintings/form/fields/SelectField.tsx +++ b/src/renderer/pages/paintings/form/fields/SelectField.tsx @@ -26,9 +26,7 @@ export default function SelectField({ return (