Skip to content

Commit 7d5e94d

Browse files
committed
feat: reconstructed-resume view with inline editing + rewrite (#57–59)
Replace the flat per-bullet feedback wall with a faithful reconstructed- resume view as the primary post-parse surface, then add inline editing of contact fields and role headers, and re-attach the per-bullet rewrite affordance — completing the three-part pivot (A/B/C). A — read-only view (#57): - Add ReconstructedResume.tsx: rollup strip → contact → roles+bullets (flagged inline, passing plain) → education → skills - Add ReconstructedRole.tsx: RoleEntry (header + bullet list) and ResumeBulletRow, decomposed to keep the container under ~200 LOC - Remove PerBulletFeedback.tsx and RoleGroup.tsx (replaced) - Move needsAttention() from PerBulletFeedback into group-bullets.ts (library predicate, not UI) - Wire ReconstructedResume into Result.tsx ParsedCard, replacing the ContactCard + PerBulletFeedback composition B — inline editing (#58): - Add EditableField.tsx (ui/ primitive): pencil-icon affordance, read/ edit mode, Enter/Escape/blur commit, semantic tokens only - Add useEditableParse.ts hook: in-memory ContactOverrides + ExperienceFieldOverrides, setters, no raw useState in feature code - Extend ContactCard with optional overrides + onFieldChange props; read-only rendering unchanged when props absent C — per-bullet rewrite (#59): - Re-home the existing WebGPU/Qwen2-1.5B rewrite affordance onto flagged bullets in ReconstructedRole, preserving WebGPU gating, lazy-load discipline, and CTA-hidden-on-unsupported behavior Resolves #57 Resolves #58 Resolves #59
1 parent 92c1a93 commit 7d5e94d

9 files changed

Lines changed: 918 additions & 246 deletions

File tree

src/components/Result.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ import { PdfPreview } from "./PdfPreview";
88
import { ScoreRing } from "./features/ScoreRing.tsx";
99
import { VerdictHeader } from "./features/VerdictHeader.tsx";
1010
import type { VerdictDimension } from "./features/VerdictHeader.tsx";
11-
import { ContactCard } from "./features/ContactCard.tsx";
1211
import { Card } from "./shared/Card.tsx";
1312
import { FeedbackControl } from "./features/FeedbackControl.tsx";
14-
import { PerBulletFeedback } from "./features/PerBulletFeedback.tsx";
13+
import { ReconstructedResume } from "./features/ReconstructedResume.tsx";
1514
import {
1615
scoreBandTextClass,
1716
scoreBandBgClass,
@@ -98,8 +97,7 @@ function ParsedCard({
9897
</header>
9998

10099
<AtsScoreReadout score={score} />
101-
<ContactCard result={result} />
102-
<PerBulletFeedback bullets={score.bullets} experiences={result.parsed.experience} />
100+
<ReconstructedResume result={result} score={score} />
103101

104102
{/* Evidence — how a generic extractor read this PDF. Reference
105103
material, so it sits below the score and per-bullet findings.

src/components/features/ContactCard.tsx

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,122 @@
77
* Detected fields show the value with a success chip; undetected fields
88
* show a warning chip with a "not detected" label. Always renders all 5
99
* fields so the reader can spot gaps at a glance.
10+
*
11+
* Edit mode (#58): when `overrides` and `onFieldChange` are provided, each
12+
* field chip gains an inline EditableField affordance. Edited values replace
13+
* the parser-detected value in the display (in memory only; lost on reset).
14+
* A cleared field reverts to the "not detected" chip state.
1015
*/
1116

1217
import type { CascadeResult } from "../../lib/heuristics/types.ts";
1318
import { buildContactFields } from "../../lib/contact.ts";
1419
import { Chip } from "../ui/Chip.tsx";
1520
import { Card } from "../shared/Card.tsx";
21+
import { EditableField } from "../ui/EditableField.tsx";
22+
import type { ContactOverrides } from "../../hooks/useEditableParse.ts";
1623

1724
interface ContactCardProps {
1825
result: CascadeResult;
26+
/** In-memory overrides for contact fields. When provided, each field gains
27+
* an inline edit affordance. */
28+
overrides?: ContactOverrides;
29+
/** Called when the user commits an edit on a contact field. */
30+
onFieldChange?: (key: keyof ContactOverrides, newValue: string) => void;
1931
}
2032

21-
export function ContactCard({ result }: ContactCardProps) {
33+
/** Map from ContactOverrides key → display label. */
34+
const FIELD_LABELS: Record<keyof ContactOverrides, string> = {
35+
full_name: "Name",
36+
email: "Email",
37+
phone: "Phone",
38+
linkedin_url: "LinkedIn",
39+
location: "Location",
40+
};
41+
42+
/** Map from ContactDisplayField.key → ContactOverrides key (only the 5 editable ones). */
43+
const KEY_MAP: Record<string, keyof ContactOverrides> = {
44+
full_name: "full_name",
45+
email: "email",
46+
phone: "phone",
47+
linkedin_url: "linkedin_url",
48+
location: "location",
49+
};
50+
51+
export function ContactCard({
52+
result,
53+
overrides,
54+
onFieldChange,
55+
}: ContactCardProps) {
2256
const fields = buildContactFields(result);
23-
const detectedCount = fields.filter((f) => !f.gated).length;
57+
const editable = overrides !== undefined && onFieldChange !== undefined;
58+
59+
// Apply in-memory overrides: a non-empty override replaces the parsed value;
60+
// an empty string means "user cleared it" → treat as absent.
61+
const displayFields = fields.map((field) => {
62+
const overrideKey = KEY_MAP[field.key];
63+
if (!editable || overrideKey === undefined) return field;
64+
const ov = overrides[overrideKey];
65+
if (ov === undefined) return field; // no override yet
66+
if (ov === "") {
67+
// User cleared → show as absent.
68+
return { ...field, value: "", gated: true, reason: "absent" as const };
69+
}
70+
// User set a value → show as detected.
71+
return { ...field, value: ov, gated: false, reason: undefined };
72+
});
73+
74+
const detectedCount = displayFields.filter((f) => !f.gated).length;
2475

2576
return (
2677
<Card id="contact" className="scroll-mt-6">
2778
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-content-muted">
2879
Contact — {detectedCount} of 5 detected
2980
</h2>
3081
<div className="flex flex-wrap gap-2">
31-
{fields.map((field) =>
32-
field.gated ? (
33-
<Chip key={field.key} tone="warning" icon="⚠">
34-
{field.label} not detected
35-
{field.reason === "low_confidence" && " (low confidence)"}
36-
</Chip>
37-
) : (
38-
<Chip key={field.key} tone="success" icon="✓">
39-
{field.value}
40-
</Chip>
41-
),
42-
)}
82+
{displayFields.map((field) => {
83+
const overrideKey = KEY_MAP[field.key] as
84+
| keyof ContactOverrides
85+
| undefined;
86+
87+
if (!editable || overrideKey === undefined) {
88+
// Read-only rendering (no edit hooks provided).
89+
return field.gated ? (
90+
<Chip key={field.key} tone="warning" icon="⚠">
91+
{field.label} not detected
92+
{field.reason === "low_confidence" && " (low confidence)"}
93+
</Chip>
94+
) : (
95+
<Chip key={field.key} tone="success" icon="✓">
96+
{field.value}
97+
</Chip>
98+
);
99+
}
100+
101+
// Editable chip: wraps value in EditableField inside a chip-shaped shell.
102+
const fieldLabel = FIELD_LABELS[overrideKey];
103+
const currentValue = field.gated ? "" : field.value;
104+
105+
return (
106+
<span
107+
key={field.key}
108+
className={[
109+
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs",
110+
field.gated
111+
? "bg-feedback-warning-bg text-feedback-warning-text"
112+
: "bg-feedback-success-bg text-feedback-success-text",
113+
].join(" ")}
114+
>
115+
<span aria-hidden="true">{field.gated ? "⚠" : "✓"}</span>
116+
<EditableField
117+
value={currentValue || undefined}
118+
placeholder={`${field.label} not detected`}
119+
label={fieldLabel}
120+
textSize="xs"
121+
onCommit={(v) => onFieldChange(overrideKey, v)}
122+
/>
123+
</span>
124+
);
125+
})}
43126
</div>
44127
</Card>
45128
);

src/components/features/PerBulletFeedback.tsx

Lines changed: 0 additions & 151 deletions
This file was deleted.

0 commit comments

Comments
 (0)