Files
petal/web/src/components/Editor/RewritePreview.tsx
prosolis 60eba25fee Phase 9: ESL superpowers — Chinese gloss + tone-rewrite
Inline Chinese gloss (offline) and a "say it more naturally" / tone-rewrite,
the two ESL features for the Mandarin-speaking writer.

Gloss: embedded English→Chinese dictionary (gloss.json.gz, 57k common words
built from ECDICT via scripts/build_gloss.py). lexicon gains Gloss()/Result.Gloss
and a lightweight GET /api/gloss/{word}; the right-click WordCard leads with the
中文; GlossTip shows it on a 350ms hover (reuses wordAt, so CJK is never glossed).
Offline + instant, works with the LLM down.

Rewrite: selecting text pops a SelectionBubble (更自然 + the tone vocabulary);
picking a style calls POST /api/docs/:id/rewrite (llm.RunRewrite, stateless,
owner-scoped) and shows a RewritePreview (original→rewrite, accept/cancel/retry).
Accept applies it in-editor.

Tests added in lexicon and suggestions. go build/vet/test, tsc, vite all clean;
live smoke vs a fake vLLM verified gloss + rewrite + 400/404/502 paths.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 00:07:26 -07:00

137 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { REWRITE_STYLES } from './SelectionBubble'
// RewritePreview shows the result of a tone-rewrite before it touches the
// document: the writer's original passage, the model's rewrite beneath it, and
// accept/cancel. It mirrors the SuggestionCard's warm, bilingual chrome. While
// the model runs it shows a breathing dot; on failure it offers a gentle retry.
export type RewriteStatus = 'loading' | 'ready' | 'error'
interface Props {
style: string // the chosen rewrite style key (for the header label)
status: RewriteStatus
original: string
rewrite: string
cardStyle: React.CSSProperties
onAccept: () => void
onCancel: () => void
onRetry: () => void
}
const CJK = "'Nunito','PingFang SC','Microsoft YaHei','Noto Sans CJK SC',sans-serif"
export function RewritePreview({
style,
status,
original,
rewrite,
cardStyle,
onAccept,
onCancel,
onRetry,
}: Props) {
const meta = REWRITE_STYLES.find((s) => s.value === style) ?? REWRITE_STYLES[0]
return (
<div
className="petal-rewrite-card absolute z-30 p-3.5 text-sm"
role="dialog"
aria-label="Rewrite preview"
onMouseDown={(e) => e.stopPropagation()}
style={{
width: 340,
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-card)',
boxShadow: 'var(--shadow-soft)',
fontFamily: CJK,
...cardStyle,
}}
>
<div className="flex items-center gap-1.5">
<span
className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-bold"
style={{ background: 'var(--color-accent)', color: 'var(--color-plum)' }}
>
<span aria-hidden>{meta.emoji}</span>
{meta.zh} · {meta.en}
</span>
<span className="text-xs font-semibold" style={{ color: 'var(--color-muted)' }}>
· Rewrite
</span>
</div>
{/* Original passage, dimmed — what's being replaced. */}
<p
className="mt-3 leading-snug"
style={{ color: 'var(--color-muted)', textDecoration: 'line-through', textDecorationColor: 'var(--color-border)' }}
>
{original}
</p>
{status === 'loading' && (
<div className="mt-3 inline-flex items-center gap-1.5" style={{ color: 'var(--color-muted)' }}>
<span
className="petal-checkpoint-dot inline-block h-2 w-2 rounded-full"
style={{ background: 'var(--color-accent)' }}
aria-hidden
/>
· Rewriting
</div>
)}
{status === 'error' && (
<div className="mt-3">
<p className="leading-snug" style={{ color: 'var(--color-muted)' }}>
· Couldnt rewrite try again
</p>
<div className="mt-2.5 flex justify-end gap-2">
<button
type="button"
onClick={onCancel}
className="rounded-full px-3 py-1 text-xs font-semibold"
style={{ background: 'var(--color-surface-alt)', color: 'var(--color-plum)' }}
>
· Cancel
</button>
<button
type="button"
onClick={onRetry}
className="rounded-full px-3 py-1 text-xs font-bold"
style={{ background: 'var(--color-accent)', color: 'var(--color-plum)' }}
>
· Retry
</button>
</div>
</div>
)}
{status === 'ready' && (
<>
<p className="mt-2 leading-snug" style={{ color: 'var(--color-plum)' }}>
{rewrite}
</p>
<div className="mt-3 flex justify-end gap-2">
<button
type="button"
onClick={onCancel}
className="rounded-full px-3 py-1 text-xs font-semibold"
style={{ background: 'var(--color-surface-alt)', color: 'var(--color-plum)' }}
>
· Cancel
</button>
<button
type="button"
onClick={onAccept}
className="rounded-full px-3 py-1 text-xs font-bold"
style={{ background: 'var(--color-accent)', color: 'var(--color-plum)' }}
>
· Use this
</button>
</div>
</>
)}
</div>
)
}