From b3f6daf0f8cd93721200059506175f4085848ed6 Mon Sep 17 00:00:00 2001 From: im-rk Date: Sat, 3 Oct 2026 10:36:22 +0530 Subject: [PATCH 1/4] feat(judges): a page mapping the brief to Cloudinary capabilities, live URLs and a 90-second test --- src/app/judges/page.tsx | 109 ++++++++++++++++++++++++++++++++ src/app/layout.tsx | 4 ++ src/app/sitemap.ts | 1 + src/lib/capabilities.ts | 23 +++++++ tests/e2e/judges.spec.ts | 21 ++++++ tests/unit/capabilities.test.ts | 16 +++++ 6 files changed, 174 insertions(+) create mode 100644 src/app/judges/page.tsx create mode 100644 src/lib/capabilities.ts create mode 100644 tests/e2e/judges.spec.ts create mode 100644 tests/unit/capabilities.test.ts diff --git a/src/app/judges/page.tsx b/src/app/judges/page.tsx new file mode 100644 index 0000000..e5e0132 --- /dev/null +++ b/src/app/judges/page.tsx @@ -0,0 +1,109 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { UnderTheHood } from "@/components/UnderTheHood"; +import { CAPABILITIES } from "@/lib/capabilities"; +import { getConcept, listConcepts } from "@/lib/concepts"; +import { conceptHoodItems, sessionHoodItems, type HoodItem } from "@/lib/hood-items"; +import { getLecture, getSegments, listLectures } from "@/lib/lectures"; +import { getStudyPack } from "@/lib/study-packs"; + +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { + title: "For judges — Pravaha", + description: "How Pravaha uses Cloudinary, mapped to the hackathon's requirements, with live URLs and a 90-second test.", +}; + +const REQUIREMENTS: [string, string][] = [ + ["Cloudinary is an active part of the product", "Media in is raw video; transcripts, chapters, streams, clips, reels, cards and tags all come from Cloudinary. Without it there is no product."], + ["Upload, manage, transform, optimize, search, deliver", "Signed upload · tags and context · Search API · splice, crop, caption, preview transformations · f_auto/q_auto · HLS delivery."], + ["A live working demo", "This site. Six real NPTEL sessions, no login for learners."], + ["Public repo with setup instructions", "github.com/Monolithic-Dev/Pravaha, SETUP.md."], + ["README: track, problem, Cloudinary usage, how to test", "Track 3 (Your Media-Savvy Startup); see the README."], +]; + +const STEPS: [string, string, string][] = [ + ["Ask", "Type: “What is the bias-variance trade-off?” The answer cites clips; open one. Try it in Hindi: “ओवरफिटिंग क्या है?”", "/"], + ["Watch the answer", "Tap “Watch the answer”: moments from different lecturers as one Cloudinary-edited video.", "/"], + ["Compare teachers", "Open the Overfitting concept: three lecturers, one video.", "/concepts/overfitting"], + ["Build a course", "Ask for “Choosing a learning rate”: a 4-step course, one video.", "/learn?topic=Choosing%20a%20learning%20rate"], + ["Share a Moment", "On any clip tap “Share as Moment”: vertical, speaker-tracked, captioned.", "/search?q=overfitting"], + ["See the proof", "Open “Cloudinary under the hood” at the bottom of a session, answer or concept.", "/#library"], +]; + +export default async function JudgesPage() { + const [lectures, concepts] = await Promise.all([listLectures({ includeAll: false }), listConcepts()]); + const lecture = lectures[0] ? await getLecture(lectures[0].id) : null; + const shared = concepts.find((c) => c.entries.length > 1); + const [segments, pack, concept] = await Promise.all([ + lecture ? getSegments(lecture.id) : [], + lecture ? getStudyPack(lecture.id) : null, + shared ? getConcept(shared.key) : null, + ]); + const items: HoodItem[] = [ + ...(lecture && segments.length ? sessionHoodItems(lecture, segments, pack?.highlights) : []), + ...(concept ? conceptHoodItems(concept.key, concept.moments) : []), + ]; + + return ( +
+

Track 3 · Your Media-Savvy Startup

+

Pravaha, for judges

+

+ Pravaha turns lecture recordings into knowledge you can ask. Every answer is a clip of the teacher saying it, and the clip, + the reel, the share card and the Hindi subtitles are all Cloudinary. This page maps that to the submission requirements. +

+ +
+

The requirements, and where they are met

+
+ {REQUIREMENTS.map(([need, met]) => ( +
+
{need}
+
{met}
+
+ ))} +
+
+ +
+

Test it in 90 seconds

+
    + {STEPS.map(([name, text, href], i) => ( +
  1. + {i + 1} + + {name} + · {text} + +
  2. + ))} +
+
+ +
+

{CAPABILITIES.length} Cloudinary capabilities in use

+
    + {CAPABILITIES.map((c) => ( +
  • +

    {c.name}

    +

    {c.does}

    +

    + See it: {c.see} · {c.code} +

    +
  • + ))} +
+
+ + {items.length > 0 && ( +
+

Live Cloudinary URLs

+

Generated from the real library right now. Open any of them.

+ +
+ )} +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0ca2c0c..18d8a8c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -93,6 +93,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) Privacy {" "} ·{" "} + + For judges + {" "} + ·{" "} Terms {" "} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 76c4726..e858ce7 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -17,6 +17,7 @@ export default async function sitemap(): Promise { { url: base, changeFrequency: "daily", priority: 1 }, { url: `${base}/learn`, changeFrequency: "monthly", priority: 0.6 }, { url: `${base}/concepts`, changeFrequency: "weekly", priority: 0.7 }, + { url: `${base}/judges`, changeFrequency: "monthly", priority: 0.5 }, { url: `${base}/try`, changeFrequency: "monthly", priority: 0.6 }, { url: `${base}/privacy`, changeFrequency: "yearly", priority: 0.2 }, { url: `${base}/terms`, changeFrequency: "yearly", priority: 0.2 }, diff --git a/src/lib/capabilities.ts b/src/lib/capabilities.ts new file mode 100644 index 0000000..faf5056 --- /dev/null +++ b/src/lib/capabilities.ts @@ -0,0 +1,23 @@ +// Every Cloudinary capability Pravaha uses, where a visitor can see it working, and where it lives in the code. +// The /judges page renders this list, so it must stay true: tests/unit/capabilities.test.ts checks every +// `code` path exists in the repository. + +export type Capability = { name: string; does: string; see: string; code: string }; + +export const CAPABILITIES: Capability[] = [ + { name: "Signed upload preset + Upload Widget", does: "Browser-to-Cloudinary video upload with real progress; the server only signs.", see: "Studio, /try", code: "src/app/api/upload-signature/route.ts" }, + { name: "auto_transcription", does: "Word-timed transcript at upload: the corpus for Find, Ask and subtitles.", see: "Watch → Transcript", code: "src/lib/ingest.ts" }, + { name: "auto_chaptering", does: "AI chapters on the seek bar and as search context.", see: "Watch → Chapters", code: "src/lib/chapters.ts" }, + { name: "Translated transcripts (hi-IN)", does: "Hindi subtitles generated by Cloudinary, plus asking in Hindi.", see: "Watch → subtitles menu", code: "src/lib/subtitles.ts" }, + { name: "Video Player + HLS streaming profile", does: "Adaptive 720p/360p/180p ladder that survives slow mobile data.", see: "Any session", code: "src/components/Player.tsx" }, + { name: "e_preview + fl_getinfo + fl_sprite", does: "AI highlights graph, seek-bar thumbnails and 6-second hover previews.", see: "Library cards, Watch", code: "src/lib/media.ts" }, + { name: "so_/eo_ trims + c_fill,ar_9:16,g_auto", does: "Moments: any sentence becomes a vertical clip that tracks the speaker.", see: "Share as Moment", code: "src/lib/media.ts" }, + { name: "Timed l_text layers", does: "Word-accurate burned-in captions and speaker labels, exact on trimmed clips.", see: "Moments, reels", code: "src/lib/media.ts" }, + { name: "l_video + fl_splice", does: "Answer Reels, Study Pack reels, Learning Paths and Compare Reels: moments from many sessions as one video.", see: "Ask, /learn, /concepts", code: "src/lib/media.ts" }, + { name: "g_auto thumbnails + gradient/text share cards", does: "Open Graph images for every moment, answer, course and concept: one URL, no image service.", see: "/m, /a, /p, /concepts", code: "src/lib/media.ts" }, + { name: "f_auto, q_auto", does: "Best format and quality for each device on every derived asset.", see: "Everywhere", code: "src/lib/media.ts" }, + { name: "Notification webhooks (signed)", does: "Upload to ready with no polling; signature and replay window verified.", see: "Studio status", code: "src/lib/webhook-signature.ts" }, + { name: "Incoming transformation eo_60", does: "Public trial uploads keep only 60 seconds, so abuse cannot cost more.", see: "/try", code: "src/lib/trial-policy.ts" }, + { name: "Tags + contextual metadata", does: "Each asset carries its concepts, speaker and language inside Cloudinary.", see: "Studio → Cloudinary asset index", code: "src/lib/cloudinary-index.ts" }, + { name: "Search API", does: "Reads the tagged assets back from Cloudinary itself.", see: "Studio → Cloudinary asset index", code: "src/lib/cloudinary-index.ts" }, +]; diff --git a/tests/e2e/judges.spec.ts b/tests/e2e/judges.spec.ts new file mode 100644 index 0000000..b7afc46 --- /dev/null +++ b/tests/e2e/judges.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from "@playwright/test"; + +// Smoke tests for the pages a judge is most likely to open (run against the deployed app, like smoke.spec.ts). + +test("the judges page maps the brief to Cloudinary capabilities", async ({ page }) => { + await page.goto("/judges"); + await expect(page.getByRole("heading", { name: /Pravaha, for judges/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /Cloudinary capabilities in use/i })).toBeVisible(); +}); + +test("the concept map opens a concept with a compare reel", async ({ page }) => { + await page.goto("/concepts"); + await page.getByRole("link", { name: /Overfitting/ }).first().click(); + await expect(page.getByRole("heading", { name: "Overfitting" })).toBeVisible(); + await expect(page.getByRole("button", { name: /back to back/i })).toBeVisible(); +}); + +test("Learn explains itself and takes a topic", async ({ page }) => { + await page.goto("/learn?topic=learning%20rate"); + await expect(page.getByRole("heading", { name: /Learn a topic/i })).toBeVisible(); +}); diff --git a/tests/unit/capabilities.test.ts b/tests/unit/capabilities.test.ts new file mode 100644 index 0000000..cd719bc --- /dev/null +++ b/tests/unit/capabilities.test.ts @@ -0,0 +1,16 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { CAPABILITIES } from "@/lib/capabilities"; + +describe("CAPABILITIES (the /judges page)", () => { + it("points every capability at code that exists", () => { + for (const c of CAPABILITIES) expect(existsSync(join(process.cwd(), c.code)), `${c.name}: ${c.code}`).toBe(true); + }); + + it("has unique names and complete entries", () => { + expect(new Set(CAPABILITIES.map((c) => c.name)).size).toBe(CAPABILITIES.length); + for (const c of CAPABILITIES) expect([c.name, c.does, c.see, c.code].every((s) => s.trim().length > 3)).toBe(true); + }); +}); From 55ab21e415f1f236b0779bccc579c7f1edec9cfb Mon Sep 17 00:00:00 2001 From: im-rk Date: Sat, 3 Oct 2026 10:36:23 +0530 Subject: [PATCH 2/4] feat(saved): a revision reel of saved moments --- src/components/SavedView.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/SavedView.tsx b/src/components/SavedView.tsx index 16bb3bc..c6e1116 100644 --- a/src/components/SavedView.tsx +++ b/src/components/SavedView.tsx @@ -3,8 +3,9 @@ import Image from "next/image"; import Link from "next/link"; +import { ReelPlayer } from "@/components/ReelPlayer"; import { formatTime } from "@/lib/format"; -import { thumbUrl } from "@/lib/media"; +import { reelUrl, thumbUrl } from "@/lib/media"; import { clearSaved, toggleSavedMoment, useRecentQuestions, useSavedMoments, useWatchProgress } from "@/lib/saved"; // The learner's Saved page: everything here lives on this device (src/lib/saved.ts). @@ -12,6 +13,8 @@ export function SavedView() { const moments = useSavedMoments(); const questions = useRecentQuestions(); const progress = useWatchProgress(); + // Revise before an exam: the latest saved moments (any sessions) as one labelled video. + const revision = moments.length > 1 ? reelUrl(moments.slice(0, 5).map((m, i) => ({ publicId: m.publicId, startS: m.startS, endS: m.endS, label: `${i + 1} · ${m.title.slice(0, 40)}` }))) : null; return (
@@ -27,7 +30,16 @@ export function SavedView() { {moments.length === 0 ? ( Tap Save on any answer clip or search result to keep the moment here. ) : ( -
    + <> + {revision && ( + + )} +
      {moments.map((m, i) => (
    • @@ -45,6 +57,7 @@ export function SavedView() {
    • ))}
    + )} From 56454579a8e369b3b9a4bf5c95f293206a0287d9 Mon Sep 17 00:00:00 2001 From: im-rk Date: Sat, 3 Oct 2026 10:36:24 +0530 Subject: [PATCH 3/4] docs(phase-20): judges page, demo scenes and submission checklist --- CHANGELOG.md | 1 + README.md | 3 +++ docs/DEMO.md | 10 ++++++++++ docs/SUBMISSION_CHECKLIST.md | 5 ++++- docs/phases/PHASE-20-judges-page-revision-reel.md | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 docs/phases/PHASE-20-judges-page-revision-reel.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ed3809c..d97e4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/); versioning: [SemVer](ht ## [Unreleased] ### Added +- `/judges`: the submission requirements mapped to 14 Cloudinary capabilities, live URLs and a 90-second test; revision reel on `/saved` (Phase 20) - Concept Map `/concepts` and Compare view `/concepts/[key]`: the same concept from every teacher as one Compare Reel (Phase 19) - Cloudinary asset index: tags and contextual metadata on each session's asset, synced from the Studio and read back with the Search API (Phase 19) diff --git a/README.md b/README.md index 50ccce9..f87ebb5 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Colleges, clubs and coaching institutes record hundreds of hours of lectures and | **Answer Reels** | The moments an answer cites, from different speakers and sessions, stitched into one labelled video. | | **Concept Map** | `/concepts` lines up every concept your lectures teach and how many different teachers explain it. Open one and hear the same idea from each lecturer, back to back, as one Cloudinary-spliced video, until one explanation clicks. | | **Learning Paths** | `/learn`: type a topic and get a 3–5 step course, ordered basics first, drawn from moments across the library and edited into one video. | +| **Revision reel** | On `/saved`, the latest saved moments from any sessions play as one labelled video: a revision reel for the night before an exam. | | **Study Packs** | Every session gets a summary, key concepts, a quiz whose explanations **play the moment** the teacher explains it, and a "Session in 60 seconds" highlight reel. Pravaha generates them automatically from the transcript. | | **Moments** | One tap turns any cited clip into a vertical, AI-cropped, subtitled short for WhatsApp or Instagram, with its own share page and preview card. The short is just a Cloudinary URL; nothing is rendered. | | **Insights** | Organizers see what learners ask, the **knowledge gaps** the library can't answer yet (what to record next), and which Moments get shared. Every report downloads as CSV. Visitors can explore a read-only demo of the Studio. | @@ -160,6 +161,8 @@ Diagrams and reasoning: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) · [`docs ## How to test it +> **Judges:** [`/judges`](https://pravaha-cyan.vercel.app/judges) maps the submission requirements to the Cloudinary features, with live URLs and a 90-second test. + Open the live app at **[pravaha-cyan.vercel.app](https://pravaha-cyan.vercel.app)**. Learners need no login. 1. On the home page, type a question or tap a suggestion. Try "Why should the learning rate decrease during training, and how do adaptive optimizers handle it?": live progress appears, then one answer citing two professors from two institutes, plus an Answer Reel. Or ask in Hindi: "ओवरफिटिंग क्या है?" diff --git a/docs/DEMO.md b/docs/DEMO.md index 70249b3..9847113 100644 --- a/docs/DEMO.md +++ b/docs/DEMO.md @@ -26,6 +26,16 @@ Never show setup, code, dashboards or waiting. Every shot is the problem, the pr | 2:24–2:35 | **Cloudinary under the hood** panel on the answer: each URL step explained | "Every clip, reel and short is a Cloudinary transformation, and Pravaha shows exactly which." | | 2:35–2:45 | Architecture strip: Cloudinary (transcription · chaptering · streaming · g_auto · e_preview · transformations) + Pravaha (retrieval · grounded Ask · Moments · Insights) · team | "Cloudinary turns video into data. Pravaha turns data into answers you can watch. Code Blooded, Track 3." | +## Phase 18–20 scenes (Oct 3) — swap in for the 2:06–2:35 shots if time is tight + +| Shot | Voice-over | +|---|---| +| Ask "What is the bias-variance trade-off?" → the answer says honestly the lecturer never uses that phrase, then explains the related ideas with clips | "It doesn’t bluff. If the lecturer never said it, Pravaha says so and teaches the closest ideas." | +| Ask the same in Hindi: "ओवरफिटिंग क्या है?" → Hindi answer citing English lectures | "Ask in Hindi, answer from English lectures." | +| `/concepts/overfitting` → play the Compare Reel: three professors, labelled | "Same idea, three teachers, one video, until one explanation clicks." | +| `/learn` → "Choosing a learning rate" → 4-step course video | "Any topic becomes a short course, ordered basics first, edited by Cloudinary." | +| `/judges` → capability list → Studio → Cloudinary asset index (Search API) | "Every capability is listed with live URLs. Even our tags are read back from Cloudinary’s Search API." | + ## Pre-Recording Checklist - **Cloudinary credits first** (free plan: 25 a month). Check usage before recording. New uploads, new answer clips, reels and Moments cost credits; already-generated URLs don't. If usage is near the limit, upgrade first. If you can't, film the upload shot from an existing recording, and ask only questions that have been asked before (their clips are cached). diff --git a/docs/SUBMISSION_CHECKLIST.md b/docs/SUBMISSION_CHECKLIST.md index 677c249..8576e0b 100644 --- a/docs/SUBMISSION_CHECKLIST.md +++ b/docs/SUBMISSION_CHECKLIST.md @@ -15,7 +15,7 @@ Deadline: submit by **Oct 3, 2026**; code freeze **Oct 4, 00:15 IST**. Submit vi - [x] Learner flows (Watch, Find, Ask, Moments) work without login (Oct 2: Playwright smoke tests 12/12 against production, desktop and mobile) - [x] Demo library: 6 public sessions (NPTEL excerpts, CC BY-NC-SA, credited in the README), uploaded through the production pipeline on Oct 2 - [x] Organizer and enterprise features merged and checked on Vercel previews (Oct 2–3): demo Studio, Insights charts and CSV export, LMS embed, Privacy and Terms, health endpoint -- [ ] **Cloudinary credits:** 24.3 of 25 used on Oct 3 (free plan). Upgrade, or confirm the reset date is before judging, so new answer clips and Moments keep generating +- [ ] **Cloudinary credits:** the Admin API reported 14.1 of 25 used (56%) at its last update (Oct 2). Every new reel, Moment and Compare Reel spends some, so avoid regenerating them in bulk before judging, and re-check `api.usage()` on submission day - [ ] Trials decision after that: keep `/try` paused (`TRIALS_PER_DAY=0`) or re-enable (`TRIALS_PER_DAY=5`, then redeploy) ## Repository @@ -25,6 +25,9 @@ Deadline: submit by **Oct 3, 2026**; code freeze **Oct 4, 00:15 IST**. Submit vi - [ ] Setup instructions a stranger can follow cold (`SETUP.md`) - [x] No API keys or credentials anywhere in history — `git log -p | grep -iE "api_secret|sk-ant|postgres://|passcode"` returns only env-variable references (checked Oct 2) +- [ ] Studio → **Cloudinary asset index** → Sync tags run once on production, so the Search API shows tagged assets +- [x] `/judges` page maps the brief to Cloudinary capabilities and the 90-second test (Phase 20) + ## Demo - [ ] 2–4 minute video recorded per `DEMO.md` (target 2:45), publicly viewable link (check credits first: see its pre-recording checklist) - [ ] Demo video link added to the README diff --git a/docs/phases/PHASE-20-judges-page-revision-reel.md b/docs/phases/PHASE-20-judges-page-revision-reel.md new file mode 100644 index 0000000..145724a --- /dev/null +++ b/docs/phases/PHASE-20-judges-page-revision-reel.md @@ -0,0 +1,15 @@ +# Phase 20 — Judges page and revision reel + +**Status: DONE (Oct 3)** · branch `feature/judges-page-revision-reel` + +## Why +A hackathon is judged in minutes. The product was strong but a judge had to discover how Cloudinary is used by reading code and docs. This phase puts the evidence in the product. + +## What shipped +1. **`/judges`** (`src/app/judges/page.tsx`, `src/lib/capabilities.ts`): the submission requirements mapped to where each is met, a 90-second test with deep links, 14 Cloudinary capabilities (what each does, where to see it, which file), and the live Cloudinary URLs generated from the real library. `tests/unit/capabilities.test.ts` fails if any listed file doesn't exist, so the page can't drift from the code. +2. **Revision reel** on `/saved`: the latest saved moments, from any sessions, as one labelled `fl_splice` video. Saved moments stay on the device; only the Cloudinary URL is built from them. +3. Playwright smoke tests for `/judges`, `/concepts` and `/learn`; the demo script and submission checklist updated. + +## Findings from the Oct 3 architecture review +- Cloudinary credits were 14.1 of 25 (56%) at the Admin API's last update (Oct 2): enough for judging if reels aren't regenerated in bulk. The biggest consumers are `sd_video_second` and AI hover previews. +- The Gemini free quota remains the main production risk; Groq is the working fallback. From 90f0c8798415cf36162feaee8e8434401eb8159c Mon Sep 17 00:00:00 2001 From: im-rk Date: Sat, 3 Oct 2026 14:33:08 +0530 Subject: [PATCH 4/4] fix(layout): phone header fits at 360px, and Studio and /try stop overflowing --- src/app/globals.css | 8 ++++++++ src/app/layout.tsx | 35 ++++++++++++++++++++--------------- src/components/Studio.tsx | 2 +- src/components/TryFlow.tsx | 4 ++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 444cfe4..75d9ff8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -180,3 +180,11 @@ body { .playhead { width: 100%; } .lift, .lift:hover { transition: none; transform: none; } } + +/* A row that scrolls sideways without showing a scrollbar (the phone header's page links). */ +.no-scrollbar { + scrollbar-width: none; +} +.no-scrollbar::-webkit-scrollbar { + display: none; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index de83d1a..2c78060 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -46,35 +46,40 @@ export default function RootLayout({ children }: { children: React.ReactNode })
    -
    diff --git a/src/components/Studio.tsx b/src/components/Studio.tsx index fe05325..5115b7f 100644 --- a/src/components/Studio.tsx +++ b/src/components/Studio.tsx @@ -74,7 +74,7 @@ export function Studio({ demo = false }: { demo?: boolean }) { } return ( -
    +
    {demo ? ( diff --git a/src/components/TryFlow.tsx b/src/components/TryFlow.tsx index d738ac5..b1586d8 100644 --- a/src/components/TryFlow.tsx +++ b/src/components/TryFlow.tsx @@ -58,7 +58,7 @@ export function TryFlow({ seconds, ttlHours }: { seconds: number; ttlHours: numb function Progress({ step }: { step: 1 | 2 | 3 }) { const labels = ["Details", "Upload", "Processing"]; return ( -
      +
        {labels.map((label, i) => { const n = i + 1; const state = n < step ? "done" : n === step ? "current" : "todo"; @@ -72,7 +72,7 @@ function Progress({ step }: { step: 1 | 2 | 3 }) { {state === "done" ? "✓" : n} {label} - {n < 3 && } + {n < 3 && } ); })}