Oviya
commited on
Commit
·
e4ab6d0
0
Parent(s):
Deploy Angular to HF Space
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .editorconfig +16 -0
- .gitattributes +12 -0
- .gitignore +42 -0
- .vscode/extensions.json +4 -0
- .vscode/launch.json +19 -0
- .vscode/tasks.json +42 -0
- GrammAI.esproj +10 -0
- GrammAI.esproj.user +8 -0
- README.md +38 -0
- angular.json +110 -0
- canvas-confetti.d.ts +1 -0
- karma.conf.js +44 -0
- nuget.config +10 -0
- obj/Debug/GrammAI.esproj.CoreCompileInputs.cache +0 -0
- obj/Debug/GrammAI.esproj.FileListAbsolute.txt +6 -0
- package-lock.json +0 -0
- package.json +48 -0
- src/app/app-routing.module.ts +52 -0
- src/app/app.component.css +12 -0
- src/app/app.component.html +2 -0
- src/app/app.component.spec.ts +35 -0
- src/app/app.component.ts +20 -0
- src/app/app.module.ts +56 -0
- src/app/auth/auth.component.css +211 -0
- src/app/auth/auth.component.html +101 -0
- src/app/auth/auth.component.spec.ts +23 -0
- src/app/auth/auth.component.ts +55 -0
- src/app/auth/auth.guard.spec.ts +17 -0
- src/app/auth/auth.guard.ts +30 -0
- src/app/auth/auth.service.spec.ts +16 -0
- src/app/auth/auth.service.ts +188 -0
- src/app/auth/root-redirect.guard.spec.ts +17 -0
- src/app/auth/root-redirect.guard.ts +29 -0
- src/app/authentication/authentication.component.css +133 -0
- src/app/authentication/authentication.component.html +47 -0
- src/app/authentication/authentication.component.spec.ts +23 -0
- src/app/authentication/authentication.component.ts +47 -0
- src/app/authentication/authentication.guard.spec.ts +17 -0
- src/app/authentication/authentication.guard.ts +24 -0
- src/app/authentication/authentication.service.spec.ts +16 -0
- src/app/authentication/authentication.service.ts +203 -0
- src/app/chat/api.service.spec.ts +16 -0
- src/app/chat/api.service.ts +81 -0
- src/app/chat/chat.component.css +960 -0
- src/app/chat/chat.component.html +127 -0
- src/app/chat/chat.component.spec.ts +23 -0
- src/app/chat/chat.component.ts +667 -0
- src/app/findword/findword.component.css +1095 -0
- src/app/findword/findword.component.html +213 -0
- 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 |
+
});
|