diff --git a/docs/features/magic-notes/logic-design.md b/docs/features/magic-notes/logic-design.md index 675f0cca..c7e445df 100644 --- a/docs/features/magic-notes/logic-design.md +++ b/docs/features/magic-notes/logic-design.md @@ -6,7 +6,7 @@ FR-2、FR-3:单条采集读取所选助手消息的非空正文;整段采集先加载该会话完整历史,再按消息顺序收录用户和助手的非空正文,保留角色分隔。预览建立后,切换会话或收到新消息不会替换正文及来源标识。生成中的消息不可采集,停止或失败后已有正文可保存,单条预览注明回答未完成。整个会话正在生成时禁用整段采集,提示等待完成或停止生成。 -首版按文字保存,保留原始 Markdown 文本、换行和代码文本,不承诺还原聊天渲染效果。推理、工具调用日志、系统提示、附件二进制和图片不进入正文;预览持续说明保存范围。用户可编辑预览,来源仍指向本次采集的会话。读取完整会话失败时不以屏幕上已有消息代替。 +首版按文字保存,保留原始 Markdown 文本、换行和代码文本,不承诺还原聊天渲染效果。推理、工具调用日志、系统提示、附件二进制和图片不进入正文;预览来源旁提供保存范围帮助,按需展开。用户可编辑预览,来源仍指向本次采集的会话。读取完整会话失败时不以屏幕上已有消息代替。 FR-5:来源独立于可编辑正文。单条来源包含会话和消息标识;整段来源包含会话标识、此次收录的消息标识及采集时间。预览使用采集时的会话标题和项目名称;保存时主进程校验消息归属,并以当时数据库中的会话标题和项目名称写入来源,无项目时显示全局。若预览期间源会话或消息已删除,保存失败并保留预览。保存完成后,修改或删除源消息不回写笔记正文和来源。 diff --git a/docs/features/magic-notes/ui-design.md b/docs/features/magic-notes/ui-design.md index 949536cb..3f2cb122 100644 --- a/docs/features/magic-notes/ui-design.md +++ b/docs/features/magic-notes/ui-design.md @@ -24,7 +24,7 @@ ```text 加入笔记 来自:项目 / 会话标题 · 单条回答或当前对话 -保存范围:用户与助手正文,不含推理、工具日志和附件文件 +来源旁帮助按钮:按需查看保存范围 [可编辑文字预览] @@ -35,7 +35,9 @@ [取消] [加入笔记] ``` -范围说明随单条或整段来源调整。预览初始包含完整待保存文字,可滚动编辑。新建目标标题默认取会话标题前 100 个字符,可修改;已有目标与新建目标使用共享 `SegmentedControl` 切换。采集内容为空或超限、缺少目标、新建标题为空或超限时就地提示,并禁用保存;普通新建允许创建空笔记。 +范围说明随单条或整段来源调整,通过来源旁的 `InlineHelp` 按需查看;回答未完成的警告仍直接显示。预览初始包含完整待保存文字,可滚动编辑。新建目标标题默认取会话标题前 100 个字符,可修改;已有目标与新建目标使用共享 `SegmentedControl` 切换。采集内容为空或超限、缺少目标、新建标题为空或超限时禁用保存;初始不显示通用校验说明,正文和标题超限时在对应字段旁提示。普通新建允许创建空笔记。 + +底栏默认仅显示右对齐的操作按钮。保存失败时保留草稿,错误与按钮在宽面板中同行,窄面板中错误换行、按钮仍靠右。完整工作区的文字保存按钮在窄窗口中也保持内容宽度并右对齐。 预览和快速追加输入框通过无障碍名称“待加入内容”标识,不再显示重复标签。提交区仅显示校验或保存错误,不显示“全局”。提交与来源跳转按钮组均居右,可在窄面板换行;来源没有项目名时省略项目及分隔符,不补写“全局”。 diff --git a/src/renderer/src/MagicNotesPanel.test.tsx b/src/renderer/src/MagicNotesPanel.test.tsx index c38ba6d2..29d694ee 100644 --- a/src/renderer/src/MagicNotesPanel.test.tsx +++ b/src/renderer/src/MagicNotesPanel.test.tsx @@ -55,6 +55,13 @@ describe('compact notes', () => { expect(screen.queryByText('Text to add')).not.toBeInTheDocument() expect(screen.queryByText('Global')).not.toBeInTheDocument() expect(create).not.toHaveBeenCalled() + expect(document.querySelector('.magic-note-panel__footer p')).toBeNull() + expect(screen.getByRole('button', { name: 'Add to note' })).toBeDisabled() + expect(screen.queryByText(/excluding reasoning/)).not.toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'What gets saved' })) + expect(screen.getByRole('tooltip')).toHaveTextContent('excluding reasoning') + fireEvent.keyDown(window, { key: 'Escape' }) + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'New note' })) expect(screen.getByLabelText('Note title')).toHaveValue('Discussion A') expect(document.getElementById('compact-note-validation')).not.toBeInTheDocument() @@ -162,11 +169,28 @@ describe('compact notes', () => { expect(preview).toHaveValue(oversized) expect(screen.getByRole('button', { name: 'Add to note' })).toBeDisabled() expect(screen.getByText(/Each entry supports up to 500 KB/)).toBeVisible() + expect(preview).toHaveAccessibleDescription(/Each entry supports up to 500 KB/) + expect(document.querySelector('.magic-note-panel__footer p')).toBeNull() fireEvent.change(preview, { target: { value: 'Within capacity' } }) expect(screen.getByRole('button', { name: 'Add to note' })).toBeEnabled() expect(create).not.toHaveBeenCalled() }) + it('keeps new-note validation beside the title without initial footer instructions', async () => { + render() + fireEvent.click(await screen.findByRole('button', { name: 'New note' })) + const title = screen.getByRole('textbox', { name: 'Note title' }) + expect(document.querySelector('.magic-note-panel__footer p')).toBeNull() + expect(screen.getByRole('button', { name: 'Create note' })).toBeDisabled() + fireEvent.change(title, { target: { value: 'x'.repeat(101) } }) + expect(title).toHaveAccessibleDescription('Use a title of up to 100 characters.') + expect(title).toHaveAttribute('aria-invalid', 'true') + expect(document.querySelector('.magic-note-panel__footer p')).toBeNull() + fireEvent.change(title, { target: { value: 'Valid title' } }) + expect(screen.queryByText('Use a title of up to 100 characters.')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Create note' })).toBeEnabled() + }) + it('preserves text when the target disappears and requires a new selection', async () => { render() fireEvent.click(screen.getByText('Capture')) diff --git a/src/renderer/src/MagicNotesPanel.tsx b/src/renderer/src/MagicNotesPanel.tsx index 0b7d3a00..fdc15901 100644 --- a/src/renderer/src/MagicNotesPanel.tsx +++ b/src/renderer/src/MagicNotesPanel.tsx @@ -7,6 +7,7 @@ import { MagicNoteSource, type OpenMagicNoteSource } from './MagicNoteSource' import type { useMagicNoteDraft } from './use-magic-note-draft' import type { AppNotificationInput } from './notifications' import { SegmentedControl } from './WorkspacePrimitives' +import { InlineHelp } from './InlineHelp' import './magic-notes-panel.css' const MagicCanvasThumbnail = lazy(async () => ({ default: (await import('./MagicCanvasThumbnail')).MagicCanvasThumbnail })) @@ -149,8 +150,8 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c setSelectedNoteId('') requestAnimationFrame(() => document.getElementById('compact-note-search')?.focus()) } - const validationMessage = tooLong ? t('capture.tooLong') : draft && !valid ? t('capture.required') : undefined - const validationId = [validationMessage && 'compact-note-validation', saveError && 'compact-note-save-error'].filter(Boolean).join(' ') || undefined + const validationId = tooLong ? 'compact-note-validation' : undefined + const titleTooLong = Boolean(draft?.newNote && draft.title.trim().length > 100) const list = ( <> @@ -202,13 +203,18 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c {draft && !quickDraft ? ( <> {draft.source &&
- {draft.source.projectName && `${draft.source.projectName} / `}{draft.source.conversationTitle} -

{t(draft.source.kind === 'message' ? 'capture.messageScope' : 'capture.conversationScope')}

+
+ {draft.source.projectName && `${draft.source.projectName} / `}{draft.source.conversationTitle} + + {t(draft.source.kind === 'message' ? 'capture.messageScope' : 'capture.conversationScope')} + +
{draft.incomplete &&

{t('capture.incomplete')}

}
}