feat: ship v0.8.0 with writing logs and AI knowledge extraction

Deliver P5.2 writing day logs, cockpit heatmap, streak tracking, and chapter knowledge auto-extraction with merge review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 18:26:29 +08:00
parent f331ddae86
commit a39e06ff07
51 changed files with 1436 additions and 89 deletions
@@ -29,8 +29,14 @@ export function KnowledgeEditorDialog({
const entries = useKnowledgeStore((s) => s.entries)
const create = useKnowledgeStore((s) => s.create)
const update = useKnowledgeStore((s) => s.update)
const approveMerge = useKnowledgeStore((s) => s.approveMerge)
const saveMergeAsNew = useKnowledgeStore((s) => s.saveMergeAsNew)
const existing = entryId ? entries.find((e) => e.id === entryId) : null
const mergeTarget = existing?.mergeTargetId
? entries.find((e) => e.id === existing.mergeTargetId)
: undefined
const isMergeMode = Boolean(existing?.mergeTargetId && mergeTarget)
const [type, setType] = useState<KnowledgeType>('fact')
const [title, setTitle] = useState('')
@@ -179,6 +185,30 @@ export function KnowledgeEditorDialog({
)}
</div>
<div className="modal-footer">
{isMergeMode && bookId && existing && (
<>
<button
type="button"
className="btn btn-primary"
data-testid="knowledge-approve-merge"
onClick={() => {
void approveMerge(bookId, existing.id).then(onClose)
}}
>
{t('knowledge.approveMerge')}
</button>
<button
type="button"
className="btn"
data-testid="knowledge-save-as-new"
onClick={() => {
void saveMergeAsNew(bookId, existing.id).then(onClose)
}}
>
{t('knowledge.saveAsNew')}
</button>
</>
)}
<button type="button" className="btn" onClick={onClose}>
{t('dialog.cancel')}
</button>
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useKnowledgeStore } from '@renderer/stores/useKnowledgeStore'
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
import { ipcCall } from '@renderer/lib/ipc-client'
import { NamingCheckModal } from '@renderer/components/ai/NamingCheckModal'
import {
@@ -12,6 +13,8 @@ import {
export function KnowledgePanel(): React.JSX.Element {
const { t } = useTranslation()
const bookId = useBookStore((s) => s.currentBookId)
const selectedChapterId = useBookStore((s) => s.selectedChapterId)
const extractThreshold = useSettingsStore((s) => s.knowledgeExtractConfidenceThreshold)
const {
entries,
stats,
@@ -25,8 +28,11 @@ export function KnowledgePanel(): React.JSX.Element {
approve,
reject,
deleteEntry,
batchApprove
batchApprove,
extractChapter,
batchApproveHighConfidence
} = useKnowledgeStore()
const [extracting, setExtracting] = useState(false)
const [namingOpen, setNamingOpen] = useState(false)
const [selected, setSelected] = useState<Set<string>>(new Set())
const [pendingCount, setPendingCount] = useState(0)
@@ -71,6 +77,30 @@ export function KnowledgePanel(): React.JSX.Element {
>
{t('naming.open')}
</button>
{tab === 'pending' && bookId && (
<button
type="button"
className="btn"
data-testid="knowledge-batch-high-confidence"
onClick={() => void batchApproveHighConfidence(bookId, extractThreshold)}
>
{t('knowledge.batchHighConfidence')}
</button>
)}
{bookId && selectedChapterId && (
<button
type="button"
className="btn"
data-testid="knowledge-extract-current"
disabled={extracting}
onClick={() => {
setExtracting(true)
void extractChapter(bookId, selectedChapterId).finally(() => setExtracting(false))
}}
>
{extracting ? t('knowledge.extracting') : t('knowledge.extractFromCurrent')}
</button>
)}
</div>
<div className="knowledge-tabs">
<button
@@ -134,10 +164,29 @@ export function KnowledgePanel(): React.JSX.Element {
/>
)}
<div className="kb-item-main">
<div className="kb-item-title">{entry.title}</div>
<div className="kb-item-title">
{entry.title}
{entry.extractionConfidence != null && (
<span className="kb-confidence-badge">
{Math.round(entry.extractionConfidence * 100)}%
</span>
)}
</div>
<div className="kb-item-meta">
{t(`knowledge.type.${entry.type}`)} · {t(`knowledge.status.${entry.status}`)}
</div>
{entry.mergeTargetId && (
<div
className="kb-merge-hint"
data-testid={`knowledge-merge-review-${entry.id}`}
>
{t('knowledge.mergeSuggest', {
title:
entries.find((e) => e.id === entry.mergeTargetId)?.title ??
entry.mergeTargetId
})}
</div>
)}
{entry.content && <div className="kb-item-content">{entry.content}</div>}
</div>
<div className="kb-item-actions">