feat: ship v0.9.0 with pomodoro, achievements, and injection history
Add pomodoro timer with goal notifications, writing streak milestones, and knowledge injection logging with UI previews across AI writing flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,9 +4,11 @@ import type {
|
||||
CreateKnowledgeInput,
|
||||
ForeshadowStatus,
|
||||
KnowledgeEntry,
|
||||
KnowledgeInjectionLog,
|
||||
KnowledgeType
|
||||
} from '@shared/types'
|
||||
import { useBookStore } from '@renderer/stores/useBookStore'
|
||||
import { useEditStore } from '@renderer/stores/useEditStore'
|
||||
import { useKnowledgeStore } from '@renderer/stores/useKnowledgeStore'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
|
||||
@@ -26,6 +28,7 @@ export function KnowledgeEditorDialog({
|
||||
const { t } = useTranslation()
|
||||
const bookId = useBookStore((s) => s.currentBookId)
|
||||
const chapters = useBookStore((s) => s.chapters)
|
||||
const switchTarget = useEditStore((s) => s.switchTarget)
|
||||
const entries = useKnowledgeStore((s) => s.entries)
|
||||
const create = useKnowledgeStore((s) => s.create)
|
||||
const update = useKnowledgeStore((s) => s.update)
|
||||
@@ -45,6 +48,17 @@ export function KnowledgeEditorDialog({
|
||||
const [foreshadowStatus, setForeshadowStatus] = useState<ForeshadowStatus>('buried')
|
||||
const [sourceChapterId, setSourceChapterId] = useState('')
|
||||
const [approveNow, setApproveNow] = useState(false)
|
||||
const [injections, setInjections] = useState<KnowledgeInjectionLog[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !bookId || !entryId) {
|
||||
setInjections([])
|
||||
return
|
||||
}
|
||||
void ipcCall(() => window.electronAPI.knowledge.listInjections(bookId, entryId)).then(
|
||||
setInjections
|
||||
)
|
||||
}, [open, bookId, entryId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
@@ -183,6 +197,39 @@ export function KnowledgeEditorDialog({
|
||||
{t('knowledge.approveNow')}
|
||||
</label>
|
||||
)}
|
||||
{existing && injections.length > 0 && (
|
||||
<div className="knowledge-injection-history" data-testid="knowledge-injection-history">
|
||||
<div className="kb-injection-label">{t('knowledge.injectionHistory')}</div>
|
||||
{injections.map((log) => {
|
||||
const chapterTitle =
|
||||
chapters.find((c) => c.id === log.chapterId)?.title ?? t('knowledge.noChapter')
|
||||
return (
|
||||
<div key={log.id} className="kb-injection-row">
|
||||
<span className="kb-injection-date">
|
||||
{new Date(log.injectedAt).toLocaleString()}
|
||||
</span>
|
||||
<span className="kb-injection-mode">
|
||||
{t(`knowledge.injectionMode.${log.flowMode}`)}
|
||||
</span>
|
||||
{log.chapterId ? (
|
||||
<button
|
||||
type="button"
|
||||
className="kb-injection-chapter-link"
|
||||
onClick={() => {
|
||||
void switchTarget({ kind: 'chapter', id: log.chapterId! })
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
{chapterTitle}
|
||||
</button>
|
||||
) : (
|
||||
<span>{chapterTitle}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{isMergeMode && bookId && existing && (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
export function KnowledgePanel(): React.JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const bookId = useBookStore((s) => s.currentBookId)
|
||||
const chapters = useBookStore((s) => s.chapters)
|
||||
const selectedChapterId = useBookStore((s) => s.selectedChapterId)
|
||||
const extractThreshold = useSettingsStore((s) => s.knowledgeExtractConfidenceThreshold)
|
||||
const {
|
||||
@@ -32,6 +33,8 @@ export function KnowledgePanel(): React.JSX.Element {
|
||||
extractChapter,
|
||||
batchApproveHighConfidence
|
||||
} = useKnowledgeStore()
|
||||
const injectionPreviews = useKnowledgeStore((s) => s.injectionPreviews)
|
||||
const loadInjectionPreviews = useKnowledgeStore((s) => s.loadInjectionPreviews)
|
||||
const [extracting, setExtracting] = useState(false)
|
||||
const [namingOpen, setNamingOpen] = useState(false)
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
@@ -51,6 +54,14 @@ export function KnowledgePanel(): React.JSX.Element {
|
||||
)
|
||||
}, [bookId, entries])
|
||||
|
||||
useEffect(() => {
|
||||
if (!bookId || entries.length === 0) return
|
||||
void loadInjectionPreviews(
|
||||
bookId,
|
||||
entries.map((e) => e.id)
|
||||
)
|
||||
}, [bookId, entries, loadInjectionPreviews])
|
||||
|
||||
const handleBatchApprove = async (): Promise<void> => {
|
||||
if (!bookId || selected.size === 0) return
|
||||
await batchApprove(bookId, [...selected])
|
||||
@@ -188,6 +199,25 @@ export function KnowledgePanel(): React.JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
{entry.content && <div className="kb-item-content">{entry.content}</div>}
|
||||
{(injectionPreviews[entry.id]?.length ?? 0) > 0 && (
|
||||
<div
|
||||
className="kb-injection-preview"
|
||||
data-testid={`knowledge-injection-preview-${entry.id}`}
|
||||
>
|
||||
<div className="kb-injection-label">{t('knowledge.injectionPreview')}</div>
|
||||
{injectionPreviews[entry.id]!.map((log) => (
|
||||
<div key={log.id} className="kb-injection-preview-row">
|
||||
{t('knowledge.injectionEntry', {
|
||||
date: new Date(log.injectedAt).toLocaleDateString(),
|
||||
mode: t(`knowledge.injectionMode.${log.flowMode}`),
|
||||
chapter:
|
||||
chapters.find((c) => c.id === log.chapterId)?.title ??
|
||||
t('knowledge.noChapter')
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="kb-item-actions">
|
||||
{entry.status === 'pending' && bookId && (
|
||||
|
||||
Reference in New Issue
Block a user