| from fastapi import APIRouter |
| from fastapi.responses import HTMLResponse |
|
|
| router = APIRouter() |
|
|
|
|
| @router.get("/api-docs", include_in_schema=False) |
| async def custom_api_docs() -> HTMLResponse: |
| return HTMLResponse(_DOCS_HTML) |
|
|
|
|
| _DOCS_HTML = r""" |
| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>LockedIn AI Service API Docs</title> |
| <meta |
| name="description" |
| content="Beautiful custom API documentation for the LockedIn AI Service roadmap generation API." |
| /> |
| <style> |
| :root { |
| color-scheme: dark; |
| --bg: #07110f; |
| --panel: rgba(255, 255, 255, 0.075); |
| --panel-strong: rgba(255, 255, 255, 0.12); |
| --line: rgba(255, 255, 255, 0.16); |
| --text: #f5fff9; |
| --muted: #b6c8bf; |
| --soft: #8aa79a; |
| --green: #72f2a7; |
| --cyan: #7be7ff; |
| --gold: #ffd166; |
| --red: #ff7a90; |
| --shadow: 0 28px 80px rgba(0, 0, 0, 0.34); |
| --radius: 8px; |
| font-family: |
| Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| } |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| html { |
| scroll-behavior: smooth; |
| } |
| |
| body { |
| margin: 0; |
| min-height: 100vh; |
| color: var(--text); |
| background: |
| radial-gradient(circle at 12% 12%, rgba(114, 242, 167, 0.17), transparent 26rem), |
| radial-gradient(circle at 85% 4%, rgba(123, 231, 255, 0.13), transparent 30rem), |
| linear-gradient(135deg, #07110f 0%, #0b1715 45%, #10170d 100%); |
| letter-spacing: 0; |
| } |
| |
| a { |
| color: inherit; |
| } |
| |
| .shell { |
| width: min(1180px, calc(100% - 32px)); |
| margin: 0 auto; |
| } |
| |
| .nav { |
| position: sticky; |
| top: 0; |
| z-index: 20; |
| border-bottom: 1px solid var(--line); |
| background: rgba(7, 17, 15, 0.82); |
| backdrop-filter: blur(18px); |
| } |
| |
| .nav-inner { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 18px; |
| padding: 14px 0; |
| } |
| |
| .brand { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| min-width: 0; |
| } |
| |
| .mark { |
| display: grid; |
| width: 38px; |
| height: 38px; |
| place-items: center; |
| border: 1px solid rgba(114, 242, 167, 0.38); |
| border-radius: var(--radius); |
| color: #06100d; |
| background: linear-gradient(135deg, var(--green), var(--cyan)); |
| font-weight: 900; |
| box-shadow: 0 14px 38px rgba(114, 242, 167, 0.22); |
| } |
| |
| .brand-copy strong { |
| display: block; |
| font-size: 0.96rem; |
| } |
| |
| .brand-copy span { |
| display: block; |
| color: var(--soft); |
| font-size: 0.8rem; |
| } |
| |
| .nav-links { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| flex-wrap: wrap; |
| justify-content: flex-end; |
| } |
| |
| .nav-links a, |
| .button { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| min-height: 36px; |
| padding: 0 12px; |
| border: 1px solid transparent; |
| border-radius: var(--radius); |
| color: var(--muted); |
| text-decoration: none; |
| font-size: 0.88rem; |
| transition: 160ms ease; |
| } |
| |
| .nav-links a:hover, |
| .button:hover { |
| border-color: var(--line); |
| color: var(--text); |
| background: rgba(255, 255, 255, 0.08); |
| } |
| |
| .button.primary { |
| color: #06100d; |
| border-color: transparent; |
| background: linear-gradient(135deg, var(--green), var(--cyan)); |
| font-weight: 800; |
| } |
| |
| .hero { |
| padding: 74px 0 46px; |
| } |
| |
| .hero-grid { |
| display: grid; |
| grid-template-columns: minmax(0, 1.08fr) minmax(330px, 0.72fr); |
| gap: 28px; |
| align-items: stretch; |
| } |
| |
| .eyebrow { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| width: fit-content; |
| padding: 8px 10px; |
| border: 1px solid rgba(114, 242, 167, 0.28); |
| border-radius: var(--radius); |
| color: var(--green); |
| background: rgba(114, 242, 167, 0.08); |
| font-size: 0.84rem; |
| font-weight: 700; |
| } |
| |
| .dot { |
| width: 8px; |
| height: 8px; |
| border-radius: 99px; |
| background: var(--green); |
| box-shadow: 0 0 18px var(--green); |
| } |
| |
| h1, |
| h2, |
| h3 { |
| margin: 0; |
| line-height: 1.04; |
| letter-spacing: 0; |
| } |
| |
| h1 { |
| max-width: 880px; |
| margin-top: 22px; |
| font-size: clamp(2.75rem, 7vw, 6.6rem); |
| } |
| |
| .lead { |
| max-width: 760px; |
| margin: 22px 0 0; |
| color: var(--muted); |
| font-size: clamp(1rem, 2vw, 1.26rem); |
| line-height: 1.65; |
| } |
| |
| .hero-actions { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 12px; |
| margin-top: 28px; |
| } |
| |
| .hero-card { |
| padding: 22px; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.055)); |
| box-shadow: var(--shadow); |
| } |
| |
| .hero-card h2 { |
| margin-bottom: 14px; |
| font-size: 1.05rem; |
| } |
| |
| .stat-grid { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 10px; |
| } |
| |
| .stat { |
| min-height: 92px; |
| padding: 14px; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| background: rgba(0, 0, 0, 0.18); |
| } |
| |
| .stat strong { |
| display: block; |
| color: var(--green); |
| font-size: 1.55rem; |
| } |
| |
| .stat span { |
| display: block; |
| margin-top: 6px; |
| color: var(--muted); |
| font-size: 0.86rem; |
| line-height: 1.35; |
| } |
| |
| section { |
| padding: 42px 0; |
| } |
| |
| .section-head { |
| display: flex; |
| align-items: end; |
| justify-content: space-between; |
| gap: 18px; |
| margin-bottom: 18px; |
| } |
| |
| .section-head h2 { |
| font-size: clamp(1.7rem, 3vw, 2.6rem); |
| } |
| |
| .section-head p { |
| max-width: 560px; |
| margin: 0; |
| color: var(--muted); |
| line-height: 1.6; |
| } |
| |
| .grid { |
| display: grid; |
| gap: 14px; |
| } |
| |
| .grid.three { |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| } |
| |
| .grid.two { |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| |
| .panel { |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| background: var(--panel); |
| box-shadow: 0 16px 44px rgba(0, 0, 0, 0.16); |
| } |
| |
| .panel-pad { |
| padding: 20px; |
| } |
| |
| .panel h3 { |
| margin-bottom: 10px; |
| font-size: 1.08rem; |
| } |
| |
| .panel p, |
| .panel li { |
| color: var(--muted); |
| line-height: 1.62; |
| } |
| |
| .panel p { |
| margin: 0; |
| } |
| |
| .endpoint { |
| overflow: hidden; |
| } |
| |
| .endpoint-top { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 12px; |
| padding: 18px 20px; |
| border-bottom: 1px solid var(--line); |
| background: rgba(255, 255, 255, 0.08); |
| } |
| |
| .method { |
| display: inline-flex; |
| align-items: center; |
| min-height: 32px; |
| padding: 0 10px; |
| border-radius: var(--radius); |
| color: #06100d; |
| background: var(--green); |
| font-weight: 900; |
| font-size: 0.82rem; |
| } |
| |
| code, |
| pre { |
| font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; |
| } |
| |
| .path { |
| color: var(--text); |
| font-weight: 700; |
| overflow-wrap: anywhere; |
| } |
| |
| .tag-list { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| } |
| |
| .tag { |
| display: inline-flex; |
| align-items: center; |
| min-height: 30px; |
| padding: 0 10px; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| color: var(--muted); |
| background: rgba(255, 255, 255, 0.05); |
| font-size: 0.82rem; |
| } |
| |
| .code-card { |
| overflow: hidden; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| background: #06100d; |
| } |
| |
| .code-top { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 12px; |
| min-height: 46px; |
| padding: 0 12px 0 16px; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.12); |
| color: var(--soft); |
| background: rgba(255, 255, 255, 0.045); |
| font-size: 0.82rem; |
| } |
| |
| .copy { |
| min-height: 30px; |
| padding: 0 10px; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| color: var(--text); |
| background: rgba(255, 255, 255, 0.08); |
| cursor: pointer; |
| } |
| |
| pre { |
| margin: 0; |
| padding: 18px; |
| overflow-x: auto; |
| color: #d9ffe8; |
| font-size: 0.88rem; |
| line-height: 1.62; |
| } |
| |
| .table { |
| overflow: hidden; |
| border: 1px solid var(--line); |
| border-radius: var(--radius); |
| } |
| |
| .row { |
| display: grid; |
| grid-template-columns: 150px minmax(0, 1fr) 130px; |
| gap: 16px; |
| padding: 14px 16px; |
| border-bottom: 1px solid var(--line); |
| } |
| |
| .row:last-child { |
| border-bottom: 0; |
| } |
| |
| .row.header { |
| color: var(--soft); |
| background: rgba(255, 255, 255, 0.07); |
| font-size: 0.78rem; |
| font-weight: 800; |
| text-transform: uppercase; |
| } |
| |
| .row div { |
| min-width: 0; |
| overflow-wrap: anywhere; |
| } |
| |
| .status { |
| display: inline-flex; |
| align-items: center; |
| width: fit-content; |
| min-height: 28px; |
| padding: 0 9px; |
| border-radius: var(--radius); |
| font-weight: 900; |
| font-size: 0.8rem; |
| } |
| |
| .ok { |
| color: #06100d; |
| background: var(--green); |
| } |
| |
| .warn { |
| color: #171003; |
| background: var(--gold); |
| } |
| |
| .bad { |
| color: #21040a; |
| background: var(--red); |
| } |
| |
| .timeline { |
| display: grid; |
| gap: 12px; |
| } |
| |
| .step { |
| display: grid; |
| grid-template-columns: 42px minmax(0, 1fr); |
| gap: 14px; |
| } |
| |
| .step-num { |
| display: grid; |
| width: 42px; |
| height: 42px; |
| place-items: center; |
| border: 1px solid rgba(114, 242, 167, 0.36); |
| border-radius: var(--radius); |
| color: var(--green); |
| background: rgba(114, 242, 167, 0.08); |
| font-weight: 900; |
| } |
| |
| .footer { |
| padding: 34px 0 56px; |
| color: var(--soft); |
| text-align: center; |
| } |
| |
| @media (max-width: 900px) { |
| .hero-grid, |
| .grid.two, |
| .grid.three { |
| grid-template-columns: 1fr; |
| } |
| |
| .section-head { |
| align-items: start; |
| flex-direction: column; |
| } |
| |
| .nav-inner { |
| align-items: flex-start; |
| flex-direction: column; |
| } |
| |
| .nav-links { |
| justify-content: flex-start; |
| } |
| |
| .row { |
| grid-template-columns: 1fr; |
| gap: 8px; |
| } |
| } |
| |
| @media (max-width: 560px) { |
| .shell { |
| width: min(100% - 22px, 1180px); |
| } |
| |
| .hero { |
| padding-top: 42px; |
| } |
| |
| .stat-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <nav class="nav"> |
| <div class="shell nav-inner"> |
| <a class="brand" href="#top" aria-label="LockedIn API docs home"> |
| <span class="mark">LI</span> |
| <span class="brand-copy"> |
| <strong>LockedIn AI Service</strong> |
| <span>Roadmap generation API</span> |
| </span> |
| </a> |
| <div class="nav-links" aria-label="Documentation navigation"> |
| <a href="#overview">Overview</a> |
| <a href="#endpoint">Endpoint</a> |
| <a href="#schema">Schema</a> |
| <a href="#errors">Errors</a> |
| <a href="/docs">Swagger</a> |
| <a class="button primary" href="/openapi.json">OpenAPI JSON</a> |
| </div> |
| </div> |
| </nav> |
| |
| <main id="top"> |
| <header class="hero"> |
| <div class="shell hero-grid"> |
| <div> |
| <span class="eyebrow"><span class="dot"></span> Production API documentation</span> |
| <h1>Generate clear learning roadmaps from one API request.</h1> |
| <p class="lead"> |
| LockedIn turns a learner's goal into a structured roadmap with phases, learning nodes, |
| curated free resources, practical projects, stable backend IDs, and metadata your frontend |
| can render immediately. |
| </p> |
| <div class="hero-actions"> |
| <a class="button primary" href="#quickstart">Start with a request</a> |
| <a class="button" href="/health">Check health</a> |
| <a class="button" href="/redoc">Open ReDoc</a> |
| </div> |
| </div> |
| |
| <aside class="hero-card" aria-label="API summary"> |
| <h2>Service snapshot</h2> |
| <div class="stat-grid"> |
| <div class="stat"> |
| <strong>1</strong> |
| <span>Main generation endpoint</span> |
| </div> |
| <div class="stat"> |
| <strong>6</strong> |
| <span>Supported free resource types</span> |
| </div> |
| <div class="stat"> |
| <strong>3 x 3</strong> |
| <span>Phases and nodes per roadmap</span> |
| </div> |
| <div class="stat"> |
| <strong>7860</strong> |
| <span>Default Hugging Face Spaces port</span> |
| </div> |
| </div> |
| </aside> |
| </div> |
| </header> |
| |
| <section id="overview"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>What This API Does</h2> |
| <p> |
| A focused backend for generating beginner-friendly learning plans. The frontend sends a |
| skill and optional preferences; the API handles search, model generation, validation, IDs, |
| caching, and safe error responses. |
| </p> |
| </div> |
| <div class="grid three"> |
| <article class="panel panel-pad"> |
| <h3>Curates free resources</h3> |
| <p> |
| The service searches Tavily and YouTube, filters paid-looking or invalid links, and only |
| allows generated roadmaps to use known candidate URLs. |
| </p> |
| </article> |
| <article class="panel panel-pad"> |
| <h3>Returns frontend-ready JSON</h3> |
| <p> |
| Responses include stable IDs, normalized skill names, phases, nodes, project details, |
| resources, source metadata, and cache status. |
| </p> |
| </article> |
| <article class="panel panel-pad"> |
| <h3>Keeps production errors safe</h3> |
| <p> |
| Users receive clean messages and a request ID. Detailed stack traces stay in container |
| logs so developers can diagnose failures without leaking internals. |
| </p> |
| </article> |
| </div> |
| </div> |
| </section> |
| |
| <section id="quickstart"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Quick Start</h2> |
| <p> |
| The smallest valid request only needs a skill. Defaults fill in learner level, goals, |
| time commitment, preferred resource types, and language. |
| </p> |
| </div> |
| <div class="grid two"> |
| <div class="code-card"> |
| <div class="code-top"> |
| <span>curl</span> |
| <button class="copy" data-copy="curl-example">Copy</button> |
| </div> |
| <pre id="curl-example">curl -X POST https://jaykay73-lockedin.hf.space/api/v1/roadmaps/generate \ |
| -H "Content-Type: application/json" \ |
| -d '{"skill":"Learn Python"}'</pre> |
| </div> |
| <div class="code-card"> |
| <div class="code-top"> |
| <span>JavaScript fetch</span> |
| <button class="copy" data-copy="fetch-example">Copy</button> |
| </div> |
| <pre id="fetch-example">const response = await fetch( |
| "https://jaykay73-lockedin.hf.space/api/v1/roadmaps/generate", |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ skill: "Learn Python" }) |
| } |
| ); |
| |
| const body = await response.json(); |
| |
| if (!response.ok || body.success === false) { |
| throw new Error(body.error?.message ?? "Roadmap generation failed"); |
| } |
| |
| console.log(body.data);</pre> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <section id="endpoint"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Endpoint Reference</h2> |
| <p> |
| The generation endpoint is asynchronous from the client perspective: send one POST request |
| and wait for a validated roadmap response. |
| </p> |
| </div> |
| |
| <article class="panel endpoint"> |
| <div class="endpoint-top"> |
| <div> |
| <span class="method">POST</span> |
| <span class="path">/api/v1/roadmaps/generate</span> |
| </div> |
| <span class="tag">Returns JSON</span> |
| </div> |
| <div class="panel-pad"> |
| <p> |
| Generates a complete learning roadmap for a requested skill. The API searches for free |
| resources, asks the model to produce a roadmap using only those resources, validates the |
| final schema, assigns IDs, and caches valid results. |
| </p> |
| <div style="height: 16px"></div> |
| <div class="tag-list"> |
| <span class="tag">Roadmaps</span> |
| <span class="tag">AI generation</span> |
| <span class="tag">Validated schema</span> |
| <span class="tag">Cached results</span> |
| </div> |
| </div> |
| </article> |
| </div> |
| </section> |
| |
| <section id="schema"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Request Body</h2> |
| <p> |
| Send only <code>skill</code> for the default experience, or include optional fields to |
| tailor the roadmap to a learner's level, goals, time, format preferences, and language. |
| </p> |
| </div> |
| |
| <div class="table" role="table" aria-label="Request fields"> |
| <div class="row header" role="row"> |
| <div>Field</div> |
| <div>Description</div> |
| <div>Required</div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>skill</code></div> |
| <div>The skill or topic to learn. Example: <code>Learn Python</code>.</div> |
| <div><span class="status ok">Required</span></div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>user_level</code></div> |
| <div>Current learner level. Default: <code>complete beginner</code>.</div> |
| <div><span class="status warn">Optional</span></div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>goal</code></div> |
| <div>The learner's outcome. Default: learn step by step and build practical confidence.</div> |
| <div><span class="status warn">Optional</span></div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>time_commitment</code></div> |
| <div>Expected study time. Default: <code>3 to 5 hours per week</code>.</div> |
| <div><span class="status warn">Optional</span></div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>preferred_resource_types</code></div> |
| <div> |
| Defaults to all supported formats: <code>youtube_video</code>, <code>article</code>, |
| <code>free_course</code>, <code>documentation</code>, <code>free_book</code>, |
| <code>interactive_practice</code>. |
| </div> |
| <div><span class="status warn">Optional</span></div> |
| </div> |
| <div class="row" role="row"> |
| <div><code>language</code></div> |
| <div>Preferred output language. Default: <code>English</code>.</div> |
| <div><span class="status warn">Optional</span></div> |
| </div> |
| </div> |
| |
| <div style="height: 16px"></div> |
| <div class="code-card"> |
| <div class="code-top"> |
| <span>Full request example</span> |
| <button class="copy" data-copy="request-example">Copy</button> |
| </div> |
| <pre id="request-example">{ |
| "skill": "Learn Python", |
| "user_level": "complete beginner", |
| "goal": "build small automation projects", |
| "time_commitment": "5 hours per week", |
| "preferred_resource_types": [ |
| "youtube_video", |
| "article", |
| "free_course", |
| "documentation", |
| "free_book", |
| "interactive_practice" |
| ], |
| "language": "English" |
| }</pre> |
| </div> |
| </div> |
| </section> |
| |
| <section id="response"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Successful Response</h2> |
| <p> |
| A successful response returns <code>success: true</code> and a roadmap object designed for |
| direct rendering in your frontend. |
| </p> |
| </div> |
| <div class="grid two"> |
| <article class="panel panel-pad"> |
| <h3>Roadmap structure</h3> |
| <ul> |
| <li>Exactly 3 phases.</li> |
| <li>Each phase has exactly 3 learning nodes.</li> |
| <li>Each node has 2 to 4 free resources.</li> |
| <li>Each phase includes one practical project.</li> |
| <li>Top-level <code>projects</code> mirrors phase projects for easy UI rendering.</li> |
| </ul> |
| </article> |
| <article class="panel panel-pad"> |
| <h3>Metadata</h3> |
| <ul> |
| <li><code>model_used</code> shows the generation model or fallback.</li> |
| <li><code>resource_sources</code> shows providers such as Tavily and YouTube.</li> |
| <li><code>generated_at</code> is an ISO timestamp.</li> |
| <li><code>cached</code> tells the frontend whether the response came from cache.</li> |
| </ul> |
| </article> |
| </div> |
| <div style="height: 16px"></div> |
| <div class="code-card"> |
| <div class="code-top"> |
| <span>Response shape</span> |
| <button class="copy" data-copy="response-example">Copy</button> |
| </div> |
| <pre id="response-example">{ |
| "success": true, |
| "data": { |
| "roadmap_id": "roadmap_python_599569", |
| "skill": "Learn Python", |
| "normalized_skill": "python", |
| "overview": "A beginner-friendly overview...", |
| "estimated_total_duration": "6-8 weeks", |
| "phases": [ |
| { |
| "id": "phase_1", |
| "title": "Getting Started", |
| "level": "beginner", |
| "goal": "Learn the foundations.", |
| "estimated_duration": "2 weeks", |
| "nodes": [], |
| "project": {} |
| } |
| ], |
| "projects": [], |
| "metadata": { |
| "model_used": "deepseek-v4-flash", |
| "resource_sources": ["tavily", "youtube"], |
| "generated_at": "2026-04-30T20:18:18.238214Z", |
| "cached": false, |
| "fallback": false |
| } |
| } |
| }</pre> |
| </div> |
| </div> |
| </section> |
| |
| <section id="errors"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Error Handling</h2> |
| <p> |
| The API uses real HTTP status codes. Production responses stay safe for users, while the |
| backend logs detailed exceptions with the same <code>request_id</code>. |
| </p> |
| </div> |
| <div class="table" role="table" aria-label="Status codes"> |
| <div class="row header" role="row"> |
| <div>Status</div> |
| <div>Meaning</div> |
| <div>Retry?</div> |
| </div> |
| <div class="row" role="row"> |
| <div><span class="status ok">200</span></div> |
| <div>Roadmap generated successfully.</div> |
| <div>No</div> |
| </div> |
| <div class="row" role="row"> |
| <div><span class="status warn">422</span></div> |
| <div>Invalid request body, such as a blank skill or unsupported resource type.</div> |
| <div>Fix request</div> |
| </div> |
| <div class="row" role="row"> |
| <div><span class="status bad">500</span></div> |
| <div>Unexpected backend error.</div> |
| <div>Maybe</div> |
| </div> |
| <div class="row" role="row"> |
| <div><span class="status bad">502</span></div> |
| <div>The model returned malformed or schema-invalid output.</div> |
| <div>Yes</div> |
| </div> |
| <div class="row" role="row"> |
| <div><span class="status bad">503</span></div> |
| <div>Generation, model provider, search provider, or validation workflow failed.</div> |
| <div>Yes</div> |
| </div> |
| </div> |
| <div style="height: 16px"></div> |
| <div class="code-card"> |
| <div class="code-top"> |
| <span>Error response</span> |
| <button class="copy" data-copy="error-example">Copy</button> |
| </div> |
| <pre id="error-example">{ |
| "success": false, |
| "error": { |
| "code": "ROADMAP_GENERATION_FAILED", |
| "message": "We could not generate this roadmap right now. Please try again.", |
| "retryable": true, |
| "request_id": "2e4d4e8fd3f049d28e1a7fd04ef63c8a" |
| } |
| }</pre> |
| </div> |
| </div> |
| </section> |
| |
| <section id="frontend"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Frontend Integration Pattern</h2> |
| <p> |
| Keep API keys on the backend. The frontend only calls LockedIn, handles loading states, |
| checks <code>response.ok</code>, and logs request IDs for failed calls. |
| </p> |
| </div> |
| <div class="timeline"> |
| <div class="step"> |
| <div class="step-num">1</div> |
| <article class="panel panel-pad"> |
| <h3>Collect the skill</h3> |
| <p>Ask the learner what they want to learn. Optional controls can collect level, goal, time, language, and preferred resource types.</p> |
| </article> |
| </div> |
| <div class="step"> |
| <div class="step-num">2</div> |
| <article class="panel panel-pad"> |
| <h3>POST to the API</h3> |
| <p>Send JSON to <code>/api/v1/roadmaps/generate</code>. Show a loading state because provider calls can take several seconds.</p> |
| </article> |
| </div> |
| <div class="step"> |
| <div class="step-num">3</div> |
| <article class="panel panel-pad"> |
| <h3>Render or recover</h3> |
| <p>If successful, render phases, nodes, resources, and projects. If failed, show a friendly message and log the <code>request_id</code>.</p> |
| </article> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <section id="ops"> |
| <div class="shell"> |
| <div class="section-head"> |
| <h2>Operational Notes</h2> |
| <p> |
| These details help keep the Hugging Face Spaces deployment stable and debuggable. |
| </p> |
| </div> |
| <div class="grid three"> |
| <article class="panel panel-pad"> |
| <h3>Secrets</h3> |
| <p> |
| Set <code>DEEPSEEK_API_KEY</code>, <code>TAVILY_API_KEY</code>, and |
| <code>YOUTUBE_API_KEY</code> as Space secrets. Never expose them in frontend code. |
| </p> |
| </article> |
| <article class="panel panel-pad"> |
| <h3>CORS</h3> |
| <p> |
| Set <code>CORS_ALLOWED_ORIGINS</code> to your frontend domain. During testing, a broad |
| value may work, but production should use explicit origins. |
| </p> |
| </article> |
| <article class="panel panel-pad"> |
| <h3>Health</h3> |
| <p> |
| Use <code>/health</code> to verify the service is running. It does not prove external |
| providers are healthy; it only checks the app process. |
| </p> |
| </article> |
| </div> |
| </div> |
| </section> |
| </main> |
| |
| <footer class="footer"> |
| <div class="shell"> |
| LockedIn AI Service API documentation. Built for frontend developers, demos, and production debugging. |
| </div> |
| </footer> |
| |
| <script> |
| document.querySelectorAll("[data-copy]").forEach((button) => { |
| button.addEventListener("click", async () => { |
| const target = document.getElementById(button.dataset.copy); |
| if (!target) return; |
| await navigator.clipboard.writeText(target.textContent); |
| const original = button.textContent; |
| button.textContent = "Copied"; |
| window.setTimeout(() => { |
| button.textContent = original; |
| }, 1100); |
| }); |
| }); |
| </script> |
| </body> |
| </html> |
| """ |
|
|