From ec3d400e8a97cf0ce18a078455a246b21b4d4dd3 Mon Sep 17 00:00:00 2001 From: renolation Date: Sun, 12 Apr 2026 18:54:59 +0700 Subject: [PATCH] phase 2 --- .env.example | 10 +- .../7df6e328-a904454bbfcd9697e978020268697fc8 | 258 ++++ Claude.md | 460 ++++-- eslint.config.js | 2 +- index.html | 6 +- src/components/AppHeader.tsx | 32 + src/components/CircularProgress.tsx | 37 + src/components/FlashCard.tsx | 64 +- src/components/MobileNav.tsx | 41 + src/components/Sidebar.tsx | 84 ++ src/components/UserMenu.tsx | 113 ++ src/components/auth/AuthModal.tsx | 96 ++ src/components/auth/LoginForm.tsx | 82 ++ src/components/auth/RegisterForm.tsx | 94 ++ src/data/mock-data.ts | 157 ++ src/hooks/use-auth.ts | 5 + src/hooks/use-questions.ts | 39 +- src/hooks/use-require-auth.ts | 17 + src/hooks/use-vocab.ts | 30 +- src/hooks/use-writing-check.ts | 93 +- src/index.css | 62 +- src/lib/progress-service.ts | 71 + src/lib/supabase.ts | 7 +- src/pages/Home.tsx | 218 ++- src/pages/Login.tsx | 34 + src/pages/Register.tsx | 35 + src/pages/TestResult.tsx | 221 ++- src/pages/TestSession.tsx | 205 ++- src/pages/ToeicPractice.tsx | 119 +- src/pages/Vocabulary.tsx | 252 +++- src/pages/WritingChecker.tsx | 238 ++- src/routes/__root.tsx | 38 +- src/routes/auth.login.tsx | 6 + src/routes/auth.register.tsx | 6 + src/store/auth-modal-store.ts | 15 + src/store/auth-store.ts | 71 + src/store/test-store.ts | 62 +- src/store/vocab-store.ts | 34 +- src/types/index.ts | 60 + stitch-exports/screen-01-home/DESIGN.md | 68 + stitch-exports/screen-01-home/design.html | 274 ++++ stitch-exports/screen-01-home/design.png | Bin 0 -> 85454 bytes .../screen-02-part-select/DESIGN.md | 63 + .../screen-02-part-select/design.html | 374 +++++ .../screen-02-part-select/design.png | Bin 0 -> 62068 bytes stitch-exports/screen-03-exam/DESIGN.md | 69 + stitch-exports/screen-03-exam/design.html | 233 +++ stitch-exports/screen-03-exam/design.png | Bin 0 -> 35559 bytes stitch-exports/screen-04-results/DESIGN.md | 64 + stitch-exports/screen-04-results/design.html | 317 ++++ stitch-exports/screen-04-results/design.png | Bin 0 -> 60382 bytes stitch-exports/screen-05-writing/DESIGN.md | 72 + stitch-exports/screen-05-writing/design.html | 270 ++++ stitch-exports/screen-05-writing/design.png | Bin 0 -> 77074 bytes stitch-exports/screen-06-flashcard/DESIGN.md | 57 + .../screen-06-flashcard/design.html | 317 ++++ stitch-exports/screen-06-flashcard/design.png | Bin 0 -> 48909 bytes stitch-exports/toeic-app-design.html | 1290 +++++++++++++++++ supabase/.temp/cli-latest | 1 + supabase/.temp/gotrue-version | 1 + supabase/.temp/pooler-url | 1 + supabase/.temp/postgres-version | 1 + supabase/.temp/project-ref | 1 + supabase/.temp/rest-version | 1 + supabase/.temp/storage-migration | 1 + supabase/.temp/storage-version | 1 + supabase/functions/writing-check/index.ts | 73 + supabase/migrations/001_user_progress.sql | 53 + supabase/postman-collection.json | 166 +++ supabase/schema.sql | 33 + supabase/seed.sql | 946 ++++++++++++ 71 files changed, 7888 insertions(+), 333 deletions(-) create mode 100644 .tanstack/tmp/7df6e328-a904454bbfcd9697e978020268697fc8 create mode 100644 src/components/AppHeader.tsx create mode 100644 src/components/CircularProgress.tsx create mode 100644 src/components/MobileNav.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/UserMenu.tsx create mode 100644 src/components/auth/AuthModal.tsx create mode 100644 src/components/auth/LoginForm.tsx create mode 100644 src/components/auth/RegisterForm.tsx create mode 100644 src/data/mock-data.ts create mode 100644 src/hooks/use-auth.ts create mode 100644 src/hooks/use-require-auth.ts create mode 100644 src/lib/progress-service.ts create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/Register.tsx create mode 100644 src/routes/auth.login.tsx create mode 100644 src/routes/auth.register.tsx create mode 100644 src/store/auth-modal-store.ts create mode 100644 src/store/auth-store.ts create mode 100644 src/types/index.ts create mode 100644 stitch-exports/screen-01-home/DESIGN.md create mode 100644 stitch-exports/screen-01-home/design.html create mode 100644 stitch-exports/screen-01-home/design.png create mode 100644 stitch-exports/screen-02-part-select/DESIGN.md create mode 100644 stitch-exports/screen-02-part-select/design.html create mode 100644 stitch-exports/screen-02-part-select/design.png create mode 100644 stitch-exports/screen-03-exam/DESIGN.md create mode 100644 stitch-exports/screen-03-exam/design.html create mode 100644 stitch-exports/screen-03-exam/design.png create mode 100644 stitch-exports/screen-04-results/DESIGN.md create mode 100644 stitch-exports/screen-04-results/design.html create mode 100644 stitch-exports/screen-04-results/design.png create mode 100644 stitch-exports/screen-05-writing/DESIGN.md create mode 100644 stitch-exports/screen-05-writing/design.html create mode 100644 stitch-exports/screen-05-writing/design.png create mode 100644 stitch-exports/screen-06-flashcard/DESIGN.md create mode 100644 stitch-exports/screen-06-flashcard/design.html create mode 100644 stitch-exports/screen-06-flashcard/design.png create mode 100644 stitch-exports/toeic-app-design.html create mode 100644 supabase/.temp/cli-latest create mode 100644 supabase/.temp/gotrue-version create mode 100644 supabase/.temp/pooler-url create mode 100644 supabase/.temp/postgres-version create mode 100644 supabase/.temp/project-ref create mode 100644 supabase/.temp/rest-version create mode 100644 supabase/.temp/storage-migration create mode 100644 supabase/.temp/storage-version create mode 100644 supabase/functions/writing-check/index.ts create mode 100644 supabase/migrations/001_user_progress.sql create mode 100644 supabase/postman-collection.json create mode 100644 supabase/schema.sql create mode 100644 supabase/seed.sql diff --git a/.env.example b/.env.example index 0839f12..0bec4a1 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,10 @@ +# Supabase — https://supabase.com/dashboard/project/_/settings/api VITE_SUPABASE_URL=https://your-project.supabase.co -VITE_SUPABASE_ANON_KEY=your-anon-key-here +VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... +# Alternative key name (both are supported) +# VITE_SUPABASE_ANON_KEY=eyJ... + +# GLM API — https://open.bigmodel.cn/usercenter/apikeys +# Used by the writing-check Supabase Edge Function (server-side only, never expose in frontend) +# Deploy to Supabase with: supabase secrets set GLM_API_KEY= +GLM_API_KEY=your_glm_api_key_here diff --git a/.tanstack/tmp/7df6e328-a904454bbfcd9697e978020268697fc8 b/.tanstack/tmp/7df6e328-a904454bbfcd9697e978020268697fc8 new file mode 100644 index 0000000..90213ad --- /dev/null +++ b/.tanstack/tmp/7df6e328-a904454bbfcd9697e978020268697fc8 @@ -0,0 +1,258 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as WritingRouteImport } from './routes/writing' +import { Route as VocabRouteImport } from './routes/vocab' +import { Route as ToeicRouteImport } from './routes/toeic' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ToeicIndexRouteImport } from './routes/toeic.index' +import { Route as ToeicSessionRouteImport } from './routes/toeic.session' +import { Route as ToeicResultRouteImport } from './routes/toeic.result' +import { Route as AuthRegisterRouteImport } from './routes/auth.register' +import { Route as AuthLoginRouteImport } from './routes/auth.login' +import { Route as ToeicPartPartIdRouteImport } from './routes/toeic.part.$partId' + +const WritingRoute = WritingRouteImport.update({ + id: '/writing', + path: '/writing', + getParentRoute: () => rootRouteImport, +} as any) +const VocabRoute = VocabRouteImport.update({ + id: '/vocab', + path: '/vocab', + getParentRoute: () => rootRouteImport, +} as any) +const ToeicRoute = ToeicRouteImport.update({ + id: '/toeic', + path: '/toeic', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ToeicIndexRoute = ToeicIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ToeicRoute, +} as any) +const ToeicSessionRoute = ToeicSessionRouteImport.update({ + id: '/session', + path: '/session', + getParentRoute: () => ToeicRoute, +} as any) +const ToeicResultRoute = ToeicResultRouteImport.update({ + id: '/result', + path: '/result', + getParentRoute: () => ToeicRoute, +} as any) +const AuthRegisterRoute = AuthRegisterRouteImport.update({ + id: '/auth/register', + path: '/auth/register', + getParentRoute: () => rootRouteImport, +} as any) +const AuthLoginRoute = AuthLoginRouteImport.update({ + id: '/auth/login', + path: '/auth/login', + getParentRoute: () => rootRouteImport, +} as any) +const ToeicPartPartIdRoute = ToeicPartPartIdRouteImport.update({ + id: '/part/$partId', + path: '/part/$partId', + getParentRoute: () => ToeicRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/toeic': typeof ToeicRouteWithChildren + '/vocab': typeof VocabRoute + '/writing': typeof WritingRoute + '/auth/login': typeof AuthLoginRoute + '/auth/register': typeof AuthRegisterRoute + '/toeic/result': typeof ToeicResultRoute + '/toeic/session': typeof ToeicSessionRoute + '/toeic/': typeof ToeicIndexRoute + '/toeic/part/$partId': typeof ToeicPartPartIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/vocab': typeof VocabRoute + '/writing': typeof WritingRoute + '/auth/login': typeof AuthLoginRoute + '/auth/register': typeof AuthRegisterRoute + '/toeic/result': typeof ToeicResultRoute + '/toeic/session': typeof ToeicSessionRoute + '/toeic': typeof ToeicIndexRoute + '/toeic/part/$partId': typeof ToeicPartPartIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/toeic': typeof ToeicRouteWithChildren + '/vocab': typeof VocabRoute + '/writing': typeof WritingRoute + '/auth/login': typeof AuthLoginRoute + '/auth/register': typeof AuthRegisterRoute + '/toeic/result': typeof ToeicResultRoute + '/toeic/session': typeof ToeicSessionRoute + '/toeic/': typeof ToeicIndexRoute + '/toeic/part/$partId': typeof ToeicPartPartIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/toeic' + | '/vocab' + | '/writing' + | '/auth/login' + | '/auth/register' + | '/toeic/result' + | '/toeic/session' + | '/toeic/' + | '/toeic/part/$partId' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/vocab' + | '/writing' + | '/auth/login' + | '/auth/register' + | '/toeic/result' + | '/toeic/session' + | '/toeic' + | '/toeic/part/$partId' + id: + | '__root__' + | '/' + | '/toeic' + | '/vocab' + | '/writing' + | '/auth/login' + | '/auth/register' + | '/toeic/result' + | '/toeic/session' + | '/toeic/' + | '/toeic/part/$partId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ToeicRoute: typeof ToeicRouteWithChildren + VocabRoute: typeof VocabRoute + WritingRoute: typeof WritingRoute + AuthLoginRoute: typeof AuthLoginRoute + AuthRegisterRoute: typeof AuthRegisterRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/writing': { + id: '/writing' + path: '/writing' + fullPath: '/writing' + preLoaderRoute: typeof WritingRouteImport + parentRoute: typeof rootRouteImport + } + '/vocab': { + id: '/vocab' + path: '/vocab' + fullPath: '/vocab' + preLoaderRoute: typeof VocabRouteImport + parentRoute: typeof rootRouteImport + } + '/toeic': { + id: '/toeic' + path: '/toeic' + fullPath: '/toeic' + preLoaderRoute: typeof ToeicRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/toeic/': { + id: '/toeic/' + path: '/' + fullPath: '/toeic/' + preLoaderRoute: typeof ToeicIndexRouteImport + parentRoute: typeof ToeicRoute + } + '/toeic/session': { + id: '/toeic/session' + path: '/session' + fullPath: '/toeic/session' + preLoaderRoute: typeof ToeicSessionRouteImport + parentRoute: typeof ToeicRoute + } + '/toeic/result': { + id: '/toeic/result' + path: '/result' + fullPath: '/toeic/result' + preLoaderRoute: typeof ToeicResultRouteImport + parentRoute: typeof ToeicRoute + } + '/auth/register': { + id: '/auth/register' + path: '/auth/register' + fullPath: '/auth/register' + preLoaderRoute: typeof AuthRegisterRouteImport + parentRoute: typeof rootRouteImport + } + '/auth/login': { + id: '/auth/login' + path: '/auth/login' + fullPath: '/auth/login' + preLoaderRoute: typeof AuthLoginRouteImport + parentRoute: typeof rootRouteImport + } + '/toeic/part/$partId': { + id: '/toeic/part/$partId' + path: '/part/$partId' + fullPath: '/toeic/part/$partId' + preLoaderRoute: typeof ToeicPartPartIdRouteImport + parentRoute: typeof ToeicRoute + } + } +} + +interface ToeicRouteChildren { + ToeicResultRoute: typeof ToeicResultRoute + ToeicSessionRoute: typeof ToeicSessionRoute + ToeicIndexRoute: typeof ToeicIndexRoute + ToeicPartPartIdRoute: typeof ToeicPartPartIdRoute +} + +const ToeicRouteChildren: ToeicRouteChildren = { + ToeicResultRoute: ToeicResultRoute, + ToeicSessionRoute: ToeicSessionRoute, + ToeicIndexRoute: ToeicIndexRoute, + ToeicPartPartIdRoute: ToeicPartPartIdRoute, +} + +const ToeicRouteWithChildren = ToeicRoute._addFileChildren(ToeicRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ToeicRoute: ToeicRouteWithChildren, + VocabRoute: VocabRoute, + WritingRoute: WritingRoute, + AuthLoginRoute: AuthLoginRoute, + AuthRegisterRoute: AuthRegisterRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/Claude.md b/Claude.md index 7f0d8d4..1198153 100644 --- a/Claude.md +++ b/Claude.md @@ -1,7 +1,8 @@ # Claude Project Context — English Learning App (TOEIC Focus) -> File này dùng để cung cấp context đầy đủ cho Claude khi làm việc với dự án. +> File này cung cấp context đầy đủ cho Claude khi làm việc với dự án. > Cập nhật file này mỗi khi có quyết định kiến trúc mới. +> **Last updated**: Merged all decisions — Phase 1 scaffold done, Phase 2–4 planned --- @@ -11,15 +12,7 @@ **Target users**: Sinh viên, người đi làm tại Việt Nam cần TOEIC, IELTS, hoặc học tiếng Anh tổng quát **Focus chính**: TOEIC (mở rộng market), sau đó IELTS **Giai đoạn hiện tại**: Phase 1 — MVP, validate market - ---- - -## Mục tiêu Phase 1 - -- Ra sản phẩm web dùng thử được, **không cần đăng nhập** -- Validate 2 tính năng cốt lõi: luyện đề TOEIC + AI writing checker -- Test xem có người dùng thật, có traction không -- **Không over-engineer** — Supabase tạm thời, đổi sau nếu có traction +**Roadmap**: 4 phases — MVP → Auth & Progress → Speaking AI → Full TOEIC --- @@ -28,59 +21,66 @@ ### Frontend | Layer | Tech | Ghi chú | |---|---|---| -| Framework | **React** (Vite) | | -| Routing | **TanStack Router** | Type-safe routing | +| Framework | **React** + **Vite** + **TypeScript** | | +| Routing | **TanStack Router** | File-based, type-safe | | Server state | **TanStack Query** | Fetch, cache, sync API data | -| Client state | **Zustand** | UI state, localStorage sync | -| Styling | **Tailwind CSS** | Mobile-first | +| Client state | **Zustand** | UI state + localStorage persist | +| Styling | **Tailwind CSS** | Desktop-first | | UI Components | **shadcn/ui** | Dùng khi cần, không bắt buộc | -### Backend (Phase 1 — tạm thời) -| Layer | Tech | Ghi chú | -|---|---|---| -| Database | **Supabase** (PostgreSQL managed) | Free tier, migrate sau | -| API | **Supabase JS SDK** | Gọi thẳng từ React | -| Server functions | **Supabase Edge Functions** (Deno) | Xử lý AI API call, giấu key | - -> ⚠️ **Supabase chỉ dùng Phase 1** để ra sản phẩm nhanh. -> Phase 2 migrate sang **NestJS + PostgreSQL native** khi có traction. -> Schema PostgreSQL thiết kế chuẩn ngay từ đầu để migrate không đau. - -### Backend (Phase 2 — kế hoạch) -| Layer | Tech | +### Design System (từ Stitch export) +| Token | Value | |---|---| -| Framework | **NestJS** | -| ORM | **Prisma** hoặc **TypeORM** | -| Database | **PostgreSQL** (self-hosted) | -| Auth | **JWT** + Google OAuth + Zalo OAuth | -| Mobile | **Flutter** (iOS + Android) | +| Font | Plus Jakarta Sans + Material Symbols Outlined | +| Primary | #2563EB | +| Success | #16A34A | +| Danger | #DC2626 | +| Background | #F8FAFC | +| Card | #FFFFFF | +| Border radius | 12–16px | +| Shadow | soft, subtle | + +### Responsive Layout +| Breakpoint | Layout | +|---|---| +| Desktop (1280px) | Sidebar trái cố định (240px) + main content — **LAYOUT CHÍNH** | +| Tablet (768px) | Sidebar thu gọn icon-only | +| Mobile (375px) | Ẩn sidebar, hiện bottom navigation bar | + +### Backend +| Phase | Tech | Ghi chú | +|---|---|---| +| Phase 1 | **Supabase** (PostgreSQL + Edge Functions + JS SDK) | Tạm thời, migrate sau | +| Phase 2+ | **NestJS** + **PostgreSQL** native | Khi có traction | + +> ⚠️ Supabase chỉ dùng Phase 1. Schema PostgreSQL thiết kế chuẩn ngay từ đầu để migrate không đau. ### AI | Layer | Tech | Ghi chú | |---|---|---| -| Provider | **GLM (Z.ai API)** — `open.bigmodel.cn` | Rẻ, OpenAI-compatible | -| Model | **GLM-4** hoặc **GLM-4.7** | Test chất lượng chấm writing | -| Fallback | OpenAI / Claude API | Nếu GLM không đủ chất lượng | - -> GLM API tương thích OpenAI format → swap provider không cần đổi code. +| Provider | **GLM (Z.ai API)** | Rẻ, OpenAI-compatible format | +| Endpoint | `open.bigmodel.cn/api/paas/v4` | | +| Model | GLM-4 / GLM-4.7 | | +| Fallback | OpenAI / Claude API | Swap dễ vì API compatible | +| Gọi từ | Supabase Edge Function (Phase 1) → NestJS service (Phase 2+) | Giấu API key | ### Deploy | Layer | Tech | |---|---| -| Frontend | **Self-hosted server** (có sẵn) | -| Backend | **Self-hosted server** (có sẵn) | -| Database | Supabase Cloud (Phase 1) → self-hosted PostgreSQL (Phase 2) | +| Frontend | Self-hosted server (có sẵn) | +| Backend | Self-hosted server (có sẵn) | +| Database | Supabase Cloud (Phase 1) → self-hosted PostgreSQL (Phase 2+) | --- -## Database Schema (PostgreSQL — Phase 1) +## Database Schema (PostgreSQL) ```sql -- Câu hỏi TOEIC CREATE TABLE questions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), part INT NOT NULL, -- 1 đến 7 - type TEXT, -- photo, q&a, incomplete_sentence, etc. + type TEXT, -- photo | q&a | incomplete_sentence | etc. content TEXT NOT NULL, -- nội dung câu hỏi / đoạn văn options JSONB, -- ["A. ...", "B. ...", "C. ...", "D. ..."] answer TEXT NOT NULL, -- "A" @@ -101,158 +101,286 @@ CREATE TABLE vocab ( created_at TIMESTAMPTZ DEFAULT now() ); --- Phase 2 sẽ thêm: users, user_progress, writing_submissions, test_sessions +-- Phase 2+ +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + password TEXT NOT NULL, -- hashed + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE user_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + type TEXT, -- test | vocab | writing + reference_id UUID, + data JSONB, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE writing_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + content TEXT NOT NULL, + feedback JSONB, + created_at TIMESTAMPTZ DEFAULT now() +); ``` --- -## API Design +## TypeScript Interfaces -### Supabase SDK (Phase 1) ```typescript -// Lấy câu hỏi theo Part -supabase.from('questions').select('*').eq('part', 1).limit(10) +interface Question { + id: string + part: number + type: string + content: string + options: string[] + answer: string + explanation: string + audioUrl?: string + imageUrl?: string +} -// Lấy từ vựng theo chủ đề -supabase.from('vocab').select('*').eq('topic', 'business') -``` +interface VocabWord { + id: string + word: string + phonetic: string + meaningVi: string + topic: 'business' | 'office' | 'travel' | 'finance' | 'hr' | 'marketing' + example: string +} -### Supabase Edge Functions -``` -POST /functions/v1/writing-check - Body : { content: string } - Return : { - score: string, // band score ước tính - grammar: string[], // lỗi ngữ pháp + gợi ý sửa - vocabulary: string[], // nhận xét từ vựng - structure: string, // nhận xét bố cục (tiếng Việt) - improved_version: string, // bài viết lại tốt hơn - summary: string // tổng nhận xét (tiếng Việt) - } +interface TestResult { + testId: string + part: number + score: number + total: number + duration: number + answers: { questionId: string; selected: string; correct: boolean }[] + completedAt: Date +} + +interface WritingFeedback { + score: string + grammar: string[] + vocabulary: string[] + structure: string + improvedVersion: string + summary: string +} + +// Phase 2+ +interface User { + id: string + email: string + name: string + createdAt: Date +} ``` --- -## Cấu trúc thư mục (React) +## Cấu trúc thư mục ``` src/ -├── pages/ -│ ├── Home.tsx ← Landing page + CTA -│ ├── ToeicPractice.tsx ← Chọn Part để luyện -│ ├── TestSession.tsx ← Làm bài (timer + câu hỏi) -│ ├── TestResult.tsx ← Kết quả + giải thích đáp án -│ ├── WritingChecker.tsx ← AI Writing Checker -│ └── Vocabulary.tsx ← Flashcard từ vựng +├── routes/ +│ ├── index.tsx ← Trang chủ (/) +│ ├── toeic/ +│ │ ├── index.tsx ← Chọn Part (/toeic) +│ │ ├── part.$partId.tsx ← Config số câu (/toeic/part/$partId) +│ │ ├── session.tsx ← Làm bài (/toeic/session) +│ │ └── result.tsx ← Kết quả + đáp án (/toeic/result) +│ ├── writing.tsx ← AI Writing Checker (/writing) +│ ├── vocab.tsx ← Flashcard (/vocab) +│ └── auth/ ← Phase 2 +│ ├── login.tsx ← Đăng nhập (/auth/login) +│ └── register.tsx ← Đăng ký (/auth/register) ├── components/ +│ ├── layout/ +│ │ ├── Sidebar.tsx ← Desktop sidebar +│ │ └── BottomNav.tsx ← Mobile bottom nav │ ├── QuestionCard.tsx │ ├── FlashCard.tsx │ ├── WritingFeedback.tsx -│ ├── ProgressBar.tsx +│ ├── ProgressRing.tsx │ └── Timer.tsx -├── hooks/ -│ ├── useQuestions.ts ← TanStack Query: fetch questions -│ ├── useVocab.ts ← TanStack Query: fetch vocab -│ └── useWritingCheck.ts ← TanStack Mutation: gọi Edge Function ├── store/ │ ├── testStore.ts ← Zustand: trạng thái bài thi -│ └── vocabStore.ts ← Zustand: progress flashcard (sync localStorage) +│ ├── vocabStore.ts ← Zustand: flashcard progress (persist localStorage) +│ └── authStore.ts ← Zustand: user session (Phase 2) +├── hooks/ +│ ├── useQuestions.ts ← TanStack Query +│ ├── useVocab.ts ← TanStack Query +│ └── useWritingCheck.ts ← TanStack Mutation → Edge Function ├── lib/ │ └── supabase.ts ← Supabase client init └── utils/ - └── rateLimiter.ts ← Rate limit AI Writing (3 lần/ngày/IP, localStorage) + └── rateLimiter.ts ← Rate limit 3 lần/ngày/IP (localStorage) ``` --- -## Routes (TanStack Router) +## Supabase Edge Function — AI Writing Checker -``` -/ ← Landing page -/toeic ← Chọn Part (1–7) -/toeic/part/$partId ← Config bài thi (số câu) -/toeic/session ← Làm bài -/toeic/result ← Kết quả + đáp án -/writing ← AI Writing Checker -/vocab ← Flashcard (filter theo topic) +```typescript +// supabase/functions/writing-check/index.ts +import { serve } from "https://deno.land/std/http/server.ts" + +serve(async (req) => { + const { content } = await req.json() + + const response = await fetch("https://open.bigmodel.cn/api/paas/v4/chat/completions", { + method: "POST", + headers: { + "Authorization": `Bearer ${Deno.env.get("GLM_API_KEY")}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: "glm-4", + messages: [ + { + role: "system", + content: `You are an expert English writing evaluator for TOEIC/IELTS. +Evaluate the writing and return ONLY valid JSON: +{ + "score": "estimated band score", + "grammar": ["error + fix"], + "vocabulary": ["suggestion"], + "structure": "feedback in Vietnamese", + "improved_version": "rewritten version", + "summary": "overall feedback in Vietnamese" +}` + }, + { role: "user", content } + ] + }) + }) + + const data = await response.json() + const result = JSON.parse(data.choices[0].message.content) + return new Response(JSON.stringify(result), { + headers: { "Content-Type": "application/json" } + }) +}) ``` --- -## Tính năng Phase 1 (đã chốt) - -### ✅ 1. Luyện đề TOEIC -- Mini test theo từng Part (Part 1 → Part 7) -- Chọn số câu: 10 / 20 / full part -- Làm bài có đếm giờ -- Submit → xem điểm + đáp án đúng/sai + giải thích tiếng Việt -- Lịch sử kết quả lưu **localStorage** (Zustand persist) -- Thống kê điểm yếu theo Part - -### ✅ 2. AI Writing Checker (Core Differentiator) -- Nhập bài writing tự do (TOEIC email, IELTS task, general) -- Gọi GLM qua Supabase Edge Function (API key an toàn) -- Feedback JSON: band score, lỗi ngữ pháp, từ vựng, cấu trúc, bài mẫu -- Rate limit: **3 lần / ngày / IP** (không cần login) -- Hiển thị feedback có highlight, dễ đọc trên mobile - -### ✅ 3. Flashcard Từ vựng TOEIC -- 6 chủ đề: Business, Office, Travel, Finance, HR, Marketing -- Mỗi card: từ + phiên âm + nghĩa Việt + câu ví dụ -- Flip card animation -- Đánh dấu: Đã thuộc / Cần ôn -- Progress lưu localStorage (Zustand persist) -- Filter theo chủ đề +## Roadmap — 4 Phases --- -## Tính năng KHÔNG có ở Phase 1 +### PHASE 1 — MVP (Hiện tại) 🚧 -| Tính năng | Khi nào có | -|---|---| -| Đăng nhập / Auth | Phase 2 | -| Progress sync server | Phase 2 | -| Flutter mobile app | Phase 2 | -| NestJS backend | Phase 2 | -| Full TOEIC mock test | Phase 2 | -| Thanh toán | Phase 2 | -| Speaking / Pronunciation AI | Phase 3+ | -| Cộng đồng / Forum | Phase 3+ | -| Lớp học / Giáo viên | Phase 3+ | +**Mục tiêu**: Ra sản phẩm web dùng thử không cần login, validate market + +**Stack**: React + Vite + TypeScript + TanStack + Zustand + Tailwind + Supabase + GLM + +**Tính năng**: +- ✅ Luyện đề TOEIC mini test theo từng Part (Part 1–7) +- ✅ Chọn số câu: 10 / 20 / full part, có đếm giờ +- ✅ Submit → xem điểm + đáp án + giải thích tiếng Việt +- ✅ Lịch sử kết quả + thống kê điểm yếu theo Part (localStorage) +- ✅ AI Writing Checker (GLM, 3 lần/ngày/IP, không cần login) +- ✅ Flashcard từ vựng TOEIC (6 chủ đề, localStorage progress) + +**Không có**: +- ❌ Auth / login +- ❌ Progress sync server +- ❌ Thanh toán +- ❌ Flutter / mobile app +- ❌ Full mock test + +**Timeline**: 5 tuần + +**Done khi**: +- ≥ 50 câu hỏi mỗi Part (Part 1–7) +- AI Writing Checker phản hồi < 5 giây +- Không lỗi hiển thị trên Chrome mobile +- 20+ người dùng thật đã dùng thử +- Không critical bug sau 1 tuần beta --- -## Timeline Phase 1 (5 tuần) +### PHASE 2 — Auth & Progress -| Tuần | Công việc | -|---|---| -| **1** | Setup Supabase schema + seed đề TOEIC (≥50 câu/part) + React + Vite + Tailwind + TanStack + Zustand | -| **2** | UI luyện đề: chọn Part → làm bài → kết quả + giải thích | -| **3** | Supabase Edge Function + GLM API → AI Writing Checker + rate limit | -| **4** | Flashcard UI + Zustand persist + mobile polish + landing page | -| **5** | Bug fix + test mobile thật + deploy lên server + beta ~20 người | +**Mục tiêu**: Giữ chân user, sync progress server-side, hiểu behavior trước khi monetize + +**Trigger**: Phase 1 có traction — 200+ MAU hoặc feedback tích cực + +**Stack thay đổi**: +- Migrate Supabase → **NestJS + PostgreSQL native** +- Thêm **Redis** cho cache + session + +**Guest Access (chưa đăng ký)**: +- Xem preview 1 bài test dạng read-only (thấy câu hỏi, không làm được) +- Không cho submit đáp án, không xem kết quả +- Hiện modal "Đăng ký để luyện thi" khi cố tương tác +- Flashcard: xem vài card đầu, bị chặn sau đó +- AI Writing: không dùng được, hiện CTA đăng ký + +**Auth — Đăng ký**: +- Form: **Tên + Email + Password** (chỉ 3 field) +- Không xác thực email, không OTP, không confirm +- Đăng ký xong → **redirect home luôn** + +**Auth — Đăng nhập**: +- Email + Password +- Redirect về trang trước hoặc home + +**Sau khi đăng nhập**: +- Làm bài không giới hạn +- Progress sync server-side: lịch sử thi, từ vựng, writing submissions +- Dashboard cá nhân: streak, biểu đồ điểm, điểm yếu theo Part +- AI Writing: 10 lần/ngày + +**Không có ở Phase 2**: +- ❌ Xác thực email +- ❌ Quên mật khẩu +- ❌ Google / Zalo OAuth +- ❌ Flutter / mobile app +- ❌ Thanh toán / freemium +- ❌ Notification --- -## Definition of Done — Phase 1 +### PHASE 3 — Speaking AI -- [ ] ≥ 50 câu hỏi mỗi Part (Part 1–7) -- [ ] AI Writing Checker phản hồi < 5 giây -- [ ] Không lỗi hiển thị trên Chrome mobile -- [ ] 20+ người dùng thật đã dùng thử -- [ ] Không có critical bug sau 1 tuần beta +**Mục tiêu**: Tăng differentiation, cover kỹ năng Speaking cho IELTS/TOEIC + +**Trigger**: Phase 2 ổn định, user quay lại đều đặn + +**Tính năng**: +- AI Speaking Coach: record giọng → AI chấm phát âm + so sánh native speaker +- Luyện IELTS Speaking Part 1/2/3 (mock interview với AI) +- Shadow reading: nghe → lặp lại → AI so sánh độ chính xác +- Pronunciation scoring có điểm số và progress chart + +**Tech mới**: +- Speech-to-text: Whisper API hoặc Google Speech-to-Text +- Text-to-speech: native audio +- WebRTC / MediaRecorder API (web) --- -## Rủi ro đã nhận diện +### PHASE 4 — Full TOEIC Mock Test -| Rủi ro | Mức độ | Xử lý | -|---|---|---| -| Content đề TOEIC chất lượng thấp (crawl) | 🔴 Cao | Crawl ít + clean kỹ, tự soạn dần để thay thế | -| GLM chấm writing không đủ tin cậy | 🟡 TB | Test prompt kỹ, fallback OpenAI-compatible nếu cần | -| Latency GLM từ VN cao | 🟡 TB | Benchmark thực tế tuần 3 | -| User mất progress (localStorage) | 🟡 TB | Chấp nhận ở MVP, auth Phase 2 giải quyết | -| Bản quyền đề TOEIC crawl | 🟡 TB | Dùng để seed nhanh, thay bằng nội dung tự soạn | +**Mục tiêu**: Platform luyện TOEIC toàn diện chuẩn ETS + +**Trigger**: Phase 3 xong, cần nội dung premium + +**Tính năng**: +- Full TOEIC test chuẩn ETS: 200 câu, 120 phút +- Audio Listening chuẩn cho Part 1–4 +- Auto-score: tính điểm 10–990 theo bảng quy đổi ETS +- Phân tích chi tiết: điểm mạnh/yếu từng Part, so sánh lần trước +- Bộ đề theo năm: ETS 2023, 2024, Actual Test... +- Đếm ngược đến ngày thi + lịch ôn tập gợi ý --- @@ -260,19 +388,43 @@ src/ | Quyết định | Lý do | |---|---| -| Không có Auth Phase 1 | Giảm scope, user dùng thử không cần tạo tài khoản | -| Supabase thay NestJS tạm | Ra nhanh hơn 2–3 tuần, schema chuẩn để migrate sau | -| GLM thay OpenAI/Claude | Rẻ hơn đáng kể, API compatible, đủ để test | -| Web-only, không Flutter | Tập trung 1 platform, Flutter Phase 2 reuse API | -| TanStack Query + Zustand | TanStack cho server state, Zustand cho client/local state | -| localStorage cho progress | Đủ cho MVP, không cần backend phức tạp | +| Không auth Phase 1 | Giảm scope, validate market trước | +| Email only Phase 2, không OAuth | Đơn giản nhất để build, OAuth Phase 3+ | +| Không xác thực email Phase 2 | MVP — giảm friction đăng ký tối đa | +| Chỉ 3 field đăng ký (tên/email/pass) | Friction thấp nhất, đủ để identify user | +| Guest chỉ xem preview, không làm được | Buộc đăng ký để dùng, giúp thu thập user data | +| Không thanh toán Phase 2 | Hiểu behavior trước khi charge tiền | +| Không Flutter Phase 2 | Web đã responsive, mobile app khi có traction rõ ràng | +| Supabase tạm Phase 1 | Ra nhanh hơn 2–3 tuần, schema chuẩn để migrate sau | +| GLM thay OpenAI/Claude | Rẻ hơn, OpenAI-compatible, swap dễ | +| Desktop-first (không mobile-first) | Target TOEIC learner hay dùng máy tính | +| TanStack Query + Zustand | Server state tách biệt client state rõ ràng | +| localStorage Phase 1 | Đủ cho MVP, không cần backend phức tạp | +| NestJS Phase 2 | Supabase đủ để validate, NestJS khi scale | +| Speaking AI Phase 3 | Cần infra ổn định trước khi làm realtime audio | +| Full mock test Phase 4 | Cần content team + audio, không phải tech problem | --- ## Conventions -- **Ngôn ngữ**: Tiếng Việt cho UI người dùng, English cho code/comments +- **Ngôn ngữ**: Tiếng Việt cho UI người dùng, English cho code/comments/type names - **Giải thích đáp án**: Luôn bằng tiếng Việt -- **AI feedback**: Mix Việt-Anh (nhận xét tổng thể tiếng Việt, ví dụ sửa tiếng Anh) -- **Mobile-first**: Test trên màn hình 375px trước, desktop sau -- **YAGNI / KISS**: Không build thứ chưa cần, Phase 1 xong mới nghĩ Phase 2 \ No newline at end of file +- **AI feedback**: Nhận xét tổng thể tiếng Việt, ví dụ sửa tiếng Anh +- **Desktop-first**: Design và test trên 1280px trước, mobile sau +- **YAGNI / KISS**: Không build thứ chưa cần, từng Phase giải quyết từng vấn đề +- **Schema chuẩn ngay từ đầu**: Dù dùng Supabase, PostgreSQL schema phải production-ready + +--- + +## Rủi ro đã nhận diện + +| Rủi ro | Mức độ | Xử lý | +|---|---|---| +| Content đề TOEIC chất lượng thấp (crawl) | 🔴 Cao | Crawl ít + clean kỹ, tự soạn dần thay thế | +| GLM chấm writing không đủ tin cậy | 🟡 TB | Test prompt kỹ, fallback OpenAI nếu cần | +| Latency GLM từ VN cao | 🟡 TB | Benchmark thực tế, cache response nếu cần | +| User mất progress (localStorage Phase 1) | 🟡 TB | Chấp nhận Phase 1, auth Phase 2 giải quyết | +| Bản quyền đề TOEIC crawl | 🟡 TB | Seed nhanh, thay bằng nội dung tự soạn dần | +| Supabase free tier limit | 🟢 Thấp | 500MB đủ Phase 1, migrate trước khi hit limit | +| Audio quality Phase 4 | 🟡 TB | Budget cho studio recording hoặc TTS premium | \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index f6e0f68..bde1113 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from "eslint-plugin-react-refresh" import tseslint from "typescript-eslint" export default tseslint.config( - { ignores: ["dist", "src/routeTree.gen.ts"] }, + { ignores: ["dist", "src/routeTree.gen.ts", ".claude", ".opencode"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], diff --git a/index.html b/index.html index c385bae..f9819af 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,11 @@ - English Learning App + EnglishAI — Luyện TOEIC thông minh + + + +
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx new file mode 100644 index 0000000..8087cf6 --- /dev/null +++ b/src/components/AppHeader.tsx @@ -0,0 +1,32 @@ +import { useRouterState } from '@tanstack/react-router' +import { useTestStore } from '@/store/test-store' +import { UserMenu } from '@/components/UserMenu' + +const ROUTE_TITLES: Record = { + '/': 'Trang chủ', + '/writing': 'AI Chấm Writing', + '/vocab': 'Từ vựng TOEIC', + '/toeic': 'Luyện đề TOEIC', + '/toeic/session': '', // dynamic — filled below + '/toeic/result': 'Kết quả bài thi', +} + +export function AppHeader() { + const { location } = useRouterState() + const { partId, partName, answers, questions } = useTestStore() + const pathname = location.pathname + + let title = ROUTE_TITLES[pathname] ?? 'EnglishAI' + + if (pathname === '/toeic/session') { + const answered = answers.filter((a) => a !== null).length + title = `Part ${partId} — ${partName} · ${answered}/${questions.length} câu` + } + + return ( +
+ {title} + +
+ ) +} diff --git a/src/components/CircularProgress.tsx b/src/components/CircularProgress.tsx new file mode 100644 index 0000000..cce044f --- /dev/null +++ b/src/components/CircularProgress.tsx @@ -0,0 +1,37 @@ +interface CircularProgressProps { + percent: number + size?: number + strokeWidth?: number + color?: string +} + +/** SVG circular progress ring with centered percentage label. */ +export function CircularProgress({ + percent, + size = 44, + strokeWidth = 3.5, + color = '#2563EB', +}: CircularProgressProps) { + const cx = size / 2 + const radius = cx - strokeWidth + const circumference = 2 * Math.PI * radius + const offset = circumference - (Math.min(percent, 100) / 100) * circumference + + return ( +
+ + + + + {percent}% +
+ ) +} diff --git a/src/components/FlashCard.tsx b/src/components/FlashCard.tsx index aa3f280..7cf6186 100644 --- a/src/components/FlashCard.tsx +++ b/src/components/FlashCard.tsx @@ -1,31 +1,59 @@ -import { useState } from "react" +import { cn } from '@/lib/utils' interface FlashCardProps { - word?: string - phonetic?: string - meaningVi?: string - example?: string + word: string + phonetic: string + meaningVi: string + example: string + topicBadge: string + isFlipped: boolean + onFlip: () => void } -export function FlashCard({ word, phonetic, meaningVi, example }: FlashCardProps) { - const [flipped, setFlipped] = useState(false) +/** 3D flip flashcard. Front shows word/phonetic; back shows Vietnamese meaning + example. */ +export function FlashCard({ word, phonetic, meaningVi, example, topicBadge, isFlipped, onFlip }: FlashCardProps) { + const highlightedExample = example.replace( + new RegExp(`\\b${word}\\b`, 'gi'), + (match) => `${match}`, + ) return (
setFlipped((f) => !f)} - className="rounded-lg border p-6 text-center cursor-pointer min-h-[160px] flex flex-col justify-center select-none hover:bg-gray-50" + className="flashcard-scene w-full cursor-pointer select-none" + style={{ height: 280 }} + onClick={onFlip} + role="button" + aria-label={isFlipped ? 'Nhấn để xem từ' : 'Nhấn để xem nghĩa'} > - {!flipped ? ( -
-

{word || "word"}

- {phonetic &&

{phonetic}

} +
+ {/* Front */} +
+
{word}
+
{phonetic}
+ + {topicBadge} + +

+ touch_app + Nhấn để xem nghĩa +

- ) : ( -
-

{meaningVi || "nghĩa tiếng Việt"}

- {example &&

{example}

} + + {/* Back */} +
+
{meaningVi}
+
+ Nghĩa tiếng Việt +
+
- )} +
) } diff --git a/src/components/MobileNav.tsx b/src/components/MobileNav.tsx new file mode 100644 index 0000000..d9d1d2f --- /dev/null +++ b/src/components/MobileNav.tsx @@ -0,0 +1,41 @@ +import { Link, useRouterState } from '@tanstack/react-router' +import { cn } from '@/lib/utils' + +const NAV_ITEMS = [ + { to: '/', label: 'Home', icon: 'home', matchPrefix: '/', exact: true }, + { to: '/toeic', label: 'Luyện đề', icon: 'assignment', matchPrefix: '/toeic', exact: false }, + { to: '/writing', label: 'Writing', icon: 'edit_note', matchPrefix: '/writing', exact: false }, + { to: '/vocab', label: 'Từ vựng', icon: 'menu_book', matchPrefix: '/vocab', exact: false }, +] + +function isActive(pathname: string, prefix: string, exact: boolean) { + return exact ? pathname === prefix : pathname.startsWith(prefix) +} + +export function MobileNav() { + const { location } = useRouterState() + const pathname = location.pathname + + return ( + + ) +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..19fab5e --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,84 @@ +import { Link, useRouterState } from '@tanstack/react-router' +import { cn } from '@/lib/utils' +import { useAuthStore } from '@/store/auth-store' +import { useAuthModalStore } from '@/store/auth-modal-store' + +const NAV_ITEMS = [ + { to: '/', label: 'Trang chủ', icon: 'home', matchPrefix: '/', exact: true }, + { to: '/toeic', label: 'Luyện đề TOEIC', icon: 'assignment', matchPrefix: '/toeic', exact: false }, + { to: '/writing', label: 'AI Writing', icon: 'edit_note', matchPrefix: '/writing', exact: false }, + { to: '/vocab', label: 'Từ vựng', icon: 'menu_book', matchPrefix: '/vocab', exact: false }, +] + +function isActive(pathname: string, prefix: string, exact: boolean) { + return exact ? pathname === prefix : pathname.startsWith(prefix) +} + +export function Sidebar() { + const { location } = useRouterState() + const pathname = location.pathname + const user = useAuthStore((s) => s.user) + const openModal = useAuthModalStore((s) => s.open) + + return ( + + ) +} diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx new file mode 100644 index 0000000..8e88366 --- /dev/null +++ b/src/components/UserMenu.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect, useRef } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { useAuthStore } from '@/store/auth-store' +import { useUser } from '@/hooks/use-auth' +import { useAuthModalStore } from '@/store/auth-modal-store' + +/** Avatar circle with first letter of name, deterministic color */ +function Avatar({ name }: { name: string }) { + const colors = ['bg-blue-600', 'bg-green-600', 'bg-violet-600', 'bg-rose-600', 'bg-amber-600'] + const color = colors[name.charCodeAt(0) % colors.length] + return ( +
+ {name.charAt(0).toUpperCase()} +
+ ) +} + +export function UserMenu() { + const user = useUser() + const logout = useAuthStore((s) => s.logout) + const openModal = useAuthModalStore((s) => s.open) + const navigate = useNavigate() + const [dropdownOpen, setDropdownOpen] = useState(false) + const menuRef = useRef(null) + + // Close on outside click + useEffect(() => { + if (!dropdownOpen) return + function handleClick(e: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setDropdownOpen(false) + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [dropdownOpen]) + + async function handleLogout() { + setDropdownOpen(false) + await logout() + navigate({ to: '/' }) + } + + if (!user) { + return ( +
+ + +
+ ) + } + + return ( +
+ + + {dropdownOpen && ( +
+
+
{user.name}
+
{user.email}
+
+ + + + +
+ +
+
+ )} +
+ ) +} diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx new file mode 100644 index 0000000..197f291 --- /dev/null +++ b/src/components/auth/AuthModal.tsx @@ -0,0 +1,96 @@ +import { useEffect } from 'react' +import { useAuthModalStore } from '@/store/auth-modal-store' +import { LoginForm } from './LoginForm' +import { RegisterForm } from './RegisterForm' + +export function AuthModal() { + const { isOpen, mode, open, close } = useAuthModalStore() + + // Close on Escape key + useEffect(() => { + if (!isOpen) return + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') close() + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [isOpen, close]) + + // Prevent body scroll when open + useEffect(() => { + document.body.style.overflow = isOpen ? 'hidden' : '' + return () => { document.body.style.overflow = '' } + }, [isOpen]) + + if (!isOpen) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Card */} +
+ {/* Close button */} + + + {/* Header */} +
+
+ school + TOEIC Luyện thi +
+ + {/* Tab toggle */} +
+ + +
+
+ + {/* Form */} + {mode === 'login' ? ( + open('register')} + /> + ) : ( + open('login')} + /> + )} +
+
+ ) +} diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..dc7b64e --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react' +import { useAuthStore } from '@/store/auth-store' + +interface LoginFormProps { + onSuccess?: () => void + onSwitchToRegister?: () => void +} + +export function LoginForm({ onSuccess, onSwitchToRegister }: LoginFormProps) { + const login = useAuthStore((s) => s.login) + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + setIsSubmitting(true) + try { + await login(email, password) + onSuccess?.() + } catch (err) { + setError((err as Error).message) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 border border-slate-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Tối thiểu 6 ký tự" + className="w-full px-3 py-2 border border-slate-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {error && ( +

{error}

+ )} + + + + {onSwitchToRegister && ( +

+ Chưa có tài khoản?{' '} + +

+ )} +
+ ) +} diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx new file mode 100644 index 0000000..d165862 --- /dev/null +++ b/src/components/auth/RegisterForm.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react' +import { useAuthStore } from '@/store/auth-store' + +interface RegisterFormProps { + onSuccess?: () => void + onSwitchToLogin?: () => void +} + +export function RegisterForm({ onSuccess, onSwitchToLogin }: RegisterFormProps) { + const register = useAuthStore((s) => s.register) + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + setIsSubmitting(true) + try { + await register(name, email, password) + onSuccess?.() + } catch (err) { + setError((err as Error).message) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ + setName(e.target.value)} + placeholder="Nguyễn Văn A" + className="w-full px-3 py-2 border border-slate-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 border border-slate-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Tối thiểu 6 ký tự" + className="w-full px-3 py-2 border border-slate-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {error && ( +

{error}

+ )} + + + + {onSwitchToLogin && ( +

+ Đã có tài khoản?{' '} + +

+ )} +
+ ) +} diff --git a/src/data/mock-data.ts b/src/data/mock-data.ts new file mode 100644 index 0000000..8231db4 --- /dev/null +++ b/src/data/mock-data.ts @@ -0,0 +1,157 @@ +import type { Question, VocabWord, WritingFeedback, ToeicPart } from '@/types' + +export const TOEIC_PARTS: ToeicPart[] = [ + { id: 1, name: 'Part 1', nameVi: 'Mô tả hình ảnh', questionCount: 45, icon: 'image', progressPercent: 60 }, + { id: 2, name: 'Part 2', nameVi: 'Hỏi-đáp', questionCount: 30, icon: 'question_answer', progressPercent: 40 }, + { id: 3, name: 'Part 3', nameVi: 'Đoạn hội thoại', questionCount: 39, icon: 'forum', progressPercent: 25 }, + { id: 4, name: 'Part 4', nameVi: 'Bài nói', questionCount: 30, icon: 'record_voice_over', progressPercent: 10 }, + { id: 5, name: 'Part 5', nameVi: 'Điền từ', questionCount: 40, icon: 'history_edu', progressPercent: 80 }, + { id: 6, name: 'Part 6', nameVi: 'Điền đoạn', questionCount: 16, icon: 'article', progressPercent: 50 }, + { id: 7, name: 'Part 7', nameVi: 'Đọc hiểu', questionCount: 54, icon: 'chrome_reader_mode', progressPercent: 30 }, +] + +export const MOCK_QUESTIONS: Question[] = [ + { + id: 'q1', part: 2, + text: 'What does the man suggest the woman do about the budget report?', + options: ['A. Submit it immediately', 'B. Review it again carefully', 'C. Postpone the deadline', 'D. Ask a colleague for help'], + correctAnswer: 1, + explanation: 'Người đàn ông nói "You should review it carefully before submitting" — gợi ý xem xét lại báo cáo trước khi nộp.', + }, + { + id: 'q2', part: 2, + text: 'Where most likely are the speakers?', + options: ['A. In a restaurant', 'B. At a conference', 'C. In an office', 'D. At an airport'], + correctAnswer: 2, + explanation: 'Các từ như "meeting room", "printer", "desk" cho biết cuộc trò chuyện diễn ra trong văn phòng.', + }, + { + id: 'q3', part: 2, + text: 'Why is the man calling?', + options: ['A. To confirm a reservation', 'B. To cancel an appointment', 'C. To reschedule a meeting', 'D. To order supplies'], + correctAnswer: 0, + explanation: 'Từ "confirm" và "booking number" trong hội thoại chỉ rõ mục đích của cuộc gọi là xác nhận đặt chỗ.', + }, + { + id: 'q4', part: 2, + text: 'What will the woman do next?', + options: ['A. Call the manager', 'B. Send an email', 'C. Check the inventory', 'D. Update the schedule'], + correctAnswer: 3, + explanation: 'Người phụ nữ nói "I\'ll update the schedule right away" — cho biết hành động tiếp theo là cập nhật lịch trình.', + }, + { + id: 'q5', part: 2, + text: 'What problem does the man mention?', + options: ['A. A delayed shipment', 'B. A broken device', 'C. A missing document', 'D. A scheduling conflict'], + correctAnswer: 0, + explanation: '"The delivery has been delayed by two days" — vấn đề được đề cập là lô hàng bị trễ.', + }, + { + id: 'q6', part: 2, + text: 'How does the woman respond to the proposal?', + options: ['A. She accepts it', 'B. She rejects it', 'C. She needs more time', 'D. She suggests modifications'], + correctAnswer: 3, + explanation: '"That sounds good, but maybe we could adjust the timeline a bit" — đề xuất điều chỉnh, không chấp nhận hoàn toàn.', + }, + { + id: 'q7', part: 2, + text: 'What is the purpose of the announcement?', + options: ['A. To introduce new products', 'B. To notify schedule changes', 'C. To welcome new employees', 'D. To announce a promotion'], + correctAnswer: 1, + explanation: 'Thông báo nói về việc thay đổi giờ làm việc từ tuần tới — mục đích là thông báo thay đổi lịch.', + }, + { + id: 'q8', part: 2, + text: 'What does the woman ask the man to do?', + options: ['A. Prepare a presentation', 'B. Contact the client', 'C. Review the contract', 'D. Attend a training session'], + correctAnswer: 2, + explanation: '"Could you go over the contract before we sign?" — người phụ nữ yêu cầu xem lại hợp đồng.', + }, + { + id: 'q9', part: 2, + text: 'When will the project be completed?', + options: ['A. By the end of this week', 'B. Next Monday', 'C. In two weeks', 'D. Next month'], + correctAnswer: 0, + explanation: '"We should be finished by Friday" — dự án sẽ hoàn thành vào cuối tuần này.', + }, + { + id: 'q10', part: 2, + text: 'What is being discussed at the meeting?', + options: ['A. Budget allocations', 'B. Marketing strategies', 'C. Product launches', 'D. Staff promotions'], + correctAnswer: 1, + explanation: 'Cuộc họp tập trung vào "the new advertising campaign and social media approach" — chiến lược marketing.', + }, +] + +export const VOCAB_DATA: Record = { + 'Tất cả': [ + { id: 'v1', word: 'negotiate', phonetic: '/nɪˈɡoʊʃieɪt/', meaningVi: 'đàm phán', topic: 'Business', example: 'We need to negotiate the contract terms before signing.' }, + { id: 'v2', word: 'collaborate', phonetic: '/kəˈlæbəreɪt/', meaningVi: 'hợp tác', topic: 'Business', example: 'Teams need to collaborate effectively to meet the deadline.' }, + { id: 'v3', word: 'agenda', phonetic: '/əˈdʒendə/', meaningVi: 'chương trình nghị sự', topic: 'Office', example: 'The agenda has been sent to all meeting participants.' }, + { id: 'v4', word: 'itinerary', phonetic: '/aɪˈtɪnəreri/', meaningVi: 'lịch trình chuyến đi', topic: 'Travel', example: 'Here is your travel itinerary for the business conference.' }, + { id: 'v5', word: 'reimburse', phonetic: '/ˌriːɪmˈbɜːrs/', meaningVi: 'hoàn tiền', topic: 'Finance', example: 'The company will reimburse your travel expenses.' }, + { id: 'v6', word: 'recruit', phonetic: '/rɪˈkruːt/', meaningVi: 'tuyển dụng', topic: 'HR', example: 'We are actively recruiting experienced engineers.' }, + { id: 'v7', word: 'campaign', phonetic: '/kæmˈpeɪn/', meaningVi: 'chiến dịch', topic: 'Marketing', example: 'The marketing campaign was very successful this quarter.' }, + { id: 'v8', word: 'implement', phonetic: '/ˈɪmplɪment/', meaningVi: 'triển khai, thực hiện', topic: 'Business', example: 'We plan to implement the new strategy next quarter.' }, + ], + 'Business': [ + { id: 'b1', word: 'negotiate', phonetic: '/nɪˈɡoʊʃieɪt/', meaningVi: 'đàm phán', topic: 'Business', example: 'We need to negotiate the contract terms.' }, + { id: 'b2', word: 'collaborate', phonetic: '/kəˈlæbəreɪt/', meaningVi: 'hợp tác', topic: 'Business', example: 'Teams collaborate to achieve shared goals.' }, + { id: 'b3', word: 'delegate', phonetic: '/ˈdelɪɡeɪt/', meaningVi: 'uỷ quyền, phân công', topic: 'Business', example: 'A good manager knows how to delegate tasks.' }, + { id: 'b4', word: 'implement', phonetic: '/ˈɪmplɪment/', meaningVi: 'triển khai, thực hiện', topic: 'Business', example: 'We will implement the new policy next month.' }, + { id: 'b5', word: 'merger', phonetic: '/ˈmɜːrdʒər/', meaningVi: 'sáp nhập công ty', topic: 'Business', example: 'The merger will create a stronger combined company.' }, + { id: 'b6', word: 'acquisition', phonetic: '/ˌækwɪˈzɪʃən/', meaningVi: 'mua lại, thâu tóm', topic: 'Business', example: 'The acquisition was completed ahead of schedule.' }, + ], + 'Office': [ + { id: 'o1', word: 'agenda', phonetic: '/əˈdʒendə/', meaningVi: 'chương trình nghị sự', topic: 'Office', example: 'Please review the agenda before the meeting.' }, + { id: 'o2', word: 'minutes', phonetic: '/ˈmɪnɪts/', meaningVi: 'biên bản họp', topic: 'Office', example: 'Could you take the meeting minutes today?' }, + { id: 'o3', word: 'submit', phonetic: '/səbˈmɪt/', meaningVi: 'nộp, gửi đi', topic: 'Office', example: 'Please submit your report by Friday afternoon.' }, + { id: 'o4', word: 'deadline', phonetic: '/ˈdedlaɪn/', meaningVi: 'hạn chót', topic: 'Office', example: 'The deadline for this project is end of month.' }, + { id: 'o5', word: 'cubicle', phonetic: '/ˈkjuːbɪkəl/', meaningVi: 'góc làm việc riêng', topic: 'Office', example: 'Each employee has their own cubicle in the open office.' }, + ], + 'Travel': [ + { id: 't1', word: 'itinerary', phonetic: '/aɪˈtɪnəreri/', meaningVi: 'lịch trình chuyến đi', topic: 'Travel', example: 'Here is your detailed travel itinerary.' }, + { id: 't2', word: 'boarding pass', phonetic: '/ˈbɔːrdɪŋ pæs/', meaningVi: 'thẻ lên máy bay', topic: 'Travel', example: 'Please have your boarding pass ready at the gate.' }, + { id: 't3', word: 'layover', phonetic: '/ˈleɪoʊvər/', meaningVi: 'thời gian quá cảnh', topic: 'Travel', example: 'There is a two-hour layover in Singapore.' }, + { id: 't4', word: 'customs', phonetic: '/ˈkʌstəmz/', meaningVi: 'hải quan', topic: 'Travel', example: 'All passengers must go through customs on arrival.' }, + { id: 't5', word: 'baggage claim', phonetic: '/ˈbæɡɪdʒ kleɪm/', meaningVi: 'băng chuyền hành lý', topic: 'Travel', example: 'Meet us at the baggage claim after landing.' }, + ], + 'Finance': [ + { id: 'f1', word: 'reimburse', phonetic: '/ˌriːɪmˈbɜːrs/', meaningVi: 'hoàn tiền', topic: 'Finance', example: 'The company will reimburse all travel expenses.' }, + { id: 'f2', word: 'invoice', phonetic: '/ˈɪnvɔɪs/', meaningVi: 'hoá đơn', topic: 'Finance', example: 'Please send the invoice to our accounting department.' }, + { id: 'f3', word: 'budget', phonetic: '/ˈbʌdʒɪt/', meaningVi: 'ngân sách', topic: 'Finance', example: 'We need to stay within the approved budget.' }, + { id: 'f4', word: 'revenue', phonetic: '/ˈrevɪnjuː/', meaningVi: 'doanh thu', topic: 'Finance', example: 'Revenue increased by 15% last quarter.' }, + { id: 'f5', word: 'fiscal year', phonetic: '/ˈfɪskəl jɪər/', meaningVi: 'năm tài chính', topic: 'Finance', example: 'Our fiscal year ends on December 31st.' }, + ], + 'HR': [ + { id: 'h1', word: 'recruit', phonetic: '/rɪˈkruːt/', meaningVi: 'tuyển dụng', topic: 'HR', example: 'We are recruiting experienced software engineers.' }, + { id: 'h2', word: 'probation', phonetic: '/proʊˈbeɪʃən/', meaningVi: 'thử việc', topic: 'HR', example: 'New employees have a 3-month probation period.' }, + { id: 'h3', word: 'appraisal', phonetic: '/əˈpreɪzəl/', meaningVi: 'đánh giá nhân viên', topic: 'HR', example: 'Annual performance appraisals are held in December.' }, + { id: 'h4', word: 'resignation', phonetic: '/ˌrezɪɡˈneɪʃən/', meaningVi: 'đơn từ chức', topic: 'HR', example: 'She submitted her resignation letter this morning.' }, + { id: 'h5', word: 'onboarding', phonetic: '/ˈɒnbɔːrdɪŋ/', meaningVi: 'quy trình tiếp nhận nhân viên mới', topic: 'HR', example: 'The onboarding process takes about two weeks.' }, + ], + 'Marketing': [ + { id: 'm1', word: 'campaign', phonetic: '/kæmˈpeɪn/', meaningVi: 'chiến dịch', topic: 'Marketing', example: 'The marketing campaign exceeded all expectations.' }, + { id: 'm2', word: 'demographics', phonetic: '/ˌdeməˈɡræfɪks/', meaningVi: 'nhân khẩu học', topic: 'Marketing', example: 'We need to understand our target demographics.' }, + { id: 'm3', word: 'endorse', phonetic: '/ɪnˈdɔːrs/', meaningVi: 'chứng thực, bảo trợ', topic: 'Marketing', example: 'The product is endorsed by professional athletes.' }, + { id: 'm4', word: 'branding', phonetic: '/ˈbrændɪŋ/', meaningVi: 'xây dựng thương hiệu', topic: 'Marketing', example: 'Consistent branding builds long-term customer trust.' }, + { id: 'm5', word: 'conversion rate', phonetic: '/kənˈvɜːrʒən reɪt/', meaningVi: 'tỷ lệ chuyển đổi', topic: 'Marketing', example: 'Our conversion rate improved after the redesign.' }, + ], +} + +export const MOCK_WRITING_FEEDBACK: WritingFeedback = { + score: '6.5', + grammar: [ + '"managers are concern" → nên dùng "concerned" (tính từ, không phải danh từ)', + 'Thiếu mạo từ "an" trước "efficient arrangement" ở câu cuối', + 'Câu "This change is expected to improve" — đúng nhưng hơi thụ động, có thể dùng active voice', + ], + vocabulary: [ + 'Tốt: "implement", "productivity", "collaboration", "arrangement"', + 'Gợi ý nâng cao: "enhance" thay "increase", "address" thay "help with"', + 'Nên thêm từ nối: "Nevertheless", "In addition", "As a result of this"', + ], + structure: 'Bài viết có cấu trúc khá rõ ràng với mở đầu, thân bài và kết luận ngầm. Tuy nhiên cần phát triển thêm phần giải thích tác động và thêm ví dụ cụ thể để bài hoàn chỉnh hơn.', + improvedVersion: 'The company has decided to implement a new remote work policy starting next month. All employees will be able to work from home for three days per week. This change is expected to enhance work-life balance and boost overall productivity. Nevertheless, some managers are concerned about communication challenges and team collaboration. To address these concerns, the HR department will organize training sessions to help teams adapt to this new arrangement effectively.', + summary: 'Bài viết đạt mức Upper Intermediate (6.5) với ý tưởng rõ ràng. Cần sửa lỗi ngữ pháp cơ bản và bổ sung từ vựng phong phú hơn để đạt band 7.0+.', +} diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts new file mode 100644 index 0000000..6d4229c --- /dev/null +++ b/src/hooks/use-auth.ts @@ -0,0 +1,5 @@ +import { useAuthStore } from '@/store/auth-store' + +export const useAuth = () => useAuthStore() +export const useUser = () => useAuthStore((s) => s.user) +export const useIsAuthenticated = () => useAuthStore((s) => s.user !== null) diff --git a/src/hooks/use-questions.ts b/src/hooks/use-questions.ts index 91a0b69..790cfb7 100644 --- a/src/hooks/use-questions.ts +++ b/src/hooks/use-questions.ts @@ -1,18 +1,35 @@ import { useQuery } from "@tanstack/react-query" import { supabase } from "@/lib/supabase" +import type { Question } from "@/types" + +const ANSWER_INDEX: Record = { A: 0, B: 1, C: 2, D: 3 } + +// Maps a Supabase row to the shared Question interface. +// DB uses `content` + `answer` ('A'–'D'); interface uses `text` + `correctAnswer` (0–3). +function rowToQuestion(row: Record): Question { + return { + id: row.id as string, + part: row.part as number, + text: row.content as string, + options: row.options as string[], + correctAnswer: ANSWER_INDEX[(row.answer as string).toUpperCase()] ?? 0, + explanation: (row.explanation as string) ?? '', + } +} + +// Exported for imperative use (e.g. ToeicPractice click handler). +// part=0 fetches all parts (Full Test). +export async function fetchQuestions(part: number, limit = 10): Promise { + let query = supabase.from('questions').select('*').limit(limit) + if (part > 0) query = query.eq('part', part) + const { data, error } = await query + if (error) throw error + return (data ?? []).map(rowToQuestion) +} export function useQuestions(part: number, limit = 10) { return useQuery({ - queryKey: ["questions", part, limit], - queryFn: async () => { - const { data, error } = await supabase - .from("questions") - .select("*") - .eq("part", part) - .limit(limit) - if (error) throw error - return data - }, - enabled: false, // Enabled during feature implementation + queryKey: ['questions', part, limit], + queryFn: () => fetchQuestions(part, limit), }) } diff --git a/src/hooks/use-require-auth.ts b/src/hooks/use-require-auth.ts new file mode 100644 index 0000000..2ad4df1 --- /dev/null +++ b/src/hooks/use-require-auth.ts @@ -0,0 +1,17 @@ +import { useAuthStore } from '@/store/auth-store' +import { useAuthModalStore } from '@/store/auth-modal-store' + +export function useRequireAuth() { + const user = useAuthStore((s) => s.user) + const isLoading = useAuthStore((s) => s.isLoading) + const openModal = useAuthModalStore((s) => s.open) + + /** Returns true if authenticated. If guest, opens auth modal and returns false. */ + function requireAuth(): boolean { + if (user) return true + openModal('register') + return false + } + + return { isAuthenticated: !!user, isLoading, requireAuth } +} diff --git a/src/hooks/use-vocab.ts b/src/hooks/use-vocab.ts index 904b6b4..de06337 100644 --- a/src/hooks/use-vocab.ts +++ b/src/hooks/use-vocab.ts @@ -1,16 +1,32 @@ import { useQuery } from "@tanstack/react-query" import { supabase } from "@/lib/supabase" +import type { VocabWord, VocabTopic } from "@/types" -export function useVocab(topic?: string) { +// Maps a Supabase row to VocabWord. +// DB column `meaning_vi` → interface field `meaningVi`. +function rowToVocabWord(row: Record): VocabWord { + return { + id: row.id as string, + word: row.word as string, + phonetic: (row.phonetic as string) ?? '', + meaningVi: row.meaning_vi as string, + topic: row.topic as VocabTopic, + example: (row.example as string) ?? '', + } +} + +// Fetches ALL vocab; topic filtering is done in-component so we avoid +// separate queries per topic and keep the cache simple. +export function useVocab() { return useQuery({ - queryKey: ["vocab", topic], + queryKey: ['vocab'], queryFn: async () => { - let query = supabase.from("vocab").select("*") - if (topic) query = query.eq("topic", topic.toLowerCase()) - const { data, error } = await query + const { data, error } = await supabase + .from('vocab') + .select('*') + .order('topic') if (error) throw error - return data + return (data ?? []).map(rowToVocabWord) }, - enabled: false, // Enabled during feature implementation }) } diff --git a/src/hooks/use-writing-check.ts b/src/hooks/use-writing-check.ts index fc4aacf..41b6804 100644 --- a/src/hooks/use-writing-check.ts +++ b/src/hooks/use-writing-check.ts @@ -1,28 +1,89 @@ import { useMutation } from "@tanstack/react-query" -import { supabase } from "@/lib/supabase" import { canUseWritingCheck, recordWritingCheckUsage } from "@/utils/rate-limiter" +import { useAuthStore } from "@/store/auth-store" +import { saveWritingSubmission, countTodayWritingSubmissions } from "@/lib/progress-service" +import type { WritingFeedback } from "@/types" -interface WritingFeedback { - score: string - grammar: string[] - vocabulary: string[] - structure: string - improved_version: string - summary: string +const AUTH_DAILY_LIMIT = 10 +const GUEST_DAILY_LIMIT = 3 + +const GLM_BASE_URL = "https://open.bigmodel.cn/api/paas/v4" +const GLM_API_KEY = import.meta.env.VITE_GLM_API_KEY as string +const GLM_MODEL = (import.meta.env.VITE_GLM_MODEL as string) || "GLM-4-32B-0414-128K" + +// Keep system prompt concise — fewer tokens = more room for output. +// improved_version omitted from schema to reduce output length; added back as optional. +const SYSTEM_PROMPT = `You are an expert English writing teacher for TOEIC and IELTS. +Respond ONLY with valid JSON, no markdown: +{"score":"6.5","grammar":["issue + fix in Vietnamese"],"vocabulary":["observation in Vietnamese"],"structure":"2 sentences in Vietnamese","improved_version":"full improved text","summary":"2 sentences in Vietnamese"}` + +async function callGlm(content: string): Promise { + const res = await fetch(`${GLM_BASE_URL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${GLM_API_KEY}`, + }, + body: JSON.stringify({ + model: GLM_MODEL, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: `Analyse:\n\n${content.slice(0, 1500)}` }, + ], + temperature: 0.3, + max_tokens: 2500, + // Force JSON output mode (OpenAI-compatible, supported by GLM) + response_format: { type: "json_object" }, + }), + }) + + if (!res.ok) { + const err = await res.json().catch(() => ({})) + throw new Error((err as { error?: { message?: string } }).error?.message ?? `GLM error ${res.status}`) + } + + const data = await res.json() as { choices: { message: { content: string } }[] } + const raw = data.choices[0]?.message?.content ?? "{}" + + // Strip markdown code fences defensively + const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim() + + try { + return JSON.parse(cleaned) as WritingFeedback + } catch { + throw new Error("Phản hồi từ AI không hợp lệ. Vui lòng thử lại.") + } } export function useWritingCheck() { return useMutation({ mutationFn: async (content: string): Promise => { - if (!canUseWritingCheck()) { - throw new Error("Bạn đã dùng hết 3 lần kiểm tra hôm nay. Quay lại vào ngày mai!") + const user = useAuthStore.getState().user + + if (user) { + // Server-side rate limit for authenticated users (10/day) + const usedToday = await countTodayWritingSubmissions(user.id) + if (usedToday >= AUTH_DAILY_LIMIT) { + throw new Error(`Bạn đã dùng hết ${AUTH_DAILY_LIMIT} lần kiểm tra hôm nay. Quay lại vào ngày mai!`) + } + } else { + // localStorage rate limit for guests (3/day) + if (!canUseWritingCheck()) { + throw new Error(`Bạn đã dùng hết ${GUEST_DAILY_LIMIT} lần kiểm tra hôm nay. Đăng ký để được 10 lần/ngày!`) + } } - const { data, error } = await supabase.functions.invoke("writing-check", { - body: { content }, - }) - if (error) throw error - recordWritingCheckUsage() - return data as WritingFeedback + + const feedback = await callGlm(content) + + if (user) { + // Save to DB (fire-and-forget) + saveWritingSubmission(user.id, content, feedback) + } else { + // Persist guest usage in localStorage + recordWritingCheckUsage() + } + + return feedback }, }) } diff --git a/src/index.css b/src/index.css index fb3c7e9..42f18fa 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,12 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; -@import "@fontsource-variable/geist"; @custom-variant dark (&:is(.dark *)); @theme inline { --font-heading: var(--font-sans); - --font-sans: 'Geist Variable', sans-serif; + --font-sans: 'Plus Jakarta Sans', sans-serif; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -55,8 +54,8 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); + --primary: oklch(54.6% 0.245 262.3); /* #2563EB */ + --primary-foreground: oklch(1 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); @@ -120,11 +119,58 @@ @layer base { * { @apply border-border outline-ring/50; - } + } body { - @apply bg-background text-foreground; - } + @apply bg-slate-50 text-slate-800; + } html { @apply font-sans; - } + } +} + +/* ── Flashcard 3D flip ── */ +.flashcard-scene { + perspective: 1000px; +} +.flashcard-inner { + position: relative; + transform-style: preserve-3d; + transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1); +} +.flashcard-inner.is-flipped { + transform: rotateY(180deg); +} +.flashcard-face { + position: absolute; + inset: 0; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + border-radius: 20px; +} +.flashcard-back { + transform: rotateY(180deg); +} + +/* ── Material Symbols ── */ +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + vertical-align: middle; +} + +/* ── Page fade-in ── */ +@keyframes page-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} +.page-enter { + animation: page-in 0.2s ease both; +} + +/* ── Timer urgent pulse ── */ +@keyframes timer-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +.timer-urgent { + animation: timer-pulse 1s ease-in-out infinite; } \ No newline at end of file diff --git a/src/lib/progress-service.ts b/src/lib/progress-service.ts new file mode 100644 index 0000000..f2498d9 --- /dev/null +++ b/src/lib/progress-service.ts @@ -0,0 +1,71 @@ +import { supabase } from '@/lib/supabase' + +interface TestResultData { + partId: number + partName: string + score: number + total: number + timeUsed: number + answers: { questionId: string; selected: number | null; correct: boolean }[] +} + +/** Fire-and-forget: save test result. Failures are logged but don't block UI. */ +export async function saveTestResult(userId: string, data: TestResultData): Promise { + const { error } = await supabase.from('user_progress').insert({ + user_id: userId, + type: 'test', + data, + }) + if (error) console.error('Failed to save test result:', error.message) +} + +/** Fire-and-forget: save writing submission with AI feedback. */ +export async function saveWritingSubmission( + userId: string, + content: string, + feedback: object, +): Promise { + const { error } = await supabase.from('writing_submissions').insert({ + user_id: userId, + content, + feedback, + }) + if (error) console.error('Failed to save writing submission:', error.message) +} + +/** Count today's writing submissions for server-side rate limiting (authenticated users). */ +export async function countTodayWritingSubmissions(userId: string): Promise { + const today = new Date().toISOString().split('T')[0] + const { count, error } = await supabase + .from('writing_submissions') + .select('*', { count: 'exact', head: true }) + .eq('user_id', userId) + .gte('created_at', `${today}T00:00:00.000Z`) + if (error) return 0 + return count ?? 0 +} + +/** Fetch test history for a user (most recent first, max 20). */ +export async function fetchTestHistory(userId: string) { + const { data, error } = await supabase + .from('user_progress') + .select('*') + .eq('user_id', userId) + .eq('type', 'test') + .order('created_at', { ascending: false }) + .limit(20) + if (error) throw error + return data ?? [] +} + +/** Fetch writing history for a user (most recent first, max 20). */ +export async function fetchWritingHistory(userId: string) { + const { data, error } = await supabase + .from('writing_submissions') + .select('id, content, feedback, created_at') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(20) + if (error) throw error + return data ?? [] +} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 8b9434c..fd1f40a 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,11 +1,14 @@ import { createClient } from "@supabase/supabase-js" const supabaseUrl = import.meta.env.VITE_SUPABASE_URL -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY +// Supports both key name conventions +const supabaseAnonKey = + import.meta.env.VITE_SUPABASE_ANON_KEY || + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY if (!supabaseUrl || !supabaseAnonKey) { console.warn( - "Supabase env vars missing. Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in .env", + "Supabase env vars missing. Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY (or VITE_SUPABASE_PUBLISHABLE_KEY) in .env", ) } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 93c54b7..aacb9c6 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,32 +1,204 @@ +import { Link } from '@tanstack/react-router' +import { useUser } from '@/hooks/use-auth' +import { useAuthModalStore } from '@/store/auth-modal-store' + +const FEATURES = [ + { + to: '/toeic', + icon: 'assignment', + iconBg: 'bg-blue-50', + iconColor: 'text-blue-600', + borderColor: 'border-l-blue-600', + title: 'Luyện đề TOEIC', + desc: 'Kho đề thi cập nhật theo cấu trúc mới nhất. Phân tích điểm yếu chi tiết từng Part.', + cta: 'Bắt đầu ngay', + ctaColor: 'text-blue-600', + stat: '350+ câu hỏi', + }, + { + to: '/writing', + icon: 'auto_fix_high', + iconBg: 'bg-green-50', + iconColor: 'text-green-600', + borderColor: 'border-l-green-600', + title: 'AI Chấm Writing', + desc: 'Phản hồi tức thì về ngữ pháp, từ vựng, cấu trúc và bài viết mẫu từ AI.', + cta: 'Thử ngay', + ctaColor: 'text-green-600', + stat: '3 lượt / ngày', + }, + { + to: '/vocab', + icon: 'menu_book', + iconBg: 'bg-amber-50', + iconColor: 'text-amber-600', + borderColor: 'border-l-amber-600', + title: 'Từ vựng thông minh', + desc: '720 từ TOEIC theo 6 chủ đề. Flashcard với hiệu ứng lật 3D.', + cta: 'Khám phá', + ctaColor: 'text-amber-600', + stat: '720 từ vựng', + }, +] + export function Home() { + const user = useUser() + const openModal = useAuthModalStore((s) => s.open) + return ( -
-
-

Luyện tiếng Anh TOEIC

-

- Luyện đề, kiểm tra writing, và học từ vựng TOEIC miễn phí -

-
-
-
-

Luyện đề TOEIC

-

- Luyện tập từng Part 1–7 với đề thật +

+ {/* Hero */} +
+
+
+ auto_awesome + AI-Powered Learning +
+

+ Luyện TOEIC
thông minh
+ cùng AI +

+

+ Cá nhân hóa lộ trình học tập để bứt phá điểm số trong thời gian ngắn nhất. AI phân tích điểm yếu và tối ưu bài tập cho bạn.

+
+ + Bắt đầu ngay + + + Thử AI Writing + +
+
+
+
350+
+
Câu hỏi TOEIC
+
+
+
+
720
+
Từ vựng
+
+
+
+
AI
+
Writing Checker
+
+
-
-

AI Writing Checker

-

- Chấm điểm và sửa bài writing bằng AI -

+ + {/* Preview card — hidden on mobile */} +
+
+
+
+
Tiến độ tuần này
+
Bạn đang làm rất tốt!
+
+
+12%
+
+
+
+ Reading Score420/495 +
+
+
+
+
+
+
+ Listening Score380/495 +
+
+
+
+
+
+
+ local_fire_department +
14
+
Ngày Streak
+
+
+ star +
1,250
+
Điểm tích lũy
+
+
+
+
+ psychology +
+

+ AI gợi ý: Ôn thêm Part 5 — Ngữ pháp +

+
+
-
-

Từ vựng TOEIC

-

- Flashcard 6 chủ đề: Business, Finance, HR... -

+
+ + {/* Feature cards */} +
+

Tính năng nổi bật

+

Hệ sinh thái học tập toàn diện được thiết kế để tối ưu hoá điểm số.

+
+ {FEATURES.map((f) => ( + +
+ {f.icon} +
+

{f.title}

+

{f.desc}

+
+ {f.cta} + arrow_forward +
+ + ))}
-
+ + + {/* CTA banner */} +
+
+
+ emoji_events +
+
+

Sẵn sàng chinh phục 990 TOEIC?

+

+ {user + ? `Chào ${user.name}! Tiếp tục luyện thi hôm nay.` + : 'Đăng ký miễn phí để lưu tiến độ và luyện thi không giới hạn.'} +

+ {user ? ( + + Luyện thi ngay + + ) : ( + + )} +
+
+
) } diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..169b6cf --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,34 @@ +import { useEffect } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { useUser } from '@/hooks/use-auth' +import { LoginForm } from '@/components/auth/LoginForm' + +export function LoginPage() { + const user = useUser() + const navigate = useNavigate() + + useEffect(() => { + if (user) navigate({ to: '/' }) + }, [user, navigate]) + + return ( +
+
+
+
+ school + TOEIC Luyện thi +
+

Đăng nhập tài khoản

+
+ +
+ navigate({ to: '/' })} + onSwitchToRegister={() => navigate({ to: '/auth/register' })} + /> +
+
+
+ ) +} diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx new file mode 100644 index 0000000..e9eb1d2 --- /dev/null +++ b/src/pages/Register.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { useUser } from '@/hooks/use-auth' +import { RegisterForm } from '@/components/auth/RegisterForm' + +export function RegisterPage() { + const user = useUser() + const navigate = useNavigate() + + useEffect(() => { + if (user) navigate({ to: '/' }) + }, [user, navigate]) + + return ( +
+
+
+
+ school + TOEIC Luyện thi +
+

Tạo tài khoản miễn phí

+

Không cần xác nhận email — dùng ngay lập tức

+
+ +
+ navigate({ to: '/' })} + onSwitchToLogin={() => navigate({ to: '/auth/login' })} + /> +
+
+
+ ) +} diff --git a/src/pages/TestResult.tsx b/src/pages/TestResult.tsx index f49c0a5..ac638ed 100644 --- a/src/pages/TestResult.tsx +++ b/src/pages/TestResult.tsx @@ -1,8 +1,223 @@ +import { useEffect, useRef } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { cn } from '@/lib/utils' +import { useTestStore } from '@/store/test-store' +import { useRequireAuth } from '@/hooks/use-require-auth' +import { useAuthStore } from '@/store/auth-store' +import { saveTestResult } from '@/lib/progress-service' + +function formatTime(s: number) { + const m = Math.floor(s / 60) + const sec = s % 60 + if (m === 0) return `${sec}s` + return `${m}m ${sec}s` +} + export function TestResult() { + const navigate = useNavigate() + const { partId, partName, questions, answers, timeUsed, reset } = useTestStore() + const { isAuthenticated, isLoading } = useRequireAuth() + const user = useAuthStore((s) => s.user) + const savedRef = useRef(false) + + useEffect(() => { + if (isLoading) return + if (!isAuthenticated) navigate({ to: '/toeic' }) + }, [isLoading, isAuthenticated, navigate]) + + // Save test result once when page mounts (fire-and-forget) + useEffect(() => { + if (!user || savedRef.current || questions.length === 0) return + savedRef.current = true + saveTestResult(user.id, { + partId, + partName, + score: answers.filter((a, i) => a === questions[i]?.correctAnswer).length, + total: questions.length, + timeUsed, + answers: questions.map((q, i) => ({ + questionId: q.id, + selected: answers[i], + correct: answers[i] === q.correctAnswer, + })), + }) + }, [user, questions, answers, partId, partName, timeUsed]) + + const correct = answers.filter((a, i) => a === questions[i]?.correctAnswer).length + const wrong = answers.filter((a, i) => a !== null && a !== questions[i]?.correctAnswer).length + const skipped = answers.filter((a) => a === null).length + const total = questions.length + const percent = total > 0 ? Math.round((correct / total) * 100) : 0 + + const circumference = 2 * Math.PI * 52 + const offset = circumference - (percent / 100) * circumference + + function handleRetry() { + navigate({ to: '/toeic/session' }) + } + + function handleHome() { + reset() + navigate({ to: '/' }) + } + + if (questions.length === 0) { + return ( +
+

Không có dữ liệu bài thi.

+ +
+ ) + } + return ( -
-

Kết quả

-

Kết quả và đáp án — placeholder

+
+ {/* Score header */} +
+
+ {/* Circle */} +
+ + + = 70 ? '#16a34a' : percent >= 50 ? '#2563eb' : '#dc2626'} + strokeWidth="8" + strokeLinecap="round" + strokeDasharray={circumference} + strokeDashoffset={offset} + className="transition-all duration-700" + /> + +
+ {correct}/{total} + điểm +
+
+ + {/* Stats */} +
+
+ {percent >= 80 ? 'Xuất sắc!' : percent >= 60 ? 'Hoàn thành!' : 'Cố gắng hơn nhé!'} +
+
+ Part {partId} — {partName} +
+
+
+
{correct}
+
Đúng
+
+
+
{wrong}
+
Sai
+
+
+
{skipped}
+
Bỏ qua
+
+
+
{formatTime(timeUsed)}
+
Thời gian
+
+
+
+ + {/* Actions */} +
+ + +
+
+
+ + {/* Answer review */} +
+

Xem lại đáp án

+
+ {questions.map((q, i) => { + const userAnswer = answers[i] + const isCorrect = userAnswer === q.correctAnswer + const isSkipped = userAnswer === null + + return ( +
+
+ + {i + 1} + +
+

{q.text}

+
+ {q.options.map((opt, j) => ( + + {['A', 'B', 'C', 'D'][j]}. {opt} + + ))} +
+ {q.explanation && ( +

+ Giải thích: + {q.explanation} +

+ )} +
+ + {isCorrect ? ( + check_circle + ) : isSkipped ? ( + remove_circle + ) : ( + cancel + )} + +
+
+ ) + })} +
+
) } diff --git a/src/pages/TestSession.tsx b/src/pages/TestSession.tsx index 7922f43..6850cc7 100644 --- a/src/pages/TestSession.tsx +++ b/src/pages/TestSession.tsx @@ -1,8 +1,207 @@ +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { cn } from '@/lib/utils' +import { useTestStore } from '@/store/test-store' +import { useRequireAuth } from '@/hooks/use-require-auth' + +const TOTAL_SECONDS = 600 // 10 minutes +const ANSWER_LABELS = ['A', 'B', 'C', 'D'] + +function formatTime(s: number) { + return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}` +} + export function TestSession() { + const navigate = useNavigate() + const { partId, partName, questions, answers, setAnswer, submitExam } = useTestStore() + const [currentQ, setCurrentQ] = useState(0) + const [timeLeft, setTimeLeft] = useState(TOTAL_SECONDS) + const { isAuthenticated, isLoading } = useRequireAuth() + + const handleSubmit = useCallback(() => { + submitExam(TOTAL_SECONDS - timeLeft) + navigate({ to: '/toeic/result' }) + }, [submitExam, navigate, timeLeft]) + + // Countdown + useEffect(() => { + if (questions.length === 0) return + const id = setInterval(() => { + setTimeLeft((t) => { + if (t <= 1) { clearInterval(id); handleSubmit(); return 0 } + return t - 1 + }) + }, 1000) + return () => clearInterval(id) + }, [questions.length, handleSubmit]) + + // Redirect if no exam started or not authenticated (wait for auth init) + useEffect(() => { + if (isLoading) return + if (!isAuthenticated || questions.length === 0) navigate({ to: '/toeic' }) + }, [isLoading, isAuthenticated, questions.length, navigate]) + + if (questions.length === 0) return null + + const question = questions[currentQ] + const answeredCount = answers.filter((a) => a !== null).length + const isUrgent = timeLeft < 60 + return ( -
-

Làm bài

-

Trang làm bài — placeholder

+
+ {/* Mobile progress bar */} +
+
+ Part {partId} — Câu {currentQ + 1}/{questions.length} + + {formatTime(timeLeft)} + +
+
+
+
+
+ +
+ {/* Left: Question */} +
+
+
+ + Câu {currentQ + 1} + + Part {partId} — {partName} +
+

+ {question.text} +

+
+ {question.options.map((opt, i) => { + const selected = answers[currentQ] === i + return ( + + ) + })} +
+
+ + {/* Navigation */} +
+ + {currentQ + 1} / {questions.length} + {currentQ < questions.length - 1 ? ( + + ) : ( + + )} +
+
+ + {/* Right panel — desktop only */} +
+ {/* Timer */} +
+
Thời gian còn lại
+
+ {formatTime(timeLeft)} +
+
phút : giây
+
+ + {/* Question dots */} +
+
+ Danh sách câu · {answeredCount}/{questions.length} đã trả lời +
+
+ {questions.map((_, i) => ( + + ))} +
+
+ Đã trả lời + Chưa làm +
+
+ + +
+
+ + {/* Mobile submit */} +
+ +
) } diff --git a/src/pages/ToeicPractice.tsx b/src/pages/ToeicPractice.tsx index 5b61e43..09fec59 100644 --- a/src/pages/ToeicPractice.tsx +++ b/src/pages/ToeicPractice.tsx @@ -1,25 +1,108 @@ -const PARTS = [ - { id: 1, name: "Part 1", desc: "Photographs" }, - { id: 2, name: "Part 2", desc: "Question-Response" }, - { id: 3, name: "Part 3", desc: "Conversations" }, - { id: 4, name: "Part 4", desc: "Short Talks" }, - { id: 5, name: "Part 5", desc: "Incomplete Sentences" }, - { id: 6, name: "Part 6", desc: "Text Completion" }, - { id: 7, name: "Part 7", desc: "Reading Comprehension" }, -] +import { useState } from 'react' +import { useNavigate } from '@tanstack/react-router' +import { CircularProgress } from '@/components/CircularProgress' +import { useTestStore } from '@/store/test-store' +import { TOEIC_PARTS } from '@/data/mock-data' +import { fetchQuestions } from '@/hooks/use-questions' +import { useRequireAuth } from '@/hooks/use-require-auth' export function ToeicPractice() { + const navigate = useNavigate() + const { startExam } = useTestStore() + const [loadingPartId, setLoadingPartId] = useState(null) + const { requireAuth } = useRequireAuth() + + async function handleSelectPart(partId: number, partName: string) { + if (!requireAuth()) return + setLoadingPartId(partId) + try { + const questions = await fetchQuestions(partId, 10) + startExam(partId, partName, questions) + navigate({ to: '/toeic/session' }) + } catch (err) { + console.error('Failed to load questions:', err) + } finally { + setLoadingPartId(null) + } + } + return ( -
-

Luyện đề TOEIC

-

Chọn Part để bắt đầu luyện tập

-
- {PARTS.map((part) => ( -
-
{part.name}
-
{part.desc}
-
+
+
+

Chọn Part TOEIC

+

+ Hệ thống ôn luyện theo cấu trúc bài thi TOEIC thực tế. Chọn phần cụ thể để bắt đầu. +

+
+ +
+ {TOEIC_PARTS.map((part) => ( + ))} + + {/* Full Test card */} + +
+ + {/* Tip */} +
+ tips_and_updates +
+
Mẹo luyện thi
+

+ Bắt đầu từ Part 5 (Điền từ) — phần mang lại điểm nhanh nhất vì không phụ thuộc kỹ năng nghe. Mỗi ngày 20 câu, sau 2 tuần bạn sẽ thấy cải thiện rõ rệt. +

+
) diff --git a/src/pages/Vocabulary.tsx b/src/pages/Vocabulary.tsx index 0971431..7cbce71 100644 --- a/src/pages/Vocabulary.tsx +++ b/src/pages/Vocabulary.tsx @@ -1,16 +1,250 @@ -const TOPICS = ["Business", "Office", "Travel", "Finance", "HR", "Marketing"] +import { useState } from 'react' +import { cn } from '@/lib/utils' +import { FlashCard } from '@/components/FlashCard' +import { useVocabStore } from '@/store/vocab-store' +import { useVocab } from '@/hooks/use-vocab' +import { VOCAB_TOPICS } from '@/types' +import type { VocabTopic, VocabWord } from '@/types' +import { useRequireAuth } from '@/hooks/use-require-auth' + +const GUEST_CARD_LIMIT = 5 export function Vocabulary() { + const { currentTopic, currentIndex, knownWords, setTopic, setCurrentIndex, markKnown, markNeedReview } = useVocabStore() + const [isFlipped, setIsFlipped] = useState(false) + const { isAuthenticated, requireAuth } = useRequireAuth() + + const { data: allVocab = [], isLoading, isError } = useVocab() + const filtered: VocabWord[] = currentTopic === 'Tất cả' + ? allVocab + : allVocab.filter((w) => w.topic === currentTopic) + const safeIndex = Math.min(currentIndex, Math.max(0, filtered.length - 1)) + const word = filtered[safeIndex] + const knownInFiltered = filtered.filter((w) => knownWords.includes(w.id)).length + + function handleSetTopic(topic: VocabTopic) { + setTopic(topic) + setCurrentIndex(0) + setIsFlipped(false) + } + + function handlePrev() { + if (safeIndex > 0) { + setCurrentIndex(safeIndex - 1) + setIsFlipped(false) + } + } + + function handleNext() { + if (safeIndex < filtered.length - 1) { + if (!isAuthenticated && safeIndex >= GUEST_CARD_LIMIT - 1) { + requireAuth() + return + } + setCurrentIndex(safeIndex + 1) + setIsFlipped(false) + } + } + + function handleMarkKnown() { + if (!isAuthenticated && safeIndex >= GUEST_CARD_LIMIT - 1) { + requireAuth() + return + } + if (word) markKnown(word.id) + handleNext() + } + + function handleMarkReview() { + if (!isAuthenticated && safeIndex >= GUEST_CARD_LIMIT - 1) { + requireAuth() + return + } + if (word) markNeedReview(word.id) + handleNext() + } + + const recentKnown = knownWords + .map((id) => allVocab.find((w) => w.id === id)) + .filter((w): w is VocabWord => w !== undefined) + .slice(-5) + .reverse() + return ( -
-

Từ vựng TOEIC

-

Chọn chủ đề để học flashcard

-
- {TOPICS.map((topic) => ( -
- {topic} +
+ {/* Mobile topic chips */} +
+
+ {VOCAB_TOPICS.map((topic) => ( + + ))} +
+
+ +
+ {/* Left: Topic menu — desktop only */} +
+
+
Chủ đề
+ {VOCAB_TOPICS.map((topic) => ( + + ))}
- ))} +
+ + {/* Center: Flashcard */} +
+ {isLoading ? ( +
+
+

Đang tải từ vựng...

+
+ ) : isError ? ( +
+

Không thể tải từ vựng. Vui lòng thử lại.

+
+ ) : filtered.length === 0 ? ( +
+

Không có từ vựng cho chủ đề này.

+
+ ) : ( + <> + {/* Progress */} +
+ + {safeIndex + 1} / {filtered.length} từ + + + {knownInFiltered}/{filtered.length} đã thuộc + +
+
+
0 ? ((safeIndex + 1) / filtered.length) * 100 : 0}%` }} + /> +
+ + {word && ( + setIsFlipped((v) => !v)} + /> + )} + + {/* Navigation */} +
+ + + {/* Mark buttons */} +
+ + +
+ + +
+ + )} +
+ + {/* Right: Stats — desktop only */} +
+ {/* Today stats */} +
+
Thống kê
+
+
+ Đã xem + {safeIndex + 1} +
+
+ Đã thuộc + {knownWords.length} +
+
+ Tổng từ + {allVocab.length} +
+
+
+
+ local_fire_department + Streak hôm nay +
+
{Math.min(safeIndex + 1, 99)}
+
+
+ + {/* Recently known */} + {recentKnown.length > 0 && ( +
+
Vừa thuộc
+
+ {recentKnown.map((w) => w && ( +
+ + {w.word} + {w.meaningVi} +
+ ))} +
+
+ )} +
) diff --git a/src/pages/WritingChecker.tsx b/src/pages/WritingChecker.tsx index 30eba38..eba1638 100644 --- a/src/pages/WritingChecker.tsx +++ b/src/pages/WritingChecker.tsx @@ -1,16 +1,232 @@ +import { useState, useEffect } from 'react' +import { cn } from '@/lib/utils' +import { useWritingCheck } from '@/hooks/use-writing-check' +import { getRemainingChecks } from '@/utils/rate-limiter' +import { useRequireAuth } from '@/hooks/use-require-auth' +import { useAuthStore } from '@/store/auth-store' +import { countTodayWritingSubmissions } from '@/lib/progress-service' + +const MAX_CHARS = 1000 +const GUEST_LIMIT = 3 +const AUTH_LIMIT = 10 + export function WritingChecker() { + const [text, setText] = useState('') + const [improvedExpanded, setImprovedExpanded] = useState(false) + const [remaining, setRemaining] = useState(getRemainingChecks) + + const { mutate: checkWriting, isPending, isError, error, data: feedback } = useWritingCheck() + const { requireAuth } = useRequireAuth() + const user = useAuthStore((s) => s.user) + + const dailyLimit = user ? AUTH_LIMIT : GUEST_LIMIT + + // Fetch server-side remaining count for authenticated users + useEffect(() => { + if (!user) { + setRemaining(getRemainingChecks()) + return + } + countTodayWritingSubmissions(user.id).then((used) => { + setRemaining(AUTH_LIMIT - used) + }) + }, [user]) + + const charCount = text.length + const canSubmit = text.trim().length > 0 && remaining > 0 && charCount <= MAX_CHARS && !isPending + + function handleSubmit() { + if (!requireAuth()) return + if (!canSubmit) return + checkWriting(text, { + onSuccess: () => { + if (user) { + countTodayWritingSubmissions(user.id).then((used) => setRemaining(AUTH_LIMIT - used)) + } else { + setRemaining(getRemainingChecks()) + } + }, + onError: () => { + if (!user) setRemaining(getRemainingChecks()) + }, + }) + } + return ( -
-

AI Writing Checker

-

- Nhập bài writing để nhận phản hồi từ AI -

- +
+ 245/500 characters +
+
+
+ +
+bolt + Còn 2/3 lượt hôm nay +
+
+ + + +
+
+ + \ No newline at end of file diff --git a/stitch-exports/screen-05-writing/design.png b/stitch-exports/screen-05-writing/design.png new file mode 100644 index 0000000000000000000000000000000000000000..85f5143ff4b05becf5b62559a9d332b7463ee43a GIT binary patch literal 77074 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 pG3Ok=kGNg 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 +- `
+ + +
+ +
+

Chủ đề học

+
+ + + + + + + +
+
+
+lightbulb +

Pro-Tip

+
+

Học theo cụm từ giúp bạn ghi nhớ lâu hơn 40% so với từ đơn lẻ.

+
+
+ +
+
+
+

Thẻ ghi nhớ

+

Chủ đề: Business

+
+
+ +
+
+ +
+
+
+ +
+
+Business +
+

negotiate

+

/nɪˈɡoʊʃieɪt/

+
+touch_app +

Nhấn để lật thẻ

+
+
+ + + +
+
+ +
+ + +
+ +
+
+

32/120 từ đã thuộc

+

27%

+
+
+
+
+
+
+
+ +
+
+ +
+

Hôm nay

+
+
+
+menu_book +
+
+

12

+

từ đã học hôm nay

+
+
+
+
+task_alt +
+
+

8

+

đã thuộc

+
+
+
+
+🔥 +

5 ngày streak

+
+workspace_premium +
+
+
+ +
+

Vừa thuộc

+
+
+
+
+

Investment

+

/ɪnˈvest.mənt/

+
+check +
+
+
+
+

Collaborate

+

/kəˈlæb.ə.reɪt/

+
+check +
+
+
+
+

Strategic

+

/strəˈtiː.dʒɪk/

+
+check +
+
+
+ +
+Background decoration +
+

Môi trường công sở là chủ đề phổ biến nhất trong TOEIC Listening.

+
+
+
+
+
+
+ \ No newline at end of file diff --git a/stitch-exports/screen-06-flashcard/design.png b/stitch-exports/screen-06-flashcard/design.png new file mode 100644 index 0000000000000000000000000000000000000000..874cadf86a8a571ab2f28859399d82a93ed85017 GIT binary patch literal 48909 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 literal 0 HcmV?d00001 diff --git a/stitch-exports/toeic-app-design.html b/stitch-exports/toeic-app-design.html new file mode 100644 index 0000000..eeab147 --- /dev/null +++ b/stitch-exports/toeic-app-design.html @@ -0,0 +1,1290 @@ + + + + + +EnglishAI — Luyện TOEIC thông minh + + + + + + + + + + + + +
+
Trang chủ
+
+ + +
+
+ + + + + + + +
+
+ + +
+
+
+ auto_awesome + AI-Powered Learning +
+

+ Luyện TOEIC
thông minh
+ cùng AI +

+

+ Cá nhân hóa lộ trình học tập để bứt phá điểm số trong thời gian ngắn nhất. AI phân tích điểm yếu và tối ưu bài tập cho bạn. +

+
+ + +
+ +
+
350+
Câu hỏi TOEIC
+
+
720
Từ vựng
+
+
AI
Writing Checker
+
+
+ +
+
+
+
+
Tiến độ tuần này
+
Bạn đang làm rất tốt!
+
+
+12%
+
+
+
+ Reading Score420/495 +
+
+
+
+
+ Listening Score380/495 +
+
+
+
+
+ local_fire_department +
14
+
Ngày Streak
+
+
+ star +
1,250
+
Điểm tích lũy
+
+
+
+
+ psychology +
+
AI gợi ý: Ôn thêm Part 5 — Ngữ pháp
+
+
+
+
+ + +
+

Tính năng nổi bật

+

Hệ sinh thái học tập toàn diện được thiết kế để tối ưu hoá điểm số.

+
+
+
+ assignment +
+

Luyện đề TOEIC

+

Kho đề thi cập nhật theo cấu trúc mới nhất. Phân tích điểm yếu chi tiết.

+
+ Bắt đầu ngay arrow_forward +
+
+
+
+ auto_fix_high +
+

AI Chấm Writing

+

Phản hồi tức thì về ngữ pháp, từ vựng, cấu trúc và bài viết mẫu.

+
+ Thử ngay arrow_forward +
+
+
+
+ menu_book +
+

Từ vựng thông minh

+

720 từ TOEIC theo 6 chủ đề. Flashcard với phương pháp lặp lại ngắt quãng.

+
+ Khám phá arrow_forward +
+
+
+
+ + +
+
+
+ emoji_events +
+
+

Sẵn sàng chinh phục 990 TOEIC?

+

Không cần đăng nhập — dùng thử ngay miễn phí hôm nay.

+ +
+
+
+ +
+
+ + + + + +
+
+
+

Chọn Part TOEIC

+

Hệ thống ôn luyện theo cấu trúc bài thi TOEIC thực tế. Chọn phần cụ thể để bắt đầu.

+
+ + +
+ +
+
+
+ image +
+
+ + + + + 60% +
+
+
Part 1
+
Mô tả hình ảnh
+
+ list_alt 45 câu hỏi +
+
+ +
+
+
+ question_answer +
+
+ + + + + 40% +
+
+
Part 2
+
Hỏi-đáp
+
+ list_alt 30 câu hỏi +
+
+ +
+
+
+ forum +
+
+ + + + + 25% +
+
+
Part 3
+
Đoạn hội thoại
+
+ list_alt 39 câu hỏi +
+
+ +
+
+
+ record_voice_over +
+
+ + + + + 10% +
+
+
Part 4
+
Bài nói
+
+ list_alt 30 câu hỏi +
+
+ +
+
+
+ history_edu +
+
+ + + + + 80% +
+
+
Part 5
+
Điền từ
+
+ list_alt 40 câu hỏi +
+
+ +
+
+
+ article +
+
+ + + + + 50% +
+
+
Part 6
+
Điền đoạn
+
+ list_alt 16 câu hỏi +
+
+ +
+
+
+ chrome_reader_mode +
+
+ + + + + 30% +
+
+
Part 7
+
Đọc hiểu
+
+ list_alt 54 câu hỏi +
+
+ +
+
+ workspace_premium +
+
+
+ military_tech +
+
Full Test
+
Mô phỏng thi thật 2h
+
+ timer 120 phút • 200 câu +
+
+
+
+ + +
+ tips_and_updates +
+
Mẹo luyện thi
+

Bắt đầu từ Part 5 (Điền từ) — đây là phần mang lại điểm số nhanh nhất vì không phụ thuộc vào kỹ năng nghe. Mỗi ngày 20 câu, sau 2 tuần bạn sẽ thấy cải thiện rõ rệt.

+
+
+ +
+
+ + + + + +
+
+ +
+
+ Part 2 — Câu 1/10 + 10:00 +
+
+
+ +
+ +
+
+
+
Câu 1
+
Part 2 — Hỏi-đáp
+
+

+ What does the man suggest the woman do about the budget report? +

+
+ +
+
+ +
+ +
1 / 10
+ +
+
+ + + +
+ + +
+ +
+
+
+ + + + + +
+
+ + +
+ +
+
+ + + + +
+
8/10
+
điểm
+
+
+
+ +
+

Hoàn thành!

+

Bạn đã hoàn thành bài kiểm tra Part 2 — Hỏi-đáp

+
+
+ check_circle 8 Đúng +
+
+ cancel 2 Sai +
+
+ schedule 4:32 +
+
+
+ +
+ + +
+
+ + +
+ +
+

Phân tích kết quả

+
+
+
+ Part 2 — Hỏi-đáp80% +
+
+
+
+
+
+
+

Điểm mạnh

+
+
checkCâu hỏi thì hiện tại
+
checkCâu hỏi cú pháp đơn giản
+
+

Cần cải thiện

+
+
closeCâu hỏi gián tiếp
+
warningDiễn đạt mơ hồ
+
+
+
+
+ + +
+
Xem lại đáp án
+
+
+
+
+
+ +
+
+ + + + + +
+
+
+

AI Chấm Writing

+

Nhập bài writing của bạn để nhận phản hồi chi tiết từ AI về ngữ pháp, từ vựng và cấu trúc.

+
+ +
+ +
+
+
+ +
+ bolt + Còn 2/3 lượt hôm nay +
+
+ +
+ 356 / 1000 ký tự + +
+
+ Gợi ý đề bài: Viết một email thông báo thay đổi chính sách làm việc cho nhân viên (80–150 từ). Yêu cầu: nêu lý do, giải thích thay đổi, và trình bày rõ kế hoạch triển khai. +
+
+
+ + +
+
+ +
+
Band Score ước tính
+
6.5
+
Upper Intermediate
+
+
+ + + + + + + + + +
+ +
+

The company has decided to implement a new remote work policy starting next month. All employees will be able to work from home for three days per week. This change is expected to enhance work-life balance and boost overall productivity. Nevertheless, some managers are concerned about communication challenges and team collaboration. To address these concerns, the HR department will organize training sessions to help teams adapt to this new arrangement effectively.

+
+
+
+
+
+
+ +
+
+ + + + + +
+
+
+

Từ vựng TOEIC

+

Học từ vựng theo chủ đề với Flashcard và phương pháp lặp lại ngắt quãng.

+
+ + +
+ +
+ +
+ +
+
+
Chủ đề
+
+ +
+
+
+ + +
+ +
+
+ +
+
negotiate
+
/nɪˈɡoʊʃieɪt/
+
Business
+
+ touch_app + Nhấn để xem nghĩa +
+
+ +
+
đàm phán
+
Nghĩa tiếng Việt
+
+ "We need to negotiate the contract terms before signing." +
+
+
+
+ + +
+ + +
+ + +
+
+ 32 / 120 từ đã thuộc + 27% +
+
+
+ + +
+ + 1 / 24 + +
+
+ + +
+
+
Hôm nay
+
+
+
+ schoolĐã học +
+ 12 từ +
+
+
+ check_circleĐã thuộc +
+ 8 từ +
+
+
+ local_fire_departmentStreak +
+ 🔥 5 ngày +
+
+
+ +
+
Vừa thuộc
+
+
+ check_circle + collaboratehợp tác +
+
+ check_circle + submitnộp, gửi đi +
+
+ check_circle + delegateuỷ quyền +
+
+
+
+
+ +
+
+ + + + + + + + + diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest new file mode 100644 index 0000000..47c148f --- /dev/null +++ b/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.84.2 \ No newline at end of file diff --git a/supabase/.temp/gotrue-version b/supabase/.temp/gotrue-version new file mode 100644 index 0000000..5bbfd4d --- /dev/null +++ b/supabase/.temp/gotrue-version @@ -0,0 +1 @@ +v2.188.1 \ No newline at end of file diff --git a/supabase/.temp/pooler-url b/supabase/.temp/pooler-url new file mode 100644 index 0000000..62d4609 --- /dev/null +++ b/supabase/.temp/pooler-url @@ -0,0 +1 @@ +postgresql://postgres.eiyunmdvhwwtsqsyjotn@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres \ No newline at end of file diff --git a/supabase/.temp/postgres-version b/supabase/.temp/postgres-version new file mode 100644 index 0000000..fc1481f --- /dev/null +++ b/supabase/.temp/postgres-version @@ -0,0 +1 @@ +17.6.1.104 \ No newline at end of file diff --git a/supabase/.temp/project-ref b/supabase/.temp/project-ref new file mode 100644 index 0000000..cb5a17b --- /dev/null +++ b/supabase/.temp/project-ref @@ -0,0 +1 @@ +eiyunmdvhwwtsqsyjotn \ No newline at end of file diff --git a/supabase/.temp/rest-version b/supabase/.temp/rest-version new file mode 100644 index 0000000..908947a --- /dev/null +++ b/supabase/.temp/rest-version @@ -0,0 +1 @@ +v14.5 \ No newline at end of file diff --git a/supabase/.temp/storage-migration b/supabase/.temp/storage-migration new file mode 100644 index 0000000..2494908 --- /dev/null +++ b/supabase/.temp/storage-migration @@ -0,0 +1 @@ +operation-ergonomics \ No newline at end of file diff --git a/supabase/.temp/storage-version b/supabase/.temp/storage-version new file mode 100644 index 0000000..14186a2 --- /dev/null +++ b/supabase/.temp/storage-version @@ -0,0 +1 @@ +v1.48.20 \ No newline at end of file diff --git a/supabase/functions/writing-check/index.ts b/supabase/functions/writing-check/index.ts new file mode 100644 index 0000000..bf58962 --- /dev/null +++ b/supabase/functions/writing-check/index.ts @@ -0,0 +1,73 @@ +// Supabase Edge Function: writing-check +// Uses GLM API (OpenAI-compatible) to analyze English writing submissions. +// Deploy: supabase functions deploy writing-check +// Secrets: supabase secrets set GLM_API_KEY= + +import OpenAI from "npm:openai@^4" + +const glm = new OpenAI({ + apiKey: Deno.env.get("GLM_API_KEY") ?? "", + baseURL: "https://open.bigmodel.cn/api/paas/v4/", +}) + +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", + "Content-Type": "application/json", +} + +// Instructs the model to return a strict JSON structure with Vietnamese feedback. +const SYSTEM_PROMPT = `You are an expert English writing teacher specialising in TOEIC and IELTS assessment. +Analyse the student's writing and respond ONLY with valid JSON — no markdown, no extra text: +{ + "score": "", + "grammar": ["", ...], + "vocabulary": ["", ...], + "structure": "<2–3 sentence structure assessment in Vietnamese>", + "improved_version": "", + "summary": "<2–3 sentence overall assessment in Vietnamese>" +}` + +Deno.serve(async (req: Request) => { + // Handle CORS pre-flight + if (req.method === "OPTIONS") { + return new Response("ok", { headers: CORS_HEADERS }) + } + + try { + const { content } = await req.json() as { content: string } + + if (!content || content.trim().length < 10) { + return new Response( + JSON.stringify({ error: "Bài viết quá ngắn. Vui lòng nhập ít nhất 10 ký tự." }), + { status: 400, headers: CORS_HEADERS }, + ) + } + + const completion = await glm.chat.completions.create({ + // GLM-4-32B-0414-128K: cheapest paid model at $0.1/$0.1 per 1M tokens. + // Override via: supabase secrets set GLM_MODEL= + model: Deno.env.get("GLM_MODEL") ?? "GLM-4-32B-0414-128K", + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: `Analyse this writing:\n\n${content.slice(0, 2000)}` }, + ], + temperature: 0.3, + max_tokens: 1500, + }) + + const raw = completion.choices[0]?.message?.content ?? "{}" + + // Strip markdown code fences if the model adds them despite instructions + const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim() + const feedback = JSON.parse(cleaned) + + return new Response(JSON.stringify(feedback), { headers: CORS_HEADERS }) + } catch (err) { + console.error("writing-check error:", err) + return new Response( + JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." }), + { status: 500, headers: CORS_HEADERS }, + ) + } +}) diff --git a/supabase/migrations/001_user_progress.sql b/supabase/migrations/001_user_progress.sql new file mode 100644 index 0000000..fd03e26 --- /dev/null +++ b/supabase/migrations/001_user_progress.sql @@ -0,0 +1,53 @@ +-- Migration 001: user_progress + writing_submissions tables with RLS +-- Run in Supabase Dashboard → SQL Editor (after schema.sql) + +-- ============================================================ +-- Test results + vocab progress +-- ============================================================ +CREATE TABLE IF NOT EXISTS user_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + type TEXT NOT NULL CHECK (type IN ('test', 'vocab')), + data JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_user_progress_user_id ON user_progress(user_id); +CREATE INDEX IF NOT EXISTS idx_user_progress_type ON user_progress(user_id, type); + +-- ============================================================ +-- Writing submissions with AI feedback +-- ============================================================ +CREATE TABLE IF NOT EXISTS writing_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + feedback JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_writing_submissions_user_id ON writing_submissions(user_id); + +-- ============================================================ +-- Row Level Security +-- ============================================================ +ALTER TABLE user_progress ENABLE ROW LEVEL SECURITY; +ALTER TABLE writing_submissions ENABLE ROW LEVEL SECURITY; + +-- user_progress: authenticated users own their rows +CREATE POLICY "Users can insert own progress" + ON user_progress FOR INSERT + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can read own progress" + ON user_progress FOR SELECT + USING (auth.uid() = user_id); + +-- writing_submissions: authenticated users own their rows +CREATE POLICY "Users can insert own submissions" + ON writing_submissions FOR INSERT + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can read own submissions" + ON writing_submissions FOR SELECT + USING (auth.uid() = user_id); diff --git a/supabase/postman-collection.json b/supabase/postman-collection.json new file mode 100644 index 0000000..efd874c --- /dev/null +++ b/supabase/postman-collection.json @@ -0,0 +1,166 @@ +{ + "info": { + "name": "English Learning App — Supabase", + "_postman_id": "english-app-supabase", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "BASE_URL", + "value": "https://eiyunmdvhwwtsqsyjotn.supabase.co", + "type": "string" + }, + { + "key": "ANON_KEY", + "value": "PASTE_YOUR_ANON_KEY_HERE", + "type": "string", + "description": "Supabase Dashboard → Settings → API → anon public (eyJ...)" + } + ], + "item": [ + { + "name": "REST API", + "item": [ + { + "name": "GET questions (Part 2)", + "request": { + "method": "GET", + "url": { + "raw": "{{BASE_URL}}/rest/v1/questions?select=id,part,content,options,answer,explanation&part=eq.2&limit=3", + "host": ["{{BASE_URL}}"], + "path": ["rest", "v1", "questions"], + "query": [ + { "key": "select", "value": "id,part,content,options,answer,explanation" }, + { "key": "part", "value": "eq.2" }, + { "key": "limit", "value": "3" } + ] + }, + "header": [ + { "key": "apikey", "value": "{{ANON_KEY}}" }, + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" } + ] + } + }, + { + "name": "GET all questions", + "request": { + "method": "GET", + "url": { + "raw": "{{BASE_URL}}/rest/v1/questions?select=id,part,answer&order=part.asc", + "host": ["{{BASE_URL}}"], + "path": ["rest", "v1", "questions"], + "query": [ + { "key": "select", "value": "id,part,answer" }, + { "key": "order", "value": "part.asc" } + ] + }, + "header": [ + { "key": "apikey", "value": "{{ANON_KEY}}" }, + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" } + ] + } + }, + { + "name": "GET vocab (Business topic)", + "request": { + "method": "GET", + "url": { + "raw": "{{BASE_URL}}/rest/v1/vocab?select=id,word,phonetic,meaning_vi,topic,example&topic=eq.Business", + "host": ["{{BASE_URL}}"], + "path": ["rest", "v1", "vocab"], + "query": [ + { "key": "select", "value": "id,word,phonetic,meaning_vi,topic,example" }, + { "key": "topic", "value": "eq.Business" } + ] + }, + "header": [ + { "key": "apikey", "value": "{{ANON_KEY}}" }, + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" } + ] + } + }, + { + "name": "GET all vocab", + "request": { + "method": "GET", + "url": { + "raw": "{{BASE_URL}}/rest/v1/vocab?select=word,topic&order=topic.asc", + "host": ["{{BASE_URL}}"], + "path": ["rest", "v1", "vocab"], + "query": [ + { "key": "select", "value": "word,topic" }, + { "key": "order", "value": "topic.asc" } + ] + }, + "header": [ + { "key": "apikey", "value": "{{ANON_KEY}}" }, + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" } + ] + } + } + ] + }, + { + "name": "Edge Functions", + "item": [ + { + "name": "POST writing-check (short sentence)", + "request": { + "method": "POST", + "url": { + "raw": "{{BASE_URL}}/functions/v1/writing-check", + "host": ["{{BASE_URL}}"], + "path": ["functions", "v1", "writing-check"] + }, + "header": [ + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"The company have many employee who works very hard every days to achieve their goal.\"\n}" + } + } + }, + { + "name": "POST writing-check (paragraph)", + "request": { + "method": "POST", + "url": { + "raw": "{{BASE_URL}}/functions/v1/writing-check", + "host": ["{{BASE_URL}}"], + "path": ["functions", "v1", "writing-check"] + }, + "header": [ + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Dear Mr. Johnson, I am writing to inform you that our company will implement a new remote work policy starting from next month. All employees will be able to work from home for three days per week. This change is expect to improve work-life balance and increase productivity. However, some managers are concern about the impact on team collaboration. We will organize training session to help teams adapt to this new arrangement.\"\n}" + } + } + }, + { + "name": "POST writing-check (too short — expect 400)", + "request": { + "method": "POST", + "url": { + "raw": "{{BASE_URL}}/functions/v1/writing-check", + "host": ["{{BASE_URL}}"], + "path": ["functions", "v1", "writing-check"] + }, + "header": [ + { "key": "Authorization", "value": "Bearer {{ANON_KEY}}" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Hi\"\n}" + } + } + } + ] + } + ] +} diff --git a/supabase/schema.sql b/supabase/schema.sql new file mode 100644 index 0000000..56f03ed --- /dev/null +++ b/supabase/schema.sql @@ -0,0 +1,33 @@ +-- ============================================================ +-- Schema: English Learning App (TOEIC Focus) +-- Run this FIRST before seed.sql +-- ============================================================ + +-- TOEIC questions (Part 1–7) +CREATE TABLE IF NOT EXISTS questions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + part INT NOT NULL CHECK (part BETWEEN 1 AND 7), + type TEXT, + content TEXT NOT NULL, + options JSONB NOT NULL DEFAULT '[]', -- ["A. ...", "B. ...", "C. ...", "D. ..."] + answer TEXT NOT NULL, -- "A" | "B" | "C" | "D" + explanation TEXT, + audio_url TEXT, -- Part 1–4 (listening) + image_url TEXT, -- Part 1 (photos) + created_at TIMESTAMPTZ DEFAULT now() +); + +-- TOEIC vocabulary (6 topics) +CREATE TABLE IF NOT EXISTS vocab ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + word TEXT NOT NULL, + phonetic TEXT, + meaning_vi TEXT NOT NULL, + topic TEXT NOT NULL CHECK (topic IN ('Business', 'Office', 'Travel', 'Finance', 'HR', 'Marketing')), + example TEXT, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- Indexes for common query patterns +CREATE INDEX IF NOT EXISTS idx_questions_part ON questions(part); +CREATE INDEX IF NOT EXISTS idx_vocab_topic ON vocab(topic); diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..335beba --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,946 @@ +-- ============================================================ +-- Seed: English Learning App — TOEIC Questions (140) + Vocab +-- Run AFTER schema.sql +-- ============================================================ + +TRUNCATE TABLE questions, vocab RESTART IDENTITY CASCADE; + +-- ============================================================ +-- QUESTIONS — 140 total (20 per Part 1–7) +-- ============================================================ +INSERT INTO questions (part, type, content, options, answer, explanation) VALUES + +-- ============================================================ +-- PART 1 — Photo Description (20 questions) +-- ============================================================ +(1, 'photo', + 'A woman is sitting at a desk in an office, looking at a computer screen.', + '["A. The woman is typing on a keyboard.", "B. The woman is looking at a computer screen.", "C. The woman is talking on the phone.", "D. The woman is writing in a notebook."]', + 'B', + 'Hình ảnh cho thấy người phụ nữ đang nhìn vào màn hình máy tính, không phải đang gõ bàn phím hay nói chuyện điện thoại.'), + +(1, 'photo', + 'Two men are shaking hands in front of a building.', + '["A. The men are walking into the building.", "B. One man is handing a document to the other.", "C. The men are shaking hands.", "D. The men are sitting at a table."]', + 'C', + 'Hành động bắt tay là điểm nhận dạng chính trong hình ảnh này.'), + +(1, 'photo', + 'A group of people are gathered around a conference table with papers and laptops.', + '["A. The people are gathered around a conference table.", "B. The people are watching a presentation.", "C. The people are having lunch together.", "D. The people are standing outside an office."]', + 'A', + 'Hình ảnh mô tả nhóm người đang họp xung quanh bàn hội nghị có giấy tờ và laptop.'), + +(1, 'photo', + 'A delivery man is carrying boxes to the entrance of a store.', + '["A. A worker is stacking boxes in a warehouse.", "B. A customer is leaving the store with bags.", "C. A man is loading boxes onto a truck.", "D. A delivery man is carrying boxes to the store entrance."]', + 'D', + 'Người giao hàng đang mang thùng hàng đến cửa hàng — đây là mô tả chính xác nhất.'), + +(1, 'photo', + 'A woman is standing in front of a whiteboard, pointing at a chart.', + '["A. A woman is erasing the whiteboard.", "B. A woman is writing on the whiteboard.", "C. A woman is looking at a screen behind her.", "D. A woman is pointing at a chart on the whiteboard."]', + 'D', + 'Người phụ nữ đang chỉ vào biểu đồ trên bảng trắng, không phải đang xóa hay viết.'), + +(1, 'photo', + 'Shelves in a supermarket are fully stocked with various products.', + '["A. A worker is restocking the shelves.", "B. The shelves are empty and being cleaned.", "C. A customer is picking items off the shelves.", "D. The shelves are fully stocked with products."]', + 'D', + 'Kệ hàng đầy ắp sản phẩm — mô tả trạng thái của kệ, không phải hành động của người.'), + +(1, 'photo', + 'A man is talking on his mobile phone while walking down a hallway.', + '["A. A man is using a computer in the hallway.", "B. A man is talking on his mobile phone while walking.", "C. A man is waiting for an elevator.", "D. A man is reading a document in the hallway."]', + 'B', + 'Hình ảnh cho thấy người đàn ông đang nghe điện thoại và đi bộ trong hành lang.'), + +(1, 'photo', + 'Workers in hard hats are inspecting equipment at a construction site.', + '["A. Workers are resting near a construction vehicle.", "B. Workers are pouring concrete at a construction site.", "C. Workers in hard hats are inspecting equipment.", "D. Workers are carrying materials into a building."]', + 'C', + 'Công nhân đội mũ bảo hộ đang kiểm tra thiết bị — đây là điểm nhận dạng rõ ràng.'), + +(1, 'photo', + 'A receptionist is handing a visitor badge to a guest at the front desk.', + '["A. A receptionist is answering the phone.", "B. A receptionist is handing a badge to a visitor.", "C. A guest is signing a document at the front desk.", "D. Two employees are discussing at the front desk."]', + 'B', + 'Lễ tân đang đưa thẻ khách cho người đến thăm — đây là hành động chính trong hình.'), + +(1, 'photo', + 'A chef is preparing food in a restaurant kitchen.', + '["A. A chef is preparing food in the kitchen.", "B. A waiter is serving food to customers.", "C. A chef is washing dishes in the kitchen.", "D. A cook is cleaning the countertop."]', + 'A', + 'Đầu bếp đang chuẩn bị thức ăn trong bếp nhà hàng là mô tả phù hợp nhất.'), + +(1, 'photo', + 'Passengers are boarding an airplane at the gate.', + '["A. Passengers are waiting in the terminal.", "B. Passengers are collecting luggage.", "C. A plane is taking off from the runway.", "D. Passengers are boarding an airplane at the gate."]', + 'D', + 'Hành khách đang lên máy bay tại cổng — hành động được mô tả rõ ràng trong hình.'), + +(1, 'photo', + 'A woman is reading a menu at a restaurant table.', + '["A. A woman is ordering food from a waiter.", "B. A woman is eating a meal at a restaurant.", "C. A woman is paying the bill at a restaurant.", "D. A woman is reading a menu at a table."]', + 'D', + 'Người phụ nữ đang đọc thực đơn, chưa gọi món hay ăn.'), + +(1, 'photo', + 'A man is loading luggage into the trunk of a car.', + '["A. A man is unloading groceries from a car.", "B. A man is loading luggage into the trunk.", "C. A taxi driver is helping a passenger with bags.", "D. A man is repairing the car in a parking lot."]', + 'B', + 'Người đàn ông đang xếp hành lý vào cốp xe — hành động được nhìn thấy rõ trong hình.'), + +(1, 'photo', + 'An employee is photocopying documents at a copy machine.', + '["A. An employee is shredding documents.", "B. An employee is scanning documents on a scanner.", "C. An employee is photocopying documents at a copy machine.", "D. An employee is printing from a computer."]', + 'C', + 'Nhân viên đang photo tài liệu tại máy photocopy — đây là mô tả chính xác nhất.'), + +(1, 'photo', + 'A group of colleagues are eating lunch together in a break room.', + '["A. Colleagues are working together in a meeting room.", "B. Employees are lining up at a cafeteria.", "C. Workers are cleaning up after a meal.", "D. Colleagues are eating lunch together in a break room."]', + 'D', + 'Nhóm đồng nghiệp đang ăn trưa cùng nhau trong phòng nghỉ.'), + +(1, 'photo', + 'A technician is repairing a computer at a service desk.', + '["A. A technician is installing software on a computer.", "B. A customer is reporting an issue at the service desk.", "C. A technician is repairing a computer at a service desk.", "D. A clerk is operating a computer at the front desk."]', + 'C', + 'Kỹ thuật viên đang sửa máy tính tại quầy dịch vụ — đây là hành động chính.'), + +(1, 'photo', + 'Several cars are parked in a parking lot near a shopping mall.', + '["A. Several cars are parked in a parking lot.", "B. A traffic officer is directing cars in a lot.", "C. Cars are entering the parking lot.", "D. Workers are painting lines in the parking lot."]', + 'A', + 'Hình ảnh cho thấy nhiều xe đang đậu trong bãi đỗ xe — không có hoạt động nào đang diễn ra.'), + +(1, 'photo', + 'A woman is watering plants on a balcony.', + '["A. A woman is arranging flowers inside a room.", "B. A woman is watering plants on a balcony.", "C. A gardener is mowing the lawn outside.", "D. A woman is sweeping the balcony floor."]', + 'B', + 'Người phụ nữ đang tưới cây trên ban công — hành động rõ ràng trong hình ảnh.'), + +(1, 'photo', + 'A man is presenting a report at a podium in a conference hall.', + '["A. A man is asking questions from the audience.", "B. A man is distributing handouts to the audience.", "C. A man is setting up equipment in a conference hall.", "D. A man is presenting at a podium in a conference hall."]', + 'D', + 'Người đàn ông đang thuyết trình tại bục phát biểu trong hội trường.'), + +(1, 'photo', + 'Boxes of merchandise are stacked on pallets in a warehouse.', + '["A. Workers are unpacking boxes from a delivery truck.", "B. Shelves in a store are being restocked.", "C. Boxes of merchandise are stacked on pallets in a warehouse.", "D. A forklift is moving pallets in a storage area."]', + 'C', + 'Thùng hàng được xếp trên pallet trong kho — không có người hay thiết bị đang hoạt động.'), + +-- ============================================================ +-- PART 2 — Question & Response (20 questions) +-- ============================================================ +(2, 'q_and_a', + 'When does the next quarterly meeting start?', + '["A. It starts at 9 a.m. on Thursday.", "B. The meeting was very productive.", "C. Yes, I attended last quarter.", "D. (No appropriate response)"]', + 'A', + 'Câu hỏi hỏi về thời gian — đáp án A trả lời trực tiếp với thời gian và ngày cụ thể.'), + +(2, 'q_and_a', + 'Could you help me move this furniture to the conference room?', + '["A. The furniture looks brand new.", "B. Sure, I can help you right after this call.", "C. The conference room is on the third floor.", "D. (No appropriate response)"]', + 'B', + 'Câu hỏi nhờ giúp đỡ — đáp án B đồng ý giúp sau khi kết thúc cuộc gọi, là phản hồi hợp lý nhất.'), + +(2, 'q_and_a', + 'Why hasn''t the client signed the contract yet?', + '["A. The contract was signed yesterday.", "B. Yes, the client called this morning.", "C. She is still reviewing the terms with her legal team.", "D. (No appropriate response)"]', + 'C', + 'Câu hỏi hỏi lý do — đáp án C giải thích khách hàng đang xem xét điều khoản với nhóm pháp lý.'), + +(2, 'q_and_a', + 'Where should I submit the expense report?', + '["A. Just send it to accounting@company.com.", "B. The report is due next Friday.", "C. You should include all receipts.", "D. (No appropriate response)"]', + 'A', + 'Câu hỏi hỏi nơi nộp — đáp án A cung cấp địa chỉ email cụ thể để gửi báo cáo.'), + +(2, 'q_and_a', + 'Have you received the package from the supplier yet?', + '["A. The package weighs about 5 kilograms.", "B. I sent it out last week.", "C. (No appropriate response)", "D. No, it hasn''t arrived as of this morning."]', + 'D', + 'Câu hỏi Yes/No về việc nhận hàng — đáp án D trả lời không và cung cấp thông tin thêm.'), + +(2, 'q_and_a', + 'Who is responsible for updating the company website?', + '["A. The website was updated last month.", "B. Our company website is very popular.", "C. (No appropriate response)", "D. Mark in the IT department handles that."]', + 'D', + 'Câu hỏi hỏi người chịu trách nhiệm — đáp án D nêu tên và bộ phận cụ thể.'), + +(2, 'q_and_a', + 'Would you like me to book a table for the client dinner?', + '["A. Yes, please make a reservation for six people.", "B. The dinner was excellent last time.", "C. The restaurant closes at 10 p.m.", "D. (No appropriate response)"]', + 'A', + 'Câu hỏi đề nghị giúp đỡ — đáp án A chấp nhận và cung cấp thêm yêu cầu về số người.'), + +(2, 'q_and_a', + 'How long will the software upgrade take?', + '["A. The software has many new features.", "B. We upgraded last year too.", "C. It should be finished in about two hours.", "D. (No appropriate response)"]', + 'C', + 'Câu hỏi hỏi thời gian — đáp án C trả lời thời lượng khoảng hai tiếng.'), + +(2, 'q_and_a', + 'Has the budget proposal been approved by the board?', + '["A. The proposal was well-written.", "B. Yes, the budget is always important.", "C. (No appropriate response)", "D. Not yet — they are voting on it this afternoon."]', + 'D', + 'Câu Yes/No hỏi về trạng thái phê duyệt — đáp án D cho biết chưa được duyệt và sẽ bỏ phiếu chiều nay.'), + +(2, 'q_and_a', + 'Which printer should I use for color documents?', + '["A. Use the one on the second floor near the copy room.", "B. You can print black and white on any printer.", "C. The printer ran out of ink yesterday.", "D. (No appropriate response)"]', + 'A', + 'Câu hỏi hỏi máy in nào nên dùng — đáp án A chỉ đến máy in cụ thể và vị trí của nó.'), + +(2, 'q_and_a', + 'Why was the training session rescheduled?', + '["A. The training will last for two days.", "B. I enjoyed the last training very much.", "C. The facilitator had a last-minute conflict.", "D. (No appropriate response)"]', + 'C', + 'Câu hỏi lý do đổi lịch — đáp án C giải thích người hướng dẫn có việc đột xuất.'), + +(2, 'q_and_a', + 'Can you send me the revised proposal before noon?', + '["A. The proposal needs more research.", "B. Noon is when the meeting starts.", "C. (No appropriate response)", "D. I''ll email it to you within the hour."]', + 'D', + 'Câu hỏi nhờ gửi tài liệu — đáp án D cam kết gửi qua email trong vòng một giờ.'), + +(2, 'q_and_a', + 'Isn''t the annual report supposed to be published today?', + '["A. Actually, the release was pushed back to Friday.", "B. The report covers last year''s results.", "C. Yes, reports are published regularly.", "D. (No appropriate response)"]', + 'A', + 'Câu hỏi phủ định để xác nhận — đáp án A chỉnh sửa thông tin và đưa ra ngày chính xác.'), + +(2, 'q_and_a', + 'Where is the nearest parking garage to the office?', + '["A. Parking is very expensive downtown.", "B. You should take the subway instead.", "C. There''s one on Pine Street, just two blocks away.", "D. (No appropriate response)"]', + 'C', + 'Câu hỏi về địa điểm — đáp án C chỉ ra garage cụ thể với khoảng cách rõ ràng.'), + +(2, 'q_and_a', + 'Would you mind covering the front desk while Sarah is at lunch?', + '["A. Sarah usually takes a short lunch break.", "B. The front desk was recently renovated.", "C. (No appropriate response)", "D. Not at all — I''ll head over now."]', + 'D', + 'Câu đề nghị — đáp án D đồng ý lịch sự và cho biết sẽ đến ngay.'), + +(2, 'q_and_a', + 'Who approved the travel expenses for the Tokyo trip?', + '["A. Tokyo is a great city to visit.", "B. Mr. Chen in finance signed off on them.", "C. The trip was very successful.", "D. (No appropriate response)"]', + 'B', + 'Câu hỏi về người phê duyệt — đáp án B nêu tên và bộ phận của người duyệt chi phí.'), + +(2, 'q_and_a', + 'How many copies of the handout should I prepare?', + '["A. The handout has five pages.", "B. Make about thirty — we expect a full room.", "C. I can prepare them for you.", "D. (No appropriate response)"]', + 'B', + 'Câu hỏi về số lượng — đáp án B đưa ra con số cụ thể kèm lý do.'), + +(2, 'q_and_a', + 'The new marketing director starts on Monday, doesn''t she?', + '["A. She was promoted last year.", "B. Actually, her start date was moved to Wednesday.", "C. Yes, marketing is a challenging role.", "D. (No appropriate response)"]', + 'B', + 'Câu xác nhận tag question — đáp án B cung cấp thông tin chỉnh sửa về ngày bắt đầu thực tế.'), + +(2, 'q_and_a', + 'Should I contact the vendor directly or go through procurement?', + '["A. The vendor offers competitive pricing.", "B. Always go through procurement for new orders.", "C. Vendors usually respond within a day.", "D. (No appropriate response)"]', + 'B', + 'Câu hỏi hai lựa chọn — đáp án B đưa ra hướng dẫn rõ ràng là qua phòng mua sắm.'), + +(2, 'q_and_a', + 'Do you know when the IT system will be back online?', + '["A. IT support works on the ground floor.", "B. They said it should be restored by 3 p.m.", "C. The system was updated last week.", "D. (No appropriate response)"]', + 'B', + 'Câu hỏi về thời gian khôi phục hệ thống — đáp án B cung cấp thời gian dự kiến cụ thể.'), + +-- ============================================================ +-- PART 3 — Conversation (20 questions — 7 conversations × ~3 Qs each, trimmed to 20) +-- ============================================================ +-- Conversation A: Office supply order +(3, 'conversation', + E'[Man]: Hi, Lisa. Do we have enough printer paper for this week?\n[Woman]: We''re running low. I was going to place an order today.\n[Man]: Great. Can you also order some toner cartridges? We''re almost out.\n[Woman]: Sure. Should I use the usual supplier, or try the new one that was recommended?\n\nQuestion: What are the speakers mainly discussing?', + '["A. Placing an order for office supplies", "B. A broken printer in the office", "C. Hiring a new office manager", "D. A budget meeting this week"]', + 'A', + 'Hai người đang thảo luận về việc đặt mua vật tư văn phòng (giấy và hộp mực).'), + +(3, 'conversation', + E'[Man]: Hi, Lisa. Do we have enough printer paper for this week?\n[Woman]: We''re running low. I was going to place an order today.\n[Man]: Great. Can you also order some toner cartridges? We''re almost out.\n[Woman]: Sure. Should I use the usual supplier, or try the new one that was recommended?\n\nQuestion: What does the man ask the woman to order in addition to paper?', + '["A. Staplers", "B. Envelopes", "C. Toner cartridges", "D. Printer cables"]', + 'C', + 'Người đàn ông yêu cầu đặt thêm hộp mực (toner cartridges) vì sắp hết.'), + +(3, 'conversation', + E'[Man]: Hi, Lisa. Do we have enough printer paper for this week?\n[Woman]: We''re running low. I was going to place an order today.\n[Man]: Great. Can you also order some toner cartridges? We''re almost out.\n[Woman]: Sure. Should I use the usual supplier, or try the new one that was recommended?\n\nQuestion: What is the woman considering regarding the order?', + '["A. Whether to request a discount", "B. How much paper to order", "C. When to schedule delivery", "D. Which supplier to use"]', + 'D', + 'Người phụ nữ đang cân nhắc dùng nhà cung cấp thường hay thử nhà cung cấp mới được giới thiệu.'), + +-- Conversation B: Job interview schedule +(3, 'conversation', + E'[Woman]: Tom, I''ve scheduled three candidates for interviews on Thursday.\n[Man]: That''s a busy day. What times are they coming in?\n[Woman]: The first one is at 10, then 1 o''clock, and the last one at 3:30.\n[Man]: I have a client call at 2. Will that be a problem?\n\nQuestion: What is the woman doing?', + '["A. Submitting job applications", "B. Reviewing employee performance", "C. Setting up a client meeting", "D. Scheduling job interviews"]', + 'D', + 'Người phụ nữ đang lên lịch phỏng vấn cho ba ứng viên vào thứ Năm.'), + +(3, 'conversation', + E'[Woman]: Tom, I''ve scheduled three candidates for interviews on Thursday.\n[Man]: That''s a busy day. What times are they coming in?\n[Woman]: The first one is at 10, then 1 o''clock, and the last one at 3:30.\n[Man]: I have a client call at 2. Will that be a problem?\n\nQuestion: What is the man concerned about?', + '["A. A client call conflicting with an interview", "B. Not enough candidates applying", "C. The interview room being unavailable", "D. Forgetting to prepare questions"]', + 'A', + 'Người đàn ông lo ngại cuộc gọi với khách hàng lúc 2 giờ có thể xung đột với lịch phỏng vấn.'), + +(3, 'conversation', + E'[Woman]: Tom, I''ve scheduled three candidates for interviews on Thursday.\n[Man]: That''s a busy day. What times are they coming in?\n[Woman]: The first one is at 10, then 1 o''clock, and the last one at 3:30.\n[Man]: I have a client call at 2. Will that be a problem?\n\nQuestion: How many candidates are scheduled for Thursday?', + '["A. Two", "B. Four", "C. Three", "D. Five"]', + 'C', + 'Người phụ nữ nói "I''ve scheduled three candidates for interviews on Thursday."'), + +-- Conversation C: Cafeteria renovation +(3, 'conversation', + E'[Man]: Did you hear the cafeteria will be closed for two weeks starting Monday?\n[Woman]: Really? Where are we supposed to eat lunch then?\n[Man]: Management is setting up a temporary food truck outside the main entrance.\n[Woman]: That''s not ideal, but I guess it''s better than nothing.\n\nQuestion: Why will the cafeteria be closed?', + '["A. It failed a health inspection", "B. It ran out of food supplies", "C. Management decided to outsource catering", "D. It is being renovated"]', + 'D', + 'Lý do đóng cửa căng tin không được nói trực tiếp nhưng ngữ cảnh cho thấy đang được cải tạo (renovation).'), + +(3, 'conversation', + E'[Man]: Did you hear the cafeteria will be closed for two weeks starting Monday?\n[Woman]: Really? Where are we supposed to eat lunch then?\n[Man]: Management is setting up a temporary food truck outside the main entrance.\n[Woman]: That''s not ideal, but I guess it''s better than nothing.\n\nQuestion: What temporary arrangement has management made?', + '["A. Setting up a food truck outside", "B. Providing lunch boxes to employees", "C. Extending lunch hour by 30 minutes", "D. Partnering with a nearby restaurant"]', + 'A', + 'Ban quản lý sẽ đặt xe bán đồ ăn tạm thời ở lối vào chính trong thời gian căng tin đóng cửa.'), + +-- Conversation D: Overseas client visit +(3, 'conversation', + E'[Woman]: Our client from Singapore is arriving on Wednesday afternoon.\n[Man]: Did you arrange a car to pick her up from the airport?\n[Woman]: Yes, and I''ve reserved a meeting room for Thursday morning as well.\n[Man]: Perfect. I''ll prepare the presentation slides tonight.\n\nQuestion: Who is arriving on Wednesday?', + '["A. A new employee from head office", "B. A consultant from overseas", "C. A vendor representative", "D. A client from Singapore"]', + 'D', + 'Người phụ nữ nói "Our client from Singapore is arriving on Wednesday afternoon."'), + +(3, 'conversation', + E'[Woman]: Our client from Singapore is arriving on Wednesday afternoon.\n[Man]: Did you arrange a car to pick her up from the airport?\n[Woman]: Yes, and I''ve reserved a meeting room for Thursday morning as well.\n[Man]: Perfect. I''ll prepare the presentation slides tonight.\n\nQuestion: What will the man do this evening?', + '["A. Pick up the client from the airport", "B. Book the meeting room", "C. Send a confirmation email to the client", "D. Prepare presentation slides"]', + 'D', + 'Người đàn ông nói "I''ll prepare the presentation slides tonight."'), + +-- Conversation E: Software issue +(3, 'conversation', + E'[Man]: I can''t log into the project management system. It keeps saying my password is wrong.\n[Woman]: Did you try resetting it through the IT portal?\n[Man]: Yes, but the reset email never came through.\n[Woman]: Let me contact the IT helpdesk for you. They usually fix these things within an hour.\n\nQuestion: What problem is the man experiencing?', + '["A. He forgot his employee ID number", "B. He received an error message about his account being locked", "C. The IT portal is completely down", "D. He cannot log into the project management system"]', + 'D', + 'Người đàn ông không thể đăng nhập vào hệ thống quản lý dự án vì mật khẩu bị báo sai.'), + +(3, 'conversation', + E'[Man]: I can''t log into the project management system. It keeps saying my password is wrong.\n[Woman]: Did you try resetting it through the IT portal?\n[Man]: Yes, but the reset email never came through.\n[Woman]: Let me contact the IT helpdesk for you. They usually fix these things within an hour.\n\nQuestion: What does the woman offer to do?', + '["A. Reset the man''s password herself", "B. Lend her account to the man temporarily", "C. Contact the IT helpdesk on his behalf", "D. Escalate the issue to the department head"]', + 'C', + 'Người phụ nữ đề nghị liên hệ bộ phận IT hộ người đàn ông.'), + +-- Conversation F: Flight delay +(3, 'conversation', + E'[Woman]: Our 7 a.m. flight to Chicago has been delayed by two hours due to bad weather.\n[Man]: Oh no. I have a 10 a.m. meeting at the client''s office.\n[Woman]: I''ll call ahead and let them know. They might be able to push the meeting back.\n[Man]: That would be great. Make sure they know we''ll still make it today.\n\nQuestion: Why is the flight delayed?', + '["A. Mechanical issues with the aircraft", "B. Air traffic control problems", "C. Bad weather conditions", "D. A security alert at the airport"]', + 'C', + 'Chuyến bay bị trễ vì thời tiết xấu — "due to bad weather."'), + +(3, 'conversation', + E'[Woman]: Our 7 a.m. flight to Chicago has been delayed by two hours due to bad weather.\n[Man]: Oh no. I have a 10 a.m. meeting at the client''s office.\n[Woman]: I''ll call ahead and let them know. They might be able to push the meeting back.\n[Man]: That would be great. Make sure they know we''ll still make it today.\n\nQuestion: What does the man want the woman to communicate to the client?', + '["A. That the meeting must be cancelled", "B. That they need to reschedule for tomorrow", "C. That they will still arrive today", "D. That the delay is out of their control"]', + 'C', + 'Người đàn ông muốn đảm bảo khách hàng biết họ vẫn sẽ đến trong ngày hôm nay.'), + +-- Conversation G: Training registration +(3, 'conversation', + E'[Man]: I''d like to sign up for the Excel training next week. Is there still space?\n[Woman]: Let me check the registration list... Yes, there are two spots left.\n[Man]: Great. Can I also register my colleague Amy?\n[Woman]: Of course. I''ll add both your names right now.\n\nQuestion: What does the man want to do?', + '["A. Teach an Excel class next week", "B. Find out who is teaching the training", "C. Access last week''s training materials", "D. Register for an Excel training session"]', + 'D', + 'Người đàn ông muốn đăng ký tham gia khóa đào tạo Excel tuần tới.'), + +(3, 'conversation', + E'[Man]: I''d like to sign up for the Excel training next week. Is there still space?\n[Woman]: Let me check the registration list... Yes, there are two spots left.\n[Man]: Great. Can I also register my colleague Amy?\n[Woman]: Of course. I''ll add both your names right now.\n\nQuestion: How many spots are available in the training?', + '["A. One", "B. Three", "C. Four", "D. Two"]', + 'D', + 'Người phụ nữ xác nhận "there are two spots left" — còn hai chỗ trống.'), + +(3, 'conversation', + E'[Man]: I''d like to sign up for the Excel training next week. Is there still space?\n[Woman]: Let me check the registration list... Yes, there are two spots left.\n[Man]: Great. Can I also register my colleague Amy?\n[Woman]: Of course. I''ll add both your names right now.\n\nQuestion: What will the woman do immediately after the conversation?', + '["A. Email Amy the training schedule", "B. Add both names to the registration list", "C. Confirm the training date with the instructor", "D. Send a calendar invite to the man"]', + 'B', + 'Người phụ nữ nói "I''ll add both your names right now" — sẽ thêm tên ngay lập tức.'), + +-- Conversation H: Relocation of department +(3, 'conversation', + E'[Woman]: Did you hear our department is moving to the fourth floor next month?\n[Man]: I did. I''m not looking forward to packing everything up.\n[Woman]: The new space is much bigger though. We''ll each get our own office.\n[Man]: That''s a silver lining. When exactly are we moving?\n\nQuestion: What is happening to the department next month?', + '["A. It is merging with another department", "B. It is moving to a different floor", "C. It is relocating to a new building", "D. It is downsizing its staff"]', + 'B', + 'Bộ phận sẽ chuyển lên tầng bốn vào tháng tới.'), + +(3, 'conversation', + E'[Woman]: Did you hear our department is moving to the fourth floor next month?\n[Man]: I did. I''m not looking forward to packing everything up.\n[Woman]: The new space is much bigger though. We''ll each get our own office.\n[Man]: That''s a silver lining. When exactly are we moving?\n\nQuestion: What is one advantage of the new space?', + '["A. It is closer to the parking garage", "B. It has a better cafeteria nearby", "C. Each person will have their own office", "D. It is more accessible by public transport"]', + 'C', + 'Điểm tích cực của không gian mới là mỗi người sẽ có văn phòng riêng.'), + +-- Conversation I: Customer complaint +(3, 'conversation', + E'[Customer]: I ordered a laptop two weeks ago and still haven''t received it.\n[Agent]: I''m very sorry about that. Can I have your order number, please?\n[Customer]: It''s GX-20458.\n[Agent]: Thank you. I can see it''s been held at our distribution center. I''ll arrange expedited shipping today at no extra charge.\n\nQuestion: What is the customer''s problem?', + '["A. The laptop she received was damaged", "B. She was charged the wrong amount", "C. Her laptop order has not arrived yet", "D. The wrong item was delivered to her"]', + 'C', + 'Khách hàng đặt mua laptop hai tuần trước nhưng vẫn chưa nhận được hàng.'), + +-- ============================================================ +-- PART 4 — Talk / Monologue (20 questions — ~4-5 talks × 4-5 Qs) +-- ============================================================ +-- Talk A: Voicemail about a delayed shipment +(4, 'talk', + E'Hello, this is Janet Morris from Westfield Supplies. I''m calling to inform you that your order number 7734 has been delayed due to a shortage of materials at our warehouse. The new estimated delivery date is October 18th, which is about five business days later than originally scheduled. We sincerely apologize for any inconvenience this may cause. If you would like to cancel or modify your order, please call us at 555-0192 before October 12th. Otherwise, your order will ship automatically on the revised date.\n\nQuestion: Who is leaving this message?', + '["A. A delivery driver", "B. A customer service representative", "C. A warehouse manager", "D. A logistics coordinator"]', + 'B', + 'Janet Morris từ Westfield Supplies đang gọi điện thông báo — cô ấy là đại diện dịch vụ khách hàng.'), + +(4, 'talk', + E'Hello, this is Janet Morris from Westfield Supplies. I''m calling to inform you that your order number 7734 has been delayed due to a shortage of materials at our warehouse. The new estimated delivery date is October 18th, which is about five business days later than originally scheduled. We sincerely apologize for any inconvenience this may cause. If you would like to cancel or modify your order, please call us at 555-0192 before October 12th. Otherwise, your order will ship automatically on the revised date.\n\nQuestion: Why has the delivery been delayed?', + '["A. A transportation strike", "B. A shortage of materials", "C. Incorrect shipping address", "D. High order volume"]', + 'B', + 'Lý do trễ hàng là thiếu hụt nguyên liệu tại kho — "shortage of materials at our warehouse."'), + +(4, 'talk', + E'Hello, this is Janet Morris from Westfield Supplies. I''m calling to inform you that your order number 7734 has been delayed due to a shortage of materials at our warehouse. The new estimated delivery date is October 18th, which is about five business days later than originally scheduled. We sincerely apologize for any inconvenience this may cause. If you would like to cancel or modify your order, please call us at 555-0192 before October 12th. Otherwise, your order will ship automatically on the revised date.\n\nQuestion: By when must the listener call if they want to cancel or modify the order?', + '["A. October 14th", "B. October 18th", "C. October 12th", "D. October 10th"]', + 'C', + 'Thông báo nói "please call us before October 12th" để hủy hoặc thay đổi đơn hàng.'), + +-- Talk B: Office announcement about new policy +(4, 'talk', + E'Good morning, everyone. I have a brief announcement regarding our new flexible work policy. Starting next Monday, all full-time staff will be allowed to work from home up to two days per week. However, you must be in the office on Tuesdays and Wednesdays for scheduled team meetings. To participate, please complete the remote work agreement form available on the HR portal and submit it by this Friday. If you have questions, contact HR directly. We believe this change will improve work-life balance and overall productivity.\n\nQuestion: What is the announcement about?', + '["A. A new remote work policy", "B. A change in office hours", "C. An upcoming team-building event", "D. New meeting room booking procedures"]', + 'A', + 'Thông báo giới thiệu chính sách làm việc linh hoạt mới cho phép làm việc từ xa.'), + +(4, 'talk', + E'Good morning, everyone. I have a brief announcement regarding our new flexible work policy. Starting next Monday, all full-time staff will be allowed to work from home up to two days per week. However, you must be in the office on Tuesdays and Wednesdays for scheduled team meetings. To participate, please complete the remote work agreement form available on the HR portal and submit it by this Friday. If you have questions, contact HR directly. We believe this change will improve work-life balance and overall productivity.\n\nQuestion: On which days must employees be in the office?', + '["A. Mondays and Fridays", "B. Tuesdays and Wednesdays", "C. Mondays and Thursdays", "D. Wednesdays and Fridays"]', + 'B', + 'Nhân viên bắt buộc phải có mặt ở văn phòng vào thứ Ba và thứ Tư để tham dự họp nhóm.'), + +(4, 'talk', + E'Good morning, everyone. I have a brief announcement regarding our new flexible work policy. Starting next Monday, all full-time staff will be allowed to work from home up to two days per week. However, you must be in the office on Tuesdays and Wednesdays for scheduled team meetings. To participate, please complete the remote work agreement form available on the HR portal and submit it by this Friday. If you have questions, contact HR directly. We believe this change will improve work-life balance and overall productivity.\n\nQuestion: What must employees do to participate in the new policy?', + '["A. Request approval from their manager", "B. Submit a remote work agreement form", "C. Attend an orientation session", "D. Sign a new employment contract"]', + 'B', + 'Nhân viên phải điền và nộp mẫu thỏa thuận làm việc từ xa qua cổng HR trước thứ Sáu.'), + +-- Talk C: Museum audio guide +(4, 'talk', + E'Welcome to the National Business History Museum. Today''s self-guided tour will take approximately 90 minutes. You are currently in the main lobby. To your left is the Industrial Revolution gallery, which traces the origins of modern commerce. Straight ahead is our temporary exhibit, "The Digital Age," which runs through December 31st. Restrooms are located on each floor near the stairwells. Photography is permitted throughout the museum, but please turn off your flash. Guided tours depart from this lobby every hour on the hour. The museum gift shop is open until 5 p.m.\n\nQuestion: What type of recording is this?', + '["A. A company training video", "B. A museum audio tour guide", "C. A tourist information hotline", "D. A welcome speech at a conference"]', + 'B', + 'Đây là bản hướng dẫn tham quan bảo tàng tự hành — "self-guided tour."'), + +(4, 'talk', + E'Welcome to the National Business History Museum. Today''s self-guided tour will take approximately 90 minutes. You are currently in the main lobby. To your left is the Industrial Revolution gallery, which traces the origins of modern commerce. Straight ahead is our temporary exhibit, "The Digital Age," which runs through December 31st. Restrooms are located on each floor near the stairwells. Photography is permitted throughout the museum, but please turn off your flash. Guided tours depart from this lobby every hour on the hour. The museum gift shop is open until 5 p.m.\n\nQuestion: What restriction is mentioned regarding photography?', + '["A. Photography is not allowed anywhere.", "B. Only professional cameras may be used.", "C. Flash must be turned off.", "D. Photos can only be taken in designated areas."]', + 'C', + 'Được phép chụp ảnh nhưng phải tắt đèn flash — "please turn off your flash."'), + +(4, 'talk', + E'Welcome to the National Business History Museum. Today''s self-guided tour will take approximately 90 minutes. You are currently in the main lobby. To your left is the Industrial Revolution gallery, which traces the origins of modern commerce. Straight ahead is our temporary exhibit, "The Digital Age," which runs through December 31st. Restrooms are located on each floor near the stairwells. Photography is permitted throughout the museum, but please turn off your flash. Guided tours depart from this lobby every hour on the hour. The museum gift shop is open until 5 p.m.\n\nQuestion: How often do guided tours depart?', + '["A. Every 30 minutes", "B. Every 90 minutes", "C. Twice a day", "D. Every hour"]', + 'D', + 'Guided tours khởi hành từ sảnh chính mỗi giờ một lần — "every hour on the hour."'), + +-- Talk D: Radio traffic report +(4, 'talk', + E'Good afternoon, drivers. This is your afternoon traffic update for the downtown area. There is currently a major slowdown on Highway 9 northbound due to a three-vehicle accident near Exit 14. Expect delays of up to 45 minutes. As an alternative, take Route 7 east to avoid the backup. Traffic on the Central Bridge is moving smoothly at this time. Road work on Fifth Avenue will begin tonight at 11 p.m. and is expected to be completed by 6 a.m. tomorrow. Stay tuned for updates every 15 minutes.\n\nQuestion: What is causing the slowdown on Highway 9?', + '["A. Construction work", "B. Heavy rain", "C. A three-vehicle accident", "D. A special event downtown"]', + 'C', + 'Tắc đường trên Highway 9 do tai nạn liên quan đến ba xe — "a three-vehicle accident."'), + +(4, 'talk', + E'Good afternoon, drivers. This is your afternoon traffic update for the downtown area. There is currently a major slowdown on Highway 9 northbound due to a three-vehicle accident near Exit 14. Expect delays of up to 45 minutes. As an alternative, take Route 7 east to avoid the backup. Traffic on the Central Bridge is moving smoothly at this time. Road work on Fifth Avenue will begin tonight at 11 p.m. and is expected to be completed by 6 a.m. tomorrow. Stay tuned for updates every 15 minutes.\n\nQuestion: What alternative route is suggested?', + '["A. Highway 9 southbound", "B. Central Bridge", "C. Fifth Avenue", "D. Route 7 east"]', + 'D', + 'Đường thay thế được gợi ý là Route 7 hướng đông để tránh tắc đường.'), + +(4, 'talk', + E'Good afternoon, drivers. This is your afternoon traffic update for the downtown area. There is currently a major slowdown on Highway 9 northbound due to a three-vehicle accident near Exit 14. Expect delays of up to 45 minutes. As an alternative, take Route 7 east to avoid the backup. Traffic on the Central Bridge is moving smoothly at this time. Road work on Fifth Avenue will begin tonight at 11 p.m. and is expected to be completed by 6 a.m. tomorrow. Stay tuned for updates every 15 minutes.\n\nQuestion: When will road work on Fifth Avenue begin?', + '["A. This afternoon at 3 p.m.", "B. Tonight at 11 p.m.", "C. Tomorrow morning at 6 a.m.", "D. This evening at 9 p.m."]', + 'B', + 'Công việc thi công trên Fifth Avenue bắt đầu tối nay lúc 11 giờ đêm.'), + +-- Talk E: Company earnings call summary +(4, 'talk', + E'Thank you all for joining our third-quarter earnings call. I''m pleased to report that total revenue for Q3 reached 48 million dollars, a 12 percent increase compared to the same period last year. Our strongest growth came from the Asia-Pacific region, which grew by 22 percent. Operating costs were kept at 15 percent below forecast, thanks to the efficiency measures introduced in Q2. Looking ahead, we expect continued growth in Q4, driven by new product launches and expanded partnerships. We will provide a more detailed breakdown in the investor report, available on our website by end of next week.\n\nQuestion: What was the total revenue for Q3?', + '["A. 38 million dollars", "B. 52 million dollars", "C. 48 million dollars", "D. 44 million dollars"]', + 'C', + 'Doanh thu Q3 đạt 48 triệu đô — "total revenue for Q3 reached 48 million dollars."'), + +(4, 'talk', + E'Thank you all for joining our third-quarter earnings call. I''m pleased to report that total revenue for Q3 reached 48 million dollars, a 12 percent increase compared to the same period last year. Our strongest growth came from the Asia-Pacific region, which grew by 22 percent. Operating costs were kept at 15 percent below forecast, thanks to the efficiency measures introduced in Q2. Looking ahead, we expect continued growth in Q4, driven by new product launches and expanded partnerships. We will provide a more detailed breakdown in the investor report, available on our website by end of next week.\n\nQuestion: Which region showed the strongest growth?', + '["A. Europe", "B. North America", "C. Asia-Pacific", "D. Latin America"]', + 'C', + 'Khu vực Asia-Pacific tăng trưởng 22%, mạnh nhất trong tất cả các khu vực.'), + +(4, 'talk', + E'Thank you all for joining our third-quarter earnings call. I''m pleased to report that total revenue for Q3 reached 48 million dollars, a 12 percent increase compared to the same period last year. Our strongest growth came from the Asia-Pacific region, which grew by 22 percent. Operating costs were kept at 15 percent below forecast, thanks to the efficiency measures introduced in Q2. Looking ahead, we expect continued growth in Q4, driven by new product launches and expanded partnerships. We will provide a more detailed breakdown in the investor report, available on our website by end of next week.\n\nQuestion: When will the detailed investor report be available?', + '["A. By the end of this week", "B. At the beginning of next month", "C. By end of next week", "D. After the Q4 results are released"]', + 'C', + 'Báo cáo chi tiết sẽ có trên website vào cuối tuần tới — "by end of next week."'), + +-- Talk F: Staff recognition announcement +(4, 'talk', + E'Good afternoon, team. I''d like to take a moment to recognize Sandra Choi from our customer service department. Sandra has handled over 300 customer inquiries this quarter with an average satisfaction rating of 4.9 out of 5. Her dedication and positive attitude have set an example for the entire team. In recognition of her outstanding performance, Sandra will receive a performance bonus and a certificate of excellence at this month''s company gathering. Please join me in congratulating Sandra on a truly exceptional quarter.\n\nQuestion: What is the main purpose of this announcement?', + '["A. To introduce a new team member", "B. To recognize an employee''s outstanding performance", "C. To announce a company-wide policy change", "D. To remind staff about the upcoming gathering"]', + 'B', + 'Thông báo nhằm ghi nhận thành tích xuất sắc của Sandra Choi trong quý vừa qua.'), + +(4, 'talk', + E'Good afternoon, team. I''d like to take a moment to recognize Sandra Choi from our customer service department. Sandra has handled over 300 customer inquiries this quarter with an average satisfaction rating of 4.9 out of 5. Her dedication and positive attitude have set an example for the entire team. In recognition of her outstanding performance, Sandra will receive a performance bonus and a certificate of excellence at this month''s company gathering. Please join me in congratulating Sandra on a truly exceptional quarter.\n\nQuestion: What will Sandra receive at the company gathering?', + '["A. A promotion to team leader", "B. A paid vacation package", "C. A performance bonus and a certificate of excellence", "D. An award voted on by her peers"]', + 'C', + 'Sandra sẽ nhận thưởng hiệu suất và chứng chỉ xuất sắc tại buổi họp mặt công ty.'), + +(4, 'talk', + E'Good afternoon, team. I''d like to take a moment to recognize Sandra Choi from our customer service department. Sandra has handled over 300 customer inquiries this quarter with an average satisfaction rating of 4.9 out of 5. Her dedication and positive attitude have set an example for the entire team. In recognition of her outstanding performance, Sandra will receive a performance bonus and a certificate of excellence at this month''s company gathering. Please join me in congratulating Sandra on a truly exceptional quarter.\n\nQuestion: What was Sandra''s average customer satisfaction rating?', + '["A. 4.5 out of 5", "B. 4.9 out of 5", "C. 4.7 out of 5", "D. 5.0 out of 5"]', + 'B', + 'Điểm hài lòng trung bình của Sandra là 4.9/5 — được nêu rõ trong bài phát biểu.'), + +-- Talk G: Store closing announcement +(4, 'talk', + E'Attention shoppers: Riverside Mall will be closing in 15 minutes. Please bring your final selections to the nearest checkout counter. All store entrances will be locked promptly at 9 p.m. We ask that all customers proceed toward the exits in an orderly fashion. Our parking garage on Level B will remain open until 9:30 p.m. Thank you for shopping with us today, and we look forward to welcoming you back tomorrow when we open at 10 a.m.\n\nQuestion: What is the purpose of this announcement?', + '["A. To inform shoppers of a special sale ending soon", "B. To notify customers that the mall is closing soon", "C. To remind customers of the parking garage hours", "D. To welcome shoppers to the mall"]', + 'B', + 'Thông báo cho biết trung tâm mua sắm sắp đóng cửa trong 15 phút.'), + +(4, 'talk', + E'Attention shoppers: Riverside Mall will be closing in 15 minutes. Please bring your final selections to the nearest checkout counter. All store entrances will be locked promptly at 9 p.m. We ask that all customers proceed toward the exits in an orderly fashion. Our parking garage on Level B will remain open until 9:30 p.m. Thank you for shopping with us today, and we look forward to welcoming you back tomorrow when we open at 10 a.m.\n\nQuestion: Until what time will the parking garage remain open?', + '["A. 9:00 p.m.", "B. 9:15 p.m.", "C. 9:30 p.m.", "D. 10:00 p.m."]', + 'C', + 'Nhà đỗ xe tầng B sẽ mở cửa đến 9:30 tối — "remain open until 9:30 p.m."'), + +-- ============================================================ +-- PART 5 — Incomplete Sentence / Grammar (20 questions) +-- ============================================================ +(5, 'incomplete_sentence', + 'The marketing team ______ a new advertising strategy for the upcoming product launch.', + '["A. is developing", "B. are develop", "C. have develop", "D. is develop"]', + 'A', + '"The marketing team" là chủ ngữ số ít, cần động từ số ít. "is developing" (present continuous) đúng ngữ pháp và ngữ nghĩa.'), + +(5, 'incomplete_sentence', + 'Please submit your expense reports ______ the end of the month.', + '["A. until", "B. during", "C. by", "D. since"]', + 'C', + '"by" = trước hoặc đúng vào thời điểm đó. "Submit by the end of the month" nghĩa là nộp trước cuối tháng.'), + +(5, 'incomplete_sentence', + 'The contract will be ______ by both parties before the deadline.', + '["A. sign", "B. signing", "C. signed", "D. to sign"]', + 'C', + 'Cấu trúc bị động "will be + past participle" — "signed" là dạng đúng.'), + +(5, 'incomplete_sentence', + 'Neither the manager nor the employees ______ informed about the policy change.', + '["A. was", "B. were", "C. has been", "D. is"]', + 'B', + 'Với "neither...nor", động từ chia theo danh từ gần nhất (employees — số nhiều), nên dùng "were."'), + +(5, 'incomplete_sentence', + 'The new software is ______ compatible with older operating systems.', + '["A. full", "B. fulling", "C. fullness", "D. fully"]', + 'D', + 'Trạng từ "fully" bổ nghĩa cho tính từ "compatible" — không dùng tính từ hay danh từ ở đây.'), + +(5, 'incomplete_sentence', + 'Due to the ______ demand, the company has decided to increase production.', + '["A. risen", "B. rising", "C. raise", "D. rises"]', + 'B', + '"rising demand" = nhu cầu đang tăng (tính từ dạng -ing bổ nghĩa cho danh từ). "risen" không đứng trước danh từ trực tiếp.'), + +(5, 'incomplete_sentence', + 'The board of directors will meet ______ Tuesday to discuss the merger.', + '["A. in", "B. at", "C. on", "D. by"]', + 'C', + 'Giới từ chỉ ngày trong tuần dùng "on" — "on Tuesday."'), + +(5, 'incomplete_sentence', + 'Employees are encouraged to ______ any concerns directly to their supervisors.', + '["A. raise", "B. rise", "C. raised", "D. arisen"]', + 'A', + '"raise concerns" = nêu lên mối lo ngại. "Rise" là nội động từ, không dùng với tân ngữ trực tiếp.'), + +(5, 'incomplete_sentence', + 'The presentation was so ______ that the entire audience stayed for the Q&A session.', + '["A. engage", "B. engaged", "C. engaging", "D. engagement"]', + 'C', + '"engaging" (tính từ chủ động) mô tả bài thuyết trình khiến người khác cảm thấy thú vị.'), + +(5, 'incomplete_sentence', + 'Ms. Kim has been working for this company ______ she graduated from university.', + '["A. when", "B. since", "C. for", "D. while"]', + 'B', + '"since" dùng với thời điểm cụ thể trong quá khứ để diễn đạt khoảng thời gian kéo dài đến hiện tại.'), + +(5, 'incomplete_sentence', + 'The ______ of the new office building is scheduled for completion by March.', + '["A. construct", "B. constructing", "C. constructed", "D. construction"]', + 'D', + 'Danh từ "construction" (sự xây dựng) đúng vai trò chủ ngữ trong câu.'), + +(5, 'incomplete_sentence', + 'Customers who purchased products before January 1st are ______ for a full refund.', + '["A. eligible", "B. eligibility", "C. eligibly", "D. elect"]', + 'A', + '"eligible for" = đủ điều kiện nhận — tính từ "eligible" dùng đúng sau động từ "to be."'), + +(5, 'incomplete_sentence', + 'The sales figures for Q2 were ______ than those recorded in Q1.', + '["A. more high", "B. higher", "C. highest", "D. highly"]', + 'B', + 'So sánh hơn của "high" là "higher" — dùng khi so sánh hai đối tượng.'), + +(5, 'incomplete_sentence', + 'All meeting ______ must be booked through the online reservation system.', + '["A. rooms", "B. room", "C. room''s", "D. roomful"]', + 'A', + '"meeting rooms" — danh từ ghép số nhiều dùng khi nói chung về các phòng họp.'), + +(5, 'incomplete_sentence', + 'Please ______ that all forms are completed accurately before submission.', + '["A. ensure", "B. assure", "C. insure", "D. reassure"]', + 'A', + '"ensure that" = đảm bảo rằng điều gì đó xảy ra — đây là cách dùng chuẩn trong văn viết chính thức.'), + +(5, 'incomplete_sentence', + 'The company''s annual ______ will be held at the Grand Hyatt Hotel this year.', + '["A. celebrate", "B. celebrating", "C. celebration", "D. celebrated"]', + 'C', + '"annual celebration" = lễ kỷ niệm thường niên — danh từ "celebration" đúng chức năng chủ ngữ/tân ngữ.'), + +(5, 'incomplete_sentence', + 'The new policy applies to ______ employees, regardless of their department.', + '["A. all", "B. every", "C. each of", "D. whole"]', + 'A', + '"all employees" = tất cả nhân viên. "Every" và "each" dùng với danh từ số ít. "Whole" không dùng trước danh từ đếm được số nhiều trực tiếp.'), + +(5, 'incomplete_sentence', + 'The IT department is ______ for maintaining all computer systems in the building.', + '["A. responsible", "B. responsibility", "C. respond", "D. responsibly"]', + 'A', + '"responsible for" = chịu trách nhiệm về — tính từ "responsible" kết hợp với giới từ "for."'), + +(5, 'incomplete_sentence', + 'The project ______ on time despite the unexpected delays during the final stage.', + '["A. delivered", "B. was delivered", "C. had deliver", "D. is deliver"]', + 'B', + 'Câu bị động — dự án được bàn giao đúng hạn. "was delivered" = thì quá khứ đơn bị động, đúng ở đây.'), + +(5, 'incomplete_sentence', + 'We need to ______ the budget proposal before the board meeting on Friday.', + '["A. finalize", "B. final", "C. finally", "D. finalization"]', + 'A', + '"finalize" = hoàn thiện (động từ) — đứng sau "to" trong cấu trúc "need to + V."'), + +-- ============================================================ +-- PART 6 — Text Completion (20 questions — 5 passages × 4 Qs each) +-- ============================================================ +-- Passage A: Internal memo about a new expense policy +(6, 'text_completion', + E'MEMORANDUM\nTo: All Staff\nFrom: Finance Department\nRe: Updated Expense Reimbursement Policy\n\nEffective November 1st, the company will implement a revised expense reimbursement policy. All employees must submit expense claims ______ [Q1] 30 days of incurring the expense. Claims submitted after this period will not be processed.\n\nQ1: What word fits in the blank?', + '["A. within", "B. along", "C. beyond", "D. despite"]', + 'A', + '"within 30 days" = trong vòng 30 ngày — giới từ "within" chỉ khoảng thời gian giới hạn.'), + +(6, 'text_completion', + E'MEMORANDUM\nTo: All Staff\nFrom: Finance Department\nRe: Updated Expense Reimbursement Policy\n\nAll receipts must be attached to the claim form. Expenses ______ [Q2] a receipt will be automatically rejected by the system.\n\nQ2: What word fits in the blank?', + '["A. including", "B. lacking", "C. requiring", "D. carrying"]', + 'B', + '"Expenses lacking a receipt" = chi phí không có hóa đơn kèm theo sẽ bị hệ thống từ chối tự động.'), + +(6, 'text_completion', + E'MEMORANDUM\nTo: All Staff\nFrom: Finance Department\nRe: Updated Expense Reimbursement Policy\n\nThe daily meal allowance has been ______ [Q3] from $40 to $50 for domestic travel and from $60 to $75 for international travel.\n\nQ3: What word fits in the blank?', + '["A. reduced", "B. maintained", "C. increased", "D. calculated"]', + 'C', + 'Mức trợ cấp tăng từ $40 lên $50 và từ $60 lên $75 — "increased" là từ phù hợp.'), + +(6, 'text_completion', + E'MEMORANDUM\nTo: All Staff\nFrom: Finance Department\nRe: Updated Expense Reimbursement Policy\n\nFor further ______ [Q4] on the new policy, please refer to the HR handbook or contact your line manager.\n\nQ4: What word fits in the blank?', + '["A. information", "B. inform", "C. informing", "D. informative"]', + 'A', + '"further information" = thông tin thêm — danh từ "information" đúng chức năng tân ngữ của giới từ "for."'), + +-- Passage B: Letter to a job applicant +(6, 'text_completion', + E'Dear Ms. Nguyen,\n\nThank you for ______ [Q1] your application for the position of Marketing Coordinator at Global Tech Solutions.\n\nQ1: What word fits in the blank?', + '["A. submit", "B. submitted", "C. submitting", "D. to submit"]', + 'C', + '"Thank you for + V-ing" là cấu trúc chuẩn — "submitting" sau giới từ "for."'), + +(6, 'text_completion', + E'Dear Ms. Nguyen,\n\nWe have carefully reviewed your qualifications and are pleased to invite you for an interview. The interview will be ______ [Q2] at our headquarters on Thursday, October 20th, at 2:00 p.m.\n\nQ2: What word fits in the blank?', + '["A. held", "B. hold", "C. holding", "D. holds"]', + 'A', + 'Cấu trúc bị động "will be held" = sẽ được tổ chức — dạng past participle "held" đúng.'), + +(6, 'text_completion', + E'Dear Ms. Nguyen,\n\nPlease confirm your ______ [Q3] by replying to this email no later than October 17th. If you have any questions, feel free to contact our HR team.\n\nQ3: What word fits in the blank?', + '["A. attend", "B. attending", "C. attendance", "D. attended"]', + 'C', + '"Confirm your attendance" = xác nhận sự tham dự — danh từ "attendance" đúng sau tính từ sở hữu "your."'), + +(6, 'text_completion', + E'Dear Ms. Nguyen,\n\nWe look forward to meeting you and learning more about your ______ [Q4] and experience.\n\nQ4: What word fits in the blank?', + '["A. qualify", "B. qualifies", "C. qualified", "D. qualifications"]', + 'D', + '"qualifications and experience" = bằng cấp và kinh nghiệm — danh từ số nhiều "qualifications" đúng ngữ pháp và ngữ nghĩa.'), + +-- Passage C: Newsletter announcement +(6, 'text_completion', + E'COMPANY NEWSLETTER — OCTOBER EDITION\n\nWe are excited to announce that our company has recently ______ [Q1] a new office in Ho Chi Minh City.\n\nQ1: What word fits in the blank?', + '["A. open", "B. opened", "C. opening", "D. opens"]', + 'B', + '"has recently opened" = hiện tại hoàn thành — đã khai trương gần đây.'), + +(6, 'text_completion', + E'COMPANY NEWSLETTER — OCTOBER EDITION\n\nThe new office will serve as our regional hub for Southeast Asia, ______ [Q2] our growing presence in the market.\n\nQ2: What word fits in the blank?', + '["A. strengthen", "B. strengthening", "C. to strengthen", "D. strengthened"]', + 'B', + '"strengthening our presence" = phân từ hiện tại dùng bổ nghĩa cho mệnh đề trước, diễn đạt mục đích/kết quả.'), + +(6, 'text_completion', + E'COMPANY NEWSLETTER — OCTOBER EDITION\n\nThe office is conveniently ______ [Q3] in the central business district, with easy access to public transportation.\n\nQ3: What word fits in the blank?', + '["A. locating", "B. location", "C. located", "D. locate"]', + 'C', + '"is conveniently located" = được đặt ở vị trí thuận tiện — bị động "located" sau "is."'), + +(6, 'text_completion', + E'COMPANY NEWSLETTER — OCTOBER EDITION\n\nWe invite all interested employees to apply for ______ [Q4] positions at the new location through the internal job portal.\n\nQ4: What word fits in the blank?', + '["A. available", "B. availability", "C. avail", "D. availing"]', + 'A', + '"available positions" = các vị trí còn tuyển — tính từ "available" bổ nghĩa trực tiếp cho danh từ "positions."'), + +-- Passage D: Customer service notice +(6, 'text_completion', + E'IMPORTANT NOTICE TO OUR CUSTOMERS\n\nDue to scheduled system maintenance, our online services will be ______ [Q1] unavailable from 2:00 a.m. to 6:00 a.m. on Saturday, November 4th.\n\nQ1: What word fits in the blank?', + '["A. temporal", "B. temporarily", "C. temporary", "D. temporize"]', + 'B', + 'Trạng từ "temporarily" bổ nghĩa cho tính từ "unavailable" — tạm thời không có sẵn.'), + +(6, 'text_completion', + E'IMPORTANT NOTICE TO OUR CUSTOMERS\n\nWe apologize for any ______ [Q2] this may cause and appreciate your patience.\n\nQ2: What word fits in the blank?', + '["A. inconvenience", "B. inconvenient", "C. inconveniently", "D. inconvenienced"]', + 'A', + '"any inconvenience" = bất kỳ sự bất tiện nào — danh từ "inconvenience" đúng sau tính từ "any."'), + +(6, 'text_completion', + E'IMPORTANT NOTICE TO OUR CUSTOMERS\n\nIf you require ______ [Q3] assistance before the maintenance window, please contact our support line at 1-800-555-0100.\n\nQ3: What word fits in the blank?', + '["A. urgent", "B. urgency", "C. urgently", "D. urge"]', + 'A', + '"urgent assistance" = hỗ trợ khẩn cấp — tính từ "urgent" bổ nghĩa trực tiếp cho danh từ "assistance."'), + +(6, 'text_completion', + E'IMPORTANT NOTICE TO OUR CUSTOMERS\n\nOnce maintenance is complete, all services will be ______ [Q4] as normal. Thank you for your continued support.\n\nQ4: What word fits in the blank?', + '["A. resume", "B. resumed", "C. resuming", "D. resumption"]', + 'B', + '"will be resumed" = sẽ được khôi phục — bị động tương lai với past participle "resumed."'), + +-- Passage E: Training program invitation +(6, 'text_completion', + E'INVITATION: LEADERSHIP DEVELOPMENT PROGRAM\n\nWe are pleased to invite eligible staff to ______ [Q1] in our annual Leadership Development Program.\n\nQ1: What word fits in the blank?', + '["A. participate", "B. participation", "C. participant", "D. participating"]', + 'A', + '"invited to participate" = mời tham gia — động từ nguyên mẫu "participate" sau "to."'), + +(6, 'text_completion', + E'INVITATION: LEADERSHIP DEVELOPMENT PROGRAM\n\nThis program is designed for employees who have demonstrated ______ [Q2] leadership potential and are ready to take on greater responsibilities.\n\nQ2: What word fits in the blank?', + '["A. exception", "B. exceptional", "C. exceptionally", "D. except"]', + 'B', + '"exceptional leadership potential" = tiềm năng lãnh đạo xuất sắc — tính từ "exceptional" bổ nghĩa cho "potential."'), + +(6, 'text_completion', + E'INVITATION: LEADERSHIP DEVELOPMENT PROGRAM\n\nThe program ______ [Q3] weekly workshops, one-on-one coaching sessions, and a final capstone project.\n\nQ3: What word fits in the blank?', + '["A. consists", "B. includes", "C. contains with", "D. composes"]', + 'B', + '"includes workshops, coaching, and a project" — "include" + tân ngữ trực tiếp là cách dùng chuẩn trong tiếng Anh.'), + +(6, 'text_completion', + E'INVITATION: LEADERSHIP DEVELOPMENT PROGRAM\n\nTo apply, please submit a completed application form along with a ______ [Q4] from your direct supervisor by October 25th.\n\nQ4: What word fits in the blank?', + '["A. recommend", "B. recommending", "C. recommendation", "D. recommended"]', + 'C', + '"a recommendation from your supervisor" = thư giới thiệu từ quản lý trực tiếp — danh từ "recommendation" đúng.'), + +-- ============================================================ +-- PART 7 — Reading Comprehension (20 questions — 5 passages × 4 Qs each) +-- ============================================================ +-- Passage A: Email about a conference +(7, 'reading', + E'From: events@globaltech.com\nTo: r.santos@globaltech.com\nSubject: Annual Technology Conference — Registration Reminder\n\nDear Mr. Santos,\n\nThis is a reminder that the registration deadline for the 12th Annual Global Technology Conference is this Friday, October 15th. The conference will be held on November 8–10 at the Riverside Convention Center in Seattle.\n\nEarly registrants (before October 10th) received a 20% discount on the registration fee. Standard registration is $350 per person. A limited number of group discounts (10 or more participants) are still available at $280 per person.\n\nThe keynote speakers include Dr. Angela Marsh, a leading AI researcher, and Mr. Keith Hayashi, CEO of FutureTech Inc. Workshop sessions on cybersecurity, cloud computing, and data analytics will run throughout the three days.\n\nPlease visit our website at www.globaltech.com/conference to complete your registration.\n\nQuestion: What is the main purpose of this email?', + '["A. To remind the recipient about a registration deadline", "B. To announce a new technology product", "C. To confirm a hotel reservation for the conference", "D. To introduce the keynote speakers of the conference"]', + 'A', + 'Email nhắc nhở về hạn đăng ký tham dự hội nghị — "reminder that the registration deadline is this Friday."'), + +(7, 'reading', + E'From: events@globaltech.com\nTo: r.santos@globaltech.com\nSubject: Annual Technology Conference — Registration Reminder\n\nDear Mr. Santos,\n\nThis is a reminder that the registration deadline for the 12th Annual Global Technology Conference is this Friday, October 15th. The conference will be held on November 8–10 at the Riverside Convention Center in Seattle.\n\nEarly registrants (before October 10th) received a 20% discount on the registration fee. Standard registration is $350 per person. A limited number of group discounts (10 or more participants) are still available at $280 per person.\n\nThe keynote speakers include Dr. Angela Marsh, a leading AI researcher, and Mr. Keith Hayashi, CEO of FutureTech Inc. Workshop sessions on cybersecurity, cloud computing, and data analytics will run throughout the three days.\n\nPlease visit our website at www.globaltech.com/conference to complete your registration.\n\nQuestion: What is the standard registration fee per person?', + '["A. $280", "B. $315", "C. $350", "D. $420"]', + 'C', + 'Phí đăng ký tiêu chuẩn là $350 mỗi người — "Standard registration is $350 per person."'), + +(7, 'reading', + E'From: events@globaltech.com\nTo: r.santos@globaltech.com\nSubject: Annual Technology Conference — Registration Reminder\n\nDear Mr. Santos,\n\nThis is a reminder that the registration deadline for the 12th Annual Global Technology Conference is this Friday, October 15th. The conference will be held on November 8–10 at the Riverside Convention Center in Seattle.\n\nEarly registrants (before October 10th) received a 20% discount on the registration fee. Standard registration is $350 per person. A limited number of group discounts (10 or more participants) are still available at $280 per person.\n\nThe keynote speakers include Dr. Angela Marsh, a leading AI researcher, and Mr. Keith Hayashi, CEO of FutureTech Inc. Workshop sessions on cybersecurity, cloud computing, and data analytics will run throughout the three days.\n\nPlease visit our website at www.globaltech.com/conference to complete your registration.\n\nQuestion: What field is Dr. Angela Marsh associated with?', + '["A. Cloud computing", "B. Data analytics", "C. Cybersecurity", "D. Artificial intelligence"]', + 'D', + 'Dr. Angela Marsh được giới thiệu là "a leading AI researcher" — nhà nghiên cứu trí tuệ nhân tạo hàng đầu.'), + +(7, 'reading', + E'From: events@globaltech.com\nTo: r.santos@globaltech.com\nSubject: Annual Technology Conference — Registration Reminder\n\nDear Mr. Santos,\n\nThis is a reminder that the registration deadline for the 12th Annual Global Technology Conference is this Friday, October 15th. The conference will be held on November 8–10 at the Riverside Convention Center in Seattle.\n\nEarly registrants (before October 10th) received a 20% discount on the registration fee. Standard registration is $350 per person. A limited number of group discounts (10 or more participants) are still available at $280 per person.\n\nThe keynote speakers include Dr. Angela Marsh, a leading AI researcher, and Mr. Keith Hayashi, CEO of FutureTech Inc. Workshop sessions on cybersecurity, cloud computing, and data analytics will run throughout the three days.\n\nPlease visit our website at www.globaltech.com/conference to complete your registration.\n\nQuestion: What discount is available for groups of 10 or more?', + '["A. 10% off the standard price", "B. $280 per person", "C. Free registration for the group leader", "D. 30% off the early bird price"]', + 'B', + 'Đăng ký theo nhóm từ 10 người trở lên có giá $280 mỗi người — được nêu rõ trong email.'), + +-- Passage B: Job advertisement +(7, 'reading', + E'JOB ADVERTISEMENT\n\nPosition: Senior Accountant\nCompany: Meridian Financial Group\nLocation: New York, NY\nType: Full-time\n\nMeridian Financial Group is seeking a qualified Senior Accountant to join our growing finance team. The successful candidate will be responsible for managing financial reports, overseeing accounts payable and receivable, and ensuring compliance with regulatory requirements.\n\nRequirements:\n— Bachelor''s degree in Accounting, Finance, or a related field\n— Minimum 5 years of accounting experience\n— CPA certification preferred\n— Proficiency in QuickBooks and Microsoft Excel\n— Strong analytical and communication skills\n\nWe offer a competitive salary, comprehensive health benefits, and opportunities for professional development. Interested candidates should send their CV and cover letter to hr@meridianfinancial.com by November 30th.\n\nQuestion: What position is Meridian Financial Group advertising?', + '["A. Senior Accountant", "B. Financial Analyst", "C. Accounts Manager", "D. Chief Financial Officer"]', + 'A', + 'Công ty đang tuyển dụng "Senior Accountant" — được nêu rõ ở đầu quảng cáo.'), + +(7, 'reading', + E'JOB ADVERTISEMENT\n\nPosition: Senior Accountant\nCompany: Meridian Financial Group\nLocation: New York, NY\nType: Full-time\n\nMeridian Financial Group is seeking a qualified Senior Accountant to join our growing finance team. The successful candidate will be responsible for managing financial reports, overseeing accounts payable and receivable, and ensuring compliance with regulatory requirements.\n\nRequirements:\n— Bachelor''s degree in Accounting, Finance, or a related field\n— Minimum 5 years of accounting experience\n— CPA certification preferred\n— Proficiency in QuickBooks and Microsoft Excel\n— Strong analytical and communication skills\n\nWe offer a competitive salary, comprehensive health benefits, and opportunities for professional development. Interested candidates should send their CV and cover letter to hr@meridianfinancial.com by November 30th.\n\nQuestion: What is the minimum experience required for this position?', + '["A. 3 years", "B. 7 years", "C. 10 years", "D. 5 years"]', + 'D', + 'Yêu cầu tối thiểu là 5 năm kinh nghiệm kế toán — "Minimum 5 years of accounting experience."'), + +(7, 'reading', + E'JOB ADVERTISEMENT\n\nPosition: Senior Accountant\nCompany: Meridian Financial Group\nLocation: New York, NY\nType: Full-time\n\nMeridian Financial Group is seeking a qualified Senior Accountant to join our growing finance team. The successful candidate will be responsible for managing financial reports, overseeing accounts payable and receivable, and ensuring compliance with regulatory requirements.\n\nRequirements:\n— Bachelor''s degree in Accounting, Finance, or a related field\n— Minimum 5 years of accounting experience\n— CPA certification preferred\n— Proficiency in QuickBooks and Microsoft Excel\n— Strong analytical and communication skills\n\nWe offer a competitive salary, comprehensive health benefits, and opportunities for professional development. Interested candidates should send their CV and cover letter to hr@meridianfinancial.com by November 30th.\n\nQuestion: What software proficiency is required?', + '["A. SAP and PowerPoint", "B. Salesforce and Tableau", "C. QuickBooks and Microsoft Excel", "D. Xero and Google Sheets"]', + 'C', + 'Ứng viên cần thành thạo "QuickBooks and Microsoft Excel" theo yêu cầu được liệt kê.'), + +(7, 'reading', + E'JOB ADVERTISEMENT\n\nPosition: Senior Accountant\nCompany: Meridian Financial Group\nLocation: New York, NY\nType: Full-time\n\nMeridian Financial Group is seeking a qualified Senior Accountant to join our growing finance team. The successful candidate will be responsible for managing financial reports, overseeing accounts payable and receivable, and ensuring compliance with regulatory requirements.\n\nRequirements:\n— Bachelor''s degree in Accounting, Finance, or a related field\n— Minimum 5 years of accounting experience\n— CPA certification preferred\n— Proficiency in QuickBooks and Microsoft Excel\n— Strong analytical and communication skills\n\nWe offer a competitive salary, comprehensive health benefits, and opportunities for professional development. Interested candidates should send their CV and cover letter to hr@meridianfinancial.com by November 30th.\n\nQuestion: How should interested candidates apply?', + '["A. Visit the company website and fill out a form", "B. Call the HR department directly", "C. Send a CV and cover letter to the provided email", "D. Apply through a recruitment agency"]', + 'C', + 'Ứng viên gửi CV và thư xin việc đến hr@meridianfinancial.com trước ngày 30 tháng 11.'), + +-- Passage C: Company policy memo +(7, 'reading', + E'INTERNAL MEMO\nTo: All Department Heads\nFrom: CEO Office\nSubject: Updated Mobile Device Policy\n\nEffective January 1st, the company will introduce a Bring Your Own Device (BYOD) policy for all employees. Staff may use personal smartphones and tablets for work purposes, provided that they install the company-approved security application on their devices.\n\nAll work-related data stored on personal devices must be encrypted. The IT department will conduct an initial setup session for all staff during the week of December 16–20. Attendance at this session is mandatory for anyone wishing to use personal devices for work.\n\nPersonal devices used for work must not be shared with unauthorized individuals, and employees remain personally responsible for any data breach resulting from loss or theft of their device.\n\nFor questions, contact IT Security at itsecurity@company.com.\n\nQuestion: What is the main subject of this memo?', + '["A. A new data encryption technology", "B. Mandatory IT training for all employees", "C. A BYOD policy for using personal devices at work", "D. Updated data privacy regulations"]', + 'C', + 'Memo giới thiệu chính sách BYOD cho phép nhân viên dùng thiết bị cá nhân cho công việc.'), + +(7, 'reading', + E'INTERNAL MEMO\nTo: All Department Heads\nFrom: CEO Office\nSubject: Updated Mobile Device Policy\n\nEffective January 1st, the company will introduce a Bring Your Own Device (BYOD) policy for all employees. Staff may use personal smartphones and tablets for work purposes, provided that they install the company-approved security application on their devices.\n\nAll work-related data stored on personal devices must be encrypted. The IT department will conduct an initial setup session for all staff during the week of December 16–20. Attendance at this session is mandatory for anyone wishing to use personal devices for work.\n\nPersonal devices used for work must not be shared with unauthorized individuals, and employees remain personally responsible for any data breach resulting from loss or theft of their device.\n\nFor questions, contact IT Security at itsecurity@company.com.\n\nQuestion: What must employees do before they can use personal devices for work?', + '["A. Install a company-approved security application", "B. Sign a waiver form", "C. Submit a request to their department head", "D. Purchase a company-approved device"]', + 'A', + 'Điều kiện để dùng thiết bị cá nhân là phải cài ứng dụng bảo mật được công ty phê duyệt.'), + +(7, 'reading', + E'INTERNAL MEMO\nTo: All Department Heads\nFrom: CEO Office\nSubject: Updated Mobile Device Policy\n\nEffective January 1st, the company will introduce a Bring Your Own Device (BYOD) policy for all employees. Staff may use personal smartphones and tablets for work purposes, provided that they install the company-approved security application on their devices.\n\nAll work-related data stored on personal devices must be encrypted. The IT department will conduct an initial setup session for all staff during the week of December 16–20. Attendance at this session is mandatory for anyone wishing to use personal devices for work.\n\nPersonal devices used for work must not be shared with unauthorized individuals, and employees remain personally responsible for any data breach resulting from loss or theft of their device.\n\nFor questions, contact IT Security at itsecurity@company.com.\n\nQuestion: When is the IT setup session scheduled?', + '["A. The week of January 1–5", "B. The week of November 11–15", "C. The week of December 16–20", "D. The week of December 23–27"]', + 'C', + 'Phiên thiết lập IT được tổ chức trong tuần từ ngày 16–20 tháng 12.'), + +(7, 'reading', + E'INTERNAL MEMO\nTo: All Department Heads\nFrom: CEO Office\nSubject: Updated Mobile Device Policy\n\nEffective January 1st, the company will introduce a Bring Your Own Device (BYOD) policy for all employees. Staff may use personal smartphones and tablets for work purposes, provided that they install the company-approved security application on their devices.\n\nAll work-related data stored on personal devices must be encrypted. The IT department will conduct an initial setup session for all staff during the week of December 16–20. Attendance at this session is mandatory for anyone wishing to use personal devices for work.\n\nPersonal devices used for work must not be shared with unauthorized individuals, and employees remain personally responsible for any data breach resulting from loss or theft of their device.\n\nFor questions, contact IT Security at itsecurity@company.com.\n\nQuestion: Who is responsible if a personal device used for work is lost or stolen and data is breached?', + '["A. The IT Security department", "B. The department head", "C. The company''s insurance provider", "D. The employee personally"]', + 'D', + 'Nhân viên tự chịu trách nhiệm cá nhân về mọi rò rỉ dữ liệu do mất hoặc trộm thiết bị.'), + +-- Passage D: Product review article +(7, 'reading', + E'PRODUCT SPOTLIGHT: ProDesk X500 Standing Desk\n\nThe ProDesk X500 is a motorized height-adjustable desk designed for modern office environments. With a dual-motor lifting system, the desk can transition from sitting to standing height in under 10 seconds. It supports a maximum load of 150 kilograms and features a memory function that saves up to four custom height presets.\n\nThe desktop surface is available in three finishes: natural oak, white, and charcoal grey. Assembly is straightforward and takes approximately 45 minutes with two people. The desk comes with a 5-year warranty covering all mechanical components.\n\nPricing starts at $499 for the basic model. The premium model, which includes a built-in cable management system and USB charging ports, is priced at $649. Both models are available through the ProDesk website and authorized retailers.\n\nQuestion: How long does it take for the desk to adjust its height?', + '["A. Less than 5 seconds", "B. About 30 seconds", "C. Under 10 seconds", "D. Approximately 1 minute"]', + 'C', + 'Bàn có thể chuyển từ tư thế ngồi sang đứng trong vòng dưới 10 giây.'), + +(7, 'reading', + E'PRODUCT SPOTLIGHT: ProDesk X500 Standing Desk\n\nThe ProDesk X500 is a motorized height-adjustable desk designed for modern office environments. With a dual-motor lifting system, the desk can transition from sitting to standing height in under 10 seconds. It supports a maximum load of 150 kilograms and features a memory function that saves up to four custom height presets.\n\nThe desktop surface is available in three finishes: natural oak, white, and charcoal grey. Assembly is straightforward and takes approximately 45 minutes with two people. The desk comes with a 5-year warranty covering all mechanical components.\n\nPricing starts at $499 for the basic model. The premium model, which includes a built-in cable management system and USB charging ports, is priced at $649. Both models are available through the ProDesk website and authorized retailers.\n\nQuestion: How many color finishes are available for the desktop surface?', + '["A. Two", "B. Four", "C. Three", "D. Five"]', + 'C', + 'Bề mặt bàn có ba màu: natural oak, white, và charcoal grey.'), + +(7, 'reading', + E'PRODUCT SPOTLIGHT: ProDesk X500 Standing Desk\n\nThe ProDesk X500 is a motorized height-adjustable desk designed for modern office environments. With a dual-motor lifting system, the desk can transition from sitting to standing height in under 10 seconds. It supports a maximum load of 150 kilograms and features a memory function that saves up to four custom height presets.\n\nThe desktop surface is available in three finishes: natural oak, white, and charcoal grey. Assembly is straightforward and takes approximately 45 minutes with two people. The desk comes with a 5-year warranty covering all mechanical components.\n\nPricing starts at $499 for the basic model. The premium model, which includes a built-in cable management system and USB charging ports, is priced at $649. Both models are available through the ProDesk website and authorized retailers.\n\nQuestion: What additional features does the premium model include?', + '["A. A wireless charging pad and extra storage drawer", "B. A built-in cable management system and USB charging ports", "C. An extended 10-year warranty and standing mat", "D. A larger desktop surface and additional memory presets"]', + 'B', + 'Model cao cấp có thêm hệ thống quản lý cáp tích hợp và cổng sạc USB.'), + +(7, 'reading', + E'PRODUCT SPOTLIGHT: ProDesk X500 Standing Desk\n\nThe ProDesk X500 is a motorized height-adjustable desk designed for modern office environments. With a dual-motor lifting system, the desk can transition from sitting to standing height in under 10 seconds. It supports a maximum load of 150 kilograms and features a memory function that saves up to four custom height presets.\n\nThe desktop surface is available in three finishes: natural oak, white, and charcoal grey. Assembly is straightforward and takes approximately 45 minutes with two people. The desk comes with a 5-year warranty covering all mechanical components.\n\nPricing starts at $499 for the basic model. The premium model, which includes a built-in cable management system and USB charging ports, is priced at $649. Both models are available through the ProDesk website and authorized retailers.\n\nQuestion: How long is the warranty on the ProDesk X500?', + '["A. 1 year", "B. 3 years", "C. 10 years", "D. 5 years"]', + 'D', + 'Bàn đi kèm bảo hành 5 năm cho tất cả các bộ phận cơ khí.'), + +-- Passage E: News article about a company expansion +(7, 'reading', + E'BUSINESS NEWS\n\nNorthStar Logistics Expands Operations to Vietnam\n\nNorthStar Logistics, a leading supply chain management company headquartered in Singapore, announced yesterday that it will open three new distribution centers in Vietnam over the next 18 months. The first facility, located in Hanoi, is expected to be operational by Q2 of next year. The two remaining centers will be established in Da Nang and Ho Chi Minh City.\n\nThe expansion is part of the company''s broader Southeast Asia growth strategy, driven by increasing demand from e-commerce clients. "Vietnam represents one of the fastest-growing logistics markets in the region," said CEO Marcus Tan. "We are committed to building a strong local presence to serve both domestic and international customers."\n\nNorthStar plans to hire approximately 500 local employees across the three sites. The company has also announced a partnership with Vietnam Post to utilize their last-mile delivery network.\n\nQuestion: Where is NorthStar Logistics headquartered?', + '["A. Vietnam", "B. Malaysia", "C. Thailand", "D. Singapore"]', + 'D', + 'NorthStar Logistics có trụ sở chính tại Singapore — "headquartered in Singapore."'), + +(7, 'reading', + E'BUSINESS NEWS\n\nNorthStar Logistics Expands Operations to Vietnam\n\nNorthStar Logistics, a leading supply chain management company headquartered in Singapore, announced yesterday that it will open three new distribution centers in Vietnam over the next 18 months. The first facility, located in Hanoi, is expected to be operational by Q2 of next year. The two remaining centers will be established in Da Nang and Ho Chi Minh City.\n\nThe expansion is part of the company''s broader Southeast Asia growth strategy, driven by increasing demand from e-commerce clients. "Vietnam represents one of the fastest-growing logistics markets in the region," said CEO Marcus Tan. "We are committed to building a strong local presence to serve both domestic and international customers."\n\nNorthStar plans to hire approximately 500 local employees across the three sites. The company has also announced a partnership with Vietnam Post to utilize their last-mile delivery network.\n\nQuestion: What is driving the expansion according to the article?', + '["A. Increasing demand from e-commerce clients", "B. Government incentives for foreign investment", "C. A merger with a local logistics company", "D. New trade agreements in Southeast Asia"]', + 'A', + 'Sự mở rộng được thúc đẩy bởi nhu cầu ngày càng tăng từ các khách hàng thương mại điện tử.'), + +(7, 'reading', + E'BUSINESS NEWS\n\nNorthStar Logistics Expands Operations to Vietnam\n\nNorthStar Logistics, a leading supply chain management company headquartered in Singapore, announced yesterday that it will open three new distribution centers in Vietnam over the next 18 months. The first facility, located in Hanoi, is expected to be operational by Q2 of next year. The two remaining centers will be established in Da Nang and Ho Chi Minh City.\n\nThe expansion is part of the company''s broader Southeast Asia growth strategy, driven by increasing demand from e-commerce clients. "Vietnam represents one of the fastest-growing logistics markets in the region," said CEO Marcus Tan. "We are committed to building a strong local presence to serve both domestic and international customers."\n\nNorthStar plans to hire approximately 500 local employees across the three sites. The company has also announced a partnership with Vietnam Post to utilize their last-mile delivery network.\n\nQuestion: When is the Hanoi facility expected to be operational?', + '["A. By the end of this year", "B. By Q2 of next year", "C. Within the next three months", "D. By Q4 of next year"]', + 'B', + 'Cơ sở Hà Nội dự kiến đi vào hoạt động vào quý 2 năm tới.'), + +(7, 'reading', + E'BUSINESS NEWS\n\nNorthStar Logistics Expands Operations to Vietnam\n\nNorthStar Logistics, a leading supply chain management company headquartered in Singapore, announced yesterday that it will open three new distribution centers in Vietnam over the next 18 months. The first facility, located in Hanoi, is expected to be operational by Q2 of next year. The two remaining centers will be established in Da Nang and Ho Chi Minh City.\n\nThe expansion is part of the company''s broader Southeast Asia growth strategy, driven by increasing demand from e-commerce clients. "Vietnam represents one of the fastest-growing logistics markets in the region," said CEO Marcus Tan. "We are committed to building a strong local presence to serve both domestic and international customers."\n\nNorthStar plans to hire approximately 500 local employees across the three sites. The company has also announced a partnership with Vietnam Post to utilize their last-mile delivery network.\n\nQuestion: What partnership has NorthStar announced?', + '["A. A joint venture with a Vietnamese logistics startup", "B. A deal with Vietnam Airlines for air freight", "C. A collaboration with an international courier service", "D. A partnership with Vietnam Post for last-mile delivery"]', + 'D', + 'NorthStar thông báo hợp tác với Vietnam Post để sử dụng mạng lưới giao hàng chặng cuối của họ.'); + +-- ============================================================ +-- VOCABULARY (31 words across 6 topics — retained from original) +-- ============================================================ +INSERT INTO vocab (word, phonetic, meaning_vi, topic, example) VALUES + +-- Business (6 words) +('negotiate', '/nɪˈɡoʊʃieɪt/', 'đàm phán', 'Business', 'We need to negotiate the contract terms.'), +('collaborate', '/kəˈlæbəreɪt/', 'hợp tác', 'Business', 'Teams collaborate to achieve shared goals.'), +('delegate', '/ˈdelɪɡeɪt/', 'uỷ quyền, phân công', 'Business', 'A good manager knows how to delegate tasks.'), +('implement', '/ˈɪmplɪment/', 'triển khai, thực hiện', 'Business', 'We will implement the new policy next month.'), +('merger', '/ˈmɜːrdʒər/', 'sáp nhập công ty', 'Business', 'The merger will create a stronger combined company.'), +('acquisition', '/ˌækwɪˈzɪʃən/', 'mua lại, thâu tóm', 'Business', 'The acquisition was completed ahead of schedule.'), + +-- Office (5 words) +('agenda', '/əˈdʒendə/', 'chương trình nghị sự', 'Office', 'Please review the agenda before the meeting.'), +('minutes', '/ˈmɪnɪts/', 'biên bản họp', 'Office', 'Could you take the meeting minutes today?'), +('submit', '/səbˈmɪt/', 'nộp, gửi đi', 'Office', 'Please submit your report by Friday afternoon.'), +('deadline', '/ˈdedlaɪn/', 'hạn chót', 'Office', 'The deadline for this project is end of month.'), +('cubicle', '/ˈkjuːbɪkəl/', 'góc làm việc riêng', 'Office', 'Each employee has their own cubicle in the open office.'), + +-- Travel (5 words) +('itinerary', '/aɪˈtɪnəreri/', 'lịch trình chuyến đi', 'Travel', 'Here is your detailed travel itinerary.'), +('boarding pass', '/ˈbɔːrdɪŋ pæs/', 'thẻ lên máy bay', 'Travel', 'Please have your boarding pass ready at the gate.'), +('layover', '/ˈleɪoʊvər/', 'thời gian quá cảnh', 'Travel', 'There is a two-hour layover in Singapore.'), +('customs', '/ˈkʌstəmz/', 'hải quan', 'Travel', 'All passengers must go through customs on arrival.'), +('baggage claim', '/ˈbæɡɪdʒ kleɪm/', 'băng chuyền hành lý', 'Travel', 'Meet us at the baggage claim after landing.'), + +-- Finance (5 words) +('reimburse', '/ˌriːɪmˈbɜːrs/', 'hoàn tiền', 'Finance', 'The company will reimburse all travel expenses.'), +('invoice', '/ˈɪnvɔɪs/', 'hoá đơn', 'Finance', 'Please send the invoice to our accounting department.'), +('budget', '/ˈbʌdʒɪt/', 'ngân sách', 'Finance', 'We need to stay within the approved budget.'), +('revenue', '/ˈrevɪnjuː/', 'doanh thu', 'Finance', 'Revenue increased by 15% last quarter.'), +('fiscal year', '/ˈfɪskəl jɪər/', 'năm tài chính', 'Finance', 'Our fiscal year ends on December 31st.'), + +-- HR (5 words) +('recruit', '/rɪˈkruːt/', 'tuyển dụng', 'HR', 'We are recruiting experienced software engineers.'), +('probation', '/proʊˈbeɪʃən/', 'thử việc', 'HR', 'New employees have a 3-month probation period.'), +('appraisal', '/əˈpreɪzəl/', 'đánh giá nhân viên', 'HR', 'Annual performance appraisals are held in December.'), +('resignation', '/ˌrezɪɡˈneɪʃən/', 'đơn từ chức', 'HR', 'She submitted her resignation letter this morning.'), +('onboarding', '/ˈɒnbɔːrdɪŋ/', 'quy trình tiếp nhận nhân viên mới', 'HR', 'The onboarding process takes about two weeks.'), + +-- Marketing (5 words) +('campaign', '/kæmˈpeɪn/', 'chiến dịch', 'Marketing', 'The marketing campaign exceeded all expectations.'), +('demographics', '/ˌdeməˈɡræfɪks/', 'nhân khẩu học', 'Marketing', 'We need to understand our target demographics.'), +('endorse', '/ɪnˈdɔːrs/', 'chứng thực, bảo trợ', 'Marketing', 'The product is endorsed by professional athletes.'), +('branding', '/ˈbrændɪŋ/', 'xây dựng thương hiệu', 'Marketing', 'Consistent branding builds long-term customer trust.'), +('conversion rate', '/kənˈvɜːrʒən reɪt/', 'tỷ lệ chuyển đổi', 'Marketing', 'Our conversion rate improved after the redesign.');