feat: ship v1.0.0 with character graph and writing analytics
Add P7 Cytoscape relationship graph with setting CRUD, P9 cockpit analytics driven by writing_sessions, chapter POV selection, and full test coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import { PomodoroDailyRepository } from '../../db/repositories/pomodoro-daily.repo'
|
||||
import { WritingSessionRepository } from '../../db/repositories/writing-session.repo'
|
||||
import { WritingAnalyticsService } from '../../services/writing-analytics.service'
|
||||
import type { BookRegistryService } from '../../services/book-registry'
|
||||
import type { WritingLogService } from '../../services/writing-log.service'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerAnalyticsHandlers(
|
||||
registry: BookRegistryService,
|
||||
writingLogs: WritingLogService,
|
||||
sessionRepo: WritingSessionRepository,
|
||||
pomodoroDaily: PomodoroDailyRepository
|
||||
): void {
|
||||
const svc = new WritingAnalyticsService(writingLogs, sessionRepo, pomodoroDaily, registry)
|
||||
|
||||
ipcMain.handle(IPC.ANALYTICS_SUMMARY, (_e, { bookId }: { bookId: string }) =>
|
||||
wrap(() => svc.getSummary(bookId))
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { scheduleChapterExtraction } from '../../services/knowledge-extraction-r
|
||||
import { trackerFor } from '../../services/chapter-writing-tracker.service'
|
||||
import { recordKnowledgeInjection } from '../../services/knowledge-injection-runner'
|
||||
import type { WritingLogService } from '../../services/writing-log.service'
|
||||
import type { WritingSessionService } from '../../services/writing-session.service'
|
||||
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
|
||||
import { wrap } from '../result'
|
||||
|
||||
@@ -38,7 +39,8 @@ export function registerAutoHandlers(
|
||||
registry: BookRegistryService,
|
||||
settings: GlobalSettingsService,
|
||||
aiClient: AiClientService,
|
||||
writingLogs: WritingLogService
|
||||
writingLogs: WritingLogService,
|
||||
writingSessions: WritingSessionService
|
||||
): void {
|
||||
|
||||
ipcMain.handle(IPC.AUTO_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
|
||||
@@ -217,7 +219,10 @@ export function registerAutoHandlers(
|
||||
wrap(() => {
|
||||
const result = serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title)
|
||||
const ch = registry.getChapterRepo(bookId).get(result.chapterId)!
|
||||
trackerFor(registry, bookId, writingLogs).supplementOnFinish(result.chapterId, ch.wordCount)
|
||||
trackerFor(registry, bookId, writingLogs, writingSessions).supplementOnFinish(
|
||||
result.chapterId,
|
||||
ch.wordCount
|
||||
)
|
||||
scheduleChapterExtraction(registry, bookId, result.chapterId, aiClient, settings)
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -2,9 +2,15 @@ import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { BookMeta, CreateBookParams } from '../../../shared/types'
|
||||
import { BookRegistryService } from '../../services/book-registry'
|
||||
import type { WritingSessionService } from '../../services/writing-session.service'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerBookHandlers(registry: BookRegistryService): void {
|
||||
let previousBookId: string | null = null
|
||||
|
||||
export function registerBookHandlers(
|
||||
registry: BookRegistryService,
|
||||
writingSessions?: WritingSessionService
|
||||
): void {
|
||||
ipcMain.handle(IPC.BOOK_LIST, () => wrap(() => registry.list()))
|
||||
|
||||
ipcMain.handle(IPC.BOOK_CREATE, (_event, params: CreateBookParams) =>
|
||||
@@ -18,7 +24,13 @@ export function registerBookHandlers(registry: BookRegistryService): void {
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC.BOOK_OPEN, (_event, { bookId }: { bookId: string }) =>
|
||||
wrap(() => registry.open(bookId))
|
||||
wrap(() => {
|
||||
if (writingSessions && previousBookId && previousBookId !== bookId) {
|
||||
writingSessions.endActive(previousBookId)
|
||||
}
|
||||
previousBookId = bookId
|
||||
return registry.open(bookId)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -4,12 +4,14 @@ import type { ChapterStatus, PublishStatus } from '../../../shared/types'
|
||||
import { BookRegistryService } from '../../services/book-registry'
|
||||
import { trackerFor } from '../../services/chapter-writing-tracker.service'
|
||||
import type { WritingLogService } from '../../services/writing-log.service'
|
||||
import type { WritingSessionService } from '../../services/writing-session.service'
|
||||
import { ftsSync } from '../../services/fts-sync.service'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerChapterHandlers(
|
||||
registry: BookRegistryService,
|
||||
writingLogs: WritingLogService
|
||||
writingLogs: WritingLogService,
|
||||
writingSessions: WritingSessionService
|
||||
): void {
|
||||
ipcMain.handle(
|
||||
IPC.VOLUME_CREATE,
|
||||
@@ -72,7 +74,8 @@ export function registerChapterHandlers(
|
||||
title,
|
||||
content,
|
||||
status,
|
||||
cursorOffset
|
||||
cursorOffset,
|
||||
povCharacterId
|
||||
}: {
|
||||
bookId: string
|
||||
chapterId: string
|
||||
@@ -80,6 +83,7 @@ export function registerChapterHandlers(
|
||||
content?: string
|
||||
status?: ChapterStatus
|
||||
cursorOffset?: number
|
||||
povCharacterId?: string | null
|
||||
}
|
||||
) =>
|
||||
wrap(() => {
|
||||
@@ -87,7 +91,8 @@ export function registerChapterHandlers(
|
||||
title,
|
||||
content,
|
||||
status,
|
||||
cursorOffset
|
||||
cursorOffset,
|
||||
povCharacterId
|
||||
})
|
||||
ftsSync.upsert(
|
||||
registry.getDb(bookId),
|
||||
@@ -96,7 +101,10 @@ export function registerChapterHandlers(
|
||||
chapter.title,
|
||||
chapter.content
|
||||
)
|
||||
trackerFor(registry, bookId, writingLogs).recordDelta(chapterId, chapter.wordCount)
|
||||
trackerFor(registry, bookId, writingLogs, writingSessions).recordDelta(
|
||||
chapterId,
|
||||
chapter.wordCount
|
||||
)
|
||||
registry.updateMeta(bookId, { lastChapterId: chapterId })
|
||||
return chapter
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { CharacterRelationship } from '../../../shared/types'
|
||||
import { CharacterGraphService } from '../../services/character-graph.service'
|
||||
import type { BookRegistryService } from '../../services/book-registry'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerGraphHandlers(registry: BookRegistryService): void {
|
||||
const svc = () => new CharacterGraphService(registry)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.GRAPH_GET_DATA,
|
||||
(_e, { bookId, filter }: { bookId: string; filter?: 'all' | 'connected' }) =>
|
||||
wrap(() => svc().getData(bookId, filter ?? 'all'))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.GRAPH_UPSERT_RELATIONSHIP,
|
||||
(
|
||||
_e,
|
||||
{
|
||||
bookId,
|
||||
sourceId,
|
||||
relationship
|
||||
}: {
|
||||
bookId: string
|
||||
sourceId: string
|
||||
relationship: Partial<CharacterRelationship> & {
|
||||
targetId: string
|
||||
label: string
|
||||
intimacy: number
|
||||
}
|
||||
}
|
||||
) => wrap(() => svc().upsertRelationship(bookId, sourceId, relationship))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.GRAPH_DELETE_RELATIONSHIP,
|
||||
(
|
||||
_e,
|
||||
{
|
||||
bookId,
|
||||
sourceId,
|
||||
relationshipId
|
||||
}: { bookId: string; sourceId: string; relationshipId: string }
|
||||
) =>
|
||||
wrap(() => {
|
||||
svc().deleteRelationship(bookId, sourceId, relationshipId)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { scheduleChapterExtraction } from '../../services/knowledge-extraction-r
|
||||
import { trackerFor } from '../../services/chapter-writing-tracker.service'
|
||||
import { recordKnowledgeInjection } from '../../services/knowledge-injection-runner'
|
||||
import type { WritingLogService } from '../../services/writing-log.service'
|
||||
import type { WritingSessionService } from '../../services/writing-session.service'
|
||||
import { wrap } from '../result'
|
||||
|
||||
const activeInteractive = new Map<string, AbortController>()
|
||||
@@ -29,7 +30,8 @@ export function registerInteractiveHandlers(
|
||||
registry: BookRegistryService,
|
||||
settings: GlobalSettingsService,
|
||||
aiClient: AiClientService,
|
||||
writingLogs: WritingLogService
|
||||
writingLogs: WritingLogService,
|
||||
writingSessions: WritingSessionService
|
||||
): void {
|
||||
ipcMain.handle(IPC.INTERACTIVE_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
|
||||
wrap(() => checkInteractiveGate(registry.getDb(bookId)))
|
||||
@@ -208,7 +210,10 @@ export function registerInteractiveHandlers(
|
||||
wrap(() => {
|
||||
const result = serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title)
|
||||
const ch = registry.getChapterRepo(bookId).get(result.chapterId)!
|
||||
trackerFor(registry, bookId, writingLogs).supplementOnFinish(result.chapterId, ch.wordCount)
|
||||
trackerFor(registry, bookId, writingLogs, writingSessions).supplementOnFinish(
|
||||
result.chapterId,
|
||||
ch.wordCount
|
||||
)
|
||||
scheduleChapterExtraction(registry, bookId, result.chapterId, aiClient, settings)
|
||||
return result
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user