Let the garden keep what she was given, not only what she sought

Two halves of the same idea, both read out of work Petal already
records.

Planting: an accepted collocation is a learnable chunk, so it becomes a
phrase card. The scheduler didn't need to know — a three-word chunk
climbs the ladder exactly like a looked-up word. What needed care was
deciding what *isn't* a chunk (single words are word choice; a
six-word-plus "collocation" is a rewritten sentence, and sentences make
miserable flashcards), and that the example must be the *corrected*
sentence — the stored draft still holds the phrasing she just left
behind. Re-accepting the same chunk leaves the existing card alone
rather than resetting a schedule it has been climbing. The whole thing
is best-effort: accepting an edit must never fail because a flashcard
couldn't be made.

The growth journal: kept this month beside kept the month before, the
phrasing that stuck, the patterns that faded. The queries were the easy
part; the honesty is the feature. "Stuck" needs the phrase in a *second*
document, because one document is just the edit where she left it.
"Faded" says nothing at all unless she has been writing lately —
otherwise a month away from Petal comes back to her as progress, which
is the one way this could lie. And a suggestion had to start recording
when she *decided* it, not when the model proposed it, so 0012 adds
resolved_at and backfills the old rows to their created_at.

It lives as a second tab in the garden, and it feeds the kitten: after
an accept she now sometimes hears something true of her alone, once per
line, half the time, never waited for.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 14:16:59 -07:00
parent 7b845644be
commit e9b8595456
19 changed files with 1328 additions and 5 deletions
+30 -1
View File
@@ -3,6 +3,7 @@ import { api, type VocabGrade, type VocabWord } from '../../api/client'
import { speak, speechSupported, stopSpeech } from '../../audio/speech'
import { useFocusTrap } from '../../hooks/useFocusTrap'
import { usePack, type Line } from '../../i18n'
import { JournalView } from './JournalView'
// GardenPanel is the vocabulary garden: every word the writer has looked up,
// grown into a blossom that opens further the more she remembers it, plus a
@@ -51,6 +52,7 @@ export function GardenPanel({ onClose, onOpenDoc }: Props) {
const [cursor, setCursor] = useState(0)
const [revealed, setRevealed] = useState(false)
const [expanded, setExpanded] = useState<string | null>(null)
const [tab, setTab] = useState<'garden' | 'journal'>('garden')
const panelRef = useFocusTrap<HTMLElement>()
// Read-aloud is fire-and-forget, so a word she tapped could still be speaking
@@ -155,7 +157,7 @@ export function GardenPanel({ onClose, onOpenDoc }: Props) {
<div>
<div className="text-base font-extrabold text-plum">{t.garden.titleWithFlower}</div>
<div className="text-xs" style={{ color: 'var(--color-muted)' }}>
{queue ? t.garden.reviewing : t.garden.subtitle}
{queue ? t.garden.reviewing : tab === 'journal' ? t.journal.subtitle : t.garden.subtitle}
</div>
</div>
<button
@@ -171,6 +173,31 @@ export function GardenPanel({ onClose, onOpenDoc }: Props) {
</button>
</header>
{/* The two halves of learning: words and phrasing she has collected
(garden), and how her writing has changed (journal). A review
session takes over the panel entirely — mid-flashcard is no moment
to be offered a different page. */}
{!queue && (
<div className="flex gap-1 px-4 pt-3">
{(['garden', 'journal'] as const).map((id) => (
<button
key={id}
type="button"
onClick={() => setTab(id)}
aria-pressed={tab === id}
className="flex-1 rounded-full px-3 py-1.5 text-xs font-bold"
style={{
background: tab === id ? 'var(--color-surface-alt)' : 'transparent',
border: `1px solid ${tab === id ? 'var(--color-border)' : 'transparent'}`,
color: tab === id ? 'var(--color-plum)' : 'var(--color-muted)',
}}
>
{id === 'garden' ? t.journal.tabGarden : t.journal.tabJournal}
</button>
))}
</div>
)}
{queue ? (
<ReviewSession
queue={queue}
@@ -180,6 +207,8 @@ export function GardenPanel({ onClose, onOpenDoc }: Props) {
onGrade={grade}
onQuit={() => setQueue(null)}
/>
) : tab === 'journal' ? (
<JournalView />
) : (
<GardenView
words={words}