diff --git a/assets/cv1239-thpt-answer-sheet-template.png b/assets/cv1239-thpt-answer-sheet-template.png new file mode 100644 index 0000000..4ca617c Binary files /dev/null and b/assets/cv1239-thpt-answer-sheet-template.png differ diff --git a/plans/260412-2233-convert-ts-to-js/phase-01-config-changes.md b/plans/260412-2233-convert-ts-to-js/phase-01-config-changes.md deleted file mode 100644 index 94e6731..0000000 --- a/plans/260412-2233-convert-ts-to-js/phase-01-config-changes.md +++ /dev/null @@ -1,121 +0,0 @@ -# Phase 1: Config Changes - -## Priority: High | Status: Pending - -## Overview - -Update/remove all TypeScript-related configuration files. This phase must complete before file conversions begin. - -## Key Insights - -- `tsconfig.json` has `@/*` path alias that must be preserved in a new `jsconfig.json` -- `next.config.ts` uses TS syntax (`import type`, type annotation) — convert to `next.config.mjs` -- `eslint.config.mjs` extends `next/typescript` — must be removed -- `next-env.d.ts` is auto-generated by Next.js for TS — delete it - -## Related Code Files - -**Delete:** -- `tsconfig.json` -- `next-env.d.ts` (if exists — auto-generated, may not be tracked) - -**Modify:** -- `package.json` — remove TS devDependencies -- `eslint.config.mjs` — remove `next/typescript` from extends -- `next.config.ts` — rename to `next.config.mjs`, strip TS syntax - -**Create:** -- `jsconfig.json` — preserve `@/*` path alias - -## Implementation Steps - -### 1. Delete `tsconfig.json` - -```bash -git rm tsconfig.json -``` - -### 2. Create `jsconfig.json` - -```json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} -``` - -### 3. Convert `next.config.ts` to `next.config.mjs` - -Before: -```ts -import type { NextConfig } from "next"; -const nextConfig: NextConfig = {}; -export default nextConfig; -``` - -After (`next.config.mjs`): -```js -/** @type {import('next').NextConfig} */ -const nextConfig = {}; -export default nextConfig; -``` - -```bash -git rm next.config.ts -# create next.config.mjs with content above -``` - -### 4. Update `package.json` - -Remove from `devDependencies`: -- `@types/node` -- `@types/react` -- `@types/react-dom` -- `typescript` - -### 5. Update `eslint.config.mjs` - -Change: -```js -...compat.extends("next/core-web-vitals", "next/typescript"), -``` -To: -```js -...compat.extends("next/core-web-vitals"), -``` - -### 6. Delete `next-env.d.ts` if it exists - -```bash -rm -f next-env.d.ts -``` - -### 7. Run `npm install` to update lockfile - -## Todo List - -- [ ] Delete `tsconfig.json` -- [ ] Create `jsconfig.json` with path alias -- [ ] Convert `next.config.ts` to `next.config.mjs` -- [ ] Remove TS deps from `package.json` -- [ ] Remove `next/typescript` from eslint config -- [ ] Delete `next-env.d.ts` -- [ ] Run `npm install` - -## Success Criteria - -- `jsconfig.json` exists with `@/*` alias -- No `tsconfig.json` in root -- No TS-related packages in `package.json` devDependencies -- `next.config.mjs` is valid JS - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| `@/*` alias breaks without jsconfig | jsconfig.json created with same paths config | -| eslint fails without typescript extend | `next/core-web-vitals` works standalone | diff --git a/plans/260412-2233-convert-ts-to-js/phase-02-convert-lib-files.md b/plans/260412-2233-convert-ts-to-js/phase-02-convert-lib-files.md deleted file mode 100644 index 337dc41..0000000 --- a/plans/260412-2233-convert-ts-to-js/phase-02-convert-lib-files.md +++ /dev/null @@ -1,114 +0,0 @@ -# Phase 2: Convert Library Files (.ts to .js) - -## Priority: High | Status: Pending - -## Overview - -Convert all 6 files in `src/lib/` from TypeScript to JavaScript. These are pure logic files (no JSX). - -## Key Insights - -- All files import types from `@/types` and `@/types/opencv` — these `import type` lines must be **removed entirely** (not converted) -- Type annotations on function params, return types, variable declarations must be stripped -- No TS enums or complex generics found — straightforward removal -- Some files use `as` type assertions — remove them - -## Related Code Files - -**Convert (rename .ts to .js, strip types):** -- `src/lib/image-preprocessing.ts` → `src/lib/image-preprocessing.js` -- `src/lib/marker-detection.ts` → `src/lib/marker-detection.js` -- `src/lib/bubble-grid-generator.ts` → `src/lib/bubble-grid-generator.js` -- `src/lib/answer-detection.ts` → `src/lib/answer-detection.js` -- `src/lib/debug-visualization.ts` → `src/lib/debug-visualization.js` -- `src/lib/scoring.ts` → `src/lib/scoring.js` - -## Implementation Steps - -For each file, apply these transformations: - -### 1. Remove all `import type` statements - -```ts -// DELETE these lines entirely: -import type { OpenCVMat } from '@/types/opencv'; -import type { Bubble, TrueFalseAnswer } from '@/types'; -``` - -### 2. Remove type annotations from function parameters - -```ts -// Before: -function processImage(src: OpenCVMat, threshold: number): OpenCVMat { -// After: -function processImage(src, threshold) { -``` - -### 3. Remove return type annotations - -```ts -// Before: -export function detectMarkers(gray: OpenCVMat): MarkerDetectionResult { -// After: -export function detectMarkers(gray) { -``` - -### 4. Remove variable type annotations - -```ts -// Before: -const result: MarkerDetectionResult = { ... }; -// After: -const result = { ... }; -``` - -### 5. Remove type assertions (`as Type`) - -```ts -// Before: -const cv = window.cv as OpenCV; -// After: -const cv = window.cv; -``` - -### 6. Remove generic type parameters - -```ts -// Before: -const answers: Array = []; -// After: -const answers = []; -``` - -### 7. Rename files - -```bash -git mv src/lib/image-preprocessing.ts src/lib/image-preprocessing.js -git mv src/lib/marker-detection.ts src/lib/marker-detection.js -git mv src/lib/bubble-grid-generator.ts src/lib/bubble-grid-generator.js -git mv src/lib/answer-detection.ts src/lib/answer-detection.js -git mv src/lib/debug-visualization.ts src/lib/debug-visualization.js -git mv src/lib/scoring.ts src/lib/scoring.js -``` - -## Todo List - -- [ ] Convert `image-preprocessing.ts` -- [ ] Convert `marker-detection.ts` -- [ ] Convert `bubble-grid-generator.ts` -- [ ] Convert `answer-detection.ts` -- [ ] Convert `debug-visualization.ts` -- [ ] Convert `scoring.ts` - -## Success Criteria - -- No `.ts` files remain in `src/lib/` -- No TypeScript syntax in any `.js` file -- All exports preserved (same function names, same default values) - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| Accidental logic removal when stripping types | Only remove type syntax, preserve all runtime code | -| Missed type annotation causes syntax error | `npm run build` in Phase 5 catches this | diff --git a/plans/260412-2233-convert-ts-to-js/phase-03-convert-components.md b/plans/260412-2233-convert-ts-to-js/phase-03-convert-components.md deleted file mode 100644 index ebcdbda..0000000 --- a/plans/260412-2233-convert-ts-to-js/phase-03-convert-components.md +++ /dev/null @@ -1,100 +0,0 @@ -# Phase 3: Convert Components (.tsx to .jsx) - -## Priority: High | Status: Pending - -## Overview - -Convert all 5 React component files from TypeScript JSX to plain JSX. These files contain JSX + type annotations. - -## Key Insights - -- Components use `import type` from `@/types` — remove entirely -- Some components define inline prop types or use TS interfaces — remove -- React event handler types (e.g., `React.ChangeEvent`) must be stripped -- State hooks with generic types like `useState()` — remove the generic - -## Related Code Files - -**Convert (rename .tsx to .jsx, strip types):** -- `src/components/ConfigurationPage.tsx` → `src/components/ConfigurationPage.jsx` -- `src/components/ImageProcessor.tsx` → `src/components/ImageProcessor.jsx` -- `src/components/Navigation.tsx` → `src/components/Navigation.jsx` -- `src/components/ResultsPage.tsx` → `src/components/ResultsPage.jsx` -- `src/components/UploadPage.tsx` → `src/components/UploadPage.jsx` - -## Implementation Steps - -For each component file, apply these transformations: - -### 1. Remove `import type` statements - -```tsx -// DELETE: -import type { TestConfig } from '@/types'; -``` - -### 2. Remove prop type definitions - -```tsx -// Before: -interface ConfigurationPageProps { - config: TestConfig; - onSave: (config: TestConfig) => void; -} -export default function ConfigurationPage({ config, onSave }: ConfigurationPageProps) { -// After: -export default function ConfigurationPage({ config, onSave }) { -``` - -### 3. Remove useState generics - -```tsx -// Before: -const [config, setConfig] = useState(initialConfig); -// After: -const [config, setConfig] = useState(initialConfig); -``` - -### 4. Remove event handler type annotations - -```tsx -// Before: -const handleChange = (e: React.ChangeEvent) => { -// After: -const handleChange = (e) => { -``` - -### 5. Remove type assertions and other TS syntax - -Same rules as Phase 2 — strip `as Type`, `: Type`, generic params. - -### 6. Rename files - -```bash -git mv src/components/ConfigurationPage.tsx src/components/ConfigurationPage.jsx -git mv src/components/ImageProcessor.tsx src/components/ImageProcessor.jsx -git mv src/components/Navigation.tsx src/components/Navigation.jsx -git mv src/components/ResultsPage.tsx src/components/ResultsPage.jsx -git mv src/components/UploadPage.tsx src/components/UploadPage.jsx -``` - -## Todo List - -- [ ] Convert `ConfigurationPage.tsx` -- [ ] Convert `ImageProcessor.tsx` -- [ ] Convert `Navigation.tsx` -- [ ] Convert `ResultsPage.tsx` -- [ ] Convert `UploadPage.tsx` - -## Success Criteria - -- No `.tsx` files remain in `src/components/` -- All components render without errors -- No TypeScript syntax in any `.jsx` file - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| Inline interface removal breaks destructuring | Only remove type annotations, keep destructuring patterns | -| Missed generic on useState causes runtime issue | Generics are compile-time only; removal is safe | diff --git a/plans/260412-2233-convert-ts-to-js/phase-04-convert-app-files.md b/plans/260412-2233-convert-ts-to-js/phase-04-convert-app-files.md deleted file mode 100644 index be41f21..0000000 --- a/plans/260412-2233-convert-ts-to-js/phase-04-convert-app-files.md +++ /dev/null @@ -1,59 +0,0 @@ -# Phase 4: Convert App Files (.tsx to .jsx) - -## Priority: High | Status: Pending - -## Overview - -Convert the 2 Next.js app directory files from TypeScript to JavaScript. - -## Related Code Files - -**Convert:** -- `src/app/layout.tsx` → `src/app/layout.jsx` -- `src/app/page.tsx` → `src/app/page.jsx` - -## Implementation Steps - -### 1. Convert `layout.tsx` - -Likely contains: -- `import type { Metadata } from 'next'` — remove -- Type annotation on props — remove -- `metadata` export with `Metadata` type — keep export, remove type - -```tsx -// Before: -import type { Metadata } from "next"; -export const metadata: Metadata = { ... }; -export default function RootLayout({ children }: { children: React.ReactNode }) { -// After: -export const metadata = { ... }; -export default function RootLayout({ children }) { -``` - -### 2. Convert `page.tsx` - -Strip any type annotations, `import type` lines. Keep all JSX and logic intact. - -### 3. Rename files - -```bash -git mv src/app/layout.tsx src/app/layout.jsx -git mv src/app/page.tsx src/app/page.jsx -``` - -## Todo List - -- [ ] Convert `layout.tsx` -- [ ] Convert `page.tsx` - -## Success Criteria - -- No `.tsx` files remain in `src/app/` -- Next.js recognizes `.jsx` layout and page files - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| Next.js not picking up `.jsx` in app dir | Next.js supports .jsx natively in app router | diff --git a/plans/260412-2233-convert-ts-to-js/phase-05-cleanup-and-verify.md b/plans/260412-2233-convert-ts-to-js/phase-05-cleanup-and-verify.md deleted file mode 100644 index 84bd977..0000000 --- a/plans/260412-2233-convert-ts-to-js/phase-05-cleanup-and-verify.md +++ /dev/null @@ -1,89 +0,0 @@ -# Phase 5: Cleanup and Verification - -## Priority: High | Status: Pending -## Depends On: Phases 1, 2, 3, 4 - -## Overview - -Delete leftover TS artifacts, update docs, verify the app builds and runs. - -## Implementation Steps - -### 1. Delete `src/types/` directory - -```bash -git rm -r src/types/ -``` - -Files deleted: -- `src/types/index.ts` (shared interfaces) -- `src/types/opencv.ts` (OpenCV type declarations) - -### 2. Verify no .ts/.tsx files remain - -```bash -find src/ -name "*.ts" -o -name "*.tsx" | head -20 -# Should return nothing -``` - -### 3. Verify no `import type` or TS syntax remains - -```bash -grep -r "import type" src/ || echo "Clean" -grep -rn ": [A-Z][a-zA-Z]*[^=]" src/ --include="*.js" --include="*.jsx" | head -20 -# Manual review of any hits — some may be legitimate JS (object properties) -``` - -### 4. Run build - -```bash -npm run build -``` - -Fix any errors. Common issues: -- Missed type annotation → syntax error pointing to exact line -- Broken import path → module not found error - -### 5. Run dev server smoke test - -```bash -npm run dev -# Open http://localhost:3000 and verify page loads -``` - -### 6. Run lint - -```bash -npm run lint -``` - -### 7. Update README.md - -- Change "Next.js 15 with TypeScript" to "Next.js 15" -- Remove TypeScript interface examples from Data Structure section, or convert to JSDoc comments -- Update Project Structure to show `.jsx`/`.js` extensions - -## Todo List - -- [ ] Delete `src/types/` directory -- [ ] Verify no TS files remain -- [ ] Verify no TS syntax remains in JS files -- [ ] `npm run build` passes -- [ ] `npm run lint` passes -- [ ] `npm run dev` starts and page loads -- [ ] Update README.md - -## Success Criteria - -- Zero `.ts`/`.tsx` files in project (except node_modules) -- `npm run build` exits 0 -- `npm run dev` serves the app -- `npm run lint` passes -- README reflects JS stack - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| Build fails from missed conversion | Error messages point to exact file:line, fix iteratively | -| Lint rules incompatible | Already removed `next/typescript` in Phase 1 | diff --git a/plans/260412-2233-convert-ts-to-js/plan.md b/plans/260412-2233-convert-ts-to-js/plan.md deleted file mode 100644 index d7750e1..0000000 --- a/plans/260412-2233-convert-ts-to-js/plan.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "Convert TypeScript to JavaScript" -description: "Remove TypeScript from Next.js project, convert all .ts/.tsx files to .js/.jsx" -status: pending -priority: P2 -effort: 2h -branch: main -tags: [migration, typescript, javascript] -created: 2026-04-12 ---- - -# Convert TypeScript to JavaScript - -## Summary - -Mechanical conversion: strip all TS annotations, rename files, update configs. No logic changes. - -## Data Flow - -``` -.ts/.tsx files --> remove type annotations --> rename to .js/.jsx -tsconfig.json --> DELETE -next.config.ts --> convert to next.config.mjs (plain JS) -package.json --> remove @types/* and typescript deps -eslint.config.mjs --> remove "next/typescript" extend -src/types/ --> DELETE entirely -``` - -## Phases - -| # | Phase | Files Touched | Status | -|---|-------|--------------|--------| -| 1 | [Config changes](phase-01-config-changes.md) | package.json, tsconfig.json, next.config.ts, eslint.config.mjs, next-env.d.ts | Pending | -| 2 | [Convert lib files](phase-02-convert-lib-files.md) | src/lib/*.ts (6 files) | Pending | -| 3 | [Convert components](phase-03-convert-components.md) | src/components/*.tsx (5 files) | Pending | -| 4 | [Convert app files](phase-04-convert-app-files.md) | src/app/layout.tsx, src/app/page.tsx | Pending | -| 5 | [Cleanup and verify](phase-05-cleanup-and-verify.md) | src/types/ (delete), jsconfig.json (create), README.md | Pending | - -## Dependency Graph - -Phase 1 (config) has no blockers. -Phases 2, 3, 4 can run in parallel after Phase 1 (no shared files). -Phase 5 depends on all of 1-4 completing. - -``` -Phase 1 --> Phase 2 --| - \-> Phase 3 --|--> Phase 5 - \-> Phase 4 --| -``` - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Broken imports after rename | Low | High | Phase 5 runs `npm run build` to catch | -| `next.config.ts` rename breaks Next.js | Low | High | Next.js supports `.mjs` natively | -| Missing type-as-value usage (enums) | Very Low | Medium | No TS enums in codebase (verified) | -| `opencv-ts` package requires TS | Low | Low | It's a runtime lib, works without TS | - -## Rollback Plan - -Git commit before starting. Single `git checkout .` reverts everything. - -## Success Criteria - -- [x] `npm run build` passes with zero errors -- [x] `npm run dev` starts successfully -- [x] No `.ts` or `.tsx` files remain in `src/` -- [x] No `typescript` or `@types/*` in package.json -- [x] No `tsconfig.json` in project root -- [x] `jsconfig.json` preserves `@/*` path alias diff --git a/plans/260413-0903-ui-ux-and-detection-fixes/phase-01-critical-bug-fixes.md b/plans/260413-0903-ui-ux-and-detection-fixes/phase-01-critical-bug-fixes.md deleted file mode 100644 index b1bc230..0000000 --- a/plans/260413-0903-ui-ux-and-detection-fixes/phase-01-critical-bug-fixes.md +++ /dev/null @@ -1,190 +0,0 @@ -# Phase 1: Critical Bug Fixes (P0) - -## Context - -- [Review report](../reports/ui-ux-review-260413-0856-answer-sheet-analysis.md) -- [Plan overview](./plan.md) - -## Overview - -- **Priority**: P0 -- **Status**: Pending -- **Effort**: 2h -- **Blocks**: Phase 3 (state lifting replaces localStorage pattern established here) - -## Key Issues - -1. **localStorage race condition** - `setTimeout(100)` in UploadPage.jsx:99-104 to save results. State may not have settled; results silently lost. -2. **`useState` misused as `useEffect`** - ImageThumbnail (UploadPage.jsx:271-275) uses `useState(() => {...})` to create object URLs. Never updates on prop change, never revokes old URLs = memory leak. -3. **localStorage overflow** - Debug image data URLs (~1-2MB each) stored in `studentResults`. 50+ students = blown 5MB localStorage limit. - -## Architecture - -### Data Flow (After Fix) - -``` -processImages loop - -> handleProcessingComplete collects result in resultsRef (not state) - -> loop ends - -> finally block: write resultsRef.current to localStorage (scores only) - -> finally block: write debug images to IndexedDB via indexed-db-store.js - -> update React state from ref - -ImageThumbnail - -> useEffect(file): create objectURL, return cleanup that revokes it - -> on file prop change: old URL revoked, new URL created -``` - -## Related Code Files - -### Files to Modify - -| File | Changes | -|------|---------| -| `src/components/UploadPage.jsx` | Fix race condition with ref-based result collection; fix ImageThumbnail useEffect | -| `src/components/ImageProcessor.jsx` | Separate debugImageUrl from result object before returning | - -### Files to Create - -| File | Purpose | -|------|---------| -| `src/lib/indexed-db-store.js` | IndexedDB wrapper for debug images (put/get/delete/clear) | - -## Implementation Steps - -### 1. Create `src/lib/indexed-db-store.js` (~60 lines) - -Simple IndexedDB wrapper. Single object store `debugImages` keyed by student result ID. - -```js -// API: -// openDB() -> Promise -// saveDebugImage(id: string, dataUrl: string) -> Promise -// getDebugImage(id: string) -> Promise -// deleteDebugImage(id: string) -> Promise -// clearDebugImages() -> Promise -``` - -- DB name: `chambai` -- Store name: `debugImages` -- Key path: `id` -- Version: 1 - -### 2. Fix localStorage race condition in UploadPage.jsx - -**Current code (lines 86-114):** -```js -// Uses setTimeout(100) + setState callback to read final results -setTimeout(() => { - setProcessedResults((current) => { - localStorage.setItem('studentResults', JSON.stringify([...finalResults, ...current])); - return current; - }); -}, 100); -``` - -**Fix:** Use a ref to accumulate results during processing loop. - -Changes to `UploadPage.jsx`: -- Add `const resultsRef = useRef([])` at component top -- In `handleProcessingComplete`: push to `resultsRef.current` (in addition to `setProcessedResults`) -- In `processImages`, after the for-loop completes: - - Strip `debugImageUrl` from each result before localStorage save - - Save debug images to IndexedDB via `saveDebugImage(result.id, result.debugImageUrl)` - - Merge with existing results and write to localStorage synchronously (no setTimeout) - - Reset `resultsRef.current = []` - -```js -// In finally block or after loop: -const existing = JSON.parse(localStorage.getItem('studentResults') || '[]'); -const toSave = resultsRef.current.map(r => { - const { debugImageUrl, ...rest } = r; - return rest; -}); -localStorage.setItem('studentResults', JSON.stringify([...existing, ...toSave])); - -// Save debug images to IndexedDB -for (const r of resultsRef.current) { - if (r.debugImageUrl) { - await saveDebugImage(r.id, r.debugImageUrl); - } -} -resultsRef.current = []; -``` - -### 3. Fix ImageThumbnail memory leak (UploadPage.jsx:267-294) - -**Current (broken):** -```js -function ImageThumbnail({ file, index, onRemove }) { - const [src, setSrc] = useState(''); - useState(() => { // <-- misused! This is NOT useEffect - const url = URL.createObjectURL(file); - setSrc(url); - return () => URL.revokeObjectURL(url); // return value ignored - }); -``` - -**Fix:** -```js -function ImageThumbnail({ file, index, onRemove }) { - const [src, setSrc] = useState(''); - - useEffect(() => { - const url = URL.createObjectURL(file); - setSrc(url); - return () => URL.revokeObjectURL(url); - }, [file]); -``` - -Add `useEffect` to the import list at top of file. - -### 4. Update ResultsPage.jsx to load debug images from IndexedDB - -When opening StudentDetailModal, load debug image from IndexedDB: - -```js -// In StudentDetailModal, add: -const [debugImageUrl, setDebugImageUrl] = useState(null); -useEffect(() => { - getDebugImage(student.id).then(url => setDebugImageUrl(url)); -}, [student.id]); -``` - -### 5. Update ImageProcessor.jsx - no debugImageUrl in result - -In `runDetectionPipeline`, return debugImageUrl separately (already done), but ensure the `onProcessingComplete` callback receives it as a separate field that UploadPage can handle independently. - -No change needed here - current code already passes `debugImageUrl` in the result object. The separation happens in UploadPage step 2. - -## Todo List - -- [ ] Create `src/lib/indexed-db-store.js` with put/get/delete/clear API -- [ ] Fix UploadPage.jsx: add resultsRef, remove setTimeout hack, save to IndexedDB -- [ ] Fix UploadPage.jsx: change ImageThumbnail `useState` -> `useEffect` with cleanup -- [ ] Add `useEffect` to UploadPage import statement -- [ ] Update ResultsPage.jsx StudentDetailModal to load debug images from IndexedDB -- [ ] Update ResultsPage clearResults to also call `clearDebugImages()` -- [ ] Test: process 5+ images, verify all results saved, verify debug images load in detail modal -- [ ] Test: verify no object URL memory leaks (check browser devtools) - -## Success Criteria - -1. Process 50+ images without localStorage overflow error -2. All results persisted after processing completes (no silent data loss) -3. Debug images viewable in StudentDetailModal (loaded from IndexedDB) -4. No object URL memory leaks in ImageThumbnail on re-render -5. `localStorage.getItem('studentResults')` contains no data URL strings - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| IndexedDB not available in old browsers | Feature-detect; fallback to not saving debug images (scores still saved to localStorage) | -| Existing localStorage data has embedded debug URLs | Migration: on first load, if results contain `debugImageUrl`, extract to IndexedDB and re-save without them | - -## Security Considerations - -- No auth concerns (client-only app) -- IndexedDB is same-origin scoped, no cross-site risk -- Debug images are transient; no PII beyond student ID numbers visible on sheet diff --git a/plans/260413-0903-ui-ux-and-detection-fixes/phase-02-detection-pipeline-fixes.md b/plans/260413-0903-ui-ux-and-detection-fixes/phase-02-detection-pipeline-fixes.md deleted file mode 100644 index 70ba7c6..0000000 --- a/plans/260413-0903-ui-ux-and-detection-fixes/phase-02-detection-pipeline-fixes.md +++ /dev/null @@ -1,290 +0,0 @@ -# Phase 2: Detection Pipeline Fixes (P1) - -## Context - -- [Review report](../reports/ui-ux-review-260413-0856-answer-sheet-analysis.md) -- [Plan overview](./plan.md) -- Reference: CV1239/BGDDT 2025 Vietnamese THPT answer sheet format - -## Overview - -- **Priority**: P1 -- **Status**: Pending -- **Effort**: 4h -- **Dependencies**: None (independent of Phase 1) -- **Blocks**: Nothing directly - -## Key Issues - -1. **SHEET_LAYOUT coordinates are wrong** - Student ID, Exam Code, and all three Phan sections have incorrect x/y values -2. **No perspective correction** - Corner markers are detected but only used for bounding box, not deskewing -3. **Phan III model is wrong** - Treats each answer as single digit; actual sheet supports multi-character (sign + digits + decimal comma) - -## Architecture - -### Detection Pipeline (After Fix) - -``` -Image -> Preprocess (grayscale + blur + threshold + morph) - -> Detect 4 corner markers - -> IF 4 markers found AND skew > 2deg: - Apply getPerspectiveTransform + warpPerspective -> corrected image - ELSE: - Use bounding box as-is (current behavior) - -> Generate bubble grid with CORRECTED coordinates - -> Phan III: multi-column per question (sign + digits + comma) - -> Measure fill + detect answers - -> Return results -``` - -### Phan III Data Model Change - -**Before:** Each question = 1 digit column (0-9) -**After:** Each question = N character columns, where each column has rows for: `-`, `,`, `0-9` - -``` -Question layout (per question): - Col 0: sign column -> rows: ["-", "blank"] (or just "-" bubble + empty) - Col 1: digit column 1 -> rows: [0,1,2,3,4,5,6,7,8,9] - Col 2: digit column 2 -> rows: [0,1,2,3,4,5,6,7,8,9] - Col 3: comma column -> rows: [",", "blank"] - Col 4: digit column 3 -> rows: [0,1,2,3,4,5,6,7,8,9] -``` - -Actual CV1239 sheet has approximately 5 character positions per question. Each character position has 12 rows: `-`, `,`, `0-9`. The detection reads filled bubbles left-to-right to form the answer string. - -## Related Code Files - -### Files to Modify - -| File | Changes | -|------|---------| -| `src/lib/bubble-grid-generator.js` | Fix SHEET_LAYOUT coordinates; rewrite `generatePhanIIIBubbles` for multi-char model | -| `src/lib/marker-detection.js` | Add `applyPerspectiveCorrection()` export | -| `src/lib/answer-detection.js` | Rewrite `detectPhanIIIAnswers` for multi-char reading | -| `src/lib/types.js` | Add `charPosition` and `charValue` fields to Bubble typedef | -| `src/components/ImageProcessor.jsx` | Insert perspective correction step in pipeline | - -### Files NOT Modified - -| File | Reason | -|------|--------| -| `src/lib/image-preprocessing.js` | No changes needed | -| `src/lib/scoring.js` | Already compares strings; multi-char answers work as-is | -| `src/lib/debug-visualization.js` | Phan III debug drawing needs update but deferred to Phase 4 (Vietnamese legend) | - -## Implementation Steps - -### 1. Fix SHEET_LAYOUT in `bubble-grid-generator.js` - -**Current (wrong) values:** -```js -studentId: { x: 0.65, y: 0.02, w: 0.25, h: 0.18, cols: 8, rows: 10 }, -examCode: { x: 0.45, y: 0.02, w: 0.15, h: 0.18, cols: 4, rows: 10 }, -phanI: { x: 0.03, y: 0.25, w: 0.94, h: 0.30, ... }, -phanII: { x: 0.03, y: 0.58, w: 0.94, h: 0.15, ... }, -phanIII: { x: 0.03, y: 0.76, w: 0.94, h: 0.22, ... }, -``` - -**Corrected values (from PDF analysis):** -```js -studentId: { x: 0.58, y: 0.05, w: 0.24, h: 0.20, cols: 8, rows: 10 }, -examCode: { x: 0.82, y: 0.05, w: 0.13, h: 0.20, cols: 4, rows: 10 }, -phanI: { x: 0.03, y: 0.28, w: 0.94, h: 0.27, ... }, -phanII: { x: 0.03, y: 0.55, w: 0.94, h: 0.17, ... }, -phanIII: { x: 0.03, y: 0.72, w: 0.94, h: 0.26, ... }, -``` - -Key fix: Exam Code moves from x:0.45 (left-center, WRONG) to x:0.82 (right of SBD, CORRECT). - -### 2. Rewrite Phan III bubble generation - -Replace the current single-digit model in `generatePhanIIIBubbles`: - -```js -// New Phan III layout config -phanIII: { - x: 0.03, y: 0.72, w: 0.94, h: 0.26, - questions: 6, - charsPerQuestion: 5, // max character positions per answer - // Row labels per character column: ['-', ',', '0', '1', ..., '9'] - charRows: 12, -} -``` - -Each question occupies `w / 6` width. Within each question column: -- 5 sub-columns for character positions -- 12 rows: index 0 = `-` (sign), index 1 = `,` (decimal), indices 2-11 = digits 0-9 - -New bubble fields: -```js -{ - section: 'section3', - question: 1, // 1-6 - charPosition: 0, // 0-4 (which character slot) - charValue: '-', // '-', ',', '0'-'9' - // row kept for backward compat with debug visualization -} -``` - -### 3. Rewrite `detectPhanIIIAnswers` in `answer-detection.js` - -```js -export function detectPhanIIIAnswers(bubbles, gray, questionCount = 6) { - const sectionBubbles = bubbles.filter(b => b.section === 'section3'); - const answers = []; - - for (let q = 1; q <= questionCount; q++) { - const qBubbles = sectionBubbles.filter(b => b.question === q); - // Group by charPosition, find best-filled in each position - const charPositions = groupByField(qBubbles, 'charPosition'); - let answer = ''; - - for (let pos = 0; pos < 5; pos++) { - const posBubbles = charPositions[pos] || []; - const best = findBestFilled(posBubbles, gray); - if (best?.charValue) { - answer += best.charValue; - } - } - - // Trim trailing empty positions, normalize - answers.push(answer.replace(/\s+$/, '')); - } - - return answers; -} -``` - -### 4. Add perspective correction to `marker-detection.js` - -Add new export function after existing `detectCornerMarkers`: - -```js -/** - * Apply perspective correction using 4 detected corner markers. - * Returns corrected image and updated bounding box, or original if skew is minimal. - */ -export function applyPerspectiveCorrection(src, markers, imageWidth, imageHeight) { - const cv = window.cv; - - // Only correct if we have exactly 4 corners - if (!markers.corners || markers.corners.length !== 4) { - return { corrected: src, markers, applied: false }; - } - - const [tl, tr, br, bl] = markers.corners; - - // Calculate skew angle; skip if < 2 degrees - const topAngle = Math.atan2(tr.y - tl.y, tr.x - tl.x) * 180 / Math.PI; - if (Math.abs(topAngle) < 2) { - return { corrected: src, markers, applied: false }; - } - - // Destination rectangle (axis-aligned) - const dstWidth = Math.max( - Math.hypot(tr.x - tl.x, tr.y - tl.y), - Math.hypot(br.x - bl.x, br.y - bl.y) - ); - const dstHeight = Math.max( - Math.hypot(bl.x - tl.x, bl.y - tl.y), - Math.hypot(br.x - tr.x, br.y - tr.y) - ); - - const srcPts = cv.matFromArray(4, 1, cv.CV_32FC2, [ - tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y - ]); - const dstPts = cv.matFromArray(4, 1, cv.CV_32FC2, [ - 0, 0, dstWidth, 0, dstWidth, dstHeight, 0, dstHeight - ]); - - const M = cv.getPerspectiveTransform(srcPts, dstPts); - const corrected = new cv.Mat(); - cv.warpPerspective(src, corrected, M, new cv.Size(dstWidth, dstHeight)); - - srcPts.delete(); - dstPts.delete(); - M.delete(); - - // New bounding box covers entire corrected image - const newMarkers = { - corners: markers.corners, - edges: markers.edges, - boundingBox: { - left: 0, top: 0, - right: dstWidth, bottom: dstHeight, - width: dstWidth, height: dstHeight, - }, - }; - - return { corrected, markers: newMarkers, applied: true }; -} -``` - -### 5. Integrate perspective correction in `ImageProcessor.jsx` - -In `runDetectionPipeline`, after marker detection, before bubble grid generation: - -```js -const markers = detectCornerMarkers(thresh, imageData.width, imageData.height); - -// NEW: apply perspective correction -const { corrected, markers: correctedMarkers, applied } = - applyPerspectiveCorrection(gray, markers, imageData.width, imageData.height); - -const activeGray = applied ? corrected : gray; -const activeMarkers = correctedMarkers; -const activeWidth = applied ? corrected.cols : imageData.width; -const activeHeight = applied ? corrected.rows : imageData.height; - -const bubbles = generateBubbleGrid(activeMarkers, activeWidth, activeHeight); -const studentId = detectStudentId(bubbles, activeGray); -// ... rest uses activeGray instead of gray - -// Clean up -if (applied) corrected.delete(); -``` - -### 6. Update `types.js` Bubble typedef - -Add optional fields: -```js - * @property {number} [charPosition] - Character position index (for section3 multi-char) - * @property {string} [charValue] - Character value: '-', ',', '0'-'9' (for section3) -``` - -## Todo List - -- [ ] Fix SHEET_LAYOUT coordinates (studentId, examCode, phanI, phanII, phanIII) -- [ ] Update phanIII layout config for multi-character model (charsPerQuestion, charRows) -- [ ] Rewrite `generatePhanIIIBubbles` for multi-character columns -- [ ] Update Bubble typedef in types.js with charPosition/charValue fields -- [ ] Rewrite `detectPhanIIIAnswers` to read multi-char answers -- [ ] Add `applyPerspectiveCorrection` to marker-detection.js -- [ ] Integrate perspective correction in ImageProcessor.jsx pipeline -- [ ] Add helper `groupByField` to answer-detection.js (or reuse groupByColumn pattern) -- [ ] Test with straight scan: verify no perspective correction applied (angle < 2deg) -- [ ] Test with skewed photo: verify perspective correction improves detection -- [ ] Test Phan III: verify "-1,5" type answers detected correctly - -## Success Criteria - -1. Student ID detected at correct position (x:0.58, not x:0.65) -2. Exam Code detected at RIGHT side of sheet (x:0.82, not x:0.45) -3. Phan III returns multi-character strings (e.g., "-1,5") not single digits -4. Perspective correction activates only on skewed images (>2 degrees) -5. Straight scans produce same or better results as before - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Corrected coordinates still off | Medium | High | Coordinates are in one const; easy to fine-tune with debug visualization overlay | -| Phan III char positions vary between print batches | Low | Medium | Keep charsPerQuestion configurable in SHEET_LAYOUT | -| Perspective correction introduces artifacts | Low | Medium | Only apply when skew > 2deg; use INTER_LINEAR interpolation | -| OpenCV.js getPerspectiveTransform not available | Very Low | High | Feature-detect; skip correction if function missing | - -## Security Considerations - -- No new attack surface; all processing is client-side -- No external network calls added diff --git a/plans/260413-0903-ui-ux-and-detection-fixes/phase-03-ux-improvements.md b/plans/260413-0903-ui-ux-and-detection-fixes/phase-03-ux-improvements.md deleted file mode 100644 index c363e70..0000000 --- a/plans/260413-0903-ui-ux-and-detection-fixes/phase-03-ux-improvements.md +++ /dev/null @@ -1,241 +0,0 @@ -# Phase 3: UX Improvements (P1) - -## Context - -- [Review report](../reports/ui-ux-review-260413-0856-answer-sheet-analysis.md) -- [Plan overview](./plan.md) - -## Overview - -- **Priority**: P1 -- **Status**: Pending -- **Effort**: 3h -- **Dependencies**: Phase 1 (IndexedDB store established; localStorage pattern understood) -- **Blocks**: Phase 4 (workflow features use lifted state) - -## Key Issues - -1. **No shared state** - Each page reads/writes localStorage independently, causing sync bugs -2. **No keyboard navigation** for Phan I - 40 answers require 40+ mouse clicks -3. **Upload tab accessible without config** - Teachers can process images with no answer key -4. **No step-completion indicators** - Teachers don't know which steps are done -5. **Save/Reset buttons at top** of ConfigurationPage - Must scroll up after filling answers -6. **Status messages auto-dismiss in 3s** - Too fast for target users - -## Architecture - -### State Lifting Design - -Currently each page reads from localStorage on mount. After lifting: - -``` -page.jsx (Home) - state: { config, results, configSaved } - | - ├── Navigation (receives: configSaved, hasResults) - ├── ConfigurationPage (receives: config, onConfigChange, onSave, onReset) - ├── UploadPage (receives: config, onResultsAdd) - └── ResultsPage (receives: results, config, onResultsUpdate, onResultsClear) -``` - -**Data flow:** -- `config` flows down from Home -> all pages -- `onConfigChange` bubbles up from ConfigPage -> Home (updates state + localStorage) -- `onResultsAdd` bubbles up from UploadPage -> Home (merges results + saves) -- `onResultsUpdate` bubbles up from ResultsPage -> Home (for manual corrections in Phase 4) -- `configSaved` boolean: true when config has been saved at least once - -**Migration:** On mount, Home reads both `testConfig` and `studentResults` from localStorage. Pages no longer read localStorage directly. - -## Related Code Files - -### Files to Modify - -| File | Changes | -|------|---------| -| `src/app/page.jsx` | Lift config + results state; pass as props; load from localStorage on mount | -| `src/components/Navigation.jsx` | Accept `configSaved` + `hasResults` props; show completion indicators; disable Upload when no config | -| `src/components/ConfigurationPage.jsx` | Accept config/callbacks as props; move Save/Reset to sticky bottom; add keyboard nav for Phan I; increase status message timeout | -| `src/components/UploadPage.jsx` | Accept config prop instead of reading localStorage; accept `onResultsAdd` callback | -| `src/components/ResultsPage.jsx` | Accept results + config as props instead of reading localStorage | - -## Implementation Steps - -### 1. Lift state to `page.jsx` - -```jsx -export default function Home() { - const [currentPage, setCurrentPage] = useState('config'); - const [config, setConfig] = useState(null); - const [results, setResults] = useState([]); - const [configSaved, setConfigSaved] = useState(false); - - // Load from localStorage on mount - useEffect(() => { - const savedConfig = localStorage.getItem('testConfig'); - const savedResults = localStorage.getItem('studentResults'); - if (savedConfig) { - setConfig(JSON.parse(savedConfig)); - setConfigSaved(true); - } - if (savedResults) { - setResults(JSON.parse(savedResults)); - } - }, []); - - const handleConfigSave = (newConfig) => { - setConfig(newConfig); - setConfigSaved(true); - localStorage.setItem('testConfig', JSON.stringify(newConfig)); - }; - - const handleResultsAdd = (newResults) => { - setResults(prev => { - const merged = [...prev, ...newResults]; - localStorage.setItem('studentResults', JSON.stringify(merged)); - return merged; - }); - }; - - const handleResultsClear = () => { - setResults([]); - localStorage.removeItem('studentResults'); - }; - - const handleResetAll = () => { - setConfig(null); - setResults([]); - setConfigSaved(false); - localStorage.clear(); - }; - // ... pass these to child components -} -``` - -### 2. Update Navigation.jsx - completion indicators + guard - -```jsx -export default function Navigation({ currentPage, onPageChange, configSaved, hasResults }) { - const buttons = [ - { key: 'config', label: '1. Cau hinh de thi', done: configSaved }, - { key: 'upload', label: '2. Tai va xu ly anh', done: hasResults, disabled: !configSaved }, - { key: 'results', label: '3. Ket qua', done: false, disabled: !hasResults }, - ]; -``` - -- Show green checkmark icon when `done` is true -- When `disabled`, use `cursor-not-allowed` + `opacity-50` + show tooltip: "Vui long luu cau hinh truoc" -- Prevent `onPageChange` call when disabled - -### 3. Update ConfigurationPage.jsx - -**a) Accept props instead of localStorage:** -```jsx -export default function ConfigurationPage({ config: initialConfig, onSave, onReset }) { - const [config, setConfig] = useState(initialConfig || DEFAULT_CONFIG); - // Remove useEffect that reads localStorage - // saveConfig calls onSave(config) instead of localStorage.setItem -} -``` - -**b) Move Save/Reset to sticky bottom bar:** -```jsx -// Remove buttons from top of page -// Add at bottom: -
- - -
-``` - -**c) Add keyboard navigation for Phan I:** - -Wrap Phan I grid items in a component that: -- On key press `A/B/C/D` (case insensitive): set answer for current question, auto-advance focus to next question -- On `Backspace`: clear current answer, move focus to previous question -- On `ArrowDown/ArrowRight`: advance to next question -- On `ArrowUp/ArrowLeft`: go to previous question -- Each question row gets `tabIndex={0}` and a ref for focus management - -```jsx -const questionRefs = useRef([]); - -const handlePhanIKeyDown = (index, e) => { - const key = e.key.toUpperCase(); - if (['A', 'B', 'C', 'D'].includes(key)) { - updatePhanIAnswer(index, key); - // Auto-advance - if (index < config.phanI.questionCount - 1) { - questionRefs.current[index + 1]?.focus(); - } - } else if (e.key === 'Backspace') { - updatePhanIAnswer(index, ''); - if (index > 0) questionRefs.current[index - 1]?.focus(); - } - // Arrow keys for navigation -}; -``` - -**d) Increase status message timeout from 3s to 5s.** - -### 4. Update UploadPage.jsx - -- Accept `config` prop (used to check config exists before processing) -- Accept `onResultsAdd` callback -- Remove `localStorage.getItem('testConfig')` call -- In `processImages`: call `onResultsAdd(resultsRef.current)` instead of writing localStorage directly -- The parent (Home) handles localStorage persistence - -### 5. Update ResultsPage.jsx - -- Accept `results` and `config` props -- Remove `useEffect` that reads localStorage -- Accept `onResultsClear` callback for the clear button -- `clearResults` calls `onResultsClear()` instead of `localStorage.removeItem` - -## Todo List - -- [ ] Lift config + results state to page.jsx -- [ ] Pass config/results/callbacks as props to all child pages -- [ ] Remove all direct localStorage reads from ConfigurationPage, UploadPage, ResultsPage -- [ ] Add completion indicators (checkmarks) to Navigation buttons -- [ ] Disable Upload tab when config not saved (with tooltip) -- [ ] Move Save/Reset buttons to sticky bottom bar in ConfigurationPage -- [ ] Add keyboard navigation for Phan I (A/B/C/D keys + arrow keys + backspace) -- [ ] Increase status message timeout from 3s to 5s -- [ ] Verify: changing config in ConfigPage immediately reflects in UploadPage processing -- [ ] Verify: results added in UploadPage immediately visible in ResultsPage tab - -## Success Criteria - -1. No direct localStorage reads in child components (all state flows from parent) -2. Type A/B/C/D on keyboard to set Phan I answer and auto-advance to next question -3. Upload tab visually disabled (grayed out + tooltip) when config not saved -4. Green checkmark appears on Config tab after saving, on Upload tab after processing -5. Save/Reset buttons visible at bottom of ConfigurationPage without scrolling - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| State lifting breaks page isolation | Medium | Medium | Props match current data shapes; pages still manage local UI state | -| Keyboard nav conflicts with browser shortcuts | Low | Low | Only capture A/B/C/D/arrows when Phan I grid is focused | -| ConfigurationPage exceeds 200 line limit after changes | Medium | Low | Extract PhanI keyboard grid into `src/components/phan-i-answer-grid.jsx` | - -## File Size Management - -Current line counts: -- `page.jsx`: 48 lines -> ~80 after lifting (OK) -- `Navigation.jsx`: 32 lines -> ~60 after indicators (OK) -- `ConfigurationPage.jsx`: 279 lines -> will EXCEED 200. Extract: - - `src/components/phan-i-answer-grid.jsx` (~80 lines) - keyboard-navigable Phan I grid - - `src/components/scoring-config-panel.jsx` (~40 lines) - scoring inputs -- `UploadPage.jsx`: 294 lines -> already over 200. Extract: - - `src/components/image-thumbnail.jsx` (~30 lines) - thumbnail component (also fixes the useState bug from Phase 1) -- `ResultsPage.jsx`: 345 lines -> extract: - - `src/components/student-detail-modal.jsx` (~100 lines) - modal component - -## Security Considerations - -- No new attack surface -- Config validation: ensure questionCount is within sane bounds before saving (already present with min/max on inputs) diff --git a/plans/260413-0903-ui-ux-and-detection-fixes/phase-04-workflow-features.md b/plans/260413-0903-ui-ux-and-detection-fixes/phase-04-workflow-features.md deleted file mode 100644 index d64b625..0000000 --- a/plans/260413-0903-ui-ux-and-detection-fixes/phase-04-workflow-features.md +++ /dev/null @@ -1,217 +0,0 @@ -# Phase 4: Workflow Features (P2) - -## Context - -- [Review report](../reports/ui-ux-review-260413-0856-answer-sheet-analysis.md) -- [Plan overview](./plan.md) - -## Overview - -- **Priority**: P2 -- **Status**: Pending -- **Effort**: 3h -- **Dependencies**: Phase 3 (lifted state, extracted components) - -## Key Features - -1. **Manual answer correction** in StudentDetailModal - teachers fix OCR errors -2. **Paste-from-spreadsheet** for Phan I answers - accept tab/comma-separated string -3. **Class summary statistics** - mean, median, score distribution -4. **Vietnamese debug legend** - translate English labels to Vietnamese - -## Related Code Files - -### Files to Modify - -| File | Changes | -|------|---------| -| `src/components/student-detail-modal.jsx` | Add edit mode for manual answer correction | -| `src/components/ConfigurationPage.jsx` | Add paste handler for Phan I bulk input | -| `src/components/ResultsPage.jsx` | Add statistics summary panel | -| `src/lib/debug-visualization.js` | Translate legend labels to Vietnamese | - -### Files to Create - -| File | Purpose | -|------|---------| -| `src/lib/statistics.js` | Calculate class statistics (mean, median, distribution buckets) | - -## Implementation Steps - -### 1. Manual answer correction in StudentDetailModal - -Add an "edit mode" toggle to the extracted `student-detail-modal.jsx`: - -```jsx -const [editing, setEditing] = useState(false); -const [editedResult, setEditedResult] = useState(null); - -const startEdit = () => { - setEditing(true); - setEditedResult({ ...student }); -}; - -const saveEdit = () => { - onResultUpdate(editedResult); // callback from parent via ResultsPage - setEditing(false); -}; -``` - -In edit mode: -- **Phan I**: Each answer cell becomes clickable A/B/C/D buttons (reuse phan-i-answer-grid pattern) -- **Phan II**: Dung/Sai buttons become toggleable -- **Phan III**: Text input fields for each answer -- Show "Sua" (Edit) button in modal header; when editing, show "Luu" (Save) and "Huy" (Cancel) - -**Data flow:** -``` -StudentDetailModal -> onResultUpdate(editedStudent) - -> ResultsPage -> onResultsUpdate(updatedResults) - -> Home -> setResults + localStorage.setItem -``` - -### 2. Paste-from-spreadsheet for Phan I - -Add a paste zone above the Phan I answer grid in ConfigurationPage: - -```jsx -
- - -
-``` - -Parse logic: -```js -const handlePasteAnswers = (e) => { - const text = (e.clipboardData?.getData('text') || '').trim(); - // Split by comma, tab, newline, or space - const answers = text.split(/[,\t\n\s]+/) - .map(s => s.trim().toUpperCase()) - .filter(s => ['A', 'B', 'C', 'D'].includes(s)); - - if (answers.length > 0) { - const newConfig = { ...config }; - newConfig.phanI.answers = answers.slice(0, config.phanI.questionCount); - setConfig(newConfig); - showStatus('success', `Da dan ${answers.length} dap an Phan I`); - e.preventDefault(); - } -}; -``` - -### 3. Class summary statistics - -Create `src/lib/statistics.js` (~50 lines): - -```js -/** - * Calculate class summary statistics from scored results. - * @param {Array<{score: {total: number, percentage: number}}>} results - * @returns {{ mean: number, median: number, min: number, max: number, distribution: Record }} - */ -export function calculateClassStatistics(results) { - if (results.length === 0) return null; - - const scores = results.map(r => r.score?.total ?? 0).sort((a, b) => a - b); - const percentages = results.map(r => r.score?.percentage ?? 0); - - const sum = scores.reduce((a, b) => a + b, 0); - const mean = sum / scores.length; - const median = scores.length % 2 === 0 - ? (scores[scores.length / 2 - 1] + scores[scores.length / 2]) / 2 - : scores[Math.floor(scores.length / 2)]; - - // Distribution buckets by percentage: 0-20, 20-40, 40-60, 60-80, 80-100 - const distribution = { '0-20': 0, '20-40': 0, '40-60': 0, '60-80': 0, '80-100': 0 }; - for (const pct of percentages) { - if (pct < 20) distribution['0-20']++; - else if (pct < 40) distribution['20-40']++; - else if (pct < 60) distribution['40-60']++; - else if (pct < 80) distribution['60-80']++; - else distribution['80-100']++; - } - - return { - mean: Math.round(mean * 100) / 100, - median: Math.round(median * 100) / 100, - min: scores[0], - max: scores[scores.length - 1], - count: scores.length, - distribution, - }; -} -``` - -Add statistics panel to ResultsPage above the table: - -```jsx -{results.length > 0 && ( - -)} -``` - -Display as a grid of cards: -- Si so: N -- Diem TB: mean -- Trung vi: median -- Cao nhat / Thap nhat -- Simple bar chart of distribution (CSS-only, no chart library) - -### 4. Vietnamese debug legend - -Update `debug-visualization.js` `drawLegend` function: - -```js -const entries = [ - { color: COLORS.allPositions, label: 'Tat ca vi tri' }, - { color: COLORS.studentId, label: 'So bao danh' }, - { color: COLORS.examCode, label: 'Ma de thi' }, - { color: COLORS.correct, label: 'Dung' }, - { color: COLORS.wrong, label: 'Sai' }, -]; -``` - -Also update the legend title from 'Debug Legend' to 'Chu thich'. - -## Todo List - -- [ ] Add edit mode to student-detail-modal.jsx (toggle editing, save/cancel) -- [ ] Wire onResultUpdate callback through ResultsPage -> Home -- [ ] Add paste input zone in ConfigurationPage for Phan I bulk answers -- [ ] Implement paste parsing (comma/tab/newline/space delimited, validate A/B/C/D) -- [ ] Create `src/lib/statistics.js` with calculateClassStatistics -- [ ] Add StatisticsSummary component to ResultsPage (inline, not separate file unless >50 lines) -- [ ] Translate debug-visualization.js legend to Vietnamese -- [ ] Test: paste "A,B,C,D,A,B,C,D" fills first 8 answers correctly -- [ ] Test: edit Phan I answer in modal, verify score recalculates -- [ ] Test: statistics show correct mean/median for 5+ results - -## Success Criteria - -1. Teachers can click "Sua" in detail modal, change any answer, save, see updated score -2. Paste "A,B,C,D..." from Excel into Phan I input fills all answer buttons -3. Statistics panel shows mean, median, min/max, distribution for class -4. Debug legend shows Vietnamese labels ("So bao danh", "Ma de thi", "Dung", "Sai") - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Paste format varies across Excel versions / locales | Medium | Low | Support multiple delimiters (comma, tab, newline, space) | -| Manual correction without audit trail | Low | Medium | Future: add edit history. For now, teachers accept this trade-off | -| Statistics calculation wrong for edge cases | Low | Low | Handle empty results, single result, all-same-score | - -## Security Considerations - -- Paste input sanitized: only accept characters matching A/B/C/D -- Manual edits validated against allowed answer formats before saving -- No external data transmitted diff --git a/plans/260413-0903-ui-ux-and-detection-fixes/plan.md b/plans/260413-0903-ui-ux-and-detection-fixes/plan.md deleted file mode 100644 index 8d636d0..0000000 --- a/plans/260413-0903-ui-ux-and-detection-fixes/plan.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "UI/UX & Detection Pipeline Fixes" -description: "Fix critical bugs, correct sheet layout coordinates, add perspective correction, and improve UX for Vietnamese THPT answer sheet scoring app" -status: pending -priority: P1 -effort: 12h -branch: main -tags: [bugfix, detection, ux, opencv] -created: 2026-04-13 ---- - -# UI/UX & Detection Pipeline Fixes - -## Context - -Review report: `plans/reports/ui-ux-review-260413-0856-answer-sheet-analysis.md` - -The chambai app scores Vietnamese THPT answer sheets using OpenCV.js. Three categories of issues were found: critical bugs (data loss), wrong detection coordinates, and UX friction for teachers. - -## Phase Overview - -| Phase | Focus | Effort | Files Modified | Status | -|-------|-------|--------|----------------|--------| -| [Phase 1](./phase-01-critical-bug-fixes.md) | P0 critical bugs (data loss, memory leak, storage overflow) | 2h | UploadPage.jsx, ImageProcessor.jsx, new: lib/indexed-db-store.js | Pending | -| [Phase 2](./phase-02-detection-pipeline-fixes.md) | Sheet layout coordinates + perspective correction + Phan III model | 4h | bubble-grid-generator.js, marker-detection.js, answer-detection.js, types.js | Pending | -| [Phase 3](./phase-03-ux-improvements.md) | State lifting, keyboard nav, navigation guards, sticky buttons | 3h | page.jsx, Navigation.jsx, ConfigurationPage.jsx, UploadPage.jsx, ResultsPage.jsx | Pending | -| [Phase 4](./phase-04-workflow-features.md) | Manual correction, paste-import, class statistics, Vietnamese debug legend | 3h | ResultsPage.jsx, ConfigurationPage.jsx, debug-visualization.js, new: lib/statistics.js | Pending | - -## Dependency Graph - -``` -Phase 1 (bugs) ──> Phase 3 (UX - depends on state lifting which replaces localStorage pattern) -Phase 2 (detection) ──> independent, can parallel with Phase 1 -Phase 3 (UX) ──> Phase 4 (workflow features use lifted state) -``` - -## Risk Matrix - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Sheet layout coordinates still wrong after fix | Medium | High | Must test with real CV1239 scan images; keep coordinates in a single constant for easy tuning | -| IndexedDB migration breaks existing localStorage data | Low | High | Phase 1 includes migration function; read from both stores, write to IndexedDB | -| Perspective correction degrades already-straight images | Low | Medium | Only apply when 4 markers detected AND skew angle > 2 degrees | -| State lifting breaks existing page isolation | Medium | Medium | Phase 3 lifts incrementally; each page still receives props matching its current interface | - -## Rollback Strategy - -- Each phase is a separate commit (or PR) -- Phase 1: revert to setTimeout-based save (lossy but functional) -- Phase 2: old SHEET_LAYOUT values are documented in phase file for revert -- Phase 3: revert state to localStorage-per-page pattern -- Phase 4: purely additive features, safe to revert - -## Success Criteria - -1. **Phase 1**: No data loss after processing 50+ images; no object URL memory leaks; IndexedDB stores debug images -2. **Phase 2**: Student ID and Exam Code detected correctly on straight CV1239 scans; Phan III returns multi-character answers -3. **Phase 3**: Keyboard-driven Phan I input; Upload tab blocked without config; step indicators visible -4. **Phase 4**: Teachers can correct answers in results modal; paste 40 answers from spreadsheet diff --git a/plans/reports/researcher-260412-1948-vietnam-answer-sheet-specifications.md b/plans/reports/researcher-260412-1948-vietnam-answer-sheet-specifications.md deleted file mode 100644 index 6152acf..0000000 --- a/plans/reports/researcher-260412-1948-vietnam-answer-sheet-specifications.md +++ /dev/null @@ -1,113 +0,0 @@ -# Vietnamese High School Answer Sheet (Phiếu Trả Lời Trắc Nghiệm) - Technical Specifications - -## Sheet Structure & Sections - -**3 Main Parts:** -1. **Phần I** - Multiple choice questions (40-50 questions per sheet typical) -2. **Phần II** - True/False section (Đúng/Sai) -3. **Phần III** - Short answer section (Tự luận số) - -**2025 Format Updates** (per Công văn 1239/BGDĐT): -- Exam code: 4 digits (changed from 3 digits) -- Student ID (Số báo danh): 8 digits (changed from 6 digits) -- Layout markers: Black squares (▪) instead of black horizontal lines - -## Physical Specifications - -**Paper Size:** A4 standard (also available in A5/A6 reduced formats) - -**Printing Requirements:** -- Offset printing (laser/inkjet acceptable; NOT photocopy/color printing) -- Red color specification: 100% TRAM density for lines/circles/text, 10% TRAM for background -- Positioning tolerance: Minimum 4.5mm margin from paper edge - -## Alignment & Positioning Markers - -**Corner Markers:** Black squares at all 4 corners (critical for OMR scanning) - -**Registration Points:** -- Test code (Mã đề) positioning points -- Student ID (Số báo danh) positioning points -- Vertical alignment guides - -Standard OMR sheets have index points at 4 corners—most important parameter for scanner alignment. - -## Bubble Specifications - -**Bubble Size:** 10-14 points optimal (per international OMR standards) -- Smaller bubbles increase recognition error -- Larger bubbles may touch adjacent bubbles - -**Spacing Requirements:** -- Minimum 3/8 inch clearance around bubbles for scanning -- No visible lines within bubble zones -- Consistent horizontal/vertical spacing between answer options (A, B, C, D typically) - -**Marking Standards:** -- Students use black pencil only -- Single bubble per question -- Bubbles must be fully darkened for recognition - -## Student ID Layout - -**Section:** Top of sheet (standardized position) -- 8 individual digit fields arranged horizontally or in 2 rows of 4 -- Each digit has: blank space for writing + bubble array below for marking (0-9 bubbles) -- Students write digit in blank, then fill corresponding bubble - -## Exam Code Layout - -**Section:** Top of sheet (separate from Student ID) -- 4 digit fields with same write + bubble structure -- Positioned adjacent to Student ID section - -## Key Technical Insights for Bubble Detection - -**Alignment:** Corner squares enable perspective correction before bubble analysis - -**Detection Approach:** Contour-based detection more reliable than circle detection (HoughCircles unreliable for scanned sheets) - -**Workflow:** -1. Perspective transform using corner markers -2. Identify question regions by alignment guides -3. Extract contours, filter by aspect ratio/size -4. Group into rows (one row = one question) -5. Mark darkest bubble in each row as selected answer - -**Data Extraction:** After bubble detection, cross-reference marked bubbles against printed answer key - -## Spacing Standards - -Per international OMR: -- Horizontal spacing between answer columns: consistent (typically 8-10mm) -- Vertical spacing between questions: consistent (typically 10-15mm) -- Left/right margins: 1cm minimum -- Top/bottom margins: 1-1.5cm - -## Critical Notes for Implementation - -✓ **Must preserve corner markers** - removing them breaks OMR scanning -✓ **Red printed areas** - low contrast makes optical detection easier (not black) -✓ **8 digits for ID + 4 digits for exam code** - account for all 12 input fields -✓ **Bubble size consistency** - varies by manufacturer; measure from actual template -✓ **Lighting tolerance** - white/light gray background acceptable, but not colored paper - ---- - -## Sources - -- [Official 2025 Answer Sheet Usage Guide](https://xaydungchinhsach.chinhphu.vn/huong-dan-su-dung-phieu-tra-loi-trac-nghiem-thi-tot-nghiep-thpt-nam-2025-119250324144831872.htm) -- [Official Dispatch 1239/BGDĐT Technical Specifications](https://thuvienphapluat.vn/chinh-sach-phap-luat-moi/vn/ho-tro-phap-luat/chinh-sach-moi/81377/cong-van-1239-huong-dan-su-dung-phieu-tra-loi-trac-nghiem-thi-tot-nghiep-thpt-nam-2025) -- [TNMaker Answer Sheet Templates](https://tnmaker.net/phieu-trac-nghiem/) -- [PyImageSearch: Bubble Sheet Scanner with OpenCV](https://pyimagesearch.com/2016/10/03/bubble-sheet-multiple-choice-scanner-and-test-grader-using-omr-python-and-opencv/) -- [OMR Bubble Size Standards](https://www.addmengroup.com/omr-design/omr-bubble-size.htm) -- [General OMR Answer Sheet Specifications](https://www.yoctel.com/blogs/what-is-the-omr-answer-sheet) - ---- - -## Unresolved Questions - -1. **Exact bubble diameter in mm** - Standards specify 10-14 points but actual Vietnamese templates may vary; need to measure from official PDF template -2. **Precise spacing in mm between columns/rows** - Guidelines exist but exact measurements depend on question count per sheet -3. **Background color tolerance** - Can background be light gray or must it be pure white? -4. **Handwriting area dimensions** - For student ID digit writing field width/height specifications diff --git a/plans/reports/ui-ux-review-260413-0856-answer-sheet-analysis.md b/plans/reports/ui-ux-review-260413-0856-answer-sheet-analysis.md deleted file mode 100644 index ca2ace9..0000000 --- a/plans/reports/ui-ux-review-260413-0856-answer-sheet-analysis.md +++ /dev/null @@ -1,176 +0,0 @@ -# UI/UX Review & Answer Sheet Analysis Report - -**Date:** 2026-04-13 | **Branch:** main | **Status:** Complete - ---- - -## Part 1: Answer Sheet Layout Analysis (CV1239/BGDDT 2025) - -### Sheet Structure (Page 0 - Answer Sheet) - -The official Vietnamese THPT answer sheet has the following layout from top to bottom: - -#### Header Section (top ~15% of sheet) -- Title: **"PHIEU TRA LOI TRAC NGHIEM"** (centered, bold) -- Fields: Ky thi, Mon thi, Ngay thi -- **Item 7: "So bao danh"** (Student ID) - top-right area, 8-digit grid -- **Item 8: "Ma de thi"** (Exam Code) - far top-right, 4-digit grid -- Left side: Examiner signature boxes (Giam thi 1, Giam thi 2) -- Items 1-6: Exam metadata (Hoi dong thi, Diem thi, Phong so, Ho va ten thi sinh, Ngay sinh, Chu ky thi sinh) -- Note at bottom of header: *"Chu y: Thi sinh can doc ky huong dan o mat sau Phieu nay."* - -#### PHAN I Section (~30% of sheet, middle area) -- Label: **"PHAN I"** (bold, left-aligned) -- Layout: **4 columns of 10 questions each = 40 questions total** -- Column 1: Questions 1-10 -- Column 2: Questions 11-20 -- Column 3: Questions 21-30 -- Column 4: Questions 31-40 -- Each question has **4 bubbles: A, B, C, D** (circles, inline) - -#### PHAN II Section (~15% of sheet) -- Label: **"PHAN II"** (bold, left-aligned) -- Layout: **8 questions (Cau 1 to Cau 8) arranged in a single row** -- Each question has: header "Cau N / Dung Sai" -- Sub-options: **a), b), c), d)** each with **2 bubbles: Dung (True) / Sai (False)** -- Arranged as 4 questions per visual group, spanning full width - -#### PHAN III Section (~25% of sheet, bottom) -- Label: **"PHAN III"** (bold, left-aligned) -- Layout: **6 questions (Cau 1 to Cau 6) in 6 columns** -- Each question has a column for writing a numerical answer -- Below each writing area: **10 rows of bubbles (digits 0-9)** -- There's also a **"-" (negative sign) bubble** at the top -- And a **"," (decimal comma) bubble** - -#### Corner/Alignment Markers -- **4 corner markers**: solid black squares at corners of the answer area -- Located at: top-left, top-right, bottom-left, bottom-right of the bubble grid area -- NOT at the paper corners - they frame the scoring area specifically - -### Page 1 - Instructions -- Shows how to fill bubbles with 4 example figures (Hinh 1-4) -- Hinh 1: Phan I example (fill one circle per question) -- Hinh 2: Phan II example (fill Dung/Sai per sub-option) -- Hinh 3: Phan III negative number example ("-1.5") -- Hinh 4: Phan III positive number example ("1.5") - -### Key Layout Proportions (for OpenCV calibration) - -Based on visual analysis of the 1638x2339 pixel rendering: - -| Section | Approx Y Start (%) | Approx Y End (%) | Height (%) | -|---------|-------------------|------------------|------------| -| Header + SBD/Ma de | 0% | 28% | 28% | -| PHAN I | 28% | 55% | 27% | -| PHAN II | 55% | 72% | 17% | -| PHAN III | 72% | 98% | 26% | - -| Section | Approx X Start (%) | Approx X End (%) | Width (%) | -|---------|-------------------|------------------|-----------| -| SBD (8 digits) | 58% | 82% | 24% | -| Ma de thi (4 digits) | 82% | 95% | 13% | -| PHAN I (all 4 cols) | 3% | 97% | 94% | -| PHAN II (all 8 qs) | 3% | 97% | 94% | -| PHAN III (all 6 qs) | 3% | 97% | 94% | - -### Critical Observations vs Current Code - -**Current `bubble-grid-generator.js` SHEET_LAYOUT discrepancies:** - -1. **Student ID position is WRONG**: Code has `{ x: 0.65, y: 0.02, w: 0.25, h: 0.18 }` but actual sheet puts SBD at ~`{ x: 0.58, y: 0.05, w: 0.24, h: 0.20 }` (within the header area, not at the very top) -2. **Exam Code position is WRONG**: Code has `{ x: 0.45, y: 0.02 }` but actual sheet puts Ma de thi to the RIGHT of SBD at ~`{ x: 0.82, y: 0.05 }` -3. **PHAN I Y position needs adjustment**: Code has `y: 0.25` but actual is closer to `y: 0.28` -4. **PHAN II Y position needs adjustment**: Code has `y: 0.58` but actual is closer to `y: 0.55` -5. **PHAN III**: Code has `y: 0.76` but actual starts at ~`y: 0.72` -6. **PHAN III missing features**: The actual sheet has **negative sign "-"** and **decimal comma ","** bubbles that the code doesn't handle -7. **PHAN III is multi-digit per question**: Each question can have multiple digits (e.g., "-1.5" = 4 characters), not just a single digit. Current code treats each question as a single digit selection. - ---- - -## Part 2: UI/UX Review - -### Critical Bugs (P0) - -| # | Issue | Location | Impact | -|---|-------|----------|--------| -| 1 | **localStorage race condition** | `UploadPage.jsx:99-104` | `setTimeout(100)` hack for saving results - results can be silently lost | -| 2 | **`useState` misused as `useEffect`** | `UploadPage.jsx:271-275` | `ImageThumbnail` never updates URL on prop change, never revokes old URLs (memory leak) | -| 3 | **localStorage overflow risk** | `UploadPage.jsx:101-103` | Debug image data URLs (~1-2MB each) stored per result. 50+ students = blown 5MB limit | - -### UX Issues (P1) - -| # | Issue | Impact | -|---|-------|--------| -| 4 | Save/Reset buttons at TOP of ConfigurationPage | Teachers must scroll up after filling 40+ answers to save | -| 5 | No validation before Upload tab | Teachers can try processing without saving config | -| 6 | No step-completion indicators on Navigation | Teachers don't know which steps are done | -| 7 | No keyboard navigation for Phan I answer input | Inputting 40 answers requires 40 mouse clicks minimum | -| 8 | Debug legend in English, rest in Vietnamese | Inconsistent language | -| 9 | Status messages auto-dismiss in 3 seconds | Too fast for non-tech-savvy teachers | -| 10 | No manual answer correction in results | Teachers can't fix OCR errors | - -### Workflow Improvements (P2) - -| # | Suggestion | Benefit | -|---|-----------|---------| -| 11 | Lift state to parent component or use React Context | Eliminates localStorage sync bugs between tabs | -| 12 | Auto-advance prompts after key actions | Guides teachers through the 3-step flow | -| 13 | Paste-from-spreadsheet for Phan I answers | Teachers often have answer keys in Excel | -| 14 | Class summary statistics (mean, median, distribution) | Essential for teacher reporting | -| 15 | Phan II: distinguish "unanswered" vs "answered Sai" visually | Currently ambiguous | - -### Visual Polish (P3) - -| # | Suggestion | -|---|-----------| -| 16 | Extract shared StatusMessage component (duplicated in Config + Upload) | -| 17 | Increase touch targets (w-8 h-8 = 32px, should be 44px for mobile/tablet) | -| 18 | Add collapsible help text on each page | -| 19 | Replace native `confirm()` with inline confirmation + undo | -| 20 | Dark mode deprioritized - low value for target users (teachers in school settings) | - ---- - -## Part 3: Core Logic Analysis - OpenCV Detection Pipeline - -### Current Pipeline -``` -Image -> Grayscale + GaussianBlur + AdaptiveThreshold + MorphClose - -> FindContours -> Filter square-ish contours -> Pick 4 corners - -> Generate bubble grid from proportional layout - -> MeasureBubbleFill per bubble (mean intensity in ROI) - -> Pick best-filled bubble per question group -``` - -### Strengths -- Solid preprocessing chain (adaptive threshold handles variable lighting) -- Morphological closing fills bubble gaps -- Fill measurement approach (mean intensity) is proven for bubble sheets -- Fallback to image bounds when markers aren't detected - -### Weaknesses & Improvement Areas - -1. **No perspective correction/deskewing**: Markers are detected but only used for bounding box. Should apply `cv.getPerspectiveTransform` + `cv.warpPerspective` to correct camera angle/rotation. - -2. **Bubble size is hardcoded ratio (0.012)**: Should be derived from marker size or from the distance between detected marker corners. - -3. **No image quality validation**: No check for blur, resolution, exposure. Should warn if image is too small or too blurry. - -4. **FILL_THRESHOLD = 0.35 is static**: Should be adaptive - calculate the mean of "definitely empty" bubbles and "definitely filled" bubbles per sheet, then set threshold between them (Otsu-like approach per sheet). - -5. **PHAN III model is wrong**: Current code assumes 1 digit per question. Actual sheet supports multi-character answers (e.g., "-1.5" needs 4 character positions including sign and decimal). - -6. **No multi-answer detection for PHAN I**: If a student fills 2 bubbles for one question, current code just picks the darkest. Should flag as "multiple answers" (invalid). - -7. **No confidence reporting per answer**: The detection returns answers but no per-answer confidence. Teachers need to see which answers are low-confidence for manual review. - ---- - -## Unresolved Questions - -1. How large are debug visualization data URLs in practice? If >1MB each, IndexedDB migration is P0. -2. Is there a standard Vietnamese THPT answer key format (Excel template) for paste-import? -3. Do teachers need to support multiple exam codes (answer keys) simultaneously? -4. PHAN III: what is the maximum number of characters per answer? The sheet shows sign + digits + decimal. -5. Should the app support camera capture directly (mobile use case) vs only file upload?