From a7caa223862781f0ec2ea30699d07ff6738e9c5a Mon Sep 17 00:00:00 2001 From: Ismail Ali Date: Tue, 28 Apr 2026 15:06:57 +0200 Subject: [PATCH] Build English AI Coach starter app --- .gitignore | 1 + App.tsx | 21 +-- README.md | 142 ++++++++++++++ app.json | 4 +- package-lock.json | 309 ++++++++++++++++++++++++++++++- package.json | 9 +- src/components/ChatBubble.tsx | 62 +++++++ src/components/PrimaryButton.tsx | 51 +++++ src/components/ProgressCard.tsx | 59 ++++++ src/hooks/useDailyReminder.ts | 8 + src/navigation/AppNavigator.tsx | 29 +++ src/navigation/types.ts | 7 + src/screens/ChatScreen.tsx | 100 ++++++++++ src/screens/HomeScreen.tsx | 98 ++++++++++ src/screens/LevelScreen.tsx | 99 ++++++++++ src/services/mockAiCoach.ts | 18 ++ src/theme/colors.ts | 13 ++ src/utils/levels.ts | 8 + 18 files changed, 1020 insertions(+), 18 deletions(-) create mode 100644 README.md create mode 100644 src/components/ChatBubble.tsx create mode 100644 src/components/PrimaryButton.tsx create mode 100644 src/components/ProgressCard.tsx create mode 100644 src/hooks/useDailyReminder.ts create mode 100644 src/navigation/AppNavigator.tsx create mode 100644 src/navigation/types.ts create mode 100644 src/screens/ChatScreen.tsx create mode 100644 src/screens/HomeScreen.tsx create mode 100644 src/screens/LevelScreen.tsx create mode 100644 src/services/mockAiCoach.ts create mode 100644 src/theme/colors.ts create mode 100644 src/utils/levels.ts diff --git a/.gitignore b/.gitignore index d914c32..da9569c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # typescript diff --git a/App.tsx b/App.tsx index 0329d0c..dc4911e 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,13 @@ import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; + +import { AppNavigator } from './src/navigation/AppNavigator'; export default function App() { return ( - - Open up App.tsx to start working on your app! - - + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..5276ad5 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# English AI Coach + +English AI Coach ist ein Starterprojekt fuer eine mobile App zum Englischlernen mit KI. Die App hilft Nutzern, kurze englische Saetze zu schreiben, Fehler zu erkennen und Korrekturen mit deutscher Erklaerung zu erhalten. + +Das Projekt ist bewusst schlank gehalten: React Native, Expo, TypeScript und React Navigation. Es nutzt keine unnoetigen Expo-only Libraries und ist so vorbereitet, dass spaeter lokale KI ueber Ollama, Speech-to-Text, Text-to-Speech und Push Notifications ergaenzt werden koennen. + +## Features MVP + +- HomeScreen mit Fortschrittskarte und Mock Daily Reminder +- Level-Auswahl fuer A1, A2, B1 und B2 +- ChatScreen mit einfacher User- und AI-Nachrichtenansicht +- Mock KI-Service fuer Beispielkorrekturen +- TypeScript-typisierte Navigation +- Struktur fuer spaetere Services, Hooks und Utilities +- Web/PWA-Ausbau vorbereitet ueber Expo Web + +## Beispiel + +```text +User: I go yesterday to work + +AI Coach: +Good try. Correct: "I went to work yesterday." +"Yesterday" braucht Past Tense. +``` + +## Tech Stack + +- React Native +- Expo +- TypeScript +- React Navigation +- Geplant: Ollama API fuer lokale KI +- Geplant: Speech-to-Text und Text-to-Speech +- Geplant: Push Notifications fuer Daily Reminder + +## Projektstruktur + +```text +english-ai-coach/ +├── App.tsx +├── app.json +├── package.json +├── src/ +│ ├── components/ +│ │ ├── ChatBubble.tsx +│ │ ├── PrimaryButton.tsx +│ │ └── ProgressCard.tsx +│ ├── hooks/ +│ │ └── useDailyReminder.ts +│ ├── navigation/ +│ │ ├── AppNavigator.tsx +│ │ └── types.ts +│ ├── screens/ +│ │ ├── ChatScreen.tsx +│ │ ├── HomeScreen.tsx +│ │ └── LevelScreen.tsx +│ ├── services/ +│ │ └── mockAiCoach.ts +│ ├── theme/ +│ │ └── colors.ts +│ └── utils/ +│ └── levels.ts +└── README.md +``` + +## Installation + +Voraussetzungen: + +- Node.js +- npm +- Expo ueber `npx expo` + +Abhaengigkeiten installieren: + +```bash +npm install +``` + +App starten: + +```bash +npm run start +``` + +iOS Preview starten: + +```bash +npm run ios +``` + +Web Preview starten: + +```bash +npm run web +``` + +Hinweis: Fuer lokale iOS-Simulator-Builds wird macOS mit Xcode benoetigt. Mit Expo Go kann die App auf einem echten Geraet getestet werden. + +## Ollama Integration Geplant + +Der aktuelle Chat nutzt `src/services/mockAiCoach.ts`. Dieser Service ist die vorgesehene Stelle, um spaeter einen echten KI-Client zu integrieren. + +Moeglicher lokaler Ablauf: + +```text +React Native Chat UI +-> AI Service +-> lokale Ollama API +-> Modell wie llama, qwen oder mistral +-> Antwort mit Korrektur und Erklaerung +``` + +Fuer eine PWA oder lokale Web-Version kann die App spaeter im Browser laufen und mit einem lokalen Ollama-Server im Netzwerk verbunden werden. + +## PWA Idee + +Die Web-Version kann spaeter als PWA erweitert werden: + +- Auf Home-Bildschirm installieren +- Lokale Ollama-Verbindung konfigurieren +- Daily Reminder ueber Web Push pruefen +- Desktop- und Tablet-Layout optimieren + +## Roadmap + +- Chat UI erweitern +- Mock KI durch echte KI ersetzen +- Ollama API Client implementieren +- Level-spezifische Prompts erstellen +- Fehleranalyse und Vokabeltraining ergaenzen +- Fortschritt lokal speichern +- Speech-to-Text hinzufuegen +- Text-to-Speech fuer Aussprachetraining hinzufuegen +- Push Notifications fuer taegliche Uebungen einbauen +- PWA-Ausbau pruefen +- App Store Release vorbereiten + +## Ziel + +Ziel ist ein skalierbares Starterprojekt fuer eine KI-basierte Englischlern-App, die zuerst als mobile Expo-App funktioniert und spaeter lokal mit Ollama oder ueber eine externe KI-API erweitert werden kann. diff --git a/app.json b/app.json index 04a1e4c..86ccb24 100644 --- a/app.json +++ b/app.json @@ -2,6 +2,7 @@ "expo": { "name": "english-ai-coach", "slug": "english-ai-coach", + "description": "Eine mobile App zum Englischlernen mit KI, Level-Auswahl und vorbereitetem Ollama-Ausbau.", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -13,7 +14,8 @@ "backgroundColor": "#ffffff" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.englishaicoach.app" }, "android": { "adaptiveIcon": { diff --git a/package-lock.json b/package-lock.json index 358dbf6..d465b2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,14 @@ "name": "english-ai-coach", "version": "1.0.0", "dependencies": { + "@react-navigation/native": "^7.2.2", + "@react-navigation/native-stack": "^7.14.12", "expo": "~54.0.33", "expo-status-bar": "~3.0.9", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" }, "devDependencies": { "@types/react": "~19.1.0", @@ -2964,6 +2968,123 @@ "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, + "node_modules/@react-navigation/core": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.2.tgz", + "integrity": "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.3", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.15.tgz", + "integrity": "sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.2.2", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz", + "integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.17.2", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.14.12", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.12.tgz", + "integrity": "sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.15", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.2.2", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/native/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", + "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -3915,6 +4036,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3930,6 +4064,34 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -4086,6 +4248,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4907,6 +5078,12 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4934,6 +5111,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -5279,6 +5465,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7226,6 +7418,24 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -7278,6 +7488,18 @@ "ws": "^7" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7351,6 +7573,31 @@ "react-native": "*" } }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", + "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", + "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", @@ -7827,6 +8074,15 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7877,6 +8133,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -7938,6 +8203,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8001,6 +8275,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8490,6 +8773,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8541,6 +8842,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index d09e684..8c3e1b6 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,18 @@ "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", - "web": "expo start --web" + "web": "expo start --web", + "prebuild:ios": "expo prebuild --platform ios" }, "dependencies": { + "@react-navigation/native": "^7.2.2", + "@react-navigation/native-stack": "^7.14.12", "expo": "~54.0.33", "expo-status-bar": "~3.0.9", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/src/components/ChatBubble.tsx b/src/components/ChatBubble.tsx new file mode 100644 index 0000000..707cd9f --- /dev/null +++ b/src/components/ChatBubble.tsx @@ -0,0 +1,62 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { colors } from '../theme/colors'; + +export type ChatMessage = { + id: string; + role: 'user' | 'assistant'; + text: string; +}; + +type ChatBubbleProps = { + message: ChatMessage; +}; + +export function ChatBubble({ message }: ChatBubbleProps) { + const isUser = message.role === 'user'; + + return ( + + + {isUser ? 'Du' : 'AI Coach'} + {message.text} + + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + marginVertical: 6, + }, + userWrapper: { + alignItems: 'flex-end', + }, + aiWrapper: { + alignItems: 'flex-start', + }, + bubble: { + borderRadius: 20, + maxWidth: '84%', + padding: 14, + }, + userBubble: { + backgroundColor: colors.userBubble, + }, + aiBubble: { + backgroundColor: colors.aiBubble, + borderColor: colors.border, + borderWidth: 1, + }, + sender: { + color: colors.primary, + fontSize: 12, + fontWeight: '900', + marginBottom: 6, + }, + text: { + color: colors.textPrimary, + fontSize: 15, + lineHeight: 22, + }, +}); diff --git a/src/components/PrimaryButton.tsx b/src/components/PrimaryButton.tsx new file mode 100644 index 0000000..84f7baa --- /dev/null +++ b/src/components/PrimaryButton.tsx @@ -0,0 +1,51 @@ +import { Pressable, StyleSheet, Text } from 'react-native'; + +import { colors } from '../theme/colors'; + +type PrimaryButtonProps = { + label: string; + onPress: () => void; + variant?: 'primary' | 'secondary'; +}; + +export function PrimaryButton({ label, onPress, variant = 'primary' }: PrimaryButtonProps) { + return ( + [ + styles.button, + variant === 'secondary' && styles.secondary, + pressed && styles.pressed, + ]} + > + {label} + + ); +} + +const styles = StyleSheet.create({ + button: { + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 18, + paddingHorizontal: 22, + paddingVertical: 16, + }, + secondary: { + backgroundColor: colors.surfaceRaised, + borderColor: colors.border, + borderWidth: 1, + }, + pressed: { + opacity: 0.82, + }, + label: { + color: colors.background, + fontSize: 16, + fontWeight: '900', + }, + secondaryLabel: { + color: colors.textPrimary, + }, +}); diff --git a/src/components/ProgressCard.tsx b/src/components/ProgressCard.tsx new file mode 100644 index 0000000..31ad14c --- /dev/null +++ b/src/components/ProgressCard.tsx @@ -0,0 +1,59 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { colors } from '../theme/colors'; + +export function ProgressCard() { + return ( + + + Heute gelernt + 12 min + + + + + Daily Reminder: 18:30 Uhr, 10 Minuten Englisch ueben. + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 24, + borderWidth: 1, + padding: 18, + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + label: { + color: colors.textSecondary, + fontSize: 14, + }, + value: { + color: colors.textPrimary, + fontSize: 20, + fontWeight: '900', + }, + track: { + backgroundColor: colors.surfaceRaised, + borderRadius: 999, + height: 10, + marginTop: 14, + overflow: 'hidden', + }, + fill: { + backgroundColor: colors.primary, + height: '100%', + width: '60%', + }, + hint: { + color: colors.textSecondary, + fontSize: 13, + lineHeight: 19, + marginTop: 14, + }, +}); diff --git a/src/hooks/useDailyReminder.ts b/src/hooks/useDailyReminder.ts new file mode 100644 index 0000000..e936d19 --- /dev/null +++ b/src/hooks/useDailyReminder.ts @@ -0,0 +1,8 @@ +export function useDailyReminder() { + // Mock hook until native push notifications are added. + return { + enabled: true, + time: '18:30', + label: 'Taegliche Uebung ist vorbereitet', + }; +} diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..0b8660c --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,29 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import { ChatScreen } from '../screens/ChatScreen'; +import { HomeScreen } from '../screens/HomeScreen'; +import { LevelScreen } from '../screens/LevelScreen'; +import { colors } from '../theme/colors'; +import type { RootStackParamList } from './types'; + +const Stack = createNativeStackNavigator(); + +export function AppNavigator() { + return ( + + + + + + + + ); +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 0000000..180ef2c --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,7 @@ +export type EnglishLevel = 'A1' | 'A2' | 'B1' | 'B2'; + +export type RootStackParamList = { + Home: undefined; + Level: undefined; + Chat: { level: EnglishLevel }; +}; diff --git a/src/screens/ChatScreen.tsx b/src/screens/ChatScreen.tsx new file mode 100644 index 0000000..0584ad4 --- /dev/null +++ b/src/screens/ChatScreen.tsx @@ -0,0 +1,100 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { useState } from 'react'; +import { FlatList, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { ChatBubble, type ChatMessage } from '../components/ChatBubble'; +import { PrimaryButton } from '../components/PrimaryButton'; +import type { RootStackParamList } from '../navigation/types'; +import { getMockAiReply } from '../services/mockAiCoach'; +import { colors } from '../theme/colors'; + +type ChatScreenProps = NativeStackScreenProps; + +export function ChatScreen({ route }: ChatScreenProps) { + const { level } = route.params; + const [input, setInput] = useState('I go yesterday to work'); + const [messages, setMessages] = useState([ + { + id: 'welcome', + role: 'assistant', + text: `Willkommen! Dein Level ist ${level}. Schreib einen englischen Satz und ich korrigiere ihn.`, + }, + ]); + const [isSending, setIsSending] = useState(false); + + async function sendMessage() { + if (isSending || !input.trim()) { + return; + } + + const userMessage: ChatMessage = { id: `user-${Date.now()}`, role: 'user', text: input.trim() }; + setMessages((current) => [...current, userMessage]); + setInput(''); + setIsSending(true); + + // This mock can later be replaced by Ollama or another AI API client. + const reply = await getMockAiReply(userMessage.text, level); + setMessages((current) => [...current, { id: `ai-${Date.now()}`, role: 'assistant', text: reply }]); + setIsSending(false); + } + + return ( + + + item.id} + renderItem={({ item }) => } + /> + + + + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + flex: 1, + }, + listContent: { + padding: 18, + }, + composer: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderTopWidth: 1, + gap: 12, + padding: 14, + }, + input: { + backgroundColor: colors.surfaceRaised, + borderColor: colors.border, + borderRadius: 18, + borderWidth: 1, + color: colors.textPrimary, + fontSize: 16, + minHeight: 70, + padding: 14, + textAlignVertical: 'top', + }, +}); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx new file mode 100644 index 0000000..e916d1a --- /dev/null +++ b/src/screens/HomeScreen.tsx @@ -0,0 +1,98 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { PrimaryButton } from '../components/PrimaryButton'; +import { ProgressCard } from '../components/ProgressCard'; +import { useDailyReminder } from '../hooks/useDailyReminder'; +import type { RootStackParamList } from '../navigation/types'; +import { colors } from '../theme/colors'; + +type HomeScreenProps = NativeStackScreenProps; + +export function HomeScreen({ navigation }: HomeScreenProps) { + const reminder = useDailyReminder(); + + return ( + + + + Personal English Tutor + Lerne Englisch mit einem KI Coach + + Starte mit deinem Level, schreibe kurze Saetze und erhalte Korrekturen mit deutscher Erklaerung. + + + + + + + Daily Reminder + {reminder.label} + {reminder.enabled ? reminder.time : 'Aus'} + + + navigation.navigate('Level')} /> + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + content: { + gap: 22, + padding: 24, + }, + hero: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 28, + borderWidth: 1, + padding: 24, + }, + eyebrow: { + color: colors.primary, + fontSize: 13, + fontWeight: '900', + letterSpacing: 0.5, + textTransform: 'uppercase', + }, + title: { + color: colors.textPrimary, + fontSize: 34, + fontWeight: '900', + lineHeight: 39, + marginTop: 12, + }, + subtitle: { + color: colors.textSecondary, + fontSize: 16, + lineHeight: 24, + marginTop: 14, + }, + reminderCard: { + backgroundColor: colors.surfaceRaised, + borderRadius: 24, + padding: 18, + }, + cardTitle: { + color: colors.textPrimary, + fontSize: 18, + fontWeight: '900', + }, + cardText: { + color: colors.textSecondary, + fontSize: 14, + lineHeight: 21, + marginTop: 8, + }, + cardMeta: { + color: colors.accent, + fontSize: 16, + fontWeight: '900', + marginTop: 10, + }, +}); diff --git a/src/screens/LevelScreen.tsx b/src/screens/LevelScreen.tsx new file mode 100644 index 0000000..2986e1a --- /dev/null +++ b/src/screens/LevelScreen.tsx @@ -0,0 +1,99 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import type { RootStackParamList } from '../navigation/types'; +import { colors } from '../theme/colors'; +import { levels } from '../utils/levels'; + +type LevelScreenProps = NativeStackScreenProps; + +export function LevelScreen({ navigation }: LevelScreenProps) { + return ( + + + Welches Englisch-Level passt zu dir? + Du kannst spaeter jederzeit wechseln. Das Level steuert die Mock-Antworten. + + {levels.map((item) => ( + navigation.navigate('Chat', { level: item.level })} + style={({ pressed }) => [styles.levelCard, pressed && styles.pressed]} + > + + {item.level} + + + {item.title} + {item.description} + + + ))} + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + content: { + gap: 14, + padding: 24, + }, + title: { + color: colors.textPrimary, + fontSize: 29, + fontWeight: '900', + lineHeight: 35, + }, + subtitle: { + color: colors.textSecondary, + fontSize: 15, + lineHeight: 23, + marginBottom: 8, + }, + levelCard: { + alignItems: 'center', + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 22, + borderWidth: 1, + flexDirection: 'row', + gap: 16, + padding: 18, + }, + pressed: { + opacity: 0.8, + }, + badge: { + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 18, + height: 54, + justifyContent: 'center', + width: 54, + }, + badgeText: { + color: colors.background, + fontSize: 18, + fontWeight: '900', + }, + levelTextBlock: { + flex: 1, + }, + levelTitle: { + color: colors.textPrimary, + fontSize: 18, + fontWeight: '900', + }, + levelDescription: { + color: colors.textSecondary, + fontSize: 14, + lineHeight: 20, + marginTop: 4, + }, +}); diff --git a/src/services/mockAiCoach.ts b/src/services/mockAiCoach.ts new file mode 100644 index 0000000..29f11a1 --- /dev/null +++ b/src/services/mockAiCoach.ts @@ -0,0 +1,18 @@ +import type { EnglishLevel } from '../navigation/types'; + +const examples: Record = { + A1: 'Fast richtig! Correct: "I am learning English." Warum? Bei "I" nutzt du "am".', + A2: 'Good try. Correct: "I went to work yesterday." "Yesterday" braucht Past Tense.', + B1: 'Nice sentence. Alternative: "I have been practicing every day." Das klingt natuerlicher.', + B2: 'Strong idea. Improve flow: use linking words like "however", "therefore" or "in addition".', +}; + +export async function getMockAiReply(input: string, level: EnglishLevel) { + await new Promise((resolve) => setTimeout(resolve, 350)); + + if (!input.trim()) { + return 'Schreib einen kurzen englischen Satz, dann korrigiere ich ihn fuer dein Level.'; + } + + return `${examples[level]}\n\nDein Satz: "${input.trim()}"`; +} diff --git a/src/theme/colors.ts b/src/theme/colors.ts new file mode 100644 index 0000000..719e82e --- /dev/null +++ b/src/theme/colors.ts @@ -0,0 +1,13 @@ +export const colors = { + background: '#0a1020', + surface: '#121a2e', + surfaceRaised: '#1b2740', + primary: '#78e6c7', + primaryDark: '#1fa987', + accent: '#ffd166', + textPrimary: '#f7fbff', + textSecondary: '#aab7cc', + border: '#2a3854', + userBubble: '#2667ff', + aiBubble: '#1e2a44', +}; diff --git a/src/utils/levels.ts b/src/utils/levels.ts new file mode 100644 index 0000000..6f58304 --- /dev/null +++ b/src/utils/levels.ts @@ -0,0 +1,8 @@ +import type { EnglishLevel } from '../navigation/types'; + +export const levels: Array<{ level: EnglishLevel; title: string; description: string }> = [ + { level: 'A1', title: 'Beginner', description: 'Einfache Saetze, Alltag und Grundlagen.' }, + { level: 'A2', title: 'Elementary', description: 'Vergangenheit, Fragen und kurze Dialoge.' }, + { level: 'B1', title: 'Intermediate', description: 'Freies Schreiben mit klaren Korrekturen.' }, + { level: 'B2', title: 'Upper Intermediate', description: 'Natuerlicher Ausdruck und bessere Struktur.' }, +];