Directory structure: └── alphatra-stunning-system/ ├── README.md ├── check_module.js ├── components.json ├── eslint.config.mjs ├── next.config.mjs ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── tailwind.config.js ├── tsconfig.json ├── docs/ │ ├── agents.md │ ├── architecture.md │ ├── plan.md │ ├── rules.md │ └── spec.md ├── src/ │ ├── app/ │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── learn/ │ │ │ ├── [topic]/ │ │ │ │ └── page.tsx │ │ │ └── analytics/ │ │ │ └── page.tsx │ │ ├── profile/ │ │ │ └── history/ │ │ │ └── page.tsx │ │ ├── settings/ │ │ │ └── page.tsx │ │ ├── test-gen/ │ │ │ └── page.tsx │ │ └── write/ │ │ └── page.tsx │ ├── components/ │ │ ├── challenge/ │ │ │ └── DailyChallengeCard.tsx │ │ ├── content/ │ │ │ └── syllabus.md │ │ ├── learning/ │ │ │ └── LearningDashboard.tsx │ │ ├── tutor/ │ │ │ ├── ExamRunner.tsx │ │ │ ├── ExamSidebar.tsx │ │ │ ├── FeedbackDisplay.tsx │ │ │ ├── QuestionCard.tsx │ │ │ ├── QuizOptionsDialog.tsx │ │ │ ├── QuizReview.tsx │ │ │ ├── QuizReviewDialog.tsx │ │ │ ├── QuizRunner.tsx │ │ │ ├── SpeakingDrill.tsx │ │ │ ├── TheoryUploader.tsx │ │ │ ├── TheoryViewer.tsx │ │ │ ├── WritingCanvas.tsx │ │ │ └── question/ │ │ │ ├── QuestionRenderer.tsx │ │ │ ├── types.ts │ │ │ ├── hooks/ │ │ │ │ └── useQuestionState.ts │ │ │ └── renderers/ │ │ │ ├── Dictation.tsx │ │ │ ├── ErrorCorrection.tsx │ │ │ ├── GapFill.tsx │ │ │ ├── MultipleChoice.tsx │ │ │ └── SimpleInput.tsx │ │ └── ui/ │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── tabs.tsx │ │ └── textarea.tsx │ ├── content/ │ │ └── courses/ │ │ └── futuro-semplice.md │ └── lib/ │ ├── utils.ts │ ├── ai/ │ │ ├── actions.ts │ │ ├── config.ts │ │ ├── examples.ts │ │ ├── explain.ts │ │ ├── memory.ts │ │ ├── prompts.ts │ │ └── schemas.ts │ ├── challenge/ │ │ └── daily.ts │ ├── config/ │ │ └── topics.ts │ ├── content/ │ │ ├── loader.ts │ │ └── slicer.ts │ ├── didactic/ │ │ └── utils.ts │ ├── learning/ │ │ ├── curriculum.ts │ │ ├── curriculumBuilder.ts │ │ ├── curriculumIntegration.ts │ │ ├── diagnostic.ts │ │ ├── metrics.ts │ │ ├── moduleProgress.ts │ │ ├── planner.ts │ │ ├── sessionPlan.ts │ │ ├── sessionTemplates.ts │ │ ├── skillState.ts │ │ └── skillTree.ts │ └── store/ │ ├── config.ts │ ├── user.ts │ └── useUserStore.ts └── .snapshots/ ├── readme.md ├── config.json └── sponsors.md ================================================ FILE: README.md ================================================ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. ================================================ FILE: check_module.js ================================================ try { const path = require.resolve('@ai-sdk/openai'); console.log('Resolution Success:', path); } catch (e) { console.log('Resolution Failed:', e.message); // debug sibling try { console.log('Sibling ai:', require.resolve('ai')); } catch (e2) { console.log('Sibling ai missing'); } } ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "src/app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "registries": {} } ================================================ FILE: eslint.config.mjs ================================================ import { defineConfig, globalIgnores } from "eslint/config"; import nextVitals from "eslint-config-next/core-web-vitals"; import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: ".next/**", "out/**", "build/**", "next-env.d.ts", ]), ]); export default eslintConfig; ================================================ FILE: next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { // config options here }; export default nextConfig; ================================================ FILE: next.config.ts ================================================ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ }; export default nextConfig; ================================================ FILE: package.json ================================================ { "name": "test_master", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint" }, "dependencies": { "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.344.0", "next": "14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", "remark-directive": "^4.0.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "unist-util-visit": "^5.0.0", "zod": "^3.22.4", "zustand": "^5.0.9" }, "devDependencies": { "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "autoprefixer": "^10.4.0", "eslint": "^8.0.0", "eslint-config-next": "14.2.3", "postcss": "^8.4.0", "tailwindcss": "^3.4.0", "typescript": "^5.0.0" } } ================================================ FILE: postcss.config.mjs ================================================ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; export default config; ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "#ea2a33", // New primary color foreground: "#ffffff", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, // New Design System Colors "background-light": "#f8f6f6", "background-dark": "#211111", "surface-light": "#ffffff", "surface-dark": "#2d1b1b", "text-main": "#1b0e0e", "text-sub": "#994d51", }, fontFamily: { "display": ["var(--font-jakarta)", "sans-serif"], // Using CSS variable for Next.js font }, borderRadius: { lg: "1.5rem", md: "1rem", // Default radius in new design is 1rem sm: "0.5rem", "xl": "2rem", "2xl": "3rem", }, boxShadow: { 'soft': '0 4px 20px -2px rgba(27, 14, 14, 0.05)', 'glow': '0 0 15px rgba(234, 42, 51, 0.3)', }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": [ "./src/*" ] } }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts" ], "exclude": [ "node_modules" ] } ================================================ FILE: docs/agents.md ================================================ # AGENTS CONFIGURATION Jesteś zespołem trzech ekspertów inżynierii oprogramowania. Twoim celem jest przeprowadzenie Stakeholdera (Użytkownika) przez proces zbierania wymagań, a następnie wygenerowanie perfekcyjnej specyfikacji. ## 🎭 TWOJE PERSONY (Działasz jako jedna z nich w danej turze) ### 1. 👔 Product Manager (PM) * **Cel:** Zrozumieć "dlaczego" i "co". Właściciel `spec.md`. * **Styl:** Empatyczny, zorganizowany, nietechniczny. * **Zadania:** * Zbiera User Stories w formacie EARS. * Definiuje Kryteria Akceptacji (Gherkin). * Dba o to, by użytkownik nie czuł się przytłoczony. ### 2. 🎨 UX Researcher (UX) * **Cel:** Zrozumieć "dla kogo" i "jak". Współtwórca `spec.md`. * **Styl:** Wizualny, skupiony na przepływie. * **Zadania:** * Tworzy Mapy Podróży Użytkownika (Journey Maps). * Opisuje ekrany i intencje (bez niskopoziomowych szczegółów UI). ### 3. 🏗️ Technical Architect (Tech) * **Cel:** Zrozumieć "czym" i "gdzie". Właściciel `architecture.md`. * **Styl:** Precyzyjny, analityczny, skupiony na SDD (Spec-Driven Development). * **Zadania:** * Projektuje schemat OpenAPI (Contract First). * Definiuje stos technologiczny i granice systemu. # AI Agent Roles & Responsibilities ## 1. Role: Product Owner & Pedagogical Architect * **Focus:** User Value, Learning Science, Curriculum Structure. * **Directives:** * Ensure all generated exercises have a clear learning objective. * Reject "lazy" multiple-choice questions unless strictly necessary. * Validate that AI feedback explains *why* an answer is wrong (metalinguistic feedback), not just *what* is wrong. * **Motto:** "We are building a teacher, not a quiz maker." ## 2. Role: Senior Frontend Developer (UI/UX) * **Focus:** React (Next.js), Tailwind, Accessibility, State Management. * **Directives:** * Use `shadcn/ui` for a clean, distraction-free interface. * Implement optimistic UI updates for instant feedback feel. * Ensure the text input area for Writing tasks supports rich text highlighting (for error marking). ## 3. Role: AI Integration Engineer (Backend/Edge) * **Focus:** Vercel AI SDK, Prompt Engineering, Structured Data (JSON). * **Directives:** * ALL LLM outputs must be forced into strict JSON Schema (using Zod). * Implement caching strategies to save tokens on repeated theory requests. * Design the "Context Injection" mechanism: How to feed 10 pages of PDF theory into the prompt context efficiently. ## 4. Role: QA & Compliance * **Focus:** Edge cases, Hallucination checks, Type Safety. * **Directives:** * Test: What happens if the LLM generates invalid JSON? (Fallback mechanisms). * Test: What happens if the user inputs Italian profanity in the Writing Lab? --- ## 🔄 PROTOKÓŁ ROZMOWY (Ściśle przestrzegaj) ### FAZA 1: Discovery (Wywiad) 1. **Start:** Przedstaw się jako PM i zadaj pytanie otwierające: *"Jestem Twoim Product Managerem. Opowiedz mi historię problemu, który chcesz rozwiązać?"* 2. **Pętla:** * Zadawaj **TYLKO JEDNO** pytanie na raz. * Unikaj żargonu technicznego w pytaniach do Stakeholdera. * Persony wymieniają się pytaniami (np. runda 1: PM, runda 2: UX, runda 3: Tech), aby pokryć wszystkie aspekty. * Kontynuuj przez minimum 3 rundy na eksperta (łącznie ~9 pytań), aż zbierzesz komplet wymagań. ### FAZA 2: Creation (Generowanie plików) 1. Po zebraniu danych, poinformuj użytkownika, że przystępujesz do tworzenia dokumentacji. 2. Zaktualizuj plik `spec.md` (wymagania). 3. Zaktualizuj plik `architecture.md` (rozwiązanie techniczne). 4. Zaktualizuj plik `plan.md` (status prac). ### FAZA 3: Feedback 1. Zaprezentuj podsumowanie zmian. 2. Zapytaj o akceptację. ================================================ FILE: docs/architecture.md ================================================ # SYSTEM ARCHITECTURE (UPDATED) ## 1. 🏗️ High-Level Design System "Local-First AI". Aplikacja automatycznie dostosowuje się do środowiska użytkownika, wykrywając dostępne modele LLM. ```mermaid graph TD User[Użytkownik] -->|Przeglądarka| FE[Next.js App] FE -->|API Request| BE[Next.js API Route] BE -- 1. Check Models --> OllamaAPI[Ollama API /api/tags] OllamaAPI -- 2. Return 'llama3' --> BE BE -- 3. Generate(prompt) --> OllamaLLM[Local LLM (e.g. Llama3/Mistral)] OllamaLLM -- 4. Structured JSON --> BE BE -->|Response| FE ``` # Technical Architecture & Stack Definition ## 1. Core Stack * **Framework:** Next.js 14+ (App Router). * **Language:** TypeScript (Strict Mode). * **Styling:** Tailwind CSS + Shadcn/UI (Radix Primitives). * **State Management:** React Context or Zustand (for session state). ## 2. AI & Data Layer * **LLM Provider:** OpenAI (gpt-4o-mini for speed/cost) or Anthropic (Claude 3.5 Sonnet for reasoning). * **Orchestration:** Vercel AI SDK (`generateObject` for JSON, `streamText` for explanations). * **Data Storage (MVP):** * `src/data/theory`: Markdown/JSON files containing the parsed content from PDFs. * `localStorage`: User progress and saved tests. * **Data Storage (Future):** Supabase (PostgreSQL) for user accounts and vector embeddings (pgvector). ## 3. System Components ### A. The "Prompt Factory" A dedicated module (`src/lib/ai/prompts`) that constructs dynamic prompts based on: 1. **Base Instructions:** (You are an Italian teacher...) 2. **Context:** (Chunks from `spec.md` theory files) 3. **User Constraints:** (Level A2, Focus: Verbs) ### B. The Validator (Zod) No data from the LLM reaches the UI without passing through a Zod Schema validation. * `TestSchema`: Array of Questions. * `FeedbackSchema`: Array of Errors with indices and explanations. ## 4. Directory Structure ```text /src /app # Next.js App Router /components /ui # Shadcn components /tutor # Domain specific (WritingArea, QuizCard) /lib /ai # Prompt templates, Server Actions /content # Parsed PDF content (JSON/MD) /types # TypeScript interfaces ================================================ FILE: docs/plan.md ================================================ # Implementation Plan ## Phase 1: Foundation & "Hello World" - [ ] Initialize Next.js project with TypeScript & Tailwind. - [ ] Install `shadcn/ui` core components (Button, Card, Textarea, Input). - [ ] Set up OpenAI API / Vercel AI SDK env variables. - [ ] Create simple `theory.json` mock data (manually extracted from PDF - e.g., "Futuro Semplice" table). ## Phase 2: The Generator Engine (Backend Logic) - [ ] Define Zod Schemas for `QuizQuestion` and `QuizResult`. - [ ] Implement Server Action: `generateQuiz(topic, difficulty)`. - [ ] **Prompt Engineering:** Create the system prompt that accepts JSON theory and outputs JSON questions. - [ ] Test generation of 3 types: Multiple Choice, Fill-in-the-gap, Scrambled Sentence. ## Phase 3: The Learning UI (Frontend) - [ ] Build `TheoryViewer` component (renders Markdown text). - [ ] Build `QuizRunner` component (handles user answers, shows progress). - [ ] Implement immediate feedback UI (Green/Red highlights). ## Phase 4: The Writing Lab (Advanced AI) - [ ] Build `WritingCanvas` (large text area). - [ ] Implement `analyzeWriting` Server Action. - [ ] Create `FeedbackOverlay`: A component that highlights text ranges based on AI response coordinates. ## Phase 5: Polish & Persistence - [ ] Add loading skeletons (AI takes time). - [ ] Save past quiz results to LocalStorage. - [ ] Final Review against `rules.md`. ================================================ FILE: docs/rules.md ================================================ # PROJECT RULES & GUIDELINES ## 🛑 NON-NEGOTIABLE RULES 1. **Spec-First:** Żaden kod (poza prototypami) nie może powstać bez wcześniejszego zdefiniowania w `spec.md` i `architecture.md`. 2. **Contract-First:** Zmiany w API muszą zaczynać się od modyfikacji definicji OpenAPI/Swagger w `architecture.md` lub dedykowanym pliku `.yaml`. 3. **One Question Limit:** Podczas interakcji z użytkownikiem, nigdy nie zadawaj serii pytań. Zawsze czekaj na odpowiedź. ## 📝 DOCUMENTATION STANDARDS * **User Stories:** Muszą używać składni EARS (Easy Approach to Requirements Syntax). * *Wzór:* "When [trigger], the [system] shall [response]." * **Diagrams:** Wszystkie diagramy muszą być renderowane w **Mermaid.js**. * **Language:** Cała dokumentacja w języku polskim (chyba że kod wymaga angielskiego). ## 🛠 TECH STACK CONSTRAINTS * **Frontend:** Next.js (App Router), Tailwind CSS, shadcn/ui. * **Backend:** Python (FastAPI) LUB Node.js (Next.js API Routes). * **Validation:** Zod (dla schematów JSON). * **AI Integration:** Vercel AI SDK + `ollama-ai-provider`. * **Endpoint Ollama:** `http://127.0.0.1:11434`. ## 💾 FILE MANAGEMENT * Nie usuwaj treści bez wyraźnego polecenia. * Zawsze aktualizuj `plan.md` po zakończeniu zadania. # PROJECT RULES & GUIDELINES ## 🛑 NON-NEGOTIABLE RULES 1. **Local-First:** Domyślny tryb pracy to lokalna Ollama. Nie używaj kluczy API zewnętrznych dostawców (OpenAI/Anthropic) w kodzie produkcyjnym. 2. **Auto-Discovery:** Nie hardcoduj nazw modeli (np. "llama2"). Kod musi dynamicznie pobierać nazwę modelu z endpointu `/api/tags`. # Operational Rules & Constraints ## 1. Coding Standards * **TypeScript:** No `any`. Use strict interfaces for all AI responses. * **Components:** Logic must be separated from UI. Use Custom Hooks for complex logic (`useQuizEngine`, `useAIWriting`). * **Naming:** Variables related to AI data should be prefixed clearly (e.g., `aiGeneratedQuestions`, `userInputText`). ## 2. AI & Prompting Guidelines (The "Safety Rail") * **JSON Only:** Always request JSON mode from the LLM to prevent parsing errors. * **Language Enforcement:** System prompts must explicitly state: "All explanations in Polish, all exercises in Italian." * **Theory Grounding:** The AI instructions must say: "Only use vocabulary and grammar rules present in the provided Context. Do not introduce C1 level words to an A1 student." ## 3. Pedagogical Design Rules * **No "Yes/No" Questions:** Avoid binary questions; they have low educational value. * **Feedback Mandatory:** Every error must be accompanied by an explanation field. * *Bad:* "Wrong." * *Good:* "Incorrect. 'Andrò' is Future Tense. We need Present Tense here ('Vado')." * **Positive Reinforcement:** UI should celebrate streaks and correct answers visually. ## 4. Git & Workflow * Update `plan.md` status after every commit. * Never commit API keys. ================================================ FILE: docs/spec.md ================================================ # Project Specification: AI Language Tutor (MVP: Italian) ## 1. High-Level Concept A web-based language learning platform that utilizes Generative AI to create dynamic, context-aware exercises based on uploaded theoretical material (PDFs/Notes). The system moves beyond simple quizzes, implementing **Second Language Acquisition (SLA)** principles like *Input Hypothesis (i+1)* and *Active Retrieval*. ## 2. Target Audience * **Primary:** Italian language learners (Levels A1-B2). * **Secondary:** Teachers creating automated assessments from their own notes. ## 3. Core Features ### 3.1 The "Context-Aware" Test Generator * **Mechanism:** User selects a topic (e.g., "Futuro Semplice") and difficulty. The system retrieves relevant theory from `knowledge-base` (e.g., conjugation tables) and generates unique questions. * **Supported Exercise Types:** 1. **Gap Fill (Cloze Tests):** Smart removal of auxiliary verbs (essere/avere) or prepositions. 2. **Syntactic Reordering:** Reassembling scrambled sentences to teach sentence structure (SVO). 3. **Error Detection:** The AI generates a plausible error (e.g., "Domani *vado* a fare la spesa" vs "andrò") and asks the user to fix it. 4. **Transformation Drills:** "Change this sentence from Presente to Passato Prossimo". ### 3.2 The Writing Lab ("Il Laboratorio di Scrittura") * **Input:** Open-ended text field with a specific prompt (e.g., "Write a formal email to a professor canceling a meeting"). * **AI Analysis Pipeline:** * **Strictness Level:** Configurable (Forgiving vs. Academic). * **Feedback Categories:** * 🔴 **Grammar:** Critical errors (conjugation, agreement). * 🟠 **Vocabulary:** Repetitive words or false friends. * 🔵 **Pragmatics:** Tone appropriateness (Tu vs. Lei). * **Output:** Highlighted text with tooltip explanations, NOT just a rewritten version. ### 3.3 Theory Hub * A clean, read-only view of the source material (Markdown/PDF content) linked directly to exercises. * **Feature:** "Peek at Theory" – during a test, user can reveal the specific rule (costing "mastery points"). ## 4. User Flows 1. **Onboarding:** Select Language -> Upload/Select Theory Module. 2. **Practice Loop:** Choose Topic -> Generate Test -> Solve -> Receive Granular Feedback -> Retry failed concepts. ## 5. Non-Functional Requirements * **Latency:** Test generation must take < 5 seconds. * **Determinism:** The same seed should generate the same test (for review purposes). * **Accuracy:** Hallucinations regarding grammar rules are strictly forbidden. ================================================ FILE: src/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: src/app/layout.tsx ================================================ import type { Metadata } from "next"; import { Plus_Jakarta_Sans } from "next/font/google"; import "./globals.css"; import { Toaster } from 'sonner'; const jakarta = Plus_Jakarta_Sans({ subsets: ["latin"], variable: '--font-jakarta', }); export const metadata: Metadata = { title: "Test Master: Learn Italian", description: "AI-powered Italian language tutor", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: src/app/page.tsx ================================================ 'use client'; import React, { useState } from 'react'; import Link from 'next/link'; import { useUserStore } from '@/lib/store/useUserStore'; import { TOPICS, Topic } from '@/lib/config/topics'; import * as LucideIcons from 'lucide-react'; import { useRouter } from 'next/navigation'; import { generateQuiz } from '@/lib/ai/actions'; import { toast } from 'sonner'; // Helper component for dynamic icon const DynamicIcon = ({ name, className }: { name: string; className?: string }) => { const Icon = (LucideIcons as any)[name] || LucideIcons.BookOpen; return ; }; export default function Dashboard() { const user = useUserStore(); const router = useRouter(); // Quiz Configuration State const [level, setLevel] = useState('A2'); const [source, setSource] = useState<'syllabus' | 'ai'>('syllabus'); const [focus, setFocus] = useState('Gramatyka & Słownictwo (Mixed)'); const [tone, setTone] = useState('Standardowy (Neutral)'); const [loading, setLoading] = useState(false); const startQuickQuiz = async () => { setLoading(true); try { // In a real scenario, we'd use these config values // For now, redirecting to a generic topic or using the first one const topicId = TOPICS[0].id; router.push(`/learn/${topicId}`); } catch (error) { toast.error("Nie udało się rozpocząć testu"); } finally { setLoading(false); } }; const activeTopics = TOPICS; // Show all topics for now return (
{/* Top Navigation */}
TM

Test Master

{/* Desktop Nav */}
{user?.name?.[0] || 'U'}
{/* Mobile Menu Icon */}
{/* Main Content Area */}
{/* Left Column: Content Stream */}
{/* Breadcrumbs */} {/* Page Heading */}

Wybierz temat

Kontynuuj swoją przygodę z językiem włoskim.

{/* Featured Topic (Hero) */}
Polecane

Codzienne rozmowy

Naucz się swobodnie rozmawiać o pogodzie, samopoczuciu i planach na weekend.

{/* Abstract pattern background */}
{/* Topics Grid Section */}

Dostępne Tematy (Topics)

{activeTopics.map((topic) => (
{user.topicStats?.[topic.id]?.mastery && (
{user.topicStats[topic.id].mastery >= 100 ? 'Ukończone' : `${Math.round(user.topicStats[topic.id].mastery)}%`}
)}

{topic.title}

{topic.description}

Postęp {Math.round(user.topicStats?.[topic.id]?.mastery || 0)}%
))}
{/* Right Column: Quiz Configuration Panel */}

Konfiguracja Testu

{/* Level Selector */}
{['A1', 'A2', 'B1', 'B2'].map((lvl) => ( ))}
{/* Source Selector */}
{/* Dropdowns */}
{/* Divider */}
{/* CTA Button */}

Szacowany czas: ~15 minut

); } ================================================ FILE: src/app/learn/[topic]/page.tsx ================================================ 'use client'; import React, { useState, useEffect } from 'react'; import { useParams, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { generateQuiz } from '@/lib/ai/actions'; import { getTopicContent } from '@/lib/content/loader'; import { Quiz } from '@/lib/ai/schemas'; import { useUserStore } from '@/lib/store/useUserStore'; import { ConfigStore } from '@/lib/store/config'; import { QuizRunner } from '@/components/tutor/QuizRunner'; import { TheoryViewer } from '@/components/tutor/TheoryViewer'; import { QuizOptionsDialog } from '@/components/tutor/QuizOptionsDialog'; import { SpeakingDrill } from '@/components/tutor/SpeakingDrill'; import { TOPICS } from '@/lib/config/topics'; import { toast } from 'sonner'; import { GraduationCap, Clock, Play, Bookmark, Calendar, Brain, ChevronRight, ChevronLeft, Sparkles, Edit3, Mic, HelpCircle, MessageCircle, Lightbulb, BarChart, FileText } from 'lucide-react'; export default function LearnPage() { const params = useParams(); const topicId = decodeURIComponent(params.topic as string); const topicConfig = TOPICS.find(t => t.id === topicId); // Use title from config if available, otherwise fallback to topicId formatted const title = topicConfig ? topicConfig.title : topicId.replace(/-/g, ' '); const [activeTab, setActiveTab] = useState('theory'); // 'theory' | 'practice' | 'examples' | 'exercises' const [quiz, setQuiz] = useState(null); const [loading, setLoading] = useState(false); const [customTheory, setCustomTheory] = useState(''); const [optionsOpen, setOptionsOpen] = useState(false); const user = useUserStore(); const searchParams = useSearchParams(); const isDailyChallenge = searchParams.get('daily') === 'true'; const [quizMode, setQuizMode] = useState<'practice' | 'exam'>('practice'); // Load Content useEffect(() => { const loadContent = async () => { if (topicId === 'smart-review') { const items = user.getMistakes(); setCustomTheory(items.length > 0 ? items.join('\n') : "Brak błędów do przeglądu! Świetna robota."); return; } const content = await getTopicContent(topicId, topicConfig?.sourceFile); if (content) { setCustomTheory(content); } else { setCustomTheory("# Brak Treści\n\nNiestety, treść tej lekcji nie została jeszcze dodana do bazy plików."); } }; loadContent(); }, [topicId, user, topicConfig]); // Check for Retry Quiz useEffect(() => { if (user.retryQuiz && user.retryQuiz.topic === title) { setQuiz({ topic: user.retryQuiz.topic, difficulty: 'A2', questions: user.retryQuiz.questions }); user.setRetryQuiz(null); setOptionsOpen(false); setActiveTab('practice'); toast.info('Wczytano quiz z historii.'); } }, [title, user.retryQuiz, user]); const startQuiz = async (options: { difficulty: string; deepCheck: boolean; amount: number; mode: 'practice' | 'exam'; config?: any; }) => { setLoading(true); setQuizMode(options.mode); try { const promptTopic = topicId === 'smart-review' ? 'Smart Review' : topicId; // Get global config at runtime const globalConfig = ConfigStore.get(); const result = await generateQuiz( promptTopic, options.difficulty, customTheory || undefined, { model: globalConfig.aiModel, deepCheck: options.deepCheck, amount: options.amount, config: options.config, aiConfig: { provider: globalConfig.aiProvider, url: globalConfig.aiUrl } } ); if (result.success && result.data) { setQuiz(result.data); if (options.deepCheck) toast.info('Supervisor zweryfikował quiz.'); setActiveTab('practice'); } } catch (error) { toast.error('Błąd generowania quizu. Spróbuj ponownie.'); console.error(error); } finally { setLoading(false); } }; // Loading State Overlay if (loading) { return (

Tworzenie Twojego Quizu... 🎨

Analizuję materiał dydaktyczny...

(Może to potrwać do 60 sekund)

); } return (
{/* Header / Breadcrumbs */}
{/* Main Content Column */}
{/* Hero Card */}
Gramatyka
15 min
{topicConfig?.level || 'A2'}

{title}

{topicConfig?.description || 'Opanuj ten temat dzięki lekcjom i ćwiczeniom interaktywnym.'}

{activeTab === 'practice' && ( )}
{/* Navigation Tabs */}
{[ { id: 'theory', label: 'Przegląd' }, { id: 'rules', label: 'Zasady' }, // Alias for theory for now { id: 'examples', label: 'Przykłady' }, { id: 'practice', label: 'Ćwiczenia' }, { id: 'speaking', label: 'Mowa' } ].map(tab => ( ))}
{/* CONTENT AREA */}
{/* THEORY TAB */} {(activeTab === 'theory' || activeTab === 'rules') && ( <>

Materiał Lekcyjny

{/* Dynamic content rendered nicely */}
)} {/* PRACTICE / QUIZ TAB */} {activeTab === 'practice' && (
{!quiz ? (

Gotowy na wyzwanie?

Wygeneruj spersonalizowany quiz AI, aby przetestować swoją wiedzę w praktyce.

) : ( setQuiz(null)} mode={quizMode} topicId={topicId} isDailyChallenge={isDailyChallenge} /> )}
)} {/* EXAMPLES / EXTRAS TAB (Placeholder) */} {activeTab === 'examples' && (

Plany na przyszłość

Opisywanie czynności, które na pewno lub prawdopodobnie się wydarzą.
"Domani andrò al mare."

Przypuszczenia

Wyrażanie wątpliwości lub przypuszczeń.
"Sarà vero?"

)} {/* SPEAKING TAB */} {activeTab === 'speaking' && (

Trening Wymowy (Beta)

Powiedz zdanie związane z tematem {title}.

{ toast.success(`Rozpoznano: "${text}"`); // In real app, we would analyze grammar here setCustomTheory(prev => prev + `\n\n[Mowa Użytkownika]: ${text}`); }} />
Wskazówka: Użyj przeglądarki Chrome lub Edge dla najlepszej jakości rozpoznawania mowy.
)}
{/* SIDEBAR */}
{/* Progress Card */}

Twój postęp

W toku
35%
Teoria opanowana Zostały jeszcze ćwiczenia
{/* Exercise Quick Links */}

Szybkie Ćwiczenia

{[ { title: 'Uzupełnij luki', desc: '10 pytań • A1/A2', icon: Edit3, color: 'text-indigo-600', bg: 'bg-indigo-50 dark:bg-indigo-900/20' }, { title: 'Wymowa zdań', desc: '5 nagrań • A2', icon: Mic, color: 'text-pink-500', bg: 'bg-pink-50 dark:bg-pink-900/20' }, { title: 'Quiz Szybki', desc: '15 pytań • Mieszany', icon: HelpCircle, color: 'text-teal-600', bg: 'bg-teal-50 dark:bg-teal-900/20' } ].map((item, i) => (
{ setActiveTab('practice'); setOptionsOpen(true); }} className="group block bg-white dark:bg-zinc-800 p-4 rounded-xl border border-gray-100 dark:border-gray-700 hover:border-indigo-500/30 hover:shadow-md transition-all cursor-pointer">

{item.title}

{item.desc}

))}
{/* AI Tutor Promo */}
AI Tutor

Masz wątpliwości?

Zapytaj asystenta AI o wyjaśnienie dowolnego przykładu.

); } ================================================ FILE: src/app/learn/analytics/page.tsx ================================================ 'use client'; import { LearningDashboard } from '@/components/learning/LearningDashboard'; import { Button } from '@/components/ui/button'; import Link from 'next/link'; import { ArrowLeft } from 'lucide-react'; export default function LearningPage() { return (
{/* Header */}

Analiza Postępów

Sprawdź, jak efektywnie się uczysz i co poprawić

{/* Dashboard */} {/* Info Footer */}

💡 Jak interpretować metryki?

  • Transfer Wiedzy - czy rozumiesz reguły, czy tylko zapamiętałeś odpowiedzi
  • Uczenie się na Błędach - czy poprawiasz się po feedbacku
  • Automatyzacja - czy odpowiadasz szybciej (znak opanowania materiału)
); } ================================================ FILE: src/app/profile/history/page.tsx ================================================ 'use client'; import React, { useState } from 'react'; import { useUserStore } from '@/lib/store/useUserStore'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Calendar, ChevronLeft, Search, RotateCcw } from 'lucide-react'; import Link from 'next/link'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { QuizReview } from '@/components/tutor/QuizReview'; import { QuizResult } from '@/lib/store/useUserStore'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; export default function HistoryPage() { const user = useUserStore(); const router = useRouter(); const [selectedQuiz, setSelectedQuiz] = useState(null); // Group by date or just list? Let's list for now. const history = user.history; if (history.length === 0) { return (

Historia jest pusta

Nie wykonałeś jeszcze żadnych quizów. Rozpocznij naukę, aby śledzić postępy!

); } return (

Twoja Historia

{user.completedQuizzes} ukończonych testów

{history.map((quiz) => ( setSelectedQuiz(quiz)} >
{quiz.date}

{quiz.topic}

{quiz.questions.length} pytań

= 0.8 ? 'text-green-600' : (quiz.score / quiz.total) >= 0.5 ? 'text-amber-500' : 'text-red-500' }`}> {Math.round((quiz.score / quiz.total) * 100)}%
Wynik
))}
!open && setSelectedQuiz(null)}> {selectedQuiz && ( { /* No-op in review */ }} onRetry={() => { user.setRetryQuiz(selectedQuiz); router.push(`/learn/${selectedQuiz.topic}`); }} /> )}
); } ================================================ FILE: src/app/settings/page.tsx ================================================ 'use client'; import React, { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { checkConnection } from '@/lib/ai/actions'; import { Settings, Cpu, ShieldCheck, Save, ArrowLeft, User, BookOpen, Bell, Moon, Sun, Trash2, Smartphone, Monitor } from 'lucide-react'; import { ConfigStore, AppConfig } from '@/lib/store/config'; import { useUserStore } from '@/lib/store/useUserStore'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; export default function SettingsPage() { const router = useRouter(); const user = useUserStore(); // Config State const [config, setConfig] = useState(null); const [availableModels, setAvailableModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); // UI State const [name, setName] = useState(''); const [theme, setTheme] = useState<'light' | 'dark'>('light'); useEffect(() => { // Load initial data const initialConfig = ConfigStore.get(); setConfig(initialConfig); setName(user.name); // Detect theme if (document.documentElement.classList.contains('dark')) { setTheme('dark'); } // Fetch models using the loaded config const fetchModels = async () => { setLoadingModels(true); try { // Use the loaded config to check connection const result = await checkConnection({ provider: initialConfig.aiProvider, url: initialConfig.aiUrl }); if (result.success && result.models) { setAvailableModels(result.models); } else { // Fallback only if check fails entirely setAvailableModels(['llama3:latest', 'mistral:latest', 'gemma:latest']); } } catch (e) { // Fallback list setAvailableModels(['llama3:latest', 'mistral:latest', 'gemma:latest']); } finally { setLoadingModels(false); } }; fetchModels(); }, [user.name]); const handleSave = () => { if (!config) return; // Save Config ConfigStore.update(config); // Save User Profile if (name !== user.name) { user.setName(name); } toast.success("Ustawienia zostały zapisane!"); }; const toggleTheme = (newTheme: 'light' | 'dark') => { setTheme(newTheme); if (newTheme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }; const handleResetProgress = () => { if (confirm("Czy na pewno chcesz zresetować cały postęp? Tej operacji nie można cofnąć.")) { localStorage.removeItem('test_master_progress'); window.location.reload(); } }; if (!config) return null; return (
{/* Header */}

Ustawienia

Dostosuj aplikację do swoich potrzeb

{/* Navigation / TOC (Hidden on mobile for now, or just a sticky list) */}

Kategorie

{['Profil', 'Wygląd', 'AI i Generator', 'Niebezpieczne'].map((item) => (
{item}
))}
{/* Main Content Form */}
{/* PROFILE SECTION */}

Profil Użytkownika

{name ? name[0] : 'U'}
setName(e.target.value)} className="w-full p-2 rounded-lg border border-border bg-white dark:bg-black/20 text-text-main dark:text-white focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" placeholder="Wpisz swoje imię..." />

Poziom Mistrzostwa

{user.masteryPoints} Punktów XP

A2
{/* APPEARANCE SECTION */}

Wygląd

{/* AI SECTION */}

AI i Generator

{/* Provider Selection */}
{(['ollama', 'lmstudio'] as const).map((p) => ( ))}
{/* Connection Config & Check */}
{ // Allow editing, but sanitize on blur or use raw in store setConfig({ ...config, aiUrl: e.target.value }); }} onBlur={() => { // Basic cleanup on blur: remove trailing slash, remove /v1... if clearly duplicated let clean = config.aiUrl.trim().replace(/\/+$/, ''); if (config.aiProvider === 'lmstudio' && clean.endsWith('/v1')) { // Keep it if user explicitly wants base/v1, but standard is base // Actually LM Studio default is port 1234. Let's leave it unless it's obviously wrong. } setConfig({ ...config, aiUrl: clean }); }} className="flex-1 p-3 rounded-xl border border-border bg-white dark:bg-black/20 text-text-main dark:text-white focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all font-mono text-sm" />

{config.aiProvider === 'ollama' ? 'Domyślny port: 11434. Endpoint: /api/tags' : 'Domyślny port: 1234. Endpoint: /v1/models (Kompatybilny z OpenAI)'}

{/* Context Length Hint for LM Studio */} {config.aiProvider === 'lmstudio' && (

ℹ️ Wskazówka: Ustawienia LM Studio

Zalecamy ustawienie Context Length na min. 4096 (zakładka Server), aby uniknąć błędów przy długich konwersacjach.
Jeśli widzisz błąd "RotatingKVCache", wyłącz Context Cache Quantization.

)}
{/* Model Selection */}
{availableModels.length === 0 ? (
Kliknij "Sprawdź Połączenie", aby załadować modele.
) : (
)}

Głęboka Weryfikacja

Agent "Supervisor" sprawdzi poprawność każdego pytania. Zwiększa jakość, ale wydłuża czas o 5-10s.

setConfig({ ...config, generationSettings: { ...config.generationSettings, deepCheck: e.target.checked } })} className="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer peer checked:right-0 checked:border-green-400" />
{['low', 'medium', 'high'].map((level) => ( ))}
{/* DATA SECTION */}

Strefa Niebezpieczna

Zresetuj Cały Postęp

Usuwa historię quizów, punkty i statystyki.

{/* SAVE BUTTON */}
); } ================================================ FILE: src/app/test-gen/page.tsx ================================================ 'use client'; import { useState } from 'react'; import { generateQuiz } from '@/lib/ai/actions'; export default function TestGenPage() { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const handleGenerate = async () => { setLoading(true); const res = await generateQuiz('Futuro Semplice', 'A2'); setResult(res); setLoading(false); }; return (

Generator Test

{result && (
                    {JSON.stringify(result, null, 2)}
                
)}
); } ================================================ FILE: src/app/write/page.tsx ================================================ 'use client'; import React, { useState, useEffect } from 'react'; import { WritingCanvas } from '@/components/tutor/WritingCanvas'; import { FeedbackDisplay } from '@/components/tutor/FeedbackDisplay'; import { WritingAnalysis } from '@/lib/ai/schemas'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; const PROMPTS = [ { id: 'free', label: 'Free Write', text: 'Write about anything you want.' }, { id: 'email', label: 'Email a Friend', text: 'Write an email to a friend inviting them to dinner on Saturday.' }, { id: 'holiday', label: 'Last Holiday', text: 'Describe where you went for your last summer holiday.' }, { id: 'opinion', label: 'Opinion', text: 'Do you prefer living in the city or the countryside? Explain why.' }, ]; import { useUserStore, WritingResult } from '@/lib/store/useUserStore'; export default function WritingPage() { const user = useUserStore(); const [selectedPrompt, setSelectedPrompt] = useState(PROMPTS[0]); const [analysis, setAnalysis] = useState(null); const history = user.writingHistory; // No effect needed! const handlePromptChange = (val: string) => { const p = PROMPTS.find(x => x.id === val) || PROMPTS[0]; setSelectedPrompt(p); setAnalysis(null); // Reset analysis on prompt change }; return (

Laboratorio di Scrittura

Practice your written Italian with instant AI feedback.

Choose a Topic Select a prompt to guide your writing. {PROMPTS.map(p => ( {p.label} ))}
Topic: {selectedPrompt.text}
{ setAnalysis(res); user.saveWritingResult({ id: crypto.randomUUID(), date: new Date().toLocaleDateString('pl-PL'), topic: selectedPrompt.label, originalText: text, analysis: res }); }} /> {/* Writing History Section */}

📜 Twoje Portfolio (Historia)

{history.length === 0 ? (

Brak zapisanych prac.

) : (
{history.map(item => (

{item.topic}

{item.date}

Ocena: {item.analysis.score}/10

"{item.originalText}"

Znaleziono błędów: {item.analysis.corrections.length}
{/* Ideally clicking restores/views it, but for MVP just listing is fine */}
))}
)}
{analysis ? ( ) : (
✍️

Waiting for text...

Write something on the left and click "Check with AI Tutor" to see detailed feedback here.

)}
); } ================================================ FILE: src/components/challenge/DailyChallengeCard.tsx ================================================ 'use client'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { getTodaysDailyChallenge, isDailyChallengeCompleted } from '@/lib/challenge/daily'; import { useUserStore } from '@/lib/store/useUserStore'; import { Flame, Calendar, Target, Star } from 'lucide-react'; import Link from 'next/link'; import { TOPICS } from '@/lib/config/topics'; export function DailyChallengeCard() { const user = useUserStore(); const challenge = getTodaysDailyChallenge(); const isCompleted = isDailyChallengeCompleted(); const streak = user.dailyProgress.currentStreak; const longestStreak = user.dailyProgress.longestStreak; const topicConfig = TOPICS.find(t => t.id === challenge.topicId); const topicTitle = topicConfig?.title || challenge.topicId; return (
Codzienny Challenge
{/* Streak Counter */}
0 ? 'text-orange-500' : 'text-slate-300'}`} /> {streak} dni
{isCompleted && (
✅ Ukończono!
)}
{!isCompleted ? ( <> {/* Challenge Info */}

{topicTitle}

{challenge.reason}

{challenge.difficulty}
{/* Rewards */}
+50 punktów bonusowych {streak === 6 && (jutro +100 za 7-dniowy streak!)}
{/* Action Button */} ) : ( <> {/* Completed State */}
🎉

Świetna robota!

Ukończyłeś dzisiejszy challenge. Wróć jutro po kolejne wyzwanie!

{/* Streak Info */}
{streak}
Aktualny streak
{longestStreak}
Najdłuższy streak
)}
); } ================================================ FILE: src/components/content/syllabus.md ================================================ # Elenco Argomenti (Syllabus) ## Parte 1: Fonetica, Morfologia e Sintassi ### Fonetica e ortografia - L’alfabeto italiano: vocali e consonanti - Digrammi e trigrammi (gli, gn, sc, ch, gh), consonanti doppie, grafemi particolari ### Morfologia e sintassi - **Nomi**: - Genere (maschile, femminile) e particolarità - Numero (singolare, plurale), plurali irregolari - **Articoli**: Determinativi (il, lo, la, i, gli, le) e indeterminativi (un, uno, una) - **Aggettivi**: Flessione e accordo con il nome - **Gradi dell'aggettivo**: Comparativo (maggioranza, minoranza, uguaglianza) e Superlativo (relativo e assoluto) - **Dimostrativi**: Questo, quello (flessione e uso) - **Possessivi**: Mio, tuo, suo, ecc. - **Pronomi**: Pronomi diretti (lo, la, li, le) - **Verbi**: - Persona e numero - Transitivi e intransitivi, modali (dovere, potere, volere), riflessivi - **Tempi e Modi**: - Presente Indicativo - Stare + Gerundio - Stare per + Infinito - Passato Prossimo - Imperfetto - Verbi chiave: Essere, Avere, 3 coniugazioni (-are, -ere, -ire), irregolari - **Preposizioni**: - Semplici: di, a, da, in, con, su, per, tra, fra - Locuzioni: sopra, sotto, vicino, di fronte... - **Avverbi e Congiunzioni**: - Tempo, frequenza (sempre, di solito) - Congiunzioni base (e, ma, o, perché) - **Altro**: - C’è / Ci sono (Esserci) - Ordine delle parole nella frase ## Parte 2: Lessico e Pragmatica ### Lessico - Tempo atmosferico (meteo) - Tempo cronologico: momenti della giornata, ore, giorni, mesi, stagioni - La città (luoghi, direzioni) - Cibo e alimentazione - Salute / Malattia - Colori, numeri, operazioni matematiche - Nomi geografici e aggettivi di nazionalità ### Competenze Comunicative - Presentarsi e salutare (formale/informale) - Chiedere informazioni o interpellare qualcuno - Esprimere bisogni, intenzioni, desideri - Esprimere accordo / disaccordo - Scusarsi, fare gli auguri - Telefonare / Rispondere al telefono - Richiedere attenzione / Parola - Chiedere chiarimenti - Descrivere persone e luoghi ## Parte 3: Cultura e Società ### Competenze Socio-culturali - **Torino**: Luoghi d’interesse storico - **Geografia**: Italia e Piemonte - **Personaggi**: Italiani famosi nel mondo - **Lifestyle**: - Le case degli italiani - Alimentazione e gastronomia - Orari, abitudini, passatempi - **Viaggi**: Mete e mezzi di trasporto - **Stereotipi**: Luoghi comuni sugli italiani ================================================ FILE: src/components/learning/LearningDashboard.tsx ================================================ 'use client'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Button } from '@/components/ui/button'; import { TrendingUp, TrendingDown, Minus, Target, Lightbulb, Zap } from 'lucide-react'; import { useLearningMetrics, decideNextLesson } from '@/lib/learning/metrics'; import Link from 'next/link'; export function LearningDashboard() { const metrics = useLearningMetrics(); // Now a hook - memoized const nextLesson = decideNextLesson(metrics); // Convert 0-1 to percentage const transferPct = Math.round(metrics.transferAccuracy * 100); const recoveryPct = Math.round(metrics.errorRecovery * 100); // Color coding const getColor = (value: number) => { if (value >= 75) return 'text-green-600 bg-green-50 border-green-200'; if (value >= 50) return 'text-yellow-600 bg-yellow-50 border-yellow-200'; return 'text-red-600 bg-red-50 border-red-200'; }; const getProgressColor = (value: number) => { if (value >= 75) return '[&>div]:bg-green-500'; if (value >= 50) return '[&>div]:bg-yellow-500'; return '[&>div]:bg-red-500'; }; const TrendIcon = metrics.timeTrend === 'improving' ? TrendingUp : metrics.timeTrend === 'worsening' ? TrendingDown : Minus; return (
{/* Section 1: Learning Progress */} Postęp w Nauce Czy faktycznie się uczysz? Sprawdź kluczowe wskaźniki. {/* Transfer Accuracy */}

Transfer Wiedzy

Rozumienie reguł, nie zapamiętywanie

= 75 ? 'text-green-600' : transferPct >= 50 ? 'text-yellow-600' : 'text-red-600'}`}> {transferPct}%
{/* Error Recovery */}

Uczenie się na Błędach

Poprawa po feedbacku

= 75 ? 'text-green-600' : recoveryPct >= 50 ? 'text-yellow-600' : 'text-red-600'}`}> {recoveryPct}%
{/* Time Trend */}

Automatyzacja

Szybkość odpowiedzi

{ metrics.timeTrend === 'improving' ? 'Lepiej' : metrics.timeTrend === 'worsening' ? 'Gorzej' : 'Stabilnie' }
{/* Summary */}
= 75 && recoveryPct >= 75) ? 'bg-green-50 border-green-200' : (transferPct >= 50 && recoveryPct >= 50) ? 'bg-yellow-50 border-yellow-200' : 'bg-red-50 border-red-200' }`}>

{(transferPct >= 75 && recoveryPct >= 75) ? '✅ Świetnie! Realnie rozumiesz materiał i uczysz się na błędach.' : (transferPct >= 50 && recoveryPct >= 50) ? '⚠️ Robisz postępy, ale jest jeszcze przestrzeń na poprawę.' : '❌ Skupmy się na utrwaleniu podstaw przed pójściem dalej.'}

{/* Section 2: Weak Areas & Fluency */}
{/* Weak Areas */} Obszary do Poprawy {metrics.weakAreas.length === 0 ? (

Brak zidentyfikowanych słabych punktów. Brawo! 🎉

) : (
{metrics.weakAreas.map((area, idx) => (

{area.label}

{area.count} błędów

))}
)}
{/* Fluency Stats */} Płynność
{Math.round(metrics.avgTimePerQuestion / 1000)}s

Średni czas odpowiedzi

{metrics.timeTrend === 'improving' ? '📈 Świetnie! Odpowiadasz coraz szybciej - znak automatyzacji.' : metrics.timeTrend === 'worsening' ? '📉 Zwalniasz - może materiał jest za trudny?' : '➡️ Stabilny czas - kontynuuj praktykę.'}

{metrics.lastScoreRatio > 0 ? Math.round(metrics.lastScoreRatio * 100) : 0}%
Ostatni wynik
{transferPct}%
Transfer
{/* Section 3: Next Best Lesson */} 🎯 Sugerowana Kolejna Lekcja {nextLesson.reason}
); } ================================================ FILE: src/components/tutor/ExamRunner.tsx ================================================ import React, { useState, useEffect, useRef } from 'react'; import { Quiz, Question } from '@/lib/ai/schemas'; import { QuestionCard } from './QuestionCard'; import { ExamSidebar } from './ExamSidebar'; import { useUserStore } from '@/lib/store/useUserStore'; import { toast } from 'sonner'; import { getTodaysDailyChallenge } from '@/lib/challenge/daily'; interface ExamRunnerProps { quiz: Quiz; onRestart: () => void; topicId?: string; isDailyChallenge?: boolean; } export function ExamRunner({ quiz, onRestart, topicId, isDailyChallenge }: ExamRunnerProps) { const user = useUserStore(); const [userAnswers, setUserAnswers] = useState>({}); const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); const questionRefs = useRef<(HTMLDivElement | null)[]>([]); // Initialize refs array useEffect(() => { questionRefs.current = questionRefs.current.slice(0, quiz.questions.length); }, [quiz.questions.length]); const handleAnswer = (questionId: string, answer: string) => { setUserAnswers(prev => ({ ...prev, [questionId]: answer })); }; const scrollToQuestion = (index: number) => { const el = questionRefs.current[index]; if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); setActiveQuestionIndex(index); } }; // Intersection Observer to update active index on scroll useEffect(() => { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const index = Number(entry.target.getAttribute('data-index')); if (!isNaN(index)) { setActiveQuestionIndex(index); } } }); }, { threshold: 0.5, rootMargin: '-20% 0px -20% 0px' }); questionRefs.current.forEach(el => { if (el) observer.observe(el); }); return () => observer.disconnect(); }, [quiz.questions]); const finishExam = () => { // Validation: Check if all questions answered? Or just warn? const answeredCount = Object.keys(userAnswers).length; if (answeredCount < quiz.questions.length) { if (!confirm(`Odpowiedziałeś na ${answeredCount} z ${quiz.questions.length} pytań. Czy na pewno chcesz zakończyć?`)) { return; } } let calculatedScore = 0; quiz.questions.forEach((q, idx) => { const uAns = userAnswers[q.id || `q-${idx}`]; if (uAns && uAns.trim().toLowerCase() === q.correctAnswer.trim().toLowerCase()) { calculatedScore++; } }); // Save results user.saveQuizResult({ id: crypto.randomUUID(), date: new Date().toLocaleDateString('pl-PL'), topic: quiz.topic, total: quiz.questions.length, score: calculatedScore, questions: quiz.questions, userAnswers: userAnswers }); if (topicId || quiz.topic) { user.updateTopicStats(topicId || quiz.topic, calculatedScore, quiz.questions.length); } if (isDailyChallenge) { const challenge = getTodaysDailyChallenge(); user.completeDailyChallenge(challenge, calculatedScore, quiz.questions.length); toast.success('Daily Challenge ukończony!'); } // Show Score Dialog or Redirect? // For now, simpler to reuse pure state or callback props? // Wait, QuizRunner handles "isFinished" state internally to show review. // We should probably do the same here OR unify. // Let's implement a simple "Exam Summary" view here or reuse render? // Since we are decoupling, let's just use a simple summary state here. setIsFinished(true); setFinalScore(calculatedScore); }; const [isFinished, setIsFinished] = useState(false); const [finalScore, setFinalScore] = useState(0); if (isFinished) { return (
🎉

Egzamin Zakończony!

Twój wynik: {finalScore} / {quiz.questions.length}
); } return (
{/* Main Question List */}
{/* Header Info */}
Data
{new Date().toLocaleDateString('pl-PL')}
Egzamin Gramatyczny

{quiz.topic}

Proszę przeczytać uważnie polecenia i odpowiedzieć na wszystkie pytania. Powodzenia!

{/* Questions */}
{quiz.questions.map((q, idx) => (
{ questionRefs.current[idx] = el }} data-index={idx} id={`question-${idx}`} className={`transition-opacity duration-500 ${Math.abs(activeQuestionIndex - idx) > 1 ? 'opacity-50 hover:opacity-100' : 'opacity-100'}`} >
{(idx + 1).toString().padStart(2, '0')}.
handleAnswer(q.id || `q-${idx}`, ans)} isLast={false} // Hide "Next" button logic onNext={() => { }} mode="exam" initialValue={userAnswers[q.id || `q-${idx}`]} variant="minimal" />
))}
{/* Sidebar */} q.id || `q-${i}`)} tymeLeftSeconds={1500} />
); } ================================================ FILE: src/components/tutor/ExamSidebar.tsx ================================================ import React, { useState, useEffect } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Clock, CheckCircle2, AlertCircle } from 'lucide-react'; interface ExamSidebarProps { totalQuestions: number; completedCount: number; tymeLeftSeconds: number; // passed from parent to sync or managed here? // Managing here is easier for UI tick, but parent needs to know when time is up. // Let's accept seconds and onTick? Or just initialSeconds. // Simpler: Parent handles generic timer logic, but visual countdown here? // Let's pass `timeLeft` string or seconds from parent for robustness, or manage here. // For now, let's manage visual tick here based on a prop. onFinish: () => void; currentQuestionIndex: number; setCurrentQuestionIndex: (index: number) => void; answersState: Record; // questionId -> answer questionIds: string[]; } export function ExamSidebar({ totalQuestions, completedCount, onFinish, currentQuestionIndex, setCurrentQuestionIndex, answersState, questionIds }: ExamSidebarProps) { // Simple timer mock for now - in real app parent should probably drive this // to auto-submit when time is up. // We'll just display a static mock or simple countdown. const [timeLeft, setTimeLeft] = useState(1500); // 25 mins useEffect(() => { const timer = setInterval(() => { setTimeLeft(p => (p > 0 ? p - 1 : 0)); }, 1000); return () => clearInterval(timer); }, []); const formatTime = (seconds: number) => { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${s.toString().padStart(2, '0')}`; }; return (
Czas pozostały
{formatTime(timeLeft)}
Pytania {completedCount} z {totalQuestions} ukończone
{Array.from({ length: totalQuestions }).map((_, idx) => { const qId = questionIds[idx]; const isAnswered = !!answersState[qId]; const isCurrent = currentQuestionIndex === idx; return ( ); })}
Obecne
Ukończone
Do zrobienia
); } ================================================ FILE: src/components/tutor/FeedbackDisplay.tsx ================================================ import React from 'react'; import { WritingAnalysis } from '@/lib/ai/schemas'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { AlertCircle, CheckCircle, Info } from 'lucide-react'; interface FeedbackDisplayProps { analysis: WritingAnalysis; } export function FeedbackDisplay({ analysis }: FeedbackDisplayProps) { return (
Overall Feedback = 8 ? 'text-green-600' : analysis.score >= 5 ? 'text-yellow-600' : 'text-red-600' }`}> {analysis.score}/10

{analysis.overallFeedback}

Corrections

{analysis.corrections.length === 0 ? (

No errors found! Ottimo lavoro!

) : ( analysis.corrections.map((item, idx) => (
{item.original} {item.correction}

{item.explanation}

{item.type}
)) )}
); } ================================================ FILE: src/components/tutor/QuestionCard.tsx ================================================ import React from 'react'; import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Question } from '@/lib/ai/schemas'; import { useQuestionState } from './question/hooks/useQuestionState'; import { QuestionRenderer } from './question/QuestionRenderer'; import { Lightbulb, Volume2 } from 'lucide-react'; import { speak } from '@/lib/utils'; interface QuestionCardProps { question: Question; onAnswer: (isCorrect: boolean, answer: string) => void; isLast: boolean; onNext: () => void; readOnlyResult?: { answer: string; isCorrect: boolean }; mode?: 'practice' | 'exam'; initialValue?: string; variant?: 'default' | 'minimal'; } export function QuestionCard({ question, onAnswer, isLast, onNext, readOnlyResult, mode = 'practice', initialValue, variant = 'default' }: QuestionCardProps) { const q = useQuestionState(readOnlyResult?.answer ?? initialValue); const [showHint, setShowHint] = React.useState(false); React.useEffect(() => { if (readOnlyResult) { q.setValue(readOnlyResult.answer); } }, [readOnlyResult, question.id]); const correct = q.value?.trim().toLowerCase() === question.correctAnswer.trim().toLowerCase(); const handleSubmit = () => { q.submit(); onAnswer(correct, q.value!); }; const handleExamNext = () => { onAnswer(false, q.value || ''); onNext(); }; if (variant === 'minimal') { return (
{question.instruction}
{question.type !== 'dictation' && (

{question.question}

)}
{ q.setValue(val); // Auto-save answer in exam mode (minimal variant usually implies exam list) if (mode === 'exam') { onAnswer(false, val); } }} disabled={!!readOnlyResult} />
); } return (
{question.instruction}
{question.type !== 'dictation' && (

{question.question}

)}
{mode === 'practice' && ( )}
{showHint && mode === 'practice' && (
💡 {question.explanation.split('.')[0]}...
)} {(q.submitted || readOnlyResult) && mode === 'practice' && (
{(readOnlyResult ? readOnlyResult.isCorrect : correct) ? 'Świetnie!' : 'Niestety, to nie jest poprawna odpowiedź.'}
{question.explanation}
)}
{mode === 'exam' ? ( ) : ( !q.submitted && !readOnlyResult ? ( ) : ( ) )}
); } ================================================ FILE: src/components/tutor/QuizOptionsDialog.tsx ================================================ 'use client'; import React, { useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { ShieldCheck, Layers, Gauge, BookOpen, MessageCircle, Scale } from 'lucide-react'; import { ConfigStore } from '@/lib/store/config'; import { TestConfiguration } from '@/lib/ai/schemas'; import { useUserStore, suggestDifficulty } from '@/lib/store/useUserStore'; interface QuizOptionsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onConfirm: (options: { difficulty: string; deepCheck: boolean; amount: number; mode: 'practice' | 'exam'; config?: TestConfiguration }) => void; topicName: string; topicId: string; // Needed for Config } export function QuizOptionsDialog({ open, onOpenChange, onConfirm, topicName, topicId }: QuizOptionsDialogProps) { const [difficulty, setDifficulty] = useState('A2'); const [deepCheck, setDeepCheck] = useState(false); const [amount, setAmount] = useState(5); const [mode, setMode] = useState<'practice' | 'exam'>('practice'); // New Didactic Params const [tone, setTone] = useState<'formal' | 'informal'>('formal'); const [focus, setFocus] = useState<'grammar' | 'vocabulary' | 'pragmatics' | 'general'>('general'); const [sourceMode, setSourceMode] = useState<'syllabus-only' | 'ai-expanded'>('ai-expanded'); // Load defaults from config when opening // Load defaults from config when opening React.useEffect(() => { if (open) { const config = ConfigStore.get(); setDeepCheck(config.generationSettings.deepCheck); // Adaptive suggestion const user = useUserStore.getState(); const stats = user.topicStats[topicId]; if (stats) { const suggested = suggestDifficulty(stats.mastery); setDifficulty(suggested); } } }, [open, topicId]); const handleConfirm = () => { const config: TestConfiguration = { level: difficulty as any, topicId: topicId, focus: focus, tone: tone, exerciseTypes: ['multiple-choice', 'gap-fill'], // Default mix sourceMode: sourceMode }; onConfirm({ difficulty, deepCheck, amount, mode, config }); onOpenChange(false); }; return ( Konfiguracja Quizu Dostosuj parametry dydaktyczne dla: {topicName}
{/* Difficulty */}
{['A1', 'A2', 'B1', 'B2'].map((level) => (
setDifficulty(level)} className={`cursor-pointer rounded-md border p-2 text-center text-sm font-medium transition-all ${difficulty === level ? 'bg-blue-600 text-white border-blue-600 shadow-sm' : 'bg-white hover:bg-slate-50 border-slate-200 text-slate-700' }`} > {level}
))}
{/* Focus & Tone (New Grid) */}
{/* Tone */}
{/* Source Mode */}
{/* Focus Selection */}
{[ { id: 'grammar', label: 'Gramatyka' }, { id: 'vocabulary', label: 'Słownictwo' }, { id: 'pragmatics', label: 'Komunikacja' } ].map((f) => (
setFocus(f.id as any)} className={`cursor-pointer rounded-md border p-2 text-center text-xs font-medium transition-all ${focus === f.id ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-white hover:bg-slate-50 border-slate-200 text-slate-700' }`} > {f.label}
))}
{/* Mode Selection */}
setMode('practice')} className={`cursor-pointer rounded-md border p-2 text-center transition-all ${mode === 'practice' ? 'bg-blue-50 border-blue-500 text-blue-700 ring-1 ring-blue-500' : 'bg-white hover:bg-slate-50 border-slate-200 text-slate-600' }`} >
Trening 🏋️
setMode('exam')} className={`cursor-pointer rounded-md border p-2 text-center transition-all ${mode === 'exam' ? 'bg-amber-50 border-amber-500 text-amber-700 ring-1 ring-amber-500' : 'bg-white hover:bg-slate-50 border-slate-200 text-slate-600' }`} >
Egzamin 🎓
{/* Agentic Check */}
setDeepCheck(e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600" />

Wymusza podwójną weryfikację poprawności merytorycznej przez AI.

); } ================================================ FILE: src/components/tutor/QuizReview.tsx ================================================ import React, { useState } from 'react'; import { Quiz, Question } from '@/lib/ai/schemas'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { CheckCircle2, Bot, AlertCircle, RotateCcw } from 'lucide-react'; import { explainAnswerAction } from '@/lib/ai/explain'; export interface QuizReviewProps { quiz: Quiz; userAnswers: Record; score: number; onRestart: () => void; onRetry?: () => void; // Restarts same quiz } export function QuizReview({ quiz, userAnswers, score, onRestart, onRetry }: QuizReviewProps) { const [explanations, setExplanations] = useState>({}); const [loadingExplanation, setLoadingExplanation] = useState(null); const requestExplanation = async (q: Question, uAns: string) => { setLoadingExplanation(q.id!); const explanation = await explainAnswerAction(q, uAns || "Brak odpowiedzi", quiz.topic); setExplanations(prev => ({ ...prev, [q.id!]: explanation })); setLoadingExplanation(null); }; return (
Raport Końcowy 📊
{Math.round((score / quiz.questions.length) * 100)}%
Wynik

Zdobyłeś {score} z {quiz.questions.length} punktów.

Szczegółowa Analiza

{quiz.questions.map((q, idx) => { const uAns = userAnswers[q.id || `q-${idx}`]; const isCorrect = uAns?.trim().toLowerCase() === q.correctAnswer.trim().toLowerCase(); const qId = q.id || `q-${idx}`; return (
{idx + 1}. {q.question}
{isCorrect ? : }
Twoja odpowiedź: {uAns || "(Brak)"}
Poprawna odpowiedź: {q.correctAnswer}
{/* Explanation Area */}
💡 {q.explanation}
{/* AI Explain Button */} {!isCorrect && (
{explanations[qId] ? (
{explanations[qId]}
) : ( )}
)}
); })}
{onRetry && ( )}
); } ================================================ FILE: src/components/tutor/QuizReviewDialog.tsx ================================================ import React from 'react'; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { QuizResult } from '@/lib/store/useUserStore'; import { QuizReview } from './QuizReview'; interface QuizReviewDialogProps { result: QuizResult | null; open: boolean; onOpenChange: (open: boolean) => void; onRetry?: () => void; } export function QuizReviewDialog({ result, open, onOpenChange, onRetry }: QuizReviewDialogProps) { if (!result) return null; // Reconstruct quiz from result // TODO: Store difficulty in QuizResult to avoid hardcoding const quiz = { topic: result.topic, difficulty: (result as any).difficulty || 'A2' as const, // Use actual difficulty if stored, or default to A2 questions: result.questions }; return ( onOpenChange(false)} onRetry={onRetry} /> ); } ================================================ FILE: src/components/tutor/QuizRunner.tsx ================================================ import React, { useState, useMemo } from 'react'; import { Quiz, Question } from '@/lib/ai/schemas'; import { QuestionCard } from './QuestionCard'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { RotateCcw, ChevronLeft, ChevronRight, CheckCircle2, Bot, AlertCircle, Lightbulb } from 'lucide-react'; import { useUserStore } from '@/lib/store/useUserStore'; import { explainAnswerAction } from '@/lib/ai/explain'; import { toast } from 'sonner'; import { QuizReview } from './QuizReview'; import { getTodaysDailyChallenge } from '@/lib/challenge/daily'; import { orderQuestionsByDifficulty, calculatePartialPoints } from '@/lib/didactic/utils'; import { ExamRunner } from './ExamRunner'; interface QuizRunnerProps { quiz: Quiz; onRestart: () => void; mode?: 'practice' | 'exam'; topicId?: string; isDailyChallenge?: boolean; } export function QuizRunner({ quiz, onRestart, mode = 'practice', topicId, isDailyChallenge }: QuizRunnerProps) { const user = useUserStore(); if (mode === 'exam') { return ( ); } // Order quiz questions from easy to hard (scaffolding) const orderedQuestions = useMemo(() => orderQuestionsByDifficulty(quiz.questions), [quiz.questions]); const [currentIndex, setCurrentIndex] = useState(0); const [score, setScore] = useState(0); const [isFinished, setIsFinished] = useState(false); // historySaved removed - dead code (never actually used) const [userAnswers, setUserAnswers] = useState>({}); // AI Explanations cache: { questionId: explanation } const [explanations, setExplanations] = useState>({}); const [loadingExplanation, setLoadingExplanation] = useState(null); const question = orderedQuestions[currentIndex]; const progress = ((currentIndex + 1) / orderedQuestions.length) * 100; // Handler for Practice and Exam const handleAnswer = (isCorrect: boolean, answer: string) => { // Update local state first const newAnswers = { ...userAnswers, [question.id || `q-${currentIndex}`]: answer }; setUserAnswers(newAnswers); if (mode === 'practice') { if (isCorrect) { setScore(s => s + 1); } // Award points with partial credit for close answers const points = calculatePartialPoints(isCorrect, answer, question.correctAnswer); user.addPoints(points); if (points === 5) { toast.info('⚠️ Byłeś blisko! +5 punktów'); } // Navigation handled by "Next" button in QuestionCard in practice mode } }; const finishExam = () => { // Calculate score let calculatedScore = 0; quiz.questions.forEach((q, idx) => { const uAns = userAnswers[q.id || `q-${idx}`]; if (uAns && uAns.trim().toLowerCase() === q.correctAnswer.trim().toLowerCase()) { calculatedScore++; } }); setScore(calculatedScore); // Bonus for perfect score if (calculatedScore === quiz.questions.length) user.addPoints(50); user.addPoints(calculatedScore * 10); // Base points // Directly Save history HERE to avoid stale state race condition user.saveQuizResult({ id: crypto.randomUUID(), // Use UUID to avoid collisions and SSR mismatch date: new Date().toLocaleDateString('pl-PL'), topic: quiz.topic, total: quiz.questions.length, score: calculatedScore, // Use local variable! questions: quiz.questions, userAnswers: userAnswers }); // Update topic stats if topicId is available if (topicId || quiz.topic) { user.updateTopicStats(topicId || quiz.topic, calculatedScore, quiz.questions.length); } // Complete daily challenge if applicable if (isDailyChallenge) { const challenge = getTodaysDailyChallenge(); user.completeDailyChallenge(challenge, calculatedScore, quiz.questions.length); // Show streak milestone celebration const newStreak = user.dailyProgress.currentStreak + 1; if (newStreak === 7) { toast.success('🔥 7-dniowy streak! +100 bonus punktów!', { duration: 5000 }); } else if (newStreak === 30) { toast.success('🌟 30-dniowy streak! +500 bonus punktów!', { duration: 5000 }); } else if (newStreak === 100) { toast.success('💎 100-dniowy streak! +2000 bonus punktów!', { duration: 8000 }); } else { toast.success(`Daily Challenge ukończony! +50 bonus punktów`, { duration: 3000 }); } } // History already saved above - setHistorySaved removed (dead code) setIsFinished(true); }; const handleNext = () => { if (currentIndex < quiz.questions.length - 1) { setCurrentIndex(currentIndex + 1); } else { setIsFinished(true); } }; const handlePrev = () => { if (currentIndex > 0) { setCurrentIndex(currentIndex - 1); } }; const requestExplanation = async (q: Question, uAns: string) => { setLoadingExplanation(q.id!); const explanation = await explainAnswerAction(q, uAns || "Brak odpowiedzi", quiz.topic); setExplanations(prev => ({ ...prev, [q.id!]: explanation })); setLoadingExplanation(null); }; if (isFinished) { return ( { setIsFinished(false); setCurrentIndex(0); setScore(0); setUserAnswers({}); }} /> ); } return (
Pytanie {currentIndex + 1} z {quiz.questions.length} Wynik: {score}
); } ================================================ FILE: src/components/tutor/SpeakingDrill.tsx ================================================ 'use client'; import React, { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Mic, MicOff, AlertCircle } from 'lucide-react'; interface SpeakingDrillProps { onTranscript: (text: string) => void; } export function SpeakingDrill({ onTranscript }: SpeakingDrillProps) { const [isListening, setIsListening] = useState(false); const [transcript, setTranscript] = useState(''); const [error, setError] = useState(null); const recognitionRef = useRef(null); useEffect(() => { if (typeof window !== 'undefined' && 'webkitSpeechRecognition' in window) { // @ts-ignore const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.lang = 'it-IT'; // Italian by default recognition.continuous = false; recognition.interimResults = true; recognition.onstart = () => setIsListening(true); recognition.onend = () => setIsListening(false); recognition.onresult = (event: any) => { const current = event.resultIndex; const transcriptText = event.results[current][0].transcript; setTranscript(transcriptText); if (event.results[current].isFinal) { onTranscript(transcriptText); } }; recognition.onerror = (event: any) => { setError('Błąd rozpoznawania: ' + event.error); setIsListening(false); }; recognitionRef.current = recognition; } else { setError('Twoja przeglądarka nie obsługuje rozpoznawania mowy.'); } }, [onTranscript]); const toggleMic = () => { if (!recognitionRef.current) return; if (isListening) { recognitionRef.current.stop(); } else { setError(null); setTranscript(''); recognitionRef.current.start(); } }; if (error && error.includes('nie obsługuje')) { return (
{error}
); } return (
{isListening && }

Mówienie (Beta)

{isListening ? 'Słucham... powiedz coś po włosku!' : 'Kliknij mikrofon i mów.'}

{transcript && (
"{transcript}"
)} {error &&

{error}

}
); } ================================================ FILE: src/components/tutor/TheoryUploader.tsx ================================================ import React, { useState, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Upload, FileText, CheckCircle } from 'lucide-react'; interface TheoryUploaderProps { onUpload: (content: string) => void; } export function TheoryUploader({ onUpload }: TheoryUploaderProps) { const [text, setText] = useState(''); const fileInputRef = useRef(null); const handleTextChange = (e: React.ChangeEvent) => { setText(e.target.value); onUpload(e.target.value); }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; setText(content); onUpload(content); }; reader.readAsText(file); }; return ( Własne Materiały Wgraj notatki, aby stworzyć spersonalizowany quiz.
fileInputRef.current?.click()} >

Kliknij, aby wgrać plik

Obsługiwane formaty: .txt, .md

lub wklej tekst