amalsp commited on
Commit
d7c0198
·
0 Parent(s):

Prepare ResumeAI Pro for Hugging Face Spaces deployment

Browse files
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .next
3
+ node_modules
4
+ backend/__pycache__
5
+ backend/app/__pycache__
6
+ backend/app/agents/__pycache__
7
+ backend/app/services/__pycache__
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ .env
12
+ .env.local
13
+ .venv
14
+ dist
15
+ coverage
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ NEXT_PUBLIC_API_URL=
2
+ BACKEND_API_URL=http://127.0.0.1:8000
3
+ APP_ENV=development
4
+ ALLOWED_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000","http://localhost:7860","http://127.0.0.1:7860"]
5
+ OPENAI_API_KEY=
6
+ OPENAI_MODEL=gpt-5-mini
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .next
2
+ node_modules
3
+ out
4
+ dist
5
+ coverage
6
+ .venv
7
+ __pycache__
8
+ .pytest_cache
9
+ .env
10
+ .env.local
11
+ .env.development.local
12
+ .env.test.local
13
+ .env.production.local
14
+ npm-debug.log*
15
+ yarn-debug.log*
16
+ yarn-error.log*
17
+ pnpm-debug.log*
Dockerfile ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-bookworm-slim AS frontend-builder
2
+
3
+ WORKDIR /app
4
+
5
+ ENV NEXT_TELEMETRY_DISABLED=1
6
+ ENV BACKEND_API_URL=http://127.0.0.1:8000
7
+
8
+ COPY package.json package-lock.json ./
9
+ RUN npm ci
10
+
11
+ COPY app ./app
12
+ COPY components ./components
13
+ COPY public ./public
14
+ COPY next.config.ts ./
15
+ COPY next-env.d.ts ./
16
+ COPY tsconfig.json ./
17
+ COPY postcss.config.js ./
18
+ COPY tailwind.config.ts ./
19
+
20
+ RUN npm run build
21
+
22
+
23
+ FROM python:3.13-slim AS backend-builder
24
+
25
+ WORKDIR /app/backend
26
+
27
+ COPY backend/requirements.txt ./
28
+ RUN python -m venv /opt/venv \
29
+ && /opt/venv/bin/pip install --upgrade pip \
30
+ && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
31
+
32
+ COPY backend/app ./app
33
+
34
+
35
+ FROM python:3.13-slim AS runtime
36
+
37
+ COPY --from=node:22-bookworm-slim /usr/local /usr/local
38
+
39
+ ENV PATH="/opt/venv/bin:$PATH"
40
+ ENV PYTHONUNBUFFERED=1
41
+ ENV NEXT_TELEMETRY_DISABLED=1
42
+ ENV PORT=7860
43
+ ENV HOSTNAME=0.0.0.0
44
+
45
+ WORKDIR /app
46
+
47
+ COPY --from=backend-builder /opt/venv /opt/venv
48
+ COPY --from=backend-builder /app/backend /app/backend
49
+ COPY --from=frontend-builder /app/.next/standalone /app/frontend
50
+ COPY --from=frontend-builder /app/.next/static /app/frontend/.next/static
51
+ COPY --from=frontend-builder /app/public /app/frontend/public
52
+ COPY scripts/start-prod.sh /app/start-prod.sh
53
+
54
+ RUN chmod +x /app/start-prod.sh
55
+
56
+ EXPOSE 7860
57
+
58
+ CMD ["/app/start-prod.sh"]
README.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ResumeAI Pro
3
+ sdk: docker
4
+ app_port: 7860
5
+ ---
6
+
7
+ # ResumeAI Pro
8
+
9
+ ResumeAI Pro is a full-stack AI agent project that autonomously analyzes, optimizes, and personalizes resumes and cover letters against specific job descriptions.
10
+
11
+ ## What is included
12
+
13
+ - A polished Next.js landing page for the project story and architecture
14
+ - A FastAPI backend with agent-style modules for parsing, analysis, optimization, cover letter generation, and ATS scoring
15
+ - Optional OpenAI-powered resume rewriting and cover-letter generation when `OPENAI_API_KEY` is configured
16
+ - PDF upload support for resume ingestion
17
+ - Deployment-ready Docker configuration for local multi-service runs and single-container Spaces deployment
18
+
19
+ ## Stack
20
+
21
+ - Next.js 15 + Tailwind CSS for the frontend
22
+ - FastAPI for the backend API layer
23
+ - OpenAI API ready architecture for future reasoning integration
24
+ - Docker and Docker Compose for local and hosted deployment
25
+
26
+ ## Frontend setup
27
+
28
+ 1. Install Node.js 22 or later.
29
+ 2. Run `npm install`.
30
+ 3. Start the frontend with `npm run dev`.
31
+ 4. Open `http://localhost:3000`.
32
+
33
+ ## Backend setup
34
+
35
+ 1. Move into `backend`.
36
+ 2. Run `python -m pip install -r requirements.txt`.
37
+ 3. Start the API with `python -m uvicorn app.main:app --reload`.
38
+ 4. Open `http://localhost:8000/docs` for the Swagger UI.
39
+
40
+ ## Environment
41
+
42
+ Copy `.env.example` to `.env` and set:
43
+
44
+ - `NEXT_PUBLIC_API_URL` if you want the browser to call an external API directly. Leave it blank to use same-origin proxying.
45
+ - `BACKEND_API_URL` for Next.js server-side rewrites to the FastAPI service
46
+ - `OPENAI_API_KEY` if you want live OpenAI rewriting instead of the local fallback
47
+ - `OPENAI_MODEL` if you want to override the default model
48
+
49
+ ## Local Docker setup
50
+
51
+ Run `docker compose up --build` from the project root to start both services together.
52
+
53
+ ## Production deployment
54
+
55
+ ### Hugging Face Spaces
56
+
57
+ This repository is configured for Docker Spaces deployment:
58
+
59
+ 1. Create a new Space with `Docker` as the SDK.
60
+ 2. Push this repository to the Space.
61
+ 3. Set repository secrets or Space variables for `OPENAI_API_KEY` if you want live OpenAI generation.
62
+ 4. The root `Dockerfile` will start both FastAPI and Next.js inside one container and expose the app on port `7860`.
63
+
64
+ ### Before push
65
+
66
+ 1. Initialize Git in this folder if needed: `git init`
67
+ 2. Remove local generated artifacts before committing: `.next`, `node_modules`, and Python `__pycache__` folders
68
+ 3. Create your first commit
69
+ 4. Connect the repo to Hugging Face Spaces or GitHub
70
+ 5. Push to the Docker Space remote
71
+
72
+ ### Generic container deployment
73
+
74
+ Build and run the production image from the project root:
75
+
76
+ 1. `docker build -t resumeai-pro .`
77
+ 2. `docker run -p 7860:7860 --env-file .env resumeai-pro`
78
+
79
+ The production container serves the Next.js frontend publicly and proxies `/api/*` requests internally to the FastAPI backend.
80
+
81
+ ## Current API
82
+
83
+ - `GET /health`
84
+ - `POST /api/pipeline`
85
+ - `POST /api/pipeline/upload`
86
+ - `POST /api/export/resume/pdf`
87
+ - `POST /api/export/resume/docx`
88
+ - `POST /api/export/cover-letter/pdf`
89
+ - `POST /api/export/cover-letter/docx`
90
+
91
+ The pipeline works without an API key using deterministic heuristics. If `OPENAI_API_KEY` is present, the backend upgrades the optimization and cover-letter steps with structured output from the OpenAI Responses API.
app/globals.css ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: #f8f3ea;
7
+ --foreground: #10121a;
8
+ }
9
+
10
+ html {
11
+ scroll-behavior: smooth;
12
+ }
13
+
14
+ body {
15
+ min-height: 100vh;
16
+ background:
17
+ radial-gradient(circle at top left, rgba(215, 162, 108, 0.28), transparent 32%),
18
+ radial-gradient(circle at 85% 12%, rgba(33, 73, 63, 0.18), transparent 24%),
19
+ linear-gradient(180deg, #f6f0e6 0%, #fbf8f2 52%, #f3ece1 100%);
20
+ color: var(--foreground);
21
+ font-family: var(--font-sans), sans-serif;
22
+ }
23
+
24
+ a {
25
+ color: inherit;
26
+ text-decoration: none;
27
+ }
28
+
29
+ .font-display {
30
+ font-family: var(--font-display), serif;
31
+ }
32
+
33
+ .page-glow {
34
+ pointer-events: none;
35
+ position: absolute;
36
+ border-radius: 999px;
37
+ filter: blur(70px);
38
+ opacity: 0.8;
39
+ }
40
+
41
+ .page-glow-one {
42
+ right: -8rem;
43
+ top: 14rem;
44
+ height: 18rem;
45
+ width: 18rem;
46
+ background: rgba(127, 59, 59, 0.12);
47
+ }
48
+
49
+ .page-glow-two {
50
+ bottom: 8rem;
51
+ left: -6rem;
52
+ height: 20rem;
53
+ width: 20rem;
54
+ background: rgba(33, 73, 63, 0.12);
55
+ }
56
+
57
+ .hero-panel {
58
+ border-radius: 2rem;
59
+ background:
60
+ linear-gradient(150deg, rgba(255, 255, 255, 0.06), transparent 36%),
61
+ linear-gradient(135deg, #141720 0%, #1d302a 70%, #7f3b3b 160%);
62
+ padding: 1.75rem;
63
+ box-shadow: 0 24px 65px rgba(16, 18, 26, 0.18);
64
+ }
65
+
66
+ .section-intro {
67
+ max-width: 52rem;
68
+ }
69
+
70
+ .eyebrow {
71
+ font-size: 0.72rem;
72
+ font-weight: 600;
73
+ letter-spacing: 0.28em;
74
+ text-transform: uppercase;
75
+ color: rgba(16, 18, 26, 0.58);
76
+ }
77
+
78
+ .section-title {
79
+ margin-top: 1rem;
80
+ font-family: var(--font-display), serif;
81
+ font-size: clamp(2.25rem, 4vw, 4rem);
82
+ line-height: 0.98;
83
+ }
84
+
85
+ .section-copy {
86
+ margin-top: 1.25rem;
87
+ max-width: 44rem;
88
+ font-size: 1rem;
89
+ line-height: 2;
90
+ color: rgba(16, 18, 26, 0.72);
91
+ }
92
+
93
+ @media (min-width: 768px) {
94
+ .hero-panel {
95
+ padding: 2rem;
96
+ }
97
+ }
app/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Cormorant_Garamond, Space_Grotesk } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const display = Cormorant_Garamond({
6
+ subsets: ["latin"],
7
+ variable: "--font-display",
8
+ weight: ["400", "500", "600", "700"],
9
+ });
10
+
11
+ const sans = Space_Grotesk({
12
+ subsets: ["latin"],
13
+ variable: "--font-sans",
14
+ weight: ["400", "500", "700"],
15
+ });
16
+
17
+ export const metadata: Metadata = {
18
+ title: "ResumeAI Pro",
19
+ description:
20
+ "Autonomous resume optimization platform with multi-agent analysis, ATS scoring, and personalized document generation.",
21
+ };
22
+
23
+ export default function RootLayout({
24
+ children,
25
+ }: Readonly<{
26
+ children: React.ReactNode;
27
+ }>) {
28
+ return (
29
+ <html lang="en">
30
+ <body className={`${display.variable} ${sans.variable}`}>{children}</body>
31
+ </html>
32
+ );
33
+ }
app/page.tsx ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DemoConsole } from "@/components/demo-console";
2
+
3
+ const agentStages = [
4
+ {
5
+ title: "Parsing Agent",
6
+ description:
7
+ "Extracts clean, structured content from uploaded PDF resumes and normalizes the text for downstream reasoning.",
8
+ outcomes: ["PDF text extraction", "Structured resume sections", "Content normalization"],
9
+ },
10
+ {
11
+ title: "Analysis Agent",
12
+ description:
13
+ "Compares the current resume against a specific job description to uncover weak positioning and ATS blind spots.",
14
+ outcomes: ["Skill gap detection", "Missing keyword analysis", "Weak bullet identification"],
15
+ },
16
+ {
17
+ title: "Optimization Agent",
18
+ description:
19
+ "Rewrites resume bullets with stronger action language, measurable outcomes, and recruiter-friendly clarity.",
20
+ outcomes: ["Impact-driven bullets", "Quantified achievements", "ATS-friendly formatting"],
21
+ },
22
+ {
23
+ title: "Cover Letter Agent",
24
+ description:
25
+ "Generates role-specific cover letters that align candidate experience, tone, and motivation with job context.",
26
+ outcomes: ["Tailored messaging", "Role alignment", "Context-aware tone"],
27
+ },
28
+ {
29
+ title: "ATS Scoring Agent",
30
+ description:
31
+ "Scores the resume against hiring criteria and surfaces practical guidance for improving match quality.",
32
+ outcomes: ["Match percentage", "Missing keyword report", "Optimization suggestions"],
33
+ },
34
+ ] as const;
35
+
36
+ const features = [
37
+ "Automated resume parsing from PDF uploads",
38
+ "Job-specific optimization and rewrite flow",
39
+ "Personalized cover letter generation",
40
+ "ATS keyword scoring and match reporting",
41
+ "Keyword gap analysis with improvement cues",
42
+ "Before-versus-after resume comparison",
43
+ "Responsive dashboard experience",
44
+ "Real-time AI processing feedback",
45
+ ] as const;
46
+
47
+ const stack = [
48
+ {
49
+ label: "Backend",
50
+ summary: "FastAPI powers a modular agent pipeline with clean API boundaries for parsing, analysis, generation, and scoring.",
51
+ },
52
+ {
53
+ label: "Frontend",
54
+ summary: "Next.js and Tailwind CSS deliver a responsive interface optimized for dashboards, document review, and quick iteration.",
55
+ },
56
+ {
57
+ label: "Deployment",
58
+ summary: "Docker packaging enables portable deployment, with Hugging Face Spaces used as the production runtime target.",
59
+ },
60
+ ] as const;
61
+
62
+ const capabilities = [
63
+ "Task decomposition across specialized agents",
64
+ "Tool-augmented reasoning with parsing and scoring engines",
65
+ "Context-aware generation for tailored resume assets",
66
+ "Iterative refinement aligned to recruiter expectations",
67
+ ] as const;
68
+
69
+ const metrics = [
70
+ { value: "80%+", label: "Manual tailoring effort reduced" },
71
+ { value: "5", label: "Specialized agents in the pipeline" },
72
+ { value: "Full-stack", label: "AI, backend, frontend, and deployment ownership" },
73
+ ] as const;
74
+
75
+ export default function Home() {
76
+ return (
77
+ <main className="relative overflow-hidden">
78
+ <div className="page-glow page-glow-one" />
79
+ <div className="page-glow page-glow-two" />
80
+
81
+ <section className="mx-auto flex min-h-screen w-full max-w-7xl flex-col px-6 pb-16 pt-8 md:px-10 lg:px-12">
82
+ <header className="flex flex-col gap-2 border-b border-black/10 pb-5 text-sm uppercase tracking-[0.3em] text-ink/65 sm:flex-row sm:items-center sm:justify-between">
83
+ <span>ResumeAI Pro</span>
84
+ <span>Autonomous Resume Optimization Agent</span>
85
+ </header>
86
+
87
+ <div className="grid flex-1 gap-10 py-12 lg:grid-cols-[1.15fr_0.85fr] lg:items-center">
88
+ <div className="space-y-8">
89
+ <div className="inline-flex items-center rounded-full border border-pine/20 bg-white/70 px-4 py-2 text-xs font-medium uppercase tracking-[0.25em] text-pine backdrop-blur">
90
+ AI agent system for recruiter-grade resume personalization
91
+ </div>
92
+
93
+ <div className="space-y-6">
94
+ <p className="max-w-2xl font-display text-6xl leading-[0.95] text-ink md:text-7xl">
95
+ Resume optimization that reasons like a hiring team, not a one-shot prompt.
96
+ </p>
97
+ <p className="max-w-xl text-base leading-8 text-ink/72 md:text-lg">
98
+ ResumeAI Pro is a full-stack multi-agent platform that analyzes resumes, compares them to job descriptions,
99
+ rewrites content for stronger recruiter impact, generates tailored cover letters, and scores ATS alignment in
100
+ one continuous workflow.
101
+ </p>
102
+ </div>
103
+
104
+ <div className="flex flex-wrap gap-4">
105
+ <a className="rounded-full bg-ink px-6 py-3 text-sm font-medium text-sand transition hover:bg-pine" href="#architecture">
106
+ Explore the pipeline
107
+ </a>
108
+ <a
109
+ className="rounded-full border border-ink/15 bg-white/80 px-6 py-3 text-sm font-medium text-ink transition hover:border-ink/30 hover:bg-white"
110
+ href="#implementation"
111
+ >
112
+ View the technical stack
113
+ </a>
114
+ </div>
115
+
116
+ <div className="grid gap-4 pt-2 md:grid-cols-3">
117
+ {metrics.map((metric) => (
118
+ <div key={metric.label} className="rounded-[1.75rem] border border-black/8 bg-white/70 p-5 shadow-card backdrop-blur">
119
+ <p className="text-2xl font-bold text-ink">{metric.value}</p>
120
+ <p className="mt-2 text-sm leading-6 text-ink/68">{metric.label}</p>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </div>
125
+
126
+ <div className="relative">
127
+ <div className="hero-panel">
128
+ <div className="flex items-start justify-between gap-5 border-b border-white/12 pb-5">
129
+ <div>
130
+ <p className="text-xs uppercase tracking-[0.3em] text-sand/70">Live workflow snapshot</p>
131
+ <h2 className="mt-3 text-2xl font-semibold text-sand">Recruiter-style evaluation loop</h2>
132
+ </div>
133
+ <div className="rounded-full border border-white/15 px-3 py-1 text-xs uppercase tracking-[0.2em] text-sand/80">
134
+ Multi-agent
135
+ </div>
136
+ </div>
137
+
138
+ <div className="mt-6 space-y-4">
139
+ {[
140
+ ["Resume uploaded", "PDF parsed into structured candidate data"],
141
+ ["Job description mapped", "Priority keywords and skill signals extracted"],
142
+ ["Optimization run", "Bullets rewritten for impact and ATS alignment"],
143
+ ["Cover letter generated", "Role-specific narrative drafted from profile context"],
144
+ ].map(([title, text], index) => (
145
+ <div key={title} className="grid grid-cols-[auto_1fr] gap-4 rounded-3xl border border-white/10 bg-white/5 p-4">
146
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-clay text-sm font-semibold text-ink">
147
+ 0{index + 1}
148
+ </div>
149
+ <div>
150
+ <p className="font-medium text-sand">{title}</p>
151
+ <p className="mt-1 text-sm leading-6 text-sand/72">{text}</p>
152
+ </div>
153
+ </div>
154
+ ))}
155
+ </div>
156
+
157
+ <div className="mt-6 grid gap-4 sm:grid-cols-2">
158
+ <div className="rounded-3xl bg-sand p-5 text-ink">
159
+ <p className="text-xs uppercase tracking-[0.25em] text-ink/60">ATS Match Score</p>
160
+ <p className="mt-3 text-4xl font-bold">92%</p>
161
+ <p className="mt-2 text-sm leading-6 text-ink/70">Keyword alignment improved after optimization and role-specific rewriting.</p>
162
+ </div>
163
+ <div className="rounded-3xl border border-white/10 bg-white/5 p-5">
164
+ <p className="text-xs uppercase tracking-[0.25em] text-sand/60">Key outputs</p>
165
+ <ul className="mt-3 space-y-3 text-sm leading-6 text-sand/80">
166
+ <li>Resume rewrite with quantified achievements</li>
167
+ <li>Missing keyword and gap report</li>
168
+ <li>Tailored cover letter draft</li>
169
+ </ul>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </section>
176
+
177
+ <section id="architecture" className="mx-auto max-w-7xl px-6 py-20 md:px-10 lg:px-12">
178
+ <div className="section-intro">
179
+ <p className="eyebrow">Agent Architecture</p>
180
+ <h2 className="section-title">A specialized pipeline where each agent owns a clear decision boundary.</h2>
181
+ <p className="section-copy">
182
+ Instead of treating resume improvement as a single prompt, the system decomposes the workflow into focused agents
183
+ that parse, evaluate, rewrite, generate, and score outputs in sequence.
184
+ </p>
185
+ </div>
186
+
187
+ <div className="mt-10 grid gap-6 lg:grid-cols-2 xl:grid-cols-3">
188
+ {agentStages.map((stage) => (
189
+ <article key={stage.title} className="rounded-[2rem] border border-black/8 bg-white/75 p-7 shadow-card backdrop-blur">
190
+ <p className="text-xs uppercase tracking-[0.25em] text-pine/70">{stage.title}</p>
191
+ <p className="mt-4 text-xl font-semibold text-ink">{stage.description}</p>
192
+ <ul className="mt-6 space-y-3 text-sm leading-6 text-ink/70">
193
+ {stage.outcomes.map((outcome) => (
194
+ <li key={outcome} className="flex items-start gap-3">
195
+ <span className="mt-2 h-2 w-2 rounded-full bg-clay" />
196
+ <span>{outcome}</span>
197
+ </li>
198
+ ))}
199
+ </ul>
200
+ </article>
201
+ ))}
202
+ </div>
203
+ </section>
204
+
205
+ <section className="mx-auto max-w-7xl px-6 py-8 md:px-10 lg:px-12">
206
+ <div className="grid gap-8 rounded-[2.25rem] bg-ink px-7 py-8 text-sand md:px-10 md:py-10 lg:grid-cols-[0.9fr_1.1fr]">
207
+ <div>
208
+ <p className="eyebrow text-sand/65">AI Capabilities</p>
209
+ <h2 className="section-title max-w-md text-sand">Built to demonstrate real agent behavior, not just text generation.</h2>
210
+ </div>
211
+ <div className="grid gap-4 sm:grid-cols-2">
212
+ {capabilities.map((item) => (
213
+ <div key={item} className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5 text-sm leading-6 text-sand/82">
214
+ {item}
215
+ </div>
216
+ ))}
217
+ </div>
218
+ </div>
219
+ </section>
220
+
221
+ <DemoConsole />
222
+
223
+ <section id="implementation" className="mx-auto max-w-7xl px-6 py-20 md:px-10 lg:px-12">
224
+ <div className="section-intro">
225
+ <p className="eyebrow">Technical Implementation</p>
226
+ <h2 className="section-title">Full-stack architecture designed for practical deployment.</h2>
227
+ <p className="section-copy">
228
+ The project combines modern frontend delivery, modular backend orchestration, and containerized deployment so the
229
+ system can move cleanly from prototype to production-hosted demo.
230
+ </p>
231
+ </div>
232
+
233
+ <div className="mt-10 grid gap-6 lg:grid-cols-3">
234
+ {stack.map((item) => (
235
+ <div key={item.label} className="rounded-[2rem] border border-black/8 bg-[#fffdf8] p-7 shadow-card">
236
+ <p className="text-sm uppercase tracking-[0.25em] text-wine/70">{item.label}</p>
237
+ <p className="mt-5 text-lg leading-8 text-ink">{item.summary}</p>
238
+ </div>
239
+ ))}
240
+ </div>
241
+
242
+ <div className="mt-10 grid gap-6 lg:grid-cols-[0.95fr_1.05fr]">
243
+ <div className="rounded-[2rem] border border-black/8 bg-white/75 p-7 shadow-card backdrop-blur">
244
+ <p className="text-sm uppercase tracking-[0.25em] text-pine/70">Core Features</p>
245
+ <div className="mt-5 grid gap-3 sm:grid-cols-2">
246
+ {features.map((feature) => (
247
+ <div key={feature} className="rounded-[1.35rem] bg-sand px-4 py-4 text-sm leading-6 text-ink">
248
+ {feature}
249
+ </div>
250
+ ))}
251
+ </div>
252
+ </div>
253
+
254
+ <div className="rounded-[2rem] border border-black/8 bg-[#f2ede4] p-7 shadow-card">
255
+ <p className="text-sm uppercase tracking-[0.25em] text-wine/70">Workflow Narrative</p>
256
+ <div className="mt-5 space-y-5">
257
+ {[
258
+ "Candidate uploads a PDF resume and submits a target job description.",
259
+ "The parsing and analysis agents extract structure, compare requirements, and identify opportunity gaps.",
260
+ "The optimization agent rewrites weak resume content while the ATS scoring agent quantifies alignment gains.",
261
+ "The cover letter agent completes the package with role-specific messaging for the same job context.",
262
+ ].map((step, index) => (
263
+ <div key={step} className="grid grid-cols-[auto_1fr] gap-4">
264
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-ink text-sm font-semibold text-sand">
265
+ {index + 1}
266
+ </div>
267
+ <p className="pt-1 text-sm leading-7 text-ink/75">{step}</p>
268
+ </div>
269
+ ))}
270
+ </div>
271
+ </div>
272
+ </div>
273
+ </section>
274
+
275
+ <section className="mx-auto max-w-7xl px-6 py-8 md:px-10 lg:px-12">
276
+ <div className="grid gap-8 lg:grid-cols-[0.92fr_1.08fr]">
277
+ <div className="rounded-[2.25rem] bg-pine p-8 text-sand shadow-card">
278
+ <p className="eyebrow text-sand/70">Impact And Value</p>
279
+ <h2 className="section-title max-w-md text-sand">A practical system built around a problem candidates face constantly.</h2>
280
+ <p className="mt-5 max-w-lg text-base leading-8 text-sand/80">
281
+ ResumeAI Pro reduces repetitive resume tailoring work, improves ATS keyword alignment, and simulates how recruiters
282
+ actually review candidate materials before shortlisting.
283
+ </p>
284
+ </div>
285
+
286
+ <div className="rounded-[2.25rem] border border-black/8 bg-white/80 p-8 shadow-card backdrop-blur">
287
+ <p className="text-sm uppercase tracking-[0.25em] text-pine/70">Resume Bullet For Your CV</p>
288
+ <div className="mt-5 rounded-[1.75rem] bg-ink p-6 text-sm leading-7 text-sand">
289
+ Developed a multi-agent AI resume optimization system using OpenAI API, FastAPI, and Next.js; implemented ATS
290
+ scoring, keyword gap analysis, and automated document rewriting with Docker-based deployment on Hugging Face
291
+ Spaces.
292
+ </div>
293
+ <div className="mt-6 grid gap-4 sm:grid-cols-2">
294
+ <div className="rounded-[1.5rem] bg-sand p-5">
295
+ <p className="text-xs uppercase tracking-[0.2em] text-ink/60">Why it stands out</p>
296
+ <p className="mt-3 text-sm leading-7 text-ink/75">
297
+ Demonstrates agent orchestration, production-minded architecture, full-stack execution, and a clear real-world use case.
298
+ </p>
299
+ </div>
300
+ <div className="rounded-[1.5rem] bg-mist p-5">
301
+ <p className="text-xs uppercase tracking-[0.2em] text-ink/60">Next up</p>
302
+ <p className="mt-3 text-sm leading-7 text-ink/75">
303
+ Memory-enabled personalization, multi-job comparisons, recruiter feedback simulation, and stronger scoring models.
304
+ </p>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </section>
310
+
311
+ <section className="mx-auto max-w-7xl px-6 pb-20 pt-10 md:px-10 lg:px-12">
312
+ <div className="rounded-[2.25rem] border border-black/8 bg-white/70 px-8 py-10 shadow-card backdrop-blur">
313
+ <p className="eyebrow">Conclusion</p>
314
+ <h2 className="section-title max-w-3xl">
315
+ ResumeAI Pro positions the builder as someone who can ship practical AI systems with real user value.
316
+ </h2>
317
+ <p className="section-copy max-w-3xl">
318
+ The project demonstrates more than prompt engineering. It shows agent orchestration, structured analysis,
319
+ measurable optimization, and production-minded delivery across the full stack.
320
+ </p>
321
+ </div>
322
+ </section>
323
+ </main>
324
+ );
325
+ }
backend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app ./app
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
backend/app/agents/analysis.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from app.schemas import AnalysisResult, ParsingResult
6
+
7
+ STOPWORDS = {
8
+ "about",
9
+ "above",
10
+ "after",
11
+ "again",
12
+ "against",
13
+ "along",
14
+ "also",
15
+ "among",
16
+ "and",
17
+ "are",
18
+ "been",
19
+ "being",
20
+ "build",
21
+ "candidate",
22
+ "clients",
23
+ "company",
24
+ "cross",
25
+ "data",
26
+ "deliver",
27
+ "from",
28
+ "have",
29
+ "into",
30
+ "looking",
31
+ "must",
32
+ "need",
33
+ "requirements",
34
+ "role",
35
+ "team",
36
+ "teams",
37
+ "that",
38
+ "their",
39
+ "then",
40
+ "they",
41
+ "this",
42
+ "with",
43
+ "work",
44
+ "years",
45
+ }
46
+
47
+ ACTION_VERBS = {
48
+ "achieved",
49
+ "accelerated",
50
+ "architected",
51
+ "built",
52
+ "created",
53
+ "delivered",
54
+ "designed",
55
+ "developed",
56
+ "drove",
57
+ "implemented",
58
+ "improved",
59
+ "launched",
60
+ "led",
61
+ "optimized",
62
+ "reduced",
63
+ "scaled",
64
+ }
65
+
66
+ KEY_PHRASES = [
67
+ "fastapi",
68
+ "next.js",
69
+ "tailwind css",
70
+ "openai api",
71
+ "docker",
72
+ "hugging face spaces",
73
+ "machine learning",
74
+ "data analysis",
75
+ "project management",
76
+ "stakeholder communication",
77
+ "api integration",
78
+ "automation",
79
+ "keyword analysis",
80
+ "resume optimization",
81
+ ]
82
+
83
+
84
+ def analyze_resume(parsed: ParsingResult, job_description: str) -> AnalysisResult:
85
+ resume_lower = parsed.normalized_text.lower()
86
+ jd_lower = job_description.lower()
87
+
88
+ keywords = _extract_keywords(jd_lower)
89
+ matched = [keyword for keyword in keywords if keyword in resume_lower]
90
+ missing = [keyword for keyword in keywords if keyword not in resume_lower]
91
+
92
+ bullet_lines = [
93
+ line.strip()
94
+ for line in parsed.normalized_text.splitlines()
95
+ if re.match(r"^([-*\u2022]|\d+\.)\s+", line.strip())
96
+ ]
97
+ weak_bullets = [line for line in bullet_lines if _is_weak_bullet(line)]
98
+
99
+ strengths = matched[:5] if matched else ["Transferable experience", "Relevant project foundation"]
100
+
101
+ return AnalysisResult(
102
+ matched_keywords=matched[:10],
103
+ missing_keywords=missing[:10],
104
+ skill_gaps=missing[:6],
105
+ weak_bullets=weak_bullets[:4],
106
+ strengths=strengths,
107
+ )
108
+
109
+
110
+ def _extract_keywords(job_description: str) -> list[str]:
111
+ phrase_hits = [phrase for phrase in KEY_PHRASES if phrase in job_description]
112
+
113
+ raw_words = re.findall(r"[a-zA-Z][a-zA-Z0-9+.#/-]{2,}", job_description)
114
+ filtered_words: list[str] = []
115
+ for word in raw_words:
116
+ normalized = word.lower()
117
+ if normalized in STOPWORDS or normalized.isdigit():
118
+ continue
119
+ if normalized not in filtered_words:
120
+ filtered_words.append(normalized)
121
+
122
+ combined = phrase_hits + filtered_words
123
+ return combined[:18]
124
+
125
+
126
+ def _is_weak_bullet(line: str) -> bool:
127
+ bullet = re.sub(r"^([-*\u2022]|\d+\.)\s+", "", line.strip()).lower()
128
+ has_metric = bool(re.search(r"\d", bullet) or "%" in bullet)
129
+ starts_with_action = any(bullet.startswith(verb) for verb in ACTION_VERBS)
130
+ return not has_metric or not starts_with_action
backend/app/agents/cover_letter.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.schemas import AnalysisResult, CoverLetterResult, PipelineRequest
4
+
5
+
6
+ def generate_cover_letter(request: PipelineRequest, analysis: AnalysisResult) -> CoverLetterResult:
7
+ candidate_name = request.candidate_name or "The candidate"
8
+ role = request.target_role or "the role"
9
+ strengths = ", ".join(analysis.strengths[:3]).lower()
10
+ focus = ", ".join(analysis.missing_keywords[:2]).lower() if analysis.missing_keywords else "priority role requirements"
11
+
12
+ opening = (
13
+ f"{candidate_name} is applying for {role} with a background that aligns closely with "
14
+ f"the role's emphasis on {strengths}."
15
+ )
16
+ body = (
17
+ f"The experience presented in the resume shows practical execution and strong project ownership, "
18
+ f"while the optimized draft now better reflects the hiring team's priorities around {focus}."
19
+ )
20
+ closing = (
21
+ "This combination of relevant experience, sharper positioning, and clearer impact makes the "
22
+ "application stronger for recruiter review and ATS screening."
23
+ )
24
+
25
+ return CoverLetterResult(opening=opening, body=body, closing=closing)
backend/app/agents/optimization.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from app.schemas import AnalysisResult, OptimizationResult, OptimizedBullet, PipelineRequest
6
+
7
+
8
+ def optimize_resume(request: PipelineRequest, analysis: AnalysisResult) -> OptimizationResult:
9
+ role = request.target_role or "the target role"
10
+ focus_areas = analysis.missing_keywords[:3] or [
11
+ "role-specific keywords",
12
+ "quantified impact",
13
+ "clear ownership signals",
14
+ ]
15
+
16
+ bullet_improvements = [
17
+ OptimizedBullet(
18
+ original=bullet,
19
+ rewritten=_rewrite_bullet(bullet, focus_areas),
20
+ )
21
+ for bullet in analysis.weak_bullets[:3]
22
+ ]
23
+
24
+ summary = (
25
+ f"Results-oriented candidate prepared for {role}, with resume content refocused around "
26
+ f"{', '.join(focus_areas[:2])} and measurable execution."
27
+ )
28
+
29
+ return OptimizationResult(
30
+ professional_summary=summary,
31
+ bullet_improvements=bullet_improvements,
32
+ focus_areas=focus_areas,
33
+ )
34
+
35
+
36
+ def _rewrite_bullet(bullet: str, focus_areas: list[str]) -> str:
37
+ cleaned = re.sub(r"^([-*\u2022]|\d+\.)\s+", "", bullet).strip().rstrip(".")
38
+ emphasis = focus_areas[0] if focus_areas else "business impact"
39
+ return (
40
+ f"Developed and improved {cleaned.lower()}, increasing clarity around {emphasis} "
41
+ "and highlighting measurable ownership across the workflow."
42
+ )
backend/app/agents/parsing.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from app.schemas import ParsingResult
6
+
7
+ COMMON_SECTIONS = {
8
+ "summary",
9
+ "experience",
10
+ "education",
11
+ "skills",
12
+ "projects",
13
+ "certifications",
14
+ "achievements",
15
+ "contact",
16
+ }
17
+
18
+
19
+ def parse_resume(resume_text: str, resume_source: str = "text") -> ParsingResult:
20
+ lines = [line.strip() for line in resume_text.splitlines() if line.strip()]
21
+ normalized_text = "\n".join(lines)
22
+ bullets = [line for line in lines if re.match(r"^([-*\u2022]|\d+\.)\s+", line)]
23
+
24
+ detected_sections: list[str] = []
25
+ for line in lines:
26
+ cleaned = re.sub(r"[^a-zA-Z ]", "", line.strip().lower().rstrip(":")).strip()
27
+ if cleaned in COMMON_SECTIONS and cleaned not in detected_sections:
28
+ detected_sections.append(cleaned.title())
29
+
30
+ return ParsingResult(
31
+ resume_source=resume_source,
32
+ word_count=len(normalized_text.split()),
33
+ bullet_count=len(bullets),
34
+ detected_sections=detected_sections,
35
+ normalized_text=normalized_text,
36
+ )
backend/app/agents/scoring.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.schemas import AnalysisResult, AtsScoreResult
4
+
5
+
6
+ def score_resume(analysis: AnalysisResult) -> AtsScoreResult:
7
+ matched = len(analysis.matched_keywords)
8
+ missing = len(analysis.missing_keywords)
9
+ total = max(matched + missing, 1)
10
+ score = round(35 + (matched / total) * 65)
11
+
12
+ suggestions = []
13
+ if analysis.missing_keywords:
14
+ suggestions.append(f"Add role-specific language for {analysis.missing_keywords[0]}.")
15
+ if analysis.weak_bullets:
16
+ suggestions.append("Rewrite weak bullets with action verbs and measurable outcomes.")
17
+ if not suggestions:
18
+ suggestions.append("Maintain keyword coverage and quantified achievements across all sections.")
19
+
20
+ return AtsScoreResult(
21
+ score=min(score, 98),
22
+ matched_count=matched,
23
+ missing_count=missing,
24
+ suggestions=suggestions,
25
+ )
backend/app/config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
7
+
8
+ app_name: str = "ResumeAI Pro API"
9
+ app_env: str = "development"
10
+ allowed_origins: list[str] = Field(
11
+ default_factory=lambda: [
12
+ "http://localhost:3000",
13
+ "http://127.0.0.1:3000",
14
+ "http://localhost:7860",
15
+ "http://127.0.0.1:7860",
16
+ ]
17
+ )
18
+ openai_api_key: str | None = None
19
+ openai_model: str = "gpt-5-mini"
20
+ use_openai_when_available: bool = True
21
+
22
+
23
+ settings = Settings()
backend/app/main.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI, File, Form, HTTPException, Response, UploadFile
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+
6
+ from app.config import settings
7
+ from app.schemas import CoverLetterExportRequest, PipelineRequest, PipelineResponse, ResumeExportRequest
8
+ from app.services.document_export import (
9
+ export_cover_letter_docx,
10
+ export_cover_letter_pdf,
11
+ export_resume_docx,
12
+ export_resume_pdf,
13
+ )
14
+ from app.services.pdf import extract_text_from_pdf
15
+ from app.services.pipeline import run_pipeline
16
+
17
+ app = FastAPI(title=settings.app_name)
18
+
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=settings.allowed_origins,
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+
28
+ @app.get("/health")
29
+ def healthcheck() -> dict[str, str]:
30
+ return {"status": "ok", "environment": settings.app_env}
31
+
32
+
33
+ @app.post("/api/pipeline", response_model=PipelineResponse)
34
+ def pipeline(payload: PipelineRequest) -> PipelineResponse:
35
+ return run_pipeline(payload)
36
+
37
+
38
+ @app.post("/api/pipeline/upload", response_model=PipelineResponse)
39
+ async def pipeline_upload(
40
+ file: UploadFile = File(...),
41
+ job_description: str = Form(...),
42
+ candidate_name: str | None = Form(default=None),
43
+ target_role: str | None = Form(default=None),
44
+ ) -> PipelineResponse:
45
+ filename = (file.filename or "").lower()
46
+ if not filename.endswith(".pdf"):
47
+ raise HTTPException(status_code=400, detail="Please upload a PDF resume.")
48
+
49
+ file_bytes = await file.read()
50
+ if not file_bytes:
51
+ raise HTTPException(status_code=400, detail="The uploaded PDF is empty.")
52
+
53
+ try:
54
+ resume_text = extract_text_from_pdf(file_bytes)
55
+ except ValueError as error:
56
+ raise HTTPException(status_code=400, detail=str(error)) from error
57
+
58
+ payload = PipelineRequest(
59
+ candidate_name=candidate_name,
60
+ target_role=target_role,
61
+ resume_text=resume_text,
62
+ job_description=job_description,
63
+ )
64
+ return run_pipeline(payload, resume_source="pdf")
65
+
66
+
67
+ @app.post("/api/export/resume/{format}")
68
+ def export_resume(format: str, payload: ResumeExportRequest) -> Response:
69
+ if format == "docx":
70
+ return Response(
71
+ content=export_resume_docx(payload),
72
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
73
+ headers={"Content-Disposition": 'attachment; filename="optimized-resume.docx"'},
74
+ )
75
+ if format == "pdf":
76
+ return Response(
77
+ content=export_resume_pdf(payload),
78
+ media_type="application/pdf",
79
+ headers={"Content-Disposition": 'attachment; filename="optimized-resume.pdf"'},
80
+ )
81
+ raise HTTPException(status_code=400, detail="Unsupported resume export format.")
82
+
83
+
84
+ @app.post("/api/export/cover-letter/{format}")
85
+ def export_cover_letter(format: str, payload: CoverLetterExportRequest) -> Response:
86
+ if format == "docx":
87
+ return Response(
88
+ content=export_cover_letter_docx(payload),
89
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
90
+ headers={"Content-Disposition": 'attachment; filename="cover-letter.docx"'},
91
+ )
92
+ if format == "pdf":
93
+ return Response(
94
+ content=export_cover_letter_pdf(payload),
95
+ media_type="application/pdf",
96
+ headers={"Content-Disposition": 'attachment; filename="cover-letter.pdf"'},
97
+ )
98
+ raise HTTPException(status_code=400, detail="Unsupported cover letter export format.")
backend/app/schemas.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PipelineRequest(BaseModel):
7
+ candidate_name: str | None = Field(default=None, max_length=80)
8
+ target_role: str | None = Field(default=None, max_length=120)
9
+ resume_text: str = Field(min_length=40)
10
+ job_description: str = Field(min_length=40)
11
+
12
+
13
+ class ParsingResult(BaseModel):
14
+ resume_source: str
15
+ word_count: int
16
+ bullet_count: int
17
+ detected_sections: list[str]
18
+ normalized_text: str
19
+
20
+
21
+ class AnalysisResult(BaseModel):
22
+ matched_keywords: list[str]
23
+ missing_keywords: list[str]
24
+ skill_gaps: list[str]
25
+ weak_bullets: list[str]
26
+ strengths: list[str]
27
+
28
+
29
+ class OptimizedBullet(BaseModel):
30
+ original: str
31
+ rewritten: str
32
+
33
+
34
+ class OptimizationResult(BaseModel):
35
+ professional_summary: str
36
+ bullet_improvements: list[OptimizedBullet]
37
+ focus_areas: list[str]
38
+
39
+
40
+ class CoverLetterResult(BaseModel):
41
+ opening: str
42
+ body: str
43
+ closing: str
44
+
45
+
46
+ class AtsScoreResult(BaseModel):
47
+ score: int
48
+ matched_count: int
49
+ missing_count: int
50
+ suggestions: list[str]
51
+
52
+
53
+ class PipelineResponse(BaseModel):
54
+ generation_mode: str
55
+ parsing: ParsingResult
56
+ analysis: AnalysisResult
57
+ optimization: OptimizationResult
58
+ cover_letter: CoverLetterResult
59
+ ats_score: AtsScoreResult
60
+
61
+
62
+ class ResumeExportRequest(BaseModel):
63
+ candidate_name: str | None = None
64
+ target_role: str | None = None
65
+ export_style: str = "editorial"
66
+ source_resume_text: str | None = None
67
+ professional_summary: str
68
+ bullet_improvements: list[OptimizedBullet]
69
+ focus_areas: list[str]
70
+ ats_suggestions: list[str]
71
+
72
+
73
+ class CoverLetterExportRequest(BaseModel):
74
+ candidate_name: str | None = None
75
+ target_role: str | None = None
76
+ export_style: str = "editorial"
77
+ opening: str
78
+ body: str
79
+ closing: str
backend/app/services/document_export.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO
4
+
5
+ from docx import Document
6
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
7
+ from docx.oxml import OxmlElement
8
+ from docx.oxml.ns import qn
9
+ from docx.shared import Inches, Pt, RGBColor
10
+ from reportlab.lib import colors
11
+ from reportlab.lib.enums import TA_CENTER
12
+ from reportlab.lib.pagesizes import A4
13
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
14
+ from reportlab.lib.units import inch
15
+ from reportlab.platypus import HRFlowable, ListFlowable, ListItem, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
16
+
17
+ from app.schemas import CoverLetterExportRequest, ResumeExportRequest
18
+
19
+ BRAND_INK = colors.HexColor("#10121A")
20
+ BRAND_PINE = colors.HexColor("#21493F")
21
+ BRAND_CLAY = colors.HexColor("#D7A26C")
22
+ BRAND_SAND = colors.HexColor("#F7F1E7")
23
+ BRAND_MUTED = colors.HexColor("#5B625F")
24
+
25
+ TEMPLATE_STYLES = {
26
+ "editorial": {
27
+ "pdf_accent": BRAND_CLAY,
28
+ "pdf_heading": BRAND_PINE,
29
+ "pdf_meta_background": BRAND_SAND,
30
+ "pdf_meta_border": colors.HexColor("#E0D3BE"),
31
+ "pdf_align": TA_CENTER,
32
+ "docx_align": WD_ALIGN_PARAGRAPH.CENTER,
33
+ "docx_title_color": RGBColor(0x10, 0x12, 0x1A),
34
+ "docx_heading_color": RGBColor(0x21, 0x49, 0x3F),
35
+ "docx_eyebrow_color": RGBColor(0x7A, 0x58, 0x3A),
36
+ },
37
+ "corporate": {
38
+ "pdf_accent": BRAND_PINE,
39
+ "pdf_heading": BRAND_INK,
40
+ "pdf_meta_background": colors.HexColor("#EEF3F1"),
41
+ "pdf_meta_border": colors.HexColor("#C6D6D0"),
42
+ "pdf_align": 0,
43
+ "docx_align": WD_ALIGN_PARAGRAPH.LEFT,
44
+ "docx_title_color": RGBColor(0x21, 0x49, 0x3F),
45
+ "docx_heading_color": RGBColor(0x10, 0x12, 0x1A),
46
+ "docx_eyebrow_color": RGBColor(0x21, 0x49, 0x3F),
47
+ },
48
+ "minimal": {
49
+ "pdf_accent": colors.HexColor("#8B8F92"),
50
+ "pdf_heading": BRAND_INK,
51
+ "pdf_meta_background": colors.white,
52
+ "pdf_meta_border": colors.HexColor("#D7D9DB"),
53
+ "pdf_align": 0,
54
+ "docx_align": WD_ALIGN_PARAGRAPH.LEFT,
55
+ "docx_title_color": RGBColor(0x10, 0x12, 0x1A),
56
+ "docx_heading_color": RGBColor(0x10, 0x12, 0x1A),
57
+ "docx_eyebrow_color": RGBColor(0x5B, 0x62, 0x5F),
58
+ },
59
+ }
60
+
61
+
62
+ def export_resume_docx(payload: ResumeExportRequest) -> bytes:
63
+ document = _create_document(payload.export_style)
64
+ _add_document_header(
65
+ document=document,
66
+ title=payload.candidate_name or "Candidate",
67
+ subtitle=payload.target_role,
68
+ eyebrow="ResumeAI Pro Optimized Resume",
69
+ style_name=payload.export_style,
70
+ )
71
+
72
+ _add_docx_section(document, "Professional Summary", [payload.professional_summary], style_name=payload.export_style, bullets=False)
73
+ _add_docx_section(
74
+ document,
75
+ "Optimized Experience Highlights",
76
+ [item.rewritten for item in payload.bullet_improvements] or ["No optimized bullets were produced for this input."],
77
+ style_name=payload.export_style,
78
+ )
79
+ _add_docx_section(document, "Focus Areas", payload.focus_areas or ["No focus areas returned."], style_name=payload.export_style)
80
+ _add_docx_section(document, "ATS Suggestions", payload.ats_suggestions or ["No ATS suggestions returned."], style_name=payload.export_style)
81
+
82
+ if payload.source_resume_text:
83
+ _add_docx_section(document, "Source Resume Snapshot", [payload.source_resume_text], style_name=payload.export_style, bullets=False)
84
+
85
+ buffer = BytesIO()
86
+ document.save(buffer)
87
+ return buffer.getvalue()
88
+
89
+
90
+ def export_resume_pdf(payload: ResumeExportRequest) -> bytes:
91
+ title = payload.candidate_name or "Candidate"
92
+ subtitle = payload.target_role or ""
93
+ metadata = [
94
+ ("Document", "Optimized Resume"),
95
+ ("Source", "ResumeAI Pro"),
96
+ ("Role", payload.target_role or "Not specified"),
97
+ ]
98
+ sections = [
99
+ ("Professional Summary", [payload.professional_summary], False),
100
+ (
101
+ "Optimized Experience Highlights",
102
+ [item.rewritten for item in payload.bullet_improvements] or ["No optimized bullets were produced for this input."],
103
+ True,
104
+ ),
105
+ ("Focus Areas", payload.focus_areas or ["No focus areas returned."], True),
106
+ ("ATS Suggestions", payload.ats_suggestions or ["No ATS suggestions returned."], True),
107
+ ]
108
+ if payload.source_resume_text:
109
+ sections.append(("Source Resume Snapshot", [payload.source_resume_text], False))
110
+
111
+ return _build_pdf_document(
112
+ title=title,
113
+ subtitle=subtitle,
114
+ eyebrow="ResumeAI Pro Optimized Resume",
115
+ metadata=metadata,
116
+ sections=sections,
117
+ style_name=payload.export_style,
118
+ )
119
+
120
+
121
+ def export_cover_letter_docx(payload: CoverLetterExportRequest) -> bytes:
122
+ document = _create_document(payload.export_style)
123
+ _add_document_header(
124
+ document=document,
125
+ title="Cover Letter",
126
+ subtitle=payload.target_role,
127
+ eyebrow=payload.candidate_name or "ResumeAI Pro",
128
+ style_name=payload.export_style,
129
+ )
130
+ config = _style_config(payload.export_style)
131
+
132
+ for paragraph_text in [payload.opening, payload.body, payload.closing]:
133
+ paragraph = document.add_paragraph()
134
+ paragraph.paragraph_format.space_after = Pt(12)
135
+ run = paragraph.add_run(paragraph_text)
136
+ run.font.size = Pt(11.5)
137
+ run.font.color.rgb = config["docx_title_color"]
138
+
139
+ buffer = BytesIO()
140
+ document.save(buffer)
141
+ return buffer.getvalue()
142
+
143
+
144
+ def export_cover_letter_pdf(payload: CoverLetterExportRequest) -> bytes:
145
+ metadata = [
146
+ ("Document", "Cover Letter"),
147
+ ("Source", "ResumeAI Pro"),
148
+ ("Candidate", payload.candidate_name or "Not specified"),
149
+ ]
150
+ return _build_pdf_document(
151
+ title="Cover Letter",
152
+ subtitle=payload.target_role or "",
153
+ eyebrow=payload.candidate_name or "ResumeAI Pro",
154
+ metadata=metadata,
155
+ sections=[
156
+ ("Opening", [payload.opening], False),
157
+ ("Body", [payload.body], False),
158
+ ("Closing", [payload.closing], False),
159
+ ],
160
+ style_name=payload.export_style,
161
+ )
162
+
163
+
164
+ def _create_document(style_name: str) -> Document:
165
+ config = _style_config(style_name)
166
+ document = Document()
167
+ section = document.sections[0]
168
+ section.top_margin = Inches(0.65)
169
+ section.bottom_margin = Inches(0.65)
170
+ section.left_margin = Inches(0.8)
171
+ section.right_margin = Inches(0.8)
172
+
173
+ normal_style = document.styles["Normal"]
174
+ normal_style.font.name = "Aptos"
175
+ normal_style.font.size = Pt(11)
176
+
177
+ title_style = document.styles["Title"]
178
+ title_style.font.name = "Aptos Display"
179
+ title_style.font.size = Pt(22)
180
+ title_style.font.bold = True
181
+ title_style.font.color.rgb = config["docx_title_color"]
182
+
183
+ heading_style = document.styles["Heading 1"]
184
+ heading_style.font.name = "Aptos"
185
+ heading_style.font.size = Pt(13)
186
+ heading_style.font.bold = True
187
+ heading_style.font.color.rgb = config["docx_heading_color"]
188
+
189
+ return document
190
+
191
+
192
+ def _add_document_header(document: Document, title: str, subtitle: str | None, eyebrow: str, style_name: str) -> None:
193
+ config = _style_config(style_name)
194
+ eyebrow_paragraph = document.add_paragraph()
195
+ eyebrow_paragraph.alignment = config["docx_align"]
196
+ eyebrow_run = eyebrow_paragraph.add_run(eyebrow.upper())
197
+ eyebrow_run.font.size = Pt(8.5)
198
+ eyebrow_run.font.bold = True
199
+ eyebrow_run.font.color.rgb = config["docx_eyebrow_color"]
200
+
201
+ title_paragraph = document.add_paragraph(style="Title")
202
+ title_paragraph.alignment = config["docx_align"]
203
+ title_paragraph.add_run(title)
204
+
205
+ if subtitle:
206
+ subtitle_paragraph = document.add_paragraph()
207
+ subtitle_paragraph.alignment = config["docx_align"]
208
+ subtitle_paragraph.paragraph_format.space_after = Pt(10)
209
+ subtitle_run = subtitle_paragraph.add_run(subtitle)
210
+ subtitle_run.font.size = Pt(10.5)
211
+ subtitle_run.font.color.rgb = RGBColor(0x5B, 0x62, 0x5F)
212
+
213
+ divider = document.add_paragraph()
214
+ divider.paragraph_format.space_after = Pt(10)
215
+ _apply_bottom_border(divider, config["docx_eyebrow_color"])
216
+
217
+
218
+ def _add_docx_section(document: Document, heading: str, items: list[str], style_name: str, bullets: bool = True) -> None:
219
+ config = _style_config(style_name)
220
+ document.add_heading(heading, level=1)
221
+ if bullets:
222
+ for item in items:
223
+ paragraph = document.add_paragraph(style="List Bullet")
224
+ paragraph.paragraph_format.space_after = Pt(4)
225
+ run = paragraph.add_run(item)
226
+ run.font.size = Pt(11)
227
+ run.font.color.rgb = config["docx_title_color"]
228
+ else:
229
+ for item in items:
230
+ paragraph = document.add_paragraph()
231
+ paragraph.paragraph_format.space_after = Pt(10)
232
+ run = paragraph.add_run(item)
233
+ run.font.size = Pt(11)
234
+ run.font.color.rgb = config["docx_title_color"]
235
+
236
+
237
+ def _apply_bottom_border(paragraph, color: RGBColor) -> None:
238
+ paragraph_format = paragraph._p.get_or_add_pPr()
239
+ border = OxmlElement("w:pBdr")
240
+ bottom = OxmlElement("w:bottom")
241
+ bottom.set(qn("w:val"), "single")
242
+ bottom.set(qn("w:sz"), "6")
243
+ bottom.set(qn("w:space"), "1")
244
+ bottom.set(qn("w:color"), f"{color[0]:02X}{color[1]:02X}{color[2]:02X}")
245
+ border.append(bottom)
246
+ paragraph_format.append(border)
247
+
248
+
249
+ def _build_pdf_document(
250
+ title: str,
251
+ subtitle: str,
252
+ eyebrow: str,
253
+ metadata: list[tuple[str, str]],
254
+ sections: list[tuple[str, list[str], bool]],
255
+ style_name: str,
256
+ ) -> bytes:
257
+ config = _style_config(style_name)
258
+ buffer = BytesIO()
259
+ document = SimpleDocTemplate(
260
+ buffer,
261
+ pagesize=A4,
262
+ rightMargin=0.72 * inch,
263
+ leftMargin=0.72 * inch,
264
+ topMargin=0.82 * inch,
265
+ bottomMargin=0.72 * inch,
266
+ )
267
+
268
+ styles = getSampleStyleSheet()
269
+ eyebrow_style = ParagraphStyle(
270
+ "Eyebrow",
271
+ parent=styles["BodyText"],
272
+ fontName="Helvetica-Bold",
273
+ fontSize=8,
274
+ leading=10,
275
+ textColor=config["pdf_accent"],
276
+ alignment=config["pdf_align"],
277
+ spaceAfter=6,
278
+ )
279
+ title_style = ParagraphStyle(
280
+ "ResumeTitle",
281
+ parent=styles["Title"],
282
+ fontName="Helvetica-Bold",
283
+ fontSize=23,
284
+ leading=27,
285
+ alignment=config["pdf_align"],
286
+ textColor=BRAND_INK,
287
+ spaceAfter=6,
288
+ )
289
+ subtitle_style = ParagraphStyle(
290
+ "ResumeSubtitle",
291
+ parent=styles["BodyText"],
292
+ fontName="Helvetica",
293
+ fontSize=10.5,
294
+ leading=14,
295
+ alignment=config["pdf_align"],
296
+ textColor=BRAND_MUTED,
297
+ spaceAfter=14,
298
+ )
299
+ heading_style = ParagraphStyle(
300
+ "SectionHeading",
301
+ parent=styles["Heading2"],
302
+ fontName="Helvetica-Bold",
303
+ fontSize=11.5,
304
+ leading=14,
305
+ textColor=config["pdf_heading"],
306
+ spaceBefore=8,
307
+ spaceAfter=7,
308
+ )
309
+ body_style = ParagraphStyle(
310
+ "SectionBody",
311
+ parent=styles["BodyText"],
312
+ fontName="Helvetica",
313
+ fontSize=10.4,
314
+ leading=15,
315
+ textColor=BRAND_INK,
316
+ spaceAfter=8,
317
+ )
318
+
319
+ story = [
320
+ Paragraph(eyebrow.upper(), eyebrow_style),
321
+ Paragraph(title, title_style),
322
+ ]
323
+ if subtitle:
324
+ story.append(Paragraph(subtitle, subtitle_style))
325
+ story.append(HRFlowable(width="100%", thickness=1, color=config["pdf_accent"], spaceBefore=0, spaceAfter=14))
326
+ story.append(_metadata_table(metadata, style_name))
327
+ story.append(Spacer(1, 12))
328
+
329
+ for heading, items, bullets in sections:
330
+ story.append(Paragraph(heading, heading_style))
331
+ if bullets:
332
+ story.append(
333
+ ListFlowable(
334
+ [
335
+ ListItem(
336
+ Paragraph(item.replace("\n", "<br/>"), body_style),
337
+ bulletColor=config["pdf_heading"],
338
+ )
339
+ for item in items
340
+ ],
341
+ bulletType="bullet",
342
+ leftIndent=16,
343
+ )
344
+ )
345
+ else:
346
+ for item in items:
347
+ story.append(Paragraph(item.replace("\n", "<br/>"), body_style))
348
+ story.append(Spacer(1, 8))
349
+
350
+ document.build(
351
+ story,
352
+ onFirstPage=lambda canvas, doc: _draw_page_frame(canvas, doc, style_name),
353
+ onLaterPages=lambda canvas, doc: _draw_page_frame(canvas, doc, style_name),
354
+ )
355
+ return buffer.getvalue()
356
+
357
+
358
+ def _metadata_table(metadata: list[tuple[str, str]], style_name: str) -> Table:
359
+ config = _style_config(style_name)
360
+ rows = [[f"<b>{label}</b>", value] for label, value in metadata]
361
+ table = Table(rows, colWidths=[1.25 * inch, 4.85 * inch])
362
+ table.setStyle(
363
+ TableStyle(
364
+ [
365
+ ("BACKGROUND", (0, 0), (-1, -1), config["pdf_meta_background"]),
366
+ ("BOX", (0, 0), (-1, -1), 0.6, config["pdf_meta_border"]),
367
+ ("INNERGRID", (0, 0), (-1, -1), 0.35, config["pdf_meta_border"]),
368
+ ("TEXTCOLOR", (0, 0), (-1, -1), BRAND_INK),
369
+ ("LEFTPADDING", (0, 0), (-1, -1), 10),
370
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
371
+ ("TOPPADDING", (0, 0), (-1, -1), 6),
372
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
373
+ ]
374
+ )
375
+ )
376
+ return table
377
+
378
+
379
+ def _draw_page_frame(canvas, document, style_name: str) -> None:
380
+ config = _style_config(style_name)
381
+ canvas.saveState()
382
+ page_width, page_height = A4
383
+
384
+ canvas.setFillColor(BRAND_INK if style_name != "minimal" else colors.HexColor("#EDEDED"))
385
+ canvas.rect(0, page_height - 18, page_width, 18, stroke=0, fill=1)
386
+
387
+ canvas.setFillColor(config["pdf_accent"])
388
+ canvas.rect(document.leftMargin, page_height - 48, 115, 7, stroke=0, fill=1)
389
+
390
+ canvas.setStrokeColor(config["pdf_meta_border"])
391
+ canvas.line(document.leftMargin, 34, page_width - document.rightMargin, 34)
392
+
393
+ canvas.setFont("Helvetica", 8.5)
394
+ canvas.setFillColor(BRAND_MUTED)
395
+ canvas.drawString(document.leftMargin, 20, "Generated by ResumeAI Pro")
396
+ canvas.drawRightString(page_width - document.rightMargin, 20, f"Page {canvas.getPageNumber()}")
397
+ canvas.restoreState()
398
+
399
+
400
+ def _style_config(style_name: str) -> dict:
401
+ return TEMPLATE_STYLES.get(style_name, TEMPLATE_STYLES["editorial"])
backend/app/services/openai_generation.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from openai import OpenAI
5
+
6
+ from app.config import settings
7
+ from app.schemas import AnalysisResult, CoverLetterResult, OptimizationResult, OptimizedBullet, PipelineRequest
8
+
9
+
10
+ class LLMBulletImprovement(BaseModel):
11
+ original: str
12
+ rewritten: str
13
+
14
+
15
+ class LLMCoverLetter(BaseModel):
16
+ opening: str
17
+ body: str
18
+ closing: str
19
+
20
+
21
+ class LLMEnhancementPackage(BaseModel):
22
+ professional_summary: str
23
+ focus_areas: list[str]
24
+ bullet_improvements: list[LLMBulletImprovement]
25
+ cover_letter: LLMCoverLetter
26
+
27
+
28
+ def generate_openai_enhancements(
29
+ request: PipelineRequest,
30
+ analysis: AnalysisResult,
31
+ ) -> tuple[OptimizationResult, CoverLetterResult] | None:
32
+ if not settings.use_openai_when_available or not settings.openai_api_key:
33
+ return None
34
+
35
+ client = OpenAI(api_key=settings.openai_api_key)
36
+ response = client.responses.parse(
37
+ model=settings.openai_model,
38
+ input=[
39
+ {
40
+ "role": "system",
41
+ "content": (
42
+ "You are ResumeAI Pro, an expert resume strategist. Strengthen resume messaging without inventing "
43
+ "employers, projects, tools, or metrics that are not supported by the source material. Keep the "
44
+ "output recruiter-friendly, ATS-aware, and concise."
45
+ ),
46
+ },
47
+ {
48
+ "role": "user",
49
+ "content": _build_prompt(request, analysis),
50
+ },
51
+ ],
52
+ text_format=LLMEnhancementPackage,
53
+ )
54
+
55
+ parsed = response.output_parsed
56
+ if parsed is None:
57
+ return None
58
+
59
+ optimization = OptimizationResult(
60
+ professional_summary=parsed.professional_summary,
61
+ bullet_improvements=[
62
+ OptimizedBullet(original=item.original, rewritten=item.rewritten)
63
+ for item in parsed.bullet_improvements
64
+ ],
65
+ focus_areas=parsed.focus_areas,
66
+ )
67
+ cover_letter = CoverLetterResult(
68
+ opening=parsed.cover_letter.opening,
69
+ body=parsed.cover_letter.body,
70
+ closing=parsed.cover_letter.closing,
71
+ )
72
+
73
+ return optimization, cover_letter
74
+
75
+
76
+ def _build_prompt(request: PipelineRequest, analysis: AnalysisResult) -> str:
77
+ role = request.target_role or "the target role"
78
+ candidate_name = request.candidate_name or "the candidate"
79
+
80
+ return f"""
81
+ Candidate name: {candidate_name}
82
+ Target role: {role}
83
+
84
+ Resume text:
85
+ {request.resume_text}
86
+
87
+ Job description:
88
+ {request.job_description}
89
+
90
+ Matched keywords:
91
+ {", ".join(analysis.matched_keywords) or "None"}
92
+
93
+ Missing keywords:
94
+ {", ".join(analysis.missing_keywords) or "None"}
95
+
96
+ Weak bullets:
97
+ {chr(10).join(analysis.weak_bullets) or "None"}
98
+
99
+ Create:
100
+ 1. A professional summary tailored to the role
101
+ 2. Up to 3 improved resume bullets based on the weak bullets
102
+ 3. Three short cover-letter sections: opening, body, closing
103
+ 4. A short list of focus areas for the candidate to improve
104
+ """.strip()
backend/app/services/pdf.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO
4
+
5
+ from pypdf import PdfReader
6
+
7
+
8
+ def extract_text_from_pdf(file_bytes: bytes) -> str:
9
+ reader = PdfReader(BytesIO(file_bytes))
10
+ pages = [page.extract_text() or "" for page in reader.pages]
11
+ text = "\n".join(page.strip() for page in pages if page.strip())
12
+
13
+ if not text:
14
+ raise ValueError("No readable text could be extracted from the uploaded PDF.")
15
+
16
+ return text
backend/app/services/pipeline.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from app.agents.analysis import analyze_resume
6
+ from app.agents.cover_letter import generate_cover_letter
7
+ from app.agents.optimization import optimize_resume
8
+ from app.agents.parsing import parse_resume
9
+ from app.agents.scoring import score_resume
10
+ from app.services.openai_generation import generate_openai_enhancements
11
+ from app.schemas import PipelineRequest, PipelineResponse
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def run_pipeline(payload: PipelineRequest, resume_source: str = "text") -> PipelineResponse:
17
+ parsing = parse_resume(payload.resume_text, resume_source=resume_source)
18
+ analysis = analyze_resume(parsing, payload.job_description)
19
+ generation_mode = "heuristic"
20
+
21
+ optimization = optimize_resume(payload, analysis)
22
+ cover_letter = generate_cover_letter(payload, analysis)
23
+
24
+ try:
25
+ llm_enhancements = generate_openai_enhancements(payload, analysis)
26
+ if llm_enhancements is not None:
27
+ optimization, cover_letter = llm_enhancements
28
+ generation_mode = "openai"
29
+ except Exception: # noqa: BLE001
30
+ logger.exception("OpenAI enhancement failed; falling back to heuristic pipeline.")
31
+
32
+ ats_score = score_resume(analysis)
33
+
34
+ return PipelineResponse(
35
+ generation_mode=generation_mode,
36
+ parsing=parsing,
37
+ analysis=analysis,
38
+ optimization=optimization,
39
+ cover_letter=cover_letter,
40
+ ats_score=ats_score,
41
+ )
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn[standard]==0.34.0
3
+ pydantic-settings==2.8.1
4
+ openai==2.30.0
5
+ pypdf==6.9.2
6
+ python-multipart==0.0.22
7
+ python-docx==1.2.0
8
+ reportlab==4.4.10
components/demo-console.tsx ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { FormEvent } from "react";
4
+ import { useState } from "react";
5
+
6
+ type ExportStyle = "editorial" | "corporate" | "minimal";
7
+
8
+ type PipelineResponse = {
9
+ generation_mode: string;
10
+ parsing: {
11
+ resume_source: string;
12
+ };
13
+ analysis: {
14
+ matched_keywords: string[];
15
+ missing_keywords: string[];
16
+ weak_bullets: string[];
17
+ };
18
+ optimization: {
19
+ professional_summary: string;
20
+ focus_areas: string[];
21
+ bullet_improvements: Array<{
22
+ original: string;
23
+ rewritten: string;
24
+ }>;
25
+ };
26
+ cover_letter: {
27
+ opening: string;
28
+ body: string;
29
+ closing: string;
30
+ };
31
+ ats_score: {
32
+ score: number;
33
+ suggestions: string[];
34
+ };
35
+ };
36
+
37
+ const sampleResume = `SUMMARY
38
+ AI-focused full-stack developer building resume and career tools with FastAPI, Next.js, and OpenAI API workflows.
39
+
40
+ EXPERIENCE
41
+ - Built resume tooling for students and job seekers
42
+ - Improved dashboard performance by 28% across multiple user journeys
43
+ - Worked with Python, FastAPI, Next.js, Tailwind CSS, Docker, and REST APIs
44
+
45
+ SKILLS
46
+ Python, FastAPI, Next.js, Tailwind CSS, Docker, OpenAI API, JavaScript`;
47
+
48
+ const sampleJobDescription = `We are hiring an AI Engineer to build automation systems for career products. The ideal candidate has experience with FastAPI, Next.js, OpenAI API, ATS scoring, keyword analysis, stakeholder communication, and Docker-based deployment.`;
49
+ const exportStyles: ExportStyle[] = ["editorial", "corporate", "minimal"];
50
+
51
+ export function DemoConsole() {
52
+ const [candidateName, setCandidateName] = useState("Aarav Sharma");
53
+ const [targetRole, setTargetRole] = useState("AI Engineer");
54
+ const [resumeText, setResumeText] = useState(sampleResume);
55
+ const [jobDescription, setJobDescription] = useState(sampleJobDescription);
56
+ const [resumeFile, setResumeFile] = useState<File | null>(null);
57
+ const [result, setResult] = useState<PipelineResponse | null>(null);
58
+ const [error, setError] = useState<string | null>(null);
59
+ const [isLoading, setIsLoading] = useState(false);
60
+ const [isExporting, setIsExporting] = useState(false);
61
+ const [exportStyle, setExportStyle] = useState<ExportStyle>("editorial");
62
+ const [includeFocusAreas, setIncludeFocusAreas] = useState(true);
63
+ const [includeAtsSuggestions, setIncludeAtsSuggestions] = useState(true);
64
+ const [includeSourceResume, setIncludeSourceResume] = useState(true);
65
+ const [selectedBullets, setSelectedBullets] = useState<Record<number, boolean>>({});
66
+ const [editableSummary, setEditableSummary] = useState("");
67
+ const [editableBullets, setEditableBullets] = useState<Array<{ original: string; rewritten: string }>>([]);
68
+ const [editableFocusAreas, setEditableFocusAreas] = useState<string[]>([]);
69
+ const [editableAtsSuggestions, setEditableAtsSuggestions] = useState<string[]>([]);
70
+ const [editableCoverLetter, setEditableCoverLetter] = useState({
71
+ opening: "",
72
+ body: "",
73
+ closing: "",
74
+ });
75
+
76
+ const selectedBulletImprovements = editableBullets.filter((_, index) => selectedBullets[index] ?? true);
77
+
78
+ const optimizedResumeText = result
79
+ ? buildOptimizedResume({
80
+ candidateName,
81
+ targetRole,
82
+ resumeText,
83
+ professionalSummary: editableSummary,
84
+ bulletImprovements: selectedBulletImprovements,
85
+ focusAreas: includeFocusAreas ? editableFocusAreas : [],
86
+ atsSuggestions: includeAtsSuggestions ? editableAtsSuggestions : [],
87
+ includeSourceResume,
88
+ })
89
+ : "";
90
+
91
+ async function handleSubmit(event: FormEvent<HTMLFormElement>) {
92
+ event.preventDefault();
93
+ setIsLoading(true);
94
+ setError(null);
95
+
96
+ try {
97
+ let response: Response;
98
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || "";
99
+
100
+ if (resumeFile) {
101
+ const formData = new FormData();
102
+ formData.append("file", resumeFile);
103
+ formData.append("candidate_name", candidateName);
104
+ formData.append("target_role", targetRole);
105
+ formData.append("job_description", jobDescription);
106
+
107
+ response = await fetch(`${apiBaseUrl}/api/pipeline/upload`, {
108
+ method: "POST",
109
+ body: formData,
110
+ });
111
+ } else {
112
+ response = await fetch(`${apiBaseUrl}/api/pipeline`, {
113
+ method: "POST",
114
+ headers: {
115
+ "Content-Type": "application/json",
116
+ },
117
+ body: JSON.stringify({
118
+ candidate_name: candidateName,
119
+ target_role: targetRole,
120
+ resume_text: resumeText,
121
+ job_description: jobDescription,
122
+ }),
123
+ });
124
+ }
125
+
126
+ if (!response.ok) {
127
+ const payload = (await response.json().catch(() => null)) as { detail?: string } | null;
128
+ throw new Error(payload?.detail ?? "The analysis request failed. Start the FastAPI backend and try again.");
129
+ }
130
+
131
+ const data = (await response.json()) as PipelineResponse;
132
+ setResult(data);
133
+ setEditableSummary(data.optimization.professional_summary);
134
+ setEditableBullets(data.optimization.bullet_improvements);
135
+ setEditableFocusAreas(data.optimization.focus_areas);
136
+ setEditableAtsSuggestions(data.ats_score.suggestions);
137
+ setEditableCoverLetter(data.cover_letter);
138
+ setSelectedBullets(
139
+ Object.fromEntries(data.optimization.bullet_improvements.map((_, index) => [index, true])),
140
+ );
141
+ } catch (submitError) {
142
+ const message =
143
+ submitError instanceof Error
144
+ ? submitError.message
145
+ : "Something went wrong while contacting the backend.";
146
+ setError(message);
147
+ setResult(null);
148
+ } finally {
149
+ setIsLoading(false);
150
+ }
151
+ }
152
+
153
+ function loadSampleData() {
154
+ setCandidateName("Aarav Sharma");
155
+ setTargetRole("AI Engineer");
156
+ setResumeText(sampleResume);
157
+ setJobDescription(sampleJobDescription);
158
+ setResumeFile(null);
159
+ setError(null);
160
+ setExportStyle("editorial");
161
+ setIncludeFocusAreas(true);
162
+ setIncludeAtsSuggestions(true);
163
+ setIncludeSourceResume(true);
164
+ setEditableSummary("");
165
+ setEditableBullets([]);
166
+ setEditableFocusAreas([]);
167
+ setEditableAtsSuggestions([]);
168
+ setEditableCoverLetter({
169
+ opening: "",
170
+ body: "",
171
+ closing: "",
172
+ });
173
+ setSelectedBullets({});
174
+ }
175
+
176
+ function downloadTextFile(filename: string, content: string) {
177
+ const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
178
+ const url = URL.createObjectURL(blob);
179
+ const anchor = document.createElement("a");
180
+ anchor.href = url;
181
+ anchor.download = filename;
182
+ anchor.click();
183
+ URL.revokeObjectURL(url);
184
+ }
185
+
186
+ async function downloadGeneratedDocument(kind: "resume" | "cover-letter", format: "pdf" | "docx") {
187
+ if (!result) {
188
+ return;
189
+ }
190
+
191
+ setIsExporting(true);
192
+ setError(null);
193
+
194
+ try {
195
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || "";
196
+ const endpoint =
197
+ kind === "resume" ? `/api/export/resume/${format}` : `/api/export/cover-letter/${format}`;
198
+ const payload =
199
+ kind === "resume"
200
+ ? {
201
+ candidate_name: candidateName,
202
+ target_role: targetRole,
203
+ export_style: exportStyle,
204
+ source_resume_text: includeSourceResume ? resumeText || "Source resume uploaded as PDF." : null,
205
+ professional_summary: editableSummary,
206
+ bullet_improvements: selectedBulletImprovements,
207
+ focus_areas: includeFocusAreas ? editableFocusAreas : [],
208
+ ats_suggestions: includeAtsSuggestions ? editableAtsSuggestions : [],
209
+ }
210
+ : {
211
+ candidate_name: candidateName,
212
+ target_role: targetRole,
213
+ export_style: exportStyle,
214
+ opening: editableCoverLetter.opening,
215
+ body: editableCoverLetter.body,
216
+ closing: editableCoverLetter.closing,
217
+ };
218
+
219
+ const response = await fetch(`${apiBaseUrl}${endpoint}`, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ },
224
+ body: JSON.stringify(payload),
225
+ });
226
+
227
+ if (!response.ok) {
228
+ const payload = (await response.json().catch(() => null)) as { detail?: string } | null;
229
+ throw new Error(payload?.detail ?? "The export request failed.");
230
+ }
231
+
232
+ const blob = await response.blob();
233
+ const url = URL.createObjectURL(blob);
234
+ const anchor = document.createElement("a");
235
+ anchor.href = url;
236
+ anchor.download = kind === "resume" ? `optimized-resume.${format}` : `cover-letter.${format}`;
237
+ anchor.click();
238
+ URL.revokeObjectURL(url);
239
+ } catch (exportError) {
240
+ const message =
241
+ exportError instanceof Error ? exportError.message : "Something went wrong while exporting the document.";
242
+ setError(message);
243
+ } finally {
244
+ setIsExporting(false);
245
+ }
246
+ }
247
+
248
+ return (
249
+ <section className="mx-auto max-w-7xl px-6 py-20 md:px-10 lg:px-12">
250
+ <div className="section-intro">
251
+ <p className="eyebrow">Interactive Demo</p>
252
+ <h2 className="section-title">Run the analysis pipeline with a live frontend-to-backend request.</h2>
253
+ <p className="section-copy">
254
+ This demo posts resume text or a PDF resume to the FastAPI pipeline, then turns the response into recruiter-style
255
+ comparisons, downloadable outputs, and a polished cover-letter draft.
256
+ </p>
257
+ </div>
258
+
259
+ <div className="mt-10 grid gap-6 xl:grid-cols-[1.02fr_0.98fr]">
260
+ <form onSubmit={handleSubmit} className="rounded-[2rem] border border-black/8 bg-white/75 p-7 shadow-card backdrop-blur">
261
+ <div className="grid gap-4 md:grid-cols-2">
262
+ <label className="space-y-2 text-sm text-ink/72">
263
+ <span className="font-medium text-ink">Candidate name</span>
264
+ <input
265
+ value={candidateName}
266
+ onChange={(event) => setCandidateName(event.target.value)}
267
+ className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-sm text-ink outline-none transition focus:border-pine"
268
+ />
269
+ </label>
270
+
271
+ <label className="space-y-2 text-sm text-ink/72">
272
+ <span className="font-medium text-ink">Target role</span>
273
+ <input
274
+ value={targetRole}
275
+ onChange={(event) => setTargetRole(event.target.value)}
276
+ className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-sm text-ink outline-none transition focus:border-pine"
277
+ />
278
+ </label>
279
+ </div>
280
+
281
+ <div className="mt-5 grid gap-5">
282
+ <label className="space-y-2 text-sm text-ink/72">
283
+ <span className="font-medium text-ink">Resume PDF</span>
284
+ <input
285
+ type="file"
286
+ accept=".pdf,application/pdf"
287
+ onChange={(event) => setResumeFile(event.target.files?.[0] ?? null)}
288
+ className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-sm text-ink outline-none transition file:mr-4 file:rounded-full file:border-0 file:bg-ink file:px-4 file:py-2 file:text-sm file:font-medium file:text-sand"
289
+ />
290
+ <p className="text-xs leading-6 text-ink/55">
291
+ Upload a PDF to test parsing, or leave this empty and paste resume text below.
292
+ </p>
293
+ </label>
294
+
295
+ <label className="space-y-2 text-sm text-ink/72">
296
+ <span className="font-medium text-ink">Resume text</span>
297
+ <textarea
298
+ value={resumeText}
299
+ onChange={(event) => setResumeText(event.target.value)}
300
+ rows={12}
301
+ placeholder="Paste resume text here if you are not uploading a PDF."
302
+ className="w-full rounded-[1.5rem] border border-black/10 bg-white px-4 py-4 text-sm leading-7 text-ink outline-none transition focus:border-pine"
303
+ />
304
+ </label>
305
+
306
+ <label className="space-y-2 text-sm text-ink/72">
307
+ <span className="font-medium text-ink">Job description</span>
308
+ <textarea
309
+ value={jobDescription}
310
+ onChange={(event) => setJobDescription(event.target.value)}
311
+ rows={8}
312
+ className="w-full rounded-[1.5rem] border border-black/10 bg-white px-4 py-4 text-sm leading-7 text-ink outline-none transition focus:border-pine"
313
+ />
314
+ </label>
315
+ </div>
316
+
317
+ <div className="mt-6 flex flex-wrap gap-3">
318
+ <button
319
+ type="submit"
320
+ disabled={isLoading}
321
+ className="rounded-full bg-ink px-6 py-3 text-sm font-medium text-sand transition hover:bg-pine disabled:cursor-not-allowed disabled:opacity-70"
322
+ >
323
+ {isLoading ? "Running analysis..." : "Run pipeline"}
324
+ </button>
325
+ <button
326
+ type="button"
327
+ onClick={loadSampleData}
328
+ className="rounded-full border border-ink/15 bg-white px-6 py-3 text-sm font-medium text-ink transition hover:border-ink/30"
329
+ >
330
+ Load sample content
331
+ </button>
332
+ </div>
333
+
334
+ {error ? (
335
+ <div className="mt-5 rounded-[1.5rem] border border-wine/20 bg-[#fff3f0] px-4 py-4 text-sm leading-7 text-wine">
336
+ {error}
337
+ </div>
338
+ ) : null}
339
+ </form>
340
+
341
+ <div className="rounded-[2rem] bg-ink p-7 text-sand shadow-card">
342
+ <div className="flex items-center justify-between gap-4 border-b border-white/10 pb-5">
343
+ <div>
344
+ <p className="text-xs uppercase tracking-[0.25em] text-sand/65">Pipeline output</p>
345
+ <h3 className="mt-2 text-2xl font-semibold">Analysis results</h3>
346
+ </div>
347
+ <div className="rounded-full border border-white/10 px-4 py-2 text-xs uppercase tracking-[0.2em] text-sand/70">
348
+ Live API
349
+ </div>
350
+ </div>
351
+
352
+ {result ? (
353
+ <div className="mt-6 space-y-5">
354
+ <div className="rounded-[1.75rem] bg-sand p-5 text-ink">
355
+ <div className="flex flex-wrap items-center justify-between gap-3">
356
+ <p className="text-xs uppercase tracking-[0.2em] text-ink/60">ATS score</p>
357
+ <div className="flex flex-wrap gap-2 text-[11px] uppercase tracking-[0.18em]">
358
+ <span className="rounded-full bg-ink px-3 py-1 text-sand">{result.generation_mode}</span>
359
+ <span className="rounded-full border border-ink/10 px-3 py-1 text-ink/70">{result.parsing.resume_source}</span>
360
+ </div>
361
+ </div>
362
+ <p className="mt-3 text-4xl font-bold">{result.ats_score.score}%</p>
363
+ <textarea
364
+ value={editableSummary}
365
+ onChange={(event) => setEditableSummary(event.target.value)}
366
+ rows={4}
367
+ className="mt-3 w-full rounded-[1.25rem] border border-black/10 bg-white px-4 py-3 text-sm leading-6 text-ink/72 outline-none transition focus:border-pine"
368
+ />
369
+
370
+ <div className="mt-5 flex flex-wrap gap-3">
371
+ <button
372
+ type="button"
373
+ onClick={() => downloadGeneratedDocument("resume", "pdf")}
374
+ className="rounded-full bg-ink px-4 py-2 text-sm font-medium text-sand transition hover:bg-pine"
375
+ disabled={isExporting}
376
+ >
377
+ {isExporting ? "Preparing..." : "Resume PDF"}
378
+ </button>
379
+ <button
380
+ type="button"
381
+ onClick={() => downloadGeneratedDocument("resume", "docx")}
382
+ className="rounded-full border border-ink/15 bg-white px-4 py-2 text-sm font-medium text-ink transition hover:border-ink/30"
383
+ disabled={isExporting}
384
+ >
385
+ Resume DOCX
386
+ </button>
387
+ <button
388
+ type="button"
389
+ onClick={() => downloadGeneratedDocument("cover-letter", "pdf")}
390
+ className="rounded-full border border-ink/15 bg-white px-4 py-2 text-sm font-medium text-ink transition hover:border-ink/30"
391
+ disabled={isExporting}
392
+ >
393
+ Letter PDF
394
+ </button>
395
+ <button
396
+ type="button"
397
+ onClick={() => downloadGeneratedDocument("cover-letter", "docx")}
398
+ className="rounded-full border border-ink/15 bg-white px-4 py-2 text-sm font-medium text-ink transition hover:border-ink/30"
399
+ disabled={isExporting}
400
+ >
401
+ Letter DOCX
402
+ </button>
403
+ </div>
404
+ </div>
405
+
406
+ <div className="grid gap-4 md:grid-cols-2">
407
+ <div className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5">
408
+ <p className="text-xs uppercase tracking-[0.2em] text-sand/60">Matched keywords</p>
409
+ <div className="mt-3 flex flex-wrap gap-2">
410
+ {result.analysis.matched_keywords.slice(0, 6).map((keyword) => (
411
+ <span key={keyword} className="rounded-full bg-white/10 px-3 py-1 text-xs text-sand/85">
412
+ {keyword}
413
+ </span>
414
+ ))}
415
+ </div>
416
+ </div>
417
+
418
+ <div className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5">
419
+ <p className="text-xs uppercase tracking-[0.2em] text-sand/60">Missing keywords</p>
420
+ <div className="mt-3 flex flex-wrap gap-2">
421
+ {result.analysis.missing_keywords.slice(0, 6).map((keyword) => (
422
+ <span key={keyword} className="rounded-full bg-clay px-3 py-1 text-xs text-ink">
423
+ {keyword}
424
+ </span>
425
+ ))}
426
+ </div>
427
+ </div>
428
+ </div>
429
+
430
+ <div className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5">
431
+ <p className="text-xs uppercase tracking-[0.2em] text-sand/60">Optimization focus</p>
432
+ <div className="mt-3 space-y-3">
433
+ {editableFocusAreas.map((area, index) => (
434
+ <input
435
+ key={`${area}-${index}`}
436
+ value={area}
437
+ onChange={(event) =>
438
+ setEditableFocusAreas((current) =>
439
+ current.map((item, itemIndex) => (itemIndex === index ? event.target.value : item)),
440
+ )
441
+ }
442
+ className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 text-sm leading-6 text-sand/82 outline-none transition focus:border-white/30"
443
+ />
444
+ ))}
445
+ </div>
446
+ </div>
447
+
448
+ <div className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5">
449
+ <p className="text-xs uppercase tracking-[0.2em] text-sand/60">Export controls</p>
450
+ <div className="mt-4 flex flex-wrap gap-2">
451
+ {exportStyles.map((style) => (
452
+ <button
453
+ key={style}
454
+ type="button"
455
+ onClick={() => setExportStyle(style)}
456
+ className={`rounded-full px-3 py-2 text-xs uppercase tracking-[0.18em] transition ${
457
+ exportStyle === style
458
+ ? "bg-sand text-ink"
459
+ : "border border-white/10 bg-white/5 text-sand/78 hover:bg-white/10"
460
+ }`}
461
+ >
462
+ {style}
463
+ </button>
464
+ ))}
465
+ </div>
466
+
467
+ <div className="mt-4 grid gap-3 text-sm text-sand/82 sm:grid-cols-2">
468
+ <label className="flex items-center gap-3">
469
+ <input
470
+ type="checkbox"
471
+ checked={includeFocusAreas}
472
+ onChange={(event) => setIncludeFocusAreas(event.target.checked)}
473
+ />
474
+ <span>Include focus areas</span>
475
+ </label>
476
+ <label className="flex items-center gap-3">
477
+ <input
478
+ type="checkbox"
479
+ checked={includeAtsSuggestions}
480
+ onChange={(event) => setIncludeAtsSuggestions(event.target.checked)}
481
+ />
482
+ <span>Include ATS suggestions</span>
483
+ </label>
484
+ <label className="flex items-center gap-3 sm:col-span-2">
485
+ <input
486
+ type="checkbox"
487
+ checked={includeSourceResume}
488
+ onChange={(event) => setIncludeSourceResume(event.target.checked)}
489
+ />
490
+ <span>Include source resume snapshot</span>
491
+ </label>
492
+ </div>
493
+ </div>
494
+
495
+ <div className="rounded-[1.5rem] border border-white/10 bg-white/5 p-5">
496
+ <p className="text-xs uppercase tracking-[0.2em] text-sand/60">ATS suggestions</p>
497
+ <div className="mt-3 space-y-3">
498
+ {editableAtsSuggestions.map((suggestion, index) => (
499
+ <textarea
500
+ key={`${suggestion}-${index}`}
501
+ value={suggestion}
502
+ onChange={(event) =>
503
+ setEditableAtsSuggestions((current) =>
504
+ current.map((item, itemIndex) => (itemIndex === index ? event.target.value : item)),
505
+ )
506
+ }
507
+ rows={2}
508
+ className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 text-sm leading-6 text-sand/82 outline-none transition focus:border-white/30"
509
+ />
510
+ ))}
511
+ </div>
512
+ </div>
513
+ </div>
514
+ ) : (
515
+ <div className="mt-6 rounded-[1.75rem] border border-dashed border-white/15 bg-white/5 p-6 text-sm leading-7 text-sand/72">
516
+ Run the pipeline to see matched keywords, missing terms, optimization focus areas, and ATS suggestions here.
517
+ </div>
518
+ )}
519
+ </div>
520
+ </div>
521
+
522
+ {result ? (
523
+ <div className="mt-6 grid gap-6 xl:grid-cols-[1fr_1fr]">
524
+ <div className="rounded-[2rem] border border-black/8 bg-white/80 p-7 shadow-card backdrop-blur">
525
+ <p className="text-sm uppercase tracking-[0.25em] text-pine/70">Before And After</p>
526
+ <div className="mt-5 space-y-4">
527
+ {editableBullets.length > 0 ? (
528
+ editableBullets.map((item, index) => (
529
+ <div key={`${item.original}-${index}`} className="grid gap-4 rounded-[1.5rem] bg-sand p-5 lg:grid-cols-2">
530
+ <div>
531
+ <label className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-[11px] uppercase tracking-[0.16em] text-ink/65">
532
+ <input
533
+ type="checkbox"
534
+ checked={selectedBullets[index] ?? true}
535
+ onChange={() =>
536
+ setSelectedBullets((current) => ({
537
+ ...current,
538
+ [index]: !(current[index] ?? true),
539
+ }))
540
+ }
541
+ />
542
+ Include in export
543
+ </label>
544
+ <p className="text-xs uppercase tracking-[0.18em] text-ink/55">Original bullet</p>
545
+ <p className="mt-3 text-sm leading-7 text-ink/74">{item.original}</p>
546
+ </div>
547
+ <div>
548
+ <p className="text-xs uppercase tracking-[0.18em] text-ink/55">Optimized bullet</p>
549
+ <textarea
550
+ value={item.rewritten}
551
+ onChange={(event) =>
552
+ setEditableBullets((current) =>
553
+ current.map((bullet, bulletIndex) =>
554
+ bulletIndex === index ? { ...bullet, rewritten: event.target.value } : bullet,
555
+ ),
556
+ )
557
+ }
558
+ rows={5}
559
+ className="mt-3 w-full rounded-[1.25rem] border border-black/10 bg-white px-4 py-3 text-sm leading-7 text-ink outline-none transition focus:border-pine"
560
+ />
561
+ </div>
562
+ </div>
563
+ ))
564
+ ) : (
565
+ <div className="rounded-[1.5rem] bg-sand p-5 text-sm leading-7 text-ink/72">
566
+ No bullet rewrites were generated for this input. Try a resume with more experience bullets to see a side-by-side comparison.
567
+ </div>
568
+ )}
569
+ </div>
570
+ </div>
571
+
572
+ <div className="rounded-[2rem] border border-black/8 bg-white/80 p-7 shadow-card backdrop-blur">
573
+ <p className="text-sm uppercase tracking-[0.25em] text-pine/70">Generated Cover Letter</p>
574
+ <div className="mt-5 rounded-[1.5rem] bg-[#f4eee3] p-6">
575
+ <textarea
576
+ value={editableCoverLetter.opening}
577
+ onChange={(event) =>
578
+ setEditableCoverLetter((current) => ({
579
+ ...current,
580
+ opening: event.target.value,
581
+ }))
582
+ }
583
+ rows={4}
584
+ className="w-full rounded-[1.25rem] border border-black/10 bg-white px-4 py-3 text-sm leading-8 text-ink/82 outline-none transition focus:border-pine"
585
+ />
586
+ <textarea
587
+ value={editableCoverLetter.body}
588
+ onChange={(event) =>
589
+ setEditableCoverLetter((current) => ({
590
+ ...current,
591
+ body: event.target.value,
592
+ }))
593
+ }
594
+ rows={6}
595
+ className="mt-4 w-full rounded-[1.25rem] border border-black/10 bg-white px-4 py-3 text-sm leading-8 text-ink/82 outline-none transition focus:border-pine"
596
+ />
597
+ <textarea
598
+ value={editableCoverLetter.closing}
599
+ onChange={(event) =>
600
+ setEditableCoverLetter((current) => ({
601
+ ...current,
602
+ closing: event.target.value,
603
+ }))
604
+ }
605
+ rows={4}
606
+ className="mt-4 w-full rounded-[1.25rem] border border-black/10 bg-white px-4 py-3 text-sm leading-8 text-ink/82 outline-none transition focus:border-pine"
607
+ />
608
+ </div>
609
+ </div>
610
+
611
+ <div className="rounded-[2rem] border border-black/8 bg-white/80 p-7 shadow-card backdrop-blur xl:col-span-2">
612
+ <div className="flex flex-wrap items-center justify-between gap-4">
613
+ <div>
614
+ <p className="text-sm uppercase tracking-[0.25em] text-pine/70">Optimized Resume Draft</p>
615
+ <p className="mt-2 text-sm leading-7 text-ink/68">
616
+ A simple export-ready draft assembled from the pipeline output for quick iteration or handoff.
617
+ </p>
618
+ </div>
619
+ <button
620
+ type="button"
621
+ onClick={() => downloadTextFile("optimized-resume.txt", optimizedResumeText)}
622
+ className="rounded-full bg-ink px-4 py-2 text-sm font-medium text-sand transition hover:bg-pine"
623
+ >
624
+ Download .txt
625
+ </button>
626
+ </div>
627
+
628
+ <pre className="mt-5 overflow-x-auto rounded-[1.5rem] bg-ink p-6 text-sm leading-7 text-sand whitespace-pre-wrap">
629
+ {optimizedResumeText}
630
+ </pre>
631
+ </div>
632
+ </div>
633
+ ) : null}
634
+ </section>
635
+ );
636
+ }
637
+
638
+ function buildOptimizedResume({
639
+ candidateName,
640
+ targetRole,
641
+ resumeText,
642
+ professionalSummary,
643
+ bulletImprovements,
644
+ focusAreas,
645
+ atsSuggestions,
646
+ includeSourceResume,
647
+ }: {
648
+ candidateName: string;
649
+ targetRole: string;
650
+ resumeText: string;
651
+ professionalSummary: string;
652
+ bulletImprovements: Array<{ rewritten: string }>;
653
+ focusAreas: string[];
654
+ atsSuggestions: string[];
655
+ includeSourceResume: boolean;
656
+ }) {
657
+ const sections = [
658
+ candidateName || "Candidate",
659
+ targetRole ? `Target Role: ${targetRole}` : "",
660
+ "",
661
+ "PROFESSIONAL SUMMARY",
662
+ professionalSummary,
663
+ "",
664
+ "OPTIMIZED EXPERIENCE HIGHLIGHTS",
665
+ ...(bulletImprovements.length > 0 ? bulletImprovements.map((item) => `- ${item.rewritten}`) : ["- No bullets selected for export."]),
666
+ ...(focusAreas.length > 0 ? ["", "FOCUS AREAS", ...focusAreas.map((area) => `- ${area}`)] : []),
667
+ ...(atsSuggestions.length > 0 ? ["", "ATS NOTES", ...atsSuggestions.map((suggestion) => `- ${suggestion}`)] : []),
668
+ ...(includeSourceResume ? ["", "SOURCE RESUME", resumeText || "Resume text came from uploaded PDF."] : []),
669
+ ];
670
+
671
+ return sections.filter((section, index, array) => !(section === "" && array[index - 1] === "")).join("\n");
672
+ }
docker-compose.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ frontend:
3
+ build:
4
+ context: .
5
+ dockerfile: frontend.Dockerfile
6
+ environment:
7
+ BACKEND_API_URL: http://backend:8000
8
+ ports:
9
+ - "3000:3000"
10
+ depends_on:
11
+ backend:
12
+ condition: service_healthy
13
+ healthcheck:
14
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
15
+ interval: 20s
16
+ timeout: 5s
17
+ retries: 5
18
+
19
+ backend:
20
+ build:
21
+ context: ./backend
22
+ dockerfile: Dockerfile
23
+ environment:
24
+ APP_ENV: development
25
+ ALLOWED_ORIGINS: '["http://localhost:3000","http://127.0.0.1:3000"]'
26
+ ports:
27
+ - "8000:8000"
28
+ healthcheck:
29
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
30
+ interval: 20s
31
+ timeout: 5s
32
+ retries: 5
frontend.Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-bookworm-slim AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ ENV NEXT_TELEMETRY_DISABLED=1
6
+ ENV BACKEND_API_URL=http://backend:8000
7
+
8
+ COPY package.json package-lock.json ./
9
+ RUN npm ci
10
+
11
+ COPY app ./app
12
+ COPY components ./components
13
+ COPY public ./public
14
+ COPY next.config.ts ./
15
+ COPY next-env.d.ts ./
16
+ COPY tsconfig.json ./
17
+ COPY postcss.config.js ./
18
+ COPY tailwind.config.ts ./
19
+
20
+ RUN npm run build
21
+
22
+
23
+ FROM node:22-bookworm-slim AS runner
24
+
25
+ WORKDIR /app
26
+
27
+ ENV NODE_ENV=production
28
+ ENV NEXT_TELEMETRY_DISABLED=1
29
+ ENV PORT=3000
30
+ ENV HOSTNAME=0.0.0.0
31
+
32
+ COPY --from=builder /app/.next/standalone ./
33
+ COPY --from=builder /app/.next/static ./.next/static
34
+ COPY --from=builder /app/public ./public
35
+
36
+ EXPOSE 3000
37
+
38
+ CMD ["node", "server.js"]
next-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ /// <reference path="./.next/types/routes.d.ts" />
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
next.config.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const backendApiUrl = process.env.BACKEND_API_URL || "http://127.0.0.1:8000";
4
+
5
+ const nextConfig: NextConfig = {
6
+ reactStrictMode: true,
7
+ output: "standalone",
8
+ async rewrites() {
9
+ return [
10
+ {
11
+ source: "/api/:path*",
12
+ destination: `${backendApiUrl}/api/:path*`,
13
+ },
14
+ {
15
+ source: "/healthz",
16
+ destination: `${backendApiUrl}/health`,
17
+ },
18
+ ];
19
+ },
20
+ };
21
+
22
+ export default nextConfig;
package-lock.json ADDED
@@ -0,0 +1,2165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "resumeai-pro",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "resumeai-pro",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "next": "15.5.14",
12
+ "react": "19.1.0",
13
+ "react-dom": "19.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "22.14.1",
17
+ "@types/react": "19.1.1",
18
+ "@types/react-dom": "19.1.2",
19
+ "autoprefixer": "10.4.21",
20
+ "postcss": "8.5.3",
21
+ "tailwindcss": "3.4.17",
22
+ "typescript": "5.8.3"
23
+ }
24
+ },
25
+ "node_modules/@alloc/quick-lru": {
26
+ "version": "5.2.0",
27
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
28
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=10"
33
+ },
34
+ "funding": {
35
+ "url": "https://github.com/sponsors/sindresorhus"
36
+ }
37
+ },
38
+ "node_modules/@emnapi/runtime": {
39
+ "version": "1.9.1",
40
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
41
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
42
+ "license": "MIT",
43
+ "optional": true,
44
+ "dependencies": {
45
+ "tslib": "^2.4.0"
46
+ }
47
+ },
48
+ "node_modules/@img/colour": {
49
+ "version": "1.1.0",
50
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
51
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
52
+ "license": "MIT",
53
+ "optional": true,
54
+ "engines": {
55
+ "node": ">=18"
56
+ }
57
+ },
58
+ "node_modules/@img/sharp-darwin-arm64": {
59
+ "version": "0.34.5",
60
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
61
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
62
+ "cpu": [
63
+ "arm64"
64
+ ],
65
+ "license": "Apache-2.0",
66
+ "optional": true,
67
+ "os": [
68
+ "darwin"
69
+ ],
70
+ "engines": {
71
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
72
+ },
73
+ "funding": {
74
+ "url": "https://opencollective.com/libvips"
75
+ },
76
+ "optionalDependencies": {
77
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
78
+ }
79
+ },
80
+ "node_modules/@img/sharp-darwin-x64": {
81
+ "version": "0.34.5",
82
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
83
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
84
+ "cpu": [
85
+ "x64"
86
+ ],
87
+ "license": "Apache-2.0",
88
+ "optional": true,
89
+ "os": [
90
+ "darwin"
91
+ ],
92
+ "engines": {
93
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
94
+ },
95
+ "funding": {
96
+ "url": "https://opencollective.com/libvips"
97
+ },
98
+ "optionalDependencies": {
99
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
100
+ }
101
+ },
102
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
103
+ "version": "1.2.4",
104
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
105
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
106
+ "cpu": [
107
+ "arm64"
108
+ ],
109
+ "license": "LGPL-3.0-or-later",
110
+ "optional": true,
111
+ "os": [
112
+ "darwin"
113
+ ],
114
+ "funding": {
115
+ "url": "https://opencollective.com/libvips"
116
+ }
117
+ },
118
+ "node_modules/@img/sharp-libvips-darwin-x64": {
119
+ "version": "1.2.4",
120
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
121
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
122
+ "cpu": [
123
+ "x64"
124
+ ],
125
+ "license": "LGPL-3.0-or-later",
126
+ "optional": true,
127
+ "os": [
128
+ "darwin"
129
+ ],
130
+ "funding": {
131
+ "url": "https://opencollective.com/libvips"
132
+ }
133
+ },
134
+ "node_modules/@img/sharp-libvips-linux-arm": {
135
+ "version": "1.2.4",
136
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
137
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
138
+ "cpu": [
139
+ "arm"
140
+ ],
141
+ "libc": [
142
+ "glibc"
143
+ ],
144
+ "license": "LGPL-3.0-or-later",
145
+ "optional": true,
146
+ "os": [
147
+ "linux"
148
+ ],
149
+ "funding": {
150
+ "url": "https://opencollective.com/libvips"
151
+ }
152
+ },
153
+ "node_modules/@img/sharp-libvips-linux-arm64": {
154
+ "version": "1.2.4",
155
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
156
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
157
+ "cpu": [
158
+ "arm64"
159
+ ],
160
+ "libc": [
161
+ "glibc"
162
+ ],
163
+ "license": "LGPL-3.0-or-later",
164
+ "optional": true,
165
+ "os": [
166
+ "linux"
167
+ ],
168
+ "funding": {
169
+ "url": "https://opencollective.com/libvips"
170
+ }
171
+ },
172
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
173
+ "version": "1.2.4",
174
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
175
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
176
+ "cpu": [
177
+ "ppc64"
178
+ ],
179
+ "libc": [
180
+ "glibc"
181
+ ],
182
+ "license": "LGPL-3.0-or-later",
183
+ "optional": true,
184
+ "os": [
185
+ "linux"
186
+ ],
187
+ "funding": {
188
+ "url": "https://opencollective.com/libvips"
189
+ }
190
+ },
191
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
192
+ "version": "1.2.4",
193
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
194
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
195
+ "cpu": [
196
+ "riscv64"
197
+ ],
198
+ "libc": [
199
+ "glibc"
200
+ ],
201
+ "license": "LGPL-3.0-or-later",
202
+ "optional": true,
203
+ "os": [
204
+ "linux"
205
+ ],
206
+ "funding": {
207
+ "url": "https://opencollective.com/libvips"
208
+ }
209
+ },
210
+ "node_modules/@img/sharp-libvips-linux-s390x": {
211
+ "version": "1.2.4",
212
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
213
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
214
+ "cpu": [
215
+ "s390x"
216
+ ],
217
+ "libc": [
218
+ "glibc"
219
+ ],
220
+ "license": "LGPL-3.0-or-later",
221
+ "optional": true,
222
+ "os": [
223
+ "linux"
224
+ ],
225
+ "funding": {
226
+ "url": "https://opencollective.com/libvips"
227
+ }
228
+ },
229
+ "node_modules/@img/sharp-libvips-linux-x64": {
230
+ "version": "1.2.4",
231
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
232
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
233
+ "cpu": [
234
+ "x64"
235
+ ],
236
+ "libc": [
237
+ "glibc"
238
+ ],
239
+ "license": "LGPL-3.0-or-later",
240
+ "optional": true,
241
+ "os": [
242
+ "linux"
243
+ ],
244
+ "funding": {
245
+ "url": "https://opencollective.com/libvips"
246
+ }
247
+ },
248
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
249
+ "version": "1.2.4",
250
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
251
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
252
+ "cpu": [
253
+ "arm64"
254
+ ],
255
+ "libc": [
256
+ "musl"
257
+ ],
258
+ "license": "LGPL-3.0-or-later",
259
+ "optional": true,
260
+ "os": [
261
+ "linux"
262
+ ],
263
+ "funding": {
264
+ "url": "https://opencollective.com/libvips"
265
+ }
266
+ },
267
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
268
+ "version": "1.2.4",
269
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
270
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
271
+ "cpu": [
272
+ "x64"
273
+ ],
274
+ "libc": [
275
+ "musl"
276
+ ],
277
+ "license": "LGPL-3.0-or-later",
278
+ "optional": true,
279
+ "os": [
280
+ "linux"
281
+ ],
282
+ "funding": {
283
+ "url": "https://opencollective.com/libvips"
284
+ }
285
+ },
286
+ "node_modules/@img/sharp-linux-arm": {
287
+ "version": "0.34.5",
288
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
289
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
290
+ "cpu": [
291
+ "arm"
292
+ ],
293
+ "libc": [
294
+ "glibc"
295
+ ],
296
+ "license": "Apache-2.0",
297
+ "optional": true,
298
+ "os": [
299
+ "linux"
300
+ ],
301
+ "engines": {
302
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
303
+ },
304
+ "funding": {
305
+ "url": "https://opencollective.com/libvips"
306
+ },
307
+ "optionalDependencies": {
308
+ "@img/sharp-libvips-linux-arm": "1.2.4"
309
+ }
310
+ },
311
+ "node_modules/@img/sharp-linux-arm64": {
312
+ "version": "0.34.5",
313
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
314
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
315
+ "cpu": [
316
+ "arm64"
317
+ ],
318
+ "libc": [
319
+ "glibc"
320
+ ],
321
+ "license": "Apache-2.0",
322
+ "optional": true,
323
+ "os": [
324
+ "linux"
325
+ ],
326
+ "engines": {
327
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
328
+ },
329
+ "funding": {
330
+ "url": "https://opencollective.com/libvips"
331
+ },
332
+ "optionalDependencies": {
333
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
334
+ }
335
+ },
336
+ "node_modules/@img/sharp-linux-ppc64": {
337
+ "version": "0.34.5",
338
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
339
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
340
+ "cpu": [
341
+ "ppc64"
342
+ ],
343
+ "libc": [
344
+ "glibc"
345
+ ],
346
+ "license": "Apache-2.0",
347
+ "optional": true,
348
+ "os": [
349
+ "linux"
350
+ ],
351
+ "engines": {
352
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
353
+ },
354
+ "funding": {
355
+ "url": "https://opencollective.com/libvips"
356
+ },
357
+ "optionalDependencies": {
358
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
359
+ }
360
+ },
361
+ "node_modules/@img/sharp-linux-riscv64": {
362
+ "version": "0.34.5",
363
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
364
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
365
+ "cpu": [
366
+ "riscv64"
367
+ ],
368
+ "libc": [
369
+ "glibc"
370
+ ],
371
+ "license": "Apache-2.0",
372
+ "optional": true,
373
+ "os": [
374
+ "linux"
375
+ ],
376
+ "engines": {
377
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
378
+ },
379
+ "funding": {
380
+ "url": "https://opencollective.com/libvips"
381
+ },
382
+ "optionalDependencies": {
383
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
384
+ }
385
+ },
386
+ "node_modules/@img/sharp-linux-s390x": {
387
+ "version": "0.34.5",
388
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
389
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
390
+ "cpu": [
391
+ "s390x"
392
+ ],
393
+ "libc": [
394
+ "glibc"
395
+ ],
396
+ "license": "Apache-2.0",
397
+ "optional": true,
398
+ "os": [
399
+ "linux"
400
+ ],
401
+ "engines": {
402
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
403
+ },
404
+ "funding": {
405
+ "url": "https://opencollective.com/libvips"
406
+ },
407
+ "optionalDependencies": {
408
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
409
+ }
410
+ },
411
+ "node_modules/@img/sharp-linux-x64": {
412
+ "version": "0.34.5",
413
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
414
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
415
+ "cpu": [
416
+ "x64"
417
+ ],
418
+ "libc": [
419
+ "glibc"
420
+ ],
421
+ "license": "Apache-2.0",
422
+ "optional": true,
423
+ "os": [
424
+ "linux"
425
+ ],
426
+ "engines": {
427
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
428
+ },
429
+ "funding": {
430
+ "url": "https://opencollective.com/libvips"
431
+ },
432
+ "optionalDependencies": {
433
+ "@img/sharp-libvips-linux-x64": "1.2.4"
434
+ }
435
+ },
436
+ "node_modules/@img/sharp-linuxmusl-arm64": {
437
+ "version": "0.34.5",
438
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
439
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
440
+ "cpu": [
441
+ "arm64"
442
+ ],
443
+ "libc": [
444
+ "musl"
445
+ ],
446
+ "license": "Apache-2.0",
447
+ "optional": true,
448
+ "os": [
449
+ "linux"
450
+ ],
451
+ "engines": {
452
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
453
+ },
454
+ "funding": {
455
+ "url": "https://opencollective.com/libvips"
456
+ },
457
+ "optionalDependencies": {
458
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
459
+ }
460
+ },
461
+ "node_modules/@img/sharp-linuxmusl-x64": {
462
+ "version": "0.34.5",
463
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
464
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
465
+ "cpu": [
466
+ "x64"
467
+ ],
468
+ "libc": [
469
+ "musl"
470
+ ],
471
+ "license": "Apache-2.0",
472
+ "optional": true,
473
+ "os": [
474
+ "linux"
475
+ ],
476
+ "engines": {
477
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
478
+ },
479
+ "funding": {
480
+ "url": "https://opencollective.com/libvips"
481
+ },
482
+ "optionalDependencies": {
483
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
484
+ }
485
+ },
486
+ "node_modules/@img/sharp-wasm32": {
487
+ "version": "0.34.5",
488
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
489
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
490
+ "cpu": [
491
+ "wasm32"
492
+ ],
493
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
494
+ "optional": true,
495
+ "dependencies": {
496
+ "@emnapi/runtime": "^1.7.0"
497
+ },
498
+ "engines": {
499
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
500
+ },
501
+ "funding": {
502
+ "url": "https://opencollective.com/libvips"
503
+ }
504
+ },
505
+ "node_modules/@img/sharp-win32-arm64": {
506
+ "version": "0.34.5",
507
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
508
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
509
+ "cpu": [
510
+ "arm64"
511
+ ],
512
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
513
+ "optional": true,
514
+ "os": [
515
+ "win32"
516
+ ],
517
+ "engines": {
518
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
519
+ },
520
+ "funding": {
521
+ "url": "https://opencollective.com/libvips"
522
+ }
523
+ },
524
+ "node_modules/@img/sharp-win32-ia32": {
525
+ "version": "0.34.5",
526
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
527
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
528
+ "cpu": [
529
+ "ia32"
530
+ ],
531
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
532
+ "optional": true,
533
+ "os": [
534
+ "win32"
535
+ ],
536
+ "engines": {
537
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
538
+ },
539
+ "funding": {
540
+ "url": "https://opencollective.com/libvips"
541
+ }
542
+ },
543
+ "node_modules/@img/sharp-win32-x64": {
544
+ "version": "0.34.5",
545
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
546
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
547
+ "cpu": [
548
+ "x64"
549
+ ],
550
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
551
+ "optional": true,
552
+ "os": [
553
+ "win32"
554
+ ],
555
+ "engines": {
556
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
557
+ },
558
+ "funding": {
559
+ "url": "https://opencollective.com/libvips"
560
+ }
561
+ },
562
+ "node_modules/@jridgewell/gen-mapping": {
563
+ "version": "0.3.13",
564
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
565
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
566
+ "dev": true,
567
+ "license": "MIT",
568
+ "dependencies": {
569
+ "@jridgewell/sourcemap-codec": "^1.5.0",
570
+ "@jridgewell/trace-mapping": "^0.3.24"
571
+ }
572
+ },
573
+ "node_modules/@jridgewell/resolve-uri": {
574
+ "version": "3.1.2",
575
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
576
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
577
+ "dev": true,
578
+ "license": "MIT",
579
+ "engines": {
580
+ "node": ">=6.0.0"
581
+ }
582
+ },
583
+ "node_modules/@jridgewell/sourcemap-codec": {
584
+ "version": "1.5.5",
585
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
586
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
587
+ "dev": true,
588
+ "license": "MIT"
589
+ },
590
+ "node_modules/@jridgewell/trace-mapping": {
591
+ "version": "0.3.31",
592
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
593
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
594
+ "dev": true,
595
+ "license": "MIT",
596
+ "dependencies": {
597
+ "@jridgewell/resolve-uri": "^3.1.0",
598
+ "@jridgewell/sourcemap-codec": "^1.4.14"
599
+ }
600
+ },
601
+ "node_modules/@next/env": {
602
+ "version": "15.5.14",
603
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz",
604
+ "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==",
605
+ "license": "MIT"
606
+ },
607
+ "node_modules/@next/swc-darwin-arm64": {
608
+ "version": "15.5.14",
609
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz",
610
+ "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==",
611
+ "cpu": [
612
+ "arm64"
613
+ ],
614
+ "license": "MIT",
615
+ "optional": true,
616
+ "os": [
617
+ "darwin"
618
+ ],
619
+ "engines": {
620
+ "node": ">= 10"
621
+ }
622
+ },
623
+ "node_modules/@next/swc-darwin-x64": {
624
+ "version": "15.5.14",
625
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz",
626
+ "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==",
627
+ "cpu": [
628
+ "x64"
629
+ ],
630
+ "license": "MIT",
631
+ "optional": true,
632
+ "os": [
633
+ "darwin"
634
+ ],
635
+ "engines": {
636
+ "node": ">= 10"
637
+ }
638
+ },
639
+ "node_modules/@next/swc-linux-arm64-gnu": {
640
+ "version": "15.5.14",
641
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz",
642
+ "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==",
643
+ "cpu": [
644
+ "arm64"
645
+ ],
646
+ "libc": [
647
+ "glibc"
648
+ ],
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "linux"
653
+ ],
654
+ "engines": {
655
+ "node": ">= 10"
656
+ }
657
+ },
658
+ "node_modules/@next/swc-linux-arm64-musl": {
659
+ "version": "15.5.14",
660
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz",
661
+ "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "libc": [
666
+ "musl"
667
+ ],
668
+ "license": "MIT",
669
+ "optional": true,
670
+ "os": [
671
+ "linux"
672
+ ],
673
+ "engines": {
674
+ "node": ">= 10"
675
+ }
676
+ },
677
+ "node_modules/@next/swc-linux-x64-gnu": {
678
+ "version": "15.5.14",
679
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz",
680
+ "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==",
681
+ "cpu": [
682
+ "x64"
683
+ ],
684
+ "libc": [
685
+ "glibc"
686
+ ],
687
+ "license": "MIT",
688
+ "optional": true,
689
+ "os": [
690
+ "linux"
691
+ ],
692
+ "engines": {
693
+ "node": ">= 10"
694
+ }
695
+ },
696
+ "node_modules/@next/swc-linux-x64-musl": {
697
+ "version": "15.5.14",
698
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz",
699
+ "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==",
700
+ "cpu": [
701
+ "x64"
702
+ ],
703
+ "libc": [
704
+ "musl"
705
+ ],
706
+ "license": "MIT",
707
+ "optional": true,
708
+ "os": [
709
+ "linux"
710
+ ],
711
+ "engines": {
712
+ "node": ">= 10"
713
+ }
714
+ },
715
+ "node_modules/@next/swc-win32-arm64-msvc": {
716
+ "version": "15.5.14",
717
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz",
718
+ "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==",
719
+ "cpu": [
720
+ "arm64"
721
+ ],
722
+ "license": "MIT",
723
+ "optional": true,
724
+ "os": [
725
+ "win32"
726
+ ],
727
+ "engines": {
728
+ "node": ">= 10"
729
+ }
730
+ },
731
+ "node_modules/@next/swc-win32-x64-msvc": {
732
+ "version": "15.5.14",
733
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz",
734
+ "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==",
735
+ "cpu": [
736
+ "x64"
737
+ ],
738
+ "license": "MIT",
739
+ "optional": true,
740
+ "os": [
741
+ "win32"
742
+ ],
743
+ "engines": {
744
+ "node": ">= 10"
745
+ }
746
+ },
747
+ "node_modules/@nodelib/fs.scandir": {
748
+ "version": "2.1.5",
749
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
750
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
751
+ "dev": true,
752
+ "license": "MIT",
753
+ "dependencies": {
754
+ "@nodelib/fs.stat": "2.0.5",
755
+ "run-parallel": "^1.1.9"
756
+ },
757
+ "engines": {
758
+ "node": ">= 8"
759
+ }
760
+ },
761
+ "node_modules/@nodelib/fs.stat": {
762
+ "version": "2.0.5",
763
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
764
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
765
+ "dev": true,
766
+ "license": "MIT",
767
+ "engines": {
768
+ "node": ">= 8"
769
+ }
770
+ },
771
+ "node_modules/@nodelib/fs.walk": {
772
+ "version": "1.2.8",
773
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
774
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
775
+ "dev": true,
776
+ "license": "MIT",
777
+ "dependencies": {
778
+ "@nodelib/fs.scandir": "2.1.5",
779
+ "fastq": "^1.6.0"
780
+ },
781
+ "engines": {
782
+ "node": ">= 8"
783
+ }
784
+ },
785
+ "node_modules/@swc/helpers": {
786
+ "version": "0.5.15",
787
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
788
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
789
+ "license": "Apache-2.0",
790
+ "dependencies": {
791
+ "tslib": "^2.8.0"
792
+ }
793
+ },
794
+ "node_modules/@types/node": {
795
+ "version": "22.14.1",
796
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
797
+ "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "dependencies": {
801
+ "undici-types": "~6.21.0"
802
+ }
803
+ },
804
+ "node_modules/@types/react": {
805
+ "version": "19.1.1",
806
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
807
+ "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
808
+ "dev": true,
809
+ "license": "MIT",
810
+ "dependencies": {
811
+ "csstype": "^3.0.2"
812
+ }
813
+ },
814
+ "node_modules/@types/react-dom": {
815
+ "version": "19.1.2",
816
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
817
+ "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
818
+ "dev": true,
819
+ "license": "MIT",
820
+ "peerDependencies": {
821
+ "@types/react": "^19.0.0"
822
+ }
823
+ },
824
+ "node_modules/any-promise": {
825
+ "version": "1.3.0",
826
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
827
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
828
+ "dev": true,
829
+ "license": "MIT"
830
+ },
831
+ "node_modules/anymatch": {
832
+ "version": "3.1.3",
833
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
834
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
835
+ "dev": true,
836
+ "license": "ISC",
837
+ "dependencies": {
838
+ "normalize-path": "^3.0.0",
839
+ "picomatch": "^2.0.4"
840
+ },
841
+ "engines": {
842
+ "node": ">= 8"
843
+ }
844
+ },
845
+ "node_modules/arg": {
846
+ "version": "5.0.2",
847
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
848
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
849
+ "dev": true,
850
+ "license": "MIT"
851
+ },
852
+ "node_modules/autoprefixer": {
853
+ "version": "10.4.21",
854
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
855
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
856
+ "dev": true,
857
+ "funding": [
858
+ {
859
+ "type": "opencollective",
860
+ "url": "https://opencollective.com/postcss/"
861
+ },
862
+ {
863
+ "type": "tidelift",
864
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
865
+ },
866
+ {
867
+ "type": "github",
868
+ "url": "https://github.com/sponsors/ai"
869
+ }
870
+ ],
871
+ "license": "MIT",
872
+ "dependencies": {
873
+ "browserslist": "^4.24.4",
874
+ "caniuse-lite": "^1.0.30001702",
875
+ "fraction.js": "^4.3.7",
876
+ "normalize-range": "^0.1.2",
877
+ "picocolors": "^1.1.1",
878
+ "postcss-value-parser": "^4.2.0"
879
+ },
880
+ "bin": {
881
+ "autoprefixer": "bin/autoprefixer"
882
+ },
883
+ "engines": {
884
+ "node": "^10 || ^12 || >=14"
885
+ },
886
+ "peerDependencies": {
887
+ "postcss": "^8.1.0"
888
+ }
889
+ },
890
+ "node_modules/baseline-browser-mapping": {
891
+ "version": "2.10.10",
892
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz",
893
+ "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==",
894
+ "dev": true,
895
+ "license": "Apache-2.0",
896
+ "bin": {
897
+ "baseline-browser-mapping": "dist/cli.cjs"
898
+ },
899
+ "engines": {
900
+ "node": ">=6.0.0"
901
+ }
902
+ },
903
+ "node_modules/binary-extensions": {
904
+ "version": "2.3.0",
905
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
906
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
907
+ "dev": true,
908
+ "license": "MIT",
909
+ "engines": {
910
+ "node": ">=8"
911
+ },
912
+ "funding": {
913
+ "url": "https://github.com/sponsors/sindresorhus"
914
+ }
915
+ },
916
+ "node_modules/braces": {
917
+ "version": "3.0.3",
918
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
919
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
920
+ "dev": true,
921
+ "license": "MIT",
922
+ "dependencies": {
923
+ "fill-range": "^7.1.1"
924
+ },
925
+ "engines": {
926
+ "node": ">=8"
927
+ }
928
+ },
929
+ "node_modules/browserslist": {
930
+ "version": "4.28.1",
931
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
932
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
933
+ "dev": true,
934
+ "funding": [
935
+ {
936
+ "type": "opencollective",
937
+ "url": "https://opencollective.com/browserslist"
938
+ },
939
+ {
940
+ "type": "tidelift",
941
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
942
+ },
943
+ {
944
+ "type": "github",
945
+ "url": "https://github.com/sponsors/ai"
946
+ }
947
+ ],
948
+ "license": "MIT",
949
+ "dependencies": {
950
+ "baseline-browser-mapping": "^2.9.0",
951
+ "caniuse-lite": "^1.0.30001759",
952
+ "electron-to-chromium": "^1.5.263",
953
+ "node-releases": "^2.0.27",
954
+ "update-browserslist-db": "^1.2.0"
955
+ },
956
+ "bin": {
957
+ "browserslist": "cli.js"
958
+ },
959
+ "engines": {
960
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
961
+ }
962
+ },
963
+ "node_modules/camelcase-css": {
964
+ "version": "2.0.1",
965
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
966
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
967
+ "dev": true,
968
+ "license": "MIT",
969
+ "engines": {
970
+ "node": ">= 6"
971
+ }
972
+ },
973
+ "node_modules/caniuse-lite": {
974
+ "version": "1.0.30001781",
975
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz",
976
+ "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==",
977
+ "funding": [
978
+ {
979
+ "type": "opencollective",
980
+ "url": "https://opencollective.com/browserslist"
981
+ },
982
+ {
983
+ "type": "tidelift",
984
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
985
+ },
986
+ {
987
+ "type": "github",
988
+ "url": "https://github.com/sponsors/ai"
989
+ }
990
+ ],
991
+ "license": "CC-BY-4.0"
992
+ },
993
+ "node_modules/chokidar": {
994
+ "version": "3.6.0",
995
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
996
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
997
+ "dev": true,
998
+ "license": "MIT",
999
+ "dependencies": {
1000
+ "anymatch": "~3.1.2",
1001
+ "braces": "~3.0.2",
1002
+ "glob-parent": "~5.1.2",
1003
+ "is-binary-path": "~2.1.0",
1004
+ "is-glob": "~4.0.1",
1005
+ "normalize-path": "~3.0.0",
1006
+ "readdirp": "~3.6.0"
1007
+ },
1008
+ "engines": {
1009
+ "node": ">= 8.10.0"
1010
+ },
1011
+ "funding": {
1012
+ "url": "https://paulmillr.com/funding/"
1013
+ },
1014
+ "optionalDependencies": {
1015
+ "fsevents": "~2.3.2"
1016
+ }
1017
+ },
1018
+ "node_modules/chokidar/node_modules/glob-parent": {
1019
+ "version": "5.1.2",
1020
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1021
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1022
+ "dev": true,
1023
+ "license": "ISC",
1024
+ "dependencies": {
1025
+ "is-glob": "^4.0.1"
1026
+ },
1027
+ "engines": {
1028
+ "node": ">= 6"
1029
+ }
1030
+ },
1031
+ "node_modules/client-only": {
1032
+ "version": "0.0.1",
1033
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
1034
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
1035
+ "license": "MIT"
1036
+ },
1037
+ "node_modules/commander": {
1038
+ "version": "4.1.1",
1039
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1040
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1041
+ "dev": true,
1042
+ "license": "MIT",
1043
+ "engines": {
1044
+ "node": ">= 6"
1045
+ }
1046
+ },
1047
+ "node_modules/cssesc": {
1048
+ "version": "3.0.0",
1049
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1050
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1051
+ "dev": true,
1052
+ "license": "MIT",
1053
+ "bin": {
1054
+ "cssesc": "bin/cssesc"
1055
+ },
1056
+ "engines": {
1057
+ "node": ">=4"
1058
+ }
1059
+ },
1060
+ "node_modules/csstype": {
1061
+ "version": "3.2.3",
1062
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1063
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1064
+ "dev": true,
1065
+ "license": "MIT"
1066
+ },
1067
+ "node_modules/detect-libc": {
1068
+ "version": "2.1.2",
1069
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1070
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1071
+ "license": "Apache-2.0",
1072
+ "optional": true,
1073
+ "engines": {
1074
+ "node": ">=8"
1075
+ }
1076
+ },
1077
+ "node_modules/didyoumean": {
1078
+ "version": "1.2.2",
1079
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1080
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1081
+ "dev": true,
1082
+ "license": "Apache-2.0"
1083
+ },
1084
+ "node_modules/dlv": {
1085
+ "version": "1.1.3",
1086
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1087
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1088
+ "dev": true,
1089
+ "license": "MIT"
1090
+ },
1091
+ "node_modules/electron-to-chromium": {
1092
+ "version": "1.5.325",
1093
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz",
1094
+ "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==",
1095
+ "dev": true,
1096
+ "license": "ISC"
1097
+ },
1098
+ "node_modules/escalade": {
1099
+ "version": "3.2.0",
1100
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1101
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1102
+ "dev": true,
1103
+ "license": "MIT",
1104
+ "engines": {
1105
+ "node": ">=6"
1106
+ }
1107
+ },
1108
+ "node_modules/fast-glob": {
1109
+ "version": "3.3.3",
1110
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1111
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1112
+ "dev": true,
1113
+ "license": "MIT",
1114
+ "dependencies": {
1115
+ "@nodelib/fs.stat": "^2.0.2",
1116
+ "@nodelib/fs.walk": "^1.2.3",
1117
+ "glob-parent": "^5.1.2",
1118
+ "merge2": "^1.3.0",
1119
+ "micromatch": "^4.0.8"
1120
+ },
1121
+ "engines": {
1122
+ "node": ">=8.6.0"
1123
+ }
1124
+ },
1125
+ "node_modules/fast-glob/node_modules/glob-parent": {
1126
+ "version": "5.1.2",
1127
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1128
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1129
+ "dev": true,
1130
+ "license": "ISC",
1131
+ "dependencies": {
1132
+ "is-glob": "^4.0.1"
1133
+ },
1134
+ "engines": {
1135
+ "node": ">= 6"
1136
+ }
1137
+ },
1138
+ "node_modules/fastq": {
1139
+ "version": "1.20.1",
1140
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
1141
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
1142
+ "dev": true,
1143
+ "license": "ISC",
1144
+ "dependencies": {
1145
+ "reusify": "^1.0.4"
1146
+ }
1147
+ },
1148
+ "node_modules/fill-range": {
1149
+ "version": "7.1.1",
1150
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1151
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1152
+ "dev": true,
1153
+ "license": "MIT",
1154
+ "dependencies": {
1155
+ "to-regex-range": "^5.0.1"
1156
+ },
1157
+ "engines": {
1158
+ "node": ">=8"
1159
+ }
1160
+ },
1161
+ "node_modules/fraction.js": {
1162
+ "version": "4.3.7",
1163
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
1164
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
1165
+ "dev": true,
1166
+ "license": "MIT",
1167
+ "engines": {
1168
+ "node": "*"
1169
+ },
1170
+ "funding": {
1171
+ "type": "patreon",
1172
+ "url": "https://github.com/sponsors/rawify"
1173
+ }
1174
+ },
1175
+ "node_modules/fsevents": {
1176
+ "version": "2.3.3",
1177
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1178
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1179
+ "dev": true,
1180
+ "hasInstallScript": true,
1181
+ "license": "MIT",
1182
+ "optional": true,
1183
+ "os": [
1184
+ "darwin"
1185
+ ],
1186
+ "engines": {
1187
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1188
+ }
1189
+ },
1190
+ "node_modules/function-bind": {
1191
+ "version": "1.1.2",
1192
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1193
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1194
+ "dev": true,
1195
+ "license": "MIT",
1196
+ "funding": {
1197
+ "url": "https://github.com/sponsors/ljharb"
1198
+ }
1199
+ },
1200
+ "node_modules/glob-parent": {
1201
+ "version": "6.0.2",
1202
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1203
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1204
+ "dev": true,
1205
+ "license": "ISC",
1206
+ "dependencies": {
1207
+ "is-glob": "^4.0.3"
1208
+ },
1209
+ "engines": {
1210
+ "node": ">=10.13.0"
1211
+ }
1212
+ },
1213
+ "node_modules/hasown": {
1214
+ "version": "2.0.2",
1215
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1216
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1217
+ "dev": true,
1218
+ "license": "MIT",
1219
+ "dependencies": {
1220
+ "function-bind": "^1.1.2"
1221
+ },
1222
+ "engines": {
1223
+ "node": ">= 0.4"
1224
+ }
1225
+ },
1226
+ "node_modules/is-binary-path": {
1227
+ "version": "2.1.0",
1228
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1229
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1230
+ "dev": true,
1231
+ "license": "MIT",
1232
+ "dependencies": {
1233
+ "binary-extensions": "^2.0.0"
1234
+ },
1235
+ "engines": {
1236
+ "node": ">=8"
1237
+ }
1238
+ },
1239
+ "node_modules/is-core-module": {
1240
+ "version": "2.16.1",
1241
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1242
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1243
+ "dev": true,
1244
+ "license": "MIT",
1245
+ "dependencies": {
1246
+ "hasown": "^2.0.2"
1247
+ },
1248
+ "engines": {
1249
+ "node": ">= 0.4"
1250
+ },
1251
+ "funding": {
1252
+ "url": "https://github.com/sponsors/ljharb"
1253
+ }
1254
+ },
1255
+ "node_modules/is-extglob": {
1256
+ "version": "2.1.1",
1257
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1258
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1259
+ "dev": true,
1260
+ "license": "MIT",
1261
+ "engines": {
1262
+ "node": ">=0.10.0"
1263
+ }
1264
+ },
1265
+ "node_modules/is-glob": {
1266
+ "version": "4.0.3",
1267
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1268
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1269
+ "dev": true,
1270
+ "license": "MIT",
1271
+ "dependencies": {
1272
+ "is-extglob": "^2.1.1"
1273
+ },
1274
+ "engines": {
1275
+ "node": ">=0.10.0"
1276
+ }
1277
+ },
1278
+ "node_modules/is-number": {
1279
+ "version": "7.0.0",
1280
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1281
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1282
+ "dev": true,
1283
+ "license": "MIT",
1284
+ "engines": {
1285
+ "node": ">=0.12.0"
1286
+ }
1287
+ },
1288
+ "node_modules/jiti": {
1289
+ "version": "1.21.7",
1290
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1291
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1292
+ "dev": true,
1293
+ "license": "MIT",
1294
+ "bin": {
1295
+ "jiti": "bin/jiti.js"
1296
+ }
1297
+ },
1298
+ "node_modules/lilconfig": {
1299
+ "version": "3.1.3",
1300
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1301
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1302
+ "dev": true,
1303
+ "license": "MIT",
1304
+ "engines": {
1305
+ "node": ">=14"
1306
+ },
1307
+ "funding": {
1308
+ "url": "https://github.com/sponsors/antonk52"
1309
+ }
1310
+ },
1311
+ "node_modules/lines-and-columns": {
1312
+ "version": "1.2.4",
1313
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1314
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1315
+ "dev": true,
1316
+ "license": "MIT"
1317
+ },
1318
+ "node_modules/merge2": {
1319
+ "version": "1.4.1",
1320
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1321
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1322
+ "dev": true,
1323
+ "license": "MIT",
1324
+ "engines": {
1325
+ "node": ">= 8"
1326
+ }
1327
+ },
1328
+ "node_modules/micromatch": {
1329
+ "version": "4.0.8",
1330
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1331
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1332
+ "dev": true,
1333
+ "license": "MIT",
1334
+ "dependencies": {
1335
+ "braces": "^3.0.3",
1336
+ "picomatch": "^2.3.1"
1337
+ },
1338
+ "engines": {
1339
+ "node": ">=8.6"
1340
+ }
1341
+ },
1342
+ "node_modules/mz": {
1343
+ "version": "2.7.0",
1344
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1345
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1346
+ "dev": true,
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "any-promise": "^1.0.0",
1350
+ "object-assign": "^4.0.1",
1351
+ "thenify-all": "^1.0.0"
1352
+ }
1353
+ },
1354
+ "node_modules/nanoid": {
1355
+ "version": "3.3.11",
1356
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1357
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1358
+ "funding": [
1359
+ {
1360
+ "type": "github",
1361
+ "url": "https://github.com/sponsors/ai"
1362
+ }
1363
+ ],
1364
+ "license": "MIT",
1365
+ "bin": {
1366
+ "nanoid": "bin/nanoid.cjs"
1367
+ },
1368
+ "engines": {
1369
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1370
+ }
1371
+ },
1372
+ "node_modules/next": {
1373
+ "version": "15.5.14",
1374
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz",
1375
+ "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==",
1376
+ "license": "MIT",
1377
+ "dependencies": {
1378
+ "@next/env": "15.5.14",
1379
+ "@swc/helpers": "0.5.15",
1380
+ "caniuse-lite": "^1.0.30001579",
1381
+ "postcss": "8.4.31",
1382
+ "styled-jsx": "5.1.6"
1383
+ },
1384
+ "bin": {
1385
+ "next": "dist/bin/next"
1386
+ },
1387
+ "engines": {
1388
+ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
1389
+ },
1390
+ "optionalDependencies": {
1391
+ "@next/swc-darwin-arm64": "15.5.14",
1392
+ "@next/swc-darwin-x64": "15.5.14",
1393
+ "@next/swc-linux-arm64-gnu": "15.5.14",
1394
+ "@next/swc-linux-arm64-musl": "15.5.14",
1395
+ "@next/swc-linux-x64-gnu": "15.5.14",
1396
+ "@next/swc-linux-x64-musl": "15.5.14",
1397
+ "@next/swc-win32-arm64-msvc": "15.5.14",
1398
+ "@next/swc-win32-x64-msvc": "15.5.14",
1399
+ "sharp": "^0.34.3"
1400
+ },
1401
+ "peerDependencies": {
1402
+ "@opentelemetry/api": "^1.1.0",
1403
+ "@playwright/test": "^1.51.1",
1404
+ "babel-plugin-react-compiler": "*",
1405
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1406
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1407
+ "sass": "^1.3.0"
1408
+ },
1409
+ "peerDependenciesMeta": {
1410
+ "@opentelemetry/api": {
1411
+ "optional": true
1412
+ },
1413
+ "@playwright/test": {
1414
+ "optional": true
1415
+ },
1416
+ "babel-plugin-react-compiler": {
1417
+ "optional": true
1418
+ },
1419
+ "sass": {
1420
+ "optional": true
1421
+ }
1422
+ }
1423
+ },
1424
+ "node_modules/next/node_modules/postcss": {
1425
+ "version": "8.4.31",
1426
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1427
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1428
+ "funding": [
1429
+ {
1430
+ "type": "opencollective",
1431
+ "url": "https://opencollective.com/postcss/"
1432
+ },
1433
+ {
1434
+ "type": "tidelift",
1435
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1436
+ },
1437
+ {
1438
+ "type": "github",
1439
+ "url": "https://github.com/sponsors/ai"
1440
+ }
1441
+ ],
1442
+ "license": "MIT",
1443
+ "dependencies": {
1444
+ "nanoid": "^3.3.6",
1445
+ "picocolors": "^1.0.0",
1446
+ "source-map-js": "^1.0.2"
1447
+ },
1448
+ "engines": {
1449
+ "node": "^10 || ^12 || >=14"
1450
+ }
1451
+ },
1452
+ "node_modules/node-releases": {
1453
+ "version": "2.0.36",
1454
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
1455
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
1456
+ "dev": true,
1457
+ "license": "MIT"
1458
+ },
1459
+ "node_modules/normalize-path": {
1460
+ "version": "3.0.0",
1461
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1462
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1463
+ "dev": true,
1464
+ "license": "MIT",
1465
+ "engines": {
1466
+ "node": ">=0.10.0"
1467
+ }
1468
+ },
1469
+ "node_modules/normalize-range": {
1470
+ "version": "0.1.2",
1471
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
1472
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
1473
+ "dev": true,
1474
+ "license": "MIT",
1475
+ "engines": {
1476
+ "node": ">=0.10.0"
1477
+ }
1478
+ },
1479
+ "node_modules/object-assign": {
1480
+ "version": "4.1.1",
1481
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1482
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1483
+ "dev": true,
1484
+ "license": "MIT",
1485
+ "engines": {
1486
+ "node": ">=0.10.0"
1487
+ }
1488
+ },
1489
+ "node_modules/object-hash": {
1490
+ "version": "3.0.0",
1491
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1492
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1493
+ "dev": true,
1494
+ "license": "MIT",
1495
+ "engines": {
1496
+ "node": ">= 6"
1497
+ }
1498
+ },
1499
+ "node_modules/path-parse": {
1500
+ "version": "1.0.7",
1501
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1502
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1503
+ "dev": true,
1504
+ "license": "MIT"
1505
+ },
1506
+ "node_modules/picocolors": {
1507
+ "version": "1.1.1",
1508
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1509
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1510
+ "license": "ISC"
1511
+ },
1512
+ "node_modules/picomatch": {
1513
+ "version": "2.3.2",
1514
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1515
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1516
+ "dev": true,
1517
+ "license": "MIT",
1518
+ "engines": {
1519
+ "node": ">=8.6"
1520
+ },
1521
+ "funding": {
1522
+ "url": "https://github.com/sponsors/jonschlinkert"
1523
+ }
1524
+ },
1525
+ "node_modules/pify": {
1526
+ "version": "2.3.0",
1527
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1528
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1529
+ "dev": true,
1530
+ "license": "MIT",
1531
+ "engines": {
1532
+ "node": ">=0.10.0"
1533
+ }
1534
+ },
1535
+ "node_modules/pirates": {
1536
+ "version": "4.0.7",
1537
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1538
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1539
+ "dev": true,
1540
+ "license": "MIT",
1541
+ "engines": {
1542
+ "node": ">= 6"
1543
+ }
1544
+ },
1545
+ "node_modules/postcss": {
1546
+ "version": "8.5.3",
1547
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
1548
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
1549
+ "dev": true,
1550
+ "funding": [
1551
+ {
1552
+ "type": "opencollective",
1553
+ "url": "https://opencollective.com/postcss/"
1554
+ },
1555
+ {
1556
+ "type": "tidelift",
1557
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1558
+ },
1559
+ {
1560
+ "type": "github",
1561
+ "url": "https://github.com/sponsors/ai"
1562
+ }
1563
+ ],
1564
+ "license": "MIT",
1565
+ "dependencies": {
1566
+ "nanoid": "^3.3.8",
1567
+ "picocolors": "^1.1.1",
1568
+ "source-map-js": "^1.2.1"
1569
+ },
1570
+ "engines": {
1571
+ "node": "^10 || ^12 || >=14"
1572
+ }
1573
+ },
1574
+ "node_modules/postcss-import": {
1575
+ "version": "15.1.0",
1576
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1577
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1578
+ "dev": true,
1579
+ "license": "MIT",
1580
+ "dependencies": {
1581
+ "postcss-value-parser": "^4.0.0",
1582
+ "read-cache": "^1.0.0",
1583
+ "resolve": "^1.1.7"
1584
+ },
1585
+ "engines": {
1586
+ "node": ">=14.0.0"
1587
+ },
1588
+ "peerDependencies": {
1589
+ "postcss": "^8.0.0"
1590
+ }
1591
+ },
1592
+ "node_modules/postcss-js": {
1593
+ "version": "4.1.0",
1594
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1595
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1596
+ "dev": true,
1597
+ "funding": [
1598
+ {
1599
+ "type": "opencollective",
1600
+ "url": "https://opencollective.com/postcss/"
1601
+ },
1602
+ {
1603
+ "type": "github",
1604
+ "url": "https://github.com/sponsors/ai"
1605
+ }
1606
+ ],
1607
+ "license": "MIT",
1608
+ "dependencies": {
1609
+ "camelcase-css": "^2.0.1"
1610
+ },
1611
+ "engines": {
1612
+ "node": "^12 || ^14 || >= 16"
1613
+ },
1614
+ "peerDependencies": {
1615
+ "postcss": "^8.4.21"
1616
+ }
1617
+ },
1618
+ "node_modules/postcss-nested": {
1619
+ "version": "6.2.0",
1620
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1621
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1622
+ "dev": true,
1623
+ "funding": [
1624
+ {
1625
+ "type": "opencollective",
1626
+ "url": "https://opencollective.com/postcss/"
1627
+ },
1628
+ {
1629
+ "type": "github",
1630
+ "url": "https://github.com/sponsors/ai"
1631
+ }
1632
+ ],
1633
+ "license": "MIT",
1634
+ "dependencies": {
1635
+ "postcss-selector-parser": "^6.1.1"
1636
+ },
1637
+ "engines": {
1638
+ "node": ">=12.0"
1639
+ },
1640
+ "peerDependencies": {
1641
+ "postcss": "^8.2.14"
1642
+ }
1643
+ },
1644
+ "node_modules/postcss-selector-parser": {
1645
+ "version": "6.1.2",
1646
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1647
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1648
+ "dev": true,
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "cssesc": "^3.0.0",
1652
+ "util-deprecate": "^1.0.2"
1653
+ },
1654
+ "engines": {
1655
+ "node": ">=4"
1656
+ }
1657
+ },
1658
+ "node_modules/postcss-value-parser": {
1659
+ "version": "4.2.0",
1660
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1661
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1662
+ "dev": true,
1663
+ "license": "MIT"
1664
+ },
1665
+ "node_modules/queue-microtask": {
1666
+ "version": "1.2.3",
1667
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1668
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1669
+ "dev": true,
1670
+ "funding": [
1671
+ {
1672
+ "type": "github",
1673
+ "url": "https://github.com/sponsors/feross"
1674
+ },
1675
+ {
1676
+ "type": "patreon",
1677
+ "url": "https://www.patreon.com/feross"
1678
+ },
1679
+ {
1680
+ "type": "consulting",
1681
+ "url": "https://feross.org/support"
1682
+ }
1683
+ ],
1684
+ "license": "MIT"
1685
+ },
1686
+ "node_modules/react": {
1687
+ "version": "19.1.0",
1688
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
1689
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
1690
+ "license": "MIT",
1691
+ "engines": {
1692
+ "node": ">=0.10.0"
1693
+ }
1694
+ },
1695
+ "node_modules/react-dom": {
1696
+ "version": "19.1.0",
1697
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
1698
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
1699
+ "license": "MIT",
1700
+ "dependencies": {
1701
+ "scheduler": "^0.26.0"
1702
+ },
1703
+ "peerDependencies": {
1704
+ "react": "^19.1.0"
1705
+ }
1706
+ },
1707
+ "node_modules/read-cache": {
1708
+ "version": "1.0.0",
1709
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1710
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1711
+ "dev": true,
1712
+ "license": "MIT",
1713
+ "dependencies": {
1714
+ "pify": "^2.3.0"
1715
+ }
1716
+ },
1717
+ "node_modules/readdirp": {
1718
+ "version": "3.6.0",
1719
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1720
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1721
+ "dev": true,
1722
+ "license": "MIT",
1723
+ "dependencies": {
1724
+ "picomatch": "^2.2.1"
1725
+ },
1726
+ "engines": {
1727
+ "node": ">=8.10.0"
1728
+ }
1729
+ },
1730
+ "node_modules/resolve": {
1731
+ "version": "1.22.11",
1732
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
1733
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
1734
+ "dev": true,
1735
+ "license": "MIT",
1736
+ "dependencies": {
1737
+ "is-core-module": "^2.16.1",
1738
+ "path-parse": "^1.0.7",
1739
+ "supports-preserve-symlinks-flag": "^1.0.0"
1740
+ },
1741
+ "bin": {
1742
+ "resolve": "bin/resolve"
1743
+ },
1744
+ "engines": {
1745
+ "node": ">= 0.4"
1746
+ },
1747
+ "funding": {
1748
+ "url": "https://github.com/sponsors/ljharb"
1749
+ }
1750
+ },
1751
+ "node_modules/reusify": {
1752
+ "version": "1.1.0",
1753
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1754
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1755
+ "dev": true,
1756
+ "license": "MIT",
1757
+ "engines": {
1758
+ "iojs": ">=1.0.0",
1759
+ "node": ">=0.10.0"
1760
+ }
1761
+ },
1762
+ "node_modules/run-parallel": {
1763
+ "version": "1.2.0",
1764
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1765
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1766
+ "dev": true,
1767
+ "funding": [
1768
+ {
1769
+ "type": "github",
1770
+ "url": "https://github.com/sponsors/feross"
1771
+ },
1772
+ {
1773
+ "type": "patreon",
1774
+ "url": "https://www.patreon.com/feross"
1775
+ },
1776
+ {
1777
+ "type": "consulting",
1778
+ "url": "https://feross.org/support"
1779
+ }
1780
+ ],
1781
+ "license": "MIT",
1782
+ "dependencies": {
1783
+ "queue-microtask": "^1.2.2"
1784
+ }
1785
+ },
1786
+ "node_modules/scheduler": {
1787
+ "version": "0.26.0",
1788
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
1789
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
1790
+ "license": "MIT"
1791
+ },
1792
+ "node_modules/semver": {
1793
+ "version": "7.7.4",
1794
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1795
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1796
+ "license": "ISC",
1797
+ "optional": true,
1798
+ "bin": {
1799
+ "semver": "bin/semver.js"
1800
+ },
1801
+ "engines": {
1802
+ "node": ">=10"
1803
+ }
1804
+ },
1805
+ "node_modules/sharp": {
1806
+ "version": "0.34.5",
1807
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1808
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1809
+ "hasInstallScript": true,
1810
+ "license": "Apache-2.0",
1811
+ "optional": true,
1812
+ "dependencies": {
1813
+ "@img/colour": "^1.0.0",
1814
+ "detect-libc": "^2.1.2",
1815
+ "semver": "^7.7.3"
1816
+ },
1817
+ "engines": {
1818
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1819
+ },
1820
+ "funding": {
1821
+ "url": "https://opencollective.com/libvips"
1822
+ },
1823
+ "optionalDependencies": {
1824
+ "@img/sharp-darwin-arm64": "0.34.5",
1825
+ "@img/sharp-darwin-x64": "0.34.5",
1826
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
1827
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
1828
+ "@img/sharp-libvips-linux-arm": "1.2.4",
1829
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
1830
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
1831
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
1832
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
1833
+ "@img/sharp-libvips-linux-x64": "1.2.4",
1834
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1835
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1836
+ "@img/sharp-linux-arm": "0.34.5",
1837
+ "@img/sharp-linux-arm64": "0.34.5",
1838
+ "@img/sharp-linux-ppc64": "0.34.5",
1839
+ "@img/sharp-linux-riscv64": "0.34.5",
1840
+ "@img/sharp-linux-s390x": "0.34.5",
1841
+ "@img/sharp-linux-x64": "0.34.5",
1842
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
1843
+ "@img/sharp-linuxmusl-x64": "0.34.5",
1844
+ "@img/sharp-wasm32": "0.34.5",
1845
+ "@img/sharp-win32-arm64": "0.34.5",
1846
+ "@img/sharp-win32-ia32": "0.34.5",
1847
+ "@img/sharp-win32-x64": "0.34.5"
1848
+ }
1849
+ },
1850
+ "node_modules/source-map-js": {
1851
+ "version": "1.2.1",
1852
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1853
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1854
+ "license": "BSD-3-Clause",
1855
+ "engines": {
1856
+ "node": ">=0.10.0"
1857
+ }
1858
+ },
1859
+ "node_modules/styled-jsx": {
1860
+ "version": "5.1.6",
1861
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
1862
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
1863
+ "license": "MIT",
1864
+ "dependencies": {
1865
+ "client-only": "0.0.1"
1866
+ },
1867
+ "engines": {
1868
+ "node": ">= 12.0.0"
1869
+ },
1870
+ "peerDependencies": {
1871
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
1872
+ },
1873
+ "peerDependenciesMeta": {
1874
+ "@babel/core": {
1875
+ "optional": true
1876
+ },
1877
+ "babel-plugin-macros": {
1878
+ "optional": true
1879
+ }
1880
+ }
1881
+ },
1882
+ "node_modules/sucrase": {
1883
+ "version": "3.35.1",
1884
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1885
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1886
+ "dev": true,
1887
+ "license": "MIT",
1888
+ "dependencies": {
1889
+ "@jridgewell/gen-mapping": "^0.3.2",
1890
+ "commander": "^4.0.0",
1891
+ "lines-and-columns": "^1.1.6",
1892
+ "mz": "^2.7.0",
1893
+ "pirates": "^4.0.1",
1894
+ "tinyglobby": "^0.2.11",
1895
+ "ts-interface-checker": "^0.1.9"
1896
+ },
1897
+ "bin": {
1898
+ "sucrase": "bin/sucrase",
1899
+ "sucrase-node": "bin/sucrase-node"
1900
+ },
1901
+ "engines": {
1902
+ "node": ">=16 || 14 >=14.17"
1903
+ }
1904
+ },
1905
+ "node_modules/supports-preserve-symlinks-flag": {
1906
+ "version": "1.0.0",
1907
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1908
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1909
+ "dev": true,
1910
+ "license": "MIT",
1911
+ "engines": {
1912
+ "node": ">= 0.4"
1913
+ },
1914
+ "funding": {
1915
+ "url": "https://github.com/sponsors/ljharb"
1916
+ }
1917
+ },
1918
+ "node_modules/tailwindcss": {
1919
+ "version": "3.4.17",
1920
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
1921
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
1922
+ "dev": true,
1923
+ "license": "MIT",
1924
+ "dependencies": {
1925
+ "@alloc/quick-lru": "^5.2.0",
1926
+ "arg": "^5.0.2",
1927
+ "chokidar": "^3.6.0",
1928
+ "didyoumean": "^1.2.2",
1929
+ "dlv": "^1.1.3",
1930
+ "fast-glob": "^3.3.2",
1931
+ "glob-parent": "^6.0.2",
1932
+ "is-glob": "^4.0.3",
1933
+ "jiti": "^1.21.6",
1934
+ "lilconfig": "^3.1.3",
1935
+ "micromatch": "^4.0.8",
1936
+ "normalize-path": "^3.0.0",
1937
+ "object-hash": "^3.0.0",
1938
+ "picocolors": "^1.1.1",
1939
+ "postcss": "^8.4.47",
1940
+ "postcss-import": "^15.1.0",
1941
+ "postcss-js": "^4.0.1",
1942
+ "postcss-load-config": "^4.0.2",
1943
+ "postcss-nested": "^6.2.0",
1944
+ "postcss-selector-parser": "^6.1.2",
1945
+ "resolve": "^1.22.8",
1946
+ "sucrase": "^3.35.0"
1947
+ },
1948
+ "bin": {
1949
+ "tailwind": "lib/cli.js",
1950
+ "tailwindcss": "lib/cli.js"
1951
+ },
1952
+ "engines": {
1953
+ "node": ">=14.0.0"
1954
+ }
1955
+ },
1956
+ "node_modules/tailwindcss/node_modules/postcss-load-config": {
1957
+ "version": "4.0.2",
1958
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
1959
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
1960
+ "dev": true,
1961
+ "funding": [
1962
+ {
1963
+ "type": "opencollective",
1964
+ "url": "https://opencollective.com/postcss/"
1965
+ },
1966
+ {
1967
+ "type": "github",
1968
+ "url": "https://github.com/sponsors/ai"
1969
+ }
1970
+ ],
1971
+ "license": "MIT",
1972
+ "dependencies": {
1973
+ "lilconfig": "^3.0.0",
1974
+ "yaml": "^2.3.4"
1975
+ },
1976
+ "engines": {
1977
+ "node": ">= 14"
1978
+ },
1979
+ "peerDependencies": {
1980
+ "postcss": ">=8.0.9",
1981
+ "ts-node": ">=9.0.0"
1982
+ },
1983
+ "peerDependenciesMeta": {
1984
+ "postcss": {
1985
+ "optional": true
1986
+ },
1987
+ "ts-node": {
1988
+ "optional": true
1989
+ }
1990
+ }
1991
+ },
1992
+ "node_modules/thenify": {
1993
+ "version": "3.3.1",
1994
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
1995
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
1996
+ "dev": true,
1997
+ "license": "MIT",
1998
+ "dependencies": {
1999
+ "any-promise": "^1.0.0"
2000
+ }
2001
+ },
2002
+ "node_modules/thenify-all": {
2003
+ "version": "1.6.0",
2004
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2005
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2006
+ "dev": true,
2007
+ "license": "MIT",
2008
+ "dependencies": {
2009
+ "thenify": ">= 3.1.0 < 4"
2010
+ },
2011
+ "engines": {
2012
+ "node": ">=0.8"
2013
+ }
2014
+ },
2015
+ "node_modules/tinyglobby": {
2016
+ "version": "0.2.15",
2017
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2018
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2019
+ "dev": true,
2020
+ "license": "MIT",
2021
+ "dependencies": {
2022
+ "fdir": "^6.5.0",
2023
+ "picomatch": "^4.0.3"
2024
+ },
2025
+ "engines": {
2026
+ "node": ">=12.0.0"
2027
+ },
2028
+ "funding": {
2029
+ "url": "https://github.com/sponsors/SuperchupuDev"
2030
+ }
2031
+ },
2032
+ "node_modules/tinyglobby/node_modules/fdir": {
2033
+ "version": "6.5.0",
2034
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2035
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2036
+ "dev": true,
2037
+ "license": "MIT",
2038
+ "engines": {
2039
+ "node": ">=12.0.0"
2040
+ },
2041
+ "peerDependencies": {
2042
+ "picomatch": "^3 || ^4"
2043
+ },
2044
+ "peerDependenciesMeta": {
2045
+ "picomatch": {
2046
+ "optional": true
2047
+ }
2048
+ }
2049
+ },
2050
+ "node_modules/tinyglobby/node_modules/picomatch": {
2051
+ "version": "4.0.4",
2052
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2053
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2054
+ "dev": true,
2055
+ "license": "MIT",
2056
+ "engines": {
2057
+ "node": ">=12"
2058
+ },
2059
+ "funding": {
2060
+ "url": "https://github.com/sponsors/jonschlinkert"
2061
+ }
2062
+ },
2063
+ "node_modules/to-regex-range": {
2064
+ "version": "5.0.1",
2065
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2066
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2067
+ "dev": true,
2068
+ "license": "MIT",
2069
+ "dependencies": {
2070
+ "is-number": "^7.0.0"
2071
+ },
2072
+ "engines": {
2073
+ "node": ">=8.0"
2074
+ }
2075
+ },
2076
+ "node_modules/ts-interface-checker": {
2077
+ "version": "0.1.13",
2078
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2079
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2080
+ "dev": true,
2081
+ "license": "Apache-2.0"
2082
+ },
2083
+ "node_modules/tslib": {
2084
+ "version": "2.8.1",
2085
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2086
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2087
+ "license": "0BSD"
2088
+ },
2089
+ "node_modules/typescript": {
2090
+ "version": "5.8.3",
2091
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
2092
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
2093
+ "dev": true,
2094
+ "license": "Apache-2.0",
2095
+ "bin": {
2096
+ "tsc": "bin/tsc",
2097
+ "tsserver": "bin/tsserver"
2098
+ },
2099
+ "engines": {
2100
+ "node": ">=14.17"
2101
+ }
2102
+ },
2103
+ "node_modules/undici-types": {
2104
+ "version": "6.21.0",
2105
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2106
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2107
+ "dev": true,
2108
+ "license": "MIT"
2109
+ },
2110
+ "node_modules/update-browserslist-db": {
2111
+ "version": "1.2.3",
2112
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2113
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2114
+ "dev": true,
2115
+ "funding": [
2116
+ {
2117
+ "type": "opencollective",
2118
+ "url": "https://opencollective.com/browserslist"
2119
+ },
2120
+ {
2121
+ "type": "tidelift",
2122
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2123
+ },
2124
+ {
2125
+ "type": "github",
2126
+ "url": "https://github.com/sponsors/ai"
2127
+ }
2128
+ ],
2129
+ "license": "MIT",
2130
+ "dependencies": {
2131
+ "escalade": "^3.2.0",
2132
+ "picocolors": "^1.1.1"
2133
+ },
2134
+ "bin": {
2135
+ "update-browserslist-db": "cli.js"
2136
+ },
2137
+ "peerDependencies": {
2138
+ "browserslist": ">= 4.21.0"
2139
+ }
2140
+ },
2141
+ "node_modules/util-deprecate": {
2142
+ "version": "1.0.2",
2143
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2144
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2145
+ "dev": true,
2146
+ "license": "MIT"
2147
+ },
2148
+ "node_modules/yaml": {
2149
+ "version": "2.8.3",
2150
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
2151
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
2152
+ "dev": true,
2153
+ "license": "ISC",
2154
+ "bin": {
2155
+ "yaml": "bin.mjs"
2156
+ },
2157
+ "engines": {
2158
+ "node": ">= 14.6"
2159
+ },
2160
+ "funding": {
2161
+ "url": "https://github.com/sponsors/eemeli"
2162
+ }
2163
+ }
2164
+ }
2165
+ }
package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "resumeai-pro",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "15.5.14",
13
+ "react": "19.1.0",
14
+ "react-dom": "19.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "22.14.1",
18
+ "@types/react": "19.1.1",
19
+ "@types/react-dom": "19.1.2",
20
+ "autoprefixer": "10.4.21",
21
+ "postcss": "8.5.3",
22
+ "tailwindcss": "3.4.17",
23
+ "typescript": "5.8.3"
24
+ }
25
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
scripts/start-prod.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ cd /app/backend
5
+ uvicorn app.main:app --host 127.0.0.1 --port 8000 &
6
+ BACKEND_PID=$!
7
+
8
+ cleanup() {
9
+ if kill -0 "$BACKEND_PID" 2>/dev/null; then
10
+ kill "$BACKEND_PID"
11
+ fi
12
+ }
13
+
14
+ trap cleanup EXIT INT TERM
15
+
16
+ cd /app/frontend
17
+ exec node server.js
tailwind.config.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: [
5
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ ],
8
+ theme: {
9
+ extend: {
10
+ colors: {
11
+ ink: "#10121a",
12
+ sand: "#f8f3ea",
13
+ clay: "#d7a26c",
14
+ pine: "#21493f",
15
+ wine: "#7f3b3b",
16
+ mist: "#dde5db",
17
+ },
18
+ boxShadow: {
19
+ card: "0 18px 45px rgba(16, 18, 26, 0.08)",
20
+ },
21
+ },
22
+ },
23
+ plugins: [],
24
+ };
25
+
26
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@/*": ["./*"]
19
+ },
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ]
25
+ },
26
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27
+ "exclude": ["node_modules"]
28
+ }