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:
@@ -1094,6 +1094,7 @@ function installRemoteProjectsSetting(enabled: boolean): {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate",
|
||||
magicNoteCommentFormat: "combined",
|
||||
magicNoteCanvasPageCount: 1,
|
||||
};
|
||||
const updateSettings = vi.fn<
|
||||
NonNullable<DesktopApi["updates"]>["updateSettings"]
|
||||
@@ -3092,6 +3093,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(),
|
||||
check: vi.fn(),
|
||||
@@ -3193,6 +3195,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
@@ -3206,6 +3209,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
check,
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -3305,6 +3309,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
@@ -3318,6 +3323,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
check,
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -5529,6 +5535,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate",
|
||||
magicNoteCommentFormat: "combined",
|
||||
magicNoteCanvasPageCount: 1,
|
||||
};
|
||||
let shortcutSnapshot: GlobalShortcutSettingsSnapshot = {
|
||||
settings: {
|
||||
@@ -13228,6 +13235,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
@@ -13241,6 +13249,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -13291,6 +13300,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(),
|
||||
check: vi.fn(),
|
||||
@@ -13348,6 +13358,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(),
|
||||
check: vi.fn(),
|
||||
@@ -13382,6 +13393,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
@@ -13395,6 +13407,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate" as const,
|
||||
magicNoteCommentFormat: "combined" as const,
|
||||
magicNoteCanvasPageCount: 1,
|
||||
})),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -13454,6 +13467,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate",
|
||||
magicNoteCommentFormat: "combined",
|
||||
magicNoteCanvasPageCount: 1,
|
||||
};
|
||||
const updateSettings = vi.fn<
|
||||
NonNullable<DesktopApi["updates"]>["updateSettings"]
|
||||
@@ -13510,6 +13524,7 @@ describe("App", () => {
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: "immediate",
|
||||
magicNoteCommentFormat: "combined",
|
||||
magicNoteCanvasPageCount: 1,
|
||||
};
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({ ...applicationSettings })),
|
||||
|
||||
@@ -279,6 +279,18 @@ describe('Application Center', () => {
|
||||
)
|
||||
fireEvent.click(screen.getByRole('switch', { name: '启用应用' }))
|
||||
expect(onUpdate).toHaveBeenLastCalledWith({ magicNotesEnabled: false })
|
||||
const pageCount = screen.getByRole('combobox', { name: '发送画布页数' })
|
||||
expect(pageCount).toHaveValue('1')
|
||||
expect(within(pageCount).getAllByRole('option')).toHaveLength(8)
|
||||
expect(pageCount).toHaveAccessibleDescription(expect.stringContaining('不是笔记记录数量'))
|
||||
expect(pageCount.parentElement).toHaveClass('field')
|
||||
const help = screen.getByRole('button', { name: '发送画布页数' })
|
||||
expect(help).toHaveClass('icon-button')
|
||||
expect(help).toHaveAccessibleDescription(help.getAttribute('title')!)
|
||||
expect(help).toHaveAttribute('title', expect.stringContaining('不是笔记记录数量'))
|
||||
expect(document.getElementById('magic-note-canvas-page-count-help')).toHaveClass('sr-only')
|
||||
fireEvent.change(pageCount, { target: { value: '8' } })
|
||||
expect(onUpdate).toHaveBeenLastCalledWith({ magicNoteCanvasPageCount: 8 })
|
||||
fireEvent.click(screen.getByRole('switch', { name: '显示未完成待办数量' }))
|
||||
expect(onUpdate).toHaveBeenLastCalledWith({
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
CircleHelp,
|
||||
Cpu,
|
||||
HeartPulse,
|
||||
Library,
|
||||
@@ -157,6 +158,27 @@ export function ApplicationSettingsView({
|
||||
</div>
|
||||
{id === 'magic-notes' && (
|
||||
<div className="application-settings__group">
|
||||
<div className="platform-feature-option application-settings__compact-option">
|
||||
<div className="application-settings__label-with-help">
|
||||
<label htmlFor="magic-note-canvas-page-count">{t('platformFeatures.magicNotes.canvasPageCount', { ns: 'settingsSections' })}</label>
|
||||
<button type="button" className="icon-button"
|
||||
aria-label={t('platformFeatures.magicNotes.canvasPageCount', { ns: 'settingsSections' })}
|
||||
aria-describedby="magic-note-canvas-page-count-help"
|
||||
title={t('platformFeatures.magicNotes.canvasPageCountHelp', { ns: 'settingsSections' })}>
|
||||
<CircleHelp size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<select id="magic-note-canvas-page-count" disabled={pending} value={settings.magicNoteCanvasPageCount ?? 1}
|
||||
aria-describedby="magic-note-canvas-page-count-help"
|
||||
onChange={event => void onUpdate({ magicNoteCanvasPageCount: Number(event.target.value) })}>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map(count => <option key={count} value={count}>{count}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<span id="magic-note-canvas-page-count-help" className="sr-only">
|
||||
{t('platformFeatures.magicNotes.canvasPageCountHelp', { ns: 'settingsSections' })}
|
||||
</span>
|
||||
<div className="platform-feature-option">
|
||||
<span>
|
||||
{t('platformFeatures.magicNotes.commentMode', {
|
||||
|
||||
@@ -14,9 +14,9 @@ export type MagicCanvasContentHandle = Pick<MagicCanvasEditorHandle, 'capturePag
|
||||
export const MagicCanvasContent = forwardRef<MagicCanvasContentHandle, MagicCanvasContentProps>(function MagicCanvasContent({ content, onError, onEdit }, ref) {
|
||||
const { t } = useTranslation('magicNotes')
|
||||
const editorRef = useRef<MagicCanvasEditorHandle>(null)
|
||||
useImperativeHandle(ref, () => ({ async capturePages() {
|
||||
useImperativeHandle(ref, () => ({ async capturePages(pageLimit, includeImages) {
|
||||
if (!editorRef.current) throw new Error(t('canvas.notReady'))
|
||||
return editorRef.current.capturePages()
|
||||
return editorRef.current.capturePages(pageLimit, includeImages)
|
||||
} }), [t])
|
||||
const [snapshot, setSnapshot] = useState({ content, revision: 0 })
|
||||
if (snapshot.content !== content) setSnapshot({ content, revision: snapshot.revision + 1 })
|
||||
|
||||
@@ -11,7 +11,7 @@ export type { MagicNoteCanvasContent } from './magic-canvas/model'
|
||||
export type MagicCanvasEditorHandle = {
|
||||
flush(): Promise<MagicNoteCanvasContent>
|
||||
focus(): void
|
||||
capturePages(): Promise<{ pageId: string; dataUrl: string }[]>
|
||||
capturePages(pageLimit?: number, includeImages?: boolean): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
|
||||
}
|
||||
|
||||
export type MagicCanvasEditorProps = {
|
||||
@@ -41,9 +41,9 @@ export const MagicCanvasEditor = forwardRef<MagicCanvasEditorHandle, MagicCanvas
|
||||
return serialize.current(await controller.current.flush())
|
||||
},
|
||||
focus() { controller.current?.focus() },
|
||||
async capturePages() {
|
||||
async capturePages(pageLimit, includeImages) {
|
||||
if (!controller.current) throw new Error('画布尚未加载')
|
||||
return controller.current.capturePages()
|
||||
return controller.current.capturePages({ pageLimit, includeImages })
|
||||
}
|
||||
}), [])
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ const settings: ApplicationSettings = {
|
||||
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
|
||||
localInferenceEnabled: true, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
|
||||
magicNotesEnabled: true, magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate', magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentMode: 'immediate', magicNoteCommentFormat: 'combined', magicNoteCanvasPageCount: 1
|
||||
}
|
||||
const list = vi.fn<DesktopApi['magicNotes']['list']>()
|
||||
const get = vi.fn<DesktopApi['magicNotes']['get']>()
|
||||
@@ -1256,8 +1256,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
|
||||
// The selected profile wins over a stale top-level capability flag.
|
||||
getRuntime.mockResolvedValue({ supportsImageInput: true, defaultModelProfileId: 'text', modelProfiles: [{ id: 'vision', supportsImageInput: true }, { id: 'text', supportsImageInput: false }] })
|
||||
get.mockResolvedValue({ ...detail, entries: [{ ...detail.entries[0]!, content: canvasContent }] })
|
||||
canvas.capture.mockRejectedValue(new Error('capture must not run'))
|
||||
canvas.viewCapture.mockRejectedValue(new Error('viewer capture must not run'))
|
||||
canvas.capture.mockResolvedValue([{ pageId: 'page-1', text: 'First page', dataUrl: '' }])
|
||||
canvas.viewCapture.mockResolvedValue([{ pageId: 'page-1', text: 'First page', dataUrl: '' }])
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
if (action === 'todo') {
|
||||
await openTodo()
|
||||
@@ -1281,8 +1281,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
|
||||
await waitFor(() => expect(analyzeCall).toHaveBeenCalledOnce())
|
||||
expect(analyzeCall.mock.calls[0]![1]).not.toHaveProperty('canvasImages')
|
||||
expect(getRuntime).toHaveBeenCalledOnce()
|
||||
expect(canvas.capture).not.toHaveBeenCalled()
|
||||
expect(canvas.viewCapture).not.toHaveBeenCalled()
|
||||
expect(action === 'entry' || action === 'todo' ? canvas.viewCapture : canvas.capture).toHaveBeenCalledWith(1, false)
|
||||
expect(analyzeCall.mock.calls[0]![1]).toHaveProperty('canvasPageText', [{ pageId: 'page-1', text: 'First page' }])
|
||||
})
|
||||
|
||||
it('reads fresh capability for every analysis rather than caching the previous model', async () => {
|
||||
@@ -1298,7 +1298,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
|
||||
await waitFor(() => expect(analyzeDraft).toHaveBeenCalledTimes(2))
|
||||
expect(analyzeDraft.mock.calls[0]![1]).toHaveProperty('canvasImages', canvasImages)
|
||||
expect(analyzeDraft.mock.calls[1]![1]).not.toHaveProperty('canvasImages')
|
||||
expect(canvas.capture).toHaveBeenCalledOnce()
|
||||
expect(canvas.capture).toHaveBeenNthCalledWith(1, 1, true)
|
||||
expect(canvas.capture).toHaveBeenNthCalledWith(2, 1, false)
|
||||
expect(getRuntime).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -1493,6 +1494,7 @@ describe('MagicNotesWorkspace canvas integration', () => {
|
||||
await waitFor(() => expect(call).toHaveBeenCalledOnce())
|
||||
expect(call.mock.calls[0]![1]).toEqual({
|
||||
requestId: expect.any(String), direction: 'general', format: 'combined', canvasImages,
|
||||
canvasPageText: [{ pageId: 'page-1', text: '' }],
|
||||
...(action === 'todo' ? { sourceEntryRevision: 7 } : action === 'draft' ? {} : { expectedRevision: action === 'entry' ? 7 : 11 })
|
||||
})
|
||||
if (action !== 'todo' && action !== 'draft') expect(analyze.mock.calls[0]![0]).toBe(savedId)
|
||||
|
||||
@@ -534,6 +534,7 @@ export function MagicNotesWorkspace({
|
||||
const createAnalysisOptions = useCallback(
|
||||
async (canvas?: MagicCanvasContentHandle | null): Promise<MagicNoteAnalysisOptions> => {
|
||||
let format = commentFormat
|
||||
let pageCount = applicationSettings?.magicNoteCanvasPageCount ?? 1
|
||||
// Read capability for this request, never reuse a previous profile's flag.
|
||||
const runtimePromise = canvas !== undefined
|
||||
? window.goodbuddy.settings.getRuntime()
|
||||
@@ -544,6 +545,7 @@ export function MagicNotesWorkspace({
|
||||
// Keep the last loaded format if application settings cannot be refreshed.
|
||||
if (settingsResult.status === 'fulfilled' && settingsResult.value) {
|
||||
format = settingsResult.value.magicNoteCommentFormat
|
||||
pageCount = settingsResult.value.magicNoteCanvasPageCount ?? 1
|
||||
setCommentFormat(format)
|
||||
}
|
||||
const options: MagicNoteAnalysisOptions = {
|
||||
@@ -556,14 +558,14 @@ export function MagicNotesWorkspace({
|
||||
if (runtime) {
|
||||
const profile = runtime.modelProfiles.find((candidate) => candidate.id === runtime.defaultModelProfileId)
|
||||
const supportsImages = (profile ? profile.supportsImageInput : runtime.supportsImageInput) === true
|
||||
if (supportsImages) {
|
||||
if (!canvas) throw new Error(tRef.current('canvas.notReady'))
|
||||
options.canvasImages = await canvas.capturePages()
|
||||
}
|
||||
if (!canvas) throw new Error(tRef.current('canvas.notReady'))
|
||||
const pages = await canvas.capturePages(pageCount, supportsImages)
|
||||
options.canvasPageText = pages.map(({ pageId, text }) => ({ pageId, text: text ?? '' }))
|
||||
if (supportsImages) options.canvasImages = pages.map(({ pageId, dataUrl }) => ({ pageId, dataUrl }))
|
||||
}
|
||||
return options
|
||||
},
|
||||
[commentDirection, commentFormat]
|
||||
[commentDirection, commentFormat, applicationSettings?.magicNoteCanvasPageCount]
|
||||
)
|
||||
|
||||
const getLayoutBounds = useCallback((): {
|
||||
|
||||
@@ -554,7 +554,8 @@ applicationNavigation: defaultApplicationNavigation, localInferenceEnabled: true
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
magicNoteCanvasPageCount: 1
|
||||
}
|
||||
const agentPackageInventory: AgentPackageInventory = {
|
||||
checkedAt: '2026-08-24T00:00:00.000Z',
|
||||
@@ -846,7 +847,8 @@ describe('SettingsPanel runtime files', () => {
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
magicNoteCanvasPageCount: 1
|
||||
}
|
||||
getAgentPackageInventory.mockReset()
|
||||
getAgentPackageInventory.mockResolvedValue(
|
||||
|
||||
@@ -32,7 +32,8 @@ describe('UpdateSettingsSection', () => {
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
magicNoteCommentFormat: 'combined' as const,
|
||||
magicNoteCanvasPageCount: 1
|
||||
}
|
||||
const updateSettings = vi.fn<
|
||||
NonNullable<DesktopApi['updates']>['updateSettings']
|
||||
|
||||
@@ -148,6 +148,20 @@
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
.application-settings .application-settings__compact-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.application-settings__label-with-help {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.application-settings__label-with-help label {
|
||||
font-size: var(--font-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.application-center .settings-panel__header {
|
||||
padding: var(--space-4);
|
||||
|
||||
@@ -694,6 +694,8 @@ export const settingsSections = {
|
||||
commentModeHelp:
|
||||
'Immediate mode comments on an unsaved draft 5 seconds after you press Enter and stop typing. Automatic mode comments after saving. Manual mode comments only after you select Analyze with AI.',
|
||||
commentFormat: 'AI comment format',
|
||||
canvasPageCount: 'Canvas pages to send',
|
||||
canvasPageCountHelp: 'Send the first N pages in the current canvas order for each AI analysis. Default: 1; range: 1–8. If fewer pages exist, send them all. This counts pages in one canvas, not note entries. Text and images use the same range, including text-only models. Applies to entries, drafts and to-do source canvases. Saving and exporting the full canvas are unaffected.',
|
||||
commentFormatAria: 'Magic Notes AI comment format',
|
||||
formats: {
|
||||
combined: 'Long-form + points',
|
||||
|
||||
@@ -648,6 +648,8 @@ export const settingsSections = {
|
||||
commentModeHelp:
|
||||
'即时模式会在按回车并停止输入 5 秒后评论未保存草稿;自动模式在保存后评论;手动模式仅在点击 AI 分析后评论。',
|
||||
commentFormat: 'AI 评论形式',
|
||||
canvasPageCount: '发送画布页数',
|
||||
canvasPageCountHelp: '每次 AI 分析按当前画布页面顺序发送前 N 页,默认 1 页,可选 1~8 页;不足 N 页时发送全部页。这是单个画布的页数,不是笔记记录数量。文字和图片使用相同范围,不支持图像的模型也只接收这些页的文字。适用于记录、草稿和待办来源画布,不影响保存或导出全部画布。',
|
||||
commentFormatAria: '魔法笔记 AI 评论形式',
|
||||
formats: {
|
||||
combined: '长评 + 要点',
|
||||
|
||||
@@ -21,7 +21,7 @@ type MagicCanvasEditorProps = {
|
||||
type MagicCanvasEditorHandle = {
|
||||
flush(): Promise<MagicNoteCanvasContent>
|
||||
focus(): void
|
||||
capturePages(): Promise<{ pageId: string; dataUrl: string }[]>
|
||||
capturePages(pageLimit?: number, includeImages?: boolean): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,7 +48,11 @@ The core controller accepts `capturePages({ firstPageOnly: true, thumbnailWidth:
|
||||
to composite only page zero, including the background, body and annotations.
|
||||
Visible thumbnails are queued and cached by content identity; they do not leave
|
||||
full editors mounted in the record list. The React editor's AI capture method
|
||||
continues to capture every page.
|
||||
passes the configured page limit (1-8, default 1) before compositing. The core
|
||||
equivalent is `capturePages({ pageLimit, includeImages })`; it returns only the
|
||||
current first N pages and their measured Quill flow text. `includeImages: false`
|
||||
skips PNG compositing and returns empty data URLs for text-only analysis. Omitting
|
||||
`pageLimit` retains the all-page API for existing export and core callers.
|
||||
|
||||
The same module exports `createEmptyCanvasContent()` and
|
||||
`canvasHasContent(content?)`.
|
||||
|
||||
@@ -431,6 +431,25 @@ export function mountFlowText(layerHost, toolbarHost, initialContent, options =
|
||||
await scheduleLayout();
|
||||
return cloneJson(content());
|
||||
},
|
||||
pageText(pageIndex) {
|
||||
// CSS columns determine automatic page breaks; Delta alone has no page geometry.
|
||||
const scale = surface.getBoundingClientRect().width / layout.width || 1;
|
||||
const span = layout.width + layout.gap;
|
||||
const boundary = (target) => {
|
||||
let low = 0, high = quill.getLength();
|
||||
while (low < high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
const bounds = quill.getBounds(middle, 1);
|
||||
// Half a logical pixel absorbs scaled DOMRect rounding at column starts.
|
||||
const column = layout.pageIndex + Math.floor(((Number(bounds?.left) || 0) / scale + 0.5) / span);
|
||||
if (column < target) low = middle + 1;
|
||||
else high = middle;
|
||||
}
|
||||
return low;
|
||||
};
|
||||
const start = boundary(pageIndex);
|
||||
return quill.getText(start, boundary(pageIndex + 1) - start);
|
||||
},
|
||||
async renderPage(pageIndex, pageWidth, pageHeight) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = pageWidth;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CoreContent } from './model'
|
||||
export interface CanvasController {
|
||||
content(): CoreContent
|
||||
flush(): Promise<CoreContent>
|
||||
capturePages(options?: { firstPageOnly?: boolean; thumbnailWidth?: number }): Promise<{ pageId: string; dataUrl: string }[]>
|
||||
capturePages(options?: { firstPageOnly?: boolean; thumbnailWidth?: number; pageLimit?: number; includeImages?: boolean }): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
|
||||
focus(): void
|
||||
setDisabled(value: boolean): void
|
||||
destroy(): Promise<void>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'fabric';
|
||||
import { jsPDF as JsPdf } from 'jspdf';
|
||||
import { mountFlowText, normalizeFlowContent } from './canvas-flow.mjs';
|
||||
import { selectMagicNotePages } from '../../../shared/magic-note-pages.mjs';
|
||||
|
||||
const VERSION = 2;
|
||||
const MAX_PAGES = 50;
|
||||
@@ -1608,7 +1609,7 @@ export function mountCanvasNote(host, initialContent, options = {}) {
|
||||
fabricCanvas.discardActiveObject();
|
||||
applyMode();
|
||||
},
|
||||
async capturePages({ firstPageOnly = false, thumbnailWidth } = {}) {
|
||||
async capturePages({ firstPageOnly = false, thumbnailWidth, pageLimit, includeImages = true } = {}) {
|
||||
await operationQueue;
|
||||
flushPendingText();
|
||||
captureCurrentObjects();
|
||||
@@ -1619,9 +1620,15 @@ export function mountCanvasNote(host, initialContent, options = {}) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = [];
|
||||
for (let index = 0; index < (firstPageOnly ? Math.min(1, pages.length) : pages.length); index += 1) {
|
||||
const selected = pageLimit === undefined ? pages : selectMagicNotePages(pages, pageLimit);
|
||||
for (let index = 0; index < (firstPageOnly ? Math.min(1, selected.length) : selected.length); index += 1) {
|
||||
if (destroyed || options.signal?.aborted) throw new DOMException('Destroyed', 'AbortError');
|
||||
const page = pages[index];
|
||||
const text = pageLimit === undefined ? undefined : flowEditor.pageText(index);
|
||||
if (!includeImages) {
|
||||
result.push({ pageId: page.id, dataUrl: '', text });
|
||||
continue;
|
||||
}
|
||||
const composite = document.createElement('canvas');
|
||||
await paintPageBackground(composite, page);
|
||||
const context = composite.getContext('2d');
|
||||
@@ -1641,7 +1648,7 @@ export function mountCanvasNote(host, initialContent, options = {}) {
|
||||
output.height = Math.round(page.height * thumbnailWidth / page.width);
|
||||
output.getContext('2d').drawImage(composite, 0, 0, output.width, output.height);
|
||||
}
|
||||
result.push({ pageId: page.id, dataUrl: output.toDataURL('image/png') });
|
||||
result.push({ pageId: page.id, dataUrl: output.toDataURL('image/png'), ...(text !== undefined ? { text } : {}) });
|
||||
} finally {
|
||||
await objectCanvas.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user