Phase 8: version history + export (trust foundation)
Version history: new document_versions table (migration 0003) holding full-body snapshots that cascade with the doc. Throttled auto-snapshots on save (>=3min apart, max 40/doc, pruned), explicit manual restore points, and a pre_restore safety copy taken before each restore so restoring is itself undoable. Endpoints under /api/docs/:id/versions, all owner-scoped. Empties and bare renames never snapshot. Export: pure-Go Tiptap-JSON -> Markdown / HTML / plain-text / docx (no cgo/pandoc, single-binary intact), CJK-safe with RFC 5987 filenames. docx is a hand-built OOXML zip. PDF is handled client-side via the browser print dialog + an @media print stylesheet so CJK renders with the reader's own fonts. Frontend: ExportMenu (downloads + Print/PDF) and HistoryPanel (snapshot list, preview, restore) wired into the title row; bilingual zh-first to match chrome. Restore remounts the editor via editorEpoch. Stop saving empty docs: blank Untitled drafts now reuse-on-create and self-discard when navigated away from (refs avoid stale closures). Tests: versions_test.go, export_test.go (incl. valid-zip docx). go build/vet/test, tsc, vite build all clean; live end-to-end smoke verified snapshot/throttle/restore/export. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -47,6 +47,23 @@ export interface WordInfo {
|
||||
|
||||
export type SuggestionType = 'grammar' | 'phrasing' | 'idiom' | 'clarity' | 'voice'
|
||||
|
||||
// A point-in-time snapshot of a document. List responses omit the heavy
|
||||
// content/content_text fields; they arrive only on getVersion (preview/restore).
|
||||
export interface DocumentVersion {
|
||||
id: string
|
||||
doc_id: string
|
||||
title: string
|
||||
content?: string
|
||||
content_text?: string
|
||||
word_count: number
|
||||
kind: 'auto' | 'manual' | 'pre_restore'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// Downloadable export formats. PDF is handled client-side via the browser's
|
||||
// print dialog (CJK-safe, no server-side font embedding).
|
||||
export type ExportFormat = 'md' | 'html' | 'txt' | 'docx'
|
||||
|
||||
// A single LLM-proposed edit. `original` is the source of truth for placement —
|
||||
// the editor re-anchors by matching this string in the live document (spec
|
||||
// Note #6); from_pos/to_pos are server-side advisory only. `replacement` is
|
||||
@@ -100,6 +117,23 @@ export const api = {
|
||||
dismissSuggestion: (id: string) =>
|
||||
req<void>(`/suggestions/${id}/dismiss`, { method: 'POST' }),
|
||||
|
||||
// Version history. listVersions returns metadata only (no bodies); getVersion
|
||||
// loads one full snapshot for preview; snapshotDoc takes an explicit restore
|
||||
// point; restoreVersion copies a snapshot back onto the live doc (capturing a
|
||||
// pre_restore safety copy server-side first) and returns the restored doc.
|
||||
listVersions: (id: string) => req<DocumentVersion[]>(`/docs/${id}/versions`),
|
||||
getVersion: (id: string, vid: string) =>
|
||||
req<DocumentVersion>(`/docs/${id}/versions/${vid}`),
|
||||
snapshotDoc: (id: string) =>
|
||||
req<DocumentVersion>(`/docs/${id}/versions`, { method: 'POST' }),
|
||||
restoreVersion: (id: string, vid: string) =>
|
||||
req<Document>(`/docs/${id}/versions/${vid}/restore`, { method: 'POST' }),
|
||||
|
||||
// Download URL for an exported document (md/html/txt/docx). Used as an <a
|
||||
// href download> target so the browser handles the file save.
|
||||
exportUrl: (id: string, format: ExportFormat) =>
|
||||
`/api/docs/${id}/export?format=${format}`,
|
||||
|
||||
// Offline word lookup (definition + synonyms) for the right-click popover.
|
||||
lookupWord: (word: string) => req<WordInfo>(`/word/${encodeURIComponent(word)}`),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user