/**
* components/CorrectionPanel.jsx
* ───────────────────────────────
* Right panel. Features:
* - Skeleton loader during processing
* - Word-level diff highlighting
* - 3D flip card: front = correction, back = refinement
* - Metric chips, confidence badge, copy, "Use as Input"
*/
import { useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import toast from 'react-hot-toast'
// ── Word diff renderer ──
function renderDiff(original, diffs) {
if (!diffs || diffs.length === 0) return {original}
const changeMap = {}
for (const d of diffs) {
if (d.original || d.corrected) changeMap[d.position] = d
}
const tokens = original.split(/(\s+)/)
let idx = 0
const nodes = []
tokens.forEach((tok, i) => {
if (/^\s+$/.test(tok)) { nodes.push(tok); return }
const ch = changeMap[idx]
if (ch) {
const cls = ch.type === 'homophone' ? 'diff-hom' : 'diff-del'
nodes.push({ch.original || tok})
if (ch.corrected) {
nodes.push(' ')
nodes.push({ch.corrected})
}
} else {
nodes.push(tok)
}
idx++
})
return <>{nodes}>
}
// ── Confidence badge ──
function ConfBadge({ value }) {
const pct = Math.round(value * 100)
const color = pct >= 85 ? 'var(--green)' : pct >= 65 ? 'var(--amber)' : 'var(--red)'
const bg = pct >= 85 ? 'var(--green-soft)' : pct >= 65 ? 'var(--amber-soft)' : 'var(--red-soft)'
return (
{pct}% confidence
)
}
// ── Skeleton ──
function Skeleton({ lines = [88, 72, 80, 55, 68] }) {
return (
{lines.map((w, i) => (
))}
)
}
// ── Copy util ──
function copyText(text) {
navigator.clipboard.writeText(text).then(() => {
toast.success('Copied to clipboard', {
style: {
background: 'var(--green-soft)', color: 'var(--green)',
border: '1px solid var(--green)',
fontFamily: "'DM Sans',sans-serif", fontSize: '13px',
},
})
})
}
// ── Card header ──
function PanelHeader({ title, tagLabel, tagClass, copyValue, extra }) {
return (
{title}
{tagLabel}
{extra}
{copyValue && (
copyText(copyValue)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
⎘ Copy
)}
)
}
// ── Empty state ──
function EmptyState() {
return (
✦
Awaiting Text Pipeline
Insert text on the left and run diagnostics to see engine evaluations.
)
}
// ── Correction face (front) ──
function CorrectionFace({ result, loading, onFlipHint }) {
const diffNode = useMemo(() => result ? renderDiff(result.original, result.diffs) : null, [result])
if (loading) return <>>
if (!result) return
return (
<>
{result.spell_fixed > 0 && (
{result.spell_fixed} spelling
)}
{result.grammar_fixed > 0 && (
{result.grammar_fixed} grammar
)}
{result.homophone_fixed > 0 && (
{result.homophone_fixed} homophone
)}
{diffNode}
✓
{result.corrected}
{onFlipHint && (
✦ Click Refine Text to synthesize a stylistic rewrite →
)}
>
)
}
// ── Refinement face (back) ──
function RefinementFace({ result, loading }) {
if (loading) return <>>
if (!result) return null
return (
<>
{
window.dispatchEvent(new CustomEvent('wr:loadText', { detail: result.refined }))
toast.success('Loaded into editor', { style: { background: 'var(--purple-soft)', color: 'var(--purple)', border: '1px solid var(--purple)', fontFamily: "'DM Sans',sans-serif", fontSize: '13px' } })
}}
whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}
>
↑ Use as Input
}
/>
{result.improvements?.length > 0 && (
{result.improvements.map((imp, i) => (
✦{imp}
))}
)}
{result.refined}
T5 Transformer Output
✦ AI-enhanced
>
)
}
// ── Main export ──
export default function CorrectionPanel({ correctionResult, refineResult, correctionLoading, refineLoading }) {
const isFlipped = Boolean(refineResult || refineLoading)
return (
{isFlipped && (
✦ Viewing refined output · card flipped 180°
)}
{isFlipped && correctionResult && (
window.dispatchEvent(new CustomEvent('wr:backToCorrection'))}
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.95 }}
>
← Back to diagnostics
)}
)
}