Phase 3: LLM grammar checkpoint
Backend (internal/llm): backend-agnostic LLMClient interface + factory
with vLLM (OpenAI-compat) and Ollama (native) clients, each Complete +
Stream. prompts.go holds the checkpoint and Ask Petal templates;
checkpoint.go salvages JSON from model output (brace-matched), enforces a
per-doc 30s RateLimiter, and truncates the doc to a latency cap.
internal/suggestions: POST /api/docs/:id/check runs a checkpoint and
replaces the doc's pending suggestions in one tx (accepted/rejected kept
as history); GET /api/docs/:id/suggestions lists pending;
POST /api/suggestions/:id/{accept,dismiss} resolves one. Throttled checks
return the current set rather than erroring.
Frontend: useCheckpoint (4s debounce, loads existing on open, stale-guard
tokens); SuggestionHighlight renders ProseMirror decorations re-anchored
by the `original` string on every doc change (not stored marks), with
precise textblock-offset→PM-position mapping; SuggestionCard shows the
type tag + diff + explanation and applies the replacement in-editor on
accept; breathing rose checkpoint dot in the StatusBar; fade-float +
breathe animations.
Tests: llm parse/rate-limit/truncate; suggestions full flow + rate-limit
over httptest with a stub client. Smoke-tested end-to-end against a fake
vLLM endpoint (anchoring verified) and the LLM-unreachable 502 path.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { api, type DocSummary, type Document } from './api/client'
|
||||
import { api, type DocSummary, type Document, type Suggestion } from './api/client'
|
||||
import { useAutoSave } from './hooks/useAutoSave'
|
||||
import { useCheckpoint } from './hooks/useCheckpoint'
|
||||
import { DocList } from './components/DocList/DocList'
|
||||
import { EditorCore, type EditorChange } from './components/Editor/EditorCore'
|
||||
import { StatusBar } from './components/StatusBar/StatusBar'
|
||||
@@ -13,6 +14,12 @@ export default function App() {
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
const { status, schedule, saveNow } = useAutoSave(currentDoc?.id ?? null)
|
||||
const {
|
||||
suggestions,
|
||||
checking,
|
||||
schedule: scheduleCheckpoint,
|
||||
removeSuggestion,
|
||||
} = useCheckpoint(currentDoc?.id ?? null)
|
||||
|
||||
// Patch a summary in the sidebar list (optimistic title / word-count updates).
|
||||
const patchSummary = useCallback((id: string, patch: Partial<DocSummary>) => {
|
||||
@@ -106,9 +113,36 @@ export default function App() {
|
||||
if (currentDoc) {
|
||||
patchSummary(currentDoc.id, { word_count: change.word_count })
|
||||
schedule(change)
|
||||
scheduleCheckpoint()
|
||||
}
|
||||
},
|
||||
[currentDoc, patchSummary, schedule],
|
||||
[currentDoc, patchSummary, schedule, scheduleCheckpoint],
|
||||
)
|
||||
|
||||
// Accept applies the replacement in the editor (handled in EditorCore) and
|
||||
// marks the suggestion accepted; dismiss just rejects it. Both drop it locally.
|
||||
const handleAccept = useCallback(
|
||||
async (s: Suggestion) => {
|
||||
removeSuggestion(s.id)
|
||||
try {
|
||||
await api.acceptSuggestion(s.id)
|
||||
} catch (err) {
|
||||
console.error('accept failed', err)
|
||||
}
|
||||
},
|
||||
[removeSuggestion],
|
||||
)
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
async (s: Suggestion) => {
|
||||
removeSuggestion(s.id)
|
||||
try {
|
||||
await api.dismissSuggestion(s.id)
|
||||
} catch (err) {
|
||||
console.error('dismiss failed', err)
|
||||
}
|
||||
},
|
||||
[removeSuggestion],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -148,10 +182,13 @@ export default function App() {
|
||||
docId={currentDoc.id}
|
||||
initialContent={currentDoc.content}
|
||||
onChange={handleEditorChange}
|
||||
suggestions={suggestions}
|
||||
onAccept={handleAccept}
|
||||
onDismiss={handleDismiss}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBar wordCount={wordCount} saveStatus={status} />
|
||||
<StatusBar wordCount={wordCount} saveStatus={status} checking={checking} />
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user