Oviya commited on
Commit
6534ea1
·
1 Parent(s): 2434088

add staticchat

Browse files
dist/gramm-ai/3rdpartylicenses.txt CHANGED
@@ -304,6 +304,20 @@ Package: @angular/animations
304
  License: "MIT"
305
 
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  --------------------------------------------------------------------------------
308
  Package: zone.js
309
  License: "MIT"
 
304
  License: "MIT"
305
 
306
 
307
+ --------------------------------------------------------------------------------
308
+ Package: uuid
309
+ License: "MIT"
310
+
311
+ The MIT License (MIT)
312
+
313
+ Copyright (c) 2010-2020 Robert Kieffer and other contributors
314
+
315
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
316
+
317
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
318
+
319
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
320
+
321
  --------------------------------------------------------------------------------
322
  Package: zone.js
323
  License: "MIT"
dist/gramm-ai/browser/index.html CHANGED
@@ -16,5 +16,5 @@
16
  </style><link rel="stylesheet" href="styles-TX4PRGSR.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-TX4PRGSR.css"></noscript></head>
17
  <body>
18
  <app-root></app-root>
19
- <script src="polyfills-FFHMD2TL.js" type="module"></script><script src="main-P2CB6RHX.js" type="module"></script></body>
20
  </html>
 
16
  </style><link rel="stylesheet" href="styles-TX4PRGSR.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-TX4PRGSR.css"></noscript></head>
17
  <body>
18
  <app-root></app-root>
19
+ <script src="polyfills-FFHMD2TL.js" type="module"></script><script src="main-OWHUQN5B.js" type="module"></script></body>
20
  </html>
dist/gramm-ai/browser/main-OWHUQN5B.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/gramm-ai/browser/main-P2CB6RHX.js DELETED
The diff for this file is too large to render. See raw diff
 
package-lock.json CHANGED
@@ -27,6 +27,7 @@
27
  "rxjs": "~7.8.0",
28
  "tailwindcss": "^3.4.17",
29
  "tslib": "^2.3.0",
 
30
  "wavesurfer.js": "^7.11.1",
31
  "zone.js": "~0.14.3"
32
  },
@@ -271,7 +272,6 @@
271
  "url": "https://github.com/sponsors/ai"
272
  }
273
  ],
274
- "peer": true,
275
  "dependencies": {
276
  "nanoid": "^3.3.7",
277
  "picocolors": "^1.0.0",
@@ -355,7 +355,6 @@
355
  "version": "17.3.12",
356
  "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz",
357
  "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==",
358
- "peer": true,
359
  "dependencies": {
360
  "tslib": "^2.3.0"
361
  },
@@ -422,7 +421,6 @@
422
  "version": "17.3.12",
423
  "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz",
424
  "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==",
425
- "peer": true,
426
  "dependencies": {
427
  "tslib": "^2.3.0"
428
  },
@@ -438,7 +436,6 @@
438
  "version": "17.3.12",
439
  "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz",
440
  "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==",
441
- "peer": true,
442
  "dependencies": {
443
  "tslib": "^2.3.0"
444
  },
@@ -459,7 +456,6 @@
459
  "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
460
  "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
461
  "dev": true,
462
- "peer": true,
463
  "dependencies": {
464
  "@babel/core": "7.23.9",
465
  "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -532,7 +528,6 @@
532
  "version": "17.3.12",
533
  "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz",
534
  "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==",
535
- "peer": true,
536
  "dependencies": {
537
  "tslib": "^2.3.0"
538
  },
@@ -548,7 +543,6 @@
548
  "version": "17.3.12",
549
  "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz",
550
  "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==",
551
- "peer": true,
552
  "dependencies": {
553
  "tslib": "^2.3.0"
554
  },
@@ -631,7 +625,6 @@
631
  "version": "17.3.12",
632
  "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz",
633
  "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==",
634
- "peer": true,
635
  "dependencies": {
636
  "tslib": "^2.3.0"
637
  },
@@ -708,7 +701,6 @@
708
  "version": "7.24.0",
709
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
710
  "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
711
- "peer": true,
712
  "dependencies": {
713
  "@ampproject/remapping": "^2.2.0",
714
  "@babel/code-frame": "^7.23.5",
@@ -5343,7 +5335,6 @@
5343
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
5344
  "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
5345
  "dev": true,
5346
- "peer": true,
5347
  "bin": {
5348
  "acorn": "bin/acorn"
5349
  },
@@ -5414,7 +5405,6 @@
5414
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
5415
  "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
5416
  "dev": true,
5417
- "peer": true,
5418
  "dependencies": {
5419
  "fast-deep-equal": "^3.1.1",
5420
  "json-schema-traverse": "^1.0.0",
@@ -5898,7 +5888,6 @@
5898
  "url": "https://github.com/sponsors/ai"
5899
  }
5900
  ],
5901
- "peer": true,
5902
  "dependencies": {
5903
  "caniuse-lite": "^1.0.30001688",
5904
  "electron-to-chromium": "^1.5.73",
@@ -8896,8 +8885,7 @@
8896
  "version": "5.1.2",
8897
  "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
8898
  "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
8899
- "dev": true,
8900
- "peer": true
8901
  },
8902
  "node_modules/jest-diff": {
8903
  "version": "30.0.0-alpha.6",
@@ -9607,7 +9595,6 @@
9607
  "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
9608
  "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
9609
  "dev": true,
9610
- "peer": true,
9611
  "dependencies": {
9612
  "@colors/colors": "1.5.0",
9613
  "body-parser": "^1.19.0",
@@ -9814,7 +9801,6 @@
9814
  "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
9815
  "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
9816
  "dev": true,
9817
- "peer": true,
9818
  "dependencies": {
9819
  "copy-anything": "^2.0.1",
9820
  "parse-node-version": "^1.0.1",
@@ -11429,7 +11415,6 @@
11429
  "url": "https://github.com/sponsors/ai"
11430
  }
11431
  ],
11432
- "peer": true,
11433
  "dependencies": {
11434
  "nanoid": "^3.3.7",
11435
  "picocolors": "^1.1.1",
@@ -12261,7 +12246,6 @@
12261
  "version": "7.8.1",
12262
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
12263
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
12264
- "peer": true,
12265
  "dependencies": {
12266
  "tslib": "^2.1.0"
12267
  }
@@ -12320,7 +12304,6 @@
12320
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
12321
  "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
12322
  "dev": true,
12323
- "peer": true,
12324
  "dependencies": {
12325
  "chokidar": ">=3.0.0 <4.0.0",
12326
  "immutable": "^4.0.0",
@@ -12896,6 +12879,16 @@
12896
  "websocket-driver": "^0.7.4"
12897
  }
12898
  },
 
 
 
 
 
 
 
 
 
 
12899
  "node_modules/socks": {
12900
  "version": "2.8.3",
12901
  "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
@@ -13438,7 +13431,6 @@
13438
  "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
13439
  "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
13440
  "dev": true,
13441
- "peer": true,
13442
  "dependencies": {
13443
  "@jridgewell/source-map": "^0.3.3",
13444
  "acorn": "^8.8.2",
@@ -13677,7 +13669,6 @@
13677
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
13678
  "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
13679
  "dev": true,
13680
- "peer": true,
13681
  "bin": {
13682
  "tsc": "bin/tsc",
13683
  "tsserver": "bin/tsserver"
@@ -13870,12 +13861,16 @@
13870
  }
13871
  },
13872
  "node_modules/uuid": {
13873
- "version": "8.3.2",
13874
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
13875
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
13876
- "dev": true,
 
 
 
 
13877
  "bin": {
13878
- "uuid": "dist/bin/uuid"
13879
  }
13880
  },
13881
  "node_modules/validate-npm-package-license": {
@@ -13911,7 +13906,6 @@
13911
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz",
13912
  "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==",
13913
  "dev": true,
13914
- "peer": true,
13915
  "dependencies": {
13916
  "esbuild": "^0.19.3",
13917
  "postcss": "^8.4.35",
@@ -14427,7 +14421,6 @@
14427
  "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
14428
  "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
14429
  "dev": true,
14430
- "peer": true,
14431
  "dependencies": {
14432
  "@types/estree": "^1.0.5",
14433
  "@webassemblyjs/ast": "^1.12.1",
@@ -14502,7 +14495,6 @@
14502
  "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
14503
  "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
14504
  "dev": true,
14505
- "peer": true,
14506
  "dependencies": {
14507
  "@types/bonjour": "^3.5.9",
14508
  "@types/connect-history-api-fallback": "^1.3.5",
@@ -14629,7 +14621,6 @@
14629
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
14630
  "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
14631
  "dev": true,
14632
- "peer": true,
14633
  "dependencies": {
14634
  "fast-deep-equal": "^3.1.1",
14635
  "fast-json-stable-stringify": "^2.0.0",
@@ -14881,8 +14872,7 @@
14881
  "node_modules/zone.js": {
14882
  "version": "0.14.10",
14883
  "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
14884
- "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
14885
- "peer": true
14886
  }
14887
  },
14888
  "dependencies": {
@@ -15001,7 +14991,6 @@
15001
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
15002
  "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
15003
  "dev": true,
15004
- "peer": true,
15005
  "requires": {
15006
  "nanoid": "^3.3.7",
15007
  "picocolors": "^1.0.0",
@@ -15057,7 +15046,6 @@
15057
  "version": "17.3.12",
15058
  "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz",
15059
  "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==",
15060
- "peer": true,
15061
  "requires": {
15062
  "tslib": "^2.3.0"
15063
  }
@@ -15102,7 +15090,6 @@
15102
  "version": "17.3.12",
15103
  "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz",
15104
  "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==",
15105
- "peer": true,
15106
  "requires": {
15107
  "tslib": "^2.3.0"
15108
  }
@@ -15111,7 +15098,6 @@
15111
  "version": "17.3.12",
15112
  "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz",
15113
  "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==",
15114
- "peer": true,
15115
  "requires": {
15116
  "tslib": "^2.3.0"
15117
  }
@@ -15121,7 +15107,6 @@
15121
  "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
15122
  "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
15123
  "dev": true,
15124
- "peer": true,
15125
  "requires": {
15126
  "@babel/core": "7.23.9",
15127
  "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -15176,7 +15161,6 @@
15176
  "version": "17.3.12",
15177
  "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz",
15178
  "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==",
15179
- "peer": true,
15180
  "requires": {
15181
  "tslib": "^2.3.0"
15182
  }
@@ -15185,7 +15169,6 @@
15185
  "version": "17.3.12",
15186
  "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz",
15187
  "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==",
15188
- "peer": true,
15189
  "requires": {
15190
  "tslib": "^2.3.0"
15191
  }
@@ -15249,7 +15232,6 @@
15249
  "version": "17.3.12",
15250
  "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz",
15251
  "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==",
15252
- "peer": true,
15253
  "requires": {
15254
  "tslib": "^2.3.0"
15255
  }
@@ -15289,7 +15271,6 @@
15289
  "version": "7.24.0",
15290
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
15291
  "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
15292
- "peer": true,
15293
  "requires": {
15294
  "@ampproject/remapping": "^2.2.0",
15295
  "@babel/code-frame": "^7.23.5",
@@ -18749,8 +18730,7 @@
18749
  "version": "8.14.0",
18750
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
18751
  "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
18752
- "dev": true,
18753
- "peer": true
18754
  },
18755
  "acorn-import-attributes": {
18756
  "version": "1.9.5",
@@ -18803,7 +18783,6 @@
18803
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
18804
  "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
18805
  "dev": true,
18806
- "peer": true,
18807
  "requires": {
18808
  "fast-deep-equal": "^3.1.1",
18809
  "json-schema-traverse": "^1.0.0",
@@ -19148,7 +19127,6 @@
19148
  "version": "4.24.3",
19149
  "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
19150
  "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
19151
- "peer": true,
19152
  "requires": {
19153
  "caniuse-lite": "^1.0.30001688",
19154
  "electron-to-chromium": "^1.5.73",
@@ -21346,8 +21324,7 @@
21346
  "version": "5.1.2",
21347
  "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
21348
  "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
21349
- "dev": true,
21350
- "peer": true
21351
  },
21352
  "jest-diff": {
21353
  "version": "30.0.0-alpha.6",
@@ -21890,7 +21867,6 @@
21890
  "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
21891
  "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
21892
  "dev": true,
21893
- "peer": true,
21894
  "requires": {
21895
  "@colors/colors": "1.5.0",
21896
  "body-parser": "^1.19.0",
@@ -22058,7 +22034,6 @@
22058
  "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
22059
  "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
22060
  "dev": true,
22061
- "peer": true,
22062
  "requires": {
22063
  "copy-anything": "^2.0.1",
22064
  "errno": "^0.1.1",
@@ -23233,7 +23208,6 @@
23233
  "version": "8.4.49",
23234
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
23235
  "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
23236
- "peer": true,
23237
  "requires": {
23238
  "nanoid": "^3.3.7",
23239
  "picocolors": "^1.1.1",
@@ -23806,7 +23780,6 @@
23806
  "version": "7.8.1",
23807
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
23808
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
23809
- "peer": true,
23810
  "requires": {
23811
  "tslib": "^2.1.0"
23812
  }
@@ -23844,7 +23817,6 @@
23844
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
23845
  "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
23846
  "dev": true,
23847
- "peer": true,
23848
  "requires": {
23849
  "chokidar": ">=3.0.0 <4.0.0",
23850
  "immutable": "^4.0.0",
@@ -24266,6 +24238,14 @@
24266
  "faye-websocket": "^0.11.3",
24267
  "uuid": "^8.3.2",
24268
  "websocket-driver": "^0.7.4"
 
 
 
 
 
 
 
 
24269
  }
24270
  },
24271
  "socks": {
@@ -24686,7 +24666,6 @@
24686
  "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
24687
  "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
24688
  "dev": true,
24689
- "peer": true,
24690
  "requires": {
24691
  "@jridgewell/source-map": "^0.3.3",
24692
  "acorn": "^8.8.2",
@@ -24854,8 +24833,7 @@
24854
  "version": "5.3.3",
24855
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
24856
  "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
24857
- "dev": true,
24858
- "peer": true
24859
  },
24860
  "ua-parser-js": {
24861
  "version": "0.7.40",
@@ -24970,10 +24948,9 @@
24970
  "dev": true
24971
  },
24972
  "uuid": {
24973
- "version": "8.3.2",
24974
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
24975
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
24976
- "dev": true
24977
  },
24978
  "validate-npm-package-license": {
24979
  "version": "3.0.4",
@@ -25002,7 +24979,6 @@
25002
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz",
25003
  "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==",
25004
  "dev": true,
25005
- "peer": true,
25006
  "requires": {
25007
  "esbuild": "^0.19.3",
25008
  "fsevents": "~2.3.3",
@@ -25256,7 +25232,6 @@
25256
  "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
25257
  "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
25258
  "dev": true,
25259
- "peer": true,
25260
  "requires": {
25261
  "@types/estree": "^1.0.5",
25262
  "@webassemblyjs/ast": "^1.12.1",
@@ -25288,7 +25263,6 @@
25288
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
25289
  "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
25290
  "dev": true,
25291
- "peer": true,
25292
  "requires": {
25293
  "fast-deep-equal": "^3.1.1",
25294
  "fast-json-stable-stringify": "^2.0.0",
@@ -25356,7 +25330,6 @@
25356
  "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
25357
  "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
25358
  "dev": true,
25359
- "peer": true,
25360
  "requires": {
25361
  "@types/bonjour": "^3.5.9",
25362
  "@types/connect-history-api-fallback": "^1.3.5",
@@ -25558,8 +25531,7 @@
25558
  "zone.js": {
25559
  "version": "0.14.10",
25560
  "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
25561
- "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
25562
- "peer": true
25563
  }
25564
  }
25565
  }
 
27
  "rxjs": "~7.8.0",
28
  "tailwindcss": "^3.4.17",
29
  "tslib": "^2.3.0",
30
+ "uuid": "^13.0.0",
31
  "wavesurfer.js": "^7.11.1",
32
  "zone.js": "~0.14.3"
33
  },
 
272
  "url": "https://github.com/sponsors/ai"
273
  }
274
  ],
 
275
  "dependencies": {
276
  "nanoid": "^3.3.7",
277
  "picocolors": "^1.0.0",
 
355
  "version": "17.3.12",
356
  "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz",
357
  "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==",
 
358
  "dependencies": {
359
  "tslib": "^2.3.0"
360
  },
 
421
  "version": "17.3.12",
422
  "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz",
423
  "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==",
 
424
  "dependencies": {
425
  "tslib": "^2.3.0"
426
  },
 
436
  "version": "17.3.12",
437
  "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz",
438
  "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==",
 
439
  "dependencies": {
440
  "tslib": "^2.3.0"
441
  },
 
456
  "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
457
  "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
458
  "dev": true,
 
459
  "dependencies": {
460
  "@babel/core": "7.23.9",
461
  "@jridgewell/sourcemap-codec": "^1.4.14",
 
528
  "version": "17.3.12",
529
  "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz",
530
  "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==",
 
531
  "dependencies": {
532
  "tslib": "^2.3.0"
533
  },
 
543
  "version": "17.3.12",
544
  "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz",
545
  "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==",
 
546
  "dependencies": {
547
  "tslib": "^2.3.0"
548
  },
 
625
  "version": "17.3.12",
626
  "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz",
627
  "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==",
 
628
  "dependencies": {
629
  "tslib": "^2.3.0"
630
  },
 
701
  "version": "7.24.0",
702
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
703
  "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
 
704
  "dependencies": {
705
  "@ampproject/remapping": "^2.2.0",
706
  "@babel/code-frame": "^7.23.5",
 
5335
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
5336
  "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
5337
  "dev": true,
 
5338
  "bin": {
5339
  "acorn": "bin/acorn"
5340
  },
 
5405
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
5406
  "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
5407
  "dev": true,
 
5408
  "dependencies": {
5409
  "fast-deep-equal": "^3.1.1",
5410
  "json-schema-traverse": "^1.0.0",
 
5888
  "url": "https://github.com/sponsors/ai"
5889
  }
5890
  ],
 
5891
  "dependencies": {
5892
  "caniuse-lite": "^1.0.30001688",
5893
  "electron-to-chromium": "^1.5.73",
 
8885
  "version": "5.1.2",
8886
  "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
8887
  "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
8888
+ "dev": true
 
8889
  },
8890
  "node_modules/jest-diff": {
8891
  "version": "30.0.0-alpha.6",
 
9595
  "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
9596
  "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
9597
  "dev": true,
 
9598
  "dependencies": {
9599
  "@colors/colors": "1.5.0",
9600
  "body-parser": "^1.19.0",
 
9801
  "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
9802
  "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
9803
  "dev": true,
 
9804
  "dependencies": {
9805
  "copy-anything": "^2.0.1",
9806
  "parse-node-version": "^1.0.1",
 
11415
  "url": "https://github.com/sponsors/ai"
11416
  }
11417
  ],
 
11418
  "dependencies": {
11419
  "nanoid": "^3.3.7",
11420
  "picocolors": "^1.1.1",
 
12246
  "version": "7.8.1",
12247
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
12248
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
 
12249
  "dependencies": {
12250
  "tslib": "^2.1.0"
12251
  }
 
12304
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
12305
  "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
12306
  "dev": true,
 
12307
  "dependencies": {
12308
  "chokidar": ">=3.0.0 <4.0.0",
12309
  "immutable": "^4.0.0",
 
12879
  "websocket-driver": "^0.7.4"
12880
  }
12881
  },
12882
+ "node_modules/sockjs/node_modules/uuid": {
12883
+ "version": "8.3.2",
12884
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
12885
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
12886
+ "dev": true,
12887
+ "license": "MIT",
12888
+ "bin": {
12889
+ "uuid": "dist/bin/uuid"
12890
+ }
12891
+ },
12892
  "node_modules/socks": {
12893
  "version": "2.8.3",
12894
  "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
 
13431
  "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
13432
  "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
13433
  "dev": true,
 
13434
  "dependencies": {
13435
  "@jridgewell/source-map": "^0.3.3",
13436
  "acorn": "^8.8.2",
 
13669
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
13670
  "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
13671
  "dev": true,
 
13672
  "bin": {
13673
  "tsc": "bin/tsc",
13674
  "tsserver": "bin/tsserver"
 
13861
  }
13862
  },
13863
  "node_modules/uuid": {
13864
+ "version": "13.0.0",
13865
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
13866
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
13867
+ "funding": [
13868
+ "https://github.com/sponsors/broofa",
13869
+ "https://github.com/sponsors/ctavan"
13870
+ ],
13871
+ "license": "MIT",
13872
  "bin": {
13873
+ "uuid": "dist-node/bin/uuid"
13874
  }
13875
  },
13876
  "node_modules/validate-npm-package-license": {
 
13906
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz",
13907
  "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==",
13908
  "dev": true,
 
13909
  "dependencies": {
13910
  "esbuild": "^0.19.3",
13911
  "postcss": "^8.4.35",
 
14421
  "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
14422
  "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
14423
  "dev": true,
 
14424
  "dependencies": {
14425
  "@types/estree": "^1.0.5",
14426
  "@webassemblyjs/ast": "^1.12.1",
 
14495
  "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
14496
  "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
14497
  "dev": true,
 
14498
  "dependencies": {
14499
  "@types/bonjour": "^3.5.9",
14500
  "@types/connect-history-api-fallback": "^1.3.5",
 
14621
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
14622
  "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
14623
  "dev": true,
 
14624
  "dependencies": {
14625
  "fast-deep-equal": "^3.1.1",
14626
  "fast-json-stable-stringify": "^2.0.0",
 
14872
  "node_modules/zone.js": {
14873
  "version": "0.14.10",
14874
  "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
14875
+ "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ=="
 
14876
  }
14877
  },
14878
  "dependencies": {
 
14991
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
14992
  "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
14993
  "dev": true,
 
14994
  "requires": {
14995
  "nanoid": "^3.3.7",
14996
  "picocolors": "^1.0.0",
 
15046
  "version": "17.3.12",
15047
  "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz",
15048
  "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==",
 
15049
  "requires": {
15050
  "tslib": "^2.3.0"
15051
  }
 
15090
  "version": "17.3.12",
15091
  "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz",
15092
  "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==",
 
15093
  "requires": {
15094
  "tslib": "^2.3.0"
15095
  }
 
15098
  "version": "17.3.12",
15099
  "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz",
15100
  "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==",
 
15101
  "requires": {
15102
  "tslib": "^2.3.0"
15103
  }
 
15107
  "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz",
15108
  "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==",
15109
  "dev": true,
 
15110
  "requires": {
15111
  "@babel/core": "7.23.9",
15112
  "@jridgewell/sourcemap-codec": "^1.4.14",
 
15161
  "version": "17.3.12",
15162
  "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz",
15163
  "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==",
 
15164
  "requires": {
15165
  "tslib": "^2.3.0"
15166
  }
 
15169
  "version": "17.3.12",
15170
  "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz",
15171
  "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==",
 
15172
  "requires": {
15173
  "tslib": "^2.3.0"
15174
  }
 
15232
  "version": "17.3.12",
15233
  "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz",
15234
  "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==",
 
15235
  "requires": {
15236
  "tslib": "^2.3.0"
15237
  }
 
15271
  "version": "7.24.0",
15272
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
15273
  "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
 
15274
  "requires": {
15275
  "@ampproject/remapping": "^2.2.0",
15276
  "@babel/code-frame": "^7.23.5",
 
18730
  "version": "8.14.0",
18731
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
18732
  "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
18733
+ "dev": true
 
18734
  },
18735
  "acorn-import-attributes": {
18736
  "version": "1.9.5",
 
18783
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
18784
  "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
18785
  "dev": true,
 
18786
  "requires": {
18787
  "fast-deep-equal": "^3.1.1",
18788
  "json-schema-traverse": "^1.0.0",
 
19127
  "version": "4.24.3",
19128
  "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
19129
  "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
 
19130
  "requires": {
19131
  "caniuse-lite": "^1.0.30001688",
19132
  "electron-to-chromium": "^1.5.73",
 
21324
  "version": "5.1.2",
21325
  "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
21326
  "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
21327
+ "dev": true
 
21328
  },
21329
  "jest-diff": {
21330
  "version": "30.0.0-alpha.6",
 
21867
  "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
21868
  "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
21869
  "dev": true,
 
21870
  "requires": {
21871
  "@colors/colors": "1.5.0",
21872
  "body-parser": "^1.19.0",
 
22034
  "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
22035
  "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
22036
  "dev": true,
 
22037
  "requires": {
22038
  "copy-anything": "^2.0.1",
22039
  "errno": "^0.1.1",
 
23208
  "version": "8.4.49",
23209
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
23210
  "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
 
23211
  "requires": {
23212
  "nanoid": "^3.3.7",
23213
  "picocolors": "^1.1.1",
 
23780
  "version": "7.8.1",
23781
  "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
23782
  "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
 
23783
  "requires": {
23784
  "tslib": "^2.1.0"
23785
  }
 
23817
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
23818
  "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
23819
  "dev": true,
 
23820
  "requires": {
23821
  "chokidar": ">=3.0.0 <4.0.0",
23822
  "immutable": "^4.0.0",
 
24238
  "faye-websocket": "^0.11.3",
24239
  "uuid": "^8.3.2",
24240
  "websocket-driver": "^0.7.4"
24241
+ },
24242
+ "dependencies": {
24243
+ "uuid": {
24244
+ "version": "8.3.2",
24245
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
24246
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
24247
+ "dev": true
24248
+ }
24249
  }
24250
  },
24251
  "socks": {
 
24666
  "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
24667
  "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
24668
  "dev": true,
 
24669
  "requires": {
24670
  "@jridgewell/source-map": "^0.3.3",
24671
  "acorn": "^8.8.2",
 
24833
  "version": "5.3.3",
24834
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
24835
  "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
24836
+ "dev": true
 
24837
  },
24838
  "ua-parser-js": {
24839
  "version": "0.7.40",
 
24948
  "dev": true
24949
  },
24950
  "uuid": {
24951
+ "version": "13.0.0",
24952
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
24953
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="
 
24954
  },
24955
  "validate-npm-package-license": {
24956
  "version": "3.0.4",
 
24979
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz",
24980
  "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==",
24981
  "dev": true,
 
24982
  "requires": {
24983
  "esbuild": "^0.19.3",
24984
  "fsevents": "~2.3.3",
 
25232
  "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
25233
  "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
25234
  "dev": true,
 
25235
  "requires": {
25236
  "@types/estree": "^1.0.5",
25237
  "@webassemblyjs/ast": "^1.12.1",
 
25263
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
25264
  "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
25265
  "dev": true,
 
25266
  "requires": {
25267
  "fast-deep-equal": "^3.1.1",
25268
  "fast-json-stable-stringify": "^2.0.0",
 
25330
  "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
25331
  "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
25332
  "dev": true,
 
25333
  "requires": {
25334
  "@types/bonjour": "^3.5.9",
25335
  "@types/connect-history-api-fallback": "^1.3.5",
 
25531
  "zone.js": {
25532
  "version": "0.14.10",
25533
  "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
25534
+ "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ=="
 
25535
  }
25536
  }
25537
  }
package.json CHANGED
@@ -30,6 +30,7 @@
30
  "rxjs": "~7.8.0",
31
  "tailwindcss": "^3.4.17",
32
  "tslib": "^2.3.0",
 
33
  "wavesurfer.js": "^7.11.1",
34
  "zone.js": "~0.14.3"
35
  },
 
30
  "rxjs": "~7.8.0",
31
  "tailwindcss": "^3.4.17",
32
  "tslib": "^2.3.0",
33
+ "uuid": "^13.0.0",
34
  "wavesurfer.js": "^7.11.1",
35
  "zone.js": "~0.14.3"
36
  },
src/app/app-routing.module.ts CHANGED
@@ -4,6 +4,7 @@ import { ChatComponent } from './chat/chat.component';
4
  import { HomeComponent } from './home/home.component';
5
  import { SignInComponent } from './sign-in/sign-in.component';
6
  import { authGuard } from './core/guards/auth.guard';
 
7
 
8
  /**
9
  * Application routing configuration
@@ -14,38 +15,45 @@ import { authGuard } from './core/guards/auth.guard';
14
  */
15
  export const routes: Routes = [
16
  // Public routes
17
- {
18
- path: '',
19
- component: HomeComponent,
20
  pathMatch: 'full',
21
  data: { title: 'Home' }
22
  },
23
- {
24
- path: 'home',
25
  component: HomeComponent,
26
  data: { title: 'Home' }
27
  },
28
- {
29
- path: 'login',
30
  component: SignInComponent,
31
  data: { title: 'Sign In' }
32
  },
33
 
34
  // Chat routes
35
- {
36
- path: 'chat',
37
  component: ChatComponent,
38
  data: { title: 'Chat' }
39
  },
40
- {
41
- path: 'chat/:id',
42
  component: ChatComponent,
43
  data: { title: 'Chat' }
44
  },
 
 
 
 
 
 
 
45
 
46
  // Fallback route
47
- {
48
- path: '**',
49
  redirectTo: '',
50
  data: { title: 'Page Not Found' }
51
  }
 
4
  import { HomeComponent } from './home/home.component';
5
  import { SignInComponent } from './sign-in/sign-in.component';
6
  import { authGuard } from './core/guards/auth.guard';
7
+ import { StaticChatComponent } from './staticchat/staticchat.component';
8
 
9
  /**
10
  * Application routing configuration
 
15
  */
16
  export const routes: Routes = [
17
  // Public routes
18
+ {
19
+ path: '',
20
+ component: HomeComponent,
21
  pathMatch: 'full',
22
  data: { title: 'Home' }
23
  },
24
+ {
25
+ path: 'home',
26
  component: HomeComponent,
27
  data: { title: 'Home' }
28
  },
29
+ {
30
+ path: 'login',
31
  component: SignInComponent,
32
  data: { title: 'Sign In' }
33
  },
34
 
35
  // Chat routes
36
+ {
37
+ path: 'chat',
38
  component: ChatComponent,
39
  data: { title: 'Chat' }
40
  },
41
+ {
42
+ path: 'chat/:id',
43
  component: ChatComponent,
44
  data: { title: 'Chat' }
45
  },
46
+ {
47
+ path: 'mj-chat',
48
+ component: StaticChatComponent,
49
+ data: { title: 'MJ-CHAT' }
50
+ },
51
+
52
+
53
 
54
  // Fallback route
55
+ {
56
+ path: '**',
57
  redirectTo: '',
58
  data: { title: 'Page Not Found' }
59
  }
src/app/app.module.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { NgModule } from '@angular/core';
2
  import { BrowserModule } from '@angular/platform-browser';
3
- import { FormsModule } from '@angular/forms';
4
  import { HttpClientModule } from '@angular/common/http';
5
  import { CommonModule } from '@angular/common';
6
  import { AppRoutingModule } from './app-routing.module';
@@ -19,18 +19,21 @@ import { SignInComponent } from './sign-in/sign-in.component';
19
  import { PronunciationComponent } from './pronunciation/pronunciation.component';
20
  import { FooterComponent } from './footer/footer.component';
21
  import { ButtonComponent } from './shared/button/button.component';
 
22
 
23
 
24
  @NgModule({
25
  declarations: [
26
  AppComponent,
27
  HomeComponent,
28
- PronunciationComponent
 
29
  ],
30
  imports: [
31
  BrowserModule,
32
  AppRoutingModule,
33
  FormsModule,
 
34
  HttpClientModule,
35
  CommonModule,
36
  MatDialogModule,
 
1
  import { NgModule } from '@angular/core';
2
  import { BrowserModule } from '@angular/platform-browser';
3
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { HttpClientModule } from '@angular/common/http';
5
  import { CommonModule } from '@angular/common';
6
  import { AppRoutingModule } from './app-routing.module';
 
19
  import { PronunciationComponent } from './pronunciation/pronunciation.component';
20
  import { FooterComponent } from './footer/footer.component';
21
  import { ButtonComponent } from './shared/button/button.component';
22
+ import { StaticChatComponent } from './staticchat/staticchat.component';
23
 
24
 
25
  @NgModule({
26
  declarations: [
27
  AppComponent,
28
  HomeComponent,
29
+ PronunciationComponent,
30
+ StaticChatComponent
31
  ],
32
  imports: [
33
  BrowserModule,
34
  AppRoutingModule,
35
  FormsModule,
36
+ ReactiveFormsModule,
37
  HttpClientModule,
38
  CommonModule,
39
  MatDialogModule,
src/app/chat/api.service.ts CHANGED
@@ -7,7 +7,7 @@ type DbLevel = 'low' | 'mid' | 'high';
7
 
8
  function resolveBaseUrl(): string {
9
  const isHF = location.hostname.endsWith('hf.space');
10
- if (isHF) return 'https://majemaai-mj-learn-backend.hf.space/rag';
11
  if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return 'http://localhost:5000/rag';
12
  return '/rag';
13
  }
 
7
 
8
  function resolveBaseUrl(): string {
9
  const isHF = location.hostname.endsWith('hf.space');
10
+ if (isHF) return 'https://pykara-py-learn-backend.hf.space/rag';
11
  if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return 'http://localhost:5000/rag';
12
  return '/rag';
13
  }
src/app/pronunciation/pronunciation.service.ts CHANGED
@@ -13,7 +13,7 @@ export interface ScoreResponse {
13
  export class PronunciationService {
14
  private readonly apiBase =
15
  location.hostname.endsWith('hf.space')
16
- ? 'https://majemaai-mj-learn-backend.hf.space'
17
  : 'http://localhost:5000';
18
 
19
  private readonly scoreEndpoint = `${this.apiBase}/pronunciation/score`;
 
13
  export class PronunciationService {
14
  private readonly apiBase =
15
  location.hostname.endsWith('hf.space')
16
+ ? 'https://pykara-py-learn-backend.hf.space'
17
  : 'http://localhost:5000';
18
 
19
  private readonly scoreEndpoint = `${this.apiBase}/pronunciation/score`;
src/app/staticchat/staticchat.component.css ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== GLOBAL FONT FOR ENTIRE CHAT ===== */
2
+ .chat-container,
3
+ .chat-container * {
4
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif !important;
5
+ }
6
+
7
+ /* ===== MAIN CONTAINER ===== */
8
+ .chat-container {
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ height: 91vh;
13
+ padding: 2vw;
14
+ gap: 2vw;
15
+ }
16
+
17
+ .chat-window {
18
+ width: 100%;
19
+ max-width: 1000px;
20
+ height: 80vh;
21
+ display: flex;
22
+ flex-direction: column;
23
+ border-radius: 20px;
24
+ backdrop-filter: blur(20px);
25
+ background: rgba(255,255,255,0.08);
26
+ box-shadow: 0 10px 40px rgba(0,0,0,0.4);
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* ===== MESSAGES AREA ===== */
31
+ .chat-messages {
32
+ flex: 1;
33
+ /* padding: 25px;*/
34
+ /* prevent input overlap: keep some bottom padding */
35
+ /*padding-bottom: 120px;*/
36
+ overflow-y: auto;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 7vw;
40
+ }
41
+
42
+ /* ===== MESSAGE ROW (AVATAR + BUBBLE) ===== */
43
+ .message-row {
44
+ display: flex;
45
+ align-items: flex-end;
46
+ gap: 10px;
47
+ }
48
+
49
+ .user-row {
50
+ justify-content: flex-end;
51
+ }
52
+
53
+ .bot-row {
54
+ justify-content: flex-start;
55
+ }
56
+
57
+ /* ===== AVATAR ===== */
58
+ .avatar {
59
+ width: 38px;
60
+ height: 38px;
61
+ border-radius: 50%;
62
+ object-fit: cover;
63
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
64
+ }
65
+
66
+ /* ===== MESSAGE BUBBLE ===== */
67
+ .message-bubble {
68
+ max-width: 60%;
69
+ padding: 14px 18px 8px 18px;
70
+ border-radius: 18px;
71
+ font-size: clamp(14px, 1vw, 18px);
72
+ line-height: 1.6;
73
+ display: flex;
74
+ flex-direction: column;
75
+ }
76
+
77
+ /* User bubble */
78
+ .user-message {
79
+ background: linear-gradient(135deg, #0099ff, #0066ff);
80
+ color: white;
81
+ border-bottom-right-radius: 6px;
82
+ }
83
+
84
+ /* Bot bubble */
85
+ .bot-message {
86
+ background: rgba(255,255,255,0.95);
87
+ color: #333;
88
+ border-bottom-left-radius: 6px;
89
+ }
90
+
91
+ /* ===== MESSAGE TIME (BOTTOM RIGHT) ===== */
92
+ .message-time {
93
+ font-size: clamp(10px, 0.7vw, 12px);
94
+ opacity: 0.6;
95
+ margin-top: 6px;
96
+ align-self: flex-end;
97
+ }
98
+
99
+ /* ===== TYPING INDICATOR ===== */
100
+ .typing-indicator {
101
+ display: flex;
102
+ gap: 5px;
103
+ align-items: center;
104
+ color: white;
105
+ padding-left: 50px;
106
+ }
107
+
108
+ .dot {
109
+ width: 6px;
110
+ height: 6px;
111
+ background: #ccc;
112
+ border-radius: 50%;
113
+ animation: blink 1.4s infinite both;
114
+ }
115
+
116
+ .dot:nth-child(2) {
117
+ animation-delay: 0.2s;
118
+ }
119
+
120
+ .dot:nth-child(3) {
121
+ animation-delay: 0.4s;
122
+ }
123
+
124
+ @keyframes blink {
125
+ 0% {
126
+ opacity: .2;
127
+ }
128
+
129
+ 20% {
130
+ opacity: 1;
131
+ }
132
+
133
+ 100% {
134
+ opacity: .2;
135
+ }
136
+ }
137
+
138
+ /* ===== SUGGESTIONS DROPDOWN ===== */
139
+ .suggestions-box {
140
+ /* background: rgba(0,0,0,0.75);*/
141
+ background: linear-gradient(135deg, #0072ff, #00c6ff );
142
+ max-height: 250px;
143
+ overflow-y: auto;
144
+ border-radius: 12px;
145
+ margin: 0 20px 10px 20px;
146
+ width: 47vw;
147
+ }
148
+
149
+ .suggestion-item {
150
+ padding: 10px 14px;
151
+ cursor: pointer;
152
+ color: white;
153
+ font-size:1vw;
154
+ font-weight:bold;
155
+ border-bottom: 1px solid rgba(255,255,255,0.1);
156
+ transition: background 0.2s ease;
157
+ }
158
+
159
+ .suggestion-item:hover {
160
+ background: rgba(255,255,255,0.15);
161
+ }
162
+
163
+ /* ===== INPUT BAR ===== */
164
+ .chat-input {
165
+ display: flex;
166
+ padding: 16px;
167
+ background: rgba(255,255,255,0.15);
168
+ backdrop-filter: blur(12px);
169
+ border-top: 1px solid rgba(255,255,255,0.2);
170
+ }
171
+
172
+ .chat-input input {
173
+ flex: 1;
174
+ padding: 12px 16px;
175
+ border-radius: 25px;
176
+ border: none;
177
+ outline: none;
178
+ font-size: 14px;
179
+ }
180
+
181
+ .chat-input button {
182
+ margin-left: 10px;
183
+ width: 48px;
184
+ height: 48px;
185
+ border-radius: 50%;
186
+ border: none;
187
+ background: linear-gradient(135deg, #00c6ff, #0072ff);
188
+ color: white;
189
+ font-size: 30px;
190
+ cursor: pointer;
191
+ transition: transform 0.2s ease;
192
+ }
193
+
194
+ .chat-input button:hover {
195
+ transform: scale(1.08);
196
+ }
197
+
198
+ .chat-input button:disabled {
199
+ opacity: 0.5;
200
+ cursor: not-allowed;
201
+ }
202
+
203
+ /* ===== SCROLLBAR ===== */
204
+ .chat-messages::-webkit-scrollbar {
205
+ width: 6px;
206
+ }
207
+
208
+ .chat-messages::-webkit-scrollbar-thumb {
209
+ background: rgba(255,255,255,0.3);
210
+ border-radius: 10px;
211
+ }
212
+
213
+ .video-window {
214
+ width: 100%;
215
+ max-width: 640px;
216
+ height: 80vh;
217
+ margin-left: 20px;
218
+ border-radius: 20px;
219
+ overflow: hidden;
220
+ background: rgba(255,255,255,0.05);
221
+ backdrop-filter: blur(10px);
222
+ box-shadow: 0 10px 30px rgba(0,0,0,0.4);
223
+ display: flex;
224
+ flex-direction: column;
225
+ align-items: center;
226
+ gap: 2vw;
227
+ }
228
+
229
+ /* Video fills top area */
230
+ .video-window video {
231
+ width: 100%;
232
+ height: 100%;
233
+ flex: 1;
234
+ object-fit: cover;
235
+ background: black;
236
+ }
237
+
238
+ .play-pause-btn {
239
+ position: relative;
240
+ top: -1vw;
241
+ width: 4vw;
242
+ min-width: 40px;
243
+ height: 4vw;
244
+ min-height: 40px;
245
+ cursor: pointer;
246
+ border: 2px solid #3f61ad;
247
+ border-radius: 50px;
248
+ box-shadow: 0 8px 20px rgba(63,97,173,0.18), 0 2px 6px rgba(0,0,0,0.25);
249
+ transition: transform 160ms ease, box-shadow 160ms ease;
250
+ background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.03));
251
+ }
252
+
253
+ .play-pause-btn:hover {
254
+ transform: translateY(-3px);
255
+ box-shadow: 0 16px 36px rgba(63,97,173,0.22), 0 6px 12px rgba(0,0,0,0.28);
256
+ }
257
+
258
+ .play-pause-btn:active {
259
+ transform: translateY(0);
260
+ box-shadow: 0 6px 12px rgba(0,0,0,0.22);
261
+ }
262
+
263
+ .bot-answer {
264
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif !important;
265
+ font-size: clamp(14px, 1vw, 18px);
266
+ line-height: 1.6;
267
+ }
268
+
269
+ .micBtn {
270
+ width: 44px;
271
+ height: 44px;
272
+ border: 0;
273
+ border-radius: 999px;
274
+ cursor: pointer;
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ background: #f3f4f6;
279
+ font-size: 30px !important;
280
+ }
281
+
282
+ .micBtn:disabled {
283
+ opacity: 0.5;
284
+ cursor: not-allowed;
285
+ }
286
+
287
+ .micBtn.active {
288
+ background: #111827;
289
+ }
290
+
291
+ .micIcon {
292
+ width: 22px;
293
+ height: 22px;
294
+ fill: #111827;
295
+ }
296
+
297
+ .micBtn.active .micIcon {
298
+ fill: #ffffff;
299
+ }
300
+
301
+ .actions {
302
+ display: inline-flex;
303
+ gap: 8px;
304
+ margin-left: 8px;
305
+ align-items: center;
306
+ }
307
+
308
+ .okBtn, .noBtn {
309
+ font-size: 18px;
310
+ width: 38px;
311
+ height: 38px;
312
+ border-radius: 999px;
313
+ border: 0;
314
+ cursor: pointer;
315
+ background: #f3f4f6;
316
+ }
317
+
318
+ .okBtn:disabled {
319
+ opacity: 0.5;
320
+ cursor: not-allowed;
321
+ }
322
+
323
+ .okBtn:hover, .noBtn:hover {
324
+ background: #e5e7eb;
325
+ }
326
+
327
+ .mediaRow {
328
+ display: flex;
329
+ gap: 1vw;
330
+ font-size: 1vw;
331
+ padding-top: 2vw;
332
+ }
333
+
334
+ .chip {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ min-width: 48px;
339
+ height: clamp(30px, 2vw, 36px);
340
+ padding: 0 12px;
341
+ background: linear-gradient(135deg, #00c6ff, #0072ff);
342
+ color: #ffffff;
343
+ font-weight: 600;
344
+ font-size: 0.9rem;
345
+ border: none;
346
+ border-radius: 999px;
347
+ box-shadow: 0 6px 20px rgba(0, 114, 255, 0.18), 0 2px 6px rgba(0, 0, 0, 0.25);
348
+ cursor: pointer;
349
+ transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
350
+ user-select: none;
351
+ }
352
+
353
+ .chip:hover {
354
+ transform: translateY(-3px);
355
+ box-shadow: 0 14px 30px rgba(0, 114, 255, 0.20), 0 4px 10px rgba(0, 0, 0, 0.28);
356
+ }
357
+
358
+ .chip:active {
359
+ transform: translateY(0);
360
+ box-shadow: 0 6px 14px rgba(0, 0, 0, 0.22);
361
+ opacity: 0.98;
362
+ }
363
+
364
+ .chip:focus {
365
+ outline: 3px solid rgba(0, 114, 255, 0.18);
366
+ outline-offset: 3px;
367
+ }
368
+
369
+ .arrow-btn {
370
+ position: fixed;
371
+ right: 16px;
372
+ width: 51px;
373
+ height: 51px;
374
+ border-radius: 50%;
375
+ border: none;
376
+ background: linear-gradient(135deg, #00c6ff, #0072ff);
377
+ /* background: #56cdc2;*/
378
+ color: #fff;
379
+ font-size: 31px;
380
+ box-shadow: 0 6px 16px #00000040;
381
+ cursor: pointer;
382
+ transition: transform .15s ease, box-shadow .2s ease, background .2s ease;
383
+ z-index: 20;
384
+ }
385
+
386
+ .up {
387
+ top: 1vw;
388
+ }
389
+
390
+ .down {
391
+ bottom: 5vw;
392
+ }
393
+
394
+ /* Add scroll-snap behavior so each pair fills the visible area and scrolls one-by-one */
395
+ .chat-messages {
396
+ overflow-y: auto;
397
+ scroll-snap-type: y mandatory;
398
+ -webkit-overflow-scrolling: touch;
399
+ }
400
+
401
+ /* Each pair is treated as a snap point */
402
+ /* Use 100% of the messages area so one pair is visible at a time; align content start so user appears at top */
403
+ .pair {
404
+ scroll-snap-align: start;
405
+ min-height: 100%;
406
+ display: flex;
407
+ flex-direction: column;
408
+ justify-content: flex-start;
409
+ padding: 4.5rem 1rem;
410
+ box-sizing: border-box;
411
+ gap: 2vw;
412
+ }
413
+
414
+ /* small visual spacing between pairs */
415
+ .pair + .pair {
416
+ margin-top: 8px;
417
+ }
418
+
419
+ /* preserve existing message-row styling assumed by template;
420
+ adjust these rules if your project already defines them */
421
+ .message-row {
422
+ display: flex;
423
+ align-items: flex-end;
424
+ gap: 0.6rem;
425
+ }
426
+
427
+ .bot-row {
428
+ justify-content: flex-start;
429
+ }
430
+
431
+ .user-row {
432
+ justify-content: flex-end;
433
+ }
434
+
435
+ .avatar {
436
+ width: 3.2rem;
437
+ height: 3.2rem;
438
+ border-radius: 50%;
439
+ object-fit: cover;
440
+ }
441
+
442
+ .message-bubble {
443
+ max-width: 70%;
444
+ padding: 0.6rem 0.8rem;
445
+ border-radius: 0.6rem;
446
+ position: relative;
447
+ }
448
+
449
+ .bot-message {
450
+ background: #f1f1f1;
451
+ color: #111;
452
+ }
453
+
454
+ .user-message {
455
+ /* background: #4caf50;*/
456
+ color: #fff;
457
+ background: linear-gradient(135deg, #00c6ff, #0072ff);
458
+ }
459
+
460
+ .message-time {
461
+ font-size: 0.7rem;
462
+ color: rgba(0,0,0,1);
463
+ margin-top: 6px;
464
+ text-align: right;
465
+ }
466
+
467
+ /* typing indicator stays at the end of the list */
468
+ .typing-indicator {
469
+ display: flex;
470
+ gap: 0.4rem;
471
+ padding: 0.5rem;
472
+ align-items: center;
473
+ }
474
+
475
+ .typing-indicator .dot {
476
+ width: 8px;
477
+ height: 8px;
478
+ border-radius: 50%;
479
+ background: #bbb;
480
+ animation: blink 1.2s infinite;
481
+ }
482
+
483
+ @keyframes blink {
484
+ 0% {
485
+ opacity: 0.2;
486
+ }
487
+
488
+ 50% {
489
+ opacity: 1;
490
+ }
491
+
492
+ 100% {
493
+ opacity: 0.2;
494
+ }
495
+ }
src/app/staticchat/staticchat.component.html ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="chat-container">
2
+ <div class="chat-window">
3
+ <button class="arrow-btn up" title="Prev message" (click)="showPreviousPair()">↑</button>
4
+
5
+ <!-- MESSAGES -->
6
+ <div class="chat-messages" #chatContainer>
7
+ <div *ngFor="let pair of pairedMessages; let i = index" class="pair" [attr.data-index]="i">
8
+
9
+ <!-- If user exists in pair (now shown first/top) -->
10
+ <div *ngIf="pair.user" class="message-row user-row">
11
+ <div class="message-bubble user-message">
12
+ <span style="font-size:1.5vw;">{{ pair.user.text }}</span>
13
+ <div class="message-time">
14
+ {{ pair.user.timestamp | date:'shortTime' }}
15
+ </div>
16
+ </div>
17
+ <img src="assets/staticchat/student.png" class="avatar" />
18
+ </div>
19
+
20
+ <!-- If bot exists in pair (now shown below user) -->
21
+ <div *ngIf="pair.bot" class="message-row bot-row">
22
+ <img src="assets/staticchat/teacher.png" class="avatar" />
23
+ <div class="message-bubble bot-message">
24
+ <span style="font-size:1.5vw;">{{ pair.bot.text }}</span>
25
+
26
+ <div class="mediaRow" *ngIf="pair.bot.rawData && (pair.bot.rawData?.audio_url || pair.bot.rawData?.video_url)">
27
+ <button class="chip" *ngIf="pair.bot.rawData?.audio_url" (click)="playAudio(pair.bot.rawData?.audio_url)">
28
+ 🔊 Audio
29
+ </button>
30
+ <button class="chip" *ngIf="pair.bot.rawData?.video_url" (click)="playResponseVideo(pair.bot.rawData?.video_url)">
31
+ 🎬 Video
32
+ </button>
33
+ <button class="chip" *ngIf="pair.bot.rawData?.detail_url" (click)="playResponseVideo(pair.bot.rawData?.detail_url)">
34
+ 💡 Detail
35
+ </button>
36
+ <button class="chip" *ngIf="pair.bot.rawData?.story_url" (click)="playResponseVideo(pair.bot.rawData?.story_url)">
37
+ 📖 Story
38
+ </button>
39
+ <button class="chip" *ngIf="pair.bot.rawData?.example_url" (click)="playResponseVideo(pair.bot.rawData?.example_url)">
40
+ 🧪 Example
41
+ </button>
42
+ </div>
43
+
44
+ <div class="message-time">
45
+ {{ pair.bot.timestamp | date:'shortTime' }}
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ </div>
51
+
52
+ <!-- Typing indicator -->
53
+ <div *ngIf="isTyping" class="typing-indicator">
54
+ <div class="dot"></div>
55
+ <div class="dot"></div>
56
+ <div class="dot"></div>
57
+ </div>
58
+ </div>
59
+ <button class="arrow-btn down" title="Next message" (click)="showNextPair()">↓</button>
60
+
61
+ <!-- SUGGESTIONS DROPDOWN -->
62
+ <div *ngIf="showSuggestions && suggestedQuestions.length > 0" class="suggestions-box">
63
+ <div *ngFor="let question of suggestedQuestions"
64
+ class="suggestion-item"
65
+ (click)="selectQuestion(question)">
66
+ {{ question }}
67
+ </div>
68
+ </div>
69
+
70
+ <!-- INPUT BAR -->
71
+ <form [formGroup]="chatForm" (ngSubmit)="sendMessage()" class="chat-input">
72
+ <!--<input type="text"
73
+ formControlName="message"
74
+ #messageInput
75
+ placeholder="Type your message..."
76
+ (focus)="onInputFocus()"
77
+ (input)="onInputChange()"
78
+ [readonly]="isSpeechProcessing" />-->
79
+ <input type="text"
80
+ formControlName="message"
81
+ #messageInput
82
+ [placeholder]="isListening ? '⏺ Listening...' : 'Type your message...'"
83
+ (focus)="onInputFocus()"
84
+ (input)="onInputChange()"
85
+ [readonly]="isSpeechProcessing || isListening" />
86
+
87
+ <button type="button"
88
+ class="micBtn"
89
+ [disabled]="!supported || isListening"
90
+ (click)="toggleMic()">
91
+ 🎤
92
+ </button>
93
+
94
+ <div class="actions" *ngIf="showActions">
95
+ <button class="reject" (click)="reject()">❌</button>
96
+ <button class="accept" (click)="accept()">✅</button>
97
+
98
+ </div>
99
+
100
+ <button type="submit" [disabled]="!chatForm.valid">
101
+
102
+ </button>
103
+ </form>
104
+
105
+ </div>
106
+
107
+
108
+ <div class="video-window">
109
+
110
+ <video #videoPlayer autoplay muted playsinline></video>
111
+ <img class="play-pause-btn"
112
+ [src]="(currentVideoType !== 'blink' && isVideoPlaying) ? 'assets/staticchat/pause.png' : 'assets/staticchat/play.png'"
113
+ (click)="togglePlayPause()" />
114
+ </div>
115
+ </div>
src/app/staticchat/staticchat.component.ts ADDED
@@ -0,0 +1,756 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // (file header unchanged)
2
+ import { Component, OnInit, ViewChild, ElementRef, HostListener, AfterViewInit } from '@angular/core';
3
+ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
4
+ import { ChatService, ChatMessage, SearchResponse, Question } from './staticchat.service';
5
+ import { Subject } from 'rxjs';
6
+ import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
7
+ import { isPlatformBrowser } from '@angular/common';
8
+ import {
9
+ Inject,
10
+ NgZone,
11
+ PLATFORM_ID,
12
+ } from '@angular/core';
13
+
14
+
15
+ @Component({
16
+ selector: 'app-staticchat',
17
+ templateUrl: './staticchat.component.html',
18
+ styleUrls: ['./staticchat.component.css']
19
+ })
20
+ export class StaticChatComponent implements OnInit, AfterViewInit {
21
+
22
+ @ViewChild('chatContainer') private chatContainer!: ElementRef;
23
+ @ViewChild('messageInput') private messageInput!: ElementRef;
24
+ @ViewChild('videoPlayer') videoRef!: ElementRef<HTMLVideoElement>;
25
+
26
+ chatForm: FormGroup;
27
+ messages: (ChatMessage & { suggestions?: string[] })[] = [];
28
+ isTyping = false;
29
+ suggestedQuestions: string[] = [];
30
+ showSuggestions = false;
31
+ allQuestions: Question[] = [];
32
+ searchQuery = new Subject<string>();
33
+ selectedQuestions: Set<string> = new Set();
34
+
35
+ // navigation index for pair view
36
+ currentPairIndex = 0;
37
+
38
+ // 🎬 Video sources
39
+ blinkVideoSrc = 'assets/staticchat/blink.mp4';
40
+ introVideoSrc = 'assets/staticchat/intro.mp4';
41
+
42
+ // Video state
43
+ // 'blink' | 'intro' | 'response' to indicate currently loaded video type
44
+ currentVideoType: 'blink' | 'intro' | 'response' = 'blink';
45
+ currentResponseVideoUrl: string | null = null;
46
+ // whether the currently loaded non-idle video is playing
47
+ isVideoPlaying = false;
48
+
49
+ // audio player for response audio
50
+ private audioPlayer: HTMLAudioElement | null = null;
51
+ hasChatStarted = false;
52
+ lastResponseVideoUrl: string | null = null;
53
+
54
+ supported = false;
55
+ isListening = false; // we will treat this as "isRecording"
56
+ showActions = false;
57
+
58
+ private isBrowser = false;
59
+
60
+ private mediaStream: MediaStream | null = null;
61
+ private recorder: MediaRecorder | null = null;
62
+ private chunks: BlobPart[] = [];
63
+
64
+ private uploadInProgress = false;
65
+ isSpeechProcessing = false;
66
+
67
+ constructor(
68
+ private fb: FormBuilder,
69
+ private chatService: ChatService,
70
+ @Inject(PLATFORM_ID) platformId: object, private zone: NgZone
71
+ ) {
72
+ this.chatForm = this.fb.group({
73
+ message: ['', Validators.required]
74
+ });
75
+
76
+ this.searchQuery.pipe(
77
+ debounceTime(300),
78
+ distinctUntilChanged()
79
+ ).subscribe(query => {
80
+ this.searchQuestions(query);
81
+ });
82
+ this.isBrowser = isPlatformBrowser(platformId);
83
+ }
84
+
85
+ ngOnInit() {
86
+ this.messages.push({
87
+ id: 1,
88
+ text: 'Hello children! Today we will learn tenses in a simple and fun way.',
89
+ sender: 'bot',
90
+ timestamp: new Date()
91
+ });
92
+
93
+ if (!this.isBrowser) return;
94
+
95
+ const hasGetUserMedia = !!navigator.mediaDevices?.getUserMedia;
96
+ const hasMediaRecorder = typeof (window as any).MediaRecorder !== 'undefined';
97
+ this.supported = hasGetUserMedia && hasMediaRecorder;
98
+
99
+ this.loadAllQuestions();
100
+
101
+ // start at last pair by default
102
+ setTimeout(() => this.scrollToLastPair(), 0);
103
+ }
104
+
105
+ ngAfterViewInit() {
106
+ this.playBlinkVideo();
107
+ }
108
+
109
+ /* ================= VIDEO CONTROL HELPERS ================= */
110
+
111
+ private safeVideo(): HTMLVideoElement | null {
112
+ try { return this.videoRef.nativeElement; } catch { return null; }
113
+ }
114
+
115
+ // Idle blink loop — keep playing but show PLAY icon in UI
116
+ playBlinkVideo() {
117
+ const video = this.safeVideo();
118
+ if (!video) return;
119
+
120
+ video.onended = null;
121
+ video.src = this.blinkVideoSrc;
122
+ video.loop = true;
123
+ video.muted = true;
124
+ video.currentTime = 0;
125
+ video.play().catch(() => { /* ignore autoplay failure for idle */ });
126
+
127
+ this.currentVideoType = 'blink';
128
+ this.currentResponseVideoUrl = null;
129
+ // For blink we show the Play icon, so set isVideoPlaying = false
130
+ this.isVideoPlaying = false;
131
+ }
132
+
133
+ // Load and start intro (user intends to watch intro)
134
+ playIntroVideo() {
135
+ const video = this.safeVideo();
136
+ if (!video) return;
137
+
138
+ // stop any audio
139
+ if (this.audioPlayer && !this.audioPlayer.paused) { this.audioPlayer.pause(); }
140
+
141
+ video.onended = () => {
142
+ this.playBlinkVideo();
143
+ };
144
+
145
+ video.src = this.introVideoSrc;
146
+ video.loop = false;
147
+ video.muted = false;
148
+ video.currentTime = 0;
149
+ video.play().catch(() => {
150
+ // fallback muted play if autoplay blocked
151
+ video.muted = true;
152
+ video.play().catch(() => { /* ignore */ });
153
+ });
154
+
155
+ this.currentVideoType = 'intro';
156
+ this.currentResponseVideoUrl = null;
157
+ this.isVideoPlaying = true;
158
+ }
159
+
160
+ // Play a response video (from chat) in the same player.
161
+ // After the response ends return to blink.
162
+ playResponseVideo(url?: string) {
163
+ if (!url) return;
164
+ const video = this.safeVideo();
165
+ if (!video) return;
166
+
167
+ // stop any audio
168
+ if (this.audioPlayer && !this.audioPlayer.paused) { this.audioPlayer.pause(); }
169
+
170
+ video.onended = () => {
171
+ this.playBlinkVideo();
172
+ };
173
+
174
+ this.currentResponseVideoUrl = url;
175
+ this.currentVideoType = 'response';
176
+
177
+ video.src = url;
178
+ video.loop = false;
179
+ video.muted = false;
180
+ video.currentTime = 0;
181
+ video.play().then(() => {
182
+ this.isVideoPlaying = true;
183
+ }).catch(() => {
184
+ // If autoplay blocked, try muted play as fallback
185
+ video.muted = true;
186
+ video.play().catch(() => { /* ignore */ });
187
+ // set state according to actual playing state
188
+ this.isVideoPlaying = !video.paused;
189
+ });
190
+ }
191
+
192
+ // Top-right button behavior:
193
+ // - If blink is running → start intro.
194
+ // - If intro/response loaded → toggle play/pause for that loaded video.
195
+ togglePlayPause() {
196
+ const video = this.safeVideo();
197
+ if (!video) return;
198
+
199
+ if (this.currentVideoType === 'blink') {
200
+ // Before first question → intro
201
+ if (!this.hasChatStarted) {
202
+ this.playIntroVideo();
203
+ return;
204
+ }
205
+
206
+ // After chat started → do NOT play intro again
207
+ // Replay last response video if available
208
+ if (this.lastResponseVideoUrl) {
209
+ this.playResponseVideo(this.lastResponseVideoUrl);
210
+ }
211
+ return;
212
+ }
213
+
214
+ // If user is starting/resuming a video, pause any playing audio first
215
+ if (video.paused) {
216
+ if (this.audioPlayer && !this.audioPlayer.paused) {
217
+ this.audioPlayer.pause();
218
+ }
219
+ video.play().catch(() => { /* ignore */ });
220
+ this.isVideoPlaying = true;
221
+ } else {
222
+ video.pause();
223
+ this.isVideoPlaying = false;
224
+ }
225
+ }
226
+
227
+
228
+ /* ================= CHAT SYSTEM ================= */
229
+
230
+ loadAllQuestions() {
231
+ this.chatService.getAllQuestions().subscribe({
232
+ next: (response) => {
233
+ if (response.success) {
234
+ this.allQuestions = response.questions;
235
+ }
236
+ },
237
+ error: (error) => console.error('Error loading questions:', error)
238
+ });
239
+ }
240
+
241
+ onInputFocus() { this.showQuestionSuggestions(); }
242
+ onInputClick() { this.showQuestionSuggestions(); }
243
+
244
+ showQuestionSuggestions() {
245
+ if (this.allQuestions.length === 0) {
246
+ this.loadAllQuestions();
247
+ return;
248
+ }
249
+
250
+ if (this.messages.length <= 1) {
251
+ this.suggestedQuestions = this.allQuestions.slice(0, 5).map(q => q.question);
252
+ this.showSuggestions = true;
253
+ return;
254
+ }
255
+
256
+ const unselected = this.allQuestions.filter(q => !this.selectedQuestions.has(q.question));
257
+
258
+ if (unselected.length === 0) {
259
+ const shuffled = [...this.allQuestions].sort(() => 0.5 - Math.random());
260
+ this.suggestedQuestions = shuffled.slice(0, 5).map(q => q.question);
261
+ } else {
262
+ this.suggestedQuestions = unselected.slice(0, 5).map(q => q.question);
263
+ }
264
+
265
+ this.showSuggestions = true;
266
+ }
267
+
268
+ searchQuestions(query: string) {
269
+ if (query.length > 0) {
270
+ const filtered = this.allQuestions
271
+ .filter(q => q.question.toLowerCase().includes(query.toLowerCase()))
272
+ .slice(0, 5);
273
+
274
+ this.suggestedQuestions = filtered.map(q => q.question);
275
+ this.showSuggestions = this.suggestedQuestions.length > 0;
276
+ } else {
277
+ this.showQuestionSuggestions();
278
+ }
279
+ }
280
+
281
+ onInputChange() {
282
+ const query = this.chatForm.get('message')?.value;
283
+ query ? this.searchQuery.next(query) : this.showQuestionSuggestions();
284
+ }
285
+
286
+ selectQuestion(question: string) {
287
+ this.selectedQuestions.add(question);
288
+ this.chatForm.get('message')?.setValue(question);
289
+ this.showSuggestions = false;
290
+ this.suggestedQuestions = this.suggestedQuestions.filter(q => q !== question);
291
+ this.sendMessage();
292
+ }
293
+
294
+ sendMessage() {
295
+ const message = this.chatForm.get('message')?.value.trim();
296
+ if (!message) return;
297
+
298
+ this.messages.push({
299
+ id: this.messages.length + 1,
300
+ text: message,
301
+ sender: 'user',
302
+ timestamp: new Date()
303
+ });
304
+ this.hasChatStarted = true;
305
+ this.chatForm.reset();
306
+ this.showSuggestions = false;
307
+ this.isTyping = true;
308
+
309
+ // show the pair containing this user message (may be a user-only pair until bot replies)
310
+ setTimeout(() => this.scrollToLastPair(), 50);
311
+
312
+ this.chatService.searchQuestion(message).subscribe({
313
+ next: (response: SearchResponse) => {
314
+ this.isTyping = false;
315
+
316
+ const botText = response.answer
317
+ ? response.answer.replace(/\n/g, ' ')
318
+ : (response.message || 'Sorry, I could not find an answer.');
319
+
320
+ this.messages.push({
321
+ id: this.messages.length + 1,
322
+ text: botText,
323
+ sender: 'bot',
324
+ timestamp: new Date(),
325
+ rawData: response
326
+ });
327
+
328
+ // scroll to the new pair (user+bot)
329
+ setTimeout(() => this.scrollToLastPair(), 50);
330
+
331
+ // Play audio/video returned by the response
332
+ if (response.audio_url) {
333
+ this.playAudio(response.audio_url);
334
+ }
335
+ if (response.video_url) {
336
+ this.lastResponseVideoUrl = response.video_url; // ✅ remember it
337
+ this.playResponseVideo(response.video_url);
338
+ }
339
+
340
+ },
341
+ error: () => {
342
+ this.isTyping = false;
343
+ this.messages.push({
344
+ id: this.messages.length + 1,
345
+ text: 'Sorry, I encountered an error. Please try again.',
346
+ sender: 'bot',
347
+ timestamp: new Date()
348
+ });
349
+ setTimeout(() => this.scrollToLastPair(), 50);
350
+ }
351
+ });
352
+ }
353
+
354
+ // Play audio directly (uses a single HTMLAudioElement instance)
355
+ playAudio(url?: string) {
356
+ if (!url) return;
357
+ try {
358
+ const video = this.safeVideo();
359
+ // If a non-idle video is currently playing, pause it before starting audio
360
+ if (video && this.currentVideoType !== 'blink' && !video.paused) {
361
+ video.pause();
362
+ this.isVideoPlaying = false;
363
+ }
364
+
365
+ if (!this.audioPlayer) {
366
+ this.audioPlayer = new Audio();
367
+ } else {
368
+ this.audioPlayer.pause();
369
+ }
370
+ this.audioPlayer.src = url;
371
+ this.audioPlayer.currentTime = 0;
372
+ this.audioPlayer.play().catch(() => { /* ignore autoplay errors */ });
373
+ } catch (e) {
374
+ console.error('Audio play failed', e);
375
+ }
376
+ }
377
+
378
+ // helper used by template to play video for a chat item
379
+ playVideoFromChat(url?: string) {
380
+ if (!url) return;
381
+ this.playResponseVideo(url);
382
+ }
383
+
384
+ formatAnswer(response: SearchResponse): string {
385
+ let html = '';
386
+
387
+ const answerText = response.answer?.replace(/\n/g, '<br>') ?? 'No answer available.';
388
+ html += `<div class="bot-answer">${answerText}</div>`;
389
+
390
+ if (response.audio_url || response.video_url) {
391
+ html += `<div class="media-row">`;
392
+
393
+ if (response.audio_url) {
394
+ html += `
395
+ <span class="media-icon"
396
+ onclick="window.dispatchEvent(new CustomEvent('playAudio', { detail: '${response.audio_url}' }))">
397
+ 🎧
398
+ </span>`;
399
+ }
400
+
401
+ if (response.video_url) {
402
+ html += `
403
+ <span class="media-icon"
404
+ onclick="window.dispatchEvent(new CustomEvent('playVideo', { detail: '${response.video_url}' }))">
405
+ 📺
406
+ </span>`;
407
+ }
408
+
409
+ html += `</div>`;
410
+ }
411
+
412
+ return html;
413
+ }
414
+
415
+ formatErrorMessage(response: SearchResponse): string {
416
+ let message = response.message || "I couldn't find an exact match.";
417
+
418
+ if (response.sample_questions?.length) {
419
+ message += '<br><br><strong>Try asking:</strong><ul>';
420
+ response.sample_questions.forEach(q => message += `<li>${q}</li>`);
421
+ message += '</ul>';
422
+ }
423
+
424
+ return message;
425
+ }
426
+
427
+ // Build pairs: each pair is { user?: ChatMessage, bot?: ChatMessage }
428
+ get pairedMessages(): Array<{ user?: ChatMessage, bot?: ChatMessage }> {
429
+ const pairs: Array<{ user?: ChatMessage, bot?: ChatMessage }> = [];
430
+ const msgs = this.messages || [];
431
+ let i = 0;
432
+ while (i < msgs.length) {
433
+ const current = msgs[i];
434
+ if (current.sender === 'user') {
435
+ const pair: { user?: ChatMessage, bot?: ChatMessage } = { user: current };
436
+ const next = msgs[i + 1];
437
+ if (next && next.sender === 'bot') {
438
+ pair.bot = next;
439
+ i += 2;
440
+ } else {
441
+ i += 1;
442
+ }
443
+ pairs.push(pair);
444
+ } else if (current.sender === 'bot') {
445
+ // bot message without preceding user (welcome message, errors, etc.)
446
+ pairs.push({ bot: current });
447
+ i += 1;
448
+ } else {
449
+ // fallback: add as single
450
+ pairs.push({ bot: current });
451
+ i += 1;
452
+ }
453
+ }
454
+ return pairs;
455
+ }
456
+
457
+ // Scroll helpers for pair navigation
458
+ showNextPair() {
459
+ const total = this.pairedMessages.length;
460
+ if (total === 0) return;
461
+ const next = Math.min(this.currentPairIndex + 1, total - 1);
462
+ this.scrollToPair(next);
463
+ }
464
+
465
+ showPreviousPair() {
466
+ const total = this.pairedMessages.length;
467
+ if (total === 0) return;
468
+ const prev = Math.max(this.currentPairIndex - 1, 0);
469
+ this.scrollToPair(prev);
470
+ }
471
+
472
+ private scrollToPair(index: number) {
473
+ setTimeout(() => {
474
+ try {
475
+ const container = this.chatContainer.nativeElement as HTMLElement;
476
+ const pairs = container.querySelectorAll('.pair');
477
+ if (!pairs || pairs.length === 0) return;
478
+ if (index < 0) index = 0;
479
+ if (index >= pairs.length) index = pairs.length - 1;
480
+ const target = pairs[index] as HTMLElement;
481
+ if (!target) return;
482
+ container.scrollTo({ top: target.offsetTop, behavior: 'smooth' });
483
+ this.currentPairIndex = index;
484
+ } catch (e) {
485
+ // fallback: scroll to bottom/top
486
+ try {
487
+ const container = this.chatContainer.nativeElement as HTMLElement;
488
+ if (index === 0) container.scrollTop = 0;
489
+ else container.scrollTop = container.scrollHeight;
490
+ } catch { }
491
+ }
492
+ }, 50);
493
+ }
494
+
495
+ private scrollToLastPair(): void {
496
+ setTimeout(() => {
497
+ try {
498
+ const total = this.pairedMessages.length;
499
+ if (total === 0) return;
500
+ this.scrollToPair(total - 1);
501
+ } catch { }
502
+ }, 50);
503
+ }
504
+
505
+ scrollToTop(): void {
506
+ setTimeout(() => {
507
+ try {
508
+ const el = this.chatContainer.nativeElement as HTMLElement;
509
+ // Use smooth scroll if supported, otherwise fall back to direct assignment
510
+ if (typeof el.scrollTo === 'function') {
511
+ el.scrollTo({ top: 0, behavior: 'smooth' });
512
+ } else {
513
+ el.scrollTop = 0;
514
+ }
515
+ } catch { }
516
+ }, 100);
517
+ }
518
+
519
+ clearChat() {
520
+ this.messages = [];
521
+ this.selectedQuestions.clear();
522
+
523
+ this.hasChatStarted = false;
524
+ this.lastResponseVideoUrl = null;
525
+
526
+ this.ngOnInit();
527
+ this.playBlinkVideo();
528
+ }
529
+
530
+
531
+ private pickMimeType(): string {
532
+ const w: any = window;
533
+
534
+ // Try in order. Different browsers support different types.
535
+ const types = [
536
+ 'audio/webm;codecs=opus', // Chrome/Edge/Firefox (best)
537
+ 'audio/webm',
538
+ 'audio/mp4', // Safari (sometimes)
539
+ 'audio/m4a',
540
+ ];
541
+
542
+ if (!w.MediaRecorder?.isTypeSupported) return '';
543
+ for (const t of types) {
544
+ if (w.MediaRecorder.isTypeSupported(t)) return t;
545
+ }
546
+ return '';
547
+ }
548
+
549
+ async toggleMic() {
550
+ if (!this.supported || this.isListening || this.uploadInProgress) return;
551
+
552
+ try {
553
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({
554
+ audio: {
555
+ echoCancellation: true,
556
+ noiseSuppression: true,
557
+ },
558
+ });
559
+
560
+ const mimeType = this.pickMimeType();
561
+ this.chunks = [];
562
+
563
+ this.recorder = mimeType
564
+ ? new MediaRecorder(this.mediaStream, { mimeType })
565
+ : new MediaRecorder(this.mediaStream);
566
+
567
+ this.recorder.ondataavailable = (e: BlobEvent) => {
568
+ if (e.data && e.data.size > 0) this.chunks.push(e.data);
569
+ };
570
+
571
+ this.recorder.onerror = () => {
572
+ this.zone.run(() => {
573
+ this.handleTranscriptionError('Audio recording error.');
574
+ this.cleanupRecorder();
575
+ });
576
+ };
577
+
578
+ this.zone.run(() => {
579
+ this.isListening = true;
580
+ this.showActions = true;
581
+ });
582
+
583
+ this.recorder.start();
584
+ } catch (e: any) {
585
+ this.zone.run(() => {
586
+ this.handleTranscriptionError('Microphone permission denied or not available.');
587
+ this.cleanupRecorder();
588
+ });
589
+ }
590
+ }
591
+
592
+ // ✅ Stop + transcribe
593
+ accept() {
594
+ if (!this.recorder || this.uploadInProgress) return;
595
+
596
+ this.uploadInProgress = true;
597
+
598
+ // We need the blob only after "stop" finishes
599
+ this.recorder.onstop = async () => {
600
+ try {
601
+ const mime = this.recorder?.mimeType || 'audio/webm';
602
+ const blob = new Blob(this.chunks, { type: mime });
603
+
604
+ // 🔄 Show loading state in input
605
+ this.zone.run(() => {
606
+ this.isSpeechProcessing = true;
607
+ this.showActions = false;
608
+ this.isListening = false;
609
+ this.chatForm.get('message')?.setValue('⏳ Converting speech to text...');
610
+ });
611
+
612
+ const text = await this.sendToBackendForTranscription(blob);
613
+
614
+ this.zone.run(() => {
615
+ this.isSpeechProcessing = false;
616
+ if (text && text.trim()) {
617
+ this.handleTranscriptionAccepted(text.trim());
618
+ } else {
619
+ this.chatForm.get('message')?.setValue('');
620
+ }
621
+ });
622
+
623
+ } catch (err: any) {
624
+ this.zone.run(() => {
625
+ this.handleTranscriptionError(
626
+ typeof err?.message === 'string' ? err.message : 'Transcription failed.'
627
+ );
628
+ this.showActions = false;
629
+ this.isListening = false;
630
+ });
631
+ } finally {
632
+ this.uploadInProgress = false;
633
+ this.cleanupRecorder();
634
+ }
635
+ };
636
+
637
+ try {
638
+ this.recorder.stop();
639
+ } catch {
640
+ // If stop fails, still cleanup
641
+ this.uploadInProgress = false;
642
+ this.cleanupRecorder();
643
+ }
644
+ }
645
+
646
+ // ❌ Stop + discard
647
+ reject() {
648
+ if (this.uploadInProgress) return;
649
+
650
+ try {
651
+ this.recorder?.stop();
652
+ } catch { }
653
+
654
+ this.zone.run(() => {
655
+ this.handleTranscriptionRejected();
656
+ this.showActions = false;
657
+ this.isListening = false;
658
+ });
659
+
660
+ this.cleanupRecorder();
661
+ }
662
+
663
+ private async sendToBackendForTranscription(blob: Blob): Promise<string> {
664
+ // Change this URL if your backend route is different
665
+ const url = 'http://localhost:5000/api/transcribe';
666
+
667
+ const form = new FormData();
668
+ // Keep extension generic; backend can read mimetype
669
+ form.append('file', blob, 'speech.webm');
670
+
671
+ const res = await fetch(url, {
672
+ method: 'POST',
673
+ body: form,
674
+ });
675
+
676
+ if (!res.ok) {
677
+ const msg = await res.text().catch(() => '');
678
+ throw new Error(msg || `Transcribe API failed (${res.status}).`);
679
+ }
680
+
681
+ const data = await res.json();
682
+ // Expect { text: "..." }
683
+ return (data?.text || '').toString();
684
+ }
685
+
686
+ private cleanupRecorder() {
687
+ try {
688
+ this.recorder?.removeEventListener?.('dataavailable', () => { });
689
+ } catch { }
690
+
691
+ this.recorder = null;
692
+ this.chunks = [];
693
+
694
+ if (this.mediaStream) {
695
+ try {
696
+ this.mediaStream.getTracks().forEach((t) => t.stop());
697
+ } catch { }
698
+ this.mediaStream = null;
699
+ }
700
+ }
701
+
702
+
703
+ @HostListener('document:click', ['$event'])
704
+ handleClickOutside(event: Event) {
705
+ if (this.showSuggestions && this.messageInput) {
706
+ const clickedInside = this.messageInput.nativeElement.contains(event.target);
707
+ if (!clickedInside) this.showSuggestions = false;
708
+ }
709
+ }
710
+
711
+ /* ========== Internal handlers (replace Outputs) ========== */
712
+
713
+ private handleTranscriptionAccepted(text: string) {
714
+ try {
715
+ // Put recognized text into input field ONLY
716
+ this.chatForm.get('message')?.setValue(text);
717
+
718
+ // Keep cursor at end (optional but good UX)
719
+ setTimeout(() => {
720
+ this.messageInput?.nativeElement.focus();
721
+ }, 0);
722
+
723
+ } catch (e) {
724
+ console.error('handleTranscriptionAccepted error', e);
725
+ }
726
+ }
727
+
728
+
729
+ private handleTranscriptionRejected() {
730
+ // Clear input and keep UI consistent
731
+ try {
732
+ this.chatForm.get('message')?.setValue('');
733
+ } catch (e) {
734
+ console.error('handleTranscriptionRejected error', e);
735
+ }
736
+ }
737
+
738
+ private handleTranscriptionError(msg: string) {
739
+ try {
740
+ this.isSpeechProcessing = false;
741
+ this.chatForm.get('message')?.setValue('');
742
+
743
+ this.messages.push({
744
+ id: this.messages.length + 1,
745
+ text: `Transcription error: ${msg}`,
746
+ sender: 'bot',
747
+ timestamp: new Date()
748
+ });
749
+ setTimeout(() => this.scrollToLastPair(), 50);
750
+ } catch (e) {
751
+ console.error('handleTranscriptionError error', e);
752
+ }
753
+ }
754
+
755
+
756
+ }
src/app/staticchat/staticchat.service.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ export interface ChatMessage {
7
+ id?: number;
8
+ text: string;
9
+ sender: 'user' | 'bot';
10
+ timestamp: Date;
11
+ isTyping?: boolean;
12
+ rawData?: any;
13
+ }
14
+
15
+ export interface SearchResponse {
16
+ success: boolean;
17
+ matched_question?: string;
18
+ answer?: string;
19
+ sno?: number;
20
+ audio_url?: string;
21
+ video_url?: string;
22
+ story_url?: string;
23
+ detail_url?: string;
24
+ example_url?: string;
25
+ confidence_score?: number;
26
+ user_question?: string;
27
+ message?: string;
28
+ suggestion?: string;
29
+ sample_questions?: string[];
30
+ total_questions_available?: number;
31
+ matching_method?: string;
32
+ is_follow_up?: boolean;
33
+ enhanced_question?: string;
34
+ scenario?: string;
35
+ context_info?: {
36
+ current_topic: string | null;
37
+ current_intent: string | null;
38
+ has_context: boolean;
39
+ history_length: number;
40
+ };
41
+ }
42
+
43
+ export interface Question {
44
+ sno: number;
45
+ question: string;
46
+ }
47
+
48
+ @Injectable({
49
+ providedIn: 'root'
50
+ })
51
+ export class ChatService {
52
+
53
+ private readonly apiBase =
54
+ location.hostname.endsWith('hf.space')
55
+ ? 'https://pykara-py-learn-backend.hf.space'
56
+ : 'http://localhost:5000/staticchat';
57
+
58
+
59
+
60
+ // =====================================================
61
+ // Static user_id: generated once per browser session.
62
+ // Persists across page navigations within the same tab,
63
+ // but resets when the tab/browser is closed.
64
+ //
65
+ // Options:
66
+ // sessionStorage — resets when tab closes (recommended)
67
+ // localStorage — persists even after browser restart
68
+ // =====================================================
69
+ private userId: string;
70
+
71
+ constructor(private http: HttpClient) {
72
+ // Try to restore existing session ID
73
+ const stored = sessionStorage.getItem('chat_user_id');
74
+ if (stored) {
75
+ this.userId = stored;
76
+ } else {
77
+ // Generate a new one for this session
78
+ this.userId = uuidv4();
79
+ sessionStorage.setItem('chat_user_id', this.userId);
80
+ }
81
+ console.log('Chat session user_id:', this.userId);
82
+ }
83
+
84
+ /**
85
+ * Get the current user ID (useful for context endpoints)
86
+ */
87
+ getUserId(): string {
88
+ return this.userId;
89
+ }
90
+
91
+ /**
92
+ * Reset the session — clears context on backend and generates a new user_id
93
+ */
94
+ resetSession(): Observable<{ success: boolean; message: string }> {
95
+ const oldUserId = this.userId;
96
+
97
+ // Generate new user_id
98
+ this.userId = uuidv4();
99
+ sessionStorage.setItem('chat_user_id', this.userId);
100
+ console.log('Session reset. New user_id:', this.userId);
101
+
102
+ // Clear old context on the backend
103
+ return this.http.post<{ success: boolean; message: string }>(
104
+ `${this.apiBase}/context/${oldUserId}/clear`, {}
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Search — sends user_id with every request for context carry-forward
110
+ */
111
+ searchQuestion(question: string): Observable<SearchResponse> {
112
+ return this.http.post<SearchResponse>(
113
+ `${this.apiBase}/search`,
114
+ {
115
+ question,
116
+ user_id: this.userId // <-- This is the key fix
117
+ }
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Get all questions for reference / autocomplete
123
+ */
124
+ getAllQuestions(): Observable<{ success: boolean; questions: Question[]; count: number }> {
125
+ return this.http.get<{ success: boolean; questions: Question[]; count: number }>(
126
+ `${this.apiBase}/questions`
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Get random suggestions
132
+ */
133
+ getRandomSuggestions(count: number = 5): Observable<{ success: boolean; suggestions: string[] }> {
134
+ return this.http.get<{ success: boolean; suggestions: string[] }>(
135
+ `${this.apiBase}/suggestions`,
136
+ { params: { count: count.toString() } }
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Get context-aware follow-up suggestions based on conversation history
142
+ */
143
+ getContextSuggestions(): Observable<{ success: boolean; suggestions: string[]; current_topic: string | null }> {
144
+ return this.http.get<{ success: boolean; suggestions: string[]; current_topic: string | null }>(
145
+ `${this.apiBase}/context/suggestions/${this.userId}`
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Get current conversation context (for debugging or UI display)
151
+ */
152
+ getContext(): Observable<any> {
153
+ return this.http.get(`${this.apiBase}/context/${this.userId}`);
154
+ }
155
+ }