diff --git a/docs/features/document-processing/ui-design.md b/docs/features/document-processing/ui-design.md index a771a7b2..8ee82c83 100644 --- a/docs/features/document-processing/ui-design.md +++ b/docs/features/document-processing/ui-design.md @@ -65,7 +65,7 @@ | 草稿图片,发送提取文字 | 查看解析结果、打开原图 | 改为发送原图、使用当前设置重新提取文字、移除 | | 历史消息附件 | 查看已保存结果、打开原件 | 需要重解析或改变发送方式时先添加到当前可编辑草稿;不修改原消息 | | 已入队附件 | 仅有附件时显示“查看附件”,查看入队时冻结的结果与发送方式;无附件时隐藏入口 | 修改整条输入使用“恢复到草稿”,按 L-H7 合并;不直接修改队列附件 | -| 知识文档 | 查看解析结果、打开来源、管理分块 | 使用当前设置重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 | +| 知识文档 | 查看解析结果、打开来源、管理分块 | 重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 | | 设置测试结果 | 查看正文、图片、详情及原件 | 重新测试、关闭;没有加入草稿、知识库或发送操作 | 操作依据对象、资源和任务状态显示。解析中仅保留可用旧结果的查看、进度和取消,不允许重复 @@ -176,7 +176,7 @@ reference 使用原路径。reference 原件失效时显示“原文件不可用 和“重试”;有部分产物时可以查看,但不能标为成功。关闭测试预览后释放无引用测试资源。 测试预览不提供“添加到会话”,防止诊断文件进入用户草稿或知识库。 -“使用当前设置重新解析”是聊天草稿和知识库文档的次操作,点击前显示将使用的已保存来源。 +“使用当前设置重新解析”是聊天草稿的次操作;知识库列表和预览中的同一操作简写为“重新解析”,仍使用当前已保存的设置。 历史消息和队列冻结已接受的输入;其预览只读。历史消息使用“添加到当前草稿重新解析”, 产生草稿附件而不改写已发送正文;队列修改使用“恢复到草稿”,避免原队列和修改稿重复发送。 重解析期间旧结果仍可查看,顶部显示“正在重新解析, diff --git a/docs/features/knowledge-base/ui-design.md b/docs/features/knowledge-base/ui-design.md index 53edac8e..8b00830a 100644 --- a/docs/features/knowledge-base/ui-design.md +++ b/docs/features/knowledge-base/ui-design.md @@ -88,6 +88,8 @@ Conversation 的检索范围,返回对话并聚焦输入框。 没有分块时禁用分块入口。来源组的菜单提供任务及来源维护。菜单支持方向键、Home、End、 Escape 和焦点返回,展开不改变行高;空间不足时翻转方向或在菜单内滚动。 大小与分块数保持单行,窄窗口在表格内部横向滚动。 +操作文案使用“打开来源 / 重新解析 / 同步 / 暂停 / 重试 / 移除来源”,不附带对象名称;更多操作按钮的可访问名称和移除确认框仍保留对象名称。 +列表和预览的操作菜单宽度为 `220px`,小视口下收窄并保留左右各 `16px` 边距。 解析预览头部按返回、文档标题与库名及全局范围、维护菜单排列,地址仍可辨认。 正文、图片和详情使用共享结果组件;知识库的会话选择及添加图片工具仅在图片页显示, diff --git a/src/renderer/src/AnchoredMenu.tsx b/src/renderer/src/AnchoredMenu.tsx index c881f033..58a390f9 100644 --- a/src/renderer/src/AnchoredMenu.tsx +++ b/src/renderer/src/AnchoredMenu.tsx @@ -3,10 +3,11 @@ import { flushSync } from 'react-dom' import { FloatingPortal } from './FloatingPortal' import './anchored-menu.css' -export function AnchoredMenu({ anchorRef, id, label, onClose, children }: { +export function AnchoredMenu({ anchorRef, id, label, width: preferredWidth = 280, onClose, children }: { anchorRef: RefObject id: string label: string + width?: number onClose: () => void children: ReactNode }): React.JSX.Element { @@ -22,7 +23,7 @@ export function AnchoredMenu({ anchorRef, id, label, onClose, children }: { const width = viewport?.width ?? window.innerWidth const height = viewport?.height ?? window.innerHeight const rect = anchor.getBoundingClientRect() - const menuWidth = Math.max(0, Math.min(280, width - 32)) + const menuWidth = Math.max(0, Math.min(preferredWidth, width - 32)) menu.style.width = `${menuWidth}px` const above = Math.max(0, Math.min(height - 32, rect.top - top - 24)) const below = Math.max(0, Math.min(height - 32, top + height - rect.bottom - 24)) diff --git a/src/renderer/src/KnowledgeWorkspace.test.tsx b/src/renderer/src/KnowledgeWorkspace.test.tsx index 61c75d51..dcb13bb3 100644 --- a/src/renderer/src/KnowledgeWorkspace.test.tsx +++ b/src/renderer/src/KnowledgeWorkspace.test.tsx @@ -281,14 +281,15 @@ describe('KnowledgeWorkspace', () => { } const trigger = within(rows[1]!).getByRole('button', { name: 'Guide 的更多操作' }) fireEvent.click(trigger) - expect(screen.getByRole('menuitem', { name: '打开 Guide 的原始来源' })).toHaveFocus() + expect(screen.getByRole('menu')).toHaveStyle({ width: '220px' }) + expect(screen.getByRole('menuitem', { name: '打开来源' })).toHaveFocus() fireEvent.keyDown(document.activeElement!, { key: 'End' }) - expect(screen.getByRole('menuitem', { name: '移除来源 Guide' })).toHaveFocus() + expect(screen.getByRole('menuitem', { name: '移除来源' })).toHaveFocus() fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) expect(trigger).toHaveFocus() expect(screen.queryByRole('menu')).not.toBeInTheDocument() fireEvent.click(trigger) - fireEvent.click(screen.getByRole('menuitem', { name: '打开 Guide 的原始来源' })) + fireEvent.click(screen.getByRole('menuitem', { name: '打开来源' })) await waitFor(() => expect(props.onOpenDocumentSource).toHaveBeenCalledWith(library.id, 'doc-1')) fireEvent.change(screen.getByRole('searchbox', { name: '搜索文档与来源' }), { target: { value: 'edition=two' } }) expect(within(table).getAllByRole('row')).toHaveLength(2) @@ -354,10 +355,10 @@ describe('KnowledgeWorkspace', () => { expect(screen.getAllByText('manual.pdf')).toHaveLength(1) const openMenu = (): void => { fireEvent.click(screen.getByRole('button', { name: 'manual.pdf 的更多操作' })) } openMenu() - fireEvent.click(screen.getByRole('menuitem', { name: '同步 manual.pdf' })) + fireEvent.click(screen.getByRole('menuitem', { name: '同步' })) await waitFor(() => expect(props.onSyncSource).toHaveBeenCalledWith(source.id)) openMenu() - fireEvent.click(screen.getByRole('menuitem', { name: '使用当前设置重新解析' })) + fireEvent.click(screen.getByRole('menuitem', { name: '重新解析' })) await waitFor(() => expect(props.onRebuildDocument).toHaveBeenCalledWith(library.id, document.id)) for (const [status, name, callback] of [ ['syncing', '暂停', props.onPauseSource], @@ -366,7 +367,7 @@ describe('KnowledgeWorkspace', () => { ] as const) { view.rerender() openMenu() - fireEvent.click(screen.getByRole('menuitem', { name: `${name} manual.pdf` })) + fireEvent.click(screen.getByRole('menuitem', { name })) await waitFor(() => { expect(callback).toHaveBeenCalledWith(source.id) expect(screen.queryByRole('menu')).not.toBeInTheDocument() @@ -408,9 +409,9 @@ describe('KnowledgeWorkspace', () => { expect(within(table).getAllByRole('row')).toHaveLength(8) expect(within(table).getByText('orphan.md')).toBeVisible() expect(within(table).getByText('unassigned.md')).toBeVisible() - fireEvent.click(screen.getByRole('button', { name: '重试 failed.pdf' })) + fireEvent.click(screen.getByRole('button', { name: '重试' })) await waitFor(() => expect(props.onRetrySource).toHaveBeenCalledWith('empty')) - fireEvent.click(screen.getByRole('button', { name: '暂停 loading.pdf' })) + fireEvent.click(screen.getByRole('button', { name: '暂停' })) await waitFor(() => expect(props.onPauseSource).toHaveBeenCalledWith('syncing')) const search = screen.getByRole('searchbox', { name: '搜索文档与来源' }) fireEvent.change(search, { target: { value: '产品手册' } }) @@ -419,7 +420,7 @@ describe('KnowledgeWorkspace', () => { expect(within(table).getAllByRole('row')).toHaveLength(3) expect(within(table).getByText('产品手册')).toBeVisible() fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' })) - fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' })) + fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' })) expect(screen.getByRole('alertdialog')).toHaveAccessibleDescription( '将删除此来源的 2 篇文档、检索索引、图谱证据和应用托管副本。磁盘上的原始文件不会改变。' ) @@ -515,8 +516,8 @@ describe('KnowledgeWorkspace', () => { } fireEvent.click(screen.getByRole('button', { name: 'A.md 的更多操作' })) if (entry === 'preview') { - expect(screen.getByRole('menuitem', { name: '同步 产品手册' })).toBeEnabled() - expect(screen.getByRole('menuitem', { name: '移除来源 产品手册' })).toBeEnabled() + expect(screen.getByRole('menuitem', { name: '同步' })).toBeEnabled() + expect(screen.getByRole('menuitem', { name: '移除来源' })).toBeEnabled() } fireEvent.click(screen.getByRole('menuitem', { name: '查看任务' })) expect(screen.getByText('共 1 个任务批次')).toBeVisible() @@ -545,14 +546,14 @@ describe('KnowledgeWorkspace', () => { }) render() const aMenuName = operation === 'sync' ? 'Source A 的更多操作' : 'A.md 的更多操作' - const aActionName = operation === 'sync' ? '同步 Source A' : '使用当前设置重新解析' + const aActionName = operation === 'sync' ? '同步' : '重新解析' const checkPending = (aPending: boolean, bPending: boolean): void => { fireEvent.click(screen.getByRole('button', { name: aMenuName })) expect(screen.getByRole('menuitem', { name: aActionName })).toHaveProperty('disabled', aPending) fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) - expect(screen.getByRole('button', { name: '暂停 Source B' })).toHaveProperty('disabled', bPending) + expect(screen.getByRole('button', { name: '暂停' })).toHaveProperty('disabled', bPending) fireEvent.click(screen.getByRole('button', { name: 'Source B 的更多操作' })) - expect(screen.getByRole('menuitem', { name: '暂停 Source B' })).toHaveProperty('disabled', bPending) + expect(screen.getByRole('menuitem', { name: '暂停' })).toHaveProperty('disabled', bPending) fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) } fireEvent.click(screen.getByRole('button', { name: aMenuName })) @@ -561,10 +562,10 @@ describe('KnowledgeWorkspace', () => { else expect(props.onRebuildDocument).toHaveBeenCalledWith(library.id, 'document-1') checkPending(true, false) fireEvent.click(screen.getByRole('button', { name: 'Source B 的更多操作' })) - fireEvent.click(screen.getByRole('menuitem', { name: '暂停 Source B' })) + fireEvent.click(screen.getByRole('menuitem', { name: '暂停' })) expect(props.onPauseSource).toHaveBeenCalledExactlyOnceWith('source-b') checkPending(true, true) - fireEvent.click(screen.getByRole('button', { name: '暂停 Source B' })) + fireEvent.click(screen.getByRole('button', { name: '暂停' })) expect(props.onPauseSource).toHaveBeenCalledTimes(1) if (completion === 'a-first') { await act(async () => { resolveA(); await a }) @@ -597,15 +598,15 @@ describe('KnowledgeWorkspace', () => { expect(screen.getByText('Preview saved-result')).toBeVisible() }) fireEvent.click(screen.getByRole('button', { name: '架构说明.md 的更多操作' })) - fireEvent.click(screen.getByRole('menuitem', { name: '使用当前设置重新解析' })) + fireEvent.click(screen.getByRole('menuitem', { name: '重新解析' })) await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('Parser unavailable') expect(screen.getByText('Preview saved-result')).toBeVisible() expect(screen.queryByRole('status')).not.toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: '架构说明.md 的更多操作' })) - expect(screen.getByRole('menuitem', { name: '使用当前设置重新解析' })).toBeEnabled() - fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' })) + expect(screen.getByRole('menuitem', { name: '重新解析' })).toBeEnabled() + fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' })) expect(screen.getByRole('alertdialog')).toBeVisible() fireEvent.click(screen.getByRole('button', { name: '取消' })) expect(screen.getByText('Preview saved-result')).toBeVisible() @@ -735,7 +736,7 @@ describe('KnowledgeWorkspace', () => { ) fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' })) - fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' })) + fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' })) expect( screen.getByRole('alertdialog', { name: '移除来源“产品手册”?' @@ -1842,11 +1843,11 @@ describe('KnowledgeWorkspace', () => { .toBeInTheDocument() expect(screen.queryByTitle('D:\\Private\\产品手册')).not.toBeInTheDocument() const syncSource = screen.getByRole('button', { - name: '同步 产品手册' + name: '同步' }) fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' })) const removeSource = screen.getByRole('menuitem', { - name: '移除来源 产品手册' + name: '移除来源' }) expect(syncSource.parentElement).toHaveClass('knowledge-document-actions') expect(removeSource.closest('[role="menu"]')).toBeInTheDocument() diff --git a/src/renderer/src/i18n/locales/en-US/knowledge.ts b/src/renderer/src/i18n/locales/en-US/knowledge.ts index 8efd3e56..d77b2178 100644 --- a/src/renderer/src/i18n/locales/en-US/knowledge.ts +++ b/src/renderer/src/i18n/locales/en-US/knowledge.ts @@ -72,6 +72,7 @@ export const knowledge = { sync: 'Sync', remove: 'Remove', openSource: 'Open source', + removeSource: 'Remove source', retryDocument: 'Process again', useInChat: 'Use in this conversation', saveEntity: 'Save entity', @@ -601,7 +602,7 @@ export const knowledge = { lastSynced: 'Last synced: {{time}}', moreActionsFor: 'More actions for {{name}}', openParsedOriginal: 'Open original used for parsing', - reparse: 'Reparse with current settings', + reparse: 'Reparse', viewResult: 'View parsed result', previewLabel: 'Parsed result: {{name}}', backToList: 'Back to document list', diff --git a/src/renderer/src/i18n/locales/zh-CN/knowledge.ts b/src/renderer/src/i18n/locales/zh-CN/knowledge.ts index e847feb7..c2975222 100644 --- a/src/renderer/src/i18n/locales/zh-CN/knowledge.ts +++ b/src/renderer/src/i18n/locales/zh-CN/knowledge.ts @@ -69,6 +69,7 @@ export const knowledge = { sync: '同步', remove: '移除', openSource: '打开来源', + removeSource: '移除来源', retryDocument: '重新处理', useInChat: '用于当前对话', saveEntity: '保存实体', @@ -565,7 +566,7 @@ export const knowledge = { lastSynced: '最近同步:{{time}}', moreActionsFor: '{{name}} 的更多操作', openParsedOriginal: '打开解析时原件', - reparse: '使用当前设置重新解析', + reparse: '重新解析', viewResult: '查看解析结果', previewLabel: '解析结果:{{name}}', backToList: '返回文档列表', diff --git a/src/renderer/src/knowledge-workspace/DocumentsView.tsx b/src/renderer/src/knowledge-workspace/DocumentsView.tsx index d6a84def..abc17587 100644 --- a/src/renderer/src/knowledge-workspace/DocumentsView.tsx +++ b/src/renderer/src/knowledge-workspace/DocumentsView.tsx @@ -234,18 +234,18 @@ export function DocumentsView({ const sourceActions = (source: KnowledgeSource): KnowledgeAction[] => [ { - label: t(source.status === 'syncing' ? 'documents.actions.pauseSource' : source.status === 'failed' ? 'documents.actions.retrySource' : 'documents.actions.syncSource', { name: source.name }), + label: t(source.status === 'syncing' ? 'actions.pause' : source.status === 'failed' ? 'actions.retry' : 'actions.sync'), disabled: pending.has(source.id), onClick: () => void run(source.id, () => source.status === 'syncing' ? onPauseSource(source.id) : source.status === 'failed' ? onRetrySource(source.id) : onSyncSource(source.id)) }, { - label: t('documents.actions.removeSource', { name: source.name }), + label: t('actions.removeSource'), disabled: pending.has(source.id), danger: true, separator: true, onClick: () => setRemovingSource(source) } ] const documentActions = (document: KnowledgeDocumentItem, source: KnowledgeSource | undefined, taskSourceId?: string): KnowledgeAction[] => [ - { label: t('documents.actions.openDocumentSource', { name: document.name }), disabled: pending.has(`open:${document.id}`), + { label: t('actions.openSource'), disabled: pending.has(`open:${document.id}`), onClick: () => void run(`open:${document.id}`, () => onOpenDocumentSource(library.id, document.id)) }, ...(document.resultId ? [{ label: t('documents.openParsedOriginal'), disabled: pending.has(`original:${document.id}`), diff --git a/src/renderer/src/knowledge-workspace/KnowledgeActionsMenu.tsx b/src/renderer/src/knowledge-workspace/KnowledgeActionsMenu.tsx index 453983a9..cd6c94a8 100644 --- a/src/renderer/src/knowledge-workspace/KnowledgeActionsMenu.tsx +++ b/src/renderer/src/knowledge-workspace/KnowledgeActionsMenu.tsx @@ -21,7 +21,7 @@ export function KnowledgeActionsMenu({ label, actions }: { - {open && setOpen(false)}> + {open && setOpen(false)}> {actions.map((action) =>
{action.separator &&
}