+ {/* Back */}
+
+
+ {/* Panel A — Job Info */}
+
+
+
+
+ {job.title}
+
+
{job.company} · {job.location}
+ {job.salary &&
{job.salary}
}
+
+ {job.url && (
+
+ View posting ↗
+
+ )}
+
+ {job.description && (
+
+ {job.description}
+
+ )}
+
+
+ {/* Panel B — Analysis */}
+
+
+
Agent Analysis
+ {analysisDone && (
+
+ )}
+
+
+ {analysisError &&
{analysisError}
}
+
+ {analyzing && (
+
+
+ Analysing job against your profile…
+
+ )}
+
+ {analysisDone && analysis && (
+
+ {/* Fit score + level */}
+
+ {analysis.fit_score != null && (
+
+
Fit Score
+
{analysis.fit_score}%
+
+ )}
+ {analysis.level_match && (
+
+
Experience Level
+
+ {analysis.level_match === "strong" ? "Good match" : analysis.level_match === "ok" ? "Reasonable match" : "Stretch role"}
+
+
+ )}
+
+
+ {/* Skills grid */}
+
+ {/* Skills possessed */}
+ {analysis.matched_skills && analysis.matched_skills.length > 0 && (
+
+
Skills Matched
+
+ {analysis.matched_skills.map((s) => (
+
+ {s}
+
+ ))}
+
+
+ )}
+
+ {/* Skills lacking */}
+ {analysis.skill_gaps && analysis.skill_gaps.length > 0 && (
+
+
Skills to Develop
+
+ {analysis.skill_gaps.map((gap) => (
+
+ {gap}
+
+ ))}
+
+
+ )}
+
+
+ {appStep === "idle" && (
+
+ )}
+
+ )}
+
+ {/* Prepare button when analysis done but no score yet */}
+ {analysisDone && !analysis && appStep === "idle" && (
+
+ )}
+
+
+ {/* Panel C — Application Preparation */}
+ {appStep !== "idle" && (
+
+
Application Preparation
+
+ {/* Step C1 — Tailored CV */}
+
+
+
+
+ {appStep === "cv" ? "1" : "✓"}
+
+ Tailored CV
+ {streamingCv && Generating…}
+
+ {tailoredCv && !streamingCv && (
+
+ {cvDocId && (
+
+ )}
+
+
+ )}
+
+ {(["cv", "cv_done", "letter", "letter_done", "apply"] as AppStep[]).includes(appStep) && (
+
+
+ {/* Step C2 — Cover Letter */}
+ {(["letter", "letter_done", "apply"] as AppStep[]).includes(appStep) && (
+
+
+
+
+ {appStep === "letter" ? "2" : "✓"}
+
+ Cover Letter
+ {streamingLetter && Generating…}
+
+ {coverLetter && !streamingLetter && (
+
+ {letterDocId && (
+
+ )}
+
+
+ )}
+
+
+ )}
+
+ )}
+
+ {/* Floating minimised pill */}
+ {showBrowser && minimised && (
+
+ )}
+
+ {/* Browser View */}
+ {showBrowser && !minimised && (
+
+ setBrowserBlocked(false)}
+ />
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/app/jobs/page.tsx b/frontend/src/app/jobs/page.tsx
new file mode 100644
index 0000000..9e7f945
--- /dev/null
+++ b/frontend/src/app/jobs/page.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+import { useEffect, useState, useCallback } from "react";
+import { authFetch } from "../api";
+import JobCard, { Job } from "@/components/JobCard";
+
+const FILTER_CHIPS = [
+ { label: "Remote", keyword: "remote" },
+ { label: "Junior", keyword: "junior" },
+ { label: "AI / ML", keyword: "ai" },
+ { label: "SWE", keyword: "software engineer" },
+ { label: "Finance", keyword: "finance" },
+];
+
+type SortKey = "fit" | "risk" | "recent";
+
+export default function JobsPage() {
+ const [jobs, setJobs] = useState