feat: ship v0.4.0 with interactive writing mode
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { create } from 'zustand'
|
||||
import type { InteractiveFlow, PlotOption, PlotSelection } 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 InteractiveState {
|
||||
gateEligible: boolean
|
||||
gateReason?: string
|
||||
flow: InteractiveFlow | null
|
||||
plots: PlotOption[]
|
||||
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>
|
||||
confirmContextAndStart: (bookId: string, flowId: string) => Promise<void>
|
||||
suggestPlots: (bookId: string) => Promise<void>
|
||||
selectPlot: (bookId: string, selection: PlotSelection) => Promise<void>
|
||||
generateScene: (bookId: string) => Promise<void>
|
||||
resolveNaming: (bookId: string, chosenName: string) => Promise<void>
|
||||
refineScene: (bookId: string, instruction: string) => Promise<void>
|
||||
acceptScene: (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 useInteractiveStore = create<InteractiveState>((set, get) => ({
|
||||
gateEligible: false,
|
||||
flow: null,
|
||||
plots: [],
|
||||
streamBuffer: '',
|
||||
streaming: false,
|
||||
sceneDraft: '',
|
||||
listenersAttached: false,
|
||||
|
||||
checkGate: async (bookId) => {
|
||||
const r = await ipcCall(() => window.electronAPI.interactive.checkGate(bookId))
|
||||
set({ gateEligible: r.eligible, gateReason: r.reason })
|
||||
},
|
||||
|
||||
loadFlow: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.interactive.getFlow(bookId, sessionId))
|
||||
set({ flow })
|
||||
if (flow) await get().refreshSceneDraft(bookId)
|
||||
},
|
||||
|
||||
start: async (bookId, sessionId) => {
|
||||
const flow = await ipcCall(() => window.electronAPI.interactive.start(bookId, sessionId))
|
||||
set({ flow, plots: [], streamBuffer: '', sceneDraft: '' })
|
||||
},
|
||||
|
||||
confirmContextAndStart: async (bookId, flowId) => {
|
||||
const session = useAiStore.getState().sessions.find(
|
||||
(s) => s.id === useAiStore.getState().activeSessionId
|
||||
)
|
||||
const binding = session?.context ?? {
|
||||
chapterIds: [],
|
||||
outlineIds: [],
|
||||
settingIds: [],
|
||||
inspirationIds: []
|
||||
}
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.interactive.confirmContext(bookId, flowId, binding)
|
||||
)
|
||||
set({ flow })
|
||||
await get().suggestPlots(bookId)
|
||||
},
|
||||
|
||||
suggestPlots: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true })
|
||||
try {
|
||||
const plots = await ipcCall(() =>
|
||||
window.electronAPI.interactive.suggestPlots(bookId, flowId)
|
||||
)
|
||||
const flow = await ipcCall(() => window.electronAPI.interactive.getFlow(bookId, get().flow!.sessionId))
|
||||
set({ plots, flow })
|
||||
} finally {
|
||||
set({ streaming: false })
|
||||
}
|
||||
},
|
||||
|
||||
selectPlot: async (bookId, selection) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.interactive.selectPlot(bookId, flowId, selection)
|
||||
)
|
||||
set({ flow, streamBuffer: '', sceneDraft: '' })
|
||||
await get().generateScene(bookId)
|
||||
},
|
||||
|
||||
generateScene: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
set({ streaming: true, streamBuffer: '' })
|
||||
try {
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.interactive.generateScene(bookId, flowId)
|
||||
)
|
||||
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 flow = await ipcCall(() =>
|
||||
window.electronAPI.interactive.resolveNaming(bookId, flowId, chosenName)
|
||||
)
|
||||
set({ flow })
|
||||
await get().refreshSceneDraft(bookId)
|
||||
} finally {
|
||||
set({ streaming: false, streamBuffer: '' })
|
||||
}
|
||||
},
|
||||
|
||||
refineScene: async (bookId, instruction) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId || !instruction.trim()) return
|
||||
set({ streaming: true, streamBuffer: '' })
|
||||
try {
|
||||
const flow = await ipcCall(() =>
|
||||
window.electronAPI.interactive.refineScene(bookId, flowId, instruction.trim())
|
||||
)
|
||||
set({ flow })
|
||||
await get().refreshSceneDraft(bookId)
|
||||
} finally {
|
||||
set({ streaming: false, streamBuffer: '' })
|
||||
}
|
||||
},
|
||||
|
||||
acceptScene: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const flow = await ipcCall(() => window.electronAPI.interactive.acceptScene(bookId, flowId))
|
||||
set({ flow, plots: [], sceneDraft: '', streamBuffer: '' })
|
||||
await get().suggestPlots(bookId)
|
||||
},
|
||||
|
||||
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.interactive.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('interactive.chapterCreated'))
|
||||
},
|
||||
|
||||
refreshSceneDraft: async (bookId) => {
|
||||
const flowId = get().flow?.id
|
||||
if (!flowId) return
|
||||
const draft = await ipcCall(() =>
|
||||
window.electronAPI.interactive.getSceneDraft(bookId, flowId)
|
||||
)
|
||||
set({ sceneDraft: draft })
|
||||
},
|
||||
|
||||
mount: () => {
|
||||
if (get().listenersAttached) return
|
||||
unsubStream = window.electronAPI.onInteractiveStreamChunk((payload) => {
|
||||
if (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