From e66141e9a033ec3d66d7cf9eb791c98f5181355b Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Sat, 10 Oct 2026 11:35:45 +0000 Subject: [PATCH] Wizard polish: step headlines, gated Next, real summary, portal action bar Mobile only (<=639px); desktop markup untouched. Bug fix: the Back/Next bar was fixed-positioned inside the screen entrance animation's transformed wrapper, so it painted under the content and jumped to the viewport bottom about a second in. It is now portalled to and only fades in, pinned to the bottom from the first frame. The other fixed/sticky elements (tab bar, drawer, sheets, install sheet) were checked and live outside the animated wrappers. - The step name is now the large headline with a one-line description; the repeated page title is dropped on phones. Thin progress bar + 1/3. - Step 1: formats/limits hint, one big dashed Add photos or files area (plus icon), Camera and Paste text instead as secondaries, and a 'nothing added yet' placeholder that becomes the thumbnail/chip list. - Next is disabled until a photo, file or pasted text exists; step 3 can never be reached empty. - Step 1 shows Cancel (back to the Library) instead of a dead Back. - Step 3 is a real summary: thumbnail/chip strip plus Files, Difficulty, Activities and Folder rows, each with an Edit link to its step; Freeze is the single primary action. - Step 2 got a visible Difficulty label and a helper line under every group, defaults still preselected. - Bottom chrome: flat full-width Back/Next bar above the safe area, and the tab bar hides inside both wizards. The lower-right orb is hidden on phones so it can never glow behind the bar. - Make my homework matches: headline, hint, big add area, placeholder, Cancel, gated Next, flat bar. Verified: new wizard suite 80/80 (bar position sampled at 5 frames during the entrance from tab bar/drawer/empty state; 360 + 390, dark + light, EN + CS, reduced motion on/off; gating, summary Edit links, hidden tab bar and orb), declutter 116/116, regression 74/74, mobile 52/52, animation 8/8, i18n 25/25. Lint + build clean. SW freezmo-v22. --- README.md | 32 +++- app/create/page.tsx | 2 - app/globals.css | 8 +- app/homework/page.tsx | 2 - components/UploadScreen.tsx | 243 ++++++++++++++++++-------- components/WizardActionBar.tsx | 39 +++++ components/homework/HomeworkInput.tsx | 128 ++++++++------ lib/i18n-dict.ts | 22 +++ public/sw.js | 2 +- 9 files changed, 342 insertions(+), 136 deletions(-) create mode 100644 components/WizardActionBar.tsx diff --git a/README.md b/README.md index 8092d27..fdfdf7e 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,16 @@ no database, no data leaving your machine except the requests you send to Ollama **Capture & generate** - Drag & drop, file picker, **mobile camera capture** and **clipboard paste** (Ctrl/⌘+V) -- **Phones get a 3-step flow** — notes → options → summary — with a progress bar and a - floating Back / Next bar above the tab bar. Step 1 is just *Add photos or files*, - *Camera* and a *Paste text instead* field (no drop zone on touch screens); step 2 keeps - folder, difficulty and activity count at their defaults and tucks the topic hint and - pasted notes behind one collapsed row; step 3 is a single summary line with **Freeze my - deck**. Going Back keeps every input. The desktop page is unchanged. +- **Phones get a 3-step flow** — notes → options → summary — with a thin progress bar and + “1 / 3”. Step 1 leads with the step name as the headline, a formats/limits hint, one big + *Add photos or files* area plus *Camera* and *Paste text instead*, and a “nothing added + yet” placeholder that becomes the thumbnail/chip list. Next stays disabled until something + is added. Step 2 keeps folder, difficulty and count at their defaults with a helper under + each group and tucks the topic hint and pasted notes behind one collapsed row. Step 3 is a + real summary — thumbnails/chips, files, difficulty, activities and folder, each with an + *Edit* link back to its step. A flat Back/Cancel + Next/Freeze bar sits at the bottom above + the safe area (the tab bar hides inside the wizard), and it is portalled to `` so the + screen entrance animation can never displace it. The desktop page is unchanged. - **Photos and documents**: JPG/PNG/WEBP/HEIC images plus **PDF, DOCX, PPTX, ODT/ODP/ODS, TXT, MD, CSV** - Documents are read **in the browser**: PDF text layers are extracted, scanned PDF pages are rendered as images for the vision model, DOCX/PPTX text, speaker notes and embedded images are pulled out. @@ -92,8 +96,10 @@ no database, no data leaving your machine except the requests you send to Ollama - Two modes: **Just make it** returns the finished homework (the answer only, ready to copy), and **Explain how to make this homework** turns it into a guided walkthrough - On phones this is a **2-step flow**: the assignment first (same simplified input as the - deck flow), then the two mode cards under a short preview — tapping a card starts it - immediately, so there is no separate button. Desktop keeps the select-then-confirm layout. + deck flow, with its own step headline, formats hint, big add area and empty placeholder), + then the two mode cards under a short preview — tapping a card starts it immediately, so + there is no separate button. Step 1 starts with *Cancel* and the flat action bar matches + the deck wizard. Desktop keeps the select-then-confirm layout. - The guide is one step per screen in the same snap-feed feel as the study feed: what to do and why, **three progressive hints** per step (nudge → hint → the full answer), and a **checkpoint** built from the normal activity components and graded through `/api/grade` @@ -954,6 +960,16 @@ Things I chose while building, with the reasoning: the same feature set the current `motion` import already pulls in. Verified with a 4x-CPU-throttled mobile trace (no long tasks, CLS 0) and a reduced-motion run. +38. **Wizard chrome lives outside the animated tree.** The step name is the headline, the + action bar is a flat full-width bar at the bottom (the tab bar hides inside the wizard), + and the bar is rendered through a React portal to ``. That last part is a real bug + fix: the screen entrance animation puts a transform on the content wrapper, and a + transform on an ancestor becomes the containing block for `position: fixed`, so the bar + used to paint inside the content and jump to the viewport bottom a second later. It now + only fades in, pinned to the bottom from its first frame — verified by sampling its + position across the entrance (5 frames, 60–450ms) from the tab bar, the drawer and the + empty state, at 360/390, both themes, both languages and with reduced motion. + 14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`). Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision diff --git a/app/create/page.tsx b/app/create/page.tsx index 58a62f1..538505e 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -4,7 +4,6 @@ import { ApiKeyGate } from "@/components/ApiKeyGate"; import { AppHeader } from "@/components/AppHeader"; import { RouteTransition } from "@/components/RouteTransition"; import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar"; -import { TabBar } from "@/components/TabBar"; import { UploadScreen } from "@/components/UploadScreen"; export const metadata = { @@ -34,7 +33,6 @@ export default async function CreatePage({ - diff --git a/app/globals.css b/app/globals.css index 49638ab..cec1d36 100644 --- a/app/globals.css +++ b/app/globals.css @@ -312,7 +312,7 @@ body { .ice-blob { animation: none; } } -/* Phones: fewer, softer blobs so the drift animation stays cheap. Unlayered + /* Phones: fewer, softer blobs so the drift animation stays cheap. Unlayered on purpose — layered rules would lose to the .ice-blob declarations above. */ @media (max-width: 1023px) { .ice-blob-c, @@ -339,11 +339,11 @@ body { --ice-orb-opacity: 0.42; } /* Phones: orbs hug the screen edges so they can never sit under the - content column's cards or primary buttons (the lower-right one used to - glow right behind the main CTAs). */ + content column's cards or primary buttons. The lower-right orb still + read as a glow behind the wizard action bar, so phones drop it. */ .ice-orb-1 { width: 96px; height: 96px; top: -44px; left: -48px; } .ice-orb-2 { width: 72px; height: 72px; top: 60%; left: -44px; } - .ice-orb-3 { width: 124px; height: 124px; bottom: 12vh; right: -84px; } + .ice-orb-3 { display: none; } } @layer components { diff --git a/app/homework/page.tsx b/app/homework/page.tsx index 2c4af0a..7e10e0c 100644 --- a/app/homework/page.tsx +++ b/app/homework/page.tsx @@ -5,7 +5,6 @@ import { AppHeader } from "@/components/AppHeader"; import { HomeworkScreen } from "@/components/homework/HomeworkScreen"; import { RouteTransition } from "@/components/RouteTransition"; import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar"; -import { TabBar } from "@/components/TabBar"; export const metadata = { title: "Make my homework", @@ -34,7 +33,6 @@ export default async function HomeworkPage({ - diff --git a/components/UploadScreen.tsx b/components/UploadScreen.tsx index ca7e5bf..008f332 100644 --- a/components/UploadScreen.tsx +++ b/components/UploadScreen.tsx @@ -37,6 +37,7 @@ import { cn, uid } from "@/lib/utils"; import { CameraIcon, ChevronDownIcon, FolderIcon, IceCubeIcon, PlusIcon, SnowflakeIcon, SparklesIcon, SunIcon } from "@/components/icons"; import { SegmentedPill } from "@/components/SegmentedPill"; import { Button, ProgressBar } from "@/components/ui"; +import { WizardActionBar } from "@/components/WizardActionBar"; type Phase = "idle" | "preparing" | "loading" | "error"; @@ -492,18 +493,6 @@ export function UploadScreen() { ) : null; - const phoneSummary = [ - text.trim() ? t("upload.summaryText") : "", - images.length > 0 ? plural(images.length, "upload.photoCount") : "", - documents.length > 0 ? plural(documents.length, "upload.docSummary") : "", - t(difficulty === "easy" ? "upload.easy" : difficulty === "hard" ? "upload.hard" : "upload.medium"), - plural(count, "deck.activities"), - folderId === UNORGANIZED_ID - ? t("common.unorganized") - : (sortedFolders.find((folder) => folder.id === folderId)?.name ?? t("common.unorganized")), - ] - .filter(Boolean) - .join(" · "); const phoneHelper = documentsBusy ? t("upload.readingDocs") : hasContent @@ -514,6 +503,31 @@ export function UploadScreen() { if (isPhone) { const stepLabel = step === 1 ? t("upload.phone.notes") : step === 2 ? t("upload.phone.options") : t("upload.phone.summary"); + const stepDesc = + step === 1 + ? t("upload.phone.notesDesc", { images: MAX_IMAGES, documents: MAX_DOCUMENTS }) + : step === 2 + ? t("upload.phone.optionsDesc") + : t("upload.phone.summaryDesc"); + const filesSummary = + images.length > 0 || documents.length > 0 + ? [ + images.length > 0 ? plural(images.length, "upload.photoCount") : "", + documents.length > 0 ? plural(documents.length, "upload.docSummary") : "", + ] + .filter(Boolean) + .join(" · ") + : t("upload.summaryText"); + const difficultyLabel = + difficulty === "easy" ? t("upload.easy") : difficulty === "hard" ? t("upload.hard") : t("upload.medium"); + const folderLabel = + folderId === UNORGANIZED_ID + ? t("common.unorganized") + : (sortedFolders.find((folder) => folder.id === folderId)?.name ?? t("common.unorganized")); + const editStep = (target: 1 | 2 | 3) => { + setStepDir(target < step ? -1 : 1); + setStep(target); + }; const fileInput = ( -
-

- {t("upload.title")} -

-

{t("upload.introShort")}

-
- +
{errorCard} - {/* step indicator */} -
-
-

{stepLabel}

- {t("common.stepOf", { n: step, total: 3 })} -
+ {/* progress: thin bar + “1 / 3” */} +
+ + {t("common.stepOf", { n: step, total: 3 })} +
+ {/* the step name is the headline; the page title lives in the chrome */} +

+ {stepLabel} +

+

{stepDesc}

+ {step === 1 ? (
+ +
- +
- - - {pasteOpen ? ( + {pasteOpen || text.trim().length > 0 ? (