Oviya commited on
Commit
679d3a0
·
1 Parent(s): 7a7b728

deploy app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .editorconfig +16 -0
  2. .gitattributes +10 -0
  3. .gitignore +42 -0
  4. .vscode/extensions.json +4 -0
  5. .vscode/launch.json +19 -0
  6. .vscode/tasks.json +42 -0
  7. README.md +37 -6
  8. angular.json +103 -0
  9. karma.conf.js +44 -0
  10. nuget.config +10 -0
  11. obj/Debug/py-match.esproj.CoreCompileInputs.cache +0 -0
  12. obj/Debug/py-match.esproj.FileListAbsolute.txt +7 -0
  13. package-lock.json +0 -0
  14. package.json +40 -0
  15. py-match.esproj +10 -0
  16. py-match.esproj.user +8 -0
  17. src/app/api.service.ts +18 -0
  18. src/app/app-routing.module.ts +37 -0
  19. src/app/app.component.css +0 -0
  20. src/app/app.component.html +1 -0
  21. src/app/app.component.spec.ts +27 -0
  22. src/app/app.component.ts +79 -0
  23. src/app/auth/sign-in/sign-in.component.css +134 -0
  24. src/app/auth/sign-in/sign-in.component.html +52 -0
  25. src/app/auth/sign-in/sign-in.component.spec.ts +21 -0
  26. src/app/auth/sign-in/sign-in.component.ts +63 -0
  27. src/app/auth/sign-up/sign-up.component.css +147 -0
  28. src/app/auth/sign-up/sign-up.component.html +76 -0
  29. src/app/auth/sign-up/sign-up.component.spec.ts +21 -0
  30. src/app/auth/sign-up/sign-up.component.ts +100 -0
  31. src/app/auth/sign-up/sign-up.service.ts +37 -0
  32. src/app/intro-page/intro-page.component.css +380 -0
  33. src/app/intro-page/intro-page.component.html +100 -0
  34. src/app/intro-page/intro-page.component.spec.ts +21 -0
  35. src/app/intro-page/intro-page.component.ts +92 -0
  36. src/app/llm-quiz/llm-quiz.component.css +274 -0
  37. src/app/llm-quiz/llm-quiz.component.html +142 -0
  38. src/app/llm-quiz/llm-quiz.component.spec.ts +21 -0
  39. src/app/llm-quiz/llm-quiz.component.ts +244 -0
  40. src/app/question-answer/question-answer-service.service.ts +50 -0
  41. src/app/question-answer/question-answer.component.css +317 -0
  42. src/app/question-answer/question-answer.component.html +67 -0
  43. src/app/question-answer/question-answer.component.spec.ts +21 -0
  44. src/app/question-answer/question-answer.component.ts +157 -0
  45. src/app/quiz/quiz.component.css +126 -0
  46. src/app/quiz/quiz.component.html +35 -0
  47. src/app/quiz/quiz.component.spec.ts +21 -0
  48. src/app/quiz/quiz.component.ts +107 -0
  49. src/app/quiz/quiz.service.spec.ts +16 -0
  50. src/app/quiz/quiz.service.ts +30 -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 CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.gif filter=lfs diff=lfs merge=lfs -text
38
+ *.jpg filter=lfs diff=lfs merge=lfs -text
39
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ *.webp filter=lfs diff=lfs merge=lfs -text
41
+ *.svg filter=lfs diff=lfs merge=lfs -text
42
+ *.otf filter=lfs diff=lfs merge=lfs -text
43
+ *.ttf filter=lfs diff=lfs merge=lfs -text
44
+ *.woff filter=lfs diff=lfs merge=lfs -text
45
+ *.woff2 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
+ }
README.md CHANGED
@@ -1,10 +1,41 @@
1
  ---
2
- title: Py Match
3
- emoji: 😻
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: static
7
- pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PY-Match
3
+ emoji: 🎯
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: static
7
+ app_build_command: npm ci && npm run build -- --configuration production
8
+ app_file: dist/py-match/index.html
9
+ fullWidth: true
10
  ---
11
 
12
+
13
+
14
+
15
+ # PyMatch
16
+
17
+ This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.0.
18
+
19
+ ## Development server
20
+
21
+ 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.
22
+
23
+ ## Code scaffolding
24
+
25
+ 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`.
26
+
27
+ ## Build
28
+
29
+ Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
30
+
31
+ ## Running unit tests
32
+
33
+ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
34
+
35
+ ## Running end-to-end tests
36
+
37
+ 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.
38
+
39
+ ## Further help
40
+
41
+ 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,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "py-match": {
7
+ "projectType": "application",
8
+ "schematics": {},
9
+ "root": "",
10
+ "sourceRoot": "src",
11
+ "prefix": "app",
12
+ "architect": {
13
+ "build": {
14
+ "builder": "@angular-devkit/build-angular:browser",
15
+ "options": {
16
+ "outputPath": "dist/py-match",
17
+ "index": "src/index.html",
18
+ "main": "src/main.ts",
19
+ "polyfills": [
20
+ "zone.js"
21
+ ],
22
+ "tsConfig": "tsconfig.app.json",
23
+ "assets": [
24
+ "src/favicon.ico",
25
+ "src/assets"
26
+ ],
27
+ "styles": [
28
+ "src/styles.css"
29
+ ],
30
+ "scripts": []
31
+ },
32
+ "configurations": {
33
+ "production": {
34
+ "budgets": [
35
+ {
36
+ "type": "initial",
37
+ "maximumWarning": "500kb",
38
+ "maximumError": "1mb"
39
+ },
40
+ {
41
+ "type": "anyComponentStyle",
42
+ "maximumWarning": "4.5kb",
43
+ "maximumError": "6kb"
44
+ }
45
+ ],
46
+ "outputHashing": "all"
47
+ },
48
+ "development": {
49
+ "buildOptimizer": false,
50
+ "optimization": false,
51
+ "vendorChunk": true,
52
+ "extractLicenses": false,
53
+ "sourceMap": true,
54
+ "namedChunks": true
55
+ }
56
+ },
57
+ "defaultConfiguration": "production"
58
+ },
59
+ "serve": {
60
+ "builder": "@angular-devkit/build-angular:dev-server",
61
+ "configurations": {
62
+ "production": {
63
+ "browserTarget": "py-match:build:production"
64
+ },
65
+ "development": {
66
+ "browserTarget": "py-match:build:development"
67
+ }
68
+ },
69
+ "defaultConfiguration": "development"
70
+ },
71
+ "extract-i18n": {
72
+ "builder": "@angular-devkit/build-angular:extract-i18n",
73
+ "options": {
74
+ "browserTarget": "py-match:build"
75
+ }
76
+ },
77
+ "test": {
78
+ "builder": "@angular-devkit/build-angular:karma",
79
+ "options": {
80
+ "polyfills": [
81
+ "zone.js",
82
+ "zone.js/testing"
83
+ ],
84
+ "tsConfig": "tsconfig.spec.json",
85
+ "assets": [
86
+ "src/favicon.ico",
87
+ "src/assets"
88
+ ],
89
+ "styles": [
90
+ "src/styles.css",
91
+ "node_modules/@fortawesome/fontawesome-free/css/all.css"
92
+ ],
93
+ "scripts": [],
94
+ "karmaConfig": "karma.conf.js"
95
+ }
96
+ }
97
+ }
98
+ }
99
+ },
100
+ "cli": {
101
+ "analytics": "c88a5f95-78ff-4d1a-be52-0edf479dc3e1"
102
+ }
103
+ }
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/py-match.esproj.CoreCompileInputs.cache ADDED
File without changes
obj/Debug/py-match.esproj.FileListAbsolute.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ D:\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
2
+ D:\py-match\nw\New folder\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
3
+ C:\Users\Admin\Desktop\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
4
+ C:\Users\Admin\Desktop\Learning\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
5
+ D:\py-match\py-match\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
6
+ D:\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
7
+ D:\py-match\Frontend\py-match\py-match\py-match\obj\Debug\py-match.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,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "py-match",
3
+ "version": "0.0.0",
4
+ "scripts": {
5
+ "ng": "ng",
6
+ "start": "ng serve --host=127.0.0.1",
7
+ "build": "ng build",
8
+ "watch": "ng build --watch --configuration development",
9
+ "test": "ng test"
10
+ },
11
+ "private": true,
12
+ "dependencies": {
13
+ "@angular/animations": "^16.1.0",
14
+ "@angular/common": "^16.1.0",
15
+ "@angular/compiler": "^16.1.0",
16
+ "@angular/core": "^16.1.0",
17
+ "@angular/forms": "^16.1.0",
18
+ "@angular/platform-browser": "^16.1.0",
19
+ "@angular/platform-browser-dynamic": "^16.1.0",
20
+ "@angular/router": "^16.1.0",
21
+ "@fortawesome/fontawesome-free": "^7.0.0",
22
+ "jest-editor-support": "*",
23
+ "rxjs": "~7.8.0",
24
+ "tslib": "^2.3.0",
25
+ "zone.js": "~0.13.0"
26
+ },
27
+ "devDependencies": {
28
+ "@angular-devkit/build-angular": "^16.1.0",
29
+ "@angular/cli": "~16.1.0",
30
+ "@angular/compiler-cli": "^16.1.0",
31
+ "@types/jasmine": "~4.3.0",
32
+ "jasmine-core": "~4.6.0",
33
+ "karma": "~6.4.0",
34
+ "karma-chrome-launcher": "~3.2.0",
35
+ "karma-coverage": "~2.2.0",
36
+ "karma-jasmine": "~5.1.0",
37
+ "karma-jasmine-html-reporter": "~2.1.0",
38
+ "typescript": "~5.1.3"
39
+ }
40
+ }
py-match.esproj ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/0.5.271090-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
+ <BuildOutputFolder>$(MSBuildProjectDirectory)\dist\py-match\</BuildOutputFolder>
9
+ </PropertyGroup>
10
+ </Project>
py-match.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>
src/app/api.service.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { environment } from '../environments/environment';
4
+
5
+ @Injectable({ providedIn: 'root' })
6
+ export class ApiService {
7
+ private base = environment.apiBase;
8
+
9
+ constructor(private http: HttpClient) { }
10
+
11
+ start(body = { n_questions: 10, batch_size: 5, domain: 'marriage' }) {
12
+ return this.http.post<any>(`${this.base}/q/start`, body);
13
+ }
14
+
15
+ next(session_id: string, selected_color: 'blue' | 'green' | 'red' | 'yellow') {
16
+ return this.http.post<any>(`${this.base}/q/next`, { session_id, selected_color });
17
+ }
18
+ }
src/app/app-routing.module.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NgModule } from '@angular/core';
2
+ import { RouterModule, Routes } from '@angular/router';
3
+ import { IntroPageComponent } from './intro-page/intro-page.component';
4
+ import { QuestionAnswerComponent } from './question-answer/question-answer.component';
5
+ import { QuizComponent } from './quiz/quiz.component';
6
+
7
+ const routes: Routes = [
8
+ {
9
+ path: 'auth',
10
+ children: [
11
+ {
12
+ path: 'signin',
13
+ loadComponent: () =>
14
+ import('./auth/sign-in/sign-in.component').then(m => m.SignInComponent),
15
+ },
16
+ {
17
+ path: 'signup',
18
+ loadComponent: () =>
19
+ import('./auth/sign-up/sign-up.component').then(m => m.SignUpComponent),
20
+ },
21
+ ],
22
+ },
23
+
24
+ { path: '', component: IntroPageComponent, pathMatch: 'full' },
25
+
26
+ { path: 'sign-in', redirectTo: 'auth/signin', pathMatch: 'full' },
27
+ { path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
28
+ { path: 'question-answer', component: QuestionAnswerComponent },
29
+ { path: 'quiz', component: QuizComponent },
30
+ { path: '**', redirectTo: '' },
31
+ ];
32
+
33
+ @NgModule({
34
+ imports: [RouterModule.forRoot(routes)],
35
+ exports: [RouterModule],
36
+ })
37
+ export class AppRoutingModule { }
src/app/app.component.css ADDED
File without changes
src/app/app.component.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <router-outlet></router-outlet>
src/app/app.component.spec.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { AppComponent } from './app.component';
3
+
4
+ describe('AppComponent', () => {
5
+ beforeEach(() => TestBed.configureTestingModule({
6
+ declarations: [AppComponent]
7
+ }));
8
+
9
+ it('should create the app', () => {
10
+ const fixture = TestBed.createComponent(AppComponent);
11
+ const app = fixture.componentInstance;
12
+ expect(app).toBeTruthy();
13
+ });
14
+
15
+ it(`should have as title 'py-match'`, () => {
16
+ const fixture = TestBed.createComponent(AppComponent);
17
+ const app = fixture.componentInstance;
18
+ expect(app.title).toEqual('py-match');
19
+ });
20
+
21
+ it('should render title', () => {
22
+ const fixture = TestBed.createComponent(AppComponent);
23
+ fixture.detectChanges();
24
+ const compiled = fixture.nativeElement as HTMLElement;
25
+ expect(compiled.querySelector('.content span')?.textContent).toContain('py-match app is running!');
26
+ });
27
+ });
src/app/app.component.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ //import { Component } from '@angular/core';
3
+ //import { FormsModule } from '@angular/forms'; // Import FormsModule
4
+ //import { CommonModule } from '@angular/common'; // Import CommonModule
5
+
6
+ //import { IntroPageComponent } from './intro-page/intro-page.component';
7
+ //import { SignInComponent } from './auth/sign-in/sign-in.component';
8
+ //import { SignUpComponent } from './auth/sign-up/sign-up.component';
9
+ //import { QuestionAnswerComponent } from './question-answer/question-answer.component';
10
+ //import { QuizComponent } from './quiz/quiz.component';
11
+ //import { ApiService } from './api.service';
12
+ //import { RouterOutlet } from '@angular/router';
13
+ //type Color = 'blue' | 'green' | 'red' | 'yellow';
14
+
15
+ //@Component({
16
+ // selector: 'app-root',
17
+ // standalone: true,
18
+ // imports: [RouterOutlet, QuestionAnswerComponent, IntroPageComponent, QuizComponent,
19
+ // SignInComponent,
20
+ // SignUpComponent,
21
+ // FormsModule, // Add FormsModule here
22
+ // CommonModule] , // Add CommonModule here],
23
+ // templateUrl: './app.component.html',
24
+ // styleUrls: ['./app.component.css']
25
+ //})
26
+ //export class AppComponent {
27
+ // title = 'py-match';
28
+ // sessionId: string | null = null;
29
+ // index = 0;
30
+ // total = 0;
31
+ // question = '';
32
+ // options: { text: string, color: Color }[] = [];
33
+ // progress?: Record<Color, number>; // percentages
34
+
35
+ // constructor(private api: ApiService) { }
36
+
37
+ // start() {
38
+ // this.api.start({ n_questions: 10, batch_size: 5, domain: 'marriage' })
39
+ // .subscribe(res => {
40
+ // this.sessionId = res.session_id;
41
+ // this.index = res.index;
42
+ // this.total = res.total;
43
+ // this.question = res.question;
44
+ // this.options = res.options;
45
+ // this.progress = undefined; // first question has no progress yet
46
+ // });
47
+ // }
48
+
49
+ // answer(color: Color) {
50
+ // if (!this.sessionId) { return; }
51
+ // this.api.next(this.sessionId, color).subscribe(res => {
52
+ // if (res.done) {
53
+ // alert('Finished! Check console for final mix.');
54
+ // console.log('Final mix (percent):', res.mix);
55
+ // return;
56
+ // }
57
+ // this.index = res.index;
58
+ // this.total = res.total;
59
+ // this.question = res.question;
60
+ // this.options = res.options;
61
+ // this.progress = res.progress; // already in percentages (from your backend change)
62
+ // });
63
+ // }
64
+ //}
65
+
66
+
67
+ import { Component } from '@angular/core';
68
+ import { RouterOutlet } from '@angular/router';
69
+
70
+ @Component({
71
+ selector: 'app-root',
72
+ standalone: true,
73
+ imports: [RouterOutlet],
74
+ templateUrl: './app.component.html',
75
+ styleUrls: ['./app.component.css']
76
+ })
77
+ export class AppComponent {
78
+ title = 'py-match';
79
+ }
src/app/auth/sign-in/sign-in.component.css ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block
3
+ }
4
+
5
+
6
+ .auth-box {
7
+ width: 49vw;
8
+ display: grid;
9
+ grid-template-columns: 1fr;
10
+ background: #2b1b6b;
11
+ border-radius: 14px;
12
+ overflow: hidden;
13
+ box-shadow: 0 20px 60px rgba(0,0,0,.35)
14
+ }
15
+
16
+ .panel-right {
17
+ position: relative;
18
+ background: radial-gradient(120% 120% at 20% 50%,rgba(0,0,0,.25) 0%,rgba(0,0,0,0) 60%)
19
+ }
20
+
21
+ .panel-right::before {
22
+ content: "";
23
+ position: absolute;
24
+ inset: 0;
25
+ background: linear-gradient(90deg,rgba(0,0,0,.45) 0%,rgba(0,0,0,0) 26%);
26
+ pointer-events: none
27
+ }
28
+
29
+ .right-image img {
30
+ max-width: 100%;
31
+ height: auto;
32
+ display: block
33
+ }
34
+
35
+ .panel-left {
36
+ padding: clamp(22px,3.5vw,36px);
37
+ background: white;
38
+ color: black;
39
+ }
40
+
41
+ .brand-mark {
42
+ width: 4vw;
43
+ margin-bottom: 14px;
44
+ border: 2px solid #b1b1b17d;
45
+ box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
46
+ }
47
+
48
+ .title {
49
+ margin: 0 0 6px;
50
+ font-size: clamp(22px,3vw,26px);
51
+ font-weight: 700
52
+ }
53
+
54
+ .form {
55
+ display: grid;
56
+ gap: 12px
57
+ }
58
+
59
+ .field {
60
+ display: grid;
61
+ gap: 6px
62
+ }
63
+
64
+ label {
65
+ font-weight: 600;
66
+ font-size: 13px;
67
+ }
68
+
69
+ input[type="email"], input[type="password"] {
70
+ color: black;
71
+ border: 1px solid rgb(0 0 0 / 57%);
72
+ border-radius: 10px;
73
+ padding: 11px 12px;
74
+ outline: none
75
+ }
76
+
77
+ input::placeholder {
78
+ color: #808080;
79
+ }
80
+
81
+ input:focus {
82
+ border-color: #a78bfa;
83
+ box-shadow: 0 0 0 3px rgba(167,139,250,.25)
84
+ }
85
+
86
+ .error {
87
+ color: red;
88
+ font-size: 12px
89
+ }
90
+
91
+ .btn {
92
+ width: 100%;
93
+ border-radius: 999px;
94
+ padding: 12px 18px;
95
+ cursor: pointer
96
+ }
97
+
98
+ .btn-primary {
99
+ background: #0b0f1a;
100
+ color: #fff;
101
+ border: none;
102
+ font-weight: 700
103
+ }
104
+
105
+ .btn[aria-busy="true"] {
106
+ opacity: .75;
107
+ cursor: progress
108
+ }
109
+
110
+ .footnote {
111
+ margin: 14px 0 0;
112
+ font-size: 13px;
113
+ }
114
+
115
+ .footnote a {
116
+ color: red;
117
+ text-decoration: underline
118
+ }
119
+
120
+ @media(min-width:900px) {
121
+ .auth-box {
122
+ grid-template-columns: 520px 1fr
123
+ }
124
+ }
125
+
126
+ .topTitle {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 21px;
130
+ }
131
+
132
+ .topHeader {
133
+ font-size: 1vw;
134
+ }
src/app/auth/sign-in/sign-in.component.html ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <section class="auth-page">
2
+ <div class="auth-box" role="dialog" aria-labelledby="siTitle">
3
+ <!-- Left (form) -->
4
+ <div class="panel-left">
5
+ <div class="topTitle">
6
+ <img class="brand-mark" src="/assets/pykara-logo.png" alt="Brand" />
7
+ <span class="topHeader"><b>Welcome back!</b></span>
8
+ </div>
9
+ <h2 id="siTitle" class="title">Log in</h2>
10
+
11
+ <form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate>
12
+ <div class="field">
13
+ <label for="email">
14
+ Email
15
+ <small *ngIf="controlHasError('email','required')" class="error">*</small>
16
+ </label>
17
+ <input id="email" type="email" formControlName="email" placeholder="you@example.com"
18
+ [attr.aria-invalid]="controlHasError('email')" />
19
+ <small *ngIf="controlHasError('email','email')" class="error">Enter a valid email.</small>
20
+ </div>
21
+
22
+ <div class="field">
23
+ <label for="password">
24
+ Password
25
+ <small *ngIf="controlHasError('password','required')" class="error">*</small>
26
+ </label>
27
+ <input id="password" type="password" formControlName="password" placeholder="••••••••"
28
+ [attr.aria-invalid]="controlHasError('password')" />
29
+ <small *ngIf="controlHasError('password','minlength')" class="error">Use at least 6 characters.</small>
30
+ </div>
31
+
32
+ <button class="btn btn-primary" type="submit"
33
+ [disabled]="form.invalid || submitting()" [attr.aria-busy]="submitting()">
34
+ Sign in
35
+ </button>
36
+
37
+ <p class="footnote">
38
+ New here?
39
+ <!-- Use click handler instead of routerLink -->
40
+ <a href="#" (click)="goToSignUp(); $event.preventDefault()">Create an account</a>
41
+ </p>
42
+ </form>
43
+ </div>
44
+
45
+ <!-- Right (image) -->
46
+ <div class="panel-right">
47
+ <div class="right-image">
48
+ <img src="/assets/user.png" alt="Decorative" />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </section>
src/app/auth/sign-in/sign-in.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { SignInComponent } from './sign-in.component';
4
+
5
+ describe('SignInComponent', () => {
6
+ let component: SignInComponent;
7
+ let fixture: ComponentFixture<SignInComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [SignInComponent]
12
+ });
13
+ fixture = TestBed.createComponent(SignInComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/auth/sign-in/sign-in.component.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, ChangeDetectionStrategy, signal, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
4
+ import { Router, RouterLink } from '@angular/router';
5
+
6
+ @Component({
7
+ selector: 'app-sign-in',
8
+ standalone: true,
9
+ imports: [CommonModule, ReactiveFormsModule, RouterLink],
10
+ templateUrl: './sign-in.component.html',
11
+ styleUrls: ['./sign-in.component.css'],
12
+ changeDetection: ChangeDetectionStrategy.OnPush
13
+ })
14
+ export class SignInComponent {
15
+ submitting = signal(false);
16
+ form: FormGroup;
17
+
18
+ @Output() switchToSignUp = new EventEmitter<void>();
19
+ @Output() signInSuccess = new EventEmitter<void>();
20
+
21
+ constructor(private fb: FormBuilder, private router: Router) {
22
+ this.form = this.fb.group({
23
+ email: ['', [Validators.required, Validators.email]],
24
+ password: ['', [Validators.required, Validators.minLength(8)]],
25
+ remember: [true]
26
+ });
27
+ }
28
+
29
+ navigateHome() {
30
+ this.router.navigate(['/']);
31
+ }
32
+
33
+ goToSignUp() {
34
+ this.switchToSignUp.emit();
35
+ }
36
+
37
+ controlHasError(name: string, key?: string) {
38
+ const c = this.form.get(name);
39
+ if (!c) return false;
40
+ if (!key) return c.invalid && (c.dirty || c.touched);
41
+ return c.hasError(key) && (c.dirty || c.touched);
42
+ }
43
+
44
+ async submit() {
45
+ if (this.form.invalid) {
46
+ this.form.markAllAsTouched();
47
+ return;
48
+ }
49
+ this.submitting.set(true);
50
+ try {
51
+ const payload = this.form.value;
52
+ console.log('SIGN IN payload', payload);
53
+
54
+ // Emit success event instead of alert
55
+ this.signInSuccess.emit();
56
+
57
+ // Optional: You can also navigate or do other actions here
58
+ // this.router.navigate(['/question-answer']);
59
+ } finally {
60
+ this.submitting.set(false);
61
+ }
62
+ }
63
+ }
src/app/auth/sign-up/sign-up.component.css ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block
3
+ }
4
+
5
+ /*.auth-page {
6
+ min-height: 100svh;
7
+ display: grid;
8
+ place-items: center;
9
+ padding: clamp(16px,3vw,24px);
10
+ background: linear-gradient(135deg,#5b21b6 0%,#7c3aed 50%,#4c1d95 100%);
11
+ }*/
12
+
13
+ .auth-box {
14
+ width: 49vw;
15
+ display: grid;
16
+ grid-template-columns: 1fr;
17
+ background: #2b1b6b;
18
+ border-radius: 14px;
19
+ overflow: hidden;
20
+ box-shadow: 0 20px 60px rgba(0,0,0,.35);
21
+ }
22
+
23
+ .panel-right {
24
+ position: relative;
25
+ background: radial-gradient(120% 120% at 20% 50%, rgba(0,0,0,.25) 0%, rgba(0,0,0,0) 60%);
26
+ }
27
+
28
+ .panel-right::before {
29
+ content: "";
30
+ position: absolute;
31
+ inset: 0;
32
+ background: linear-gradient(90deg, rgba(0,0,0,.45) 0%, rgba(0,0,0,0) 26%);
33
+ pointer-events: none;
34
+ }
35
+
36
+ .right-image {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ }
41
+
42
+ .right-image img {
43
+ max-width: 100%;
44
+ height: auto;
45
+ display: block
46
+ }
47
+
48
+ .panel-left {
49
+ padding: clamp(22px,3.5vw,36px);
50
+ background: white;
51
+ color: black;
52
+ }
53
+
54
+ .brand-mark {
55
+ width: 4vw;
56
+ margin-bottom: 14px;
57
+ border: 2px solid #b1b1b17d;
58
+ box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
59
+ }
60
+
61
+ .title {
62
+ margin: 0 0 6px;
63
+ font-size: clamp(22px,3vw,26px);
64
+ font-weight: 700
65
+ }
66
+
67
+ .form {
68
+ display: grid;
69
+ gap: 12px
70
+ }
71
+
72
+ .field {
73
+ display: grid;
74
+ gap: 6px
75
+ }
76
+
77
+ label {
78
+ font-weight: 600;
79
+ font-size: 13px;
80
+ }
81
+
82
+ input[type="text"], input[type="password"] {
83
+ color: #000000;
84
+ border: 1px solid rgb(0 0 0 / 57%);
85
+ border-radius: 10px;
86
+ padding: 11px 12px;
87
+ outline: none;
88
+ }
89
+
90
+ input::placeholder {
91
+ color: #808080
92
+ }
93
+
94
+ input:focus {
95
+ border-color: #a78bfa;
96
+ box-shadow: 0 0 0 3px rgba(167,139,250,.25)
97
+ }
98
+
99
+ .error {
100
+ color: red;
101
+ font-size: 12px
102
+ }
103
+
104
+ .btn {
105
+ width: 100%;
106
+ border-radius: 999px;
107
+ padding: 12px 18px;
108
+ cursor: pointer
109
+ }
110
+
111
+ .btn-primary {
112
+ background: #0b0f1a;
113
+ color: #fff;
114
+ border: none;
115
+ font-weight: 700
116
+ }
117
+
118
+ .btn[aria-busy="true"] {
119
+ opacity: .75;
120
+ cursor: progress
121
+ }
122
+
123
+ .footnote {
124
+ margin: 14px 0 0;
125
+ font-size: 13px;
126
+ }
127
+
128
+ .footnote a {
129
+ color: red;
130
+ text-decoration: underline
131
+ }
132
+
133
+ @media(min-width:900px) {
134
+ .auth-box {
135
+ grid-template-columns: 520px 1fr
136
+ }
137
+ }
138
+
139
+ .topTitle {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 21px;
143
+ }
144
+
145
+ .topHeader {
146
+ font-size: 1vw;
147
+ }
src/app/auth/sign-up/sign-up.component.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <section class="auth-page">
2
+ <div class="auth-box" role="dialog" aria-labelledby="suTitle">
3
+ <!-- Left (form) -->
4
+ <div class="panel-left">
5
+ <div class="topTitle">
6
+ <img class="brand-mark" src="/assets/pykara-logo.png" alt="Brand" />
7
+ <p class="topHeader"><b>Let's get started!</b></p>
8
+ </div>
9
+ <h2 id="suTitle" class="title">Create an account</h2>
10
+
11
+ <form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate>
12
+ <!-- Name -->
13
+ <div class="field">
14
+ <label for="name">
15
+ Name
16
+ <small *ngIf="controlHasError('name','required')" class="error">*</small>
17
+ </label>
18
+ <input id="name" type="text" placeholder="John"
19
+ formControlName="name" [attr.aria-invalid]="controlHasError('name')" />
20
+ <small *ngIf="controlHasError('name','minlength')" class="error">Enter at least 2 characters.</small>
21
+ </div>
22
+
23
+ <!-- Email/Phone (contact) -->
24
+ <div class="field">
25
+ <label for="contact">
26
+ Email
27
+ <small *ngIf="controlHasError('contact','required')" class="error">*</small>
28
+ </label>
29
+ <input id="contact" type="text" placeholder="hello@gmail.com"
30
+ formControlName="contact" [attr.aria-invalid]="controlHasError('contact')" />
31
+ <small *ngIf="controlHasError('contact','pattern')" class="error">Enter a valid email/phone.</small>
32
+ </div>
33
+
34
+ <!-- Password -->
35
+ <div class="field">
36
+ <label for="password">
37
+ Password
38
+ <small *ngIf="controlHasError('password','required')" class="error">*</small>
39
+ </label>
40
+ <input id="password" type="password" placeholder="••••••••"
41
+ formControlName="password" [attr.aria-invalid]="controlHasError('password')" />
42
+ <small *ngIf="controlHasError('password','minlength')" class="error">Use at least 6 characters.</small>
43
+ </div>
44
+
45
+ <!-- Confirm password -->
46
+ <div class="field">
47
+ <label for="confirmPassword">
48
+ Confirm password
49
+ <small *ngIf="controlHasError('confirmPassword','required')" class="error">*</small>
50
+ </label>
51
+ <input id="confirmPassword" type="password" placeholder="••••••••"
52
+ formControlName="confirmPassword" [attr.aria-invalid]="showPwdMismatch()" />
53
+ <small *ngIf="showPwdMismatch()" class="error">Passwords do not match.</small>
54
+ </div>
55
+
56
+ <button class="btn btn-primary" type="submit"
57
+ [disabled]="form.invalid || submitting()" [attr.aria-busy]="submitting()">
58
+ Sign up
59
+ </button>
60
+
61
+ <p class="footnote">
62
+ Already have an account?
63
+ <!-- Use click handler instead of routerLink -->
64
+ <a href="#" (click)="goToLogin(); $event.preventDefault()">Log in</a>
65
+ </p>
66
+ </form>
67
+ </div>
68
+
69
+ <!-- Right (image) -->
70
+ <div class="panel-right">
71
+ <div class="right-image">
72
+ <img src="/assets/user.png" alt="Decorative" />
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </section>
src/app/auth/sign-up/sign-up.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { SignUpComponent } from './sign-up.component';
4
+
5
+ describe('SignUpComponent', () => {
6
+ let component: SignUpComponent;
7
+ let fixture: ComponentFixture<SignUpComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [SignUpComponent]
12
+ });
13
+ fixture = TestBed.createComponent(SignUpComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/auth/sign-up/sign-up.component.ts ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl } from '@angular/forms';
4
+ import { Router, RouterLink } from '@angular/router';
5
+ import { SignupService, SignupPayload } from './sign-up.service';
6
+
7
+ @Component({
8
+ selector: 'app-sign-up',
9
+ standalone: true,
10
+ imports: [CommonModule, ReactiveFormsModule, RouterLink],
11
+ templateUrl: './sign-up.component.html',
12
+ styleUrls: ['./sign-up.component.css']
13
+ })
14
+ export class SignUpComponent {
15
+ form: FormGroup;
16
+ private isSubmitting = false;
17
+
18
+ @Output() switchToSignIn = new EventEmitter<void>();
19
+ @Output() signUpSuccess = new EventEmitter<void>();
20
+
21
+ constructor(private fb: FormBuilder, private router: Router, private signupService: SignupService) {
22
+ this.form = this.fb.group({
23
+ name: ['', [Validators.required, Validators.minLength(2)]],
24
+ contact: ['', [Validators.required, Validators.pattern(/(^[^\s@]+@[^\s@]+\.[^\s@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)]],
25
+ password: ['', [Validators.required, Validators.minLength(6)]],
26
+ confirmPassword: ['', [Validators.required]],
27
+ }, { validators: [this.passwordsMatchValidator] });
28
+ }
29
+
30
+ control(path: string): AbstractControl | null {
31
+ return this.form.get(path);
32
+ }
33
+
34
+ showPwdMismatch(): boolean {
35
+ const pw = this.control('password');
36
+ const cpw = this.control('confirmPassword');
37
+ const groupMismatch = this.form.errors?.['passwordMismatch'];
38
+ return !!(pw && cpw && (cpw.touched || pw.touched) && groupMismatch);
39
+ }
40
+
41
+ passwordsMatchValidator(group: AbstractControl) {
42
+ const pw = group.get('password')?.value;
43
+ const cpw = group.get('confirmPassword')?.value;
44
+ return pw && cpw && pw === cpw ? null : { passwordMismatch: true };
45
+ }
46
+
47
+ submitting(): boolean {
48
+ return this.isSubmitting;
49
+ }
50
+
51
+ async submit() {
52
+ this.form.markAllAsTouched();
53
+
54
+ if (this.form.invalid) {
55
+ return;
56
+ }
57
+
58
+ const payload: SignupPayload = {
59
+ name: this.control('name')?.value,
60
+ email: this.control('contact')?.value,
61
+ password: this.control('password')?.value
62
+ };
63
+
64
+ this.isSubmitting = true;
65
+
66
+ try {
67
+ const res = await this.signupService.signup(payload).toPromise();
68
+ if (res && res.message) {
69
+ alert('Success: ' + res.message);
70
+ this.signUpSuccess.emit(); // Emit success instead of navigating
71
+ } else if (res && res.error) {
72
+ alert('Signup failed: ' + res.error);
73
+ } else {
74
+ alert('Unexpected response');
75
+ }
76
+ } catch (err: any) {
77
+ console.error('Signup error:', err);
78
+ alert('Signup failed due to server or network error.');
79
+ } finally {
80
+ this.isSubmitting = false;
81
+ }
82
+ }
83
+
84
+ controlHasError(path: string, error?: string): boolean {
85
+ const c = this.control(path);
86
+ if (!c) return false;
87
+ return error ? !!(c.touched && c.errors?.[error]) : !!(c.touched && c.invalid);
88
+ }
89
+
90
+ navigateHome() { this.router.navigateByUrl('/'); }
91
+
92
+ goToLogin() {
93
+ this.switchToSignIn.emit();
94
+ }
95
+
96
+ tr(key: string): string {
97
+ const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
98
+ return map[key] || '';
99
+ }
100
+ }
src/app/auth/sign-up/sign-up.service.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
3
+ import { Observable, catchError, throwError } from 'rxjs';
4
+
5
+ import { environment } from '../../../environments/environment';
6
+ export interface SignupPayload {
7
+ name: string;
8
+ email: string; // email or phone
9
+
10
+ password: string;
11
+ }
12
+
13
+ export interface SignupResponse {
14
+ message?: string; // success message from backend
15
+ error?: string; // error message from backend
16
+ }
17
+
18
+ @Injectable({
19
+ providedIn: 'root'
20
+ })
21
+ export class SignupService {
22
+
23
+ // private apiUrl = 'http://localhost:5000/api/signup'; // Replace with your backend URL
24
+ private apiUrl = 'http://localhost:5000/api'; // Replace with your backend URL
25
+ //private base = environment.apiBase; // e.g., http://192.168.29.27:5000
26
+ constructor(private http: HttpClient) { }
27
+
28
+ signup(payload: SignupPayload): Observable<SignupResponse> {
29
+ return this.http.post<SignupResponse>(`${this.apiUrl}/signup`, payload).pipe(
30
+ catchError(this.handleError)
31
+ );
32
+ }
33
+
34
+ private handleError(error: HttpErrorResponse) {
35
+ return throwError(() => new Error(error.message || 'Something went wrong'));
36
+ }
37
+ }
src/app/intro-page/intro-page.component.css ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ .sr-only {
10
+ position: absolute;
11
+ width: 1px;
12
+ height: 1px;
13
+ margin: -1px;
14
+ padding: 0;
15
+ overflow: hidden;
16
+ clip: rect(0,0,0,0);
17
+ border: 0;
18
+ }
19
+
20
+ .skip-link {
21
+ position: absolute;
22
+ left: -9999px;
23
+ top: auto;
24
+ width: 1px;
25
+ height: 1px;
26
+ overflow: hidden;
27
+ }
28
+
29
+ .skip-link:focus {
30
+ left: 16px;
31
+ top: 12px;
32
+ width: auto;
33
+ height: auto;
34
+ padding: 8px 10px;
35
+ background: #fff;
36
+ color: #000;
37
+ border-radius: 6px;
38
+ z-index: 10;
39
+ }
40
+
41
+ .page {
42
+ height: 100vh;
43
+ display: flex;
44
+ flex-direction: column;
45
+ background: url(/assets/4.png) no-repeat center center fixed;
46
+ background-size: cover;
47
+ color: #e5e7eb;
48
+ justify-content: space-around;
49
+ gap: 6vw;
50
+ }
51
+
52
+ /* Topbar */
53
+ .topbar {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: space-around;
57
+ gap: 54vw;
58
+ width: 100%;
59
+ }
60
+
61
+ .brand {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 10px;
65
+ }
66
+
67
+ .brand__logo {
68
+ width: 34px;
69
+ height: 34px;
70
+ border-radius: 8px;
71
+ background: #00ff88;
72
+ }
73
+
74
+ .brand__name {
75
+ letter-spacing: 3.2px;
76
+ font-size: 2.5vw;
77
+ font-family: Roliana;
78
+ font-weight: lighter;
79
+ }
80
+
81
+ .actions {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 8px;
85
+ }
86
+
87
+ /* Controls / buttons */
88
+ .select {
89
+ background: #0b0f1a;
90
+ color: #e5e7eb;
91
+ border: 1px solid #2b3246;
92
+ border-radius: 10px;
93
+ padding: 8px 10px;
94
+ }
95
+
96
+ .btn {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ gap: 8px;
101
+ background: #0b0f1a;
102
+ color: #e5e7eb;
103
+ border: 1px solid #2b3246;
104
+ border-radius: 10px;
105
+ padding: 10px 14px;
106
+ cursor: pointer;
107
+ text-decoration: none;
108
+ border: 2px solid #f3a54c;
109
+ }
110
+
111
+ .btn:hover {
112
+ border-color: #00ff88;
113
+ }
114
+
115
+ .btn-primary {
116
+ background: #f3a54c;
117
+ color: #0b0f1a;
118
+ border: none;
119
+ font-weight: 700;
120
+ }
121
+
122
+ .btn-ghost {
123
+ background: #0b0f1a;
124
+ }
125
+
126
+ /*.btn-disabled {
127
+ opacity: 0.6;
128
+ cursor: not-allowed;
129
+ pointer-events: none;
130
+ }*/
131
+
132
+ .btn-disabled:hover {
133
+ border-color: #f3a54c !important;
134
+ }
135
+
136
+ /* Hero */
137
+ .hero {
138
+ margin-left: 5vw;
139
+ }
140
+
141
+ .hero__content {
142
+ max-width: 620px;
143
+ }
144
+
145
+ .hero__title {
146
+ margin: 0 0 10px;
147
+ font-size: clamp(26px, 4.2vw, 44px);
148
+ line-height: 1.12;
149
+ font-weight: 800;
150
+ }
151
+
152
+ .hero__text {
153
+ color: #b9c2d0;
154
+ margin: 0 0 18px;
155
+ font-size: clamp(14px, 2vw, 16px);
156
+ }
157
+
158
+ .cta {
159
+ display: flex;
160
+ gap: 10px;
161
+ flex-wrap: wrap;
162
+ }
163
+
164
+ /* Footer */
165
+ .footer {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 16px;
169
+ margin-left: 5vw;
170
+ font-size: 0.7vw;
171
+ }
172
+
173
+ .footer a {
174
+ text-decoration: unset;
175
+ align-items: center;
176
+ border: 2px solid #f3a54c;
177
+ color: white;
178
+ border-radius: 2vw;
179
+ display: inline-flex;
180
+ align-items: center;
181
+ border-radius: 8px;
182
+ border: 1px solid rgba(255, 255, 255, .25);
183
+ background: rgba(11, 15, 26, 0.6);
184
+ color: #cfe8ff;
185
+ cursor: pointer;
186
+ padding: 0 4px;
187
+ }
188
+
189
+ .footer a i {
190
+ margin-right: 2px;
191
+ line-height: 1;
192
+ }
193
+
194
+ .about-btn {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ padding: 6px 8px;
199
+ border-radius: 8px;
200
+ border: 1px solid rgba(255,255,255,.25);
201
+ background: rgba(11, 15, 26, 0.6);
202
+ color: #cfe8ff;
203
+ cursor: pointer;
204
+ }
205
+
206
+ .about-btn:hover, .footer a:hover {
207
+ border-color: #00ff88;
208
+ }
209
+
210
+ /* High contrast mode */
211
+ .page.high-contrast {
212
+ background: #000;
213
+ color: #fff;
214
+ }
215
+
216
+ .page.high-contrast a {
217
+ color: #fff;
218
+ text-decoration: underline;
219
+ }
220
+
221
+ .page.high-contrast .card, .page.high-contrast .banner {
222
+ background: #000;
223
+ border: 2px solid #fff;
224
+ }
225
+
226
+ .page.high-contrast .btn, .page.high-contrast .select {
227
+ border-color: #fff !important;
228
+ }
229
+
230
+ .brand__logo-img {
231
+ background: white;
232
+ max-width: 5vw;
233
+ height: auto;
234
+ border-radius: 1vw;
235
+ margin: 0.5vw;
236
+ }
237
+
238
+ .brand__link {
239
+ display: inline-flex;
240
+ align-items: center;
241
+ }
242
+
243
+ /* AUTH Modal overlay */
244
+ .modal-backdrop {
245
+ position: fixed;
246
+ inset: 0;
247
+ background: rgb(255 255 255 / 34%);
248
+ display: grid;
249
+ place-items: center;
250
+ z-index: 1000;
251
+ }
252
+
253
+ button.modal-close {
254
+ border: 3px solid #ff0000;
255
+ background: transparent;
256
+ color: white;
257
+ position: relative;
258
+ left: 47vw;
259
+ top: 1.8vw;
260
+ z-index: 11;
261
+ border-radius: 10px;
262
+ cursor: pointer;
263
+ }
264
+
265
+
266
+
267
+ /* Ensure the auth components fit properly in the modal */
268
+ .modal-panel app-sign-in,
269
+ .modal-panel app-sign-up {
270
+ display: block;
271
+ width: 100%;
272
+ }
273
+
274
+
275
+ /* ABOUT Modal (new) */
276
+ .about-backdrop {
277
+ position: fixed;
278
+ inset: 0;
279
+ background: rgb(255 255 255 / 34%);
280
+ z-index: 1100; /* above auth backdrop */
281
+ }
282
+
283
+ .about-modal {
284
+ position: fixed;
285
+ z-index: 1101; /* above auth modal */
286
+ inset: 50% auto auto 50%;
287
+ transform: translate(-50%, -50%);
288
+ width: min(720px, 92vw);
289
+ max-height: 80vh;
290
+ overflow: auto;
291
+ background: #ffffff;
292
+ color: #111;
293
+ border-radius: 12px;
294
+ box-shadow: 0 20px 50px rgba(0,0,0,.25);
295
+ outline: none;
296
+ }
297
+
298
+ .about-modal__header,
299
+ .about-modal__footer {
300
+ padding: 16px 20px;
301
+ border-bottom: 1px solid rgba(0,0,0,.08);
302
+ display: flex;
303
+ }
304
+
305
+ .about-modal__footer {
306
+ border-top: 1px solid rgba(0,0,0,.08);
307
+ border-bottom: none;
308
+ display: flex;
309
+ justify-content: flex-end;
310
+ gap: 8px;
311
+ }
312
+
313
+ .about-modal__body {
314
+ padding: 16px 20px;
315
+ line-height: 1.6;
316
+ }
317
+
318
+ .about-close {
319
+ margin-left: auto;
320
+ border: none;
321
+ background: transparent;
322
+ font-size: 20px;
323
+ cursor: pointer;
324
+ color: #111;
325
+ }
326
+
327
+ /* Make the embedded auth components look like panels */
328
+ /*:host ::ng-deep app-sign-in .auth-page,
329
+ :host ::ng-deep app-sign-up .auth-page {
330
+ min-height: auto !important;
331
+ padding: 0 !important;
332
+ background: transparent !important;
333
+ }*/
334
+
335
+ /*:host ::ng-deep app-sign-in .auth-box,
336
+ :host ::ng-deep app-sign-up .auth-box {
337
+ width: min(920px, 92vw) !important;
338
+ margin: 0 auto !important;
339
+ }*/
340
+
341
+ /* Optional: if you want a tighter width for Sign-In
342
+ :host ::ng-deep app-sign-in .auth-box {
343
+ width: min(720px, 92vw) !important;
344
+ }
345
+ */
346
+ .modal-backdrop {
347
+ position: fixed;
348
+ inset: 0;
349
+ background: rgb(255 255 255 / 34%);
350
+ display: grid;
351
+ place-items: center;
352
+ z-index: 1000;
353
+ animation: fadeIn 0.2s ease-out;
354
+ }
355
+
356
+ .modal-panel {
357
+ animation: slideIn 0.3s ease-out;
358
+ }
359
+
360
+ @keyframes fadeIn {
361
+ from {
362
+ opacity: 0;
363
+ }
364
+
365
+ to {
366
+ opacity: 1;
367
+ }
368
+ }
369
+
370
+ @keyframes slideIn {
371
+ from {
372
+ opacity: 0;
373
+ transform: translateY(-20px);
374
+ }
375
+
376
+ to {
377
+ opacity: 1;
378
+ transform: translateY(0);
379
+ }
380
+ }
src/app/intro-page/intro-page.component.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <a class="skip-link" href="#main">Skip to content</a>
2
+
3
+ <section class="page">
4
+
5
+ <!-- Top bar -->
6
+ <header class="topbar" role="banner" aria-label="Top navigation">
7
+ <div class="brand" aria-label="Application name">
8
+ <img src="assets/pykara-logo.png" alt="Pykara Technologies logo" class="brand__logo-img" />
9
+ <span class="brand__name">{{ tr('brand') }}</span>
10
+ </div>
11
+
12
+ <nav class="actions" aria-label="Actions">
13
+ <!-- CHANGED: open modal instead of routerLink -->
14
+ <button type="button" class="btn btn-primary" (click)="openSignUp()">{{ tr('signup') }}</button>
15
+ <button type="button" class="btn btn-ghost" (click)="openSignIn()">{{ tr('signin') }}</button>
16
+ </nav>
17
+ </header>
18
+
19
+ <!-- Hero (unchanged) -->
20
+ <main id="main" class="hero" role="main">
21
+ <div class="hero__content">
22
+ <h1 class="hero__title">{{ tr('tagline') }}</h1>
23
+ <p class="hero__text">{{ tr('subtext') }}</p>
24
+ <div class="cta">
25
+ <!-- Updated: Add disabled attribute and tooltip -->
26
+ <a routerLink="/question-answer"
27
+ class="btn btn-primary"
28
+ [class.btn-disabled]="!isSignedIn"
29
+ [attr.aria-disabled]="!isSignedIn"
30
+ [title]="!isSignedIn ? 'Please sign in first' : ''">
31
+ {{ tr('getStarted') }}
32
+ </a>
33
+ <small *ngIf="!isSignedIn" style="display: block; margin-top: 8px; color: #ffa500;">
34
+ Please sign in to get started
35
+ </small>
36
+ </div>
37
+ </div>
38
+ </main>
39
+
40
+ <!-- Footer (updated) -->
41
+ <footer class="footer" aria-label="Footer">
42
+ <a href="https://pykara.net/"
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ aria-label="Visit Pykara website">
46
+ <i class="fa-solid fa-globe" aria-hidden="true"></i>
47
+ <span style="font-family: sans-serif">Visit Pykara</span>
48
+ </a>
49
+
50
+ <!-- About us button -->
51
+ <button type="button"
52
+ class="about-btn"
53
+ (click)="openAbout()"
54
+ aria-haspopup="dialog"
55
+ aria-controls="aboutDialog"
56
+ [attr.aria-expanded]="isAboutOpen ? 'true' : 'false'">
57
+ <i class="fa-solid fa-circle-info" aria-hidden="true"></i>
58
+ <span>About us</span>
59
+ </button>
60
+ </footer>
61
+
62
+ <!-- AUTH MODAL OVERLAY (unchanged) -->
63
+ <div class="modal-backdrop" *ngIf="modal" (click)="backdropClick($event)">
64
+ <div class="modal-panel modal-panel--white"
65
+ role="dialog"
66
+ aria-modal="true"
67
+ [attr.aria-label]="modal === 'signin' ? 'Sign in' : 'Sign up'">
68
+ <button type="button" class="modal-close" (click)="closeModal()" aria-label="Close">×</button>
69
+
70
+ <app-sign-in *ngIf="modal === 'signin'"
71
+ (switchToSignUp)="openSignUp()"
72
+ (signInSuccess)="handleSignInSuccess()"></app-sign-in>
73
+ <app-sign-up *ngIf="modal === 'signup'"
74
+ (switchToSignIn)="openSignIn()"
75
+ (signUpSuccess)="handleSignUpSuccess()"></app-sign-up> <!-- Add this line -->
76
+ </div>
77
+ </div>
78
+
79
+ <!-- ABOUT MODAL OVERLAY (new) -->
80
+ <div class="about-backdrop" *ngIf="isAboutOpen" (click)="closeAbout()" tabindex="-1"></div>
81
+
82
+ <div class="about-modal"
83
+ *ngIf="isAboutOpen"
84
+ role="dialog"
85
+ aria-modal="true"
86
+ aria-labelledby="aboutTitle"
87
+ id="aboutDialog"
88
+ tabindex="-1">
89
+ <div class="about-modal__header">
90
+ <h2 id="aboutTitle">About Py-Match</h2>
91
+ <button class="about-close" (click)="closeAbout()" aria-label="Close dialog">×</button>
92
+ </div>
93
+
94
+ <div class="about-modal__body">
95
+ <p><strong>Py-Match</strong> is an AI-driven personality profiling and matching system that models human traits using four colors — Red, Blue, Green, and Yellow. It conducts an adaptive Q&amp;A session where each response secretly maps to a color; the engine then computes a percentage distribution across the four colors to describe the user’s dominant and supporting traits.</p>
96
+ <p>The platform applies these profiles to practical scenarios — such as marriage compatibility, recruitment, leadership assessment, and personal development — by comparing user profiles to role templates with weighted color mixes. The system architecture includes a web/mobile UI, an AI question generator, a scoring and profile calculator, a matching engine, and secure data storage. The design emphasizes simplicity, scalability, localization, and privacy.</p>
97
+ </div>
98
+ </div>
99
+
100
+ </section>
src/app/intro-page/intro-page.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { IntroPageComponent } from './intro-page.component';
4
+
5
+ describe('IntroPageComponent', () => {
6
+ let component: IntroPageComponent;
7
+ let fixture: ComponentFixture<IntroPageComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [IntroPageComponent]
12
+ });
13
+ fixture = TestBed.createComponent(IntroPageComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/intro-page/intro-page.component.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, ChangeDetectionStrategy, HostListener } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { RouterLink } from '@angular/router';
4
+ import { SignInComponent } from '../auth/sign-in/sign-in.component';
5
+ import { SignUpComponent } from '../auth/sign-up/sign-up.component';
6
+
7
+ type AuthModal = 'signin' | 'signup' | null;
8
+
9
+ @Component({
10
+ selector: 'app-intro-page',
11
+ standalone: true,
12
+ imports: [CommonModule, RouterLink, SignInComponent, SignUpComponent],
13
+ templateUrl: './intro-page.component.html',
14
+ styleUrls: ['./intro-page.component.css'],
15
+ changeDetection: ChangeDetectionStrategy.OnPush
16
+ })
17
+ export class IntroPageComponent {
18
+ modal: AuthModal = null;
19
+ isSignedIn = false;
20
+ isAboutOpen = false;
21
+
22
+ // --- Auth modal handlers ---
23
+ openSignIn(): void {
24
+ this.modal = 'signin';
25
+ document.body.style.overflow = 'hidden';
26
+ }
27
+
28
+ openSignUp(): void {
29
+ this.modal = 'signup';
30
+ document.body.style.overflow = 'hidden';
31
+ }
32
+
33
+ closeModal(): void {
34
+ this.modal = null;
35
+ if (!this.isAboutOpen) document.body.style.overflow = '';
36
+ }
37
+
38
+
39
+ // Called by child components on success
40
+ handleSignInSuccess(): void {
41
+ this.isSignedIn = true;
42
+ this.closeModal();
43
+ }
44
+
45
+ // Handle sign-up success - switch to sign-in modal
46
+ handleSignUpSuccess(): void {
47
+ // Close sign-up and open sign-in
48
+ this.modal = 'signin';
49
+ // You can optionally show a success message here
50
+ alert('Sign up successful! Please sign in to continue.');
51
+ }
52
+
53
+ backdropClick(e: MouseEvent): void {
54
+ if ((e.target as HTMLElement).classList.contains('modal-backdrop')) {
55
+ this.closeModal();
56
+ }
57
+ }
58
+
59
+ // --- About modal handlers ---
60
+ openAbout(): void {
61
+ this.isAboutOpen = true;
62
+ document.body.style.overflow = 'hidden';
63
+ // Move focus into the dialog for accessibility
64
+ setTimeout(() => document.getElementById('aboutDialog')?.focus(), 0);
65
+ }
66
+
67
+ closeAbout(): void {
68
+ this.isAboutOpen = false;
69
+ // If Auth is also open, keep scroll locked. Otherwise restore.
70
+ if (!this.modal) document.body.style.overflow = '';
71
+ }
72
+
73
+ // --- Global ESC handler closes whichever is open ---
74
+ @HostListener('document:keydown.escape')
75
+ onEsc(): void {
76
+ if (this.modal) this.closeModal();
77
+ else if (this.isAboutOpen) this.closeAbout();
78
+ }
79
+
80
+ // Your existing i18n helper, keep as-is if already present:
81
+ tr(key: string): string {
82
+ const dict = {
83
+ brand: 'Py-Match',
84
+ signup: 'Sign up',
85
+ signin: 'Sign in',
86
+ tagline: 'Better match starts here',
87
+ subtext: 'Designed for trust, focused on fit.',
88
+ getStarted: 'Get started'
89
+ };
90
+ return dict[key as keyof typeof dict] ?? key;
91
+ }
92
+ }
src/app/llm-quiz/llm-quiz.component.css ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*.quiz-wrap {
2
+ max-width: 880px;
3
+ margin: 20px auto;
4
+ padding: 12px;
5
+ font-family: system-ui, Arial, sans-serif;
6
+ }
7
+
8
+ h2 {
9
+ margin: 0 0 12px;
10
+ }
11
+
12
+ .panel, .qcard, .result {
13
+ border: 1px solid #ddd;
14
+ border-radius: 8px;
15
+ padding: 12px;
16
+ background: #fff;
17
+ margin-bottom: 14px;
18
+ }
19
+
20
+ .row {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 12px;
24
+ margin-bottom: 10px;
25
+ }
26
+
27
+ .row.column {
28
+ flex-direction: column;
29
+ align-items: stretch;
30
+ }
31
+
32
+ .row label {
33
+ width: 180px;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .row input[type="number"], .row select, textarea {
38
+ flex: 1;
39
+ padding: 8px;
40
+ border: 1px solid #ccc;
41
+ border-radius: 6px;
42
+ }
43
+
44
+ .actions {
45
+ display: flex;
46
+ gap: 10px;
47
+ margin-top: 8px;
48
+ }
49
+
50
+ .actions button {
51
+ padding: 8px 14px;
52
+ border: 0;
53
+ border-radius: 6px;
54
+ background: #111827;
55
+ color: #fff;
56
+ cursor: pointer;
57
+ }
58
+
59
+ .actions button[disabled] {
60
+ opacity: 0.5;
61
+ cursor: not-allowed;
62
+ }
63
+
64
+ .error {
65
+ color: #b91c1c;
66
+ margin-top: 8px;
67
+ }
68
+
69
+ .loading {
70
+ margin-top: 10px;
71
+ }
72
+
73
+ .meta {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ margin-bottom: 8px;
77
+ color: #374151;
78
+ font-size: 13px;
79
+ }
80
+
81
+ .question {
82
+ font-size: 18px;
83
+ font-weight: 700;
84
+ margin-bottom: 10px;
85
+ }
86
+
87
+ .options {
88
+ display: grid;
89
+ gap: 8px;
90
+ }
91
+
92
+ .opt {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 10px;
96
+ padding: 8px;
97
+ border: 1px solid #eee;
98
+ border-radius: 6px;
99
+ }
100
+
101
+ .opt input {
102
+ transform: scale(1.1);
103
+ }
104
+
105
+ .badge {
106
+ display: inline-block;
107
+ padding: 3px 8px;
108
+ border-radius: 999px;
109
+ font-size: 12px;
110
+ color: #fff;
111
+ }
112
+
113
+ .badge.blue {
114
+ background: #2563eb;
115
+ }
116
+
117
+ .badge.green {
118
+ background: #059669;
119
+ }
120
+
121
+ .badge.red {
122
+ background: #dc2626;
123
+ }
124
+
125
+ .badge.yellow {
126
+ background: #d97706;
127
+ }
128
+
129
+ .mix, .result {
130
+ margin-top: 14px;
131
+ }
132
+
133
+ .bar {
134
+ display: grid;
135
+ grid-template-columns: 90px 1fr 48px;
136
+ align-items: center;
137
+ gap: 8px;
138
+ margin: 6px 0;
139
+ }
140
+
141
+ .bar-label {
142
+ font-weight: 600;
143
+ }
144
+
145
+ .bar-track {
146
+ height: 10px;
147
+ background: #f3f4f6;
148
+ border-radius: 999px;
149
+ overflow: hidden;
150
+ }
151
+
152
+ .bar-fill {
153
+ height: 100%;
154
+ }
155
+
156
+ .bar-fill.blue {
157
+ background: #93c5fd;
158
+ }
159
+
160
+ .bar-fill.green {
161
+ background: #86efac;
162
+ }
163
+
164
+ .bar-fill.red {
165
+ background: #fca5a5;
166
+ }
167
+
168
+ .bar-fill.yellow {
169
+ background: #fde68a;
170
+ }
171
+
172
+ .bar-val {
173
+ text-align: right;
174
+ font-variant-numeric: tabular-nums;
175
+ }
176
+ */
177
+
178
+
179
+ .panel {
180
+ padding: 12px;
181
+ border: 1px solid #ddd;
182
+ border-radius: 8px;
183
+ margin-bottom: 16px;
184
+ }
185
+
186
+ .row {
187
+ display: flex;
188
+ gap: 8px;
189
+ align-items: center;
190
+ margin: 8px 0;
191
+ }
192
+
193
+ .row label {
194
+ width: 160px;
195
+ }
196
+
197
+ .actions {
198
+ margin-top: 12px;
199
+ display: flex;
200
+ gap: 8px;
201
+ }
202
+
203
+ .loader-overlay {
204
+ position: fixed;
205
+ inset: 0;
206
+ display: grid;
207
+ place-items: center;
208
+ background: rgba(0,0,0,0.08);
209
+ }
210
+
211
+ .question-card {
212
+ padding: 16px;
213
+ border: 1px solid #eee;
214
+ border-radius: 8px;
215
+ }
216
+
217
+ .q-meta {
218
+ display: flex;
219
+ gap: 16px;
220
+ color: #666;
221
+ margin-bottom: 8px;
222
+ }
223
+
224
+ .q-text {
225
+ margin: 8px 0 16px;
226
+ }
227
+
228
+ .options {
229
+ display: grid;
230
+ gap: 8px;
231
+ }
232
+
233
+ .option {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 8px;
237
+ }
238
+
239
+ .opt-color {
240
+ color: #888;
241
+ }
242
+
243
+ .progress, .result {
244
+ margin-top: 16px;
245
+ }
246
+
247
+ .bar {
248
+ display: grid;
249
+ grid-template-columns: 80px 1fr 60px;
250
+ align-items: center;
251
+ gap: 8px;
252
+ margin: 6px 0;
253
+ }
254
+
255
+ .bar-track {
256
+ height: 10px;
257
+ background: #eee;
258
+ border-radius: 8px;
259
+ overflow: hidden;
260
+ }
261
+
262
+ .bar-fill {
263
+ height: 100%;
264
+ background: #7dafff;
265
+ }
266
+
267
+ .bar-val {
268
+ text-align: right;
269
+ }
270
+
271
+ .empty {
272
+ color: #777;
273
+ margin-top: 16px;
274
+ }
src/app/llm-quiz/llm-quiz.component.html ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Control panel -->
2
+ <!--<section class="panel">
3
+ <div class="row">
4
+ <label>User ID</label>
5
+ <input type="text" [(ngModel)]="userId" placeholder="e.g. 1" />
6
+ </div>
7
+
8
+ <div class="row">
9
+ <label>Role</label>
10
+ <select [(ngModel)]="role">
11
+ <option value="interview">interview</option>
12
+ <option value="marriage">marriage</option>
13
+ <option value="partnership">partnership</option>
14
+ <option value="team">team</option>
15
+ <option value="general">general</option>
16
+ <option value="assistant">assistant</option>
17
+ <option value="ceo">ceo</option>
18
+ </select>
19
+ </div>
20
+
21
+ <div class="row">
22
+ <label>No. of questions</label>
23
+ <input type="number" [(ngModel)]="nQuestions" min="1" max="50" />
24
+ </div>
25
+
26
+ <div class="row">
27
+ <label>Batch size</label>
28
+ <input type="number" [(ngModel)]="batchSize" min="1" max="20" />
29
+ </div>
30
+
31
+ <div class="actions">
32
+ <button type="button" (click)="start()" [disabled]="loading">Start</button>
33
+ <button type="button" (click)="restart()" [disabled]="loading">Reset</button>
34
+ </div>
35
+
36
+ <p class="error" *ngIf="errorMsg">{{ errorMsg }}</p>
37
+ </section>-->
38
+
39
+ <!-- Loader -->
40
+ <div class="loader-overlay" *ngIf="loading">
41
+ <div class="loader">Loading…</div>
42
+ </div>
43
+
44
+ <!-- Quiz area -->
45
+ <section class="quiz" *ngIf="!loading">
46
+
47
+ <!-- When there is an active question -->
48
+ <div class="question-card" *ngIf="!isDone && question">
49
+ <div class="q-meta">
50
+ <div class="q-count">Question {{ index }} / {{ total }}</div>
51
+ <div class="q-role">Role: {{ role }}</div>
52
+ </div>
53
+
54
+ <h2 class="q-text">{{ question }}</h2>
55
+
56
+ <form (ngSubmit)="submitAnswer()">
57
+ <div class="options">
58
+ <label class="option" *ngFor="let opt of options; let i = index">
59
+ <input type="radio"
60
+ name="answer"
61
+ [value]="opt.color"
62
+ [(ngModel)]="selectedColor"
63
+ required />
64
+ <span class="opt-text">{{ opt.text }}</span>
65
+ <!--<small class="opt-color">({{ opt.color }})</small>-->
66
+ </label>
67
+ </div>
68
+
69
+ <div class="actions">
70
+ <button type="submit" [disabled]="!selectedColor || loading">Submit</button>
71
+ </div>
72
+ </form>
73
+
74
+ <!-- In-progress mix (live) -->
75
+ <!--<div class="progress" *ngIf="inProgressMix">
76
+ <h3>Current mix</h3>
77
+ <div class="bar">
78
+ <span>Blue</span>
79
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('blue')"></div></div>
80
+ <div class="bar-val">{{ asPercent(getInProgressMixValue('blue')) }}%</div>
81
+ </div>
82
+
83
+ <div class="bar">
84
+ <span>Green</span>
85
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('green')"></div></div>
86
+ <div class="bar-val">{{ asPercent(getInProgressMixValue('green')) }}%</div>
87
+ </div>
88
+
89
+ <div class="bar">
90
+ <span>Red</span>
91
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('red')"></div></div>
92
+ <div class="bar-val">{{ asPercent(getInProgressMixValue('red')) }}%</div>
93
+ </div>
94
+
95
+ <div class="bar">
96
+ <span>Yellow</span>
97
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('yellow')"></div></div>
98
+ <div class="bar-val">{{ asPercent(getInProgressMixValue('yellow')) }}%</div>
99
+ </div>
100
+ </div>-->
101
+ </div>
102
+
103
+ <!-- Final result -->
104
+ <div>DONE</div>
105
+ <!--<div class="result" *ngIf="isDone && finalMix">
106
+ <h2>Final result</h2>
107
+
108
+ <div class="bar">
109
+ <span>Blue</span>
110
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('blue')"></div></div>-->
111
+ <!--<div class="bar-val">{{ asPercent(getFinalMixValue('blue')) }}%</div>-->
112
+ <!--</div>
113
+
114
+ <div class="bar">
115
+ <span>Green</span>
116
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('green')"></div></div>-->
117
+ <!--<div class="bar-val">{{ asPercent(getFinalMixValue('green')) }}%</div>-->
118
+ <!--</div>
119
+
120
+ <div class="bar">
121
+ <span>Red</span>
122
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('red')"></div></div>-->
123
+ <!--<div class="bar-val">{{ asPercent(getFinalMixValue('red')) }}%</div>-->
124
+ <!--</div>
125
+
126
+ <div class="bar">
127
+ <span>Yellow</span>
128
+ <div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('yellow')"></div></div>-->
129
+ <!--<div class="bar-val">{{ asPercent(getFinalMixValue('yellow')) }}%</div>-->
130
+ <!--</div>
131
+
132
+ <div class="actions">
133
+ <button type="button" (click)="restart()">Restart</button>
134
+ </div>
135
+ </div>-->
136
+
137
+ <!-- Empty state -->
138
+ <div class="empty" *ngIf="!isDone && !question && !errorMsg">
139
+ <p>Press <strong>Start</strong> to begin the quiz.</p>
140
+ </div>
141
+ </section>
142
+
src/app/llm-quiz/llm-quiz.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { LlmQuizComponent } from './llm-quiz.component';
4
+
5
+ describe('LlmQuizComponent', () => {
6
+ let component: LlmQuizComponent;
7
+ let fixture: ComponentFixture<LlmQuizComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ imports: [LlmQuizComponent]
12
+ });
13
+ fixture = TestBed.createComponent(LlmQuizComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/llm-quiz/llm-quiz.component.ts ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { FormsModule } from '@angular/forms';
3
+ import { CommonModule } from '@angular/common';
4
+ import { LlmQaService, Role, ColorKey, StartResponse, NextResponse } from '../services/llm-qa.service';
5
+ import { ActivatedRoute, Router } from '@angular/router';
6
+
7
+ type Mix = Record<ColorKey, number>;
8
+
9
+ @Component({
10
+ selector: 'app-llm-quiz',
11
+ standalone: true,
12
+ imports: [CommonModule, FormsModule],
13
+ templateUrl: './llm-quiz.component.html',
14
+ styleUrls: ['./llm-quiz.component.css'],
15
+ })
16
+ export class LlmQuizComponent implements OnInit {
17
+ // --- Config bound to the UI ---
18
+ role: Role = 'marriage';
19
+ nQuestions = 5;
20
+ batchSize = 5;
21
+
22
+ /** Provide user id. Will auto-read from localStorage('user_id') if available. */
23
+ userId = '';
24
+
25
+ // --- Runtime state ---
26
+ sessionId = '';
27
+ index = 0;
28
+ total = 0;
29
+
30
+ question = '';
31
+ options: { text: string; color: ColorKey }[] = [];
32
+ selectedColor: ColorKey | '' = '';
33
+
34
+ loading = false;
35
+ errorMsg = '';
36
+ isDone = false;
37
+
38
+ inProgressMix: Mix | null = null; // live progress returned by backend
39
+ finalMix: Mix | null = null;
40
+
41
+ /*constructor(private api: LlmQaService) { }*/
42
+
43
+
44
+ constructor(
45
+ private api: LlmQaService,
46
+ private route: ActivatedRoute,
47
+ private router: Router
48
+ ) { }
49
+
50
+ ngOnInit(): void {
51
+ const qp = this.route.snapshot.queryParamMap;
52
+ const uid = (qp.get('user_id') || localStorage.getItem('user_id') || '').trim();
53
+ const role = (qp.get('role') || localStorage.getItem('role') || '').toLowerCase();
54
+ const autostart = qp.get('autostart') === '1';
55
+
56
+ if(uid) this.userId = uid;
57
+ if(role) this.role = role as any;
58
+
59
+ if(autostart && this.userId && this.role) {
60
+ this.start();
61
+ }
62
+ }
63
+
64
+
65
+ // ---------- Helpers ----------
66
+ asPercent(v: number | undefined | null): number {
67
+ const n = Number(v);
68
+ return Number.isFinite(n) ? Math.round(n) : 0;
69
+ }
70
+
71
+ /** Safe getter for in-progress mix values. */
72
+ getInProgressMixValue(c: string): number {
73
+ if (!this.inProgressMix) return 0;
74
+ const key = this.toColorKey(c);
75
+ return key ? this.inProgressMix[key] ?? 0 : 0;
76
+ }
77
+
78
+ /** Safe getter for final mix values. */
79
+ getFinalMixValue(c: string): number {
80
+ if (!this.finalMix) return 0;
81
+ const key = this.toColorKey(c);
82
+ return key ? this.finalMix[key] ?? 0 : 0;
83
+ }
84
+
85
+ private toColorKey(c: string): ColorKey | null {
86
+ switch (c) {
87
+ case 'blue':
88
+ case 'green':
89
+ case 'red':
90
+ case 'yellow':
91
+ return c;
92
+ default:
93
+ return null;
94
+ }
95
+ }
96
+
97
+ // ---------- Flow ----------
98
+ //start(): void {
99
+ // this.errorMsg = '';
100
+ // this.isDone = false;
101
+ // this.finalMix = null;
102
+ // this.inProgressMix = null;
103
+ // this.selectedColor = '';
104
+ // this.sessionId = '';
105
+ // this.index = 0;
106
+ // this.total = 0;
107
+ // this.question = '';
108
+ // this.options = [];
109
+
110
+ // if (!this.userId?.trim()) {
111
+ // this.errorMsg = 'User ID is required.';
112
+ // return;
113
+ // }
114
+
115
+ // this.loading = true;
116
+ // this.api .start({
117
+
118
+ // user_id: this.userId.trim(),
119
+ // role: this.role,
120
+ // n_questions: this.nQuestions,
121
+ // batch_size: this.batchSize,
122
+ // })
123
+ // .subscribe({
124
+ // next: (res: StartResponse) => {
125
+ // this.loading = false;
126
+ // this.sessionId = res.session_id;
127
+ // this.index = res.index;
128
+ // this.total = res.total;
129
+ // this.question = res.question;
130
+ // this.options = (res.options || []) as any;
131
+ // // Optional: inspect res.profile_used
132
+ // if (res.profile_used === false) {
133
+ // this.errorMsg =
134
+ // 'No profile found for this user and role. Please complete the profile first.';
135
+ // }
136
+ // },
137
+ // error: (err) => {
138
+ // this.loading = false;
139
+ // this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
140
+ // },
141
+ // });
142
+ //}
143
+ start(): void {
144
+ this.errorMsg = '';
145
+ this.isDone = false;
146
+ this.finalMix = null;
147
+ this.inProgressMix = null;
148
+ this.selectedColor = '';
149
+ this.sessionId = '';
150
+ this.index = 0;
151
+ this.total = 0;
152
+ this.question = '';
153
+ this.options = [];
154
+
155
+ if (!this.userId?.trim()) {
156
+ this.errorMsg = 'User ID is required.';
157
+ return;
158
+ }
159
+
160
+ this.loading = true;
161
+
162
+ // Log the payload before the API request
163
+ const payload = {
164
+ user_id: this.userId.trim(),
165
+ role: this.role,
166
+ n_questions: this.nQuestions,
167
+ batch_size: this.batchSize,
168
+ };
169
+
170
+ console.log('Payload:', payload); // Add this line to log the payload
171
+
172
+ this.api.start(payload).subscribe({
173
+ next: (res: StartResponse) => {
174
+ this.loading = false;
175
+ this.sessionId = res.session_id;
176
+ this.index = res.index;
177
+ this.total = res.total;
178
+ this.question = res.question;
179
+ this.options = (res.options || []) as any;
180
+ console.log("res", res);
181
+ // Optional: inspect res.profile_used
182
+ if (res.profile_used === false) {
183
+ this.errorMsg =
184
+ 'No profile found for this user and role. Please complete the profile first.';
185
+ }
186
+ },
187
+ error: (err) => {
188
+ this.loading = false;
189
+ this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
190
+ },
191
+ });
192
+ }
193
+
194
+ submitAnswer(): void {
195
+ if (!this.sessionId || !this.selectedColor) return;
196
+ this.loading = true;
197
+ this.errorMsg = '';
198
+
199
+ this.api
200
+ .next({ session_id: this.sessionId, selected_color: this.selectedColor })
201
+ .subscribe({
202
+ next: (res: NextResponse) => {
203
+ this.loading = false;
204
+
205
+ // Finished?
206
+ if (res.done) {
207
+ this.isDone = true;
208
+ this.finalMix = (res.mix || {
209
+ blue: 0,
210
+ green: 0,
211
+ red: 0,
212
+ yellow: 0,
213
+ }) as Mix;
214
+ return;
215
+ }
216
+
217
+ // Continue
218
+ this.index = res.index ?? this.index + 1;
219
+ this.total = res.total ?? this.total;
220
+ this.question = res.question || '';
221
+ this.options = (res.options || []) as any;
222
+ this.inProgressMix = (res.progress || null) as Mix;
223
+ this.selectedColor = ''; // reset selection
224
+ },
225
+ error: (err) => {
226
+ this.loading = false;
227
+ this.errorMsg = err?.error?.error || 'Failed to fetch next question.';
228
+ },
229
+ });
230
+ }
231
+
232
+ restart(): void {
233
+ this.isDone = false;
234
+ this.finalMix = null;
235
+ this.inProgressMix = null;
236
+ this.selectedColor = '';
237
+ this.sessionId = '';
238
+ this.index = 0;
239
+ this.total = 0;
240
+ this.question = '';
241
+ this.options = [];
242
+ this.errorMsg = '';
243
+ }
244
+ }
src/app/question-answer/question-answer-service.service.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { environment } from '../../environments/environment';
4
+ import { Observable } from 'rxjs';
5
+ export interface QAItem {
6
+ label: string;
7
+ input_type: 'text' | 'date' | 'radio' | 'select';
8
+ options?: string[];
9
+ column_key: string;
10
+ }
11
+ @Injectable({ providedIn: 'root' })
12
+ export class QuestionAnswerService {
13
+ private baseUrl = 'http://127.0.0.1:5000/api/questions';
14
+
15
+ constructor(private http: HttpClient) { }
16
+
17
+ // Assign role to user
18
+ assignRole(payload: { user_id: number; role_name: string; assigned_at: string }): Observable<any> {
19
+ return this.http.post<any>(`${this.baseUrl}/select-role`, payload);
20
+ }
21
+
22
+ // Fetch questions by role
23
+
24
+ //getQuestions(role: string): Observable<any> {
25
+ // return this.http.get<any>(`${this.baseUrl}/${role}`);
26
+ //}
27
+
28
+
29
+ // Submit answers
30
+ //submitAnswers(role: string, userId: number, answers: any[]): Observable<any> {
31
+ // const payload = {
32
+ // user_id: userId,
33
+ // ...answers.reduce((acc, ans, i) => {
34
+ // acc[`q${i + 1}`] = ans; // map answers to fields (adjust backend expected fields if needed)
35
+ // return acc;
36
+ // }, {}),
37
+ // created_at: new Date().toISOString()
38
+ // };
39
+
40
+ // return this.http.post(`${this.baseUrl}/submit-answers/${role}`, payload);
41
+ //}
42
+ getQuestions(role: string): Observable<QAItem[]> {
43
+ return this.http.get<QAItem[]>(`${this.baseUrl}/${role}`);
44
+ }
45
+
46
+ // fields = { full_name: 'John', created_at: '...' }
47
+ submitAnswers(role: string, userId: number, fields: Record<string, any>): Observable<any> {
48
+ return this.http.post(`${this.baseUrl}/submit-answers/${role}`, { user_id: userId, ...fields });
49
+ }
50
+ }
src/app/question-answer/question-answer.component.css ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ---------- Page & Topbar ---------- */
2
+ .qa-page {
3
+ height: 100%;
4
+ position: relative;
5
+ background-image: url('/assets/Q&A-BG.png'); /* Set the background image */
6
+ background-size: cover; /* Ensure the image covers the entire area */
7
+ background-position: center center; /* Center the background image */
8
+ color: #1a1a1a;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .topbar {
13
+ position: relative;
14
+ z-index: 2;
15
+ display: grid;
16
+ grid-template-columns: auto 1fr auto;
17
+ align-items: center;
18
+ gap: 12px;
19
+ padding: 14px 18px;
20
+ background: transparent;
21
+ }
22
+
23
+ .home-btn {
24
+ appearance: none;
25
+ border: 1px solid rgba(0,0,0,0.08);
26
+ background: rgba(255,255,255,0.9);
27
+ backdrop-filter: blur(6px);
28
+ padding: 8px 12px;
29
+ border-radius: 10px;
30
+ cursor: pointer;
31
+ font-weight: 600;
32
+ transition: transform .15s ease, box-shadow .15s ease;
33
+ }
34
+
35
+ .home-btn:hover {
36
+ transform: translateY(-1px);
37
+ box-shadow: 0 8px 20px rgba(0,0,0,0.08);
38
+ border: 1px solid #f3a54c;
39
+ }
40
+
41
+ .home-btn:active {
42
+ transform: translateY(0);
43
+ }
44
+
45
+ .home-btn:focus-visible {
46
+ outline: 3px solid #7cc4ff;
47
+ outline-offset: 2px;
48
+ }
49
+
50
+ .title {
51
+ margin: 0;
52
+ text-align: center;
53
+ color: #3a3a3a;
54
+ font-size: 3vw;
55
+ color: white;
56
+ }
57
+
58
+ .topbar-spacer {
59
+ width: 52px;
60
+ height: 1px;
61
+ }
62
+
63
+ /* ---------- Ambient soft background (subtle stripes + light blobs) ---------- */
64
+ .ambient {
65
+ position: absolute;
66
+ inset: 0;
67
+ z-index: 0;
68
+ pointer-events: none;
69
+ background: #e9cfad57;
70
+ opacity: 0.8;
71
+ top: 50%;
72
+ left: 50%;
73
+ transform: translate(-50%, -50%);
74
+ height: 28vw;
75
+ margin: 0 auto;
76
+ border-radius: 8vw;
77
+ border: 2px solid #e9cfad;
78
+ }
79
+
80
+ .stage {
81
+ position: absolute;
82
+ z-index: 1;
83
+ top: 50%;
84
+ left: 50%;
85
+ transform: translate(-50%, -50%);
86
+ }
87
+
88
+ /* Shared orb styles */
89
+ .orb {
90
+ --size: clamp(160px, 26vw, 260px);
91
+ width: var(--size);
92
+ height: var(--size);
93
+ border-radius: 50%;
94
+ display: grid;
95
+ place-items: center;
96
+ border: 10px solid rgba(255,255,255,0.9); /* white rim to mimic gaps */
97
+ box-shadow: 0 28px 60px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.6);
98
+ position: absolute;
99
+ cursor: pointer;
100
+ text-align: center;
101
+ text-transform: uppercase;
102
+ font-weight: 800;
103
+ letter-spacing: .04em;
104
+ transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
105
+ }
106
+
107
+ /* Color palette inspired by your reference image */
108
+ .orb-top {
109
+ background: #595a5e;
110
+ color: #ffffff;
111
+ }
112
+ /* dark grey */
113
+ .orb-left {
114
+ background: #f3a54c;
115
+ color: #2b2316;
116
+ }
117
+ /* warm orange */
118
+ .orb-right {
119
+ background: #d2a784;
120
+ color: #2a1e17;
121
+ }
122
+ /* soft tan */
123
+
124
+ /* Placement to form a neat triad */
125
+ .orb-top {
126
+ bottom: -2vw;
127
+ right: -6vw;
128
+ }
129
+
130
+ .orb-left {
131
+ left: -2vw;
132
+ top: -2vw;
133
+ }
134
+
135
+ .orb-right {
136
+ right: 0vw;
137
+ top: -2vw;
138
+ }
139
+
140
+ /* Hover / focus interactions */
141
+ .orb:hover {
142
+ /*transform: translate(-50%, 0) scale(1.02);*/
143
+ box-shadow: 0 36px 80px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.65);
144
+ transform: translate(0,0) scale(1.02);
145
+ }
146
+
147
+ .orb-left:hover {
148
+ transform: translate(0,0) scale(1.02);
149
+ }
150
+
151
+ .orb-right:hover {
152
+ transform: translate(0,0) scale(1.02);
153
+ }
154
+
155
+ .orb:focus-visible {
156
+ outline: 4px solid #7cc4ff;
157
+ outline-offset: 3px;
158
+ }
159
+
160
+ /* Text inside circles */
161
+ .orb > span {
162
+ line-height: 1.15;
163
+ font-size: clamp(13px, 1.6vw, 18px);
164
+ padding: 0 18px;
165
+ }
166
+
167
+
168
+ /* Modal Overlay */
169
+ .modal-overlay {
170
+ position: fixed;
171
+ top: 0;
172
+ left: 0;
173
+ right: 0;
174
+ bottom: 0;
175
+ background: rgba(0, 0, 0, 0.7);
176
+ display: flex;
177
+ justify-content: center;
178
+ align-items: center;
179
+ z-index: 1000;
180
+ animation: 0.3s ease-in-out;
181
+ }
182
+
183
+ /* Modal Content */
184
+ .modal {
185
+ display: flex;
186
+ border-radius: 20px;
187
+ width: 70%;
188
+ max-height: 80%;
189
+ overflow-y: auto;
190
+ position: relative;
191
+ animation: slideUp 0.3s ease-out;
192
+ }
193
+
194
+ /* Left Side: Header and Image */
195
+ .modal .left-side {
196
+ width: 50%;
197
+ background: url('/assets/popup.png') no-repeat center center;
198
+ background-size: cover;
199
+ position: relative;
200
+ padding: 20px;
201
+ display: flex;
202
+ justify-content: flex-start;
203
+ align-items: flex-start;
204
+ }
205
+
206
+ .modal h2 {
207
+ font-size: 24px;
208
+ font-weight: 700;
209
+ color: #fff;
210
+ position: absolute;
211
+ top: 20px;
212
+ left: 20px;
213
+ z-index: 2;
214
+ background-color: rgba(0, 0, 0, 0.5); /* Background for readability */
215
+ padding: 5px 10px;
216
+ border-radius: 5px;
217
+ }
218
+
219
+ /* Right Side: Questions and Options */
220
+ .modal .right-side {
221
+ width: 50%;
222
+ padding: 20px;
223
+ color: #eb4814;
224
+ background-color: #fbeccf;
225
+ overflow-y: auto;
226
+ }
227
+
228
+ /* Modal Question Styling */
229
+ .modal label {
230
+ display: block;
231
+ font-size: 18px;
232
+ font-weight: 500;
233
+ margin-bottom: 10px;
234
+ color: #000000;
235
+ }
236
+
237
+ .modal div {
238
+ margin-bottom: 20px;
239
+ }
240
+
241
+ .modal input[type="radio"] {
242
+ margin-right: 10px;
243
+ }
244
+
245
+ .modal input[type="text"] {
246
+ padding: 10px;
247
+ border-radius: 5px;
248
+ border: 1px solid #ccc;
249
+ width: 100%;
250
+ margin-top: 10px;
251
+ }
252
+
253
+ /* Close Button */
254
+ .close-btn {
255
+ position: absolute;
256
+ top: 10px;
257
+ right: 25px;
258
+ background: transparent;
259
+ font-size: 24px;
260
+ color: #aaa;
261
+ cursor: pointer;
262
+ transition: color 0.3s;
263
+ width: 2vw;
264
+ border: 3px solid #eb48149c;
265
+ padding: 1px;
266
+ }
267
+
268
+ .close-btn:hover {
269
+ color: #333;
270
+ }
271
+
272
+ /* Button Styling */
273
+ button {
274
+ padding: 12px 20px;
275
+ background-color: #007bff;
276
+ color: black;
277
+ border: none;
278
+ border-radius: 8px;
279
+ font-size: 16px;
280
+ cursor: pointer;
281
+ transition: background-color 0.3s ease, transform 0.2s ease;
282
+ width: 100%;
283
+ text-align: center;
284
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
285
+ }
286
+
287
+ button:active {
288
+ transform: translateY(1px);
289
+ }
290
+
291
+ button:focus-visible {
292
+ outline: 3px solid #7cc4ff;
293
+ outline-offset: 3px;
294
+ }
295
+
296
+ /* Animations for modal */
297
+ @keyframes fadeIn {
298
+ from {
299
+ opacity: 0;
300
+ }
301
+
302
+ to {
303
+ opacity: 1;
304
+ }
305
+ }
306
+
307
+ @keyframes slideUp {
308
+ from {
309
+ transform: translateY(20px);
310
+ opacity: 0;
311
+ }
312
+
313
+ to {
314
+ transform: translateY(0);
315
+ opacity: 1;
316
+ }
317
+ }
src/app/question-answer/question-answer.component.html ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="qa-page">
2
+ <header class="topbar">
3
+ <button class="home-btn" type="button" (click)="goHome()" aria-label="Go to Home">Home</button>
4
+ <h1 class="title">{{ heading }}</h1>
5
+ </header>
6
+
7
+ <main class="stage">
8
+ <button class="orb orb-top" type="button" (click)="onSelect('marriage')" aria-label="Select MARRIAGE">
9
+ <span><i class="fa-solid fa-ring"></i>&nbsp;MARRIAGE</span>
10
+ </button>
11
+ <button class="orb orb-left" type="button" (click)="onSelect('interview')" aria-label="Select INTERVIEW">
12
+ <span><i class="fa-solid fa-user-tie"></i>&nbsp;INTERVIEW</span>
13
+ </button>
14
+ <button class="orb orb-right" type="button" (click)="onSelect('partnership')" aria-label="Select BUSINESS PARTNERS">
15
+ <span><i class="fa-solid fa-handshake"></i>&nbsp;BUSINESS PARTNERS</span>
16
+ </button>
17
+ </main>
18
+
19
+ <div *ngIf="isModalOpen" class="modal-overlay">
20
+ <div class="modal">
21
+ <button class="close-btn" (click)="closeModal()">×</button>
22
+
23
+ <div class="left-side">
24
+ <h2>Questions for {{ selectedOption | uppercase }}</h2>
25
+ </div>
26
+
27
+ <div class="right-side">
28
+ <div *ngIf="selectedQuestions.length > 0; else noQuestions">
29
+ <form (ngSubmit)="submitAnswers()">
30
+ <div *ngFor="let q of selectedQuestions; let i = index">
31
+ <label>{{ q.label }}</label>
32
+
33
+ <!-- text/date -->
34
+ <div *ngIf="q.input_type === 'text' || q.input_type === 'date'">
35
+ <input [type]="q.input_type === 'date' ? 'date' : 'text'"
36
+ [(ngModel)]="answers[i]"
37
+ name="answer{{ i }}"
38
+ (ngModelChange)="onAnswerChange(i, $event)"
39
+ placeholder="Enter your answer" />
40
+ </div>
41
+
42
+ <!-- radio/select -->
43
+ <div *ngIf="(q.input_type === 'select' || q.input_type === 'radio') && q.options?.length">
44
+ <label *ngFor="let option of q.options" class="option">
45
+ <input type="radio"
46
+ [name]="'answer' + i"
47
+ [value]="option"
48
+ [(ngModel)]="answers[i]"
49
+ (change)="onAnswerChange(i, option)" />
50
+ {{ option }}
51
+ </label>
52
+ </div>
53
+ </div>
54
+
55
+ <button type="submit">Submit Answers</button>
56
+ </form>
57
+ <!--<button [routerLink]="'/quiz'">Go to Quiz</button>-->
58
+ <a routerLink="/llmquiz" class="btn btn-primary">{{ tr('Go to Quiz') }}</a>
59
+ </div>
60
+
61
+ <ng-template #noQuestions>
62
+ <p>No questions available for this role.</p>
63
+ </ng-template>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
src/app/question-answer/question-answer.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { QuestionAnswerComponent } from './question-answer.component';
4
+
5
+ describe('QuestionAnswerComponent', () => {
6
+ let component: QuestionAnswerComponent;
7
+ let fixture: ComponentFixture<QuestionAnswerComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [QuestionAnswerComponent]
12
+ });
13
+ fixture = TestBed.createComponent(QuestionAnswerComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/question-answer/question-answer.component.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { QuestionAnswerService, QAItem } from './question-answer-service.service';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { FormsModule } from '@angular/forms';
5
+ import { CommonModule } from '@angular/common';
6
+ import { HttpErrorResponse } from '@angular/common/http';
7
+
8
+ @Component({
9
+ selector: 'app-question-answer',
10
+ standalone: true,
11
+ imports: [FormsModule, CommonModule, RouterLink],
12
+ templateUrl: './question-answer.component.html',
13
+ styleUrls: ['./question-answer.component.css']
14
+ })
15
+ export class QuestionAnswerComponent implements OnInit {
16
+ heading: string = 'CHOOSE YOUR ROLE';
17
+ selectedOption: string = ''; // 'marriage' | 'interview' | 'partnership'
18
+ selectedQuestions: QAItem[] = []; // questions fetched for selected role
19
+ isModalOpen: boolean = false;
20
+
21
+ // If you already know the logged-in user id, set it here or load from localStorage in ngOnInit.
22
+ userId: number = 1;
23
+
24
+ // Template-driven answers storage (bound via [(ngModel)])
25
+ answers: any[] = [];
26
+
27
+ constructor(
28
+ private qaService: QuestionAnswerService,
29
+ private router: Router
30
+ ) { }
31
+
32
+ ngOnInit(): void {
33
+ // Optional: restore from localStorage if available
34
+ const uid = localStorage.getItem('user_id');
35
+ if (uid) {
36
+ const n = Number(uid);
37
+ if (!Number.isNaN(n)) this.userId = n;
38
+ }
39
+
40
+ const savedRole = localStorage.getItem('role');
41
+ if (savedRole) this.selectedOption = savedRole;
42
+ }
43
+
44
+ onSelect(role: string): void {
45
+ this.selectedOption = role;
46
+ this.fetchQuestions(role);
47
+ this.isModalOpen = true;
48
+ this.assignRoleToUser(role);
49
+ }
50
+
51
+ assignRoleToUser(role: string): void {
52
+ const payload = {
53
+ user_id: this.userId,
54
+ role_name: role,
55
+ assigned_at: new Date().toISOString()
56
+ };
57
+
58
+ this.qaService.assignRole(payload).subscribe({
59
+ next: (res) => console.log('Role assigned successfully:', res),
60
+ error: (err: HttpErrorResponse) => console.error('Error assigning role:', err)
61
+ });
62
+ }
63
+
64
+ fetchQuestions(role: string): void {
65
+ this.qaService.getQuestions(role).subscribe({
66
+ next: (data: QAItem[]) => {
67
+ this.selectedQuestions = data;
68
+ // Prepare answers array for template-driven binding
69
+ this.answers = new Array(data.length).fill('');
70
+ console.log('Fetched questions:', data);
71
+ },
72
+ error: (err: HttpErrorResponse) => console.error('Error fetching questions:', err)
73
+ });
74
+ }
75
+
76
+ submitAnswers(): void {
77
+ const role = (this.selectedOption || '').toLowerCase(); // 'marriage' | 'interview' | 'partnership'
78
+ const userIdNum = Number(this.userId);
79
+
80
+ if (!role || !userIdNum) {
81
+ console.error('Role and user id are required.');
82
+ return;
83
+ }
84
+
85
+ // Build the "fields" object from questions + answers.
86
+ // We prefer the backend-provided DB column name: r.column_key.
87
+ const fields: Record<string, any> = {};
88
+ this.selectedQuestions.forEach((q: QAItem, idx: number) => {
89
+ const key =
90
+ (q as any).column_key || // from RoleQuestions table
91
+ (q as any).key || // fallback if present
92
+ `q_${idx + 1}`; // final fallback
93
+ fields[key] = this.answers[idx] ?? '';
94
+ });
95
+
96
+ // Many insert statements expect created_at. Add if your backend uses it.
97
+ fields['created_at'] = new Date().toISOString();
98
+
99
+ // Service signature: submitAnswers(role: string, userId: number, fields: Record<string, any>)
100
+ this.qaService.submitAnswers(role, userIdNum, fields).subscribe({
101
+ next: () => {
102
+ // Close profile modal
103
+ this.isModalOpen = false;
104
+
105
+ // Persist for LLM Quiz
106
+ localStorage.setItem('user_id', String(userIdNum));
107
+ localStorage.setItem('role', role);
108
+ // Log the values passed to router
109
+ console.log('Navigating to LLM Quiz with queryParams:', fields);
110
+ // Navigate to LLM Quiz with autostart
111
+ // Prepare queryParams
112
+ const queryParams = {
113
+ user_id: String(userIdNum),
114
+ role,
115
+ fields, // Fields object (you might want to reconsider passing this depending on the size/complexity)
116
+ autostart: '1'
117
+ };
118
+
119
+ // Log the queryParams before navigating
120
+ this.router.navigate(['/llmquiz'], {
121
+ queryParams: { user_id: String(userIdNum), role, fields, autostart: '1' }
122
+ });
123
+ console.log('queryParams:', queryParams);
124
+ },
125
+ error: (err: HttpErrorResponse) => {
126
+ console.error('Error submitting answers:', err);
127
+ }
128
+ });
129
+ }
130
+
131
+ onAnswerChange(i: number, v: any): void {
132
+ this.answers[i] = v;
133
+ }
134
+
135
+ closeModal(): void {
136
+ this.isModalOpen = false;
137
+ }
138
+
139
+ goHome(): void {
140
+ this.router.navigate(['/intro-page']);
141
+ }
142
+
143
+ // i18n helper (keep if used in template)
144
+ tr(key: string): string {
145
+ const dict = {
146
+ brand: 'Pykara',
147
+ signup: 'Sign up',
148
+ signin: 'Sign in',
149
+ tagline: 'Discover your profile. Match with purpose.',
150
+ subtext: 'Py-Match runs a short, privacy-first assessment and builds a profile used for responsible matching. Color results are not shown to users.',
151
+ getStarted: 'Get started',
152
+ tos: 'Terms',
153
+ privacy: 'Privacy'
154
+ };
155
+ return dict[key as keyof typeof dict] ?? key;
156
+ }
157
+ }
src/app/quiz/quiz.component.css ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* General styling for the quiz container */
2
+ .quiz-container {
3
+ display: flex;
4
+ justify-content: center;
5
+ align-items: center;
6
+ height: 100vh; /* Full viewport height */
7
+ background-image: url('src/assets/background.png');
8
+ background-size: cover; /* Ensures the image covers the entire area */
9
+ background-position: center; /* Center the image */
10
+ background-repeat: no-repeat; /* Prevents the image from repeating */
11
+ margin: 0; /* Remove default margin */
12
+ }
13
+
14
+
15
+ /* Overlay for dimming the background */
16
+ .quiz-overlay {
17
+ position: absolute;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black overlay */
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ }
27
+
28
+ /* Card for the quiz with soft green background */
29
+ .quiz-card {
30
+ background-color: #eaf6e2; /* Soft light green background */
31
+ border-radius: 15px;
32
+ padding: 30px;
33
+ text-align: center;
34
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
35
+ max-width: 450px;
36
+ width: 100%;
37
+ position: relative;
38
+ }
39
+
40
+ /* Profile image */
41
+ .profile-picture {
42
+ width: 80px;
43
+ height: 80px;
44
+ border-radius: 50%;
45
+ object-fit: cover;
46
+ margin-bottom: 20px;
47
+ }
48
+
49
+ /* Title styling */
50
+ .quiz-title {
51
+ font-size: 22px;
52
+ font-weight: bold;
53
+ color: #4caf50; /* Green color */
54
+ margin-bottom: 20px;
55
+ }
56
+
57
+ /* Question text styling */
58
+ .question {
59
+ font-size: 18px;
60
+ color: #333;
61
+ margin-bottom: 30px;
62
+ }
63
+
64
+ /* Button styling for options */
65
+ .option-button {
66
+ background-color: #4caf50;
67
+ color: white;
68
+ padding: 12px 20px;
69
+ margin: 10px;
70
+ border: none;
71
+ border-radius: 10px;
72
+ width: 100%;
73
+ text-align: center;
74
+ cursor: pointer;
75
+ transition: background-color 0.3s ease;
76
+ }
77
+
78
+ .option-button:hover {
79
+ background-color: #45a049;
80
+ }
81
+
82
+ /* Button styling for "Next" and "Submit" */
83
+ .next-button, .submit-button {
84
+ background-color: #4caf50;
85
+ color: white;
86
+ padding: 12px 20px;
87
+ border: none;
88
+ border-radius: 10px;
89
+ margin-top: 20px;
90
+ width: 100%;
91
+ cursor: pointer;
92
+ transition: background-color 0.3s ease;
93
+ }
94
+
95
+ .next-button:hover, .submit-button:hover {
96
+ background-color: #45a049;
97
+ }
98
+
99
+ /* Success Popup */
100
+ .success-popup {
101
+ position: absolute;
102
+ top: 50%;
103
+ left: 50%;
104
+ transform: translate(-50%, -50%);
105
+ background: rgba(0, 0, 0, 0.8);
106
+ color: white;
107
+ padding: 20px;
108
+ border-radius: 10px;
109
+ text-align: center;
110
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
111
+ }
112
+
113
+ .success-popup button {
114
+ background-color: #4caf50;
115
+ color: white;
116
+ padding: 10px 20px;
117
+ border: none;
118
+ border-radius: 5px;
119
+ cursor: pointer;
120
+ margin-top: 15px;
121
+ transition: background-color 0.3s ease;
122
+ }
123
+
124
+ .success-popup button:hover {
125
+ background-color: #45a049;
126
+ }
src/app/quiz/quiz.component.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="quiz-container">
2
+ <div class="quiz-overlay">
3
+ <div class="quiz-card">
4
+ <h1 class="quiz-title">QUIZ</h1>
5
+ <div class="question-container">
6
+ <p class="question">{{ questions[currentQuestionIndex].question }}</p>
7
+ <div class="options">
8
+ <button *ngFor="let option of questions[currentQuestionIndex].options"
9
+ class="option-button"
10
+ (click)="selectOption(option)">
11
+ {{ option }}
12
+ </button>
13
+ </div>
14
+ <button *ngIf="currentQuestionIndex < questions.length - 1"
15
+ class="next-button"
16
+ [disabled]="!isOptionSelected"
17
+ (click)="nextQuestion()">
18
+ Next
19
+ </button>
20
+ <button *ngIf="currentQuestionIndex === questions.length - 1"
21
+ class="submit-button"
22
+ [disabled]="!isOptionSelected"
23
+ (click)="submitQuiz()">
24
+ Submit
25
+ </button>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Success Popup -->
32
+ <div *ngIf="isSubmitted" class="success-popup">
33
+ <p>Your test is submitted!</p>
34
+ <button (click)="closePopup()">Close</button>
35
+ </div>
src/app/quiz/quiz.component.spec.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { QuizComponent } from './quiz.component';
4
+
5
+ describe('QuizComponent', () => {
6
+ let component: QuizComponent;
7
+ let fixture: ComponentFixture<QuizComponent>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ declarations: [QuizComponent]
12
+ });
13
+ fixture = TestBed.createComponent(QuizComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
src/app/quiz/quiz.component.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { CommonModule } from '@angular/common';
5
+ import { RouterLink } from '@angular/router';
6
+ interface QuizQuestion {
7
+ question: string;
8
+ options: string[];
9
+ // store the selected answer; start as null and set to a string later
10
+ answer: string | null;
11
+ }
12
+
13
+ @Component({
14
+ selector: 'app-quiz',
15
+ standalone: true,
16
+ imports: [FormsModule, CommonModule, RouterLink],
17
+ templateUrl: './quiz.component.html',
18
+ styleUrls: ['./quiz.component.css'],
19
+ })
20
+ export class QuizComponent implements OnInit {
21
+
22
+ constructor(public router: Router) { }
23
+ ngOnInit(): void { }
24
+ questions: QuizQuestion[] = [
25
+ {
26
+ question: 'You are leading a small team. A new member is quiet and avoids discussions. What will you do first?',
27
+ options: [
28
+ 'Assign a simple task and review later',
29
+ 'Speak privately to understand concerns',
30
+ 'Ask them to present in the next meeting',
31
+ 'Ignore it and focus on deadlines'
32
+ ],
33
+ answer: null,
34
+ },
35
+ {
36
+ question: 'A stakeholder requests a big change late in the sprint. What is your approach?',
37
+ options: [
38
+ 'Accept it immediately',
39
+ 'Reject it immediately',
40
+ 'Evaluate impact and negotiate scope/time',
41
+ 'Ask the team to work overtime'
42
+ ],
43
+ answer: null,
44
+ },
45
+ {
46
+ question: 'Two teammates disagree on a solution. How do you proceed?',
47
+ options: [
48
+ 'Pick the faster option',
49
+ 'Let them resolve it alone',
50
+ 'Facilitate a short decision session with facts',
51
+ 'Escalate to management'
52
+ ],
53
+ answer: null,
54
+ },
55
+ {
56
+ question: 'You find a recurring production bug. What is your first step?',
57
+ options: [
58
+ 'Hotfix every time',
59
+ 'Add logs and create a root-cause ticket',
60
+ 'Disable the feature',
61
+ 'Rollback to an old release'
62
+ ],
63
+ answer: null,
64
+ },
65
+ {
66
+ question: 'Your release is on time but test coverage is low. What will you do?',
67
+ options: [
68
+ 'Release as-is',
69
+ 'Block the release',
70
+ 'Risk-assess, add critical tests, and plan coverage',
71
+ 'Let QA handle it later'
72
+ ],
73
+ answer: null,
74
+ },
75
+ ];
76
+
77
+ currentQuestionIndex = 0;
78
+ isOptionSelected = false; // controls Next/Submit button enabled state
79
+ isSubmitted = false; // controls the success popup
80
+
81
+ selectOption(option: string): void {
82
+ this.questions[this.currentQuestionIndex].answer = option; // type-safe: string | null
83
+ this.isOptionSelected = true;
84
+ }
85
+
86
+ nextQuestion(): void {
87
+ if (!this.isOptionSelected) return;
88
+ this.currentQuestionIndex++;
89
+
90
+ // Reset the enable/disable state for the next question
91
+ const next = this.questions[this.currentQuestionIndex];
92
+ this.isOptionSelected = !!next?.answer; // true if already answered, else false
93
+ }
94
+
95
+ submitQuiz(): void {
96
+ if (!this.isOptionSelected) return;
97
+ this.isSubmitted = true; // shows the “Your test is submitted!” popup
98
+ }
99
+
100
+ closePopup(): void {
101
+ this.isSubmitted = false;
102
+ // Optionally navigate or reset after closing:
103
+ // this.currentQuestionIndex = 0;
104
+ // this.questions.forEach(q => (q.answer = null));
105
+ // this.isOptionSelected = false;
106
+ }
107
+ }
src/app/quiz/quiz.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { QuizService } from './quiz.service';
4
+
5
+ describe('QuizService', () => {
6
+ let service: QuizService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(QuizService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/quiz/quiz.service.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 QuizService {
9
+ private apiUrl = 'http://localhost:5000'; // Your Flask backend URL
10
+
11
+ constructor(private http: HttpClient) { }
12
+
13
+ // Start the quiz by sending the profile and role
14
+ startQuiz(nQuestions: number, batchSize: number, role: string, profile: any): Observable<any> {
15
+ return this.http.post<any>(`${this.apiUrl}/q/start`, {
16
+ n_questions: nQuestions,
17
+ batch_size: batchSize,
18
+ role: role,
19
+ profile: profile
20
+ });
21
+ }
22
+
23
+ // Fetch next question
24
+ nextQuestion(sessionId: string, selectedColor: string): Observable<any> {
25
+ return this.http.post<any>(`${this.apiUrl}/q/next`, {
26
+ session_id: sessionId,
27
+ selected_color: selectedColor
28
+ });
29
+ }
30
+ }