feat: configure canvas pages sent for note analysis
Canvas analysis previously sent every page. Add a Magic Notes setting for 1-8 canvas pages, defaulting to 1 for new and historical settings. Select the first N pages in current canvas order before capture and use the same text/image range for saved entries, drafts and canvas-source todos. Keep full 50-page saving/export, expose the same setting through config tools, and use the shared settings select with an inline help icon. Include pagination, persistence, production IPC and isolated Electron regressions, including text-only input and scaled page boundaries. Validation: focused feature tests and isolated Electron regressions passed; the final compact UI passed 14 tests. Typecheck and lint were reported passing by the coordinating agent. No full-suite pass is claimed because the completed full test command exposed no summary. No live model calls were made for this commit preparation. Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
This commit is contained in:
@@ -3,6 +3,7 @@ import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { registerIpcHandlers } from '../../src/main/ipc'
|
||||
import { AssistantDatabase } from '../../src/main/assistant/assistant-database'
|
||||
import { ApplicationSettingsStore } from '../../src/main/application-settings-store'
|
||||
|
||||
const directory = process.env.GB_NOTES_PROBE_DIRECTORY!
|
||||
const url = process.env.GB_NOTES_PROBE_URL!
|
||||
@@ -14,13 +15,16 @@ app.whenReady().then(async () => {
|
||||
} })
|
||||
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
|
||||
database.initialize(directory)
|
||||
const runtimeSettings = { workspacePath: directory, supportsImageInput: true,
|
||||
defaultModelProfileId: 'vision', modelProfiles: [{ id: 'vision', supportsImageInput: true }] }
|
||||
const supportsImageInput = process.env.GB_NOTES_PROBE_VISION === 'true'
|
||||
const runtimeSettings = { workspacePath: directory, supportsImageInput,
|
||||
defaultModelProfileId: 'vision', modelProfiles: [{ id: 'vision', supportsImageInput }] }
|
||||
const applicationSettings = new ApplicationSettingsStore(join(directory, 'application.json'))
|
||||
await applicationSettings.update({ magicNoteCommentMode: 'after-save-auto', magicNoteCommentFormat: 'structured' })
|
||||
const dispose = registerIpcHandlers(win, { capability: 'text' } as never, 'CommandOrControl+Shift+Space',
|
||||
{ getResolvedSettings: async () => runtimeSettings, getPublicSettings: async () => runtimeSettings } as never,
|
||||
{} as never, { clear() {}, cancelImport() {} } as never, {} as never, database,
|
||||
{ clear() {} } as never, {} as never, async () => {}, undefined, undefined, undefined, undefined,
|
||||
{ get: async () => ({ magicNoteCommentMode: 'after-save-auto', magicNoteCommentFormat: 'structured' }), onChanged: () => () => {} } as never)
|
||||
applicationSettings)
|
||||
win.webContents.on('console-message', event => console.log(event.message))
|
||||
try {
|
||||
await win.loadURL(url)
|
||||
@@ -29,7 +33,7 @@ app.whenReady().then(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
}
|
||||
const result = await win.webContents.executeJavaScript('window.notesAnalysisRegression().catch(error => ({ error: error.stack, body: document.body.innerText }))')
|
||||
writeFileSync(join(directory, 'result.json'), JSON.stringify(result))
|
||||
writeFileSync(join(directory, 'result.json'), JSON.stringify({ ...result, requests: (globalThis as typeof globalThis & { analysisRequests: unknown[] }).analysisRequests }))
|
||||
} finally {
|
||||
await dispose()
|
||||
database.close()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import Quill from 'quill'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { useState } from 'react'
|
||||
import { ApplicationSettingsView } from '../../src/renderer/src/ApplicationCenter'
|
||||
import type { ApplicationSettings } from '../../src/shared/application-settings-contracts'
|
||||
import { MagicNotesWorkspace } from '../../src/renderer/src/MagicNotesWorkspace'
|
||||
import type { MagicNoteCanvasContent } from '../../src/shared/magic-notes-contracts'
|
||||
import i18n from '../../src/renderer/src/i18n'
|
||||
@@ -9,17 +12,28 @@ import '../../src/renderer/src/magic-canvas.css'
|
||||
async function notesAnalysisRegression() {
|
||||
await i18n.changeLanguage('en-US')
|
||||
const api = window.goodbuddy.magicNotes
|
||||
const initialSettings = await window.goodbuddy.updates.getSettings()
|
||||
if (initialSettings.magicNoteCanvasPageCount !== 1) throw new Error('Expected default page count 1')
|
||||
const inputMode = (await window.goodbuddy.settings.getRuntime()).supportsImageInput ? 'canvas-images' : 'text-fallback'
|
||||
const note = await api.create({ title: 'IPC canvas analysis' })
|
||||
const content: MagicNoteCanvasContent = { version: 2, kind: 'paged-canvas', assets: [],
|
||||
pages: [{ id: 'page-1', width: 794, height: 1123, background: { type: 'template', template: 'blank' }, objects: [] }],
|
||||
flow: { version: 1, ops: [{ insert: 'Review task' }, { insert: '\n', attributes: { list: 'unchecked' } }] } }
|
||||
pages: [1, 2, 3].map(index => ({ id: `page-${index}`, width: 794, height: 1123, background: { type: 'template', template: 'blank' }, objects: [] })),
|
||||
flow: { version: 1, ops: [{ insert: 'Review task' }, { insert: '\n', attributes: { list: 'unchecked' } },
|
||||
{ insert: { canvasPageBreak: 'page-2' } }, { insert: 'Included second\n' },
|
||||
{ insert: { canvasPageBreak: 'page-3' } }, { insert: 'Excluded third\n' }] } }
|
||||
const seeded = await api.createEntry({ noteId: note.id, content })
|
||||
const entryId = seeded.createdEntryId
|
||||
const host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
const errors: string[] = []
|
||||
const root = createRoot(host)
|
||||
root.render(<MagicNotesWorkspace onNotify={notification => { if (notification.tone === 'error') errors.push(notification.message) }} />)
|
||||
function Probe() {
|
||||
const [settings, setSettings] = useState<ApplicationSettings>(initialSettings)
|
||||
return <><ApplicationSettingsView id="magic-notes" settings={settings} pending={false} onUpdate={async updates => {
|
||||
setSettings(await window.goodbuddy.updates.updateSettings(updates)); return true
|
||||
}} /><MagicNotesWorkspace applicationSettings={settings} onNotify={notification => { if (notification.tone === 'error') errors.push(notification.message) }} /></>
|
||||
}
|
||||
root.render(<Probe />)
|
||||
const wait = async (predicate: () => unknown | Promise<unknown>) => {
|
||||
for (let i = 0; i < 400; i++) {
|
||||
if (await predicate()) return
|
||||
@@ -34,10 +48,22 @@ async function notesAnalysisRegression() {
|
||||
button(label, parent).click()
|
||||
}
|
||||
const entry = async () => (await api.get(note.id)).entries.find(item => item.id === entryId)!
|
||||
await wait(() => host.querySelector('select[aria-describedby="magic-note-canvas-page-count-help"]'))
|
||||
const pageSelect = host.querySelector<HTMLSelectElement>('select[aria-describedby="magic-note-canvas-page-count-help"]')!
|
||||
pageSelect.value = '2'
|
||||
pageSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await wait(async () => (await window.goodbuddy.updates.getSettings()).magicNoteCanvasPageCount === 2)
|
||||
await wait(() => host.querySelector('[id^="magic-note-select-"]'))
|
||||
host.querySelector<HTMLButtonElement>('[id^="magic-note-select-"]')!.click()
|
||||
const nextPage = () => host.querySelector<HTMLButtonElement>('.magic-note-entry .canvas-note-next-page')!
|
||||
await wait(() => nextPage() && !nextPage().disabled)
|
||||
nextPage().click()
|
||||
await wait(() => nextPage() && !nextPage().disabled)
|
||||
nextPage().click()
|
||||
await wait(() => nextPage()?.disabled)
|
||||
host.querySelector<HTMLButtonElement>('.magic-note-entry .canvas-note-zoom-out')!.click()
|
||||
await click('Analyze with AI')
|
||||
await wait(async () => (await entry()).comments[0]?.inputMode === 'canvas-images')
|
||||
await wait(async () => (await entry()).comments[0]?.inputMode === inputMode)
|
||||
const manualRevision = (await entry()).revision
|
||||
await click('Expand canvas editor')
|
||||
const editor = () => host.querySelector<HTMLElement>('.magic-note-entry__editor')!
|
||||
@@ -56,16 +82,29 @@ async function notesAnalysisRegression() {
|
||||
await wait(() => composer().querySelector<HTMLButtonElement>('[data-tool="flow-text"]')?.disabled === false)
|
||||
composer().querySelector<HTMLButtonElement>('[data-tool="flow-text"]')!.click()
|
||||
await wait(() => composer().querySelector('.canvas-flow-quill'))
|
||||
;(Quill.find(composer().querySelector('.canvas-flow-quill')!) as Quill).setText('Created canvas\n', 'user')
|
||||
;(Quill.find(composer().querySelector('.canvas-flow-quill')!) as Quill).setContents(content.flow!.ops, 'user')
|
||||
await click('Analyze canvas draft', composer())
|
||||
await wait(() => button('Analyze canvas draft', composer())?.disabled === false)
|
||||
await click('Save entry', composer())
|
||||
await wait(async () => (await api.get(note.id)).entries.some(item => item.id !== entryId && item.comments[0]?.inputMode === 'canvas-images'))
|
||||
await wait(async () => (await api.get(note.id)).entries.some(item => item.id !== entryId && item.comments[0]?.inputMode === inputMode))
|
||||
await click('Back to overview')
|
||||
await click('Switch to to-dos')
|
||||
await wait(() => host.querySelector<HTMLButtonElement>('.magic-todo-list-item__content'))
|
||||
host.querySelector<HTMLButtonElement>('.magic-todo-list-item__content')!.click()
|
||||
await click('Analyze with AI')
|
||||
await wait(async () => (await api.listTodos()).todos[0]?.comments[0]?.inputMode === 'canvas-images')
|
||||
await wait(async () => (await api.listTodos()).todos[0]?.comments[0]?.inputMode === inputMode)
|
||||
const todo = (await api.listTodos()).todos[0]!
|
||||
// Automatic CSS-column pagination must obey the same boundary without embeds.
|
||||
await click('Switch to notes')
|
||||
await wait(() => host.querySelector('[id^="magic-note-select-"]'))
|
||||
host.querySelector<HTMLButtonElement>('[id^="magic-note-select-"]')!.click()
|
||||
await wait(() => composer())
|
||||
await wait(() => composer().querySelector<HTMLButtonElement>('[data-tool="flow-text"]')?.disabled === false)
|
||||
composer().querySelector<HTMLButtonElement>('[data-tool="flow-text"]')!.click()
|
||||
const autoFlow = Quill.find(composer().querySelector('.canvas-flow-quill')!) as Quill
|
||||
autoFlow.setText('Automatic start\n' + 'Flow line\n'.repeat(180) + 'Automatic excluded tail\n', 'user')
|
||||
await click('Analyze canvas draft', composer())
|
||||
await wait(() => button('Analyze canvas draft', composer())?.disabled === false)
|
||||
const result = { errors, manualRevision, editedRevision, createdAnalyzed: true, todoAnalyzed: todo.comments[0]?.inputMode }
|
||||
root.unmount()
|
||||
host.remove()
|
||||
|
||||
Reference in New Issue
Block a user