feat: enhance magic notes AI comments
This commit is contained in:
@@ -481,7 +481,8 @@ const api: DesktopApi = {
|
||||
listTodos: vi.fn(async () => ({ todos: [] })),
|
||||
analyzeTodo: vi.fn(async () => {
|
||||
throw new Error('not used')
|
||||
})
|
||||
}),
|
||||
onAnalysisEvent: vi.fn(() => vi.fn())
|
||||
},
|
||||
knowledge: {
|
||||
getSnapshot: vi.fn(async () => ({
|
||||
@@ -643,12 +644,14 @@ describe('App', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
check,
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -690,12 +693,14 @@ describe('App', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
check,
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -4031,12 +4036,14 @@ describe('App', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -4072,12 +4079,14 @@ describe('App', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate' as const
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
@@ -4107,7 +4116,8 @@ describe('App', () => {
|
||||
let applicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({ ...applicationSettings })),
|
||||
|
||||
@@ -154,18 +154,30 @@ const createEntry = vi.fn<DesktopApi['magicNotes']['createEntry']>()
|
||||
const analyze = vi.fn<DesktopApi['magicNotes']['analyze']>()
|
||||
const analyzeTodo = vi.fn<DesktopApi['magicNotes']['analyzeTodo']>()
|
||||
const analyzeDraft = vi.fn<DesktopApi['magicNotes']['analyzeDraft']>()
|
||||
let analysisEventListener:
|
||||
| Parameters<DesktopApi['magicNotes']['onAnalysisEvent']>[0]
|
||||
| undefined
|
||||
const onAnalysisEvent = vi.fn<
|
||||
DesktopApi['magicNotes']['onAnalysisEvent']
|
||||
>((listener) => {
|
||||
analysisEventListener = listener
|
||||
return vi.fn()
|
||||
})
|
||||
const getApplicationSettings = vi.fn<() => Promise<ApplicationSettings>>(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}))
|
||||
const onNotify = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
analysisEventListener = undefined
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
list.mockResolvedValue({ notes: [detail] })
|
||||
get.mockResolvedValue(detail)
|
||||
@@ -247,7 +259,8 @@ beforeEach(() => {
|
||||
createEntry,
|
||||
analyze,
|
||||
analyzeTodo,
|
||||
analyzeDraft
|
||||
analyzeDraft,
|
||||
onAnalysisEvent
|
||||
},
|
||||
updates: {
|
||||
getSettings: getApplicationSettings
|
||||
@@ -399,6 +412,12 @@ describe('MagicNotesWorkspace', () => {
|
||||
)
|
||||
|
||||
const pane = await screen.findByLabelText('AI 评论')
|
||||
expect(
|
||||
screen.queryByRole('group', { name: 'AI 评论形式' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'AI 评论方向' })
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '关闭 AI 评论面板' })
|
||||
)
|
||||
@@ -410,6 +429,69 @@ describe('MagicNotesWorkspace', () => {
|
||||
expect(pane).toBeVisible()
|
||||
})
|
||||
|
||||
it('resizes the AI comments pane with pointer and keyboard controls', async () => {
|
||||
const originalInnerWidth = window.innerWidth
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 1200
|
||||
})
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
|
||||
await screen.findByText('记录正文')
|
||||
const separator = screen.getByRole('separator', {
|
||||
name: '调整编辑区与 AI 评论宽度'
|
||||
})
|
||||
const layout = separator.closest('.magic-notes-layout') as HTMLElement
|
||||
const setPointerCapture = vi.fn()
|
||||
const releasePointerCapture = vi.fn()
|
||||
Object.defineProperties(separator, {
|
||||
setPointerCapture: { value: setPointerCapture },
|
||||
hasPointerCapture: { value: () => true },
|
||||
releasePointerCapture: { value: releasePointerCapture }
|
||||
})
|
||||
|
||||
fireEvent.pointerDown(separator, {
|
||||
button: 0,
|
||||
clientX: 800,
|
||||
pointerId: 12
|
||||
})
|
||||
fireEvent.pointerMove(separator, {
|
||||
clientX: 600,
|
||||
pointerId: 12
|
||||
})
|
||||
|
||||
expect(setPointerCapture).toHaveBeenCalledWith(12)
|
||||
expect(layout).toHaveClass('magic-notes-layout--resizing')
|
||||
expect(
|
||||
layout.style.getPropertyValue('--magic-notes-ai-width')
|
||||
).toBe('520px')
|
||||
|
||||
fireEvent.pointerUp(separator, { pointerId: 12 })
|
||||
expect(releasePointerCapture).toHaveBeenCalledWith(12)
|
||||
expect(layout).not.toHaveClass('magic-notes-layout--resizing')
|
||||
|
||||
fireEvent.keyDown(separator, { key: 'Home' })
|
||||
expect(
|
||||
layout.style.getPropertyValue('--magic-notes-ai-width')
|
||||
).toBe('240px')
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '240')
|
||||
|
||||
fireEvent.keyDown(separator, { key: 'ArrowLeft' })
|
||||
expect(
|
||||
layout.style.getPropertyValue('--magic-notes-ai-width')
|
||||
).toBe('256px')
|
||||
|
||||
fireEvent.keyDown(separator, { key: 'End' })
|
||||
expect(
|
||||
layout.style.getPropertyValue('--magic-notes-ai-width')
|
||||
).toBe('520px')
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: originalInnerWidth
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the selected note aligned with the latest detail request', async () => {
|
||||
const second = alternateDetail(secondNoteId, '第二篇笔记')
|
||||
const third = alternateDetail(thirdNoteId, '第三篇笔记')
|
||||
@@ -509,7 +591,8 @@ describe('MagicNotesWorkspace', () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual'
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
render(
|
||||
<MagicNotesWorkspace onNotify={onNotify} />
|
||||
@@ -519,12 +602,103 @@ describe('MagicNotesWorkspace', () => {
|
||||
fireEvent.click(screen.getByRole('tab', { name: '待办' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'AI 分析' }))
|
||||
|
||||
await waitFor(() => expect(analyzeTodo).toHaveBeenCalledWith(noteTodo.id))
|
||||
await waitFor(() =>
|
||||
expect(analyzeTodo).toHaveBeenCalledWith(
|
||||
noteTodo.id,
|
||||
expect.objectContaining({
|
||||
requestId: expect.any(String),
|
||||
direction: 'general',
|
||||
format: 'combined'
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(
|
||||
await screen.findByText('先补充明确的验收条件。')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('streams with snapshotted sidebar options while later changes stay local', async () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative'
|
||||
})
|
||||
let finishAnalysis: (() => void) | undefined
|
||||
analyzeTodo.mockImplementationOnce(
|
||||
(_todoId, options) =>
|
||||
new Promise<MagicTodoItem>((resolve) => {
|
||||
analysisEventListener?.({
|
||||
requestId: options.requestId,
|
||||
type: 'text',
|
||||
delta: '正在扩展这一段内容。',
|
||||
direction: 'expand',
|
||||
format: 'narrative'
|
||||
})
|
||||
finishAnalysis = () =>
|
||||
resolve({
|
||||
...noteTodo,
|
||||
comments: [
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000620',
|
||||
kind: 'narrative',
|
||||
content: '扩展后的完整评论。',
|
||||
direction: 'expand',
|
||||
format: 'narrative'
|
||||
}
|
||||
],
|
||||
analyzedAt: '2026-08-01T00:07:00.000Z',
|
||||
revision: 2
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
|
||||
await screen.findByText('记录正文')
|
||||
fireEvent.change(
|
||||
screen.getByRole('combobox', { name: 'AI 评论方向' }),
|
||||
{ target: { value: 'expand' } }
|
||||
)
|
||||
fireEvent.click(screen.getByRole('tab', { name: '待办' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'AI 分析' }))
|
||||
|
||||
expect(
|
||||
await screen.findByText('正在扩展这一段内容。')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText(/正在生成 ·/)).toHaveTextContent(
|
||||
'正在生成 · 扩展写作'
|
||||
)
|
||||
fireEvent.change(
|
||||
screen.getByRole('combobox', { name: 'AI 评论方向' }),
|
||||
{ target: { value: 'polish' } }
|
||||
)
|
||||
expect(screen.getByText(/正在生成 ·/)).toHaveTextContent(
|
||||
'正在生成 · 扩展写作'
|
||||
)
|
||||
|
||||
await act(async () => finishAnalysis?.())
|
||||
|
||||
expect(await screen.findByText('扩展后的完整评论。')).toBeInTheDocument()
|
||||
expect(
|
||||
screen
|
||||
.getAllByText('扩展写作')
|
||||
.some((element) =>
|
||||
element.classList.contains('magic-note-comment__direction')
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'AI 评论方向' })
|
||||
).toHaveValue('polish')
|
||||
expect(analyzeTodo).toHaveBeenCalledWith(
|
||||
noteTodo.id,
|
||||
expect.objectContaining({
|
||||
direction: 'expand',
|
||||
format: 'narrative'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('clears note searches and todo status filters with no results', async () => {
|
||||
listTodos.mockResolvedValue({
|
||||
todos: [
|
||||
@@ -560,7 +734,8 @@ describe('MagicNotesWorkspace', () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-auto'
|
||||
magicNoteCommentMode: 'after-save-auto',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
|
||||
@@ -578,7 +753,14 @@ describe('MagicNotesWorkspace', () => {
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(analyze).toHaveBeenCalledWith(createdEntryId)
|
||||
expect(analyze).toHaveBeenCalledWith(
|
||||
createdEntryId,
|
||||
expect.objectContaining({
|
||||
requestId: expect.any(String),
|
||||
direction: 'general',
|
||||
format: 'combined'
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(
|
||||
await screen.findByText('保存后的自动评论。')
|
||||
@@ -600,10 +782,17 @@ describe('MagicNotesWorkspace', () => {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
})
|
||||
expect(analyzeDraft).toHaveBeenCalledWith({
|
||||
version: 1,
|
||||
ops: [{ insert: '新的句子\n' }]
|
||||
})
|
||||
expect(analyzeDraft).toHaveBeenCalledWith(
|
||||
{
|
||||
version: 1,
|
||||
ops: [{ insert: '新的句子\n' }]
|
||||
},
|
||||
expect.objectContaining({
|
||||
requestId: expect.any(String),
|
||||
direction: 'general',
|
||||
format: 'combined'
|
||||
})
|
||||
)
|
||||
expect(screen.getByText('这是最新的草稿评论。')).toBeInTheDocument()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
@@ -24,6 +24,9 @@ import {
|
||||
useState
|
||||
} from 'react'
|
||||
import type {
|
||||
MagicNoteAnalysisOptions,
|
||||
MagicNoteCommentDirection,
|
||||
MagicNoteCommentFormat,
|
||||
MagicNoteDraftAnalysis,
|
||||
MagicNoteComment,
|
||||
MagicNoteDetail,
|
||||
@@ -35,6 +38,7 @@ import type {
|
||||
import type { MagicNoteCommentMode } from '../../shared/application-settings-contracts'
|
||||
import { MagicNoteContent } from './MagicNoteContent'
|
||||
import { MagicNoteEditor } from './MagicNoteEditor'
|
||||
import { MarkdownRenderer } from './MarkdownRenderer'
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import {
|
||||
EmptyState,
|
||||
@@ -74,6 +78,56 @@ const todoListModes = [
|
||||
{ value: 'directory', label: '目录视图' }
|
||||
] as const
|
||||
|
||||
const commentDirections: ReadonlyArray<{
|
||||
value: MagicNoteCommentDirection
|
||||
label: string
|
||||
}> = [
|
||||
{ value: 'general', label: '综合点评' },
|
||||
{ value: 'expand', label: '扩展写作' },
|
||||
{ value: 'polish', label: '润色改写' },
|
||||
{ value: 'challenge', label: '质疑审校' },
|
||||
{ value: 'brainstorm', label: '灵感发散' }
|
||||
]
|
||||
|
||||
const commentDirectionLabels = Object.fromEntries(
|
||||
commentDirections.map((direction) => [
|
||||
direction.value,
|
||||
direction.label
|
||||
])
|
||||
) as Record<MagicNoteCommentDirection, string>
|
||||
|
||||
const defaultAiPaneWidth = 300
|
||||
const minimumAiPaneWidth = 240
|
||||
const maximumAiPaneWidth = 520
|
||||
const magicNotesListPaneWidth = 220
|
||||
const minimumMagicNotesEditorWidth = 300
|
||||
const magicNotesResizeHandleWidth = 9
|
||||
const aiPaneKeyboardResizeStep = 16
|
||||
|
||||
function getAiPaneWidthLimits(layoutWidth: number): {
|
||||
minimum: number
|
||||
maximum: number
|
||||
} {
|
||||
return {
|
||||
minimum: minimumAiPaneWidth,
|
||||
maximum: Math.max(
|
||||
minimumAiPaneWidth,
|
||||
Math.min(
|
||||
maximumAiPaneWidth,
|
||||
layoutWidth -
|
||||
magicNotesListPaneWidth -
|
||||
minimumMagicNotesEditorWidth -
|
||||
magicNotesResizeHandleWidth
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function clampAiPaneWidth(width: number, layoutWidth: number): number {
|
||||
const limits = getAiPaneWidthLimits(layoutWidth)
|
||||
return Math.min(limits.maximum, Math.max(limits.minimum, width))
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -131,13 +185,26 @@ function AiComment({
|
||||
</span>
|
||||
<div>
|
||||
<strong>
|
||||
{comment.kind === 'warning'
|
||||
{comment.kind === 'narrative'
|
||||
? '长评'
|
||||
: comment.kind === 'warning'
|
||||
? '提醒'
|
||||
: comment.kind === 'suggestion'
|
||||
? '建议'
|
||||
: '摘要'}
|
||||
</strong>
|
||||
<p>{comment.content}</p>
|
||||
{comment.direction && (
|
||||
<span className="magic-note-comment__direction">
|
||||
{commentDirectionLabels[comment.direction]}
|
||||
</span>
|
||||
)}
|
||||
{comment.kind === 'narrative' ? (
|
||||
<div className="magic-note-comment__narrative markdown-content">
|
||||
<MarkdownRenderer>{comment.content}</MarkdownRenderer>
|
||||
</div>
|
||||
) : (
|
||||
<p>{comment.content}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -187,6 +254,10 @@ export function MagicNotesWorkspace({
|
||||
useState<TodoListMode>('list')
|
||||
const [commentMode, setCommentMode] =
|
||||
useState<MagicNoteCommentMode>('immediate')
|
||||
const [commentDirection, setCommentDirection] =
|
||||
useState<MagicNoteCommentDirection>('general')
|
||||
const [commentFormat, setCommentFormat] =
|
||||
useState<MagicNoteCommentFormat>('combined')
|
||||
const [selectedNoteId, setSelectedNoteId] = useState('')
|
||||
const [selectedTodoId, setSelectedTodoId] = useState('')
|
||||
const [detail, setDetail] = useState<MagicNoteDetail>()
|
||||
@@ -207,10 +278,21 @@ export function MagicNotesWorkspace({
|
||||
const [editingEntry, setEditingEntry] = useState<MagicNoteEntry>()
|
||||
const [deletingEntryId, setDeletingEntryId] = useState('')
|
||||
const [aiPaneOpen, setAiPaneOpen] = useState(true)
|
||||
const [aiPaneWidth, setAiPaneWidth] = useState(defaultAiPaneWidth)
|
||||
const [aiPaneResizing, setAiPaneResizing] = useState(false)
|
||||
const [magicNotesLayoutWidth, setMagicNotesLayoutWidth] = useState(
|
||||
window.innerWidth
|
||||
)
|
||||
const [draftAnalyses, setDraftAnalyses] = useState<
|
||||
MagicNoteDraftAnalysis[]
|
||||
>([])
|
||||
const [draftAnalysisRunning, setDraftAnalysisRunning] = useState(false)
|
||||
const [liveAnalysis, setLiveAnalysis] = useState<{
|
||||
requestId: string
|
||||
content: string
|
||||
direction: MagicNoteCommentDirection
|
||||
format: MagicNoteCommentFormat
|
||||
}>()
|
||||
const [validation, setValidation] = useState<{
|
||||
target: ValidationTarget
|
||||
message: string
|
||||
@@ -235,10 +317,149 @@ export function MagicNotesWorkspace({
|
||||
const draftAnalysisArmedRef = useRef(false)
|
||||
const draftAnalysisContextRef = useRef(0)
|
||||
const lastDraftAnalysisStartedAtRef = useRef(0)
|
||||
const magicNotesLayoutRef = useRef<HTMLDivElement>(null)
|
||||
const liveAiPaneWidthRef = useRef(defaultAiPaneWidth)
|
||||
const aiResizePointerIdRef = useRef<number | undefined>(undefined)
|
||||
const runDraftAnalysisRef = useRef<
|
||||
(content: MagicNoteRichContent) => Promise<void>
|
||||
>(async () => undefined)
|
||||
|
||||
const createAnalysisOptions = useCallback(
|
||||
async (): Promise<MagicNoteAnalysisOptions> => {
|
||||
let format = commentFormat
|
||||
try {
|
||||
const settings = await window.goodbuddy.updates?.getSettings()
|
||||
if (settings) {
|
||||
format = settings.magicNoteCommentFormat
|
||||
setCommentFormat(format)
|
||||
}
|
||||
} catch {
|
||||
// Keep the last loaded format if settings cannot be refreshed.
|
||||
}
|
||||
return {
|
||||
requestId: crypto.randomUUID(),
|
||||
direction: commentDirection,
|
||||
format
|
||||
}
|
||||
},
|
||||
[commentDirection, commentFormat]
|
||||
)
|
||||
|
||||
const getLayoutBounds = useCallback((): {
|
||||
width: number
|
||||
right: number
|
||||
} => {
|
||||
const bounds = magicNotesLayoutRef.current?.getBoundingClientRect()
|
||||
const width = bounds?.width || window.innerWidth
|
||||
return {
|
||||
width,
|
||||
right: bounds?.right || width
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resizeAiPaneFromClientX = useCallback(
|
||||
(clientX: number, commit: boolean): void => {
|
||||
const bounds = getLayoutBounds()
|
||||
const width = clampAiPaneWidth(
|
||||
bounds.right - clientX,
|
||||
bounds.width
|
||||
)
|
||||
liveAiPaneWidthRef.current = width
|
||||
if (commit) {
|
||||
setAiPaneWidth(width)
|
||||
return
|
||||
}
|
||||
magicNotesLayoutRef.current?.style.setProperty(
|
||||
'--magic-notes-ai-width',
|
||||
`${width}px`
|
||||
)
|
||||
},
|
||||
[getLayoutBounds]
|
||||
)
|
||||
|
||||
const finishAiPaneResize = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>): void => {
|
||||
if (aiResizePointerIdRef.current !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
aiResizePointerIdRef.current = undefined
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
setAiPaneWidth(liveAiPaneWidthRef.current)
|
||||
setAiPaneResizing(false)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const resizeAiPaneWithKeyboard = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
const bounds = getLayoutBounds()
|
||||
const limits = getAiPaneWidthLimits(bounds.width)
|
||||
const nextWidth =
|
||||
event.key === 'Home'
|
||||
? limits.minimum
|
||||
: event.key === 'End'
|
||||
? limits.maximum
|
||||
: event.key === 'ArrowLeft'
|
||||
? aiPaneWidth + aiPaneKeyboardResizeStep
|
||||
: event.key === 'ArrowRight'
|
||||
? aiPaneWidth - aiPaneKeyboardResizeStep
|
||||
: undefined
|
||||
if (nextWidth === undefined) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
const width = clampAiPaneWidth(nextWidth, bounds.width)
|
||||
liveAiPaneWidthRef.current = width
|
||||
setAiPaneWidth(width)
|
||||
},
|
||||
[aiPaneWidth, getLayoutBounds]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
window.goodbuddy.magicNotes.onAnalysisEvent((event) => {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === event.requestId
|
||||
? {
|
||||
...current,
|
||||
content: current.content + event.delta
|
||||
}
|
||||
: current
|
||||
)
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const layout = magicNotesLayoutRef.current
|
||||
const updateLayoutWidth = (): void => {
|
||||
const width = layout?.getBoundingClientRect().width || window.innerWidth
|
||||
setMagicNotesLayoutWidth(width)
|
||||
if (width > 800) {
|
||||
setAiPaneWidth((current) => {
|
||||
const next = clampAiPaneWidth(current, width)
|
||||
liveAiPaneWidthRef.current = next
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
updateLayoutWidth()
|
||||
window.addEventListener('resize', updateLayoutWidth)
|
||||
const observer =
|
||||
layout && typeof ResizeObserver !== 'undefined'
|
||||
? new ResizeObserver(updateLayoutWidth)
|
||||
: undefined
|
||||
if (layout && observer) {
|
||||
observer.observe(layout)
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateLayoutWidth)
|
||||
observer?.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const notifyError = useCallback(
|
||||
(error: unknown): void =>
|
||||
onNotify({
|
||||
@@ -287,6 +508,7 @@ export function MagicNotesWorkspace({
|
||||
.then((settings) => {
|
||||
if (active) {
|
||||
setCommentMode(settings.magicNoteCommentMode)
|
||||
setCommentFormat(settings.magicNoteCommentFormat)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
@@ -307,9 +529,16 @@ export function MagicNotesWorkspace({
|
||||
const analysisContext = draftAnalysisContextRef.current
|
||||
lastDraftAnalysisStartedAtRef.current = Date.now()
|
||||
setDraftAnalysisRunning(true)
|
||||
const options = await createAnalysisOptions()
|
||||
setLiveAnalysis({
|
||||
requestId: options.requestId,
|
||||
content: '',
|
||||
direction: options.direction,
|
||||
format: options.format
|
||||
})
|
||||
try {
|
||||
const analysis =
|
||||
await window.goodbuddy.magicNotes.analyzeDraft(content)
|
||||
await window.goodbuddy.magicNotes.analyzeDraft(content, options)
|
||||
if (draftAnalysisContextRef.current === analysisContext) {
|
||||
setDraftAnalyses((current) => [analysis, ...current].slice(0, 20))
|
||||
}
|
||||
@@ -318,6 +547,9 @@ export function MagicNotesWorkspace({
|
||||
notifyError(analysisError)
|
||||
}
|
||||
} finally {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === options.requestId ? undefined : current
|
||||
)
|
||||
draftAnalysisRunningRef.current = false
|
||||
setDraftAnalysisRunning(false)
|
||||
if (draftAnalysisQueuedRef.current) {
|
||||
@@ -336,7 +568,7 @@ export function MagicNotesWorkspace({
|
||||
}
|
||||
}
|
||||
},
|
||||
[notifyError]
|
||||
[createAnalysisOptions, notifyError]
|
||||
)
|
||||
useEffect(() => {
|
||||
runDraftAnalysisRef.current = runDraftAnalysis
|
||||
@@ -583,6 +815,8 @@ export function MagicNotesWorkspace({
|
||||
.reverse(),
|
||||
[detail]
|
||||
)
|
||||
const aiPaneWidthLimits = getAiPaneWidthLimits(magicNotesLayoutWidth)
|
||||
const canResizeAiPane = aiPaneOpen && magicNotesLayoutWidth > 800
|
||||
|
||||
const createNote = async (): Promise<void> => {
|
||||
const title = newTitle.trim()
|
||||
@@ -617,12 +851,24 @@ export function MagicNotesWorkspace({
|
||||
if (!beginBusy(operation)) {
|
||||
return
|
||||
}
|
||||
const options = await createAnalysisOptions()
|
||||
setLiveAnalysis({
|
||||
requestId: options.requestId,
|
||||
content: '',
|
||||
direction: options.direction,
|
||||
format: options.format
|
||||
})
|
||||
try {
|
||||
applyTodo(await window.goodbuddy.magicNotes.analyzeTodo(todoId))
|
||||
notifySuccess('AI 评论已更新')
|
||||
applyTodo(
|
||||
await window.goodbuddy.magicNotes.analyzeTodo(todoId, options)
|
||||
)
|
||||
notifySuccess('AI 评论已添加')
|
||||
} catch (analysisError) {
|
||||
notifyError(analysisError)
|
||||
} finally {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === options.requestId ? undefined : current
|
||||
)
|
||||
endBusy(operation)
|
||||
}
|
||||
}
|
||||
@@ -693,13 +939,29 @@ export function MagicNotesWorkspace({
|
||||
(entry) => !existingEntryIds.has(entry.id)
|
||||
)
|
||||
if (commentMode === 'after-save-auto' && createdEntry) {
|
||||
const options = await createAnalysisOptions()
|
||||
setLiveAnalysis({
|
||||
requestId: options.requestId,
|
||||
content: '',
|
||||
direction: options.direction,
|
||||
format: options.format
|
||||
})
|
||||
try {
|
||||
applyDetail(
|
||||
await window.goodbuddy.magicNotes.analyze(createdEntry.id)
|
||||
await window.goodbuddy.magicNotes.analyze(
|
||||
createdEntry.id,
|
||||
options
|
||||
)
|
||||
)
|
||||
notifySuccess('AI 评论已更新')
|
||||
notifySuccess('AI 评论已添加')
|
||||
} catch (analysisError) {
|
||||
notifyError(analysisError)
|
||||
} finally {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === options.requestId
|
||||
? undefined
|
||||
: current
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (saveError) {
|
||||
@@ -732,13 +994,29 @@ export function MagicNotesWorkspace({
|
||||
editingContentRef.current = undefined
|
||||
notifySuccess('记录已更新,原 AI 评论已清除')
|
||||
if (commentMode === 'after-save-auto') {
|
||||
const options = await createAnalysisOptions()
|
||||
setLiveAnalysis({
|
||||
requestId: options.requestId,
|
||||
content: '',
|
||||
direction: options.direction,
|
||||
format: options.format
|
||||
})
|
||||
try {
|
||||
applyDetail(
|
||||
await window.goodbuddy.magicNotes.analyze(editingEntry.id)
|
||||
await window.goodbuddy.magicNotes.analyze(
|
||||
editingEntry.id,
|
||||
options
|
||||
)
|
||||
)
|
||||
notifySuccess('AI 评论已更新')
|
||||
notifySuccess('AI 评论已添加')
|
||||
} catch (analysisError) {
|
||||
notifyError(analysisError)
|
||||
} finally {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === options.requestId
|
||||
? undefined
|
||||
: current
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (updateError) {
|
||||
@@ -753,12 +1031,24 @@ export function MagicNotesWorkspace({
|
||||
if (!beginBusy(operation)) {
|
||||
return
|
||||
}
|
||||
const options = await createAnalysisOptions()
|
||||
setLiveAnalysis({
|
||||
requestId: options.requestId,
|
||||
content: '',
|
||||
direction: options.direction,
|
||||
format: options.format
|
||||
})
|
||||
try {
|
||||
applyDetail(await window.goodbuddy.magicNotes.analyze(entryId))
|
||||
notifySuccess('AI 评论已更新')
|
||||
applyDetail(
|
||||
await window.goodbuddy.magicNotes.analyze(entryId, options)
|
||||
)
|
||||
notifySuccess('AI 评论已添加')
|
||||
} catch (analysisError) {
|
||||
notifyError(analysisError)
|
||||
} finally {
|
||||
setLiveAnalysis((current) =>
|
||||
current?.requestId === options.requestId ? undefined : current
|
||||
)
|
||||
endBusy(operation)
|
||||
}
|
||||
}
|
||||
@@ -836,10 +1126,20 @@ export function MagicNotesWorkspace({
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={magicNotesLayoutRef}
|
||||
aria-busy={Boolean(busy)}
|
||||
className={`magic-notes-layout${
|
||||
aiPaneOpen ? '' : ' magic-notes-layout--ai-hidden'
|
||||
}${
|
||||
aiPaneResizing && canResizeAiPane
|
||||
? ' magic-notes-layout--resizing'
|
||||
: ''
|
||||
}`}
|
||||
style={
|
||||
{
|
||||
'--magic-notes-ai-width': `${aiPaneWidth}px`
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<aside
|
||||
aria-label={libraryView === 'notes' ? '笔记列表' : '待办列表'}
|
||||
@@ -1542,6 +1842,53 @@ export function MagicNotesWorkspace({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{aiPaneOpen && (
|
||||
<div
|
||||
aria-controls="magic-notes-ai-pane"
|
||||
aria-disabled={!canResizeAiPane}
|
||||
aria-label="调整编辑区与 AI 评论宽度"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax={aiPaneWidthLimits.maximum}
|
||||
aria-valuemin={aiPaneWidthLimits.minimum}
|
||||
aria-valuenow={aiPaneWidth}
|
||||
aria-valuetext={`AI 评论栏 ${aiPaneWidth} 像素`}
|
||||
className="magic-notes-ai-resize-handle"
|
||||
onKeyDown={resizeAiPaneWithKeyboard}
|
||||
onLostPointerCapture={(event) => {
|
||||
if (aiResizePointerIdRef.current === event.pointerId) {
|
||||
aiResizePointerIdRef.current = undefined
|
||||
setAiPaneWidth(liveAiPaneWidthRef.current)
|
||||
setAiPaneResizing(false)
|
||||
}
|
||||
}}
|
||||
onPointerCancel={finishAiPaneResize}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0 || !canResizeAiPane) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
aiResizePointerIdRef.current = event.pointerId
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
resizeAiPaneFromClientX(event.clientX, true)
|
||||
setAiPaneResizing(true)
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (aiResizePointerIdRef.current !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
if (!canResizeAiPane) {
|
||||
finishAiPaneResize(event)
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
resizeAiPaneFromClientX(event.clientX, false)
|
||||
}}
|
||||
onPointerUp={finishAiPaneResize}
|
||||
role="separator"
|
||||
tabIndex={canResizeAiPane ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside
|
||||
aria-label="AI 评论"
|
||||
className="magic-notes-ai-pane"
|
||||
@@ -1560,13 +1907,72 @@ export function MagicNotesWorkspace({
|
||||
<PanelRightClose aria-hidden="true" size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="magic-notes-ai-controls">
|
||||
<label>
|
||||
<span>评论方向</span>
|
||||
<select
|
||||
aria-label="AI 评论方向"
|
||||
onChange={(event) =>
|
||||
setCommentDirection(
|
||||
event.target.value as MagicNoteCommentDirection
|
||||
)
|
||||
}
|
||||
value={commentDirection}
|
||||
>
|
||||
{commentDirections.map((direction) => (
|
||||
<option key={direction.value} value={direction.value}>
|
||||
{direction.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<small>
|
||||
评论方向更改仅用于下一次评论,不会改动已生成的内容;评论形式可在设置中心修改。
|
||||
</small>
|
||||
</div>
|
||||
{liveAnalysis?.format === 'structured' ? (
|
||||
<p className="magic-notes-muted" role="status">
|
||||
正在生成
|
||||
{commentDirectionLabels[liveAnalysis.direction]}
|
||||
要点…
|
||||
</p>
|
||||
) : liveAnalysis ? (
|
||||
<section
|
||||
aria-live="polite"
|
||||
className="magic-notes-ai-group magic-notes-ai-live"
|
||||
>
|
||||
<span className="magic-notes-ai-source">
|
||||
正在生成 ·{' '}
|
||||
{commentDirectionLabels[liveAnalysis.direction]}
|
||||
</span>
|
||||
<div className="magic-note-comment magic-note-comment--narrative">
|
||||
<span aria-hidden="true">
|
||||
<Bot size={15} />
|
||||
</span>
|
||||
<div>
|
||||
<strong>长评</strong>
|
||||
{liveAnalysis.content ? (
|
||||
<div className="magic-note-comment__narrative markdown-content">
|
||||
<MarkdownRenderer>
|
||||
{liveAnalysis.content}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
) : (
|
||||
<p role="status">正在准备评论…</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
{libraryView === 'todos' ? (
|
||||
!selectedTodo ? (
|
||||
<p className="magic-notes-muted">选择待办后显示 AI 评论。</p>
|
||||
) : selectedTodo.comments.length === 0 ? (
|
||||
<p className="magic-notes-muted">
|
||||
点击待办详情中的“AI 分析”,评论会显示在这里。
|
||||
</p>
|
||||
!liveAnalysis && (
|
||||
<p className="magic-notes-muted">
|
||||
点击待办详情中的“AI 分析”,评论会显示在这里。
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<div className="magic-notes-ai-feed">
|
||||
<section className="magic-notes-ai-group">
|
||||
@@ -1583,7 +1989,8 @@ export function MagicNotesWorkspace({
|
||||
<p className="magic-notes-muted">选择笔记后显示 AI 评论。</p>
|
||||
) : aiEntries.length === 0 &&
|
||||
draftAnalyses.length === 0 &&
|
||||
!draftAnalysisRunning ? (
|
||||
!draftAnalysisRunning &&
|
||||
!liveAnalysis ? (
|
||||
<p className="magic-notes-muted">
|
||||
{commentMode === 'immediate'
|
||||
? '写完一句后按回车,停止输入 5 秒,评论会显示在这里。'
|
||||
@@ -1593,7 +2000,7 @@ export function MagicNotesWorkspace({
|
||||
</p>
|
||||
) : (
|
||||
<div className="magic-notes-ai-feed">
|
||||
{draftAnalysisRunning && (
|
||||
{draftAnalysisRunning && !liveAnalysis && (
|
||||
<p className="magic-notes-muted" role="status">
|
||||
正在评论当前草稿…
|
||||
</p>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ApplicationSettings,
|
||||
MagicNoteCommentMode
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { MagicNoteCommentFormat } from '../../shared/magic-notes-contracts'
|
||||
import { SegmentedControl } from './WorkspacePrimitives'
|
||||
|
||||
type PlatformFeaturesSettingsSectionProps = {
|
||||
@@ -86,6 +87,26 @@ export function PlatformFeaturesSettingsSection({
|
||||
}
|
||||
}
|
||||
|
||||
const changeCommentFormat = async (
|
||||
magicNoteCommentFormat: MagicNoteCommentFormat
|
||||
): Promise<void> => {
|
||||
const updates = window.goodbuddy.updates
|
||||
if (!updates || !settings) {
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError(undefined)
|
||||
try {
|
||||
setSettings(
|
||||
await updates.updateSettings({ magicNoteCommentFormat })
|
||||
)
|
||||
} catch {
|
||||
setError('保存 AI 评论形式失败,请重试')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="platform-features-heading"
|
||||
@@ -136,6 +157,23 @@ export function PlatformFeaturesSettingsSection({
|
||||
即时模式会在按回车并停止输入 5 秒后评论未保存草稿;自动模式在保存后评论;手动模式仅在点击 AI 分析后评论。
|
||||
</small>
|
||||
</div>
|
||||
<div className="platform-feature-option">
|
||||
<span>AI 评论形式</span>
|
||||
<SegmentedControl
|
||||
ariaLabel="魔法笔记 AI 评论形式"
|
||||
disabled={!settings || saving}
|
||||
onChange={(value) => void changeCommentFormat(value)}
|
||||
options={[
|
||||
{ value: 'combined', label: '长评 + 要点' },
|
||||
{ value: 'narrative', label: '长评' },
|
||||
{ value: 'structured', label: '要点' }
|
||||
]}
|
||||
value={settings?.magicNoteCommentFormat ?? 'combined'}
|
||||
/>
|
||||
<small>
|
||||
默认同时生成流式长评和结构化要点;也可以只保留其中一种。
|
||||
</small>
|
||||
</div>
|
||||
</article>
|
||||
{error && (
|
||||
<p className="settings-warning" role="alert">
|
||||
|
||||
@@ -329,7 +329,8 @@ const onEmbeddingStatus = vi.fn(
|
||||
let applicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
const getApplicationSettings = vi.fn(async () => ({
|
||||
...applicationSettings
|
||||
@@ -350,7 +351,8 @@ describe('SettingsPanel runtime files', () => {
|
||||
applicationSettings = {
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
embeddingStatusListeners.splice(0)
|
||||
Object.defineProperty(window, 'goodbuddy', {
|
||||
@@ -474,6 +476,15 @@ describe('SettingsPanel runtime files', () => {
|
||||
magicNoteCommentMode: 'after-save-auto'
|
||||
})
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('button', { name: '长评 + 要点' })
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
fireEvent.click(screen.getByRole('button', { name: '要点' }))
|
||||
await waitFor(() =>
|
||||
expect(updateApplicationSettings).toHaveBeenCalledWith({
|
||||
magicNoteCommentFormat: 'structured'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps page navigation beside an independently scrollable panel', () => {
|
||||
|
||||
@@ -23,7 +23,9 @@ describe('UpdateSettingsSection', () => {
|
||||
input.checkUpdatesOnStartup ?? true,
|
||||
magicNotesEnabled: input.magicNotesEnabled ?? true,
|
||||
magicNoteCommentMode:
|
||||
input.magicNoteCommentMode ?? 'immediate'
|
||||
input.magicNoteCommentMode ?? 'immediate',
|
||||
magicNoteCommentFormat:
|
||||
input.magicNoteCommentFormat ?? 'combined'
|
||||
}))
|
||||
const check = vi.fn<
|
||||
NonNullable<DesktopApi['updates']>['check']
|
||||
@@ -62,7 +64,8 @@ describe('UpdateSettingsSection', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})),
|
||||
updateSettings,
|
||||
check,
|
||||
@@ -111,12 +114,14 @@ describe('UpdateSettingsSection', () => {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate'
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})),
|
||||
check: vi.fn(async () => {
|
||||
throw new Error(
|
||||
|
||||
+104
-2
@@ -110,13 +110,57 @@
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface-raised);
|
||||
grid-template-columns: 220px minmax(300px, 1fr) minmax(240px, 280px);
|
||||
grid-template-columns:
|
||||
220px minmax(300px, 1fr) 9px
|
||||
var(--magic-notes-ai-width, 300px);
|
||||
}
|
||||
|
||||
.magic-notes-layout--ai-hidden {
|
||||
grid-template-columns: 220px minmax(300px, 1fr);
|
||||
}
|
||||
|
||||
.magic-notes-layout--resizing {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.magic-notes-ai-resize-handle {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 9px;
|
||||
min-width: 9px;
|
||||
padding: 0;
|
||||
background: var(--surface-subtle);
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.magic-notes-ai-resize-handle::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 4px;
|
||||
width: 1px;
|
||||
background: var(--border-subtle);
|
||||
content: '';
|
||||
transition:
|
||||
width var(--motion-fast) ease-out,
|
||||
background var(--motion-fast) ease-out;
|
||||
}
|
||||
|
||||
.magic-notes-ai-resize-handle:hover::after,
|
||||
.magic-notes-ai-resize-handle:focus-visible::after,
|
||||
.magic-notes-layout--resizing
|
||||
> .magic-notes-ai-resize-handle::after {
|
||||
width: 2px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.magic-notes-ai-resize-handle:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.magic-notes-list-pane,
|
||||
.magic-notes-stream-pane,
|
||||
.magic-notes-ai-pane {
|
||||
@@ -158,7 +202,37 @@
|
||||
|
||||
.magic-notes-ai-pane {
|
||||
padding: var(--space-4);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.magic-notes-ai-controls {
|
||||
display: grid;
|
||||
padding: var(--space-3) 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.magic-notes-ai-controls label {
|
||||
display: grid;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-caption);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.magic-notes-ai-controls select {
|
||||
width: 100%;
|
||||
min-height: var(--control-height);
|
||||
padding: 0 var(--space-3);
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.magic-notes-ai-controls > small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.magic-notes-pane-heading {
|
||||
@@ -700,6 +774,11 @@
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.magic-notes-ai-live {
|
||||
padding: var(--space-3) 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.magic-notes-ai-group {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
@@ -739,6 +818,13 @@
|
||||
font-size: var(--font-body);
|
||||
}
|
||||
|
||||
.magic-note-comment__direction {
|
||||
display: inline-block;
|
||||
margin-left: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.magic-note-comment p {
|
||||
margin: var(--space-1) 0 0;
|
||||
color: var(--text-secondary);
|
||||
@@ -746,6 +832,18 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.magic-note-comment__narrative.markdown-content {
|
||||
margin-top: var(--space-2);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-body);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.magic-note-comment__narrative.markdown-content
|
||||
:where(p, ul, ol, blockquote) {
|
||||
margin-block: var(--space-2);
|
||||
}
|
||||
|
||||
.magic-notes-page .danger-solid {
|
||||
min-height: var(--control-height);
|
||||
padding: 0 13px;
|
||||
@@ -762,6 +860,10 @@
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.magic-notes-ai-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.magic-notes-ai-pane {
|
||||
max-height: 280px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
|
||||
Reference in New Issue
Block a user