A dismissed card stays dismissed, even offline

The server already suppressed every span she had accepted or dismissed, on
both the LLM reconcile and the mechanics pass, with tests either side. What
had no memory was the half that never asks it: item 3b's rule pack renders
250 ms after a keystroke with no network, and its record of "she already
answered this" was a set cleared on every document switch and added to only
for cards dismissed while still provisional.

So dismissing a persisted rule-pack card recorded nothing client-side and the
next keystroke put it straight back until the server's reply removed it again;
and after a reload the client knew nothing at all — permanently so with the
server unreachable, which is the case the rule pack exists for.

GET /docs/{id}/settled hands over the normalized originals of the document's
actioned rows, scoped through documents because an original quotes her
sentence. The client seeds a SettledSpans from it on open and adds to it for
every card that leaves, keyed on the original alone the way the server keys
it. The load adds rather than assigns, so a dismissal made while it is in
flight survives it.

normalizeForDedup now exists in both languages, compared across a network
boundary, so the same nine cases are asserted on both sides and each test
names the other.

Also: the status-bar count — "🌸 5片花瓣待打磨 · 5 petals to polish" beside the
word count, from the packs, hidden at zero. An empty rail already says nothing
is waiting; a badge announcing it after every check is a verdict, which the
review's non-goals rule out.

Verified in Chrome at 1517x810 with the server killed: a new violation was
detected, underlined and counted with no network, while the dismissed span
stayed gone.
This commit is contained in:
prosolis
2026-07-28 07:27:49 -07:00
parent 1aa3a14030
commit b2d50e9136
15 changed files with 562 additions and 7 deletions
+30 -1
View File
@@ -19,6 +19,9 @@ interface Props {
// True when Petal can't reach its LLM helper — shows a gentle, reassuring note
// (the writing still saves locally, so this is awareness, not an error).
llmDown: boolean
// How many suggestions the rail is holding. Shown as a soft bilingual line
// beside the word count; hidden at zero.
suggestionCount: number
}
// Save-state labels. English except for the lapsed-session case, which is the
@@ -48,9 +51,23 @@ interface Indicator {
label: string
}
export function StatusBar({ wordCount, text, saveStatus, checking, voicing, collocating, llmDown }: Props) {
export function StatusBar({
wordCount,
text,
saveStatus,
checking,
voicing,
collocating,
llmDown,
suggestionCount,
}: Props) {
const t = usePack()
const label = saveLabel(saveStatus, t)
// The gentle counterpart of Grammarly's score: how much is waiting, never how
// well she wrote. Nothing waiting says itself — an empty rail — so a zero here
// would only be a verdict delivered after every check, which is the pressure
// this app's non-goals rule out.
const petals = suggestionCount > 0 ? t.status.petalsToPolish(suggestionCount) : null
const indicators: Indicator[] = [
{
@@ -109,6 +126,18 @@ export function StatusBar({ wordCount, text, saveStatus, checking, voicing, coll
</button>
{statsOpen && <StatsPanel text={text} wordCount={wordCount} />}
</div>
{petals && (
<>
<span aria-hidden>·</span>
{/* Native half first, as everywhere else in Petal. (The review wrote it
English-first; the pack's order is the one she reads all day.) */}
<span className="inline-flex items-center gap-1.5">
<span aria-hidden>🌸</span>
<span>{petals.native}</span>
<span style={{ opacity: 0.75 }}>· {petals.en}</span>
</span>
</>
)}
{indicators
.filter((i) => i.active)
.map((i) => (