Anupriya commited on
Commit
59fecf2
·
1 Parent(s): 16002bb

Added Chat stop method and image

Browse files
src/app/chat/chat.component.css CHANGED
@@ -1,11 +1,9 @@
1
- /* Header Container Styling */
2
  .header-container {
3
  display: flex;
4
  justify-content: space-between;
5
  align-items: center;
6
  padding: 0vw 1vw;
7
- background-color: #03182d;
8
- /* background-color: #c19929;*/
9
  box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.2);
10
  width: 100%;
11
  position: sticky;
@@ -17,17 +15,17 @@
17
  max-width: 5vw;
18
  height: auto;
19
  background: #e5e7eb;
20
- border-radius: 1vw;
21
  margin: 0.5vw;
22
  }
23
 
24
  .home-btn img {
25
- width: 5vw; /* Adjust the size of the home icon */
26
  }
27
 
28
- .home-btn img:hover {
29
- transform: scale(1.1); /* Slight zoom-in effect on hover */
30
- }
31
 
32
  h1 {
33
  font-size: 3vw;
@@ -47,17 +45,12 @@ h1 {
47
  font-display: swap;
48
  }
49
 
50
-
51
-
52
- /* Full-Screen Chat Container */
53
  .chat-container {
54
  display: flex;
55
  flex-direction: column;
56
- height: 100vh; /* Full viewport height */
57
- width: 100%; /* Full viewport width */
58
- /*background: linear-gradient(to bottom right, #fefefe, #f3f4f6);*/ /* Soft white-gray gradient */
59
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
60
-
61
  }
62
 
63
  .chat-bg {
@@ -71,72 +64,53 @@ h1 {
71
  opacity: 0.2;
72
  }
73
 
74
- /*.chat-box {
75
- position: relative;
76
- }*/
77
-
78
-
79
-
80
- /* Chat Header */
81
  .chat-header {
82
  text-align: center;
83
- background: linear-gradient(to right, #4ca1af, #6ac5cb); /* Modern purple-pink gradient */
84
  color: white;
85
  padding: 0px 0;
86
  font-size: 2.2rem;
87
  font-weight: bold;
88
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
89
- flex-shrink: 0; /* Prevent shrinking */
90
  }
91
 
92
-
93
  .chat-box {
94
  display: flex;
95
  flex-direction: column;
96
  overflow-y: auto;
97
- height: calc(100vh - 180px); /* Adjust height as needed */
98
  padding: 2vw;
99
  background-color: rgba(35, 34, 32, 0.43);
100
- scroll-behavior: smooth; /* Smooth scrolling */
101
  flex-grow: 1;
102
  padding-bottom: 90px;
103
  }
104
 
105
-
106
-
107
-
108
-
109
-
110
  .input-box {
111
  display: flex;
112
- gap: 10px; /* Reduce gap between elements */
113
- padding: 8px; /* Reduce padding */
114
- background: #03182d; /* Blue background */
115
- /*background: #c19929;*/ /* Blue background */
116
- box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); /* Lighter shadow for a subtle effect */
117
  flex-shrink: 0;
118
  width: 100%;
119
- height: auto; /* Adjust height dynamically */
120
- min-height: 40px; /* Ensure it doesn't shrink too much */
121
  font-weight: bold;
122
  }
123
 
124
-
125
-
126
-
127
-
128
-
129
  .input-box textarea {
130
  flex: 1;
131
- height: auto; /* Automatically adjust height */
132
- max-height: 80px; /* Limit the maximum height */
133
- overflow-y: auto; /* Allow scrolling if needed */
134
- resize: none; /* Disable manual resizing */
135
- padding: 8px; /* Adjust padding for proper alignment */
136
- font-size: 1rem; /* Font size for input text */
137
- line-height: 1.2; /* Adjust line height for vertical centering */
138
  border: 1px solid rgb(93, 145, 195);
139
- border-radius: 8px; /* Smaller border radius */
140
  background: rgba(255, 255, 255, 0.04);
141
  color: white;
142
  outline: none;
@@ -145,66 +119,56 @@ h1 {
145
  box-sizing: border-box;
146
  }
147
 
148
-
149
  .input-box textarea:focus {
150
- border-color: rgb(135, 185, 235); /* Lighter shade of original color */
151
- box-shadow: 0 0 5px rgba(93, 145, 195, 0.6); /* Soft glow effect */
152
- text-align: left; /* Align text to the left */
153
- vertical-align: middle; /* This helps maintain proper alignment */
154
- line-height: normal; /* Ensures proper vertical centering */
155
- font-size: 1rem; /* Adjust font size if needed */
156
  color: white;
157
  font-weight: bold;
158
  }
159
 
160
-
161
  .input-box textarea::placeholder {
162
- text-align: left; /* Align text to the left */
163
- vertical-align: middle; /* This helps maintain proper alignment */
164
- line-height: normal; /* Ensures proper vertical centering */
165
- font-size: 1rem; /* Adjust font size if needed */
166
- color: rgba(255, 255, 255, 0.5); /* Subtle color for placeholder */
167
  }
168
 
169
-
170
-
171
-
172
-
173
  .input-box button {
174
- width: 55px; /* Fixed button size */
175
- height: 55px; /* Fixed button size */
176
  display: flex;
177
  align-items: center;
178
  justify-content: center;
179
- /*background: #a855f7;*/
180
  background: #5d91c3;
181
  border: none;
182
  border-radius: 8px;
183
  cursor: pointer;
184
  }
185
 
186
-
187
  .input-box button:hover {
188
  transform: scale(1.05);
189
- box-shadow: 0px 4px 10px rgba(14, 165, 233, 0.4); /* Subtle hover effect */
190
  }
191
 
192
  .button-icon {
193
- width: 34px; /* Default size for all icons */
194
  height: 34px;
195
  object-fit: contain;
196
  }
197
 
198
-
199
-
200
  .resume-icon {
201
- width: 40px; /* Slightly larger for balance */
202
  height: 40px;
203
  object-fit: contain;
204
  }
205
 
206
  .pause-icon {
207
- width: 31px; /* Custom size for pause icon */
208
  height: 31px;
209
  object-fit: contain;
210
  }
@@ -229,21 +193,21 @@ h1 {
229
  }
230
 
231
  .suggestion-box li {
232
- padding: 8px 12px; /* Spacing inside each suggestion */
233
  cursor: pointer;
234
  font-size: 1rem;
235
  transition: background-color 0.3s ease;
236
- color: rgb(67 65 65); /* Text color */
237
  }
238
 
239
  .suggestion-box li:hover {
240
- background-color: rgba(0, 0, 0, 0.1); /* Subtle hover effect */
241
- border-radius: 4px; /* Rounded corners for hover */
242
  }
243
 
244
  .input-container {
245
  display: flex;
246
- flex-direction: column-reverse; /* Reverse order to place suggestions above input */
247
  position: relative;
248
  width: 100%;
249
  }
@@ -252,8 +216,7 @@ h1 {
252
  display: flex;
253
  gap: 10px;
254
  padding: 8px;
255
- background: #03182d; /* Background color of the input box */
256
- /*background: #c19929;*/ /* Background color of the input box */
257
  box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
258
  flex-shrink: 0;
259
  width: 100%;
@@ -261,10 +224,6 @@ h1 {
261
  min-height: 40px;
262
  }
263
 
264
-
265
-
266
-
267
- /* Glass Transparent Listening Indicator (Fully Centered) */
268
  .listening-box {
269
  position: fixed;
270
  top: 50%;
@@ -274,30 +233,28 @@ h1 {
274
  padding: 45px;
275
  border-radius: 20px;
276
  width: 30vw;
277
- height: auto; /* Adjust height dynamically */
278
- min-height: 40vh; /* Ensure height remains balanced */
279
  box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.15);
280
  backdrop-filter: blur(20px);
281
  -webkit-backdrop-filter: blur(20px);
282
  animation: fadeInScale 0.3s ease-in-out;
283
  display: flex;
284
  flex-direction: column;
285
- align-items: center; /* Ensure all elements stay centered */
286
  justify-content: center;
287
  border: 1px solid rgba(255, 255, 255, 0.2);
288
- text-align: center; /* Center text alignment */
289
  }
290
 
291
- /* Microphone Image (Centered Properly) */
292
  .microphone-image {
293
- width: 100px; /* Slightly bigger */
294
  height: 100px;
295
  display: block;
296
- margin: 0 auto 20px; /* Ensures it's centered with space below */
297
  animation: pulse 1.5s infinite alternate ease-in-out;
298
  }
299
 
300
- /* Pulse Effect */
301
  @keyframes pulse {
302
  from {
303
  transform: scale(1);
@@ -310,37 +267,34 @@ h1 {
310
  }
311
  }
312
 
313
- /* Listening Text (Fully Centered) */
314
  .listening-box p {
315
- font-size: 15px; /* Increased size */
316
  color: black;
317
  font-weight: bold;
318
- text-align: center; /* Ensure it's aligned centrally */
319
- margin: 10px 0; /* Adjusted margin */
320
  text-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);
321
  margin-top: 3vw;
322
  }
323
 
324
- /* Mute & Close Buttons (Evenly Spaced & Centered) */
325
  .listening-actions {
326
  display: flex;
327
- justify-content: center; /* Ensure icons are centered */
328
- gap: 70px; /* More space between icons */
329
  margin-top: 65px;
330
  width: 100%;
331
  }
332
 
333
- /* Button Icons (Increased Size) */
334
  .mute-btn,
335
  .close-btn {
336
  background: rgba(255, 255, 255, 0.3);
337
  border: none;
338
- padding: 15px; /* Increased padding for larger buttons */
339
  border-radius: 50%;
340
  cursor: pointer;
341
  transition: transform 0.2s ease-in-out, background 0.3s;
342
- width: 75px; /* Increased button width */
343
- height: 75px; /* Increased button height */
344
  display: flex;
345
  align-items: center;
346
  justify-content: center;
@@ -348,25 +302,22 @@ h1 {
348
  -webkit-backdrop-filter: blur(12px);
349
  }
350
 
351
- /* Mute and Close Icon Styles (Proper Scaling) */
352
  .mute-btn img,
353
  .close-btn img {
354
- width: 50px; /* Bigger icons */
355
  height: 50px;
356
  }
357
 
358
- /* Hover Effects */
359
  .mute-btn:hover {
360
- background: rgba(255, 0, 0, 0.5); /* Red for mute */
361
  transform: scale(1.15);
362
  }
363
 
364
  .close-btn:hover {
365
- background: rgba(0, 0, 0, 0.5); /* Dark for close */
366
  transform: scale(1.15);
367
  }
368
 
369
- /* Fade-in with Scale Animation */
370
  @keyframes fadeInScale {
371
  from {
372
  opacity: 0;
@@ -379,45 +330,39 @@ h1 {
379
  }
380
  }
381
 
382
-
383
-
384
  .error-text {
385
  color: black;
386
- background: #ffccccad; /* Light red */
387
  font-size: 2vw;
388
  font-weight: bold;
389
  text-align: center;
390
  padding: 8px 12px;
391
  border-radius: 5px;
392
  cursor: pointer;
393
- width: auto; /* Auto width based on text */
394
- margin-top: 10px; /* Space below buttons */
395
- display: inline-block; /* Single line */
396
- white-space: nowrap; /* Prevent text wrapping */
397
  transition: background 0.3s;
398
  }
399
 
400
  .error-text:hover {
401
- background: #ffaaaa; /* Slightly darker red on hover */
402
  }
403
 
404
-
405
-
406
- /* Popup Overlay */
407
  .popup-overlay {
408
  position: fixed;
409
  top: 0;
410
  left: 0;
411
  width: 100%;
412
  height: 100%;
413
- background: rgba(0, 0, 0, 0.5); /* Dark transparent background */
414
  display: flex;
415
  justify-content: center;
416
  align-items: center;
417
  z-index: 1000;
418
  }
419
 
420
- /* Popup Box */
421
  .popup-box {
422
  background: white;
423
  padding: 20px;
@@ -427,19 +372,16 @@ h1 {
427
  box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
428
  }
429
 
430
- /* Popup Title */
431
  .popup-box h3 {
432
  font-size: 18px;
433
  margin-bottom: 10px;
434
  }
435
 
436
- /* Popup Text */
437
  .popup-box p {
438
  font-size: 14px;
439
  color: #555;
440
  }
441
 
442
- /* Popup Button */
443
  .popup-button {
444
  background: #007bff;
445
  color: white;
@@ -471,7 +413,6 @@ h1 {
471
  animation: fadeIn 0.3s ease-in-out;
472
  }
473
 
474
- /* Typing dots animation */
475
  .typing-indicator span {
476
  width: 10px;
477
  height: 10px;
@@ -481,7 +422,6 @@ h1 {
481
  animation: typingDots 1.5s infinite ease-in-out;
482
  }
483
 
484
- /* Add animation delays for each dot */
485
  .typing-indicator span:nth-child(1) {
486
  animation-delay: 0s;
487
  }
@@ -494,7 +434,6 @@ h1 {
494
  animation-delay: 0.4s;
495
  }
496
 
497
- /* Keyframes for the typing dots */
498
  @keyframes typingDots {
499
  0%, 100% {
500
  transform: scale(0.8);
@@ -507,7 +446,6 @@ h1 {
507
  }
508
  }
509
 
510
- /* Smooth fade-in effect */
511
  @keyframes fadeIn {
512
  from {
513
  opacity: 0;
@@ -520,51 +458,43 @@ h1 {
520
  }
521
  }
522
 
523
-
524
-
525
-
526
-
527
- /* Container for hardcoded questions - Aligned left above the text field */
528
  .hardcoded-questions-container {
529
  display: flex;
530
- justify-content: flex-start; /* Align to the left */
531
- gap: 15px; /* Space between tabs */
532
  padding: 10px;
533
- background: rgba(255, 255, 255, 0.1); /* Subtle background */
534
  border-radius: 8px;
535
- margin-bottom: 5px; /* Space between questions and input box */
536
- flex-wrap: wrap; /* Allow wrapping on smaller screens */
537
  position: absolute;
538
- bottom: 60px; /* Adjust based on text field height */
539
- left: 10px; /* Align to the left side */
540
- width: auto; /* Adjust width dynamically */
541
  }
542
 
543
- /* Individual question tab */
544
  .hardcoded-question {
545
  padding: 10px 15px;
546
- background: rgba(200, 200, 200, 0.3); /* Light grey faded color */
547
  border-radius: 5px;
548
  font-size: 14px;
549
  font-weight: bold;
550
  cursor: pointer;
551
  transition: background 0.3s ease, transform 0.2s ease;
552
- white-space: nowrap; /* Prevent wrapping */
553
  border: 1px solid rgba(0, 0, 0, 0.2);
554
  }
555
 
556
- /* Hover effect */
557
  .hardcoded-question:hover {
558
- background: rgba(180, 180, 180, 0.5); /* Slightly darker grey on hover */
559
- transform: scale(1.05); /* Subtle pop effect */
560
  }
561
 
562
- /* Ensure responsiveness */
563
  @media (max-width: 600px) {
564
  .hardcoded-questions-container {
565
  width: 90%;
566
- bottom: 80px; /* Adjust spacing for smaller screens */
567
- left: 10px; /* Keep it left aligned */
568
  }
569
 
570
  .hardcoded-question {
@@ -573,18 +503,13 @@ h1 {
573
  }
574
  }
575
 
576
-
577
-
578
-
579
- /* Message wrapper for each chat bubble */
580
  .message-wrapper {
581
  display: flex;
582
  align-items: flex-start;
583
  gap: 1vw;
584
- margin-bottom: 1vw; /* Space between each message */
585
  }
586
 
587
- /* Separate AI and user styles */
588
  .message-wrapper.user {
589
  flex-direction: row-reverse;
590
  text-align: right;
@@ -595,7 +520,6 @@ h1 {
595
  text-align: left;
596
  }
597
 
598
- /* Profile Picture Styling */
599
  .profile-pic {
600
  width: 4vw;
601
  height: 4vw;
@@ -610,8 +534,6 @@ h1 {
610
  object-fit: cover;
611
  }
612
 
613
-
614
- /* Separate paragraph blocks */
615
  .ai .message {
616
  align-self: flex-start;
617
  background: #2b6296;
@@ -624,7 +546,6 @@ h1 {
624
  color: white;
625
  }
626
 
627
- /* Timestamp Styling */
628
  .message-timestamp {
629
  font-size: 0.8vw;
630
  color: rgba(255, 255, 255, 0.8);
@@ -633,20 +554,17 @@ h1 {
633
  text-align: right;
634
  }
635
 
636
-
637
-
638
  .paragraph-block {
639
  background: #2b6296;
640
  color: white;
641
  padding: 1vw;
642
  border-radius: 1vw;
643
- margin-bottom: 0.5vw; /* ✅ Adjusted for better spacing */
644
  box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
645
  line-height: 1.5;
646
  max-width: 48vw;
647
  }
648
 
649
- /* Normal messages remain in a single block */
650
  .message {
651
  max-width: 50vw;
652
  padding: 1vw 2vw;
@@ -658,41 +576,37 @@ h1 {
658
  color: white;
659
  }
660
 
661
-
662
-
663
  .structured-response {
664
  background: #2b6296;
665
  color: white;
666
  padding: 1vw;
667
  border-radius: 1vw;
668
- margin-bottom: 0.3vw; /* ✅ Reduced spacing */
669
  box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
670
- line-height: 1.4; /* ✅ Adjusted line height */
671
  max-width: 48vw;
672
  word-break: break-word;
673
  }
674
 
675
- /* ✅ Reduce space between numbered and bulleted list items */
676
  .structured-response b {
677
  display: inline-block;
678
- margin-top: 0.3vw; /* ✅ Reduced margin for numbers */
679
  }
680
 
681
  .structured-response br {
682
  display: block;
683
  content: "";
684
- margin: 0.2vw 0; /* ✅ Reduce line spacing */
685
  }
686
 
687
- /* ✅ Ensure numbered and bulleted lists have proper spacing */
688
  .structured-response ul,
689
  .structured-response ol {
690
- padding-left: 1.2vw; /* ✅ Slightly reduced indentation */
691
- margin-bottom: 0; /* ✅ Remove extra bottom margin */
692
  }
693
 
694
  .structured-response ul li,
695
  .structured-response ol li {
696
- margin-bottom: 0.2vw; /* ✅ Reduced space between list items */
697
  line-height: 1.4;
698
  }
 
 
1
  .header-container {
2
  display: flex;
3
  justify-content: space-between;
4
  align-items: center;
5
  padding: 0vw 1vw;
6
+ background-color: #03182d;
 
7
  box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.2);
8
  width: 100%;
9
  position: sticky;
 
15
  max-width: 5vw;
16
  height: auto;
17
  background: #e5e7eb;
18
+ border-radius: 50%;
19
  margin: 0.5vw;
20
  }
21
 
22
  .home-btn img {
23
+ width: 5vw;
24
  }
25
 
26
+ .home-btn img:hover {
27
+ transform: scale(1.1);
28
+ }
29
 
30
  h1 {
31
  font-size: 3vw;
 
45
  font-display: swap;
46
  }
47
 
 
 
 
48
  .chat-container {
49
  display: flex;
50
  flex-direction: column;
51
+ height: 100vh;
52
+ width: 100%;
 
53
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
54
  }
55
 
56
  .chat-bg {
 
64
  opacity: 0.2;
65
  }
66
 
 
 
 
 
 
 
 
67
  .chat-header {
68
  text-align: center;
69
+ background: linear-gradient(to right, #4ca1af, #6ac5cb);
70
  color: white;
71
  padding: 0px 0;
72
  font-size: 2.2rem;
73
  font-weight: bold;
74
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
75
+ flex-shrink: 0;
76
  }
77
 
 
78
  .chat-box {
79
  display: flex;
80
  flex-direction: column;
81
  overflow-y: auto;
82
+ height: calc(100vh - 180px);
83
  padding: 2vw;
84
  background-color: rgba(35, 34, 32, 0.43);
85
+ scroll-behavior: smooth;
86
  flex-grow: 1;
87
  padding-bottom: 90px;
88
  }
89
 
 
 
 
 
 
90
  .input-box {
91
  display: flex;
92
+ gap: 10px;
93
+ padding: 8px;
94
+ background: #03182d;
95
+ box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
 
96
  flex-shrink: 0;
97
  width: 100%;
98
+ height: auto;
99
+ min-height: 40px;
100
  font-weight: bold;
101
  }
102
 
 
 
 
 
 
103
  .input-box textarea {
104
  flex: 1;
105
+ height: auto;
106
+ max-height: 80px;
107
+ overflow-y: auto;
108
+ resize: none;
109
+ padding: 8px;
110
+ font-size: 1rem;
111
+ line-height: 1.2;
112
  border: 1px solid rgb(93, 145, 195);
113
+ border-radius: 8px;
114
  background: rgba(255, 255, 255, 0.04);
115
  color: white;
116
  outline: none;
 
119
  box-sizing: border-box;
120
  }
121
 
 
122
  .input-box textarea:focus {
123
+ border-color: rgb(135, 185, 235);
124
+ box-shadow: 0 0 5px rgba(93, 145, 195, 0.6);
125
+ text-align: left;
126
+ vertical-align: middle;
127
+ line-height: normal;
128
+ font-size: 1rem;
129
  color: white;
130
  font-weight: bold;
131
  }
132
 
 
133
  .input-box textarea::placeholder {
134
+ text-align: left;
135
+ vertical-align: middle;
136
+ line-height: normal;
137
+ font-size: 1rem;
138
+ color: rgba(255, 255, 255, 0.5);
139
  }
140
 
 
 
 
 
141
  .input-box button {
142
+ width: 55px;
143
+ height: 55px;
144
  display: flex;
145
  align-items: center;
146
  justify-content: center;
 
147
  background: #5d91c3;
148
  border: none;
149
  border-radius: 8px;
150
  cursor: pointer;
151
  }
152
 
 
153
  .input-box button:hover {
154
  transform: scale(1.05);
155
+ box-shadow: 0px 4px 10px rgba(14, 165, 233, 0.4);
156
  }
157
 
158
  .button-icon {
159
+ width: 34px;
160
  height: 34px;
161
  object-fit: contain;
162
  }
163
 
 
 
164
  .resume-icon {
165
+ width: 40px;
166
  height: 40px;
167
  object-fit: contain;
168
  }
169
 
170
  .pause-icon {
171
+ width: 31px;
172
  height: 31px;
173
  object-fit: contain;
174
  }
 
193
  }
194
 
195
  .suggestion-box li {
196
+ padding: 8px 12px;
197
  cursor: pointer;
198
  font-size: 1rem;
199
  transition: background-color 0.3s ease;
200
+ color: rgb(67 65 65);
201
  }
202
 
203
  .suggestion-box li:hover {
204
+ background-color: rgba(0, 0, 0, 0.1);
205
+ border-radius: 4px;
206
  }
207
 
208
  .input-container {
209
  display: flex;
210
+ flex-direction: column-reverse;
211
  position: relative;
212
  width: 100%;
213
  }
 
216
  display: flex;
217
  gap: 10px;
218
  padding: 8px;
219
+ background: #03182d;
 
220
  box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
221
  flex-shrink: 0;
222
  width: 100%;
 
224
  min-height: 40px;
225
  }
226
 
 
 
 
 
227
  .listening-box {
228
  position: fixed;
229
  top: 50%;
 
233
  padding: 45px;
234
  border-radius: 20px;
235
  width: 30vw;
236
+ height: auto;
237
+ min-height: 40vh;
238
  box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.15);
239
  backdrop-filter: blur(20px);
240
  -webkit-backdrop-filter: blur(20px);
241
  animation: fadeInScale 0.3s ease-in-out;
242
  display: flex;
243
  flex-direction: column;
244
+ align-items: center;
245
  justify-content: center;
246
  border: 1px solid rgba(255, 255, 255, 0.2);
247
+ text-align: center;
248
  }
249
 
 
250
  .microphone-image {
251
+ width: 100px;
252
  height: 100px;
253
  display: block;
254
+ margin: 0 auto 20px;
255
  animation: pulse 1.5s infinite alternate ease-in-out;
256
  }
257
 
 
258
  @keyframes pulse {
259
  from {
260
  transform: scale(1);
 
267
  }
268
  }
269
 
 
270
  .listening-box p {
271
+ font-size: 15px;
272
  color: black;
273
  font-weight: bold;
274
+ text-align: center;
275
+ margin: 10px 0;
276
  text-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);
277
  margin-top: 3vw;
278
  }
279
 
 
280
  .listening-actions {
281
  display: flex;
282
+ justify-content: center;
283
+ gap: 70px;
284
  margin-top: 65px;
285
  width: 100%;
286
  }
287
 
 
288
  .mute-btn,
289
  .close-btn {
290
  background: rgba(255, 255, 255, 0.3);
291
  border: none;
292
+ padding: 15px;
293
  border-radius: 50%;
294
  cursor: pointer;
295
  transition: transform 0.2s ease-in-out, background 0.3s;
296
+ width: 75px;
297
+ height: 75px;
298
  display: flex;
299
  align-items: center;
300
  justify-content: center;
 
302
  -webkit-backdrop-filter: blur(12px);
303
  }
304
 
 
305
  .mute-btn img,
306
  .close-btn img {
307
+ width: 50px;
308
  height: 50px;
309
  }
310
 
 
311
  .mute-btn:hover {
312
+ background: rgba(255, 0, 0, 0.5);
313
  transform: scale(1.15);
314
  }
315
 
316
  .close-btn:hover {
317
+ background: rgba(0, 0, 0, 0.5);
318
  transform: scale(1.15);
319
  }
320
 
 
321
  @keyframes fadeInScale {
322
  from {
323
  opacity: 0;
 
330
  }
331
  }
332
 
 
 
333
  .error-text {
334
  color: black;
335
+ background: #ffccccad;
336
  font-size: 2vw;
337
  font-weight: bold;
338
  text-align: center;
339
  padding: 8px 12px;
340
  border-radius: 5px;
341
  cursor: pointer;
342
+ width: auto;
343
+ margin-top: 10px;
344
+ display: inline-block;
345
+ white-space: nowrap;
346
  transition: background 0.3s;
347
  }
348
 
349
  .error-text:hover {
350
+ background: #ffaaaa;
351
  }
352
 
 
 
 
353
  .popup-overlay {
354
  position: fixed;
355
  top: 0;
356
  left: 0;
357
  width: 100%;
358
  height: 100%;
359
+ background: rgba(0, 0, 0, 0.5);
360
  display: flex;
361
  justify-content: center;
362
  align-items: center;
363
  z-index: 1000;
364
  }
365
 
 
366
  .popup-box {
367
  background: white;
368
  padding: 20px;
 
372
  box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
373
  }
374
 
 
375
  .popup-box h3 {
376
  font-size: 18px;
377
  margin-bottom: 10px;
378
  }
379
 
 
380
  .popup-box p {
381
  font-size: 14px;
382
  color: #555;
383
  }
384
 
 
385
  .popup-button {
386
  background: #007bff;
387
  color: white;
 
413
  animation: fadeIn 0.3s ease-in-out;
414
  }
415
 
 
416
  .typing-indicator span {
417
  width: 10px;
418
  height: 10px;
 
422
  animation: typingDots 1.5s infinite ease-in-out;
423
  }
424
 
 
425
  .typing-indicator span:nth-child(1) {
426
  animation-delay: 0s;
427
  }
 
434
  animation-delay: 0.4s;
435
  }
436
 
 
437
  @keyframes typingDots {
438
  0%, 100% {
439
  transform: scale(0.8);
 
446
  }
447
  }
448
 
 
449
  @keyframes fadeIn {
450
  from {
451
  opacity: 0;
 
458
  }
459
  }
460
 
 
 
 
 
 
461
  .hardcoded-questions-container {
462
  display: flex;
463
+ justify-content: flex-start;
464
+ gap: 15px;
465
  padding: 10px;
466
+ background: rgba(255, 255, 255, 0.1);
467
  border-radius: 8px;
468
+ margin-bottom: 5px;
469
+ flex-wrap: wrap;
470
  position: absolute;
471
+ bottom: 60px;
472
+ left: 10px;
473
+ width: auto;
474
  }
475
 
 
476
  .hardcoded-question {
477
  padding: 10px 15px;
478
+ background: rgba(200, 200, 200, 0.3);
479
  border-radius: 5px;
480
  font-size: 14px;
481
  font-weight: bold;
482
  cursor: pointer;
483
  transition: background 0.3s ease, transform 0.2s ease;
484
+ white-space: nowrap;
485
  border: 1px solid rgba(0, 0, 0, 0.2);
486
  }
487
 
 
488
  .hardcoded-question:hover {
489
+ background: rgba(180, 180, 180, 0.5);
490
+ transform: scale(1.05);
491
  }
492
 
 
493
  @media (max-width: 600px) {
494
  .hardcoded-questions-container {
495
  width: 90%;
496
+ bottom: 80px;
497
+ left: 10px;
498
  }
499
 
500
  .hardcoded-question {
 
503
  }
504
  }
505
 
 
 
 
 
506
  .message-wrapper {
507
  display: flex;
508
  align-items: flex-start;
509
  gap: 1vw;
510
+ margin-bottom: 1vw;
511
  }
512
 
 
513
  .message-wrapper.user {
514
  flex-direction: row-reverse;
515
  text-align: right;
 
520
  text-align: left;
521
  }
522
 
 
523
  .profile-pic {
524
  width: 4vw;
525
  height: 4vw;
 
534
  object-fit: cover;
535
  }
536
 
 
 
537
  .ai .message {
538
  align-self: flex-start;
539
  background: #2b6296;
 
546
  color: white;
547
  }
548
 
 
549
  .message-timestamp {
550
  font-size: 0.8vw;
551
  color: rgba(255, 255, 255, 0.8);
 
554
  text-align: right;
555
  }
556
 
 
 
557
  .paragraph-block {
558
  background: #2b6296;
559
  color: white;
560
  padding: 1vw;
561
  border-radius: 1vw;
562
+ margin-bottom: 0.5vw;
563
  box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
564
  line-height: 1.5;
565
  max-width: 48vw;
566
  }
567
 
 
568
  .message {
569
  max-width: 50vw;
570
  padding: 1vw 2vw;
 
576
  color: white;
577
  }
578
 
 
 
579
  .structured-response {
580
  background: #2b6296;
581
  color: white;
582
  padding: 1vw;
583
  border-radius: 1vw;
584
+ margin-bottom: 0.3vw;
585
  box-shadow: 0 0.3vw 0.8vw rgba(0, 0, 0, 0.1);
586
+ line-height: 1.4;
587
  max-width: 48vw;
588
  word-break: break-word;
589
  }
590
 
 
591
  .structured-response b {
592
  display: inline-block;
593
+ margin-top: 0.3vw;
594
  }
595
 
596
  .structured-response br {
597
  display: block;
598
  content: "";
599
+ margin: 0.2vw 0;
600
  }
601
 
 
602
  .structured-response ul,
603
  .structured-response ol {
604
+ padding-left: 1.2vw;
605
+ margin-bottom: 0;
606
  }
607
 
608
  .structured-response ul li,
609
  .structured-response ol li {
610
+ margin-bottom: 0.2vw;
611
  line-height: 1.4;
612
  }
src/app/chat/chat.component.html CHANGED
@@ -1,8 +1,5 @@
1
-
2
-
3
-
4
  <div class="chat-container">
5
- <div class="header-container">
6
  <div class="logo">
7
  <a (click)="goToHome()" routerLink="/home" class="brand-link">
8
  <img src="assets/images/pykara-logo.png" alt="Pykara Logo" />
@@ -17,19 +14,11 @@
17
  <img src="assets/images/home.png" alt="Home" class="home-icon" />
18
  </a>
19
  </div>
20
- </div>
21
-
22
-
23
-
24
-
25
 
26
-
27
-
28
- <div class="chat-box" #chatBox>
29
  <img src="assets/images/chat/chatbg.png" alt="Chat Background" class="chat-bg" />
30
-
31
- <div *ngFor="let message of messages">
32
- <!-- User Messages -->
33
  <div *ngIf="message.from === 'user'" class="message-wrapper user">
34
  <div class="profile-pic">
35
  <img src="assets/images/chat/rabbit.png" alt="User Profile Picture" />
@@ -39,8 +28,6 @@
39
  <div class="message-timestamp">{{ message.timestamp }}</div>
40
  </div>
41
  </div>
42
-
43
- <!-- AI Messages -->
44
  <div *ngIf="message.from === 'ai'" class="message-wrapper ai">
45
  <div class="profile-pic">
46
  <img src="assets/images/chat/lion.png" alt="AI Profile Picture" />
@@ -50,24 +37,16 @@
50
  <div class="message-timestamp">{{ message.timestamp }}</div>
51
  </div>
52
  </div>
53
- </div>
54
-
55
- <!-- AI typing indicator -->
56
- <div *ngIf="isTyping" class="typing-indicator">
57
  AI is typing
58
  <span></span>
59
  <span></span>
60
  <span></span>
61
  </div>
62
- </div>
63
-
64
-
65
-
66
-
67
-
68
-
69
 
70
- <div class="input-container">
71
  <div class="input-box">
72
  <textarea [(ngModel)]="userInput"
73
  (focus)="showHardcodedQuestions()"
@@ -75,17 +54,16 @@
75
  (input)="adjustTextareaHeight($event); getSuggestions()"
76
  (keydown)="handleEnterPress($event)"
77
  placeholder="Type your message here..."
78
- [disabled]="isSpeaking">
79
- </textarea>
80
-
81
- <button (click)="isSpeaking ? (isAudioPaused ? resumeAudio() : pauseAudio()) : handleButtonClick()">
82
- <img [src]="isSpeaking ? (isAudioPaused ? 'assets/images/chat/resume-icon.png' : 'assets/images/chat/pause-icon.png') : getButtonIcon()"
83
- alt="Button Icon"
 
84
  class="button-icon" />
85
  </button>
86
  </div>
87
-
88
- <!-- Hardcoded questions (Shown only when focused) -->
89
  <div class="hardcoded-questions-container" *ngIf="showQuestions">
90
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('What is grammar?')">What is grammar?</div>
91
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('What are the rules to be followed in grammar?')">What are the rules to be followed in grammar?</div>
@@ -93,14 +71,12 @@
93
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('Why do we need to follow grammar rules while writing and speaking?')">Why do we need to follow grammar rules while writing and speaking?</div>
94
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('How do you identify a subject and a predicate in a sentence?')">How do you identify a subject and a predicate in a sentence?</div>
95
  </div>
96
- </div>
97
 
98
- <!-- Listening Box -->
99
- <div class="listening-box" *ngIf="isListening">
100
  <div class="listening-content">
101
  <img src="assets/images/chat/microphone-icon.png" alt="Microphone" class="microphone-image" />
102
  <p *ngIf="!errorMessage">Listening...</p>
103
-
104
  <div class="listening-actions">
105
  <button class="mute-btn" (click)="muteMicrophone()">
106
  <img src="assets/images/chat/mic.png" alt="Mute" />
@@ -109,14 +85,10 @@
109
  <img src="assets/images/chat/cross.png" alt="Close" />
110
  </button>
111
  </div>
112
-
113
- <p *ngIf="errorMessage" class="error-text" (click)="openMicrophoneSettings()">
114
- {{ errorMessage }}
115
- </p>
116
  </div>
117
  </div>
118
 
119
- <!-- Microphone Access Popup -->
120
  <div class="popup-overlay" *ngIf="showMicPopup">
121
  <div class="popup-box">
122
  <h3>Microphone access required</h3>
@@ -125,4 +97,3 @@
125
  </div>
126
  </div>
127
  </div>
128
-
 
 
 
 
1
  <div class="chat-container">
2
+ <header class="header-container">
3
  <div class="logo">
4
  <a (click)="goToHome()" routerLink="/home" class="brand-link">
5
  <img src="assets/images/pykara-logo.png" alt="Pykara Logo" />
 
14
  <img src="assets/images/home.png" alt="Home" class="home-icon" />
15
  </a>
16
  </div>
17
+ </header>
 
 
 
 
18
 
19
+ <main class="chat-box" #chatBox>
 
 
20
  <img src="assets/images/chat/chatbg.png" alt="Chat Background" class="chat-bg" />
21
+ <ng-container *ngFor="let message of messages">
 
 
22
  <div *ngIf="message.from === 'user'" class="message-wrapper user">
23
  <div class="profile-pic">
24
  <img src="assets/images/chat/rabbit.png" alt="User Profile Picture" />
 
28
  <div class="message-timestamp">{{ message.timestamp }}</div>
29
  </div>
30
  </div>
 
 
31
  <div *ngIf="message.from === 'ai'" class="message-wrapper ai">
32
  <div class="profile-pic">
33
  <img src="assets/images/chat/lion.png" alt="AI Profile Picture" />
 
37
  <div class="message-timestamp">{{ message.timestamp }}</div>
38
  </div>
39
  </div>
40
+ </ng-container>
41
+ <div *ngIf="isTyping" class="typing-indicator" role="status" aria-live="polite">
 
 
42
  AI is typing
43
  <span></span>
44
  <span></span>
45
  <span></span>
46
  </div>
47
+ </main>
 
 
 
 
 
 
48
 
49
+ <section class="input-container">
50
  <div class="input-box">
51
  <textarea [(ngModel)]="userInput"
52
  (focus)="showHardcodedQuestions()"
 
54
  (input)="adjustTextareaHeight($event); getSuggestions()"
55
  (keydown)="handleEnterPress($event)"
56
  placeholder="Type your message here..."
57
+ [disabled]="isSpeaking"
58
+ aria-label="Message input"></textarea>
59
+ <button (click)="isSpeaking ? stopSpeaking() : handleButtonClick()"
60
+ [disabled]="isSubmitting"
61
+ aria-label="Send or voice">
62
+ <img [src]="isSpeaking ? 'assets/images/chat/stop.png' : getButtonIcon()"
63
+ alt="Action"
64
  class="button-icon" />
65
  </button>
66
  </div>
 
 
67
  <div class="hardcoded-questions-container" *ngIf="showQuestions">
68
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('What is grammar?')">What is grammar?</div>
69
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('What are the rules to be followed in grammar?')">What are the rules to be followed in grammar?</div>
 
71
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('Why do we need to follow grammar rules while writing and speaking?')">Why do we need to follow grammar rules while writing and speaking?</div>
72
  <div class="hardcoded-question" (click)="selectHardcodedQuestion('How do you identify a subject and a predicate in a sentence?')">How do you identify a subject and a predicate in a sentence?</div>
73
  </div>
74
+ </section>
75
 
76
+ <div class="listening-box" *ngIf="isListening" role="dialog" aria-modal="true">
 
77
  <div class="listening-content">
78
  <img src="assets/images/chat/microphone-icon.png" alt="Microphone" class="microphone-image" />
79
  <p *ngIf="!errorMessage">Listening...</p>
 
80
  <div class="listening-actions">
81
  <button class="mute-btn" (click)="muteMicrophone()">
82
  <img src="assets/images/chat/mic.png" alt="Mute" />
 
85
  <img src="assets/images/chat/cross.png" alt="Close" />
86
  </button>
87
  </div>
88
+ <p *ngIf="errorMessage" class="error-text" (click)="openMicrophoneSettings()">{{ errorMessage }}</p>
 
 
 
89
  </div>
90
  </div>
91
 
 
92
  <div class="popup-overlay" *ngIf="showMicPopup">
93
  <div class="popup-box">
94
  <h3>Microphone access required</h3>
 
97
  </div>
98
  </div>
99
  </div>
 
src/app/chat/chat.component.ts CHANGED
@@ -1,14 +1,12 @@
1
  import { Component, Inject, OnDestroy, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
2
- import { ApiService } from './api.service'; // Import ApiService
3
- import { FormsModule } from '@angular/forms'; // Import FormsModule for two-way binding
4
  import { CommonModule } from '@angular/common';
5
  import { Router, RouterModule } from '@angular/router';
6
  import { isPlatformBrowser } from '@angular/common';
7
  import { ViewChild, ElementRef } from '@angular/core';
8
  import { Renderer2 } from '@angular/core';
9
-
10
-
11
-
12
 
13
  @Component({
14
  selector: 'app-chat',
@@ -18,48 +16,51 @@ import { Renderer2 } from '@angular/core';
18
  styleUrl: './chat.component.css'
19
  })
20
  export class ChatComponent implements OnDestroy {
21
- showQuestions: boolean = false; // Flag to show hardcoded questions
22
  userInput: string = '';
23
  messages: { from: string, text: string, timestamp: string; isPlaying?: boolean }[] = [];
24
- isTyping: boolean = false; // Typing indicator
25
  @ViewChild('chatBox') chatBox!: ElementRef;
26
 
27
- isLoadingSpeech: boolean = false; // ✅ Add this line to fix the error
28
  selectedVoice: SpeechSynthesisVoice | null = null;
29
  errorMessage: string = "";
30
- recognition: any; // SpeechRecognition object
31
  speechSynthesisInstance: SpeechSynthesisUtterance | null = null;
32
  isListening: boolean = false;
33
- isProcessingSpeech: boolean = false; // Flag to prevent duplicate processing
34
- // isVoiceInput: boolean = false; // Indicates if the input is from voice
35
- isSpeaking: boolean = false; // Track if text-to-speech is ongoing
36
- isAudioPaused: boolean = false; // Indicates if the audio is paused
37
  isInputValid: boolean = false;
38
- suggestions: string[] = []; // Holds suggested grammar questions
39
  showMicPopup: boolean = false;
40
-
 
 
41
 
42
  ngAfterViewChecked() {
43
- // Adding a delay for smooth scrolling
44
  setTimeout(() => {
45
  this.scrollToBottom();
46
- }, 100); // Adjust the timeout if necessary
47
  }
48
 
49
- // Function to scroll to the bottom of the chat
50
  private scrollToBottom(): void {
51
  try {
52
  this.chatBox.nativeElement.scrollTop = this.chatBox.nativeElement.scrollHeight;
53
  } catch (err) { }
54
  }
55
 
56
- constructor(private apiService: ApiService, private cdr: ChangeDetectorRef, @Inject(PLATFORM_ID,) private platformId: object, private router: Router, private renderer: Renderer2) {
57
-
 
 
 
 
 
58
  window.speechSynthesis.onvoiceschanged = () => {
59
  console.log("Available Voices:", window.speechSynthesis.getVoices());
60
  };
61
 
62
- // Initialize SpeechRecognition if supported
63
  if (isPlatformBrowser(this.platformId)) {
64
  const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
65
  if (SpeechRecognition) {
@@ -72,43 +73,35 @@ export class ChatComponent implements OnDestroy {
72
  if (event.results && event.results[0]) {
73
  const transcript = event.results[0][0].transcript.trim();
74
  console.log('Recognized speech:', transcript);
75
-
76
- this.userInput = transcript; // Set the recognized speech as input
77
- this.sendMessage(); // Automatically send the message after recognition
78
-
79
- // Stop recognition to prevent multiple triggers
80
  this.recognition.stop();
81
- this.isListening = false; // Hide the overlay
82
  }
83
  };
84
 
85
  this.recognition.onerror = (event: any) => {
86
  console.error('Speech Recognition Error:', event.error);
87
- this.isProcessingSpeech = false; // Reset the flag on error
88
  };
89
  } else {
90
  console.warn('Speech Recognition is not supported in this browser.');
91
  }
92
- // Add beforeunload listener to stop audio on page refresh or unload
93
  window.addEventListener('beforeunload', this.handleUnload);
94
  }
95
-
96
  }
97
 
98
- // Handle page unload to stop audio
99
  private handleUnload = (): void => {
100
  if (window.speechSynthesis) {
101
  window.speechSynthesis.cancel();
102
  }
103
  };
104
 
105
- // Lifecycle hook to clean up resources
106
  ngOnDestroy(): void {
107
  if (isPlatformBrowser(this.platformId)) {
108
  if (window.speechSynthesis) {
109
  window.speechSynthesis.cancel();
110
  }
111
- // Remove the unload event listener
112
  window.removeEventListener('beforeunload', this.handleUnload);
113
  }
114
  }
@@ -121,41 +114,30 @@ export class ChatComponent implements OnDestroy {
121
  this.showMicPopup = false;
122
  }
123
 
124
-
125
-
126
-
127
-
128
-
129
-
130
- // Show hardcoded questions when user focuses on input
131
  showHardcodedQuestions(): void {
132
  setTimeout(() => {
133
  this.showQuestions = true;
134
- }, 100); // Small delay to prevent blur from closing it immediately
135
  }
136
 
137
- // Hide hardcoded questions when user clicks outside
138
  hideHardcodedQuestions(): void {
139
  setTimeout(() => {
140
  this.showQuestions = false;
141
- }, 200); // Allow time for click event to register
142
  }
143
 
144
-
145
  selectHardcodedQuestion(question: string): void {
146
- this.userInput = question; // Set the selected question
147
- this.showQuestions = false; // Hide the hardcoded questions
148
  setTimeout(() => {
149
- this.sendMessage(); // Send the selected question
150
- this.userInput = ''; // Clear the text field after sending
151
- }, 100); // Small delay to ensure UI updates before clearing
152
  }
153
 
154
-
155
-
156
  getSuggestions(): void {
157
  if (!this.userInput || this.userInput.trim().length < 1 || this.isSpeaking) {
158
- this.suggestions = []; // Clear suggestions if no input or AI is speaking
159
  return;
160
  }
161
 
@@ -164,8 +146,8 @@ export class ChatComponent implements OnDestroy {
164
  console.log("API Response:", response);
165
  if (response.suggestions) {
166
  this.suggestions = response.suggestions
167
- .filter((s: string) => s && s.trim().length > 0) // Remove empty suggestions
168
- .map((s: string) => s.replace(/^\d+\.\s*/, "")); // Remove numbering
169
  } else {
170
  this.suggestions = [];
171
  }
@@ -177,19 +159,12 @@ export class ChatComponent implements OnDestroy {
177
  );
178
  }
179
 
180
-
181
-
182
-
183
- // When a user selects a suggestion, send it as a message
184
  selectSuggestion(suggestion: string): void {
185
  this.userInput = suggestion;
186
- this.suggestions = []; // Clear suggestions after selection
187
- this.sendMessage(); // Send the selected suggestion
188
  }
189
 
190
-
191
-
192
-
193
  sendMessage(inputText?: string): void {
194
  const message = inputText ? inputText.trim() : this.userInput.trim();
195
  if (!message) {
@@ -198,70 +173,65 @@ export class ChatComponent implements OnDestroy {
198
 
199
  let sessionId = localStorage.getItem('session_id');
200
 
201
- // ✅ Add user message to chat
202
  this.messages.push({ from: 'user', text: message, timestamp: new Date().toLocaleTimeString() });
203
- this.userInput = ''; // ✅ Clear input field after sending
204
  this.isTyping = true;
205
  this.cdr.detectChanges();
206
- this.scrollToBottom(); // ✅ Scroll after adding user message
207
 
208
- this.apiService.askQuestion(message, sessionId).subscribe(
209
  (response) => {
210
  this.isTyping = false;
211
- let explanation = response?.response || 'No explanation available.';
 
212
 
213
  if (response.session_id && !sessionId) {
214
  localStorage.setItem('session_id', response.session_id);
215
  }
216
 
217
- let formattedExplanation = explanation.trim().split('\n').map((para: string) => para.trim()).join('\n');
 
 
 
 
 
 
 
 
 
 
218
 
219
- this.messages.push({ from: 'ai', text: formattedExplanation, timestamp: new Date().toLocaleTimeString() });
220
- this.cdr.detectChanges();
221
- this.scrollToBottom(); // ✅ Scroll after AI response
222
  this.speakResponse(explanation);
223
  },
224
  (error) => {
225
  this.isTyping = false;
226
  const errorMessage = 'Error: Could not get a response from the server.';
227
- console.error("API Error:", error);
228
- this.messages.push({ from: 'ai', text: errorMessage, timestamp: new Date().toLocaleTimeString() });
229
- this.cdr.detectChanges();
230
- this.scrollToBottom(); // ✅ Scroll even if an error occurs
 
 
 
 
 
 
 
231
  this.speakResponse(errorMessage);
232
  }
233
  );
234
  }
235
 
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
  formatStructuredResponse(text: string): string {
244
  let formattedText = text
245
- .replace(/\n/g, '<br>') // Preserve line breaks
246
- .replace(/(\d+)\.\s/g, '<b>$1.</b> ') // Bold numbered lists
247
- .replace(/\•\s/g, '✔️ ') // Replace bullets with custom emojis
248
- .replace(/\-\s/g, '🔹 ') // Replace hyphen bullets with emoji
249
- .replace(/(\*\*)(.*?)\1/g, '<b>$2</b>'); // Convert "**bold text**" to <b>bold text</b>
250
-
251
  return formattedText;
252
  }
253
 
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
  speakResponse(responseText: string): void {
266
  if (!responseText) {
267
  console.warn('No response text provided for speech.');
@@ -270,36 +240,29 @@ export class ChatComponent implements OnDestroy {
270
 
271
  console.log('Initiating text-to-speech with response:', responseText);
272
 
273
- // Find the last AI message and update it dynamically
274
  let lastAiMessage = this.messages.slice().reverse().find((msg) => msg.from === 'ai');
275
 
276
  if (!lastAiMessage) {
277
- // If no AI message exists, create one
278
  lastAiMessage = { from: 'ai', text: '', timestamp: new Date().toLocaleTimeString() };
279
  this.messages.push(lastAiMessage);
280
  } else {
281
- // Clear existing text before speaking
282
  lastAiMessage.text = '';
283
  }
284
 
285
- this.cdr.detectChanges(); // Ensure UI updates immediately
286
 
287
- // Split response text into words for live updates
288
  const words = responseText.split(' ');
289
  let currentWordIndex = 0;
290
 
291
- // Create SpeechSynthesisUtterance
292
  const speech = new SpeechSynthesisUtterance();
293
  speech.text = responseText;
294
  speech.lang = 'en-US';
295
- speech.pitch = 1; // Adjust pitch (higher values sound more feminine)
296
- speech.rate = 1; // Normal speaking speed
297
  this.isSpeaking = true;
298
 
299
- // Fetch available voices
300
  const voices = window.speechSynthesis.getVoices();
301
 
302
- // ✅ Use Microsoft Zira for a female voice
303
  let femaleVoice = voices.find(voice => voice.name === "Microsoft Zira - English (United States)");
304
 
305
  if (femaleVoice) {
@@ -309,41 +272,25 @@ export class ChatComponent implements OnDestroy {
309
  console.warn("Microsoft Zira not found, using default.");
310
  }
311
 
312
- // Update AI response dynamically as words are spoken
313
  speech.onboundary = (event) => {
314
  if (event.name === 'word' && currentWordIndex < words.length) {
315
- lastAiMessage!.text = words.slice(0, currentWordIndex + 1).join(' '); // Update text word by word
316
  currentWordIndex++;
317
- this.cdr.detectChanges(); // Update UI dynamically
318
  }
319
  };
320
 
321
- // When speech ends, keep the full response or leave the last word
322
  speech.onend = () => {
323
  console.log('Speech ended.');
324
  this.isSpeaking = false;
325
- lastAiMessage!.text = responseText; // Show full response after speech ends
326
  this.cdr.detectChanges();
327
  };
328
 
329
- // Start speaking
330
  console.log('Starting speech synthesis...');
331
  window.speechSynthesis.speak(speech);
332
  }
333
 
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
  ngOnInit(): void {
348
  if (window.speechSynthesis.onvoiceschanged !== undefined) {
349
  window.speechSynthesis.onvoiceschanged = () => {
@@ -351,34 +298,29 @@ export class ChatComponent implements OnDestroy {
351
  };
352
  }
353
 
354
- // Load voices immediately in case they are already available
355
  this.loadVoices();
356
  }
357
 
358
-
359
-
360
  loadVoices(): void {
361
  const voices = window.speechSynthesis.getVoices();
362
 
363
  if (!voices.length) {
364
  console.warn("No voices available yet, retrying...");
365
- setTimeout(() => this.loadVoices(), 500); // Retry loading voices
366
  return;
367
  }
368
 
369
- console.log("Available Voices:", voices.map(v => v.name)); // Debugging
370
 
371
- // ✅ Preferred female voices
372
  const preferredVoices = [
373
  "Google UK English Female",
374
  "Google US English Female",
375
- "Microsoft Zira - English (United States)", // Edge/Windows
376
  "Microsoft Hazel - English (United Kingdom)",
377
  "Google en-GB Female",
378
  "Google en-US Female"
379
  ];
380
 
381
- // Try to find a preferred female voice
382
  for (let voiceName of preferredVoices) {
383
  const foundVoice = voices.find(voice => voice.name === voiceName);
384
  if (foundVoice) {
@@ -387,7 +329,6 @@ export class ChatComponent implements OnDestroy {
387
  }
388
  }
389
 
390
- // If no preferred female voice found, pick any available female voice
391
  if (!this.selectedVoice) {
392
  this.selectedVoice = voices.find(voice => voice.name.toLowerCase().includes("female")) || voices[0];
393
  }
@@ -395,15 +336,12 @@ export class ChatComponent implements OnDestroy {
395
  console.log("Selected AI Voice:", this.selectedVoice?.name);
396
  }
397
 
398
-
399
-
400
-
401
  pauseAudio(): void {
402
  if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
403
  window.speechSynthesis.pause();
404
  this.isAudioPaused = true;
405
  console.log('AI Speech Paused');
406
- this.cdr.detectChanges(); // Update UI
407
  }
408
  }
409
 
@@ -412,24 +350,17 @@ export class ChatComponent implements OnDestroy {
412
  window.speechSynthesis.resume();
413
  this.isAudioPaused = false;
414
  console.log('AI Speech Resumed');
415
- this.cdr.detectChanges(); // Update UI
416
  }
417
  }
418
 
419
-
420
-
421
-
422
  muteMicrophone(): void {
423
  console.log("Microphone muted");
424
- // Logic to mute the microphone
425
  }
426
 
427
-
428
-
429
- // Start listening for voice input
430
  startListening(): void {
431
- this.isListening = true; // Show the overlay
432
- this.isProcessingSpeech = false; // Reset the flag before starting
433
 
434
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
435
  navigator.mediaDevices
@@ -439,30 +370,25 @@ export class ChatComponent implements OnDestroy {
439
  console.log('Starting speech recognition...');
440
  this.recognition.start();
441
 
442
- // Debugging events
443
  this.recognition.onaudiostart = () => console.log('Audio capturing started.');
444
  this.recognition.onspeechstart = () => console.log('Speech has been detected.');
445
  this.recognition.onspeechend = () => console.log('Speech ended, processing...');
446
  this.recognition.onaudioend = () => console.log('Audio capturing ended.');
447
 
448
- // Automatically process recognized speech
449
  this.recognition.onresult = (event: any) => {
450
  if (event.results && event.results[0]) {
451
  const transcript = event.results[0][0].transcript.trim();
452
  console.log('Recognized speech:', transcript);
453
 
454
- // Set the recognized speech as input
455
  this.userInput = transcript;
456
 
457
- // Automatically send the message
458
  if (this.userInput.trim()) {
459
  console.log('Sending question automatically:', this.userInput);
460
- this.sendMessage(); // Call sendMessage to process the question
461
  }
462
 
463
- // Stop recognition to prevent multiple triggers
464
  this.recognition.stop();
465
- this.isListening = false; // Hide the overlay
466
  }
467
  };
468
 
@@ -488,7 +414,7 @@ export class ChatComponent implements OnDestroy {
488
  .catch((error) => {
489
  console.error('Microphone access denied:', error);
490
  this.errorMessage = 'Please enable microphone access to use this feature.';
491
- this.isListening = true; // Keep the overlay visible
492
  });
493
 
494
  } else {
@@ -497,85 +423,69 @@ export class ChatComponent implements OnDestroy {
497
  }
498
 
499
  stopListening(): void {
500
- this.isListening = false; // Hide the overlay
501
  if (this.recognition) {
502
- this.recognition.stop(); // Stop speech recognition
503
  }
504
  }
505
 
506
-
507
- // Toggle between play and pause for a specific message
508
  toggleAudio(message: { text: string, isPlaying?: boolean }): void {
509
  if (this.speechSynthesisInstance && this.speechSynthesisInstance.text === message.text) {
510
  if (message.isPlaying) {
511
- // Pause the currently playing audio
512
  window.speechSynthesis.pause();
513
  message.isPlaying = false;
514
  } else {
515
- // Resume the paused audio
516
  window.speechSynthesis.resume();
517
  message.isPlaying = true;
518
  }
519
  } else {
520
- // Stop any currently playing audio
521
  if (this.speechSynthesisInstance) {
522
  window.speechSynthesis.cancel();
523
  }
524
- this.messages.forEach((msg) => (msg.isPlaying = false)); // Reset all states
525
 
526
- // Start playback for the selected message
527
  message.isPlaying = true;
528
  this.speechSynthesisInstance = new SpeechSynthesisUtterance(message.text);
529
  this.speechSynthesisInstance.lang = 'en-US';
530
  this.speechSynthesisInstance.pitch = 1;
531
  this.speechSynthesisInstance.rate = 1;
532
 
533
- // When playback ends, reset the state
534
  this.speechSynthesisInstance.onend = () => {
535
  message.isPlaying = false;
536
  this.speechSynthesisInstance = null;
537
  };
538
 
539
- // Play the audio
540
  window.speechSynthesis.speak(this.speechSynthesisInstance);
541
  }
542
  }
543
 
544
-
545
-
546
  goToHome() {
547
  this.router.navigate(['/home']);
548
  }
549
 
550
- copySuccessIndex: number | null = null; // Track copied message index
551
 
552
  copyToClipboard(text: string, index: number): void {
553
  navigator.clipboard.writeText(text).then(() => {
554
- this.copySuccessIndex = index; // Show tick icon for copied message
555
  setTimeout(() => {
556
- this.copySuccessIndex = null; // Hide tick after 2 seconds
557
  }, 2000);
558
  }).catch(err => {
559
  console.error('Failed to copy: ', err);
560
  });
561
  }
562
 
563
-
564
-
565
-
566
- // Function to check input validity
567
  checkInput() {
568
  this.isInputValid = this.userInput.trim().length > 0;
569
  }
570
 
571
-
572
-
573
  handleButtonClick(): void {
574
  if (this.userInput.trim().length > 0) {
575
- this.showQuestions = false; // Hide tabs when user manually enters text
576
- const messageToSend = this.userInput; // Store input before clearing
577
- this.userInput = ''; // Clear input for UI update
578
- this.sendMessage(messageToSend); // Send the stored message
579
  } else if (this.isSpeaking) {
580
  this.pauseAudio();
581
  } else if (this.isAudioPaused) {
@@ -585,65 +495,38 @@ export class ChatComponent implements OnDestroy {
585
  }
586
  }
587
 
588
-
589
-
590
  getButtonIcon(): string {
591
  if (this.userInput.trim().length > 0) {
592
- return 'assets/images/chat/send-icon.png'; // Replace with your send icon image
593
  } else if (this.isSpeaking) {
594
- return 'assets/images/chat/pause-icon.png'; // Replace with your pause icon image
595
  } else if (this.isAudioPaused) {
596
- return 'assets/images/chat/resume-icon.png'; // Replace with your resume icon image
597
  } else {
598
- return 'assets/images/chat/microphone-icon.png'; // Replace with your microphone icon image
599
  }
600
  }
601
 
602
-
603
-
604
  addNewLine(event: KeyboardEvent): void {
605
  if (event.key === 'Enter' && event.shiftKey) {
606
- event.preventDefault(); // Prevent form submission
607
- this.userInput += '\n'; // Add a new line to the textarea
608
  }
609
  }
610
 
611
-
612
  adjustTextareaHeight(event: Event): void {
613
  const textarea = event.target as HTMLTextAreaElement;
614
- textarea.style.height = 'auto'; // Reset height to calculate new height
615
- textarea.style.height = `${textarea.scrollHeight}px`; // Adjust height based on content
616
- }
617
-
618
-
619
-
620
-
621
- handleEnterPress(event: KeyboardEvent): void {
622
- if (this.isSpeaking) {
623
- event.preventDefault(); // Prevent typing when AI is speaking
624
- return;
625
- }
626
-
627
- if (event.key === 'Enter' && !event.shiftKey) {
628
- event.preventDefault();
629
- this.handleButtonClick();
630
- } else if (event.key === 'Enter' && event.shiftKey) {
631
- event.preventDefault();
632
- this.userInput += '\n';
633
- }
634
  }
635
 
636
-
637
-
638
-
639
-
640
  getButtonIconClass(): string {
641
  return this.userInput.trim().length > 0
642
  ? 'send-icon'
643
  : this.isSpeaking
644
  ? 'pause-icon'
645
  : this.isAudioPaused
646
- ? 'resume-icon' // Class for resume icon
647
  : 'microphone-icon';
648
  }
649
 
@@ -661,7 +544,35 @@ export class ChatComponent implements OnDestroy {
661
  }
662
  }
663
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
 
 
 
 
665
 
 
 
 
 
 
 
 
 
 
 
 
666
 
 
667
  }
 
1
  import { Component, Inject, OnDestroy, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
2
+ import { ApiService } from './api.service';
3
+ import { FormsModule } from '@angular/forms';
4
  import { CommonModule } from '@angular/common';
5
  import { Router, RouterModule } from '@angular/router';
6
  import { isPlatformBrowser } from '@angular/common';
7
  import { ViewChild, ElementRef } from '@angular/core';
8
  import { Renderer2 } from '@angular/core';
9
+ import { Subscription } from 'rxjs';
 
 
10
 
11
  @Component({
12
  selector: 'app-chat',
 
16
  styleUrl: './chat.component.css'
17
  })
18
  export class ChatComponent implements OnDestroy {
19
+ showQuestions: boolean = false;
20
  userInput: string = '';
21
  messages: { from: string, text: string, timestamp: string; isPlaying?: boolean }[] = [];
22
+ isTyping: boolean = false;
23
  @ViewChild('chatBox') chatBox!: ElementRef;
24
 
25
+ isLoadingSpeech: boolean = false;
26
  selectedVoice: SpeechSynthesisVoice | null = null;
27
  errorMessage: string = "";
28
+ recognition: any;
29
  speechSynthesisInstance: SpeechSynthesisUtterance | null = null;
30
  isListening: boolean = false;
31
+ isProcessingSpeech: boolean = false;
32
+ isSpeaking: boolean = false;
33
+ isAudioPaused: boolean = false;
 
34
  isInputValid: boolean = false;
35
+ suggestions: string[] = [];
36
  showMicPopup: boolean = false;
37
+ isSubmitting: boolean = false;
38
+ private responseSub?: Subscription;
39
+ private lastFullAiText: string = '';
40
 
41
  ngAfterViewChecked() {
 
42
  setTimeout(() => {
43
  this.scrollToBottom();
44
+ }, 100);
45
  }
46
 
 
47
  private scrollToBottom(): void {
48
  try {
49
  this.chatBox.nativeElement.scrollTop = this.chatBox.nativeElement.scrollHeight;
50
  } catch (err) { }
51
  }
52
 
53
+ constructor(
54
+ private apiService: ApiService,
55
+ private cdr: ChangeDetectorRef,
56
+ @Inject(PLATFORM_ID,) private platformId: object,
57
+ private router: Router,
58
+ private renderer: Renderer2
59
+ ) {
60
  window.speechSynthesis.onvoiceschanged = () => {
61
  console.log("Available Voices:", window.speechSynthesis.getVoices());
62
  };
63
 
 
64
  if (isPlatformBrowser(this.platformId)) {
65
  const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
66
  if (SpeechRecognition) {
 
73
  if (event.results && event.results[0]) {
74
  const transcript = event.results[0][0].transcript.trim();
75
  console.log('Recognized speech:', transcript);
76
+ this.userInput = transcript;
77
+ this.sendMessage();
 
 
 
78
  this.recognition.stop();
79
+ this.isListening = false;
80
  }
81
  };
82
 
83
  this.recognition.onerror = (event: any) => {
84
  console.error('Speech Recognition Error:', event.error);
85
+ this.isProcessingSpeech = false;
86
  };
87
  } else {
88
  console.warn('Speech Recognition is not supported in this browser.');
89
  }
 
90
  window.addEventListener('beforeunload', this.handleUnload);
91
  }
 
92
  }
93
 
 
94
  private handleUnload = (): void => {
95
  if (window.speechSynthesis) {
96
  window.speechSynthesis.cancel();
97
  }
98
  };
99
 
 
100
  ngOnDestroy(): void {
101
  if (isPlatformBrowser(this.platformId)) {
102
  if (window.speechSynthesis) {
103
  window.speechSynthesis.cancel();
104
  }
 
105
  window.removeEventListener('beforeunload', this.handleUnload);
106
  }
107
  }
 
114
  this.showMicPopup = false;
115
  }
116
 
 
 
 
 
 
 
 
117
  showHardcodedQuestions(): void {
118
  setTimeout(() => {
119
  this.showQuestions = true;
120
+ }, 100);
121
  }
122
 
 
123
  hideHardcodedQuestions(): void {
124
  setTimeout(() => {
125
  this.showQuestions = false;
126
+ }, 200);
127
  }
128
 
 
129
  selectHardcodedQuestion(question: string): void {
130
+ this.userInput = question;
131
+ this.showQuestions = false;
132
  setTimeout(() => {
133
+ this.sendMessage();
134
+ this.userInput = '';
135
+ }, 100);
136
  }
137
 
 
 
138
  getSuggestions(): void {
139
  if (!this.userInput || this.userInput.trim().length < 1 || this.isSpeaking) {
140
+ this.suggestions = [];
141
  return;
142
  }
143
 
 
146
  console.log("API Response:", response);
147
  if (response.suggestions) {
148
  this.suggestions = response.suggestions
149
+ .filter((s: string) => s && s.trim().length > 0)
150
+ .map((s: string) => s.replace(/^\d+\.\s*/, ""));
151
  } else {
152
  this.suggestions = [];
153
  }
 
159
  );
160
  }
161
 
 
 
 
 
162
  selectSuggestion(suggestion: string): void {
163
  this.userInput = suggestion;
164
+ this.suggestions = [];
165
+ this.sendMessage();
166
  }
167
 
 
 
 
168
  sendMessage(inputText?: string): void {
169
  const message = inputText ? inputText.trim() : this.userInput.trim();
170
  if (!message) {
 
173
 
174
  let sessionId = localStorage.getItem('session_id');
175
 
 
176
  this.messages.push({ from: 'user', text: message, timestamp: new Date().toLocaleTimeString() });
177
+ this.userInput = '';
178
  this.isTyping = true;
179
  this.cdr.detectChanges();
180
+ this.scrollToBottom();
181
 
182
+ this.responseSub = this.apiService.askQuestion(message, sessionId).subscribe(
183
  (response) => {
184
  this.isTyping = false;
185
+
186
+ const explanation = (response?.response || 'No explanation available.').trim();
187
 
188
  if (response.session_id && !sessionId) {
189
  localStorage.setItem('session_id', response.session_id);
190
  }
191
 
192
+ const lines: string[] = String(explanation).split('\n');
193
+ const formatted: string = lines.map((line: string) => line.trim()).join('\n');
194
+ this.messages.push({
195
+ from: 'ai',
196
+ text: formatted,
197
+ timestamp: new Date().toLocaleTimeString(),
198
+ });
199
+ this.cdr.detectChanges();
200
+ this.scrollToBottom();
201
+
202
+ this.lastFullAiText = formatted;
203
 
 
 
 
204
  this.speakResponse(explanation);
205
  },
206
  (error) => {
207
  this.isTyping = false;
208
  const errorMessage = 'Error: Could not get a response from the server.';
209
+ console.error('API Error:', error);
210
+
211
+ this.messages.push({
212
+ from: 'ai',
213
+ text: errorMessage,
214
+ timestamp: new Date().toLocaleTimeString(),
215
+ });
216
+ this.cdr.detectChanges();
217
+ this.scrollToBottom();
218
+
219
+ this.lastFullAiText = errorMessage;
220
  this.speakResponse(errorMessage);
221
  }
222
  );
223
  }
224
 
 
 
 
 
 
 
 
225
  formatStructuredResponse(text: string): string {
226
  let formattedText = text
227
+ .replace(/\n/g, '<br>')
228
+ .replace(/(\d+)\.\s/g, '<b>$1.</b> ')
229
+ .replace(/\•\s/g, '✔️ ')
230
+ .replace(/\-\s/g, '🔹 ')
231
+ .replace(/(\*\*)(.*?)\1/g, '<b>$2</b>');
 
232
  return formattedText;
233
  }
234
 
 
 
 
 
 
 
 
 
 
 
 
235
  speakResponse(responseText: string): void {
236
  if (!responseText) {
237
  console.warn('No response text provided for speech.');
 
240
 
241
  console.log('Initiating text-to-speech with response:', responseText);
242
 
 
243
  let lastAiMessage = this.messages.slice().reverse().find((msg) => msg.from === 'ai');
244
 
245
  if (!lastAiMessage) {
 
246
  lastAiMessage = { from: 'ai', text: '', timestamp: new Date().toLocaleTimeString() };
247
  this.messages.push(lastAiMessage);
248
  } else {
 
249
  lastAiMessage.text = '';
250
  }
251
 
252
+ this.cdr.detectChanges();
253
 
 
254
  const words = responseText.split(' ');
255
  let currentWordIndex = 0;
256
 
 
257
  const speech = new SpeechSynthesisUtterance();
258
  speech.text = responseText;
259
  speech.lang = 'en-US';
260
+ speech.pitch = 1;
261
+ speech.rate = 1;
262
  this.isSpeaking = true;
263
 
 
264
  const voices = window.speechSynthesis.getVoices();
265
 
 
266
  let femaleVoice = voices.find(voice => voice.name === "Microsoft Zira - English (United States)");
267
 
268
  if (femaleVoice) {
 
272
  console.warn("Microsoft Zira not found, using default.");
273
  }
274
 
 
275
  speech.onboundary = (event) => {
276
  if (event.name === 'word' && currentWordIndex < words.length) {
277
+ lastAiMessage!.text = words.slice(0, currentWordIndex + 1).join(' ');
278
  currentWordIndex++;
279
+ this.cdr.detectChanges();
280
  }
281
  };
282
 
 
283
  speech.onend = () => {
284
  console.log('Speech ended.');
285
  this.isSpeaking = false;
286
+ lastAiMessage!.text = responseText;
287
  this.cdr.detectChanges();
288
  };
289
 
 
290
  console.log('Starting speech synthesis...');
291
  window.speechSynthesis.speak(speech);
292
  }
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  ngOnInit(): void {
295
  if (window.speechSynthesis.onvoiceschanged !== undefined) {
296
  window.speechSynthesis.onvoiceschanged = () => {
 
298
  };
299
  }
300
 
 
301
  this.loadVoices();
302
  }
303
 
 
 
304
  loadVoices(): void {
305
  const voices = window.speechSynthesis.getVoices();
306
 
307
  if (!voices.length) {
308
  console.warn("No voices available yet, retrying...");
309
+ setTimeout(() => this.loadVoices(), 500);
310
  return;
311
  }
312
 
313
+ console.log("Available Voices:", voices.map(v => v.name));
314
 
 
315
  const preferredVoices = [
316
  "Google UK English Female",
317
  "Google US English Female",
318
+ "Microsoft Zira - English (United States)",
319
  "Microsoft Hazel - English (United Kingdom)",
320
  "Google en-GB Female",
321
  "Google en-US Female"
322
  ];
323
 
 
324
  for (let voiceName of preferredVoices) {
325
  const foundVoice = voices.find(voice => voice.name === voiceName);
326
  if (foundVoice) {
 
329
  }
330
  }
331
 
 
332
  if (!this.selectedVoice) {
333
  this.selectedVoice = voices.find(voice => voice.name.toLowerCase().includes("female")) || voices[0];
334
  }
 
336
  console.log("Selected AI Voice:", this.selectedVoice?.name);
337
  }
338
 
 
 
 
339
  pauseAudio(): void {
340
  if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
341
  window.speechSynthesis.pause();
342
  this.isAudioPaused = true;
343
  console.log('AI Speech Paused');
344
+ this.cdr.detectChanges();
345
  }
346
  }
347
 
 
350
  window.speechSynthesis.resume();
351
  this.isAudioPaused = false;
352
  console.log('AI Speech Resumed');
353
+ this.cdr.detectChanges();
354
  }
355
  }
356
 
 
 
 
357
  muteMicrophone(): void {
358
  console.log("Microphone muted");
 
359
  }
360
 
 
 
 
361
  startListening(): void {
362
+ this.isListening = true;
363
+ this.isProcessingSpeech = false;
364
 
365
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
366
  navigator.mediaDevices
 
370
  console.log('Starting speech recognition...');
371
  this.recognition.start();
372
 
 
373
  this.recognition.onaudiostart = () => console.log('Audio capturing started.');
374
  this.recognition.onspeechstart = () => console.log('Speech has been detected.');
375
  this.recognition.onspeechend = () => console.log('Speech ended, processing...');
376
  this.recognition.onaudioend = () => console.log('Audio capturing ended.');
377
 
 
378
  this.recognition.onresult = (event: any) => {
379
  if (event.results && event.results[0]) {
380
  const transcript = event.results[0][0].transcript.trim();
381
  console.log('Recognized speech:', transcript);
382
 
 
383
  this.userInput = transcript;
384
 
 
385
  if (this.userInput.trim()) {
386
  console.log('Sending question automatically:', this.userInput);
387
+ this.sendMessage();
388
  }
389
 
 
390
  this.recognition.stop();
391
+ this.isListening = false;
392
  }
393
  };
394
 
 
414
  .catch((error) => {
415
  console.error('Microphone access denied:', error);
416
  this.errorMessage = 'Please enable microphone access to use this feature.';
417
+ this.isListening = true;
418
  });
419
 
420
  } else {
 
423
  }
424
 
425
  stopListening(): void {
426
+ this.isListening = false;
427
  if (this.recognition) {
428
+ this.recognition.stop();
429
  }
430
  }
431
 
 
 
432
  toggleAudio(message: { text: string, isPlaying?: boolean }): void {
433
  if (this.speechSynthesisInstance && this.speechSynthesisInstance.text === message.text) {
434
  if (message.isPlaying) {
 
435
  window.speechSynthesis.pause();
436
  message.isPlaying = false;
437
  } else {
 
438
  window.speechSynthesis.resume();
439
  message.isPlaying = true;
440
  }
441
  } else {
 
442
  if (this.speechSynthesisInstance) {
443
  window.speechSynthesis.cancel();
444
  }
445
+ this.messages.forEach((msg) => (msg.isPlaying = false));
446
 
 
447
  message.isPlaying = true;
448
  this.speechSynthesisInstance = new SpeechSynthesisUtterance(message.text);
449
  this.speechSynthesisInstance.lang = 'en-US';
450
  this.speechSynthesisInstance.pitch = 1;
451
  this.speechSynthesisInstance.rate = 1;
452
 
 
453
  this.speechSynthesisInstance.onend = () => {
454
  message.isPlaying = false;
455
  this.speechSynthesisInstance = null;
456
  };
457
 
 
458
  window.speechSynthesis.speak(this.speechSynthesisInstance);
459
  }
460
  }
461
 
 
 
462
  goToHome() {
463
  this.router.navigate(['/home']);
464
  }
465
 
466
+ copySuccessIndex: number | null = null;
467
 
468
  copyToClipboard(text: string, index: number): void {
469
  navigator.clipboard.writeText(text).then(() => {
470
+ this.copySuccessIndex = index;
471
  setTimeout(() => {
472
+ this.copySuccessIndex = null;
473
  }, 2000);
474
  }).catch(err => {
475
  console.error('Failed to copy: ', err);
476
  });
477
  }
478
 
 
 
 
 
479
  checkInput() {
480
  this.isInputValid = this.userInput.trim().length > 0;
481
  }
482
 
 
 
483
  handleButtonClick(): void {
484
  if (this.userInput.trim().length > 0) {
485
+ this.showQuestions = false;
486
+ const messageToSend = this.userInput;
487
+ this.userInput = '';
488
+ this.sendMessage(messageToSend);
489
  } else if (this.isSpeaking) {
490
  this.pauseAudio();
491
  } else if (this.isAudioPaused) {
 
495
  }
496
  }
497
 
 
 
498
  getButtonIcon(): string {
499
  if (this.userInput.trim().length > 0) {
500
+ return 'assets/images/chat/send-icon.png';
501
  } else if (this.isSpeaking) {
502
+ return 'assets/images/chat/pause-icon.png';
503
  } else if (this.isAudioPaused) {
504
+ return 'assets/images/chat/resume-icon.png';
505
  } else {
506
+ return 'assets/images/chat/microphone-icon.png';
507
  }
508
  }
509
 
 
 
510
  addNewLine(event: KeyboardEvent): void {
511
  if (event.key === 'Enter' && event.shiftKey) {
512
+ event.preventDefault();
513
+ this.userInput += '\n';
514
  }
515
  }
516
 
 
517
  adjustTextareaHeight(event: Event): void {
518
  const textarea = event.target as HTMLTextAreaElement;
519
+ textarea.style.height = 'auto';
520
+ textarea.style.height = `${textarea.scrollHeight}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  }
522
 
 
 
 
 
523
  getButtonIconClass(): string {
524
  return this.userInput.trim().length > 0
525
  ? 'send-icon'
526
  : this.isSpeaking
527
  ? 'pause-icon'
528
  : this.isAudioPaused
529
+ ? 'resume-icon'
530
  : 'microphone-icon';
531
  }
532
 
 
544
  }
545
  }
546
 
547
+ stopSpeaking(): void {
548
+ try {
549
+ if (window.speechSynthesis.speaking || window.speechSynthesis.paused) {
550
+ window.speechSynthesis.cancel();
551
+ }
552
+ } catch { }
553
+
554
+ (this as any).speechSynthesisInstance = null;
555
+
556
+ if (this.responseSub && !this.responseSub.closed) {
557
+ this.responseSub.unsubscribe();
558
+ }
559
 
560
+ this.isSpeaking = false;
561
+ this.isAudioPaused = false;
562
+ this.isTyping = false;
563
+ }
564
 
565
+ handleEnterPress(event: KeyboardEvent): void {
566
+ if (this.isSpeaking) {
567
+ event.preventDefault();
568
+ return;
569
+ }
570
+ if (event.key === 'Enter' && !event.shiftKey) {
571
+ event.preventDefault();
572
+ const text = (this.userInput || '').trim();
573
+ if (text) this.sendMessage();
574
+ }
575
+ }
576
 
577
+
578
  }
src/assets/images/chat/control.png ADDED

Git LFS Details

  • SHA256: 2d37166a1708f4d6e27babc1b9bc408c505e5b79e27a7a81ef0ef29f1680ac65
  • Pointer size: 129 Bytes
  • Size of remote file: 5.89 kB
src/assets/images/chat/stop-button.png ADDED

Git LFS Details

  • SHA256: a01d5cf4aa76de7da282c98decad20b19a4fe37076ec9f195e850966cdae8389
  • Pointer size: 129 Bytes
  • Size of remote file: 4.08 kB
src/assets/images/chat/stop.png ADDED

Git LFS Details

  • SHA256: 5c10275efe01a0421881c8a70652110158155578243833c1434472f6cbad805f
  • Pointer size: 130 Bytes
  • Size of remote file: 31.7 kB