import React, { useCallback, useEffect, useState } from "react"; import { Alert, Box, Button, Chip, Divider, IconButton, MenuItem, Paper, Skeleton, Stack, TextField, Tooltip, Typography, } from "@mui/material"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import RestoreIcon from "@mui/icons-material/Restore"; import { getApiErrorMessage } from "../api"; import { ApplicationCv, CoverLetter, TailoringPlan, applicationAssetsApi, } from "../applicationWorkspace"; // Phase 5.4 — Application Assets sections for the workspace. // // These compose systems that already exist. CV editing, preview, PDF export, themes and version // history all live in the CV builder at /cv-builder — this section only chooses WHICH variant the // application uses and links out. The cover letter is the one thing genuinely owned here, because it // is application-specific by nature. docs/architecture/application-workspace.md. function useAsset(load: () => Promise, deps: React.DependencyList) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); // eslint-disable-next-line react-hooks/exhaustive-deps const run = useCallback(load, deps); const reload = useCallback(() => { let cancelled = false; setLoading(true); run() .then((r) => { if (!cancelled) { setData(r); setError(null); } }) .catch((err) => { if (!cancelled) setError(getApiErrorMessage(err, "Could not load this section.")); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [run]); useEffect(() => reload(), [reload]); return { data, error, loading, setData, setError, reload }; } function Shell({ title, subtitle, loading, error, children }: { title: string; subtitle?: string; loading: boolean; error: string | null; children: React.ReactNode; }) { return ( {title} {subtitle && {subtitle}} {loading ? ( {[0, 1, 2].map((i) => )} ) : error ? ( {error} ) : ( children )} ); } // ---------- CV ---------- export function ApplicationCvSection({ jobId }: { jobId: number }) { const { data, error, loading, setData, setError } = useAsset( () => applicationAssetsApi.cv(jobId), [jobId], ); const [busy, setBusy] = useState(false); const attach = async (variantId: number | null) => { setBusy(true); try { setData(await applicationAssetsApi.attachVariant(jobId, variantId)); setError(null); } catch (err) { setError(getApiErrorMessage(err, "Could not change the attached CV.")); } finally { setBusy(false); } }; const attached = data?.attachedVariantId ?? ""; return ( {(data?.availableVariants.length ?? 0) === 0 ? ( No CV variants yet. Build one in the CV builder — it starts from your master career profile, so you never retype your history. ) : ( attach(e.target.value === "" ? null : Number(e.target.value))} helperText="Changing this only re-points the application. The variant itself is untouched." > None {(data?.availableVariants ?? []).map((v) => ( {v.name} · {v.themeId} · v{v.version} ))} )} {data?.attachedVariantId ? ( {data.attachedVariantName} Theme {data.attachedThemeId} · version {data.attachedVersion} {data.attachedIsPublic ? " · public" : ""} ) : ( No CV attached to this application yet. )} {data?.hasTailoredCvText && ( This application also has legacy tailored CV text saved on it. A CV variant supersedes it. )} ); } // ---------- Tailoring ---------- export function ApplicationTailoringSection({ jobId }: { jobId: number }) { const { data, error, loading } = useAsset( () => applicationAssetsApi.tailoring(jobId), [jobId], ); return ( {data && !data.hasCareerProfile && ( Build your career profile to get experience and project suggestions. )} {data && !data.hasJobDescription && ( Paste the advert text to get keyword and requirement suggestions. )} {(data?.suggestions.length ?? 0) === 0 ? ( Nothing to suggest yet. ) : ( (data?.suggestions ?? []).map((s) => ( {s.title} {s.detail && ( {s.detail} )} {s.items.map((item) => ( ))} )) )} ); } // ---------- Cover letter ---------- const TEMPLATE = `Dear Hiring Manager, I am writing to apply for the [role] position at [company]. [One sentence on why this company, specifically.] In my current role I [the most relevant thing you have done, with a concrete outcome]. [A second example that matches what the advert asks for.] [Why you want this job, in your own words.] I would welcome the chance to talk it through. Kind regards, [Your name]`; export function ApplicationCoverLetterSection({ jobId }: { jobId: number }) { const { data, error, loading, setData, setError } = useAsset( () => applicationAssetsApi.coverLetter(jobId), [jobId], ); const [draft, setDraft] = useState(null); const [busy, setBusy] = useState(false); // The textarea is only seeded from the server until the user starts typing, so a reload never // clobbers unsaved edits. const text = draft ?? data?.text ?? ""; const dirty = draft !== null && draft !== (data?.text ?? ""); const save = async (value: string, source = "manual") => { setBusy(true); try { setData(await applicationAssetsApi.saveCoverLetter(jobId, value, source)); setDraft(null); setError(null); } catch (err) { setError(getApiErrorMessage(err, "Could not save the cover letter.")); } finally { setBusy(false); } }; const restore = async (version: number) => { setBusy(true); try { setData(await applicationAssetsApi.restoreCoverLetter(jobId, version)); setDraft(null); setError(null); } catch (err) { setError(getApiErrorMessage(err, "Could not restore that version.")); } finally { setBusy(false); } }; return ( setDraft(e.target.value)} placeholder="Write it yourself, start from the template, or generate a draft with the AI panel below." /> {dirty && ( )} {(data?.versions.length ?? 0) === 0 ? ( No versions yet. The first save starts the history. ) : ( {(data?.versions ?? []).map((v) => ( v{v.version} {v.isCurrent && } {new Date(v.createdAtUtc).toLocaleString()} · {v.length} characters {!v.isCurrent && ( restore(v.version)} > )} ))} )} ); }