fix: simplify knowledge action menus
Remove repeated document names from action labels, shorten the reparse label, and narrow knowledge menus to 220px while preserving other menu widths and viewport bounds. Keep object context on menu triggers and removal confirmations. Validation: 72 focused and Electron layout tests, typecheck, lint, and diff checks passed. Release note: 知识库操作菜单去掉重复文件名,将操作简写为重新解析,并收窄菜单宽度,减少换行。
This commit is contained in:
@@ -65,7 +65,7 @@
|
||||
| 草稿图片,发送提取文字 | 查看解析结果、打开原图 | 改为发送原图、使用当前设置重新提取文字、移除 |
|
||||
| 历史消息附件 | 查看已保存结果、打开原件 | 需要重解析或改变发送方式时先添加到当前可编辑草稿;不修改原消息 |
|
||||
| 已入队附件 | 仅有附件时显示“查看附件”,查看入队时冻结的结果与发送方式;无附件时隐藏入口 | 修改整条输入使用“恢复到草稿”,按 L-H7 合并;不直接修改队列附件 |
|
||||
| 知识文档 | 查看解析结果、打开来源、管理分块 | 使用当前设置重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 |
|
||||
| 知识文档 | 查看解析结果、打开来源、管理分块 | 重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 |
|
||||
| 设置测试结果 | 查看正文、图片、详情及原件 | 重新测试、关闭;没有加入草稿、知识库或发送操作 |
|
||||
|
||||
操作依据对象、资源和任务状态显示。解析中仅保留可用旧结果的查看、进度和取消,不允许重复
|
||||
@@ -176,7 +176,7 @@ reference 使用原路径。reference 原件失效时显示“原文件不可用
|
||||
和“重试”;有部分产物时可以查看,但不能标为成功。关闭测试预览后释放无引用测试资源。
|
||||
测试预览不提供“添加到会话”,防止诊断文件进入用户草稿或知识库。
|
||||
|
||||
“使用当前设置重新解析”是聊天草稿和知识库文档的次操作,点击前显示将使用的已保存来源。
|
||||
“使用当前设置重新解析”是聊天草稿的次操作;知识库列表和预览中的同一操作简写为“重新解析”,仍使用当前已保存的设置。
|
||||
历史消息和队列冻结已接受的输入;其预览只读。历史消息使用“添加到当前草稿重新解析”,
|
||||
产生草稿附件而不改写已发送正文;队列修改使用“恢复到草稿”,避免原队列和修改稿重复发送。
|
||||
重解析期间旧结果仍可查看,顶部显示“正在重新解析,
|
||||
|
||||
@@ -88,6 +88,8 @@ Conversation 的检索范围,返回对话并聚焦输入框。
|
||||
没有分块时禁用分块入口。来源组的菜单提供任务及来源维护。菜单支持方向键、Home、End、
|
||||
Escape 和焦点返回,展开不改变行高;空间不足时翻转方向或在菜单内滚动。
|
||||
大小与分块数保持单行,窄窗口在表格内部横向滚动。
|
||||
操作文案使用“打开来源 / 重新解析 / 同步 / 暂停 / 重试 / 移除来源”,不附带对象名称;更多操作按钮的可访问名称和移除确认框仍保留对象名称。
|
||||
列表和预览的操作菜单宽度为 `220px`,小视口下收窄并保留左右各 `16px` 边距。
|
||||
|
||||
解析预览头部按返回、文档标题与库名及全局范围、维护菜单排列,地址仍可辨认。
|
||||
正文、图片和详情使用共享结果组件;知识库的会话选择及添加图片工具仅在图片页显示,
|
||||
|
||||
@@ -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<HTMLButtonElement | null>
|
||||
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))
|
||||
|
||||
@@ -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(<KnowledgeWorkspace {...props} sources={[{ ...source, name: document.name, status }]} />)
|
||||
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(<KnowledgeWorkspace {...props} />)
|
||||
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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '返回文档列表',
|
||||
|
||||
@@ -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}`),
|
||||
|
||||
@@ -21,7 +21,7 @@ export function KnowledgeActionsMenu({ label, actions }: {
|
||||
<button ref={anchorRef} type="button" className="icon-button" aria-label={label} title={label}
|
||||
aria-haspopup="menu" aria-expanded={open} aria-controls={open ? id : undefined}
|
||||
onClick={() => setOpen(!open)}><Ellipsis aria-hidden="true" size={16} /></button>
|
||||
{open && <AnchoredMenu anchorRef={anchorRef} id={id} label={label} onClose={() => setOpen(false)}>
|
||||
{open && <AnchoredMenu anchorRef={anchorRef} id={id} label={label} width={220} onClose={() => setOpen(false)}>
|
||||
{actions.map((action) => <div key={action.label}>
|
||||
{action.separator && <div role="separator" />}
|
||||
<button type="button" role="menuitem" className={`knowledge-action${action.danger ? ' knowledge-action--danger' : ''}`}
|
||||
|
||||
@@ -58,6 +58,7 @@ it('renders knowledge rows, menus, preview and task history in wide and narrow E
|
||||
await wait('!!document.querySelector("[role=menuitem]")'); await settle();
|
||||
assert.equal(await js('document.querySelector("tbody tr").getBoundingClientRect().height'), height);
|
||||
const menu = await js('document.querySelector("[role=menu]").getBoundingClientRect().toJSON()');
|
||||
assert.equal(menu.width, 220);
|
||||
assert(menu.left >= 15 && menu.right <= width - 15 && menu.top >= 15 && menu.height >= 32, JSON.stringify(menu));
|
||||
await capture('knowledge-menu-' + theme + '-' + width);
|
||||
console.log('Closing list menu');
|
||||
@@ -77,10 +78,16 @@ it('renders knowledge rows, menus, preview and task history in wide and narrow E
|
||||
console.log('Opening preview menu');
|
||||
await js('document.querySelector(".knowledge-document-preview__header [aria-haspopup=menu]").click()'); await settle();
|
||||
const previewMenu = await js('document.querySelector("[role=menu]").getBoundingClientRect().toJSON()');
|
||||
assert.equal(previewMenu.width, 220);
|
||||
assert(previewMenu.height >= 100 && previewMenu.top >= 16 && previewMenu.bottom <= (width === 420 ? 480 : 720) - 15, JSON.stringify(previewMenu));
|
||||
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'End' });
|
||||
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'End' }); await settle();
|
||||
assert(await js('document.activeElement.textContent.includes("移除来源")'));
|
||||
assert(await js('document.activeElement.textContent === "移除来源"'));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Up' });
|
||||
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Up' }); await settle();
|
||||
}
|
||||
assert(await js('document.activeElement.textContent === "重新解析"'));
|
||||
await capture('knowledge-preview-menu-' + theme + '-' + width);
|
||||
console.log('Closing preview menu');
|
||||
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
|
||||
|
||||
Reference in New Issue
Block a user