Oviya commited on
Commit
e4ab6d0
·
0 Parent(s):

Deploy Angular to HF Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .editorconfig +16 -0
  2. .gitattributes +12 -0
  3. .gitignore +42 -0
  4. .vscode/extensions.json +4 -0
  5. .vscode/launch.json +19 -0
  6. .vscode/tasks.json +42 -0
  7. GrammAI.esproj +10 -0
  8. GrammAI.esproj.user +8 -0
  9. README.md +38 -0
  10. angular.json +110 -0
  11. canvas-confetti.d.ts +1 -0
  12. karma.conf.js +44 -0
  13. nuget.config +10 -0
  14. obj/Debug/GrammAI.esproj.CoreCompileInputs.cache +0 -0
  15. obj/Debug/GrammAI.esproj.FileListAbsolute.txt +6 -0
  16. package-lock.json +0 -0
  17. package.json +48 -0
  18. src/app/app-routing.module.ts +52 -0
  19. src/app/app.component.css +12 -0
  20. src/app/app.component.html +2 -0
  21. src/app/app.component.spec.ts +35 -0
  22. src/app/app.component.ts +20 -0
  23. src/app/app.module.ts +56 -0
  24. src/app/auth/auth.component.css +211 -0
  25. src/app/auth/auth.component.html +101 -0
  26. src/app/auth/auth.component.spec.ts +23 -0
  27. src/app/auth/auth.component.ts +55 -0
  28. src/app/auth/auth.guard.spec.ts +17 -0
  29. src/app/auth/auth.guard.ts +30 -0
  30. src/app/auth/auth.service.spec.ts +16 -0
  31. src/app/auth/auth.service.ts +188 -0
  32. src/app/auth/root-redirect.guard.spec.ts +17 -0
  33. src/app/auth/root-redirect.guard.ts +29 -0
  34. src/app/authentication/authentication.component.css +133 -0
  35. src/app/authentication/authentication.component.html +47 -0
  36. src/app/authentication/authentication.component.spec.ts +23 -0
  37. src/app/authentication/authentication.component.ts +47 -0
  38. src/app/authentication/authentication.guard.spec.ts +17 -0
  39. src/app/authentication/authentication.guard.ts +24 -0
  40. src/app/authentication/authentication.service.spec.ts +16 -0
  41. src/app/authentication/authentication.service.ts +203 -0
  42. src/app/chat/api.service.spec.ts +16 -0
  43. src/app/chat/api.service.ts +81 -0
  44. src/app/chat/chat.component.css +960 -0
  45. src/app/chat/chat.component.html +127 -0
  46. src/app/chat/chat.component.spec.ts +23 -0
  47. src/app/chat/chat.component.ts +667 -0
  48. src/app/findword/findword.component.css +1095 -0
  49. src/app/findword/findword.component.html +213 -0
  50. src/app/findword/findword.component.spec.ts +23 -0
.editorconfig ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editor configuration, see https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.ts]
12
+ quote_type = single
13
+
14
+ [*.md]
15
+ max_line_length = off
16
+ trim_trailing_whitespace = false
.gitattributes ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.gif filter=lfs diff=lfs merge=lfs -text
5
+ *.svg filter=lfs diff=lfs merge=lfs -text
6
+ *.ttf filter=lfs diff=lfs merge=lfs -text
7
+ *.woff filter=lfs diff=lfs merge=lfs -text
8
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
9
+ *.eot filter=lfs diff=lfs merge=lfs -text
10
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
11
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
12
+ *.zip filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+
3
+ # Compiled output
4
+ /dist
5
+ /tmp
6
+ /out-tsc
7
+ /bazel-out
8
+
9
+ # Node
10
+ /node_modules
11
+ npm-debug.log
12
+ yarn-error.log
13
+
14
+ # IDEs and editors
15
+ .idea/
16
+ .project
17
+ .classpath
18
+ .c9/
19
+ *.launch
20
+ .settings/
21
+ *.sublime-workspace
22
+
23
+ # Visual Studio Code
24
+ .vscode/*
25
+ !.vscode/settings.json
26
+ !.vscode/tasks.json
27
+ !.vscode/launch.json
28
+ !.vscode/extensions.json
29
+ .history/*
30
+
31
+ # Miscellaneous
32
+ /.angular/cache
33
+ .sass-cache/
34
+ /connect.lock
35
+ /coverage
36
+ /libpeerconnection.log
37
+ testem.log
38
+ /typings
39
+
40
+ # System files
41
+ .DS_Store
42
+ Thumbs.db
.vscode/extensions.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3
+ "recommendations": ["angular.ng-template"]
4
+ }
.vscode/launch.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "type": "edge",
6
+ "request": "launch",
7
+ "name": "localhost (Edge)",
8
+ "url": "http://localhost:4200",
9
+ "webRoot": "${workspaceFolder}"
10
+ },
11
+ {
12
+ "type": "chrome",
13
+ "request": "launch",
14
+ "name": "localhost (Chrome)",
15
+ "url": "http://localhost:4200",
16
+ "webRoot": "${workspaceFolder}"
17
+ }
18
+ ]
19
+ }
.vscode/tasks.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3
+ "version": "2.0.0",
4
+ "tasks": [
5
+ {
6
+ "type": "npm",
7
+ "script": "start",
8
+ "isBackground": true,
9
+ "problemMatcher": {
10
+ "owner": "typescript",
11
+ "pattern": "$tsc",
12
+ "background": {
13
+ "activeOnStart": true,
14
+ "beginsPattern": {
15
+ "regexp": "(.*?)"
16
+ },
17
+ "endsPattern": {
18
+ "regexp": "bundle generation complete"
19
+ }
20
+ }
21
+ }
22
+ },
23
+ {
24
+ "type": "npm",
25
+ "script": "test",
26
+ "isBackground": true,
27
+ "problemMatcher": {
28
+ "owner": "typescript",
29
+ "pattern": "$tsc",
30
+ "background": {
31
+ "activeOnStart": true,
32
+ "beginsPattern": {
33
+ "regexp": "(.*?)"
34
+ },
35
+ "endsPattern": {
36
+ "regexp": "bundle generation complete"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ]
42
+ }
GrammAI.esproj ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/0.5.128-alpha">
2
+ <PropertyGroup>
3
+ <StartupCommand>npm start</StartupCommand>
4
+ <JavaScriptTestFramework>Jasmine</JavaScriptTestFramework>
5
+ <!-- Allows the build (or compile) script located on package.json to run on Build -->
6
+ <ShouldRunBuildScript>false</ShouldRunBuildScript>
7
+ <!-- Folder where production build objects will be placed -->
8
+ <PublishAssetsDirectory>$(MSBuildProjectDirectory)\dist\GrammAI\</PublishAssetsDirectory>
9
+ </PropertyGroup>
10
+ </Project>
GrammAI.esproj.user ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
4
+ <DebuggerFlavor>LaunchJsonDebugger</DebuggerFlavor>
5
+ <LaunchJsonTarget>
6
+ </LaunchJsonTarget>
7
+ </PropertyGroup>
8
+ </Project>
README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ---
3
+ title: py-learn (Angular 17)
4
+ sdk: static
5
+ app_build_command: npm ci && npm run build -- --configuration=production
6
+ app_file: dist/gramm-ai/browser/index.html
7
+ emoji: 🅰️
8
+ ---
9
+
10
+
11
+
12
+ # GrammAI
13
+
14
+ This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2.
15
+
16
+ ## Development server
17
+
18
+ Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
19
+
20
+ ## Code scaffolding
21
+
22
+ Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
23
+
24
+ ## Build
25
+
26
+ Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
27
+
28
+ ## Running unit tests
29
+
30
+ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
31
+
32
+ ## Running end-to-end tests
33
+
34
+ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
35
+
36
+ ## Further help
37
+
38
+ To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
angular.json ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "GrammAI": {
7
+ "projectType": "application",
8
+ "schematics": {
9
+ "@schematics/angular:component": {
10
+ "standalone": false
11
+ },
12
+ "@schematics/angular:directive": {
13
+ "standalone": false
14
+ },
15
+ "@schematics/angular:pipe": {
16
+ "standalone": false
17
+ }
18
+ },
19
+ "root": "",
20
+ "sourceRoot": "src",
21
+ "prefix": "app",
22
+ "architect": {
23
+ "build": {
24
+ "builder": "@angular-devkit/build-angular:application",
25
+ "options": {
26
+ "outputPath": "dist/gramm-ai",
27
+ "index": "src/index.html",
28
+ "browser": "src/main.ts",
29
+ "polyfills": [
30
+ "zone.js"
31
+ ],
32
+ "tsConfig": "tsconfig.app.json",
33
+ "assets": [
34
+ "src/favicon.ico",
35
+ "src/assets"
36
+ ],
37
+ "styles": [
38
+ "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
39
+ "src/styles.css"
40
+ ],
41
+ "scripts": []
42
+ },
43
+ "configurations": {
44
+ "production": {
45
+ "budgets": [
46
+ {
47
+ "type": "initial",
48
+ "maximumWarning": "900kb",
49
+ "maximumError": "2mb"
50
+ },
51
+ {
52
+ "type": "anyComponentStyle",
53
+ "maximumWarning": "16kb",
54
+ "maximumError": "32kb"
55
+ }
56
+ ],
57
+ "outputHashing": "all"
58
+ },
59
+ "development": {
60
+ "optimization": false,
61
+ "extractLicenses": false,
62
+ "sourceMap": true
63
+ }
64
+ },
65
+ "defaultConfiguration": "production"
66
+ },
67
+ "serve": {
68
+ "builder": "@angular-devkit/build-angular:dev-server",
69
+ "configurations": {
70
+ "production": {
71
+ "buildTarget": "GrammAI:build:production"
72
+ },
73
+ "development": {
74
+ "buildTarget": "GrammAI:build:development"
75
+ }
76
+ },
77
+ "defaultConfiguration": "development"
78
+ },
79
+ "extract-i18n": {
80
+ "builder": "@angular-devkit/build-angular:extract-i18n",
81
+ "options": {
82
+ "buildTarget": "GrammAI:build"
83
+ }
84
+ },
85
+ "test": {
86
+ "builder": "@angular-devkit/build-angular:karma",
87
+ "options": {
88
+ "polyfills": [
89
+ "zone.js",
90
+ "zone.js/testing"
91
+ ],
92
+ "tsConfig": "tsconfig.spec.json",
93
+ "assets": [
94
+ "src/favicon.ico",
95
+ "src/assets"
96
+ ],
97
+ "styles": [
98
+ "src/styles.css"
99
+ ],
100
+ "scripts": [],
101
+ "karmaConfig": "karma.conf.js"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "cli": {
108
+ "analytics": false
109
+ }
110
+ }
canvas-confetti.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ declare module 'canvas-confetti';
karma.conf.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = function (config) {
2
+ config.set({
3
+ basePath: '',
4
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
5
+ plugins: [
6
+ require('karma-jasmine'),
7
+ require('karma-chrome-launcher'),
8
+ require('karma-jasmine-html-reporter'),
9
+ require('karma-coverage'),
10
+ require('@angular-devkit/build-angular/plugins/karma')
11
+ ],
12
+ client: {
13
+ jasmine: {
14
+ // you can add configuration options for Jasmine here
15
+ // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
16
+ // for example, you can disable the random execution with `random: false`
17
+ // or set a specific seed with `seed: 4321`
18
+ },
19
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
20
+ },
21
+ jasmineHtmlReporter: {
22
+ suppressAll: true // removes the duplicated traces
23
+ },
24
+ coverageReporter: {
25
+ dir: require('path').join(__dirname, './coverage/'),
26
+ subdir: '.',
27
+ reporters: [
28
+ { type: 'html' },
29
+ { type: 'text-summary' }
30
+ ]
31
+ },
32
+ reporters: ['progress', 'kjhtml'],
33
+ port: 9876,
34
+ colors: true,
35
+ logLevel: config.LOG_INFO,
36
+ autoWatch: true,
37
+ browsers: ['Chrome'],
38
+ singleRun: false,
39
+ restartOnFileChange: true,
40
+ listenAddress: 'localhost',
41
+ hostname: 'localhost'
42
+ });
43
+ };
44
+
nuget.config ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <configuration>
3
+ <packageSources>
4
+ <clear />
5
+ <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
6
+ </packageSources>
7
+ <disabledPackageSources>
8
+ <clear />
9
+ </disabledPackageSources>
10
+ </configuration>
obj/Debug/GrammAI.esproj.CoreCompileInputs.cache ADDED
File without changes
obj/Debug/GrammAI.esproj.FileListAbsolute.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ C:\Users\Admin\Desktop\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
2
+ C:\Users\Admin\Desktop\LE_AI\frontend\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
3
+ C:\Users\Admin\Desktop\LE_AI\New folder\frontend\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
4
+ C:\Users\Admin\Desktop\LE_AI\Frontend-saranya\frontend\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
5
+ C:\Users\Admin\Desktop\pylearn\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
6
+ C:\Users\Admin\Desktop\LE_AI\Frontend\py-learn\GrammAI\GrammAI\obj\Debug\GrammAI.esproj.CoreCompileInputs.cache
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gramm-ai",
3
+ "version": "0.0.0",
4
+ "engines": { "node": ">=18" },
5
+ "scripts": {
6
+ "ng": "ng",
7
+ "start": "ng serve --host=127.0.0.1 ",
8
+ "build": "ng build",
9
+ "watch": "ng build --watch --configuration development",
10
+ "test": "ng test"
11
+ },
12
+ "private": true,
13
+ "dependencies": {
14
+ "@angular/animations": "^17.1.0",
15
+ "@angular/common": "^17.1.0",
16
+ "@angular/compiler": "^17.1.0",
17
+ "@angular/core": "^17.1.0",
18
+ "@angular/forms": "^17.1.0",
19
+ "@angular/platform-browser": "^17.1.0",
20
+ "@angular/platform-browser-dynamic": "^17.1.0",
21
+ "@angular/router": "^17.1.0",
22
+ "@fortawesome/fontawesome-free": "^6.7.2",
23
+ "@ngx-translate/core": "^16.0.4",
24
+ "@ngx-translate/http-loader": "^16.0.1",
25
+ "autoprefixer": "^10.4.20",
26
+ "canvas-confetti": "^1.9.3",
27
+ "jest-editor-support": "*",
28
+ "postcss": "^8.4.49",
29
+ "rxjs": "~7.8.0",
30
+ "tailwindcss": "^3.4.17",
31
+ "tslib": "^2.3.0",
32
+ "zone.js": "~0.14.3"
33
+ },
34
+ "devDependencies": {
35
+ "@angular-devkit/build-angular": "^17.1.2",
36
+ "@angular/cli": "^17.1.2",
37
+ "@angular/compiler-cli": "^17.1.0",
38
+ "@types/canvas-confetti": "^1.9.0",
39
+ "@types/jasmine": "~5.1.0",
40
+ "jasmine-core": "~5.1.0",
41
+ "karma": "~6.4.0",
42
+ "karma-chrome-launcher": "~3.2.0",
43
+ "karma-coverage": "~2.2.0",
44
+ "karma-jasmine": "~5.1.0",
45
+ "karma-jasmine-html-reporter": "~2.1.0",
46
+ "typescript": "~5.3.2"
47
+ }
48
+ }
src/app/app-routing.module.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NgModule } from '@angular/core';
2
+ import { RouterModule, Routes } from '@angular/router';
3
+ import { ChatComponent } from './chat/chat.component';
4
+ import { HomeComponent } from './home/home.component';
5
+ import { ListenComponent } from './listen/listen.component';
6
+
7
+ import { GenerateQuestionsComponent } from './generate-questions/generate-questions.component';
8
+ import { VoiceComponent } from './voice/voice.component';
9
+ import { WritingComponent } from './writing/writing.component';
10
+ import { VocabularyBuilderComponent } from './vocabulary-builder/vocabulary-builder.component';
11
+ import { FindwordComponent } from './findword/findword.component';
12
+ import { ReadingComponent } from './reading/reading.component';
13
+ import { AuthenticationComponent } from './authentication/authentication.component';
14
+ import { authenticationGuard } from './authentication/authentication.guard'; // Use correct import
15
+
16
+ import { AuthComponent } from './auth/auth.component';
17
+ import { authGuard } from './auth/auth.guard';
18
+ import { rootRedirectGuard } from './auth/root-redirect.guard';
19
+ import { RootRedirectComponent } from './root-redirect/root-redirect.component';
20
+
21
+
22
+ // Define and export routes
23
+ export const routes: Routes = [
24
+ //{ path: '', redirectTo: 'authentication', pathMatch: 'full' },
25
+ { path: '', component: RootRedirectComponent, canActivate: [rootRedirectGuard], pathMatch: 'full' },
26
+
27
+ { path: 'home', component: HomeComponent, canActivate: [authGuard] },
28
+ { path: 'chat', component: ChatComponent, canActivate: [authGuard] },
29
+ { path: 'generate-questions', component: GenerateQuestionsComponent, canActivate: [authGuard] },
30
+ { path: 'voice', component: VoiceComponent, canActivate: [authGuard] },
31
+ { path: 'listen', component: ListenComponent, canActivate: [authGuard] },
32
+ { path: 'writing', component: WritingComponent, canActivate: [authGuard] },
33
+ { path: 'vocabulary-builder', component: VocabularyBuilderComponent, canActivate: [authGuard] },
34
+ { path: 'findword', component: FindwordComponent, canActivate: [authGuard] },
35
+ { path: 'reading', component: ReadingComponent, canActivate: [authGuard] },
36
+ { path: 'authentication', component: AuthenticationComponent },
37
+ { path: 'auth', component: AuthComponent },
38
+ { path: '**', redirectTo: 'auth' },
39
+
40
+ //{ path: 'generate-questions', component: GenerateQuestionsComponent },
41
+ //{ path: 'chat', component: ChatComponent},
42
+ //{ path: 'findword', component: FindwordComponent },
43
+ //{ path: 'reading', component: ReadingComponent},
44
+
45
+
46
+ ];
47
+
48
+ @NgModule({
49
+ imports: [RouterModule.forRoot(routes)],
50
+ exports: [RouterModule]
51
+ })
52
+ export class AppRoutingModule { }
src/app/app.component.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ nav {
2
+ margin: 20px;
3
+ }
4
+
5
+ button {
6
+ margin-right: 10px;
7
+ padding: 10px 20px;
8
+ font-size: 16px;
9
+ cursor: pointer;
10
+ }
11
+
12
+
src/app/app.component.html ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+ <router-outlet></router-outlet>
src/app/app.component.spec.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { RouterTestingModule } from '@angular/router/testing';
3
+ import { AppComponent } from './app.component';
4
+
5
+ describe('AppComponent', () => {
6
+ beforeEach(async () => {
7
+ await TestBed.configureTestingModule({
8
+ imports: [
9
+ RouterTestingModule
10
+ ],
11
+ declarations: [
12
+ AppComponent
13
+ ],
14
+ }).compileComponents();
15
+ });
16
+
17
+ it('should create the app', () => {
18
+ const fixture = TestBed.createComponent(AppComponent);
19
+ const app = fixture.componentInstance;
20
+ expect(app).toBeTruthy();
21
+ });
22
+
23
+ it(`should have as title 'GrammAI'`, () => {
24
+ const fixture = TestBed.createComponent(AppComponent);
25
+ const app = fixture.componentInstance;
26
+ expect(app.title).toEqual('GrammAI');
27
+ });
28
+
29
+ it('should render title', () => {
30
+ const fixture = TestBed.createComponent(AppComponent);
31
+ fixture.detectChanges();
32
+ const compiled = fixture.nativeElement as HTMLElement;
33
+ expect(compiled.querySelector('h1')?.textContent).toContain('Hello, GrammAI');
34
+ });
35
+ });
src/app/app.component.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { AuthService } from './auth/auth.service';
3
+
4
+ @Component({
5
+ selector: 'app-root',
6
+ templateUrl: './app.component.html',
7
+ styleUrl: './app.component.css'
8
+ })
9
+ export class AppComponent {
10
+ constructor(private authService: AuthService) { }
11
+ title = 'Py-Learn';
12
+
13
+ ngOnInit(): void {
14
+ this.authService.checkSession().subscribe((status) => {
15
+ if (status) {
16
+ this.authService.startAutoRefresh(); // ✅ Start auto-refresh once app loads and session is valid
17
+ }
18
+ });
19
+ }
20
+ }
src/app/app.module.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NgModule } from '@angular/core';
2
+ import { BrowserModule } from '@angular/platform-browser';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { HttpClientModule } from '@angular/common/http';
5
+ import { AppRoutingModule } from './app-routing.module';
6
+
7
+ // Components
8
+ import { AppComponent } from './app.component';
9
+ import { GenerateQuestionsComponent } from './generate-questions/generate-questions.component';
10
+ import { HomeComponent } from './home/home.component';
11
+ import { VoiceComponent } from './voice/voice.component';
12
+ import { ListenComponent } from './listen/listen.component';
13
+ import { WritingComponent } from './writing/writing.component';
14
+ import { VocabularyBuilderComponent } from './vocabulary-builder/vocabulary-builder.component';
15
+ import { FindwordComponent } from './findword/findword.component';
16
+ import { ReadingComponent } from './reading/reading.component';
17
+ import { AuthenticationComponent } from './authentication/authentication.component';
18
+ import { AuthComponent } from './auth/auth.component';
19
+
20
+ import { RootRedirectComponent } from './root-redirect/root-redirect.component';
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+ @NgModule({
30
+ declarations: [
31
+ AppComponent, // Add AppComponent here
32
+ //GenerateQuestionsComponent,
33
+ HomeComponent,
34
+ VoiceComponent,
35
+ ListenComponent,
36
+ WritingComponent,
37
+ VocabularyBuilderComponent,
38
+ FindwordComponent,
39
+ ReadingComponent,
40
+ AuthenticationComponent,
41
+ AuthComponent,
42
+ RootRedirectComponent
43
+
44
+
45
+ ],
46
+ imports: [
47
+ BrowserModule,
48
+ AppRoutingModule,
49
+ FormsModule,
50
+ HttpClientModule,
51
+
52
+ ],
53
+ providers: [],
54
+ bootstrap: [AppComponent] // Bootstrap AppComponent
55
+ })
56
+ export class AppModule { }
src/app/auth/auth.component.css ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .py-learn-text {
2
+ font-size: 4vw;
3
+ font-weight: 600;
4
+ color: #073879;
5
+ font-family: Amonk_Outline;
6
+ }
7
+
8
+ .self-learning-system {
9
+ font-size: 1.2vw;
10
+ font-weight: 500;
11
+ color: #073879;
12
+ margin-top: -0.8vw;
13
+ }
14
+
15
+ .logo-header {
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ text-align: center;
21
+ }
22
+
23
+ .login-wrapper {
24
+ height: 100vh;
25
+ display: flex;
26
+ justify-content: center;
27
+ align-items: center;
28
+ background-color: #05234b;
29
+ }
30
+
31
+ .login-container {
32
+ display: flex;
33
+ width: 66vw;
34
+ height: 600px;
35
+ background-color: white;
36
+ border-radius: 24px;
37
+ /*box-shadow: #ffffff 0px 5px 15px;*/
38
+ box-shadow: #ffffff 1px 1px 51px;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .login-image {
43
+ flex: 1;
44
+ background-color: #e8f0fe;
45
+ display: flex;
46
+ align-items: center;
47
+ }
48
+
49
+ .bgImage {
50
+ width: 100%;
51
+ height: 66vh;
52
+ }
53
+
54
+ .login-box {
55
+ flex: 1;
56
+ padding: 48px;
57
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
58
+ display: flex;
59
+ flex-direction: column;
60
+ justify-content: center;
61
+ }
62
+
63
+ .logo {
64
+ width: 6vw;
65
+ position: absolute;
66
+ top: 1vw;
67
+ left: 1vw;
68
+ background-color: #ffffff;
69
+ border-radius: 1vw;
70
+ }
71
+
72
+ h2 {
73
+ font-size: 1.7vw;
74
+ margin-bottom: 6px;
75
+ text-align: left;
76
+ margin-top: 1vw;
77
+ font-weight: 500;
78
+ }
79
+
80
+ .subtitle {
81
+ font-size: 18px;
82
+ color: #1a73e8;
83
+ text-align: center;
84
+ font-weight: bold;
85
+ }
86
+
87
+ input[type="text"],
88
+ input[type="password"] {
89
+ width: 100%;
90
+ padding: 16px;
91
+ /* margin: 12px 0; */
92
+ border: 1px solid #dadce0;
93
+ border-radius: 6px;
94
+ font-size: 18px;
95
+ margin-bottom: 1vw;
96
+ }
97
+
98
+ button {
99
+ background-color: #1a73e8;
100
+ color: white;
101
+ border: none;
102
+ padding: 14px;
103
+ font-size: 1.2vw;
104
+ border-radius: 6px;
105
+ width: 100%;
106
+ margin-top: 20px;
107
+ cursor: pointer;
108
+ font-weight: 700;
109
+ }
110
+
111
+ button:hover {
112
+ background-color: #1669c1;
113
+ }
114
+
115
+ .error {
116
+ color: red;
117
+ margin-top: 10px;
118
+ font-size: 15px;
119
+ }
120
+
121
+ .language-switcher, .grade-switcher {
122
+ margin-top: 30px;
123
+ }
124
+
125
+ .password-field {
126
+ position: relative;
127
+ width: 100%;
128
+ }
129
+
130
+ .password-field input {
131
+ padding-right: 40px;
132
+ }
133
+
134
+ .toggle-password {
135
+ position: absolute;
136
+ right: 12px;
137
+ top: 41%;
138
+ transform: translateY(-50%);
139
+ cursor: pointer;
140
+ font-size: 1.2vw;
141
+ color: #555;
142
+ user-select: none;
143
+ }
144
+
145
+
146
+ .footer-link {
147
+ position: absolute;
148
+ bottom: 1vw;
149
+ right: 6vw;
150
+ font-size: 14px;
151
+ color: #1a73e8;
152
+ }
153
+
154
+ .footer-link a {
155
+ text-decoration: none;
156
+ color: white;
157
+ font-size: 1.2vw;
158
+ font-weight: bold;
159
+ }
160
+
161
+ .footer-link a:hover {
162
+ text-decoration: underline;
163
+ }
164
+
165
+
166
+ /* Positioning the social media icons at the top-right corner */
167
+ .social-media-icons {
168
+ position: absolute;
169
+ top: 1vw;
170
+ right: 2vw;
171
+ display: flex;
172
+ gap: 15px;
173
+ }
174
+
175
+ .social-icon img {
176
+ width: 3vw;
177
+ height: 3vw;
178
+ transition: transform 0.3s;
179
+ }
180
+
181
+ .social-icon img:hover {
182
+ transform: scale(1.1); /* Slight zoom effect on hover */
183
+ }
184
+
185
+ .language-grade-container {
186
+ display: flex;
187
+ gap: 13vw; /* Gap between Language and Grade sections */
188
+ align-items: center;
189
+ }
190
+
191
+ .language-switcher,
192
+ .grade-switcher {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 0.5vw; /* Gap between label and select box */
196
+ }
197
+
198
+ .language-label, .grade-label {
199
+ color: #007bff; /* Blue for Language label */
200
+ font-size: 1vw;
201
+ font-weight: bold;
202
+ }
203
+
204
+
205
+
206
+ .dropdown-select {
207
+ /* padding: 0.5vw; */
208
+ font-size: 1vw;
209
+ border-radius: 5px;
210
+ /* border: 1px solid #000; */
211
+ }
src/app/auth/auth.component.html ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="login-wrapper">
2
+ <div class="login-container">
3
+ <!-- Left side image and logo -->
4
+ <div class="login-image">
5
+ <img src="assets/images/login/lion.png" alt="Login Illustration" class="bgImage" />
6
+ <img src="assets/images/pykara-logo.png" alt="Pykara Logo" class="logo" />
7
+ </div>
8
+
9
+ <!-- Right side form -->
10
+ <div class="login-box">
11
+ <div class="logo-header">
12
+ <span class="py-learn-text">Py- Learn</span>
13
+ <!-- Added self-learning system text here -->
14
+ <div class="self-learning-system">(A Self-Learning System)</div>
15
+ </div>
16
+
17
+ <h2>Login</h2>
18
+ <!--<p class="subtitle">Use your Pykara account</p>-->
19
+ <!--<input type="text" [(ngModel)]="username" placeholder="Username or Email" />
20
+
21
+ <div class="password-field">
22
+ <input [type]="showPassword ? 'text' : 'password'"
23
+ [(ngModel)]="password"
24
+ placeholder="Password" />
25
+ <span class="toggle-password" (click)="togglePasswordVisibility()">
26
+ <i class="{{ showPassword ? 'fas fa-eye' : 'fas fa-eye-slash' }}"></i>
27
+ </span>
28
+ </div>
29
+
30
+ <p *ngIf="errorMessage" class="error">{{ errorMessage }}</p>
31
+
32
+ <button (click)="login()">Login</button>-->
33
+
34
+ <form (ngSubmit)="login()" #loginForm="ngForm" autocomplete="on">
35
+ <input type="text" name="username" [(ngModel)]="username" placeholder="Username or Email" required />
36
+
37
+ <div class="password-field">
38
+ <input [type]="showPassword ? 'text' : 'password'"
39
+ name="password"
40
+ [(ngModel)]="password"
41
+ placeholder="Password"
42
+ required
43
+ autocomplete="current-password"
44
+ (keydown.enter)="login(); $event.preventDefault()"/>
45
+ <span class="toggle-password" (click)="togglePasswordVisibility()">
46
+ <i class="{{ showPassword ? 'fas fa-eye' : 'fas fa-eye-slash' }}"></i>
47
+ </span>
48
+ </div>
49
+
50
+ <p *ngIf="errorMessage" class="error">{{ errorMessage }}</p>
51
+
52
+ <button type="submit">Login</button>
53
+ </form>
54
+
55
+
56
+ <div class="language-grade-container">
57
+ <div class="language-switcher">
58
+ <label for="lang-select" class="language-label">Language:</label>
59
+ <select id="lang-select" class="dropdown-select">
60
+ <option value="en-GB">English (UK)</option>
61
+ <option value="en-US">English (US)</option>
62
+ <option value="sv">Svenska</option>
63
+ <option value="fr">Français</option>
64
+ <option value="de">Deutsch</option>
65
+ <option value="es">Español</option>
66
+ </select>
67
+ </div>
68
+
69
+ <div class="grade-switcher">
70
+ <label for="grade-select" class="grade-label">Grade:</label>
71
+ <select id="grade-select" class="dropdown-select">
72
+ <option value="4th">4th</option>
73
+ <option value="5th">5th</option>
74
+ <option value="6th">6th</option>
75
+ </select>
76
+ </div>
77
+ </div>
78
+
79
+
80
+ </div>
81
+ </div>
82
+
83
+ <div class="footer-link">
84
+ <a href="https://pykara.ai" target="_blank">www.pykara.ai</a>
85
+ </div>
86
+
87
+
88
+ <!-- Social Media Icons at the top-right -->
89
+ <div class="social-media-icons">
90
+ <a href="https://www.youtube.com/@PykaraTechnologies/videos" target="_blank" class="social-icon">
91
+
92
+ <img src="assets/images/home/youtube-icon.png" alt="YouTube">
93
+ </a>
94
+ <a href="https://www.linkedin.com/in/pykara-technologies" target="_blank" class="social-icon">
95
+
96
+ <img src="assets/images/home/linkedin-icon.png" alt="LinkedIn">
97
+ </a>
98
+ </div>
99
+
100
+
101
+ </div>
src/app/auth/auth.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { AuthComponent } from './auth.component';
4
+
5
+ describe('AuthComponent', () => {
6
+ let component: AuthComponent;
7
+ let fixture: ComponentFixture<AuthComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [AuthComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(AuthComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/auth/auth.component.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { AuthService } from './auth.service';
3
+ import { Router } from '@angular/router';
4
+
5
+ @Component({
6
+ selector: 'app-auth',
7
+ templateUrl: './auth.component.html',
8
+ styleUrl: './auth.component.css'
9
+ })
10
+ export class AuthComponent {
11
+
12
+ username: string = '';
13
+ password: string = '';
14
+ errorMessage: string = '';
15
+ showPassword: boolean = false;
16
+
17
+ constructor(private authService: AuthService, private router: Router) { }
18
+
19
+ //ngOnInit(): void {
20
+ // this.authService.checkSession();
21
+ //}
22
+ // ✅ Handle Login
23
+ login(): void {
24
+ this.authService.login(this.username, this.password).subscribe(
25
+ (response) => {
26
+ console.log('✅ Login success. Redirecting to /home...');
27
+ this.authService.setLoggedIn(true);
28
+ this.authService.startAutoRefresh();
29
+ /*this.router.navigate(['/home']); // Redirect to dashboard after login*/
30
+ // 🔁 Redirect to stored URL or default to /home
31
+ const redirectUrl = localStorage.getItem('redirectAfterLogin') || '/home';
32
+ localStorage.removeItem('redirectAfterLogin');
33
+ this.router.navigate([redirectUrl]);
34
+ },
35
+ (error) => {
36
+ this.errorMessage = 'Invalid username or password';
37
+ }
38
+ );
39
+ }
40
+
41
+
42
+
43
+
44
+ // ✅ Check if user is logged in
45
+ isLoggedIn(): boolean {
46
+ return this.authService.isLoggedIn();
47
+ }
48
+
49
+
50
+ togglePasswordVisibility(): void {
51
+ this.showPassword = !this.showPassword;
52
+ }
53
+
54
+
55
+ }
src/app/auth/auth.guard.spec.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { CanActivateFn } from '@angular/router';
3
+
4
+ import { authGuard } from './auth.guard';
5
+
6
+ describe('authGuard', () => {
7
+ const executeGuard: CanActivateFn = (...guardParameters) =>
8
+ TestBed.runInInjectionContext(() => authGuard(...guardParameters));
9
+
10
+ beforeEach(() => {
11
+ TestBed.configureTestingModule({});
12
+ });
13
+
14
+ it('should be created', () => {
15
+ expect(executeGuard).toBeTruthy();
16
+ });
17
+ });
src/app/auth/auth.guard.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { inject } from '@angular/core';
2
+ import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
3
+ import { AuthService } from './auth.service';
4
+ import { map, catchError } from 'rxjs/operators';
5
+ import { of } from 'rxjs';
6
+
7
+ export const authGuard: CanActivateFn = (
8
+ route: ActivatedRouteSnapshot,
9
+ state: RouterStateSnapshot
10
+ ) => {
11
+ const authService = inject(AuthService);
12
+ const router = inject(Router);
13
+
14
+ return authService.checkSession().pipe(
15
+ map(() => {
16
+ if (authService.isLoggedIn()) {
17
+ return true;
18
+ } else {
19
+ localStorage.setItem('redirectAfterLogin', state.url);
20
+ router.navigate(['/auth']);
21
+ return false;
22
+ }
23
+ }),
24
+ catchError(() => {
25
+ localStorage.setItem('redirectAfterLogin', state.url);
26
+ router.navigate(['/auth']);
27
+ return of(false);
28
+ })
29
+ );
30
+ };
src/app/auth/auth.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { AuthService } from './auth.service';
4
+
5
+ describe('AuthService', () => {
6
+ let service: AuthService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(AuthService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/auth/auth.service.ts ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
4
+ import { tap, catchError } from 'rxjs/operators';
5
+ import { Router } from '@angular/router';
6
+
7
+
8
+
9
+ @Injectable({
10
+ providedIn: 'root'
11
+ })
12
+
13
+ export class AuthService {
14
+ private apiUrl = 'http://localhost:5000'; // Your Flask API base URL
15
+ private loggedInSubject = new BehaviorSubject<boolean>(false); // Shared state
16
+ public isLoggedIn$ = this.loggedInSubject.asObservable();
17
+ private refreshIntervalId: any;
18
+ constructor(private http: HttpClient, private router: Router) { }
19
+
20
+ // ✅ Called to check current status
21
+ isLoggedIn(): boolean {
22
+ return this.loggedInSubject.value;
23
+ }
24
+
25
+ // ✅ Set login status manually
26
+ setLoggedIn(status: boolean): void {
27
+ this.loggedInSubject.next(status);
28
+ }
29
+ // ✅ Login Method
30
+
31
+
32
+ login(username: string, password: string): Observable<any> {
33
+ return this.http.post(`${this.apiUrl}/login`, { username, password }, { withCredentials: true });
34
+ }
35
+
36
+
37
+ startAutoRefresh(): void {
38
+ if (this.refreshIntervalId) return;
39
+ this.refreshIntervalId = setInterval(() => {
40
+ this.refreshAccessToken();
41
+ }, 12 * 60 * 1000);
42
+ }
43
+
44
+ clearAutoRefresh(): void {
45
+ clearInterval(this.refreshIntervalId);
46
+ this.refreshIntervalId = null;
47
+ }
48
+
49
+ refreshAccessToken(): void {
50
+ this.http.post(`${this.apiUrl}/refresh`, {}, { withCredentials: true }).subscribe(
51
+ (response: any) => {
52
+ console.log("✅ Access token refreshed:", response.access_token);
53
+ //alert("🔄 Access token has been refreshed!");
54
+ },
55
+ (error) => {
56
+ console.error("❌ Refresh failed:", error);
57
+
58
+ if (
59
+ error.status === 401 &&
60
+ error.error &&
61
+ (error.error.message === 'Refresh token has expired' || error.error.message === 'Invalid refresh token')
62
+ ) {
63
+ //alert("⚠️ Your session has expired. Please log in again.");
64
+
65
+ // Clear cookies manually (if needed)
66
+ this.clearTokens();
67
+
68
+ // Redirect to login
69
+ this.router.navigate(['/auth']);
70
+ }
71
+ }
72
+ );
73
+ }
74
+
75
+
76
+
77
+
78
+ logout(): Observable<any> {
79
+ console.log("🔧 Sending logout request with credentials");
80
+
81
+ return this.http.post(`${this.apiUrl}/logout`, {}, { withCredentials: true }).pipe(
82
+ tap(response => {
83
+ console.log('🔙 Response from backend:', response);
84
+ this.clearTokens();
85
+ this.clearAutoRefresh(); // ✅ Stop auto-refresh
86
+ }),
87
+ catchError(error => {
88
+ console.error('❌ Error from backend:', error);
89
+ return throwError(() => error);
90
+ })
91
+ );
92
+ }
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ clearTokens(): void {
101
+ document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
102
+ document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
103
+ this.clearAutoRefresh(); // ✅
104
+ }
105
+
106
+
107
+ //// ✅ Check if user is logged in (check for presence of cookies)
108
+ //isLoggedIn(): boolean {
109
+ // // Check cookies directly (this will happen automatically when the browser sends cookies)
110
+ // // Check if the cookies are present (you can do it manually using a service or via backend)
111
+ // return document.cookie.includes('access_token');
112
+ //}
113
+
114
+ // ✅ Get access token (using cookies)
115
+ getAccessToken(): string | null {
116
+ const cookies = document.cookie.split('; ');
117
+ for (let cookie of cookies) {
118
+ if (cookie.startsWith('access_token=')) {
119
+ return cookie.split('=')[1];
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // ✅ Save tokens (Not necessary, as tokens are managed via cookies)
126
+ saveTokens(accessToken: string, refreshToken: string): void {
127
+ // No need for this if you're using cookies, but if you want to persist in localStorage, use:
128
+ localStorage.setItem('access_token', accessToken);
129
+ localStorage.setItem('refresh_token', refreshToken);
130
+ }
131
+
132
+ //checkSession(): Observable<boolean> {
133
+ // return this.http.get(`${this.apiUrl}/check-auth`, { withCredentials: true }).pipe(
134
+ // tap((res: any) => {
135
+ // console.log('✅ Session valid:', res);
136
+ // this.setLoggedIn(true);
137
+ // }),
138
+ // catchError((err) => {
139
+ // console.warn('❌ Session check failed:', err);
140
+ // this.setLoggedIn(false);
141
+ // return [false]; // Return false on error
142
+ // })
143
+ // );
144
+ //}
145
+
146
+ checkSession(): Observable<boolean> {
147
+ return this.http.get(`${this.apiUrl}/check-auth`, { withCredentials: true }).pipe(
148
+ tap((res: any) => {
149
+ console.log('✅ Session valid:', res);
150
+ this.setLoggedIn(true);
151
+ this.startAutoRefresh();
152
+ return true; // ✅ Important!
153
+ }),
154
+ catchError((err) => {
155
+ if (err.status === 401) {
156
+ // Access token may be expired. Try refresh
157
+ console.warn('🔄 Access token expired. Trying to refresh...');
158
+
159
+ return this.http.post(`${this.apiUrl}/refresh`, {}, { withCredentials: true }).pipe(
160
+ tap((refreshRes: any) => {
161
+ console.log("✅ Token refreshed during checkSession.");
162
+ //alert("✅ Token refreshed during checkSession.");
163
+ this.setLoggedIn(true);
164
+ this.startAutoRefresh();
165
+ }),
166
+ catchError((refreshErr) => {
167
+ console.error("❌ Refresh token failed during checkSession.", refreshErr);
168
+ this.setLoggedIn(false);
169
+ return of(false);
170
+ })
171
+ );
172
+ } else {
173
+ console.error("❌ Unknown error during checkSession", err);
174
+ this.setLoggedIn(false);
175
+ return of(false);
176
+ }
177
+ })
178
+ );
179
+ }
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+ }
src/app/auth/root-redirect.guard.spec.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { CanActivateFn } from '@angular/router';
3
+
4
+ import { rootRedirectGuard } from './root-redirect.guard';
5
+
6
+ describe('rootRedirectGuard', () => {
7
+ const executeGuard: CanActivateFn = (...guardParameters) =>
8
+ TestBed.runInInjectionContext(() => rootRedirectGuard(...guardParameters));
9
+
10
+ beforeEach(() => {
11
+ TestBed.configureTestingModule({});
12
+ });
13
+
14
+ it('should be created', () => {
15
+ expect(executeGuard).toBeTruthy();
16
+ });
17
+ });
src/app/auth/root-redirect.guard.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { inject } from '@angular/core';
2
+ import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
3
+ import { AuthService } from './auth.service';
4
+ import { map, catchError, of } from 'rxjs';
5
+
6
+
7
+ export const rootRedirectGuard: CanActivateFn = (
8
+ route: ActivatedRouteSnapshot,
9
+ state: RouterStateSnapshot
10
+ ) => {
11
+ const authService = inject(AuthService);
12
+ const router = inject(Router);
13
+
14
+ return authService.checkSession().pipe(
15
+ map(() => {
16
+ if (authService.isLoggedIn()) {
17
+ router.navigate(['/home']);
18
+ } else {
19
+ router.navigate(['/auth']);
20
+ }
21
+ return false;
22
+ }),
23
+ catchError(() => {
24
+ router.navigate(['/auth']);
25
+ return of(false);
26
+ })
27
+ );
28
+
29
+ };
src/app/authentication/authentication.component.css ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .py-learn-text {
3
+ font-size: 4vw;
4
+ font-weight: 600;
5
+ color: #073879;
6
+ font-family: Amonk_Outline;
7
+ }
8
+
9
+
10
+
11
+
12
+ /* Wrapper for the full screen */
13
+ .login-wrapper {
14
+ height: 100vh;
15
+ display: flex;
16
+ justify-content: center;
17
+ align-items: center;
18
+ background-color: #05234b;
19
+ }
20
+
21
+ /* Container for left and right split */
22
+ .login-container {
23
+ display: flex;
24
+ width: 1100px;
25
+ height: 600px;
26
+ background-color: white;
27
+ border-radius: 24px;
28
+ box-shadow: #ffffff 0px 5px 15px;
29
+ overflow: hidden;
30
+ }
31
+
32
+ /* Left section (image) */
33
+ .login-image {
34
+ flex: 1;
35
+ background-color: #e8f0fe;
36
+ display: flex;
37
+ align-items: center;
38
+ }
39
+
40
+ .bgImage {
41
+ width: 100%;
42
+ height: 66vh;
43
+ /*opacity: 0.5;*/
44
+ }
45
+
46
+ /* Right section (form) */
47
+ .login-box {
48
+ flex: 1;
49
+ padding: 60px;
50
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
51
+ display: flex;
52
+ flex-direction: column;
53
+ justify-content: center;
54
+
55
+ }
56
+
57
+ .logo {
58
+ width: 6vw;
59
+ position: absolute;
60
+ top: 1vw;
61
+ left: 1vw;
62
+ background-color: #ffffff;
63
+ border-radius: 1vw;
64
+ }
65
+
66
+ .login-box h2 {
67
+ font-size: 36px;
68
+ margin-bottom: 10px;
69
+ }
70
+
71
+ .subtitle {
72
+ font-size: 18px;
73
+ color: #1a73e8;
74
+ margin-bottom: 30px;
75
+ font-weight: bold
76
+ }
77
+
78
+ input[type="text"],
79
+ input[type="password"] {
80
+ width: 100%;
81
+ padding: 16px;
82
+ margin: 12px 0;
83
+ border: 1px solid #dadce0;
84
+ border-radius: 6px;
85
+ font-size: 16px;
86
+ }
87
+
88
+ button {
89
+ background-color: #1a73e8;
90
+ color: white;
91
+ border: none;
92
+ padding: 14px;
93
+ font-size: 16px;
94
+ border-radius: 6px;
95
+ width: 100%;
96
+ margin-top: 20px;
97
+ cursor: pointer;
98
+ }
99
+
100
+ button:hover {
101
+ background-color: #1669c1;
102
+ }
103
+
104
+ .error {
105
+ color: red;
106
+ margin-top: 10px;
107
+ font-size: 15px;
108
+ }
109
+
110
+ .language-switcher {
111
+ margin-top: 30px;
112
+ font-size: 15px;
113
+ }
114
+ .password-field {
115
+ position: relative;
116
+ width: 100%;
117
+ }
118
+
119
+ .password-field input {
120
+ width: 100%;
121
+ padding-right: 40px; /* space for the icon */
122
+ }
123
+
124
+ .toggle-password {
125
+ position: absolute;
126
+ right: 12px;
127
+ top: 50%;
128
+ transform: translateY(-50%);
129
+ cursor: pointer;
130
+ font-size: 1.2vw;
131
+ color: #555;
132
+ user-select: none;
133
+ }
src/app/authentication/authentication.component.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="login-wrapper">
2
+ <div class="login-container">
3
+ <!-- Left side image -->
4
+ <div class="login-image">
5
+ <img src="assets/images/lion1.png" alt="Login Illustration" class="bgImage"/>
6
+ <img src="assets/images/pykara-logo.png" alt="Pykara Logo" class="logo" />
7
+ </div>
8
+
9
+ <!-- Right side form -->
10
+ <div class="login-box">
11
+
12
+ <div class="logo-header">
13
+ <span class="py-learn-text">Py Learn</span>
14
+ </div>
15
+
16
+ <h2>Sign in</h2>
17
+ <p class="subtitle">Use your Pykara account</p>
18
+
19
+ <div *ngIf="!isLoggedIn">
20
+ <input type="text" [(ngModel)]="username" placeholder="Email or phone" />
21
+ <div class="password-field">
22
+ <input [type]="showPassword ? 'text' : 'password'"
23
+ [(ngModel)]="password"
24
+ placeholder="Password" />
25
+ <span class="toggle-password" (click)="togglePasswordVisibility()">
26
+ <i class="{{ showPassword ? 'fas fa-eye' : 'fas fa-eye-slash' }}"></i>
27
+ <!-- {{ showPassword ? '👁️' : '🙈' }}-->
28
+ </span>
29
+ </div>
30
+
31
+ <p *ngIf="errorMessage" class="error">{{ errorMessage }}</p>
32
+
33
+ <button (click)="login()">Login</button>
34
+ </div>
35
+
36
+ <div class="language-switcher">
37
+ <label for="lang-select">Language:</label>
38
+ <select id="lang-select">
39
+ <option value="en">English</option>
40
+ <option value="fr">Français</option>
41
+ <option value="sv">Svenska</option>
42
+ <option value="de">Deutsch</option>
43
+ </select>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
src/app/authentication/authentication.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { AuthenticationComponent } from './authentication.component';
4
+
5
+ describe('AuthenticationComponent', () => {
6
+ let component: AuthenticationComponent;
7
+ let fixture: ComponentFixture<AuthenticationComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [AuthenticationComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(AuthenticationComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/authentication/authentication.component.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { AuthenticationService } from './authentication.service'; // Import the AuthenticationService
3
+ import { Router } from '@angular/router';
4
+
5
+ @Component({
6
+ selector: 'app-authentication',
7
+ templateUrl: './authentication.component.html',
8
+ styleUrl: './authentication.component.css'
9
+ })
10
+ export class AuthenticationComponent {
11
+ username: string = '';
12
+ password: string = '';
13
+ errorMessage: string = '';
14
+
15
+ constructor(private authService: AuthenticationService, private router: Router) { }
16
+
17
+ // ✅ Login functionality
18
+ login(): void {
19
+ this.authService.login(this.username, this.password).subscribe(
20
+ (response: any) => {
21
+ // Directly access the tokens from the response body
22
+ const token = response.access_token; // Access the access token directly from the body
23
+ this.authService.storeToken(token);
24
+ this.router.navigate(['/home']); // Redirect to the home page or dashboard after successful login
25
+ },
26
+ (error) => {
27
+ this.errorMessage = 'Invalid username or password'; // Show error message if login fails
28
+ }
29
+ );
30
+ }
31
+
32
+
33
+ // ✅ Check if the user is logged in (token is stored in localStorage)
34
+ get isLoggedIn(): boolean {
35
+ return this.authService.isLoggedIn();
36
+ }
37
+
38
+
39
+ showPassword: boolean = false;
40
+
41
+ togglePasswordVisibility(): void {
42
+ this.showPassword = !this.showPassword;
43
+ }
44
+
45
+
46
+
47
+ }
src/app/authentication/authentication.guard.spec.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { CanActivateFn } from '@angular/router';
3
+
4
+ import { authenticationGuard } from './authentication.guard';
5
+
6
+ describe('authenticationGuard', () => {
7
+ const executeGuard: CanActivateFn = (...guardParameters) =>
8
+ TestBed.runInInjectionContext(() => authenticationGuard(...guardParameters));
9
+
10
+ beforeEach(() => {
11
+ TestBed.configureTestingModule({});
12
+ });
13
+
14
+ it('should be created', () => {
15
+ expect(executeGuard).toBeTruthy();
16
+ });
17
+ });
src/app/authentication/authentication.guard.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { inject } from '@angular/core';
2
+ import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
3
+ import { AuthenticationService } from './authentication.service';
4
+
5
+ //export const authenticationGuard: CanActivateFn = (route, state) => {
6
+ // return true;
7
+ //};
8
+
9
+ export const authenticationGuard: CanActivateFn = (
10
+ route: ActivatedRouteSnapshot,
11
+ state: RouterStateSnapshot
12
+ ) => {
13
+ const authService = inject(AuthenticationService);
14
+ const router = inject(Router);
15
+
16
+ if (authService.isLoggedIn()) {
17
+ return true; // Allow access to the requested route
18
+ } else {
19
+ // Optionally, you can save the intended URL for redirection after login
20
+ router.navigate(['/authentication']);
21
+ return false; // Deny access and redirect to login page
22
+ }
23
+ };
24
+
src/app/authentication/authentication.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { AuthenticationService } from './authentication.service';
4
+
5
+ describe('AuthenticationService', () => {
6
+ let service: AuthenticationService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(AuthenticationService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/authentication/authentication.service.ts ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
4
+ import { Observable } from 'rxjs/internal/Observable';
5
+ import { tap } from 'rxjs/operators';
6
+ import { catchError } from 'rxjs/operators';
7
+ import { throwError } from 'rxjs';
8
+ @Injectable({
9
+ providedIn: 'root'
10
+ })
11
+ export class AuthenticationService {
12
+ private apiUrl = 'http://127.0.0.1:5000'; // Your backend URL
13
+ private loggedInSubject = new BehaviorSubject<boolean>(this.isLoggedIn()); // BehaviorSubject to track login status
14
+
15
+ constructor(private http: HttpClient) {
16
+ // Change from 5 minutes to 30 seconds
17
+ setInterval(() => this.checkAndRefreshToken(), 30 * 1000); // Every 30 seconds
18
+
19
+ // Periodically check for token expiry and refresh it if necessary
20
+ //setInterval(() => this.checkAndRefreshToken(), 5 * 60 * 1000); // Check every 5 minutes
21
+ }
22
+
23
+
24
+
25
+
26
+ // ✅ Login Method
27
+ // ✅ Login Method (returns an Observable)
28
+ login(username: string, password: string): Observable<any> {
29
+ const loginData = { username, password };
30
+ return this.http.post(`${this.apiUrl}/login`, loginData).pipe(
31
+ tap((response: any) => {
32
+ //const now = new Date();
33
+ // Store tokens in localStorage
34
+ localStorage.setItem('access_token', response.access_token);
35
+ localStorage.setItem('refresh_token', response.refresh_token);
36
+
37
+
38
+ // Optionally store expiry time of the access token (2 minutes)
39
+ const accessTokenExpiry = Date.now() + 2 * 60 * 1000; // 2 minutes expiration for access token
40
+ localStorage.setItem('access_token_expiry', accessTokenExpiry.toString());
41
+
42
+ // Optionally store expiry time of the refresh token (5 minutes)
43
+ const refreshTokenExpiry = Date.now() + 10 * 60 * 1000; // 5 minutes expiration for refresh token
44
+ localStorage.setItem('refresh_token_expiry', refreshTokenExpiry.toString());
45
+
46
+ this.loggedInSubject.next(true);
47
+
48
+ }),
49
+ catchError(error => {
50
+ // Handle error (token expiry / invalid login)
51
+ if (error.status === 401 && error.error.message === 'Token has expired') {
52
+ //alert('Your session has expired, please log in again.');
53
+ }
54
+ return throwError(error);
55
+ })
56
+ );
57
+ }
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+ logout(): Observable<any> {
67
+ const token = this.getToken();
68
+ console.log('🔑 Token at logout start:', token);
69
+
70
+ this.removeToken(); // Remove token first
71
+ this.loggedInSubject.next(false);
72
+
73
+ if (!token) {
74
+ console.warn('⚠️ No token found. Emitting success anyway.');
75
+ return new Observable(observer => {
76
+ observer.next(true);
77
+ observer.complete();
78
+ });
79
+ }
80
+
81
+ const headers = new HttpHeaders().set('Authorization', `Bearer ${token}`);
82
+ console.log('📤 Sending logout request with headers:', headers);
83
+
84
+ return this.http.post(`${this.apiUrl}/logout`, {}, { headers }).pipe(
85
+ tap(() => {
86
+ console.log('✅ Logout API request completed'); // This should now appear
87
+ }),
88
+ catchError((error) => {
89
+ console.error('❌ Logout error in service:', error);
90
+ return new Observable(observer => {
91
+ observer.next(true); // Continue flow even on error
92
+ observer.complete();
93
+ });
94
+ })
95
+ );
96
+ }
97
+
98
+
99
+
100
+
101
+
102
+
103
+ // ✅ Get Token (to use for making authorized requests)
104
+ getToken(): string | null {
105
+ return localStorage.getItem('access_token');
106
+ }
107
+
108
+ // ✅ Store Token
109
+ storeToken(token: string): void {
110
+ localStorage.setItem('access_token', token);
111
+ console.log('Token stored:', token); // Log the token to confirm it's saved
112
+ this.loggedInSubject.next(true); // Update login status
113
+ }
114
+ // ✅ Remove Token
115
+ removeToken(): void {
116
+ localStorage.removeItem('access_token');
117
+ localStorage.removeItem('refresh_token');
118
+ localStorage.removeItem('access_token_expiry');
119
+ this.loggedInSubject.next(false); // Update login status
120
+ }
121
+
122
+ // ✅ Check if the user is logged in
123
+ isLoggedIn(): boolean {
124
+ return !!this.getToken();
125
+ }
126
+ // ✅ Observable to track login status
127
+ get isLoggedIn$(): Observable<boolean> {
128
+ return this.loggedInSubject.asObservable();
129
+ }
130
+
131
+
132
+
133
+ checkAndRefreshToken() {
134
+ const accessTokenExpiry = localStorage.getItem('access_token_expiry');
135
+ if (accessTokenExpiry && Date.now() >= (+accessTokenExpiry - 5000)) {
136
+ // Refresh 5 seconds before expiry
137
+ this.refreshAccessToken();
138
+ }
139
+
140
+
141
+ }
142
+
143
+ // Check token every 5 minutes
144
+
145
+ // ✅ Refresh Access Token Method
146
+ // ✅ Refresh Access Token Method
147
+ //refreshAccessToken() {
148
+ // const refreshToken = localStorage.getItem('refresh_token');
149
+ // if (refreshToken) {
150
+ // this.http.post(`${this.apiUrl}/refresh`, { refresh_token: refreshToken }).subscribe((response: any) => {
151
+ // // Store the new access token
152
+ // localStorage.setItem('access_token', response.access_token);
153
+
154
+ // // Optionally, update the expiry time of the new access token (2 minutes)
155
+ // const newAccessTokenExpiry = Date.now() + 2 * 60 * 1000; // 2 minutes
156
+ // localStorage.setItem('access_token_expiry', newAccessTokenExpiry.toString());
157
+ // alert('Your access token has been refreshed successfully!');
158
+ // }, error => {
159
+ // console.log("Error refreshing token", error);
160
+ // // Handle token refresh failure (e.g., log out the user)
161
+ // this.logout();
162
+ // });
163
+ // }
164
+ //}
165
+ refreshAccessToken() {
166
+ const refreshToken = localStorage.getItem('refresh_token');
167
+
168
+ if (refreshToken) {
169
+ this.http.post(`${this.apiUrl}/refresh`, { refresh_token: refreshToken }).subscribe(
170
+ (response: any) => {
171
+ // Store the new access token
172
+ localStorage.setItem('access_token', response.access_token);
173
+
174
+ // Update expiry
175
+ const newAccessTokenExpiry = Date.now() + 2 * 60 * 1000; // 2 minutes
176
+ localStorage.setItem('access_token_expiry', newAccessTokenExpiry.toString());
177
+
178
+ //alert('Your access token has been refreshed successfully!');
179
+ },
180
+ error => {
181
+ console.log("Error refreshing token", error);
182
+
183
+ if (
184
+ error.status === 401 &&
185
+ (error.error.message === 'Refresh token has expired' || error.error.message === 'Invalid refresh token')
186
+ ) {
187
+ //alert('Your session has expired. Please log in again.');
188
+
189
+ // ✅ Remove tokens and redirect to login
190
+ this.removeToken();
191
+ this.loggedInSubject.next(false);
192
+ window.location.href = '/authentication'; // Adjust this if your login route is different
193
+ } else {
194
+ // Other errors can be handled differently
195
+ console.error("Unexpected error:", error);
196
+ }
197
+ }
198
+ );
199
+ }
200
+ }
201
+
202
+
203
+ }
src/app/chat/api.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { ApiService } from './api.service';
4
+
5
+ describe('ApiService', () => {
6
+ let service: ApiService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(ApiService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/chat/api.service.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+
5
+ @Injectable({
6
+ providedIn: 'root'
7
+ })
8
+ export class ApiService {
9
+
10
+ //private apiUrl = 'http://127.0.0.1:5000'; // Flask backend URL
11
+
12
+ private apiUrl = 'http://localhost:5000/explain-grammar';
13
+
14
+ constructor(private http: HttpClient) { }
15
+
16
+ // Method to send the grammar question and receive the explanation from Flask backend
17
+ //askQuestion(question: string): Observable<any> {
18
+ // const body = { topic: question }; // Prepare the request body
19
+ // return this.http.post<any>(`${this.apiUrl}/explain-grammar`, body); // Call the Flask API and return the observable
20
+ //}
21
+
22
+ // Method to send the user's question to the Flask backend
23
+ askQuestion(userInput: string): Observable<any> {
24
+ const data = { topic: userInput }; // Assuming you're sending the user input as a topic
25
+
26
+ return this.http.post<any>(this.apiUrl, data); // POST request to Flask backend
27
+ }
28
+
29
+ }
30
+ */
31
+
32
+
33
+
34
+
35
+ import { Injectable } from '@angular/core';
36
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
37
+ import { Observable } from 'rxjs';
38
+
39
+ @Injectable({
40
+ providedIn: 'root'
41
+ })
42
+ export class ApiService {
43
+ private baseUrl = 'http://localhost:5012';
44
+ //private apiUrl = 'http://localhost:5012/explain-grammar';
45
+ constructor(private http: HttpClient) { }
46
+
47
+
48
+ //askQuestion(userInput: string, sessionId: string | null): Observable<any> {
49
+ // const headers = { 'Content-Type': 'application/json' };
50
+
51
+ // // Include `session_id` in request only if available
52
+ // const data = sessionId
53
+ // ? { topic: userInput, session_id: sessionId }
54
+ // : { topic: userInput };
55
+
56
+ // console.log("Sending request to Flask:", this.apiUrl, data);
57
+
58
+ // return this.http.post<any>(this.apiUrl, data, { headers });
59
+ //}
60
+
61
+ // Fetch grammar question suggestions
62
+
63
+
64
+ //getGrammarSuggestions(input: string): Observable<any> {
65
+ // const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
66
+
67
+ // return this.http.post<any>(this.apiUrl, { topic: input }, { headers }); // ✅ Fix: Send input as "topic"
68
+ //}
69
+
70
+ askQuestion(userInput: string, sessionId: string | null): Observable<any> {
71
+ const headers = { 'Content-Type': 'application/json' };
72
+ const data = sessionId ? { topic: userInput, session_id: sessionId } : { topic: userInput };
73
+
74
+ return this.http.post<any>(`${this.baseUrl}/explain-grammar`, data, { headers });
75
+ }
76
+
77
+ getGrammarSuggestions(input: string): Observable<any> {
78
+ const headers = { 'Content-Type': 'application/json' };
79
+ return this.http.post<any>(`${this.baseUrl}/suggest-grammar-questions`, { input }, { headers });
80
+ }
81
+ }
src/app/chat/chat.component.css ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Header Container Styling */
2
+ .header-container {
3
+ display: flex;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ padding: 0vw 1vw;
7
+ background-color: #03182d;
8
+ /* background-color: #c19929;*/
9
+ box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.2);
10
+ width: 100%;
11
+ position: sticky;
12
+ top: 0;
13
+ z-index: 1000;
14
+ }
15
+
16
+ .logo img {
17
+ max-width: 5vw;
18
+ height: auto;
19
+ background: #e5e7eb;
20
+ border-radius: 1vw;
21
+ margin: 0.5vw;
22
+ }
23
+
24
+ .home-btn img {
25
+ width: 5vw; /* Adjust the size of the home icon */
26
+ }
27
+
28
+ .home-btn img:hover {
29
+ transform: scale(1.1); /* Slight zoom-in effect on hover */
30
+ }
31
+
32
+ h1 {
33
+ font-size: 3vw;
34
+ color: white;
35
+ font-family: Super Cartoon;
36
+ }
37
+
38
+ @font-face {
39
+ font-family: 'Super Cartoon';
40
+ src: url('../../assets/font/Super Cartoon.ttf') format('truetype');
41
+ font-weight: normal;
42
+ font-style: normal;
43
+ font-display: swap;
44
+ }
45
+
46
+
47
+
48
+ /* Full-Screen Chat Container */
49
+ .chat-container {
50
+ display: flex;
51
+ flex-direction: column;
52
+ height: 100vh; /* Full viewport height */
53
+ width: 100%; /* Full viewport width */
54
+ /*background: linear-gradient(to bottom right, #fefefe, #f3f4f6);*/ /* Soft white-gray gradient */
55
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
56
+
57
+ }
58
+
59
+ .chat-bg {
60
+ position: absolute;
61
+ top: 0;
62
+ left: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ object-fit: fill;
66
+ z-index: -1;
67
+ opacity: 0.2;
68
+ }
69
+
70
+ /*.chat-box {
71
+ position: relative;
72
+ }*/
73
+
74
+
75
+
76
+ /* Chat Header */
77
+ .chat-header {
78
+ text-align: center;
79
+ background: linear-gradient(to right, #4ca1af, #6ac5cb); /* Modern purple-pink gradient */
80
+ color: white;
81
+ padding: 0px 0;
82
+ font-size: 2.2rem;
83
+ font-weight: bold;
84
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
85
+ flex-shrink: 0; /* Prevent shrinking */
86
+ }
87
+
88
+ /* Wrapper for Message and Profile Picture */
89
+ /*.message-wrapper {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ gap: 1vw;*/ /* Space between profile picture and message */
93
+ /*margin-bottom: 1.5vw;
94
+ }
95
+
96
+ .message-wrapper.user {
97
+ flex-direction: row-reverse;*/ /* User messages on the right */
98
+ /*text-align: right;
99
+ }*/
100
+
101
+ /* Profile Picture Styling */
102
+ /*.profile-pic {
103
+ flex-shrink: 0;
104
+ width: 4vw;*/ /* Responsive profile picture size */
105
+ /*height: 4vw;
106
+ border-radius: 50%;
107
+ overflow: hidden;
108
+ background-color: #ddd;*/ /* Fallback color */
109
+ /*}
110
+
111
+ .profile-pic img {
112
+ width: 100%;
113
+ height: 100%;
114
+ object-fit: cover;*/ /* Ensure image fits nicely */
115
+ /*}*/
116
+
117
+ /* Message Styles */
118
+ /*.message {
119
+ max-width: 50vw;*/ /* Adjust message width for smaller screens */
120
+ /*padding: 1vw 2vw;
121
+ border-radius: 2vw;
122
+ font-size: 1.2vw;
123
+ line-height: 1.5;
124
+ box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
125
+ animation: fadeIn 0.5s ease;
126
+ }
127
+
128
+ .message-content {
129
+ text-align: justify;
130
+ }*/
131
+
132
+ /* User and AI Message Colors */
133
+ /*.user .message {
134
+ align-self: flex-end;
135
+ background: #2b6296;
136
+ color: white;
137
+ }
138
+
139
+ .ai .message {
140
+ align-self: flex-start;*/
141
+ /* background: #a855f7;*/
142
+ /* background: #404d95f7;*/
143
+ /*background: #2b6296;
144
+ color: white;
145
+ }*/
146
+
147
+ /* Timestamp Styling */
148
+ /*.message-timestamp {
149
+ font-size: 1vw;
150
+ color: rgba(255, 255, 255, 0.8);
151
+ margin-top: 0.5vw;
152
+ display: block;
153
+ text-align: right;
154
+ }*/
155
+
156
+ /*.typing-indicator {
157
+ font-style: italic;
158
+ color: #888;
159
+ font-size: 1.2vw;
160
+ text-align: left;
161
+ margin: 1vw 0;
162
+ display: flex;
163
+ align-items: center;
164
+ }*/
165
+
166
+ /*.typing-indicator::after {
167
+ content: "⠿";
168
+ display: inline-block;
169
+ font-size: 1.5vw;
170
+ color: #888;
171
+ animation: typing-animation 1s infinite;
172
+ }
173
+
174
+ @keyframes typing-animation {
175
+ 0% {
176
+ opacity: 0.2;
177
+ }
178
+
179
+ 50% {
180
+ opacity: 1;
181
+ }
182
+
183
+ 100% {
184
+ opacity: 0.2;
185
+ }
186
+ }*/
187
+
188
+
189
+
190
+ /* General Flexbox Adjustments */
191
+ /*.chat-box {
192
+ padding: 2vw;
193
+ display: flex;
194
+ flex: 1;
195
+ flex-direction: column;
196
+ gap: 1vw;*/ /* Adjust spacing between messages */
197
+ /*background-color: #f9fafb;*/ /* Light background */
198
+ /*overflow-y: auto;
199
+ height: 80vh;*/ /* Maintain height of the chat box */
200
+ /*}*/
201
+
202
+
203
+ /*.chat-box {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 1vw;
207
+ overflow-y: auto;
208
+ height: calc(100vh - 120px);
209
+ padding: 2vw;*/
210
+ /*background-color: rgb(35 34 32 / 45%);*/
211
+ /* background-color: rgb(35 34 32 / 23%);*/
212
+ /*background-color: rgb(35 34 32 / 43%);
213
+ }*/
214
+
215
+ /*.chat-box {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 1vw;
219
+ overflow-y: auto;*/ /* ✅ Enables scrolling */
220
+ /*height: calc(100vh - 120px);
221
+ padding: 2vw;
222
+ background-color: rgba(35, 34, 32, 0.43);
223
+ scroll-behavior: smooth;*/ /* ✅ Smooth scrolling */
224
+ /*}*/
225
+
226
+
227
+
228
+ /*.chat-box {
229
+ display: flex;
230
+ flex-direction: column;
231
+ overflow-y: auto;*/ /* ✅ Enables scrolling */
232
+ /*height: calc(100vh - 120px);
233
+ padding: 2vw;
234
+ background-color: rgba(35, 34, 32, 0.43);
235
+ scroll-behavior: smooth;*/ /* ✅ Smooth scrolling */
236
+ /*}*/
237
+
238
+
239
+ .chat-box {
240
+ display: flex;
241
+ flex-direction: column;
242
+ overflow-y: auto;
243
+ height: calc(100vh - 180px); /* Adjust height as needed */
244
+ padding: 2vw;
245
+ background-color: rgba(35, 34, 32, 0.43);
246
+ scroll-behavior: smooth; /* Smooth scrolling */
247
+ flex-grow: 1;
248
+ padding-bottom: 90px;
249
+ }
250
+
251
+
252
+
253
+ /*.chat-box {
254
+ display: flex;
255
+ flex-direction: column;
256
+ overflow-y: auto;
257
+ height: calc(100vh - 180px);*/ /* Adjust height to create space */
258
+ /*padding: 2vw;
259
+ background-color: rgba(35, 34, 32, 0.43);
260
+ scroll-behavior: smooth;
261
+ flex-grow: 1;
262
+ overflow-y: auto;
263
+ padding-bottom: 90px;*/ /* ✅ Creates gap between chat and input field */
264
+ /*}*/
265
+
266
+
267
+
268
+ /*
269
+ .message-wrapper {
270
+ display: flex;
271
+ align-items: flex-start;
272
+ margin-bottom: 1.5vw;
273
+ }
274
+ */
275
+
276
+
277
+
278
+ .input-box {
279
+ display: flex;
280
+ gap: 10px; /* Reduce gap between elements */
281
+ padding: 8px; /* Reduce padding */
282
+ background: #03182d; /* Blue background */
283
+ /*background: #c19929;*/ /* Blue background */
284
+ box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); /* Lighter shadow for a subtle effect */
285
+ flex-shrink: 0;
286
+ width: 100%;
287
+ height: auto; /* Adjust height dynamically */
288
+ min-height: 40px; /* Ensure it doesn't shrink too much */
289
+ font-weight: bold;
290
+ }
291
+
292
+
293
+
294
+
295
+
296
+ /*.input-box textarea {
297
+ flex: 1;
298
+ height: auto;*/ /* Automatically adjust height */
299
+ /*max-height: 80px;*/ /* Limit the maximum height */
300
+ /*overflow-y: auto;*/ /* Allow scrolling if needed */
301
+ /*resize: none;*/ /* Disable manual resizing */
302
+ /*padding: 8px;*/ /* Adjust padding for proper alignment */
303
+ /*font-size: 1rem;*/ /* Font size for input text */
304
+ /*line-height: 1.2;*/ /* Adjust line height for vertical centering */
305
+ /* border: 1px solid rgba(255, 255, 255, 0.13);*/
306
+ /*border: 1px solid rgb(93 145 195);
307
+ border-radius: 8px;*/ /* Smaller border radius */
308
+ /*background: rgba(255, 255, 255, 0.04);
309
+ color: white;
310
+ outline: none;
311
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
312
+ width: 100%;
313
+ box-sizing: border-box;
314
+ }*/
315
+
316
+ .input-box textarea {
317
+ flex: 1;
318
+ height: auto; /* Automatically adjust height */
319
+ max-height: 80px; /* Limit the maximum height */
320
+ overflow-y: auto; /* Allow scrolling if needed */
321
+ resize: none; /* Disable manual resizing */
322
+ padding: 8px; /* Adjust padding for proper alignment */
323
+ font-size: 1rem; /* Font size for input text */
324
+ line-height: 1.2; /* Adjust line height for vertical centering */
325
+ border: 1px solid rgb(93, 145, 195);
326
+ border-radius: 8px; /* Smaller border radius */
327
+ background: rgba(255, 255, 255, 0.04);
328
+ color: white;
329
+ outline: none;
330
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
331
+ width: 100%;
332
+ box-sizing: border-box;
333
+ }
334
+
335
+
336
+ .input-box textarea:focus {
337
+ border-color: rgb(135, 185, 235); /* Lighter shade of original color */
338
+ box-shadow: 0 0 5px rgba(93, 145, 195, 0.6); /* Soft glow effect */
339
+ text-align: left; /* Align text to the left */
340
+ vertical-align: middle; /* This helps maintain proper alignment */
341
+ line-height: normal; /* Ensures proper vertical centering */
342
+ font-size: 1rem; /* Adjust font size if needed */
343
+ color: white;
344
+ font-weight: bold;
345
+ }
346
+
347
+
348
+ .input-box textarea::placeholder {
349
+ text-align: left; /* Align text to the left */
350
+ vertical-align: middle; /* This helps maintain proper alignment */
351
+ line-height: normal; /* Ensures proper vertical centering */
352
+ font-size: 1rem; /* Adjust font size if needed */
353
+ color: rgba(255, 255, 255, 0.5); /* Subtle color for placeholder */
354
+ }
355
+
356
+
357
+
358
+ /*.input-box button {
359
+ font-size: 1rem;*/ /* Smaller font size */
360
+ /*padding: 6px 12px;*/ /* Smaller button padding */
361
+ /*border-radius: 8px;*/ /* Match text area border radius */
362
+ /*background: #a855f7;
363
+ color: white;
364
+ cursor: pointer;
365
+ transition: all 0.3s ease;
366
+ }*/
367
+
368
+ .input-box button {
369
+ width: 55px; /* Fixed button size */
370
+ height: 55px; /* Fixed button size */
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ /*background: #a855f7;*/
375
+ background: #5d91c3;
376
+ border: none;
377
+ border-radius: 8px;
378
+ cursor: pointer;
379
+ }
380
+
381
+
382
+ .input-box button:hover {
383
+ transform: scale(1.05);
384
+ box-shadow: 0px 4px 10px rgba(14, 165, 233, 0.4); /* Subtle hover effect */
385
+ }
386
+
387
+ .button-icon {
388
+ width: 34px; /* Default size for all icons */
389
+ height: 34px;
390
+ object-fit: contain;
391
+ }
392
+
393
+
394
+
395
+ .resume-icon {
396
+ width: 40px; /* Slightly larger for balance */
397
+ height: 40px;
398
+ object-fit: contain;
399
+ }
400
+
401
+ .pause-icon {
402
+ width: 31px; /* Custom size for pause icon */
403
+ height: 31px;
404
+ object-fit: contain;
405
+ }
406
+
407
+ .suggestion-box {
408
+ position: absolute;
409
+ bottom: 100%;
410
+ width: 100%;
411
+ background: #d1dae3;
412
+ border: 1px solid rgba(0, 0, 0, 0.2);
413
+ border-radius: 8px;
414
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
415
+ color: black;
416
+ z-index: 100;
417
+ padding: 8px 0;
418
+ }
419
+
420
+ .suggestion-box ul {
421
+ list-style: none;
422
+ margin: 0;
423
+ padding: 0;
424
+ }
425
+
426
+ .suggestion-box li {
427
+ padding: 8px 12px; /* Spacing inside each suggestion */
428
+ cursor: pointer;
429
+ font-size: 1rem;
430
+ transition: background-color 0.3s ease;
431
+ color: rgb(67 65 65); /* Text color */
432
+ }
433
+
434
+ .suggestion-box li:hover {
435
+ background-color: rgba(0, 0, 0, 0.1); /* Subtle hover effect */
436
+ border-radius: 4px; /* Rounded corners for hover */
437
+ }
438
+
439
+ .input-container {
440
+ display: flex;
441
+ flex-direction: column-reverse; /* Reverse order to place suggestions above input */
442
+ position: relative;
443
+ width: 100%;
444
+ }
445
+
446
+ .input-box {
447
+ display: flex;
448
+ gap: 10px;
449
+ padding: 8px;
450
+ background: #03182d; /* Background color of the input box */
451
+ /*background: #c19929;*/ /* Background color of the input box */
452
+ box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
453
+ flex-shrink: 0;
454
+ width: 100%;
455
+ height: auto;
456
+ min-height: 40px;
457
+ }
458
+
459
+
460
+
461
+
462
+ /* Glass Transparent Listening Indicator (Fully Centered) */
463
+ .listening-box {
464
+ position: fixed;
465
+ top: 50%;
466
+ left: 50%;
467
+ transform: translate(-50%, -50%);
468
+ background: rgb(171 188 205);
469
+ padding: 45px;
470
+ border-radius: 20px;
471
+ width: 30vw;
472
+ height: auto; /* Adjust height dynamically */
473
+ min-height: 40vh; /* Ensure height remains balanced */
474
+ box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.15);
475
+ backdrop-filter: blur(20px);
476
+ -webkit-backdrop-filter: blur(20px);
477
+ animation: fadeInScale 0.3s ease-in-out;
478
+ display: flex;
479
+ flex-direction: column;
480
+ align-items: center; /* Ensure all elements stay centered */
481
+ justify-content: center;
482
+ border: 1px solid rgba(255, 255, 255, 0.2);
483
+ text-align: center; /* Center text alignment */
484
+ }
485
+
486
+ /* Microphone Image (Centered Properly) */
487
+ .microphone-image {
488
+ width: 100px; /* Slightly bigger */
489
+ height: 100px;
490
+ display: block;
491
+ margin: 0 auto 20px; /* Ensures it's centered with space below */
492
+ animation: pulse 1.5s infinite alternate ease-in-out;
493
+ }
494
+
495
+ /* Pulse Effect */
496
+ @keyframes pulse {
497
+ from {
498
+ transform: scale(1);
499
+ filter: drop-shadow(0px 0px 8px rgba(0, 0, 0, 0.4));
500
+ }
501
+
502
+ to {
503
+ transform: scale(1.2);
504
+ filter: drop-shadow(0px 0px 14px rgba(0, 0, 0, 0.7));
505
+ }
506
+ }
507
+
508
+ /* Listening Text (Fully Centered) */
509
+ .listening-box p {
510
+ font-size: 15px; /* Increased size */
511
+ color: black;
512
+ font-weight: bold;
513
+ text-align: center; /* Ensure it's aligned centrally */
514
+ margin: 10px 0; /* Adjusted margin */
515
+ text-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);
516
+ margin-top: 3vw;
517
+ }
518
+
519
+ /* Mute & Close Buttons (Evenly Spaced & Centered) */
520
+ .listening-actions {
521
+ display: flex;
522
+ justify-content: center; /* Ensure icons are centered */
523
+ gap: 70px; /* More space between icons */
524
+ margin-top: 65px;
525
+ width: 100%;
526
+ }
527
+
528
+ /* Button Icons (Increased Size) */
529
+ .mute-btn,
530
+ .close-btn {
531
+ background: rgba(255, 255, 255, 0.3);
532
+ border: none;
533
+ padding: 15px; /* Increased padding for larger buttons */
534
+ border-radius: 50%;
535
+ cursor: pointer;
536
+ transition: transform 0.2s ease-in-out, background 0.3s;
537
+ width: 75px; /* Increased button width */
538
+ height: 75px; /* Increased button height */
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ backdrop-filter: blur(12px);
543
+ -webkit-backdrop-filter: blur(12px);
544
+ }
545
+
546
+ /* Mute and Close Icon Styles (Proper Scaling) */
547
+ .mute-btn img,
548
+ .close-btn img {
549
+ width: 50px; /* Bigger icons */
550
+ height: 50px;
551
+ }
552
+
553
+ /* Hover Effects */
554
+ .mute-btn:hover {
555
+ background: rgba(255, 0, 0, 0.5); /* Red for mute */
556
+ transform: scale(1.15);
557
+ }
558
+
559
+ .close-btn:hover {
560
+ background: rgba(0, 0, 0, 0.5); /* Dark for close */
561
+ transform: scale(1.15);
562
+ }
563
+
564
+ /* Fade-in with Scale Animation */
565
+ @keyframes fadeInScale {
566
+ from {
567
+ opacity: 0;
568
+ transform: translate(-50%, -50%) scale(0.85);
569
+ }
570
+
571
+ to {
572
+ opacity: 1;
573
+ transform: translate(-50%, -50%) scale(1);
574
+ }
575
+ }
576
+
577
+ /* Centered Error Message */
578
+ /*.error-box {
579
+ position: fixed;
580
+ top: 60%;
581
+ left: 50%;
582
+ transform: translate(-50%, -50%);
583
+ background: rgba(255, 0, 0, 0.8);
584
+ color: white;
585
+ padding: 12px 20px;
586
+ border-radius: 8px;
587
+ font-size: 14px;
588
+ text-align: center;
589
+ z-index: 1000;
590
+ animation: fadeIn 0.3s ease-in-out;
591
+ }*/
592
+
593
+ .error-text {
594
+ color: black;
595
+ background: #ffccccad; /* Light red */
596
+ font-size: 2vw;
597
+ font-weight: bold;
598
+ text-align: center;
599
+ padding: 8px 12px;
600
+ border-radius: 5px;
601
+ cursor: pointer;
602
+ width: auto; /* Auto width based on text */
603
+ margin-top: 10px; /* Space below buttons */
604
+ display: inline-block; /* Single line */
605
+ white-space: nowrap; /* Prevent text wrapping */
606
+ transition: background 0.3s;
607
+ }
608
+
609
+ .error-text:hover {
610
+ background: #ffaaaa; /* Slightly darker red on hover */
611
+ }
612
+
613
+
614
+
615
+ /* Popup Overlay */
616
+ .popup-overlay {
617
+ position: fixed;
618
+ top: 0;
619
+ left: 0;
620
+ width: 100%;
621
+ height: 100%;
622
+ background: rgba(0, 0, 0, 0.5); /* Dark transparent background */
623
+ display: flex;
624
+ justify-content: center;
625
+ align-items: center;
626
+ z-index: 1000;
627
+ }
628
+
629
+ /* Popup Box */
630
+ .popup-box {
631
+ background: white;
632
+ padding: 20px;
633
+ border-radius: 8px;
634
+ width: 300px;
635
+ text-align: center;
636
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
637
+ }
638
+
639
+ /* Popup Title */
640
+ .popup-box h3 {
641
+ font-size: 18px;
642
+ margin-bottom: 10px;
643
+ }
644
+
645
+ /* Popup Text */
646
+ .popup-box p {
647
+ font-size: 14px;
648
+ color: #555;
649
+ }
650
+
651
+ /* Popup Button */
652
+ .popup-button {
653
+ background: #007bff;
654
+ color: white;
655
+ border: none;
656
+ padding: 10px 20px;
657
+ font-size: 14px;
658
+ border-radius: 5px;
659
+ cursor: pointer;
660
+ margin-top: 10px;
661
+ }
662
+
663
+ .popup-button:hover {
664
+ background: #0056b3;
665
+ }
666
+
667
+ .typing-indicator {
668
+ display: flex;
669
+ align-items: center;
670
+ gap: 8px;
671
+ font-style: italic;
672
+ color: #ffffff;
673
+ font-size: 1.2vw;
674
+ margin-left: 4vw;
675
+ margin-top: 1vw;
676
+ background: rgba(255, 255, 255, 0.2);
677
+ padding: 0.8vw 1.5vw;
678
+ border-radius: 2vw;
679
+ width: fit-content;
680
+ animation: fadeIn 0.3s ease-in-out;
681
+ }
682
+
683
+ /* Typing dots animation */
684
+ .typing-indicator span {
685
+ width: 10px;
686
+ height: 10px;
687
+ background-color: #ffffff;
688
+ border-radius: 50%;
689
+ display: inline-block;
690
+ animation: typingDots 1.5s infinite ease-in-out;
691
+ }
692
+
693
+ /* Add animation delays for each dot */
694
+ .typing-indicator span:nth-child(1) {
695
+ animation-delay: 0s;
696
+ }
697
+
698
+ .typing-indicator span:nth-child(2) {
699
+ animation-delay: 0.2s;
700
+ }
701
+
702
+ .typing-indicator span:nth-child(3) {
703
+ animation-delay: 0.4s;
704
+ }
705
+
706
+ /* Keyframes for the typing dots */
707
+ @keyframes typingDots {
708
+ 0%, 100% {
709
+ transform: scale(0.8);
710
+ opacity: 0.3;
711
+ }
712
+
713
+ 50% {
714
+ transform: scale(1);
715
+ opacity: 1;
716
+ }
717
+ }
718
+
719
+ /* Smooth fade-in effect */
720
+ @keyframes fadeIn {
721
+ from {
722
+ opacity: 0;
723
+ transform: translateY(5px);
724
+ }
725
+
726
+ to {
727
+ opacity: 1;
728
+ transform: translateY(0);
729
+ }
730
+ }
731
+
732
+
733
+
734
+
735
+ /*
736
+ Oviya - Hardcoded*/
737
+ /*.hardcoded-questions {
738
+ position: absolute;
739
+ background: white;
740
+ border: 1px solid #ccc;
741
+ border-radius: 5px;
742
+ padding: 10px;
743
+ width: 90%;
744
+ max-width: 400px;
745
+ box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
746
+ margin-top: 5px;
747
+ z-index: 1000;
748
+ }
749
+
750
+ .hardcoded-questions ul {
751
+ list-style: none;
752
+ padding: 0;
753
+ margin: 0;
754
+ }
755
+
756
+ .hardcoded-questions li {
757
+ padding: 8px;
758
+ cursor: pointer;
759
+ transition: background 0.3s;
760
+ }
761
+
762
+ .hardcoded-questions li:hover {
763
+ background: #f0f0f0;
764
+ }*/
765
+
766
+
767
+ /* Container for hardcoded questions - Aligned left above the text field */
768
+ .hardcoded-questions-container {
769
+ display: flex;
770
+ justify-content: flex-start; /* Align to the left */
771
+ gap: 15px; /* Space between tabs */
772
+ padding: 10px;
773
+ background: rgba(255, 255, 255, 0.1); /* Subtle background */
774
+ border-radius: 8px;
775
+ margin-bottom: 5px; /* Space between questions and input box */
776
+ flex-wrap: wrap; /* Allow wrapping on smaller screens */
777
+ position: absolute;
778
+ bottom: 60px; /* Adjust based on text field height */
779
+ left: 10px; /* Align to the left side */
780
+ width: auto; /* Adjust width dynamically */
781
+ }
782
+
783
+ /* Individual question tab */
784
+ .hardcoded-question {
785
+ padding: 10px 15px;
786
+ background: rgba(200, 200, 200, 0.3); /* Light grey faded color */
787
+ border-radius: 5px;
788
+ font-size: 14px;
789
+ font-weight: bold;
790
+ cursor: pointer;
791
+ transition: background 0.3s ease, transform 0.2s ease;
792
+ white-space: nowrap; /* Prevent wrapping */
793
+ border: 1px solid rgba(0, 0, 0, 0.2);
794
+ }
795
+
796
+ /* Hover effect */
797
+ .hardcoded-question:hover {
798
+ background: rgba(180, 180, 180, 0.5); /* Slightly darker grey on hover */
799
+ transform: scale(1.05); /* Subtle pop effect */
800
+ }
801
+
802
+ /* Ensure responsiveness */
803
+ @media (max-width: 600px) {
804
+ .hardcoded-questions-container {
805
+ width: 90%;
806
+ bottom: 80px; /* Adjust spacing for smaller screens */
807
+ left: 10px; /* Keep it left aligned */
808
+ }
809
+
810
+ .hardcoded-question {
811
+ font-size: 12px;
812
+ padding: 8px 12px;
813
+ }
814
+ }
815
+
816
+
817
+ /*.paragraph-block {
818
+ background: #2b6296;
819
+ color: white;
820
+ padding: 1vw;
821
+ border-radius: 1vw;
822
+ margin-bottom: 0.3vw;*/ /* Reduce space between paragraphs */
823
+ /*box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
824
+ line-height: 1.5;
825
+ max-width: 48vw;
826
+ }*/
827
+
828
+
829
+
830
+ /* Message wrapper for each chat bubble */
831
+ .message-wrapper {
832
+ display: flex;
833
+ align-items: flex-start;
834
+ gap: 1vw;
835
+ margin-bottom: 1vw; /* Space between each message */
836
+ }
837
+
838
+ /* Separate AI and user styles */
839
+ .message-wrapper.user {
840
+ flex-direction: row-reverse;
841
+ text-align: right;
842
+ }
843
+
844
+ .message-wrapper.ai {
845
+ flex-direction: row;
846
+ text-align: left;
847
+ }
848
+
849
+ /* Profile Picture Styling */
850
+ .profile-pic {
851
+ width: 4vw;
852
+ height: 4vw;
853
+ border-radius: 50%;
854
+ overflow: hidden;
855
+ background-color: #ddd;
856
+ }
857
+
858
+ .profile-pic img {
859
+ width: 100%;
860
+ height: 100%;
861
+ object-fit: cover;
862
+ }
863
+
864
+ /* Individual Message Styling */
865
+ /*.message {
866
+ max-width: 50vw;
867
+ padding: 1vw 2vw;
868
+ border-radius: 2vw;
869
+ font-size: 1.2vw;
870
+ line-height: 1.5;
871
+ box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
872
+ background: #2b6296;
873
+ color: white;
874
+ }*/
875
+
876
+ /* Separate paragraph blocks */
877
+ .ai .message {
878
+ align-self: flex-start;
879
+ background: #2b6296;
880
+ color: white;
881
+ }
882
+
883
+ .user .message {
884
+ align-self: flex-end;
885
+ background: #2b6296;
886
+ color: white;
887
+ }
888
+
889
+ /* Timestamp Styling */
890
+ .message-timestamp {
891
+ font-size: 0.8vw;
892
+ color: rgba(255, 255, 255, 0.8);
893
+ margin-top: 0.5vw;
894
+ display: block;
895
+ text-align: right;
896
+ }
897
+
898
+
899
+
900
+ .paragraph-block {
901
+ background: #2b6296;
902
+ color: white;
903
+ padding: 1vw;
904
+ border-radius: 1vw;
905
+ margin-bottom: 0.5vw; /* ✅ Adjusted for better spacing */
906
+ box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
907
+ line-height: 1.5;
908
+ max-width: 48vw;
909
+ }
910
+
911
+ /* Normal messages remain in a single block */
912
+ .message {
913
+ max-width: 50vw;
914
+ padding: 1vw 2vw;
915
+ border-radius: 2vw;
916
+ font-size: 1.2vw;
917
+ line-height: 1.5;
918
+ box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
919
+ background: #2b6296;
920
+ color: white;
921
+ }
922
+
923
+
924
+
925
+ .structured-response {
926
+ background: #2b6296;
927
+ color: white;
928
+ padding: 1vw;
929
+ border-radius: 1vw;
930
+ margin-bottom: 0.3vw; /* ✅ Reduced spacing */
931
+ box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
932
+ line-height: 1.4; /* ✅ Adjusted line height */
933
+ max-width: 48vw;
934
+ word-break: break-word;
935
+ }
936
+
937
+ /* ✅ Reduce space between numbered and bulleted list items */
938
+ .structured-response b {
939
+ display: inline-block;
940
+ margin-top: 0.3vw; /* ✅ Reduced margin for numbers */
941
+ }
942
+
943
+ .structured-response br {
944
+ display: block;
945
+ content: "";
946
+ margin: 0.2vw 0; /* ✅ Reduce line spacing */
947
+ }
948
+
949
+ /* ✅ Ensure numbered and bulleted lists have proper spacing */
950
+ .structured-response ul,
951
+ .structured-response ol {
952
+ padding-left: 1.2vw; /* ✅ Slightly reduced indentation */
953
+ margin-bottom: 0; /* ✅ Remove extra bottom margin */
954
+ }
955
+
956
+ .structured-response ul li,
957
+ .structured-response ol li {
958
+ margin-bottom: 0.2vw; /* ✅ Reduced space between list items */
959
+ line-height: 1.4;
960
+ }
src/app/chat/chat.component.html ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+ <div class="chat-container">
5
+ <div class="header-container">
6
+ <div class="logo">
7
+ <a (click)="goToHome()" routerLink="/home">
8
+ <img src="assets/images/pykara-logo.png" alt="Pykara Logo" />
9
+ </a>
10
+ </div>
11
+ <div class="header-title">
12
+ <h1>Grammar Chat</h1>
13
+ </div>
14
+ <div class="home-btn">
15
+ <a (click)="goToHome()" routerLink="/home">
16
+ <img src="assets/images/home.png" alt="Home" class="home-icon" />
17
+ </a>
18
+ </div>
19
+ </div>
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+ <div class="chat-box" #chatBox>
28
+ <img src="assets/images/chat/chatbg.png" alt="Chat Background" class="chat-bg" />
29
+
30
+ <div *ngFor="let message of messages">
31
+ <!-- User Messages -->
32
+ <div *ngIf="message.from === 'user'" class="message-wrapper user">
33
+ <div class="profile-pic">
34
+ <img src="assets/images/chat/rabbit.png" alt="User Profile Picture" />
35
+ </div>
36
+ <div class="message">
37
+ {{ message.text }}
38
+ <div class="message-timestamp">{{ message.timestamp }}</div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- AI Messages -->
43
+ <div *ngIf="message.from === 'ai'" class="message-wrapper ai">
44
+ <div class="profile-pic">
45
+ <img src="assets/images/chat/lion.png" alt="AI Profile Picture" />
46
+ </div>
47
+ <div class="message structured-response">
48
+ <div [innerHTML]="formatStructuredResponse(message.text)"></div>
49
+ <div class="message-timestamp">{{ message.timestamp }}</div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- AI typing indicator -->
55
+ <div *ngIf="isTyping" class="typing-indicator">
56
+ AI is typing
57
+ <span></span>
58
+ <span></span>
59
+ <span></span>
60
+ </div>
61
+ </div>
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+ <div class="input-container">
70
+ <div class="input-box">
71
+ <textarea [(ngModel)]="userInput"
72
+ (focus)="showHardcodedQuestions()"
73
+ (blur)="hideHardcodedQuestions()"
74
+ (input)="adjustTextareaHeight($event); getSuggestions()"
75
+ (keydown)="handleEnterPress($event)"
76
+ placeholder="Type your message here..."
77
+ [disabled]="isSpeaking">
78
+ </textarea>
79
+
80
+ <button (click)="isSpeaking ? (isAudioPaused ? resumeAudio() : pauseAudio()) : handleButtonClick()">
81
+ <img [src]="isSpeaking ? (isAudioPaused ? 'assets/images/chat/resume-icon.png' : 'assets/images/chat/pause-icon.png') : getButtonIcon()"
82
+ alt="Button Icon"
83
+ class="button-icon" />
84
+ </button>
85
+ </div>
86
+
87
+ <!-- Hardcoded questions (Shown only when focused) -->
88
+ <div class="hardcoded-questions-container" *ngIf="showQuestions">
89
+ <div class="hardcoded-question" (click)="selectHardcodedQuestion('What is grammar?')">What is grammar?</div>
90
+ <div class="hardcoded-question" (click)="selectHardcodedQuestion('What are the rules to be followed in grammar?')">What are the rules to be followed in grammar?</div>
91
+ <div class="hardcoded-question" (click)="selectHardcodedQuestion('What are the types of tenses?')">What are the types of tenses?</div>
92
+ <div class="hardcoded-question" (click)="selectHardcodedQuestion('Why do we need to follow grammar rules while writing and speaking?')">Why do we need to follow grammar rules while writing and speaking?</div>
93
+ <div class="hardcoded-question" (click)="selectHardcodedQuestion('How do you identify a subject and a predicate in a sentence?')">How do you identify a subject and a predicate in a sentence?</div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Listening Box -->
98
+ <div class="listening-box" *ngIf="isListening">
99
+ <div class="listening-content">
100
+ <img src="assets/images/chat/microphone-icon.png" alt="Microphone" class="microphone-image" />
101
+ <p *ngIf="!errorMessage">Listening...</p>
102
+
103
+ <div class="listening-actions">
104
+ <button class="mute-btn" (click)="muteMicrophone()">
105
+ <img src="assets/images/chat/mic.png" alt="Mute" />
106
+ </button>
107
+ <button class="close-btn" (click)="stopListening()">
108
+ <img src="assets/images/chat/cross.png" alt="Close" />
109
+ </button>
110
+ </div>
111
+
112
+ <p *ngIf="errorMessage" class="error-text" (click)="openMicrophoneSettings()">
113
+ {{ errorMessage }}
114
+ </p>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Microphone Access Popup -->
119
+ <div class="popup-overlay" *ngIf="showMicPopup">
120
+ <div class="popup-box">
121
+ <h3>Microphone access required</h3>
122
+ <p>To use voice mode, you'll need to enable your microphone and try again.</p>
123
+ <button class="popup-button" (click)="closeMicrophonePopup()">OK</button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
src/app/chat/chat.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { ChatComponent } from './chat.component';
4
+
5
+ describe('ChatComponent', () => {
6
+ let component: ChatComponent;
7
+ let fixture: ComponentFixture<ChatComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [ChatComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(ChatComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/chat/chat.component.ts ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, Inject, OnDestroy, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
2
+ import { ApiService } from './api.service'; // Import ApiService
3
+ import { FormsModule } from '@angular/forms'; // Import FormsModule for two-way binding
4
+ import { CommonModule } from '@angular/common';
5
+ import { Router, RouterModule } from '@angular/router';
6
+ import { isPlatformBrowser } from '@angular/common';
7
+ import { ViewChild, ElementRef } from '@angular/core';
8
+ import { Renderer2 } from '@angular/core';
9
+
10
+
11
+
12
+
13
+ @Component({
14
+ selector: 'app-chat',
15
+ standalone: true,
16
+ imports: [FormsModule, CommonModule, RouterModule],
17
+ templateUrl: './chat.component.html',
18
+ styleUrl: './chat.component.css'
19
+ })
20
+ export class ChatComponent implements OnDestroy {
21
+ showQuestions: boolean = false; // Flag to show hardcoded questions
22
+ userInput: string = '';
23
+ messages: { from: string, text: string, timestamp: string; isPlaying?: boolean }[] = [];
24
+ isTyping: boolean = false; // Typing indicator
25
+ @ViewChild('chatBox') chatBox!: ElementRef;
26
+
27
+ isLoadingSpeech: boolean = false; // ✅ Add this line to fix the error
28
+ selectedVoice: SpeechSynthesisVoice | null = null;
29
+ errorMessage: string = "";
30
+ recognition: any; // SpeechRecognition object
31
+ speechSynthesisInstance: SpeechSynthesisUtterance | null = null;
32
+ isListening: boolean = false;
33
+ isProcessingSpeech: boolean = false; // Flag to prevent duplicate processing
34
+ // isVoiceInput: boolean = false; // Indicates if the input is from voice
35
+ isSpeaking: boolean = false; // Track if text-to-speech is ongoing
36
+ isAudioPaused: boolean = false; // Indicates if the audio is paused
37
+ isInputValid: boolean = false;
38
+ suggestions: string[] = []; // Holds suggested grammar questions
39
+ showMicPopup: boolean = false;
40
+
41
+
42
+ ngAfterViewChecked() {
43
+ // Adding a delay for smooth scrolling
44
+ setTimeout(() => {
45
+ this.scrollToBottom();
46
+ }, 100); // Adjust the timeout if necessary
47
+ }
48
+
49
+ // Function to scroll to the bottom of the chat
50
+ private scrollToBottom(): void {
51
+ try {
52
+ this.chatBox.nativeElement.scrollTop = this.chatBox.nativeElement.scrollHeight;
53
+ } catch (err) { }
54
+ }
55
+
56
+ constructor(private apiService: ApiService, private cdr: ChangeDetectorRef, @Inject(PLATFORM_ID,) private platformId: object, private router: Router, private renderer: Renderer2) {
57
+
58
+ window.speechSynthesis.onvoiceschanged = () => {
59
+ console.log("Available Voices:", window.speechSynthesis.getVoices());
60
+ };
61
+
62
+ // Initialize SpeechRecognition if supported
63
+ if (isPlatformBrowser(this.platformId)) {
64
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
65
+ if (SpeechRecognition) {
66
+ this.recognition = new SpeechRecognition();
67
+ this.recognition.continuous = false;
68
+ this.recognition.lang = 'en-US';
69
+ this.recognition.interimResults = false;
70
+
71
+ this.recognition.onresult = (event: any) => {
72
+ if (event.results && event.results[0]) {
73
+ const transcript = event.results[0][0].transcript.trim();
74
+ console.log('Recognized speech:', transcript);
75
+
76
+ this.userInput = transcript; // Set the recognized speech as input
77
+ this.sendMessage(); // Automatically send the message after recognition
78
+
79
+ // Stop recognition to prevent multiple triggers
80
+ this.recognition.stop();
81
+ this.isListening = false; // Hide the overlay
82
+ }
83
+ };
84
+
85
+ this.recognition.onerror = (event: any) => {
86
+ console.error('Speech Recognition Error:', event.error);
87
+ this.isProcessingSpeech = false; // Reset the flag on error
88
+ };
89
+ } else {
90
+ console.warn('Speech Recognition is not supported in this browser.');
91
+ }
92
+ // Add beforeunload listener to stop audio on page refresh or unload
93
+ window.addEventListener('beforeunload', this.handleUnload);
94
+ }
95
+
96
+ }
97
+
98
+ // Handle page unload to stop audio
99
+ private handleUnload = (): void => {
100
+ if (window.speechSynthesis) {
101
+ window.speechSynthesis.cancel();
102
+ }
103
+ };
104
+
105
+ // Lifecycle hook to clean up resources
106
+ ngOnDestroy(): void {
107
+ if (isPlatformBrowser(this.platformId)) {
108
+ if (window.speechSynthesis) {
109
+ window.speechSynthesis.cancel();
110
+ }
111
+ // Remove the unload event listener
112
+ window.removeEventListener('beforeunload', this.handleUnload);
113
+ }
114
+ }
115
+
116
+ openMicrophonePopup(): void {
117
+ this.showMicPopup = true;
118
+ }
119
+
120
+ closeMicrophonePopup(): void {
121
+ this.showMicPopup = false;
122
+ }
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+ // Show hardcoded questions when user focuses on input
131
+ showHardcodedQuestions(): void {
132
+ setTimeout(() => {
133
+ this.showQuestions = true;
134
+ }, 100); // Small delay to prevent blur from closing it immediately
135
+ }
136
+
137
+ // Hide hardcoded questions when user clicks outside
138
+ hideHardcodedQuestions(): void {
139
+ setTimeout(() => {
140
+ this.showQuestions = false;
141
+ }, 200); // Allow time for click event to register
142
+ }
143
+
144
+
145
+ selectHardcodedQuestion(question: string): void {
146
+ this.userInput = question; // Set the selected question
147
+ this.showQuestions = false; // Hide the hardcoded questions
148
+ setTimeout(() => {
149
+ this.sendMessage(); // Send the selected question
150
+ this.userInput = ''; // Clear the text field after sending
151
+ }, 100); // Small delay to ensure UI updates before clearing
152
+ }
153
+
154
+
155
+
156
+ getSuggestions(): void {
157
+ if (!this.userInput || this.userInput.trim().length < 1 || this.isSpeaking) {
158
+ this.suggestions = []; // Clear suggestions if no input or AI is speaking
159
+ return;
160
+ }
161
+
162
+ this.apiService.getGrammarSuggestions(this.userInput).subscribe(
163
+ (response) => {
164
+ console.log("API Response:", response);
165
+ if (response.suggestions) {
166
+ this.suggestions = response.suggestions
167
+ .filter((s: string) => s && s.trim().length > 0) // Remove empty suggestions
168
+ .map((s: string) => s.replace(/^\d+\.\s*/, "")); // Remove numbering
169
+ } else {
170
+ this.suggestions = [];
171
+ }
172
+ },
173
+ (error) => {
174
+ console.error("Error fetching suggestions:", error);
175
+ this.suggestions = [];
176
+ }
177
+ );
178
+ }
179
+
180
+
181
+
182
+
183
+ // When a user selects a suggestion, send it as a message
184
+ selectSuggestion(suggestion: string): void {
185
+ this.userInput = suggestion;
186
+ this.suggestions = []; // Clear suggestions after selection
187
+ this.sendMessage(); // Send the selected suggestion
188
+ }
189
+
190
+
191
+
192
+
193
+ sendMessage(inputText?: string): void {
194
+ const message = inputText ? inputText.trim() : this.userInput.trim();
195
+ if (!message) {
196
+ return;
197
+ }
198
+
199
+ let sessionId = localStorage.getItem('session_id');
200
+
201
+ // ✅ Add user message to chat
202
+ this.messages.push({ from: 'user', text: message, timestamp: new Date().toLocaleTimeString() });
203
+ this.userInput = ''; // ✅ Clear input field after sending
204
+ this.isTyping = true;
205
+ this.cdr.detectChanges();
206
+ this.scrollToBottom(); // ✅ Scroll after adding user message
207
+
208
+ this.apiService.askQuestion(message, sessionId).subscribe(
209
+ (response) => {
210
+ this.isTyping = false;
211
+ let explanation = response?.response || 'No explanation available.';
212
+
213
+ if (response.session_id && !sessionId) {
214
+ localStorage.setItem('session_id', response.session_id);
215
+ }
216
+
217
+ let formattedExplanation = explanation.trim().split('\n').map((para: string) => para.trim()).join('\n');
218
+
219
+ this.messages.push({ from: 'ai', text: formattedExplanation, timestamp: new Date().toLocaleTimeString() });
220
+ this.cdr.detectChanges();
221
+ this.scrollToBottom(); // ✅ Scroll after AI response
222
+ this.speakResponse(explanation);
223
+ },
224
+ (error) => {
225
+ this.isTyping = false;
226
+ const errorMessage = 'Error: Could not get a response from the server.';
227
+ console.error("API Error:", error);
228
+ this.messages.push({ from: 'ai', text: errorMessage, timestamp: new Date().toLocaleTimeString() });
229
+ this.cdr.detectChanges();
230
+ this.scrollToBottom(); // ✅ Scroll even if an error occurs
231
+ this.speakResponse(errorMessage);
232
+ }
233
+ );
234
+ }
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+ formatStructuredResponse(text: string): string {
244
+ let formattedText = text
245
+ .replace(/\n/g, '<br>') // Preserve line breaks
246
+ .replace(/(\d+)\.\s/g, '<b>$1.</b> ') // Bold numbered lists
247
+ .replace(/\•\s/g, '✔️ ') // Replace bullets with custom emojis
248
+ .replace(/\-\s/g, '🔹 ') // Replace hyphen bullets with emoji
249
+ .replace(/(\*\*)(.*?)\1/g, '<b>$2</b>'); // Convert "**bold text**" to <b>bold text</b>
250
+
251
+ return formattedText;
252
+ }
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+ speakResponse(responseText: string): void {
266
+ if (!responseText) {
267
+ console.warn('No response text provided for speech.');
268
+ return;
269
+ }
270
+
271
+ console.log('Initiating text-to-speech with response:', responseText);
272
+
273
+ // Find the last AI message and update it dynamically
274
+ let lastAiMessage = this.messages.slice().reverse().find((msg) => msg.from === 'ai');
275
+
276
+ if (!lastAiMessage) {
277
+ // If no AI message exists, create one
278
+ lastAiMessage = { from: 'ai', text: '', timestamp: new Date().toLocaleTimeString() };
279
+ this.messages.push(lastAiMessage);
280
+ } else {
281
+ // Clear existing text before speaking
282
+ lastAiMessage.text = '';
283
+ }
284
+
285
+ this.cdr.detectChanges(); // Ensure UI updates immediately
286
+
287
+ // Split response text into words for live updates
288
+ const words = responseText.split(' ');
289
+ let currentWordIndex = 0;
290
+
291
+ // Create SpeechSynthesisUtterance
292
+ const speech = new SpeechSynthesisUtterance();
293
+ speech.text = responseText;
294
+ speech.lang = 'en-US';
295
+ speech.pitch = 1; // Adjust pitch (higher values sound more feminine)
296
+ speech.rate = 1; // Normal speaking speed
297
+ this.isSpeaking = true;
298
+
299
+ // Fetch available voices
300
+ const voices = window.speechSynthesis.getVoices();
301
+
302
+ // ✅ Use Microsoft Zira for a female voice
303
+ let femaleVoice = voices.find(voice => voice.name === "Microsoft Zira - English (United States)");
304
+
305
+ if (femaleVoice) {
306
+ speech.voice = femaleVoice;
307
+ console.log("Using voice:", femaleVoice.name);
308
+ } else {
309
+ console.warn("Microsoft Zira not found, using default.");
310
+ }
311
+
312
+ // Update AI response dynamically as words are spoken
313
+ speech.onboundary = (event) => {
314
+ if (event.name === 'word' && currentWordIndex < words.length) {
315
+ lastAiMessage!.text = words.slice(0, currentWordIndex + 1).join(' '); // Update text word by word
316
+ currentWordIndex++;
317
+ this.cdr.detectChanges(); // Update UI dynamically
318
+ }
319
+ };
320
+
321
+ // When speech ends, keep the full response or leave the last word
322
+ speech.onend = () => {
323
+ console.log('Speech ended.');
324
+ this.isSpeaking = false;
325
+ lastAiMessage!.text = responseText; // Show full response after speech ends
326
+ this.cdr.detectChanges();
327
+ };
328
+
329
+ // Start speaking
330
+ console.log('Starting speech synthesis...');
331
+ window.speechSynthesis.speak(speech);
332
+ }
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+ ngOnInit(): void {
348
+ if (window.speechSynthesis.onvoiceschanged !== undefined) {
349
+ window.speechSynthesis.onvoiceschanged = () => {
350
+ this.loadVoices();
351
+ };
352
+ }
353
+
354
+ // Load voices immediately in case they are already available
355
+ this.loadVoices();
356
+ }
357
+
358
+
359
+
360
+ loadVoices(): void {
361
+ const voices = window.speechSynthesis.getVoices();
362
+
363
+ if (!voices.length) {
364
+ console.warn("No voices available yet, retrying...");
365
+ setTimeout(() => this.loadVoices(), 500); // Retry loading voices
366
+ return;
367
+ }
368
+
369
+ console.log("Available Voices:", voices.map(v => v.name)); // Debugging
370
+
371
+ // ✅ Preferred female voices
372
+ const preferredVoices = [
373
+ "Google UK English Female",
374
+ "Google US English Female",
375
+ "Microsoft Zira - English (United States)", // Edge/Windows
376
+ "Microsoft Hazel - English (United Kingdom)",
377
+ "Google en-GB Female",
378
+ "Google en-US Female"
379
+ ];
380
+
381
+ // Try to find a preferred female voice
382
+ for (let voiceName of preferredVoices) {
383
+ const foundVoice = voices.find(voice => voice.name === voiceName);
384
+ if (foundVoice) {
385
+ this.selectedVoice = foundVoice;
386
+ break;
387
+ }
388
+ }
389
+
390
+ // If no preferred female voice found, pick any available female voice
391
+ if (!this.selectedVoice) {
392
+ this.selectedVoice = voices.find(voice => voice.name.toLowerCase().includes("female")) || voices[0];
393
+ }
394
+
395
+ console.log("Selected AI Voice:", this.selectedVoice?.name);
396
+ }
397
+
398
+
399
+
400
+
401
+ pauseAudio(): void {
402
+ if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
403
+ window.speechSynthesis.pause();
404
+ this.isAudioPaused = true;
405
+ console.log('AI Speech Paused');
406
+ this.cdr.detectChanges(); // Update UI
407
+ }
408
+ }
409
+
410
+ resumeAudio(): void {
411
+ if (window.speechSynthesis.paused) {
412
+ window.speechSynthesis.resume();
413
+ this.isAudioPaused = false;
414
+ console.log('AI Speech Resumed');
415
+ this.cdr.detectChanges(); // Update UI
416
+ }
417
+ }
418
+
419
+
420
+
421
+
422
+ muteMicrophone(): void {
423
+ console.log("Microphone muted");
424
+ // Logic to mute the microphone
425
+ }
426
+
427
+
428
+
429
+ // Start listening for voice input
430
+ startListening(): void {
431
+ this.isListening = true; // Show the overlay
432
+ this.isProcessingSpeech = false; // Reset the flag before starting
433
+
434
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
435
+ navigator.mediaDevices
436
+ .getUserMedia({ audio: true })
437
+ .then(() => {
438
+ if (this.recognition) {
439
+ console.log('Starting speech recognition...');
440
+ this.recognition.start();
441
+
442
+ // Debugging events
443
+ this.recognition.onaudiostart = () => console.log('Audio capturing started.');
444
+ this.recognition.onspeechstart = () => console.log('Speech has been detected.');
445
+ this.recognition.onspeechend = () => console.log('Speech ended, processing...');
446
+ this.recognition.onaudioend = () => console.log('Audio capturing ended.');
447
+
448
+ // Automatically process recognized speech
449
+ this.recognition.onresult = (event: any) => {
450
+ if (event.results && event.results[0]) {
451
+ const transcript = event.results[0][0].transcript.trim();
452
+ console.log('Recognized speech:', transcript);
453
+
454
+ // Set the recognized speech as input
455
+ this.userInput = transcript;
456
+
457
+ // Automatically send the message
458
+ if (this.userInput.trim()) {
459
+ console.log('Sending question automatically:', this.userInput);
460
+ this.sendMessage(); // Call sendMessage to process the question
461
+ }
462
+
463
+ // Stop recognition to prevent multiple triggers
464
+ this.recognition.stop();
465
+ this.isListening = false; // Hide the overlay
466
+ }
467
+ };
468
+
469
+ this.recognition.onnomatch = () =>
470
+ alert('No speech detected. Please try again.');
471
+ this.recognition.onend = () => {
472
+ console.log('Speech recognition service disconnected.');
473
+ this.isListening = false;
474
+ };
475
+ this.recognition.onerror = (error: any) => {
476
+ console.error('Speech Recognition Error:', error);
477
+ this.isListening = false;
478
+ if (error.error === 'not-allowed') {
479
+ alert('Microphone permission denied.');
480
+ } else if (error.error === 'no-speech') {
481
+ alert('No speech detected. Please try speaking clearly.');
482
+ }
483
+ };
484
+ } else {
485
+ alert('Speech Recognition is not supported in this browser.');
486
+ }
487
+ })
488
+ .catch((error) => {
489
+ console.error('Microphone access denied:', error);
490
+ this.errorMessage = 'Please enable microphone access to use this feature.';
491
+ this.isListening = true; // Keep the overlay visible
492
+ });
493
+
494
+ } else {
495
+ alert('Microphone access is not supported in this browser.');
496
+ }
497
+ }
498
+
499
+ stopListening(): void {
500
+ this.isListening = false; // Hide the overlay
501
+ if (this.recognition) {
502
+ this.recognition.stop(); // Stop speech recognition
503
+ }
504
+ }
505
+
506
+
507
+ // Toggle between play and pause for a specific message
508
+ toggleAudio(message: { text: string, isPlaying?: boolean }): void {
509
+ if (this.speechSynthesisInstance && this.speechSynthesisInstance.text === message.text) {
510
+ if (message.isPlaying) {
511
+ // Pause the currently playing audio
512
+ window.speechSynthesis.pause();
513
+ message.isPlaying = false;
514
+ } else {
515
+ // Resume the paused audio
516
+ window.speechSynthesis.resume();
517
+ message.isPlaying = true;
518
+ }
519
+ } else {
520
+ // Stop any currently playing audio
521
+ if (this.speechSynthesisInstance) {
522
+ window.speechSynthesis.cancel();
523
+ }
524
+ this.messages.forEach((msg) => (msg.isPlaying = false)); // Reset all states
525
+
526
+ // Start playback for the selected message
527
+ message.isPlaying = true;
528
+ this.speechSynthesisInstance = new SpeechSynthesisUtterance(message.text);
529
+ this.speechSynthesisInstance.lang = 'en-US';
530
+ this.speechSynthesisInstance.pitch = 1;
531
+ this.speechSynthesisInstance.rate = 1;
532
+
533
+ // When playback ends, reset the state
534
+ this.speechSynthesisInstance.onend = () => {
535
+ message.isPlaying = false;
536
+ this.speechSynthesisInstance = null;
537
+ };
538
+
539
+ // Play the audio
540
+ window.speechSynthesis.speak(this.speechSynthesisInstance);
541
+ }
542
+ }
543
+
544
+
545
+
546
+ goToHome() {
547
+ this.router.navigate(['/home']);
548
+ }
549
+
550
+ copySuccessIndex: number | null = null; // Track copied message index
551
+
552
+ copyToClipboard(text: string, index: number): void {
553
+ navigator.clipboard.writeText(text).then(() => {
554
+ this.copySuccessIndex = index; // Show tick icon for copied message
555
+ setTimeout(() => {
556
+ this.copySuccessIndex = null; // Hide tick after 2 seconds
557
+ }, 2000);
558
+ }).catch(err => {
559
+ console.error('Failed to copy: ', err);
560
+ });
561
+ }
562
+
563
+
564
+
565
+
566
+ // Function to check input validity
567
+ checkInput() {
568
+ this.isInputValid = this.userInput.trim().length > 0;
569
+ }
570
+
571
+
572
+
573
+ handleButtonClick(): void {
574
+ if (this.userInput.trim().length > 0) {
575
+ this.showQuestions = false; // Hide tabs when user manually enters text
576
+ const messageToSend = this.userInput; // Store input before clearing
577
+ this.userInput = ''; // Clear input for UI update
578
+ this.sendMessage(messageToSend); // Send the stored message
579
+ } else if (this.isSpeaking) {
580
+ this.pauseAudio();
581
+ } else if (this.isAudioPaused) {
582
+ this.resumeAudio();
583
+ } else {
584
+ this.startListening();
585
+ }
586
+ }
587
+
588
+
589
+
590
+ getButtonIcon(): string {
591
+ if (this.userInput.trim().length > 0) {
592
+ return 'assets/images/chat/send-icon.png'; // Replace with your send icon image
593
+ } else if (this.isSpeaking) {
594
+ return 'assets/images/chat/pause-icon.png'; // Replace with your pause icon image
595
+ } else if (this.isAudioPaused) {
596
+ return 'assets/images/chat/resume-icon.png'; // Replace with your resume icon image
597
+ } else {
598
+ return 'assets/images/chat/microphone-icon.png'; // Replace with your microphone icon image
599
+ }
600
+ }
601
+
602
+
603
+
604
+ addNewLine(event: KeyboardEvent): void {
605
+ if (event.key === 'Enter' && event.shiftKey) {
606
+ event.preventDefault(); // Prevent form submission
607
+ this.userInput += '\n'; // Add a new line to the textarea
608
+ }
609
+ }
610
+
611
+
612
+ adjustTextareaHeight(event: Event): void {
613
+ const textarea = event.target as HTMLTextAreaElement;
614
+ textarea.style.height = 'auto'; // Reset height to calculate new height
615
+ textarea.style.height = `${textarea.scrollHeight}px`; // Adjust height based on content
616
+ }
617
+
618
+
619
+
620
+
621
+ handleEnterPress(event: KeyboardEvent): void {
622
+ if (this.isSpeaking) {
623
+ event.preventDefault(); // Prevent typing when AI is speaking
624
+ return;
625
+ }
626
+
627
+ if (event.key === 'Enter' && !event.shiftKey) {
628
+ event.preventDefault();
629
+ this.handleButtonClick();
630
+ } else if (event.key === 'Enter' && event.shiftKey) {
631
+ event.preventDefault();
632
+ this.userInput += '\n';
633
+ }
634
+ }
635
+
636
+
637
+
638
+
639
+
640
+ getButtonIconClass(): string {
641
+ return this.userInput.trim().length > 0
642
+ ? 'send-icon'
643
+ : this.isSpeaking
644
+ ? 'pause-icon'
645
+ : this.isAudioPaused
646
+ ? 'resume-icon' // Class for resume icon
647
+ : 'microphone-icon';
648
+ }
649
+
650
+ openMicrophoneSettings(): void {
651
+ const userAgent = navigator.userAgent;
652
+
653
+ if (userAgent.includes("Chrome")) {
654
+ window.open("chrome://settings/content/microphone", "_blank");
655
+ } else if (userAgent.includes("Firefox")) {
656
+ window.open("about:preferences#privacy", "_blank");
657
+ } else if (userAgent.includes("Edge")) {
658
+ window.open("edge://settings/content/microphone", "_blank");
659
+ } else {
660
+ alert("Please check your browser's settings to enable the microphone.");
661
+ }
662
+ }
663
+
664
+
665
+
666
+
667
+ }
src/app/findword/findword.component.css ADDED
@@ -0,0 +1,1095 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* General Styles */
2
+ body {
3
+ font-family: 'Roboto', sans-serif;
4
+ background-color: #f9f9f9;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+
10
+
11
+ /* Header */
12
+ .header-container {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ align-items: center;
16
+ padding: 0vw 2vw;
17
+ background-color: #009688;
18
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
19
+ width: 100%;
20
+ position: sticky;
21
+ top: 0;
22
+ }
23
+
24
+ .logo img {
25
+ max-width: 5vw;
26
+ height: auto;
27
+ background: #fff;
28
+ border-radius: 1vw;
29
+ margin: 0.5vw;
30
+ }
31
+
32
+
33
+ .header-title {
34
+ font-size: 2.6vw;
35
+ font-family: Super Cartoon;
36
+ color: #009688;
37
+ font-size: 3vw;
38
+ color: #fff;
39
+ margin: 0;
40
+ }
41
+
42
+ .home-btn img {
43
+ width: 5vw;
44
+ }
45
+
46
+ .home-btn img:hover {
47
+ transform: scale(1.1);
48
+ }
49
+
50
+ /* Background */
51
+ .imgbgcontainter {
52
+ background-image: url(/assets/images/grammar-bg.png);
53
+ background-size: auto;
54
+ background-position: center;
55
+ background-attachment: fixed;
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+
60
+ .grammar-bg {
61
+ position: absolute;
62
+ top: 10%;
63
+ left: 0;
64
+ width: 100%;
65
+ height: auto;
66
+ max-height: calc(100vh - 100px);
67
+ object-fit: fill;
68
+ z-index: -1;
69
+ opacity: 0.2;
70
+ }
71
+
72
+ /* Responsive Card */
73
+ .card1 {
74
+ background: #fff;
75
+ width: 80vw;
76
+ /*max-width: 1000px;*/
77
+ margin: 4vh auto;
78
+ padding: 2vw;
79
+ border: 10px solid #009688;
80
+ border-radius: 1vw;
81
+ box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
82
+ position: absolute;
83
+ top: 50%;
84
+ left: 50%;
85
+ transform: translate(-50%, -50%);
86
+ }
87
+
88
+ .content-container {
89
+ display: flex;
90
+ flex-direction: row;
91
+ gap: 2vw;
92
+ flex-wrap: wrap;
93
+ justify-content: center;
94
+ align-items: center;
95
+ }
96
+
97
+ .image-container {
98
+ flex: 1 1 300px;
99
+ text-align: center;
100
+ }
101
+
102
+ .quiz-image {
103
+ width: 100%;
104
+ /*max-width: 350px;*/
105
+ height: auto;
106
+ border-radius: 10px;
107
+ }
108
+
109
+ .description-container {
110
+ flex: 1 1 300px;
111
+ }
112
+
113
+ h2 {
114
+ font-size: 2vw;
115
+ color: #006780;
116
+ margin-bottom: 1vw;
117
+ font-weight: 800;
118
+ }
119
+
120
+ .description-container p {
121
+ font-size: 1.2vw;
122
+ text-align: justify;
123
+ margin-bottom: 2vw;
124
+ }
125
+
126
+ .submit-button {
127
+ font-size: 1.2vw;
128
+ padding: 0.8vw 2vw;
129
+ background-color: #006780;
130
+ color: white;
131
+ border: none;
132
+ border-radius: 0.5vw;
133
+ cursor: pointer;
134
+ font-weight: bold;
135
+ }
136
+
137
+ .submit-button:hover {
138
+ background-color: #bdc3c7;
139
+ box-shadow: 0 12px 16px rgba(0,0,0,0.24), 0 17px 50px rgba(0,0,0,0.19);
140
+ }
141
+
142
+ .submit-button:disabled {
143
+ background-color: #ccc;
144
+ cursor: not-allowed;
145
+ }
146
+
147
+ /* Media Queries for Responsiveness */
148
+
149
+ @media (max-width: 768px) {
150
+ .content-container {
151
+ flex-direction: column;
152
+ }
153
+
154
+ h2 {
155
+ font-size: 5vw;
156
+ }
157
+
158
+ .description-container p {
159
+ font-size: 3.5vw;
160
+ }
161
+
162
+ .submit-button {
163
+ font-size: 3.5vw;
164
+ padding: 3vw 6vw;
165
+ height: auto;
166
+ margin-top: 2vw;
167
+ }
168
+ }
169
+
170
+ @media (max-width: 480px) {
171
+ .card1 {
172
+ padding: 5vw;
173
+ }
174
+
175
+ .submit-button {
176
+ width: 100%;
177
+ font-size: 4vw;
178
+ }
179
+ }
180
+
181
+ /* Game Screen */
182
+ .game-screen {
183
+ height: 71vh;
184
+ display: flex;
185
+ flex-direction: column;
186
+ justify-content: space-evenly;
187
+ background: #fff;
188
+ width: 85vw;
189
+ margin: 4vh auto;
190
+ padding: 2vw;
191
+ border: 10px solid #009688;
192
+ border-radius: 1vw;
193
+ box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
194
+ position: absolute;
195
+ top: 52%;
196
+ left: 50%;
197
+ transform: translate(-50%, -50%);
198
+ }
199
+
200
+ /* Game Content Layout */
201
+ .game-content {
202
+ display: flex;
203
+ justify-content: space-between;
204
+ gap: 2rem;
205
+ align-items: center;
206
+ }
207
+
208
+ /* Cards */
209
+ .audio-card, .input-card {
210
+ width: 50%;
211
+ height: 20vw;
212
+ padding: 4rem;
213
+ background-color: #deefef;
214
+ border-radius: 8px;
215
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
216
+ text-align: center;
217
+ display: flex;
218
+ flex-direction: column;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ }
222
+
223
+ /* Input Wrapper */
224
+ .input-card {
225
+ display: flex;
226
+ flex-direction: column;
227
+ justify-content: space-between;
228
+ align-items: center;
229
+ padding-bottom: 1rem;
230
+ }
231
+
232
+ .input-wrapper {
233
+ width: 100%;
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 1rem;
237
+ align-items: center;
238
+ }
239
+
240
+ .input-wrapper input {
241
+ width: 80%;
242
+ font-weight: bold;
243
+ padding: 1rem;
244
+ border-radius: 8px;
245
+ border: 2px solid #007bff;
246
+ font-size: 1.2rem;
247
+ text-align: center;
248
+ transition: border-color 0.3s;
249
+ }
250
+
251
+ .input-wrapper input:disabled {
252
+ background-color: #e0e0e0;
253
+ color: #7a7a7a;
254
+ border-color: #bdbdbd;
255
+ cursor: not-allowed;
256
+ opacity: 0.8;
257
+ }
258
+
259
+ /* Green border for correct input */
260
+ .input-wrapper input.correct {
261
+ border: 2px solid #4caf50 !important; /* green */
262
+ background-color: #e8f5e9;
263
+ }
264
+
265
+ /* Red border for incorrect input */
266
+ .input-wrapper input.error {
267
+ border: 2px solid #f44336 !important; /* red */
268
+ background-color: #ffebee;
269
+ }
270
+
271
+
272
+ .error-message {
273
+ color: #ff4d4d;
274
+ font-weight: bold;
275
+ font-size: 1.1rem;
276
+ margin-top: 0.5rem;
277
+ text-align: center;
278
+ }
279
+
280
+ /* Submit Button */
281
+ .submit-btn {
282
+ width: 85%;
283
+ background-color: #006780;
284
+ color: white;
285
+ font-size: 1rem;
286
+ padding: 1rem;
287
+ border: none;
288
+ border-radius: 8px;
289
+ cursor: pointer;
290
+ transition: background-color 0.3s;
291
+ text-align: center;
292
+ }
293
+
294
+ .submit-btn:hover {
295
+ background-color: #004d5c;
296
+ }
297
+
298
+ .submit-btn:disabled {
299
+ background-color: #cccccc;
300
+ cursor: not-allowed;
301
+ }
302
+
303
+ /* Action Buttons */
304
+ .action-buttons {
305
+ width: 100%;
306
+ display: flex;
307
+ justify-content: space-between;
308
+ margin-top: 0.5rem;
309
+ padding: 0 1rem;
310
+ align-items: center;
311
+ }
312
+
313
+ .left-buttons {
314
+ display: flex;
315
+ gap: 10px;
316
+ }
317
+
318
+ .right-button {
319
+ margin-left: auto;
320
+ }
321
+
322
+ .action-buttons .btn {
323
+ padding: 0.8rem 1.5rem;
324
+ font-size: 1rem;
325
+ border-radius: 8px;
326
+ cursor: pointer;
327
+ transition: background-color 0.3s;
328
+ }
329
+
330
+ .info-btn {
331
+ background-color: #f8c102;
332
+ color: white;
333
+ font-weight: bold;
334
+ }
335
+
336
+ .info-btn:hover {
337
+ background-color: #e6a500;
338
+ }
339
+
340
+ .next-btn {
341
+ background-color: #006780;
342
+ color: white;
343
+ font-weight: bold;
344
+ }
345
+
346
+ .next-btn:hover {
347
+ background-color: #004d5c;
348
+ }
349
+
350
+ .next-btn:disabled {
351
+ background-color: #cccccc;
352
+ cursor: not-allowed;
353
+ }
354
+
355
+ /* Generate Button Section */
356
+ .generate-buttons {
357
+ display: flex;
358
+ gap: 10px;
359
+ }
360
+
361
+ .generate-btn {
362
+ background-color: #006780;
363
+ color: white;
364
+ border: none;
365
+ padding: 15px 32px;
366
+ font-size: 1.2vw;
367
+ border-radius: 0.5vw;
368
+ cursor: pointer;
369
+ transition: background-color 0.3s, transform 0.3s;
370
+ font-weight: bold;
371
+ }
372
+
373
+ .generate-btn:disabled {
374
+ background-color: #cccccc !important;
375
+ color: #666666;
376
+ cursor: not-allowed;
377
+ opacity: 0.7;
378
+ box-shadow: none;
379
+ transition: none;
380
+ }
381
+
382
+ .extra-btn {
383
+ background-color: #ff9800;
384
+ color: white;
385
+ padding: 0.8rem 1.5rem;
386
+ font-size: 1rem;
387
+ border-radius: 8px;
388
+ cursor: pointer;
389
+ transition: background-color 0.3s;
390
+ border: none;
391
+ }
392
+
393
+ .extra-btn:hover {
394
+ background-color: #e68900;
395
+ }
396
+
397
+ /* Responsive */
398
+ @media (max-width: 768px) {
399
+ .game-content {
400
+ flex-direction: column;
401
+ align-items: center;
402
+ }
403
+
404
+ .audio-card, .input-card {
405
+ width: 90%;
406
+ height: auto;
407
+ }
408
+
409
+ .submit-btn, .action-buttons {
410
+ width: 90%;
411
+ }
412
+
413
+ .action-buttons {
414
+ flex-direction: column;
415
+ gap: 0.5rem;
416
+ }
417
+ }
418
+
419
+ /* Popup */
420
+ .popup-overlay {
421
+ position: fixed;
422
+ top: 0;
423
+ left: 0;
424
+ width: 100%;
425
+ height: 100%;
426
+ background-color: rgba(0, 0, 0, 0.5);
427
+ display: flex;
428
+ justify-content: center;
429
+ align-items: center;
430
+ z-index: 20;
431
+ }
432
+
433
+ .popup-content {
434
+ background-color: #daf5ff;
435
+ padding: 2vw;
436
+ border-radius: 8px;
437
+ text-align: center;
438
+ }
439
+
440
+ .popup-content p {
441
+ font-size: 1vw;
442
+ margin-bottom: 2vw;
443
+ color: #004d5c;
444
+ font-weight: bold;
445
+ }
446
+
447
+ @keyframes fadeInCenter {
448
+ 0% {
449
+ opacity: 0;
450
+ transform: translateY(50px);
451
+ }
452
+
453
+ 100% {
454
+ opacity: 1;
455
+ transform: translateY(0);
456
+ }
457
+ }
458
+
459
+ .close-btn {
460
+ padding: 0.7vw;
461
+ font-size: 1vw;
462
+ background-color: #009688;
463
+ color: white;
464
+ border: none;
465
+ border-radius: 8px;
466
+ cursor: pointer;
467
+ transition: background-color 0.3s;
468
+ font-weight: bold;
469
+ }
470
+
471
+ .close-btn:hover {
472
+ background-color: #00796b;
473
+ }
474
+
475
+ /* Game Header */
476
+ .game-header {
477
+ position: relative;
478
+ width: 100%;
479
+ display: flex;
480
+ justify-content: center;
481
+ align-items: center;
482
+ padding-bottom: 1rem;
483
+ }
484
+
485
+ .back-btn {
486
+ position: absolute;
487
+ left: 0;
488
+ top: 40%;
489
+ transform: translateY(-50%);
490
+ background: transparent;
491
+ border: none;
492
+ font-size: 2rem;
493
+ color: #006780;
494
+ cursor: pointer;
495
+ padding: 0 1rem;
496
+ transition: transform 0.3s;
497
+ width: 6vw;
498
+ }
499
+
500
+ .back-btn:hover {
501
+ transform: translateY(-50%) scale(1.1);
502
+ }
503
+
504
+ .game-title {
505
+ font-size: 2vw;
506
+ color: #004d5c;
507
+ /*font-family: 'Super Cartoon', sans-serif;*/
508
+ margin: 0;
509
+ text-align: center;
510
+ }
511
+
512
+
513
+
514
+
515
+ /* Optional: Modify the appearance of the disabled "Sentence" button specifically */
516
+ .info-btn:disabled {
517
+ background-color: #b0bec5; /* Light gray color for the disabled button */
518
+ color: #757575; /* Dark gray color for text */
519
+ cursor: not-allowed;
520
+ opacity: 0.6;
521
+ }
522
+
523
+
524
+
525
+ /*step 2 left aside css start here*/
526
+
527
+
528
+ /* ===== Step 2: Left Audio Panel — Kid-friendly Player ===== */
529
+ .audio-card--kids {
530
+ --primary: #006780;
531
+ --accent: #009688;
532
+ --soft: #deefef;
533
+ --bar: rgba(0, 103, 128, 0.28);
534
+ --bar-strong: rgba(0, 103, 128, 0.65);
535
+ background: var(--soft);
536
+ padding: 2rem;
537
+ border-radius: 14px;
538
+ border: 3px solid var(--accent);
539
+ position: relative;
540
+ }
541
+
542
+ .ac-header {
543
+ width: 100%;
544
+ display: flex;
545
+ justify-content: center;
546
+ margin-bottom: 1rem;
547
+ }
548
+
549
+ .ac-generate {
550
+ border-radius: 999px;
551
+ font-weight: 700;
552
+ min-width: 230px;
553
+ display: inline-flex;
554
+ align-items: center;
555
+ gap: 10px;
556
+ text-align: center;
557
+ display: flex;
558
+ justify-content: center;
559
+ }
560
+
561
+ .spinner {
562
+ width: 16px;
563
+ height: 16px;
564
+ display: inline-block;
565
+ border: 3px solid #fff;
566
+ border-top-color: transparent;
567
+ border-radius: 50%;
568
+ animation: spin 0.8s linear infinite;
569
+ }
570
+
571
+ @keyframes spin {
572
+ to {
573
+ transform: rotate(360deg);
574
+ }
575
+ }
576
+
577
+ .ac-player {
578
+ display: grid;
579
+ grid-template-rows: auto auto auto auto;
580
+ gap: 1rem;
581
+ place-items: center;
582
+ padding: 1rem 0 0.5rem;
583
+ }
584
+
585
+ .ac-player.is-disabled {
586
+ opacity: 0.6;
587
+ filter: grayscale(0.2);
588
+ }
589
+
590
+ /* Big circular play button */
591
+ .play-btn {
592
+ position: relative;
593
+ width: 65px;
594
+ height: 65px;
595
+ border-radius: 50%;
596
+ border: none;
597
+ background: var(--primary);
598
+ color: #fff;
599
+ cursor: pointer;
600
+ box-shadow: 0 8px 20px rgba(0,0,0,0.18);
601
+ display: grid;
602
+ place-items: center;
603
+ transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.3s ease;
604
+ outline: none;
605
+ }
606
+
607
+ .play-btn:disabled {
608
+ cursor: not-allowed;
609
+ opacity: 0.7;
610
+ }
611
+
612
+ .play-btn:hover:not(:disabled) {
613
+ transform: translateY(-2px) scale(1.02);
614
+ }
615
+
616
+ .play-btn:active:not(:disabled) {
617
+ transform: translateY(0) scale(0.98);
618
+ }
619
+
620
+ .play-btn .icon {
621
+ width: 40px;
622
+ height: 40px;
623
+ fill: #fff;
624
+ }
625
+
626
+ .play-btn .pulse {
627
+ position: absolute;
628
+ inset: -8px;
629
+ border-radius: 50%;
630
+ border: 2px solid var(--primary);
631
+ opacity: 0.18;
632
+ transform: scale(0.9);
633
+ }
634
+
635
+ .ac-player.is-playing .play-btn .pulse {
636
+ animation: pulse 1.6s ease-out infinite;
637
+ }
638
+
639
+ @keyframes pulse {
640
+ 0% {
641
+ transform: scale(0.9);
642
+ opacity: 0.22;
643
+ }
644
+
645
+ 70% {
646
+ transform: scale(1.25);
647
+ opacity: 0;
648
+ }
649
+
650
+ 100% {
651
+ transform: scale(1.25);
652
+ opacity: 0;
653
+ }
654
+ }
655
+
656
+ /* Wave shell (equalizer bars) */
657
+ .wave-shell {
658
+ display: flex;
659
+ align-items: flex-end;
660
+ justify-content: center;
661
+ gap: 6px;
662
+ height: 46px;
663
+ width: 80%;
664
+ max-width: 420px;
665
+ }
666
+
667
+ .wave-shell .bar {
668
+ width: 6px;
669
+ height: 16px;
670
+ background: var(--bar);
671
+ border-radius: 4px;
672
+ animation: wave 1s ease-in-out infinite;
673
+ animation-play-state: paused; /* start paused */
674
+ }
675
+
676
+ .ac-player.is-playing .wave-shell .bar {
677
+ animation-play-state: running;
678
+ }
679
+
680
+ @keyframes wave {
681
+ 0%, 100% {
682
+ height: 12px;
683
+ background: var(--bar);
684
+ }
685
+
686
+ 50% {
687
+ height: 38px;
688
+ background: var(--bar-strong);
689
+ }
690
+ }
691
+
692
+ /* Timeline / progress */
693
+ .timeline {
694
+ position: relative;
695
+ height: 10px;
696
+ width: 80%;
697
+ max-width: 520px;
698
+ background: #e9f3f3;
699
+ border-radius: 999px;
700
+ overflow: hidden;
701
+ border: 1px solid rgba(0,0,0,0.06);
702
+ }
703
+
704
+ .timeline.is-disabled {
705
+ opacity: 0.5;
706
+ }
707
+
708
+ .timeline .progress {
709
+ height: 100%;
710
+ width: 0%;
711
+ background: linear-gradient(90deg, var(--primary), var(--accent));
712
+ border-radius: 999px 0 0 999px;
713
+ transition: width 0.15s linear;
714
+ }
715
+
716
+ /* Time labels */
717
+ .time {
718
+ width: 80%;
719
+ max-width: 520px;
720
+ display: flex;
721
+ justify-content: space-between;
722
+ font-weight: 700;
723
+ color: #004d5c;
724
+ }
725
+
726
+ /* Small helper text */
727
+ .ac-hint {
728
+ font-size: 0.95rem;
729
+ color: #004d5c;
730
+ background: #ffffffa6;
731
+ border: 1px dashed var(--accent);
732
+ padding: 8px 12px;
733
+ border-radius: 10px;
734
+ }
735
+
736
+ /* Responsive */
737
+ @media (max-width: 768px) {
738
+ .play-btn {
739
+ width: 84px;
740
+ height: 84px;
741
+ }
742
+
743
+ .play-btn .icon {
744
+ width: 34px;
745
+ height: 34px;
746
+ }
747
+
748
+ .wave-shell {
749
+ width: 92%;
750
+ }
751
+
752
+ .timeline, .time {
753
+ width: 92%;
754
+ }
755
+ }
756
+
757
+
758
+ /*step =2 right aside css wstart herwe */
759
+
760
+
761
+ /* === Step 2: Right-side kid panel === */
762
+ .kid-panel {
763
+ --primary: #006780;
764
+ --accent: #009688;
765
+ --soft: #deefef;
766
+ background: var(--soft);
767
+ border: 3px solid var(--accent);
768
+ border-radius: 14px;
769
+ padding: 1.2rem 1.4rem 1.4rem;
770
+ display: flex;
771
+ flex-direction: column;
772
+ gap: 0.9rem;
773
+ position: relative;
774
+ }
775
+
776
+ /* top row */
777
+ .kp-top {
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: space-between;
781
+ }
782
+
783
+ .kp-title {
784
+ margin: 0;
785
+ font-size: 1.25rem;
786
+ color: #004d5c;
787
+ font-weight: 800;
788
+ }
789
+
790
+ /* attempts (hearts) */
791
+ .kp-attempts .heart {
792
+ font-size: 1.15rem;
793
+ margin-left: 6px;
794
+ transition: opacity 0.25s ease, transform 0.25s ease;
795
+
796
+ }
797
+
798
+ .kp-attempts .heart.is-off {
799
+ opacity: 0.25;
800
+ transform: scale(0.88);
801
+ }
802
+
803
+ /* input with subtle animation */
804
+ .kp-input-wrap {
805
+ position: relative;
806
+ display: grid;
807
+ place-items: center;
808
+ }
809
+
810
+ .kp-input {
811
+ width: 100%;
812
+ font-weight: 700;
813
+ padding: 0.95rem 1rem;
814
+ border-radius: 10px;
815
+ border: 2px solid var(--primary);
816
+ font-size: 1.15rem;
817
+ text-align: center;
818
+ background: #fff;
819
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
820
+ transition: transform 0.1s ease, box-shadow 0.2s ease, border-color 0.3s ease;
821
+ caret-color: var(--primary);
822
+ }
823
+
824
+ .kp-input:focus {
825
+ outline: none;
826
+ transform: translateY(-1px);
827
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.1);
828
+ border-color: var(--accent);
829
+ }
830
+
831
+ /* success + error states (reuse your classes) */
832
+ .kp-input.correct {
833
+ border-color: #4caf50 !important;
834
+ background: #e8f5e9;
835
+ }
836
+
837
+ .kp-input.error {
838
+ border-color: #f44336 !important;
839
+ background: #ffebee;
840
+ }
841
+
842
+ /* pop tick */
843
+ .ok-badge {
844
+ position: absolute;
845
+ right: 8%;
846
+ top: 50%;
847
+ transform: translateY(-50%) scale(0.6);
848
+ animation: okPop 0.6s ease forwards;
849
+ pointer-events: none;
850
+ }
851
+
852
+ .ok-icon {
853
+ width: 42px;
854
+ height: 42px;
855
+ display: block;
856
+ }
857
+
858
+ .ok-icon circle {
859
+ fill: rgba(0, 103, 128, 0.12);
860
+ stroke: var(--primary);
861
+ stroke-width: 2;
862
+ }
863
+
864
+ .ok-icon path {
865
+ fill: none;
866
+ stroke: #2e7d32;
867
+ stroke-width: 3;
868
+ stroke-linecap: round;
869
+ stroke-linejoin: round;
870
+ }
871
+
872
+ @keyframes okPop {
873
+ 0% {
874
+ transform: translateY(-50%) scale(0);
875
+ opacity: 0;
876
+ }
877
+
878
+ 60% {
879
+ transform: translateY(-50%) scale(1.08);
880
+ opacity: 1;
881
+ }
882
+
883
+ 100% {
884
+ transform: translateY(-50%) scale(1);
885
+ opacity: 1;
886
+ }
887
+ }
888
+
889
+ /* subtle shake on wrong */
890
+ .kid-panel.is-shake .kp-input {
891
+ animation: kpShake 0.35s ease;
892
+ }
893
+
894
+ @keyframes kpShake {
895
+ 0%, 100% {
896
+ transform: translateX(0);
897
+ }
898
+
899
+ 25% {
900
+ transform: translateX(-6px);
901
+ }
902
+
903
+ 50% {
904
+ transform: translateX(6px);
905
+ }
906
+
907
+ 75% {
908
+ transform: translateX(-4px);
909
+ }
910
+ }
911
+
912
+ /* primary submit re-using your color token */
913
+ .kp-submit {
914
+ width: 44%;
915
+ margin: 0 auto;
916
+ font-weight: 800;
917
+ letter-spacing: 0.3px;
918
+ font-size: 1.2vw;
919
+ }
920
+
921
+ /* info panel for meaning/example */
922
+ .info-panel {
923
+ background: #ffffffd9;
924
+ border: 1px dashed var(--accent);
925
+ border-radius: 12px;
926
+ padding: 0.75rem 1rem;
927
+ display: flex;
928
+ flex-direction: column;
929
+ gap: 0.65rem;
930
+ }
931
+
932
+ .info-line {
933
+ display: grid;
934
+ grid-template-columns: auto 1fr;
935
+ align-items: start;
936
+ gap: 0.6rem;
937
+ }
938
+
939
+ .info-chip {
940
+ background: var(--primary);
941
+ color: #fff;
942
+ font-weight: 700;
943
+ font-size: 0.85rem;
944
+ padding: 0.25rem 0.55rem;
945
+ border-radius: 999px;
946
+ white-space: nowrap;
947
+ }
948
+
949
+ .info-text {
950
+ margin: 0;
951
+ color: #004d5c;
952
+ line-height: 1.35rem;
953
+ font-size: 0.98rem;
954
+ }
955
+
956
+ /* responsive tweaks */
957
+ @media (max-width: 768px) {
958
+ .kp-title {
959
+ font-size: 1.05rem;
960
+ }
961
+
962
+ .kp-input {
963
+ width: 92%;
964
+ font-size: 1.05rem;
965
+ }
966
+
967
+ .ok-badge {
968
+ right: 4%;
969
+ }
970
+
971
+ .kp-submit {
972
+ width: 92%;
973
+ }
974
+ }
975
+
976
+
977
+ /* Shared action bar layout */
978
+ .action-bar {
979
+ display: flex;
980
+ justify-content: space-between;
981
+ align-items: center;
982
+ gap: 12px;
983
+ margin-top: 10px;
984
+ }
985
+
986
+ .action-bar .left-buttons {
987
+ display: flex;
988
+ gap: 10px;
989
+ flex-wrap: wrap;
990
+ }
991
+
992
+ /* Base button using your spec */
993
+ .py-btn {
994
+ background-color: #006780;
995
+ color: #ffffff;
996
+ border: none;
997
+ padding: 15px 32px;
998
+ font-size: 1.2vw; /* per your request */
999
+ border-radius: 0.5vw; /* per your request */
1000
+ cursor: pointer;
1001
+ transition: background-color 0.3s, transform 0.3s;
1002
+ font-weight: bold;
1003
+ display: inline-flex;
1004
+ align-items: center;
1005
+ gap: 10px;
1006
+ line-height: 1;
1007
+ }
1008
+
1009
+ /* Hover (your spec) */
1010
+ .py-btn:hover:not(:disabled) {
1011
+ background-color: #18788f;
1012
+ transform: scale(1.05);
1013
+ }
1014
+
1015
+ /* Press feedback */
1016
+ .py-btn:active:not(:disabled) {
1017
+ transform: scale(0.98);
1018
+ }
1019
+
1020
+ /* Disabled */
1021
+ .py-btn:disabled {
1022
+ opacity: 0.55;
1023
+ cursor: not-allowed;
1024
+ transform: none;
1025
+ }
1026
+
1027
+ /* Focus ring for keyboard users */
1028
+ .py-btn:focus-visible {
1029
+ outline: 3px solid #94c7d6;
1030
+ outline-offset: 2px;
1031
+ }
1032
+
1033
+ /* Optional tiny icon motion on hover */
1034
+ .py-btn .btn__icon {
1035
+ display: inline-block;
1036
+ transform: translateY(0);
1037
+ transition: transform 0.25s ease;
1038
+
1039
+ }
1040
+
1041
+ .py-btn:hover:not(:disabled) .btn__icon {
1042
+ transform: translateY(-2px);
1043
+ }
1044
+
1045
+ /* Small-screen fallback so 1.5vw does not get too small */
1046
+ @media (max-width: 768px) {
1047
+ .py-btn {
1048
+ font-size: 16px; /* fallback for phones/tablets */
1049
+ border-radius: 10px; /* visual balance on small screens */
1050
+ padding: 12px 22px;
1051
+ }
1052
+ }
1053
+
1054
+
1055
+ .close-btn1 {
1056
+ background-color: #ffffff;
1057
+ color: #0097a7;
1058
+ border: none;
1059
+ padding: 0.4vw 0.9vw;
1060
+ border-radius: 50%;
1061
+ cursor: pointer;
1062
+ border: 1px solid black;
1063
+ width: 3vw;
1064
+ top: -5vw;
1065
+ right: -3.5vw;
1066
+ position: absolute;
1067
+ height: 3vw;
1068
+ font-size: 1vw;
1069
+ font-weight: bold;
1070
+ }
1071
+
1072
+
1073
+ /* Clean, static image at the left side (no blur, no animation) */
1074
+ .left-illustration {
1075
+ position: absolute;
1076
+ left: -2vw;
1077
+ top: 50%;
1078
+ transform: translateY(-50%);
1079
+ width: 19vw;
1080
+ height: auto;
1081
+ z-index: 1;
1082
+ pointer-events: none;
1083
+ filter: none !important;
1084
+ image-rendering: auto;
1085
+ image-rendering: -webkit-optimize-contrast;
1086
+ image-rendering: crisp-edges;
1087
+ }
1088
+
1089
+ /* Smaller width on narrow screens */
1090
+ @media (max-width: 768px) {
1091
+ .left-illustration {
1092
+ width: 170px;
1093
+ left: 8px;
1094
+ }
1095
+ }
src/app/findword/findword.component.html ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="full-container">
2
+ <div class="header-container">
3
+ <div class="logo">
4
+ <a (click)="goToHome()" routerLink="/home">
5
+ <img src="assets/images/pykara-logo.png" alt="Pykara Logo" />
6
+ </a>
7
+ </div>
8
+ <div class="header-title">
9
+ <h1>Find the Word</h1>
10
+ </div>
11
+ <div class="home-btn">
12
+ <a (click)="goToHome()" routerLink="/home">
13
+ <img src="assets/images/home.png" alt="Home" class="home-icon" />
14
+ </a>
15
+ </div>
16
+ </div>
17
+
18
+ <img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
19
+
20
+ <div class="findword-container">
21
+ <div *ngIf="step === 1" class="card1">
22
+ <div class="content-container">
23
+ <div class="image-container">
24
+ <img src="assets/images/find_word/4.png" alt="Vocabulary Image" class="quiz-image" />
25
+ </div>
26
+ <div class="description-container">
27
+ <h2>Improve Your Word Skills!</h2>
28
+ <p>
29
+ Find the Word is a fun and educational activity that helps students improve their listening, spelling, and vocabulary skills.
30
+ Students hear a word through audio and then type what they hear. They get instant feedback to know if their answer is right or wrong.
31
+ If needed, they can listen again. The exercise also has buttons to show the word’s meaning and an example sentence.
32
+ A reset option lets students try again. This activity helps students practice listening and writing skills in an easy and engaging way, making learning more enjoyable and effective.
33
+ </p>
34
+ <button (click)="startGame()" class="submit-button">Start Learning</button>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+
40
+
41
+ <div *ngIf="step === 2" class="game-screen">
42
+ <div class="game-header">
43
+ <img src="assets/images/back.png" alt="Go Back" class="back-btn" (click)="goBack()" />
44
+ <h2 class="game-title">Listen & Type</h2>
45
+ <!--<img class="wave-photo" src="assets/images/find_word/audio.png" alt="Learning image">-->
46
+ <button class="close-btn1" (click)=" closeToStart()" aria-label="Close">✖</button>
47
+ </div>
48
+
49
+ <div class="game-content">
50
+
51
+ <!-- Audio Section -->
52
+ <!-- Audio Section (Left) -->
53
+ <div class="audio-card audio-card--kids">
54
+ <div class="ac-header">
55
+ <button (click)="fetchAudio()"
56
+ [disabled]="isGenerateDisabled"
57
+ class="btn generate-btn ac-generate"
58
+ aria-label="Generate a new question">
59
+ <span *ngIf="!isLoading">Generate audio</span>
60
+ <!-- <span *ngIf="isLoading" class="spinner" aria-hidden="true"></span>-->
61
+ <span *ngIf="isLoading">Generating</span>
62
+ </button>
63
+ </div>
64
+ <!-- Left mascot (decorative) -->
65
+ <img class="left-illustration"
66
+ src="assets/images/find_word/audio.png"
67
+ alt="" />
68
+ <!-- Player Shell -->
69
+ <div class="ac-player" [class.is-disabled]="!audioUrl" [class.is-playing]="isPlaying">
70
+ <button class="play-btn"
71
+ [disabled]="!audioUrl"
72
+ (click)="togglePlayback()"
73
+ [attr.aria-pressed]="isPlaying"
74
+ [attr.aria-label]="isPlaying ? 'Pause audio' : 'Play audio'">
75
+ <!-- Inline SVG icons (no external deps) -->
76
+ <svg *ngIf="!isPlaying" viewBox="0 0 24 24" class="icon">
77
+ <path d="M8 5v14l11-7z"></path>
78
+ </svg>
79
+ <svg *ngIf="isPlaying" viewBox="0 0 24 24" class="icon">
80
+ <path d="M6 5h4v14H6zM14 5h4v14h-4z"></path>
81
+ </svg>
82
+ <span class="pulse" aria-hidden="true"></span>
83
+ </button>
84
+
85
+
86
+ <!-- Wave shell (animated bars while playing) -->
87
+ <div class="wave-shell" aria-hidden="true">
88
+ <span class="bar" *ngFor="let b of bars; let i = index" [style.animation-delay.ms]="(i%6)*120"></span>
89
+ </div>
90
+
91
+ <!-- Timeline -->
92
+ <div class="timeline" [class.is-disabled]="!audioUrl">
93
+ <div class="progress" [style.width.%]="progress"></div>
94
+ </div>
95
+
96
+
97
+
98
+ <div class="ac-hint" *ngIf="!audioUrl">
99
+ Tap <strong>Generate audio</strong> to load the audio.
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Keep the audio element; wire player events -->
104
+ <audio #audioPlayer
105
+ (timeupdate)="onTimeUpdate()"
106
+ (ended)="onAudioEnded()"
107
+ (loadedmetadata)="onLoadedMeta()"
108
+ hidden>
109
+ <source [src]="audioUrl" type="audio/mp3" />
110
+ Your browser does not support the audio tag.
111
+ </audio>
112
+ </div>
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+ <!-- RIGHT SIDE: Kid-friendly input panel -->
121
+ <div class="input-card kid-panel"
122
+ [class.is-correct]="isCorrect"
123
+ [class.is-shake]="ui.shake">
124
+
125
+ <!-- Top row: title + attempts -->
126
+ <div class="kp-top">
127
+ <h3 class="kp-title">Type the word</h3>
128
+
129
+ <!-- Attempts with hearts -->
130
+ <div class="kp-attempts" aria-label="Attempts left">
131
+ <span class="heart" [class.is-off]="attemptsLeft < 1"></span>
132
+ <span class="heart" [class.is-off]="attemptsLeft < 2"></span>
133
+ <span class="heart" [class.is-off]="attemptsLeft < 3"></span>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Input + success badge -->
138
+ <div class="kp-input-wrap">
139
+ <input type="text"
140
+ class="kp-input"
141
+ [(ngModel)]="userInput"
142
+ [disabled]="!audioFinished"
143
+ (ngModelChange)="onInputChange()"
144
+ placeholder="Type what you heard"
145
+ [ngClass]="{ 'correct': isCorrect, 'error': !isCorrect && validationMessage }" />
146
+
147
+ <!-- Pop tick on correct -->
148
+ <div class="ok-badge" *ngIf="isCorrect || ui?.pulseOk" aria-hidden="true">
149
+ <svg viewBox="0 0 24 24" class="ok-icon">
150
+ <circle cx="12" cy="12" r="10"></circle>
151
+ <path d="M7 12l3 3 7-7"></path>
152
+ </svg>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Validation message (only when needed) -->
157
+ <p *ngIf="validationMessage" class="error-message">{{ validationMessage }}</p>
158
+
159
+ <!-- Primary action -->
160
+ <button (click)="validateWord()"
161
+ [disabled]="!canSubmit || attemptsLeft === 0"
162
+ class="btn submit-btn kp-submit">
163
+ Submit
164
+ </button>
165
+
166
+ <div class="action-bar">
167
+ <div class="left-buttons">
168
+ <button class="py-btn"
169
+ (click)="showMeaningPanel()"
170
+ [disabled]="isMeaningButtonDisabled">
171
+ <span class="btn__icon" aria-hidden="true">📘</span>
172
+ <span>Meaning</span>
173
+ </button>
174
+
175
+ <button class="py-btn"
176
+ (click)="showSentencePanel()"
177
+ [disabled]="isExampleButtonDisabled">
178
+ <span class="btn__icon" aria-hidden="true">✍️</span>
179
+ <span>Example</span>
180
+ </button>
181
+ </div>
182
+
183
+ <button class="py-btn"
184
+ (click)="nextQuestion()"
185
+ [disabled]="attemptsLeft > 0 && !isCorrect">
186
+ <span class="btn__icon" aria-hidden="true">⟲</span>
187
+ <span>Reset</span>
188
+ </button>
189
+ </div>
190
+
191
+
192
+ </div>
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+ </div>
201
+ </div>
202
+
203
+
204
+
205
+
206
+ <div *ngIf="isPopupVisible" class="popup-overlay">
207
+ <div class="popup-content">
208
+ <p>{{ popupMessage }}</p>
209
+ <button class="btn close-btn" (click)="closePopup()">Close</button>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
src/app/findword/findword.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { FindwordComponent } from './findword.component';
4
+
5
+ describe('FindwordComponent', () => {
6
+ let component: FindwordComponent;
7
+ let fixture: ComponentFixture<FindwordComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [FindwordComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(FindwordComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });