feat: ship v0.3.0 with P2.1 editor polish and P3 AI collaboration

Add chapter reorder, search replace, inspiration capture, and AI chat with context editing, settings, offline handling, and naming checks. Fix dev black screen by keeping process.env reads in the main process only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:33:56 +08:00
parent 0b5aa146bd
commit 8ce35a1e8e
59 changed files with 3808 additions and 100 deletions
+23 -10
View File
@@ -16,10 +16,12 @@ import { useTabStore } from '@renderer/stores/useTabStore'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useEditStore } from '@renderer/stores/useEditStore'
import { SearchModal } from '@renderer/components/search/SearchModal'
import { InspirationModal } from '@renderer/components/inspiration/InspirationModal'
import { useSearchStore } from '@renderer/stores/useSearchStore'
import { useReferenceStore } from '@renderer/stores/useReferenceStore'
import { flushEditorSave } from '@renderer/components/editor/TipTapEditor'
import { insertLandmarkInEditor } from '@renderer/lib/editor-commands'
import { useAiStore } from '@renderer/stores/useAiStore'
import { ipcCall } from '@renderer/lib/ipc-client'
function AppInner(): React.JSX.Element {
@@ -48,6 +50,11 @@ function AppInner(): React.JSX.Element {
useEffect(() => {
const openSearch = (): void => useSearchStore.getState().setOpen(true)
const openLandmarks = (): void => useAppStore.getState().setLandmarksModalOpen(true)
const openInspiration = (): void => {
if (useAppStore.getState().view !== 'editor') return
if (!useBookStore.getState().currentBookId) return
useAppStore.getState().setInspirationModalOpen(true)
}
const onInsertLandmark = (e: Event): void => {
const label = (e as CustomEvent<{ label: string }>).detail?.label
if (!label) return
@@ -59,13 +66,26 @@ function AppInner(): React.JSX.Element {
window.electronAPI.bookmark.create(currentBookId, target.id, 0, label, 'todo')
)
}
const onSetAiOnline = (e: Event): void => {
const online = (e as CustomEvent<{ online: boolean }>).detail?.online
if (typeof online === 'boolean') useAiStore.setState({ online })
}
const onFlushEditor = (): void => {
void flushEditorSave()
}
window.addEventListener('bilin:e2e-open-search', openSearch)
window.addEventListener('bilin:e2e-open-landmarks', openLandmarks)
window.addEventListener('bilin:e2e-capture-inspiration', openInspiration)
window.addEventListener('bilin:e2e-insert-landmark', onInsertLandmark)
window.addEventListener('bilin:e2e-set-ai-online', onSetAiOnline)
window.addEventListener('bilin:e2e-flush-editor', onFlushEditor)
return () => {
window.removeEventListener('bilin:e2e-open-search', openSearch)
window.removeEventListener('bilin:e2e-open-landmarks', openLandmarks)
window.removeEventListener('bilin:e2e-capture-inspiration', openInspiration)
window.removeEventListener('bilin:e2e-insert-landmark', onInsertLandmark)
window.removeEventListener('bilin:e2e-set-ai-online', onSetAiOnline)
window.removeEventListener('bilin:e2e-flush-editor', onFlushEditor)
}
}, [])
@@ -123,16 +143,8 @@ function AppInner(): React.JSX.Element {
}
if (action === 'captureInspiration') {
if (view !== 'editor') return
const { currentBookId, addInspirationLocal } = useBookStore.getState()
if (!currentBookId) return
void (async () => {
const item = await ipcCall(() =>
window.electronAPI.inspiration.create(currentBookId, t('inspiration.newItem'))
)
addInspirationLocal(item)
useAppStore.getState().setSidebarPanel('inspiration')
await useEditStore.getState().switchTarget({ kind: 'inspiration', id: item.id })
})()
if (!useBookStore.getState().currentBookId) return
useAppStore.getState().setInspirationModalOpen(true)
return
}
showToast(t('feature.comingSoon'))
@@ -154,6 +166,7 @@ function AppInner(): React.JSX.Element {
</div>
{showOnboarding && <OnboardingWizard onComplete={handleOnboardingComplete} />}
<SearchModal />
<InspirationModal />
{toast && <div className="toast">{toast}</div>}
</div>
)