feat: ship v0.5.0 with auto and wizard writing modes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { create } from 'zustand'
|
||||
import type { AutoWritingConfig, InteractiveFlow } from '@shared/types'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
import { useBookStore } from '@renderer/stores/useBookStore'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
import { useEditStore } from '@renderer/stores/useEditStore'
|
||||
import { useAiStore } from '@renderer/stores/useAiStore'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
interface AutoState {
|
||||
gateEligible: boolean
|
||||
flow: InteractiveFlow | null
|
||||
streamBuffer: string
|
||||
streaming: boolean
|
||||
sceneDraft: string
|
||||
listenersAttached: boolean
|
||||
checkGate: (bookId: string) => Promise<void>
|
||||
loadFlow: (bookId: string, sessionId: string) => Promise<void>
|
||||
start: (bookId: string, sessionId: string) => Promise<void>
|
||||
setGoal: (bookId: string, config: Partial<AutoWritingConfig>) => Promise<void>
|
||||
planScenes: (bookId: string) => Promise<void>
|
||||
generateNext: (bookId: string) => Promise<void>
|
||||
resolveNaming: (bookId: string, chosenName: string) => Promise<void>
|
||||
pause: (bookId: string) => Promise<void>
|
||||
resume: (bookId: string) => Promise<void>
|
||||
handoffInteractive: (bookId: string) => Promise<void>
|
||||
finishChapter: (bookId: string, title?: string) => Promise<void>
|
||||
refreshSceneDraft: (bookId: string) => Promise<void>
|
||||
mount: () => void
|
||||
unmount: () => void
|
||||
}
|
||||
|
||||
let unsubStream: (() => void) | null = null
|
||||
|
||||
export const useAutoStore = create<AutoState>((set, get) => ({
|
||||
gateEligible: false,
|
||||
flow: null,
|
||||
streamBuffer: '',
|
||||
streaming: false,
|
||||
sceneDraft: '',
|
||||
listenersAttached: false,
|
||||
|
||||
checkGate: async (bookId) => {
|
||||
const r = await ipcCall(() => window.electronAPI.auto.checkGate(bookId))
|
||||
set({ gateEligible: r.eligible })
|
||||
},
|
||||
|
||||
loadFlow: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.getFlow(bookId, sessionId))
|
||||
set({ flow })
|
||||
if (flow) await get().refreshSceneDraft(bookId)
|
||||
},
|
||||
|
||||
start: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.start(bookId, sessionId))
|
||||
set({ flow, streamBuffer: '', sceneDraft: '' })
|
||||
},
|
||||
|
||||
setGoal: async (bookId, config) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.setGoal(bookId, flowId, config))
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
planScenes: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true })
|
||||
try {
|
||||
const summary = useAiStore.getState().contextSummary
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.auto.planScenes(bookId, flowId, summary)
|
||||
)
|
||||
set({ flow })
|
||||
} finally {
|
||||
set({ streaming: false })
|
||||
}
|
||||
},
|
||||
|
||||
generateNext: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true, streamBuffer: '' })
|
||||
try {
|
||||
const summary = useAiStore.getState().contextSummary
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.auto.generate(bookId, flowId, summary)
|
||||
)
|
||||
set({ flow })
|
||||
await get().refreshSceneDraft(bookId)
|
||||
} finally {
|
||||
set({ streaming: false, streamBuffer: '' })
|
||||
}
|
||||
},
|
||||
|
||||
resolveNaming: async (bookId, chosenName) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true, streamBuffer: '' })
|
||||
try {
|
||||
const summary = useAiStore.getState().contextSummary
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.auto.resolveNaming(bookId, flowId, chosenName, summary)
|
||||
)
|
||||
set({ flow })
|
||||
await get().refreshSceneDraft(bookId)
|
||||
} finally {
|
||||
set({ streaming: false, streamBuffer: '' })
|
||||
}
|
||||
},
|
||||
|
||||
pause: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.pause(bookId, flowId))
|
||||
set({ flow, streaming: false, streamBuffer: '' })
|
||||
},
|
||||
|
||||
resume: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.resume(bookId, flowId))
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
handoffInteractive: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.auto.handoffInteractive(bookId, flowId))
|
||||
set({ flow })
|
||||
useAiStore.getState().setWritingMode('interactive')
|
||||
useAppStore.getState().showToast(i18n.t('wizard.handoffToast'))
|
||||
},
|
||||
|
||||
finishChapter: async (bookId, title) => {
|
||||
const flowId = get().flow?.id
|
||||
const volumeId = useBookStore.getState().activeVolumeId
|
||||
if (!flowId || !volumeId) throw new Error('no volume')
|
||||
const { chapterId } = await ipcCall(() =>
|
||||
window.electronAPI.auto.finishChapter(bookId, flowId, volumeId, title)
|
||||
)
|
||||
await useBookStore.getState().refreshChapters()
|
||||
await useBookStore.getState().openBook(bookId)
|
||||
useBookStore.getState().setSelectedChapter(chapterId)
|
||||
useEditStore.getState().setTarget({ kind: 'chapter', id: chapterId })
|
||||
useAppStore.getState().setView('editor')
|
||||
useAppStore.getState().showToast(i18n.t('auto.chapterCreated'))
|
||||
},
|
||||
|
||||
refreshSceneDraft: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const draft = await ipcCall(() => window.electronAPI.auto.getSceneDraft(bookId, flowId))
|
||||
set({ sceneDraft: draft })
|
||||
},
|
||||
|
||||
mount: () => {
|
||||
if (get().listenersAttached) return
|
||||
unsubStream = window.electronAPI.onInteractiveStreamChunk((payload) => {
|
||||
if (payload.flowMode !== 'auto' || payload.done) return
|
||||
set((s) => ({
|
||||
streaming: true,
|
||||
streamBuffer: s.streamBuffer + payload.delta
|
||||
}))
|
||||
})
|
||||
set({ listenersAttached: true })
|
||||
},
|
||||
|
||||
unmount: () => {
|
||||
unsubStream?.()
|
||||
unsubStream = null
|
||||
set({ listenersAttached: false })
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,137 @@
|
||||
import { create } from 'zustand'
|
||||
import type {
|
||||
AiContextBinding,
|
||||
AutoRhythm,
|
||||
AutoWritingConfig,
|
||||
InteractiveFlow,
|
||||
WizardWritingConfig
|
||||
} from '@shared/types'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
import { useAiStore } from '@renderer/stores/useAiStore'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
interface WizardState {
|
||||
gateEligible: boolean
|
||||
flow: InteractiveFlow | null
|
||||
previewHtml: string
|
||||
streaming: boolean
|
||||
streamBuffer: string
|
||||
listenersAttached: boolean
|
||||
checkGate: (bookId: string) => Promise<void>
|
||||
loadFlow: (bookId: string, sessionId: string) => Promise<void>
|
||||
start: (bookId: string, sessionId: string) => Promise<void>
|
||||
setGoal: (bookId: string, config: Partial<AutoWritingConfig>) => Promise<void>
|
||||
setRhythm: (bookId: string, rhythm: AutoRhythm, styleNote?: string) => Promise<void>
|
||||
setPov: (bookId: string, povSettingId: string) => Promise<void>
|
||||
confirmContext: (bookId: string, binding: AiContextBinding) => Promise<void>
|
||||
buildPreview: (bookId: string) => Promise<void>
|
||||
startGenerate: (bookId: string) => Promise<void>
|
||||
mount: () => void
|
||||
unmount: () => void
|
||||
}
|
||||
|
||||
let unsubStream: (() => void) | null = null
|
||||
|
||||
export const useWizardStore = create<WizardState>((set, get) => ({
|
||||
gateEligible: false,
|
||||
flow: null,
|
||||
previewHtml: '',
|
||||
streaming: false,
|
||||
streamBuffer: '',
|
||||
listenersAttached: false,
|
||||
|
||||
checkGate: async (bookId) => {
|
||||
const r = await ipcCall(() => window.electronAPI.auto.checkGate(bookId))
|
||||
set({ gateEligible: r.eligible })
|
||||
},
|
||||
|
||||
loadFlow: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.wizard.getFlow(bookId, sessionId))
|
||||
const config = flow?.modeConfig as WizardWritingConfig | undefined
|
||||
set({
|
||||
flow,
|
||||
previewHtml: config?.strategyPreview ?? ''
|
||||
})
|
||||
},
|
||||
|
||||
start: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.wizard.start(bookId, sessionId))
|
||||
set({ flow, previewHtml: '', streamBuffer: '' })
|
||||
},
|
||||
|
||||
setGoal: async (bookId, config) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.wizard.setGoal(bookId, flowId, config))
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
setRhythm: async (bookId, rhythm, styleNote) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.wizard.setRhythm(bookId, flowId, rhythm, styleNote)
|
||||
)
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
setPov: async (bookId, povSettingId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.wizard.setPov(bookId, flowId, povSettingId))
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
confirmContext: async (bookId, binding) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.wizard.confirmContext(bookId, flowId, binding)
|
||||
)
|
||||
set({ flow })
|
||||
},
|
||||
|
||||
buildPreview: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true })
|
||||
try {
|
||||
const { preview, flow } = await ipcCall(() =>
|
||||
window.electronAPI.wizard.buildPreview(bookId, flowId)
|
||||
)
|
||||
set({ previewHtml: preview, flow })
|
||||
} finally {
|
||||
set({ streaming: false })
|
||||
}
|
||||
},
|
||||
|
||||
startGenerate: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true, streamBuffer: '' })
|
||||
try {
|
||||
const flow = await ipcCall(() => window.electronAPI.wizard.startGenerate(bookId, flowId))
|
||||
set({ flow })
|
||||
useAiStore.getState().setWritingMode('interactive')
|
||||
useAppStore.getState().showToast(i18n.t('wizard.handoffToast'))
|
||||
} finally {
|
||||
set({ streaming: false, streamBuffer: '' })
|
||||
}
|
||||
},
|
||||
|
||||
mount: () => {
|
||||
if (get().listenersAttached) return
|
||||
unsubStream = window.electronAPI.onInteractiveStreamChunk((payload) => {
|
||||
if (payload.flowMode !== 'wizard' || payload.done) return
|
||||
set((s) => ({ streaming: true, streamBuffer: s.streamBuffer + payload.delta }))
|
||||
})
|
||||
set({ listenersAttached: true })
|
||||
},
|
||||
|
||||
unmount: () => {
|
||||
unsubStream?.()
|
||||
unsubStream = null
|
||||
set({ listenersAttached: false })
|
||||
}
|
||||
}))
|
||||
Reference in New Issue
Block a user