⚡ DraftMe
Upload a PDF CV, paste a job description, and generate an ATS-friendly resume.
import json from datetime import datetime from html import escape from pathlib import Path import gradio as gr from fastapi import File, Form, HTTPException, UploadFile from fastapi.responses import FileResponse, HTMLResponse, Response, StreamingResponse from core.pipeline import run_pipeline from models.config import AppSettings from models.pipeline import PipelineResult, StatusEvent APP_CSS = """ :root { color-scheme: light; --bg: #f5f6f8; --panel: #ffffff; --panel-soft: #fbfcfd; --ink: #1f242c; --muted: #687080; --line: #dce1e8; --line-strong: #c9d1db; --accent: #176b87; --accent-dark: #104f65; --accent-soft: #e7f3f6; --green: #18794e; --green-soft: #e7f5ee; --amber: #a15c07; --amber-soft: #fff4df; --red: #bf2c2c; --red-soft: #fdecec; --shadow: 0 12px 30px rgba(25, 35, 50, 0.08); } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } button, input, textarea { font: inherit; } .app { width: min(1220px, calc(100vw - 36px)); margin: 0 auto; padding: 28px 0 36px; } .topbar { display: flex; align-items: end; justify-content: space-between; gap: 28px; margin-bottom: 22px; } .brand h1 { margin: 0; font-size: 34px; line-height: 1.05; letter-spacing: 0; } .brand p { margin: 8px 0 0; color: var(--muted); } .settings { width: 360px; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow); } .setting-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; color: var(--muted); font-size: 13px; font-weight: 700; } .settings-grid { display: grid; gap: 14px; } .setting-field label { display: block; margin-bottom: 7px; color: var(--muted); font-size: 13px; font-weight: 800; } .model-select { width: 100%; min-height: 38px; padding: 0 12px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); background: #ffffff; outline: none; } .model-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(23, 107, 135, 0.15); } .range-row { display: grid; grid-template-columns: 1fr 34px; gap: 12px; align-items: center; } input[type="range"] { width: 100%; accent-color: var(--accent); } .range-value { min-height: 28px; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; color: var(--accent-dark); background: var(--accent-soft); font-size: 13px; font-weight: 800; } .workspace { display: grid; grid-template-columns: 0.78fr 1.22fr; gap: 18px; align-items: stretch; } .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow); } .input-panel { padding: 18px; } .panel-title { display: flex; align-items: center; gap: 9px; margin: 0 0 12px; font-size: 14px; font-weight: 800; } .icon { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; } .dropzone { position: relative; display: grid; place-items: center; min-height: 158px; padding: 20px; border: 1.5px dashed var(--line-strong); border-radius: 8px; background: var(--panel-soft); transition: border-color 150ms ease, background 150ms ease; } .dropzone.dragging { border-color: var(--accent); background: var(--accent-soft); } .dropzone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } .drop-inner { text-align: center; pointer-events: none; } .drop-inner strong { display: block; margin-bottom: 6px; } .file-name { color: var(--muted); font-size: 13px; overflow-wrap: anywhere; } .field { margin-top: 16px; } .field label { display: block; margin-bottom: 8px; color: var(--muted); font-size: 13px; font-weight: 800; } textarea { width: 100%; min-height: 286px; resize: vertical; padding: 13px 14px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); background: #ffffff; outline: none; } textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(23, 107, 135, 0.15); } .actions { display: flex; justify-content: flex-end; margin-top: 16px; } .primary { display: inline-flex; align-items: center; justify-content: center; gap: 9px; min-height: 42px; padding: 0 18px; border: 0; border-radius: 8px; color: #ffffff; background: var(--accent); font-weight: 800; cursor: pointer; } .primary:hover { background: var(--accent-dark); } .primary:disabled { cursor: not-allowed; opacity: 0.6; } .output-panel { display: grid; grid-template-rows: auto minmax(300px, 1fr) auto; overflow: hidden; } .gauge-panel { display: grid; grid-template-columns: 270px 1fr; gap: 18px; align-items: center; padding: 18px 18px 14px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, #fbfcfd 0%, #ffffff 100%); } .gauge { position: relative; height: 176px; } .gauge svg { width: 100%; height: 150px; display: block; } .gauge-track { stroke: #e3e8ee; stroke-width: 16; fill: none; stroke-linecap: round; } .gauge-fill { stroke: var(--accent); stroke-width: 16; fill: none; stroke-linecap: round; stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset 350ms ease, stroke 200ms ease; } .gauge-fill.done { stroke: var(--green); } .gauge-fill.error { stroke: var(--red); } .gauge-needle { stroke: var(--ink); stroke-width: 4; stroke-linecap: round; transform-origin: 120px 120px; transform: rotate(-90deg); transition: transform 350ms ease; } .gauge-hub { fill: var(--panel); stroke: var(--ink); stroke-width: 4; } .gauge-scale { position: absolute; left: 24px; right: 24px; bottom: 2px; display: flex; justify-content: space-between; margin: 0; color: var(--muted); font-size: 12px; font-weight: 800; } .gauge-readout { display: grid; gap: 8px; } .gauge-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .gauge-value { font-size: 42px; line-height: 1; font-weight: 900; letter-spacing: 0; font-variant-numeric: tabular-nums; } .gauge-stage { color: var(--muted); font-weight: 800; } .telemetry-title { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 900; } .status-pill { display: inline-flex; align-items: center; min-height: 26px; padding: 0 10px; border-radius: 999px; color: var(--accent-dark); background: var(--accent-soft); font-size: 12px; font-weight: 900; } .status-pill.done { color: var(--green); background: var(--green-soft); } .status-pill.error { color: var(--red); background: var(--red-soft); } .telemetry-body { height: 100%; max-height: 420px; overflow-y: auto; padding: 12px 16px 18px; } .empty-state { height: 100%; min-height: 360px; display: grid; place-items: center; color: var(--muted); text-align: center; } .event { display: grid; grid-template-columns: 92px 112px 1fr; gap: 12px; padding: 11px 0; border-bottom: 1px solid #edf0f4; } .event:last-child { border-bottom: 0; } .event-time { color: var(--muted); font-size: 12px; line-height: 24px; font-variant-numeric: tabular-nums; } .event-step { display: inline-flex; align-items: center; justify-content: center; min-height: 24px; padding: 0 9px; border-radius: 999px; color: var(--accent-dark); background: var(--accent-soft); font-size: 12px; font-weight: 900; text-transform: uppercase; } .event.active .event-step { color: var(--amber); background: var(--amber-soft); } .event.done .event-step { color: var(--green); background: var(--green-soft); } .event.error .event-step { color: var(--red); background: var(--red-soft); } .event-message { min-width: 0; line-height: 1.45; overflow-wrap: anywhere; } .event-meta { display: inline-flex; margin-left: 8px; padding: 1px 7px; border-radius: 999px; color: var(--muted); background: #f0f3f6; font-size: 12px; font-weight: 700; } .download-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 74px; padding: 14px 16px; border-top: 1px solid var(--line); background: var(--panel-soft); } .download-meta { min-width: 0; } .download-meta strong { display: block; } .download-meta span { display: block; margin-top: 3px; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; } .download-link { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 38px; padding: 0 14px; border-radius: 8px; color: #ffffff; background: var(--green); text-decoration: none; font-weight: 800; white-space: nowrap; } .download-link.hidden { display: none; } @media (max-width: 900px) { .topbar, .workspace { display: block; } .settings { width: 100%; margin-top: 18px; } .output-panel { margin-top: 18px; } .gauge-panel { grid-template-columns: 1fr; } .gauge { max-width: 320px; margin: 0 auto; width: 100%; } } @media (max-width: 680px) { .app { width: min(100vw - 24px, 1220px); padding-top: 18px; } .brand h1 { font-size: 28px; } .event { grid-template-columns: 1fr; gap: 5px; } .event-time { line-height: 1.2; } .download-bar { align-items: stretch; flex-direction: column; } } """ APP_JS = """ const form = document.querySelector("#optimize-form"); const fileInput = document.querySelector("#cv-file"); const fileName = document.querySelector("#file-name"); const dropzone = document.querySelector("#dropzone"); const modelSelect = document.querySelector("#model-select"); const iterations = document.querySelector("#max-iterations"); const iterationValue = document.querySelector("#iteration-value"); const runButton = document.querySelector("#run-button"); const telemetryBody = document.querySelector("#telemetry-body"); const statusPill = document.querySelector("#status-pill"); const downloadLink = document.querySelector("#download-link"); const downloadName = document.querySelector("#download-name"); const gaugeFill = document.querySelector("#gauge-fill"); const gaugeNeedle = document.querySelector("#gauge-needle"); const gaugeValue = document.querySelector("#gauge-value"); const gaugeStage = document.querySelector("#gauge-stage"); const icons = { run: '', busy: '' }; function setStatus(label, state) { statusPill.textContent = label; statusPill.className = "status-pill" + (state ? " " + state : ""); gaugeFill.className.baseVal = "gauge-fill" + (state ? " " + state : ""); } function clearTelemetry() { telemetryBody.innerHTML = '
Upload a PDF CV, paste a job description, and generate an ATS-friendly resume.