feat: ship v0.6.0 with cockpit, knowledge base, and chapter bridge
Deliver P5 writer workflow: writing cockpit, manual knowledge CRUD with review, chapter bridge with optional AI, publish status, and stock buffer reminders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import type { BookMeta, Volume, Chapter, OutlineItem, SettingEntry, InspirationEntry } from '@shared/types'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
import { useCockpitStore } from '@renderer/stores/useCockpitStore'
|
||||
|
||||
interface BookState {
|
||||
books: BookMeta[]
|
||||
@@ -62,6 +63,16 @@ export const useBookStore = create<BookState>((set, get) => ({
|
||||
activeVolumeId: result.chapters.find((c) => c.id === lastChapter)?.volumeId ?? firstVolume,
|
||||
selectedChapterId: lastChapter
|
||||
})
|
||||
const cockpit = useCockpitStore.getState()
|
||||
const shouldShow = await cockpit.shouldShowOnOpen(bookId)
|
||||
if (shouldShow) {
|
||||
await cockpit.markSeen(bookId)
|
||||
cockpit.openModal()
|
||||
void cockpit.loadSummary(
|
||||
bookId,
|
||||
result.chapters.find((c) => c.id === lastChapter)?.volumeId ?? firstVolume ?? undefined
|
||||
)
|
||||
}
|
||||
},
|
||||
setSelectedChapter: (chapterId) => {
|
||||
const ch = get().chapters.find((c) => c.id === chapterId)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { create } from 'zustand'
|
||||
import type { CockpitSummary } from '@shared/types'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
|
||||
interface CockpitState {
|
||||
open: boolean
|
||||
summary: CockpitSummary | null
|
||||
loading: boolean
|
||||
bridgeChapterId: string | null
|
||||
loadSummary: (bookId: string, volumeId?: string) => Promise<void>
|
||||
openModal: () => void
|
||||
close: () => void
|
||||
markSeen: (bookId: string) => Promise<void>
|
||||
shouldShowOnOpen: (bookId: string) => Promise<boolean>
|
||||
openBridgeForChapter: (chapterId: string) => void
|
||||
clearBridgeChapter: () => void
|
||||
}
|
||||
|
||||
export const useCockpitStore = create<CockpitState>((set) => ({
|
||||
open: false,
|
||||
summary: null,
|
||||
loading: false,
|
||||
bridgeChapterId: null,
|
||||
loadSummary: async (bookId, volumeId) => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const summary = await ipcCall(() => window.electronAPI.cockpit.getSummary(bookId, volumeId))
|
||||
set({ summary })
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
openModal: () => set({ open: true }),
|
||||
close: () => set({ open: false }),
|
||||
markSeen: async (bookId) => {
|
||||
await ipcCall(() => window.electronAPI.cockpit.markSeen(bookId))
|
||||
},
|
||||
shouldShowOnOpen: async (bookId) =>
|
||||
ipcCall(() => window.electronAPI.cockpit.shouldShowOnOpen(bookId)),
|
||||
openBridgeForChapter: (chapterId) => set({ bridgeChapterId: chapterId, open: false }),
|
||||
clearBridgeChapter: () => set({ bridgeChapterId: null })
|
||||
}))
|
||||
@@ -0,0 +1,97 @@
|
||||
import { create } from 'zustand'
|
||||
import type {
|
||||
CreateKnowledgeInput,
|
||||
KnowledgeEntry,
|
||||
KnowledgeStatus,
|
||||
KnowledgeType
|
||||
} from '@shared/types'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
|
||||
export type KnowledgeTab = 'pending' | 'all' | 'foreshadow'
|
||||
|
||||
interface KnowledgeState {
|
||||
entries: KnowledgeEntry[]
|
||||
stats: { buried: number; resolved: number; forgotten: number }
|
||||
tab: KnowledgeTab
|
||||
forgottenOnly: boolean
|
||||
editorOpen: boolean
|
||||
editingId: string | null
|
||||
load: (bookId: string) => Promise<void>
|
||||
setTab: (tab: KnowledgeTab) => void
|
||||
openForgottenFilter: () => void
|
||||
openEditor: (id?: string | null) => void
|
||||
closeEditor: () => void
|
||||
create: (bookId: string, input: CreateKnowledgeInput) => Promise<KnowledgeEntry>
|
||||
update: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
patch: Partial<CreateKnowledgeInput & { status: KnowledgeStatus }>
|
||||
) => Promise<KnowledgeEntry>
|
||||
deleteEntry: (bookId: string, id: string) => Promise<void>
|
||||
approve: (bookId: string, id: string) => Promise<void>
|
||||
reject: (bookId: string, id: string) => Promise<void>
|
||||
batchApprove: (bookId: string, ids: string[]) => Promise<void>
|
||||
filteredEntries: () => KnowledgeEntry[]
|
||||
}
|
||||
|
||||
export const useKnowledgeStore = create<KnowledgeState>((set, get) => ({
|
||||
entries: [],
|
||||
stats: { buried: 0, resolved: 0, forgotten: 0 },
|
||||
tab: 'all',
|
||||
forgottenOnly: false,
|
||||
editorOpen: false,
|
||||
editingId: null,
|
||||
load: async (bookId) => {
|
||||
const [entries, stats] = await Promise.all([
|
||||
ipcCall(() => window.electronAPI.knowledge.list(bookId)),
|
||||
ipcCall(() => window.electronAPI.knowledge.stats(bookId))
|
||||
])
|
||||
set({ entries, stats })
|
||||
},
|
||||
setTab: (tab) => set({ tab, forgottenOnly: tab === 'foreshadow' ? get().forgottenOnly : false }),
|
||||
openForgottenFilter: () => {
|
||||
useAppStore.getState().setRightPanel('knowledge')
|
||||
set({ tab: 'foreshadow', forgottenOnly: true })
|
||||
},
|
||||
openEditor: (id = null) => set({ editorOpen: true, editingId: id }),
|
||||
closeEditor: () => set({ editorOpen: false, editingId: null }),
|
||||
create: async (bookId, input) => {
|
||||
const entry = await ipcCall(() => window.electronAPI.knowledge.create(bookId, input))
|
||||
await get().load(bookId)
|
||||
return entry
|
||||
},
|
||||
update: async (bookId, id, patch) => {
|
||||
const entry = await ipcCall(() => window.electronAPI.knowledge.update(bookId, id, patch))
|
||||
await get().load(bookId)
|
||||
return entry
|
||||
},
|
||||
deleteEntry: async (bookId, id) => {
|
||||
await ipcCall(() => window.electronAPI.knowledge.delete(bookId, id))
|
||||
await get().load(bookId)
|
||||
},
|
||||
approve: async (bookId, id) => {
|
||||
await ipcCall(() => window.electronAPI.knowledge.approve(bookId, id))
|
||||
await get().load(bookId)
|
||||
},
|
||||
reject: async (bookId, id) => {
|
||||
await ipcCall(() => window.electronAPI.knowledge.reject(bookId, id))
|
||||
await get().load(bookId)
|
||||
},
|
||||
batchApprove: async (bookId, ids) => {
|
||||
await ipcCall(() => window.electronAPI.knowledge.batchApprove(bookId, ids))
|
||||
await get().load(bookId)
|
||||
},
|
||||
filteredEntries: () => {
|
||||
const { entries, tab, forgottenOnly } = get()
|
||||
let list = entries
|
||||
if (tab === 'pending') list = list.filter((e) => e.status === 'pending')
|
||||
else if (tab === 'foreshadow') {
|
||||
list = list.filter((e) => e.type === 'foreshadow')
|
||||
if (forgottenOnly) {
|
||||
// forgotten filter applied client-side via reload with filter in panel
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
}))
|
||||
@@ -17,6 +17,8 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
theme: 'default',
|
||||
language: 'zh-CN',
|
||||
dailyWordGoal: 0,
|
||||
updateSchedule: 'none',
|
||||
stockBufferThreshold: 3,
|
||||
shortcuts: {} as Record<ActionId, string>,
|
||||
aiConfig: { ...DEFAULT_AI_CONFIG },
|
||||
namingIgnoreWords: [] as string[],
|
||||
|
||||
Reference in New Issue
Block a user