feat: show diff view for AI CV rewrites
The master-CV rewrite preview replaced text without showing what changed -- the teardown flagged this as the biggest unmanaged AI risk (a rewrite silently upgrading "assisted with migration" to "led migration" was invisible). Add a "Show changes" toggle on the rewrite preview panel that renders a word-level diff (before = current master text or the targeted section's stored content, after = the AI's rewrite) instead of the flat replacement text. Defaults to off: an existing test proved diff-by-default breaks the familiar plain-text read (word-fragmented spans aren't matchable as one block), and it's a better UX default regardless -- read normally, opt into the diff when you want the trust signal. Uses the `diff` package (word-level diffWords) rather than hand-rolled LCS; no existing dependency covers this, and it's a solved problem.
This commit is contained in:
@@ -13,6 +13,7 @@ import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
|
||||
import AuthStatusCard from "../components/AuthStatusCard";
|
||||
import EmailProviderConnections from "../components/EmailProviderConnections";
|
||||
import CropImageDialog from "../components/CropImageDialog";
|
||||
import TextDiff from "../components/TextDiff";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import {
|
||||
@@ -256,6 +257,7 @@ export default function ProfilePage() {
|
||||
const [cvLanguage, setCvLanguage] = useState<CvBuilderLanguage>("English");
|
||||
const [selectedRewriteJobId, setSelectedRewriteJobId] = useState<string>("");
|
||||
const [rewritePreview, setRewritePreview] = useState<CvBuilderPreview | null>(null);
|
||||
const [showRewriteDiff, setShowRewriteDiff] = useState(false);
|
||||
const [rewritePreviewTemplate, setRewritePreviewTemplate] = useState<RewriteTemplateOption | null>(null);
|
||||
const [pdfCarousel, setPdfCarousel] = useState<PdfCarouselItem[]>([]);
|
||||
const [activePdfIndex, setActivePdfIndex] = useState(0);
|
||||
@@ -372,6 +374,11 @@ export default function ProfilePage() {
|
||||
const selectedRewriteTemplate = REWRITE_TEMPLATES.find((option) => option.id === cvSectionStyle) ?? REWRITE_TEMPLATES[0];
|
||||
const selectedRewriteJob = savedJobs.find((job) => String(job.id) === selectedRewriteJobId) ?? null;
|
||||
const rewriteReady = Boolean(rewritePreview?.html && rewritePreview.fullText.trim());
|
||||
// What the rewrite is replacing, so the preview can show a diff instead of silently swapping
|
||||
// text out from under the user (career-workspace-implementation-roadmap.md Phase F5).
|
||||
const rewriteBeforeText = rewritePreview?.sectionName
|
||||
? structuredCv.sections.find((section) => section.name === rewritePreview.sectionName)?.content ?? ""
|
||||
: profileCvText;
|
||||
const activePdfItem = pdfCarousel[activePdfIndex] ?? null;
|
||||
|
||||
const releasePdfCarousel = useCallback((items: PdfCarouselItem[]) => {
|
||||
@@ -1180,13 +1187,28 @@ export default function ProfilePage() {
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", xl: "0.9fr 1.1fr" }, gap: 1.5 }}>
|
||||
<Paper sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 1 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 1, flexWrap: "wrap" }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{rewritePreview?.sectionName || "Full rewritten CV text"}</Typography>
|
||||
{rewriteReady ? <Chip size="small" color="success" label={`${(rewritePreview?.fullText || "").trim().split(/\s+/).filter(Boolean).length} words`} /> : null}
|
||||
<Box sx={{ display: "flex", gap: 0.75, alignItems: "center" }}>
|
||||
{rewriteReady ? (
|
||||
<Chip
|
||||
size="small"
|
||||
variant={showRewriteDiff ? "filled" : "outlined"}
|
||||
color={showRewriteDiff ? "primary" : "default"}
|
||||
label="Show changes"
|
||||
onClick={() => setShowRewriteDiff((current) => !current)}
|
||||
/>
|
||||
) : null}
|
||||
{rewriteReady ? <Chip size="small" color="success" label={`${(rewritePreview?.fullText || "").trim().split(/\s+/).filter(Boolean).length} words`} /> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ minHeight: 220, maxHeight: 520, overflow: "auto", borderRadius: 2.5, backgroundColor: "background.default", border: "1px dashed", borderColor: "divider", p: 1.5 }}>
|
||||
{rewriteReady ? (
|
||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>{rewritePreview?.sectionName ? rewritePreview?.rewrittenText : rewritePreview?.fullText}</Typography>
|
||||
showRewriteDiff ? (
|
||||
<TextDiff before={rewriteBeforeText} after={rewritePreview?.sectionName ? rewritePreview?.rewrittenText ?? "" : rewritePreview?.fullText ?? ""} />
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>{rewritePreview?.sectionName ? rewritePreview?.rewrittenText : rewritePreview?.fullText}</Typography>
|
||||
)
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>Choose a template and generate a live preview. The builder will show rewritten content here and render the PDF layout beside it.</Typography>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user