AptlyDigital commited on
Commit
cdeeebc
·
verified ·
1 Parent(s): e820db8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1930 -0
index.html CHANGED
@@ -0,0 +1,1930 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Tutor 3D Interface</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ background: #0a0a0f;
19
+ color: #fff;
20
+ font-family: 'Segoe UI', system-ui, sans-serif;
21
+ overflow: hidden;
22
+ height: 100vh;
23
+ }
24
+
25
+ #canvasContainer {
26
+ width: 100%;
27
+ height: 100%;
28
+ position: relative;
29
+ }
30
+
31
+ canvas {
32
+ display: block;
33
+ outline: none;
34
+ width: 100%;
35
+ height: 100%;
36
+ }
37
+
38
+ .ui-overlay {
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ pointer-events: none;
45
+ z-index: 100;
46
+ }
47
+
48
+ /* === CHAT INTERFACE === */
49
+ .chat-interface {
50
+ position: absolute;
51
+ top: 30px;
52
+ right: 30px;
53
+ width: 420px;
54
+ height: calc(100vh - 100px);
55
+ display: flex;
56
+ flex-direction: column;
57
+ pointer-events: all;
58
+ z-index: 101;
59
+ opacity: 0;
60
+ transform: translateY(20px);
61
+ animation: slideIn 0.5s ease 1s forwards;
62
+ }
63
+
64
+ @keyframes slideIn {
65
+ to {
66
+ opacity: 1;
67
+ transform: translateY(0);
68
+ }
69
+ }
70
+
71
+ .chat-header {
72
+ background: rgba(20, 20, 30, 0.9);
73
+ backdrop-filter: blur(10px);
74
+ border-radius: 20px 20px 0 0;
75
+ padding: 20px;
76
+ border: 1px solid rgba(255, 255, 255, 0.1);
77
+ border-bottom: none;
78
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ }
83
+
84
+ .chat-title {
85
+ font-size: 1.2em;
86
+ font-weight: 300;
87
+ color: #a0b0ff;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 10px;
91
+ }
92
+
93
+ .chat-status {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 8px;
97
+ font-size: 0.85em;
98
+ color: #8892b0;
99
+ }
100
+
101
+ .status-indicator {
102
+ width: 8px;
103
+ height: 8px;
104
+ border-radius: 50%;
105
+ background: #00ff9d;
106
+ animation: pulse 2s infinite;
107
+ }
108
+
109
+ .chat-container {
110
+ flex: 1;
111
+ background: rgba(20, 20, 30, 0.85);
112
+ backdrop-filter: blur(10px);
113
+ border: 1px solid rgba(255, 255, 255, 0.1);
114
+ border-top: none;
115
+ overflow: hidden;
116
+ display: flex;
117
+ flex-direction: column;
118
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
119
+ }
120
+
121
+ .chat-messages {
122
+ flex: 1;
123
+ overflow-y: auto;
124
+ padding: 20px;
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 20px;
128
+ scrollbar-width: thin;
129
+ scrollbar-color: #5a6cff rgba(255, 255, 255, 0.1);
130
+ }
131
+
132
+ .chat-messages::-webkit-scrollbar {
133
+ width: 6px;
134
+ }
135
+
136
+ .chat-messages::-webkit-scrollbar-track {
137
+ background: rgba(255, 255, 255, 0.05);
138
+ border-radius: 3px;
139
+ }
140
+
141
+ .chat-messages::-webkit-scrollbar-thumb {
142
+ background: #5a6cff;
143
+ border-radius: 3px;
144
+ }
145
+
146
+ .message {
147
+ max-width: 85%;
148
+ padding: 15px;
149
+ border-radius: 18px;
150
+ line-height: 1.5;
151
+ position: relative;
152
+ animation: messageAppear 0.3s ease;
153
+ }
154
+
155
+ @keyframes messageAppear {
156
+ from {
157
+ opacity: 0;
158
+ transform: translateY(10px);
159
+ }
160
+ to {
161
+ opacity: 1;
162
+ transform: translateY(0);
163
+ }
164
+ }
165
+
166
+ .message-user {
167
+ align-self: flex-end;
168
+ background: linear-gradient(135deg, #5a6cff, #7a8aff);
169
+ color: white;
170
+ border-bottom-right-radius: 5px;
171
+ }
172
+
173
+ .message-ai {
174
+ align-self: flex-start;
175
+ background: rgba(255, 255, 255, 0.1);
176
+ color: #e0e0ff;
177
+ border-bottom-left-radius: 5px;
178
+ border: 1px solid rgba(255, 255, 255, 0.05);
179
+ }
180
+
181
+ .message-time {
182
+ font-size: 0.7em;
183
+ opacity: 0.7;
184
+ margin-top: 5px;
185
+ text-align: right;
186
+ }
187
+
188
+ .message-typing {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 5px;
192
+ padding: 15px;
193
+ background: rgba(255, 255, 255, 0.05);
194
+ border-radius: 18px;
195
+ border-bottom-left-radius: 5px;
196
+ align-self: flex-start;
197
+ width: 120px;
198
+ }
199
+
200
+ .typing-dot {
201
+ width: 8px;
202
+ height: 8px;
203
+ background: #a0b0ff;
204
+ border-radius: 50%;
205
+ animation: typing 1.4s infinite ease-in-out;
206
+ }
207
+
208
+ .typing-dot:nth-child(1) { animation-delay: -0.32s; }
209
+ .typing-dot:nth-child(2) { animation-delay: -0.16s; }
210
+
211
+ @keyframes typing {
212
+ 0%, 80%, 100% { transform: translateY(0); }
213
+ 40% { transform: translateY(-10px); }
214
+ }
215
+
216
+ .chat-input-container {
217
+ padding: 20px;
218
+ background: rgba(15, 15, 25, 0.9);
219
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
220
+ }
221
+
222
+ .input-wrapper {
223
+ display: flex;
224
+ gap: 10px;
225
+ align-items: flex-end;
226
+ }
227
+
228
+ .chat-input {
229
+ flex: 1;
230
+ background: rgba(255, 255, 255, 0.07);
231
+ border: 1px solid rgba(255, 255, 255, 0.1);
232
+ border-radius: 15px;
233
+ padding: 15px 20px;
234
+ color: white;
235
+ font-family: 'Segoe UI', system-ui, sans-serif;
236
+ font-size: 1em;
237
+ resize: none;
238
+ min-height: 56px;
239
+ max-height: 120px;
240
+ transition: all 0.3s ease;
241
+ }
242
+
243
+ .chat-input:focus {
244
+ outline: none;
245
+ border-color: #5a6cff;
246
+ background: rgba(255, 255, 255, 0.1);
247
+ box-shadow: 0 0 0 2px rgba(90, 108, 255, 0.2);
248
+ }
249
+
250
+ .chat-input::placeholder {
251
+ color: rgba(255, 255, 255, 0.4);
252
+ }
253
+
254
+ .voice-input-btn {
255
+ width: 56px;
256
+ height: 56px;
257
+ border-radius: 15px;
258
+ background: linear-gradient(135deg, #5a6cff, #7a8aff);
259
+ border: none;
260
+ color: white;
261
+ cursor: pointer;
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: center;
265
+ transition: all 0.3s ease;
266
+ flex-shrink: 0;
267
+ }
268
+
269
+ .voice-input-btn:hover {
270
+ transform: translateY(-2px);
271
+ box-shadow: 0 5px 15px rgba(90, 108, 255, 0.4);
272
+ }
273
+
274
+ .voice-input-btn.listening {
275
+ background: linear-gradient(135deg, #ff5a5a, #ff7a7a);
276
+ animation: pulseListening 1.5s infinite;
277
+ }
278
+
279
+ @keyframes pulseListening {
280
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 90, 90, 0.7); }
281
+ 70% { box-shadow: 0 0 0 10px rgba(255, 90, 90, 0); }
282
+ }
283
+
284
+ .send-btn {
285
+ width: 56px;
286
+ height: 56px;
287
+ border-radius: 15px;
288
+ background: rgba(255, 255, 255, 0.1);
289
+ border: 1px solid rgba(255, 255, 255, 0.1);
290
+ color: #a0b0ff;
291
+ cursor: pointer;
292
+ display: flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ transition: all 0.3s ease;
296
+ flex-shrink: 0;
297
+ }
298
+
299
+ .send-btn:hover {
300
+ background: rgba(90, 108, 255, 0.2);
301
+ color: white;
302
+ transform: translateY(-2px);
303
+ }
304
+
305
+ .voice-visualizer {
306
+ height: 40px;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ gap: 3px;
311
+ margin-top: 10px;
312
+ opacity: 0;
313
+ transition: opacity 0.3s ease;
314
+ }
315
+
316
+ .voice-visualizer.active {
317
+ opacity: 1;
318
+ }
319
+
320
+ .voice-bar {
321
+ width: 4px;
322
+ height: 10px;
323
+ background: #5a6cff;
324
+ border-radius: 2px;
325
+ transition: height 0.1s ease;
326
+ }
327
+
328
+ .voice-bar.listening {
329
+ background: #ff5a5a;
330
+ }
331
+
332
+ /* === VOICE CONTROLS PANEL === */
333
+ .voice-controls {
334
+ position: absolute;
335
+ bottom: 30px;
336
+ right: 470px;
337
+ background: rgba(20, 20, 30, 0.9);
338
+ backdrop-filter: blur(10px);
339
+ border-radius: 20px;
340
+ padding: 25px;
341
+ width: 280px;
342
+ pointer-events: all;
343
+ border: 1px solid rgba(255, 255, 255, 0.1);
344
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
345
+ z-index: 102;
346
+ opacity: 0;
347
+ transform: translateY(20px);
348
+ animation: slideIn 0.5s ease 1.2s forwards;
349
+ }
350
+
351
+ .voice-header {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 10px;
355
+ margin-bottom: 20px;
356
+ color: #a0b0ff;
357
+ font-size: 1.1em;
358
+ font-weight: 300;
359
+ }
360
+
361
+ .voice-control-group {
362
+ margin-bottom: 20px;
363
+ }
364
+
365
+ .voice-label {
366
+ display: block;
367
+ margin-bottom: 8px;
368
+ font-size: 0.9em;
369
+ color: #8892b0;
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 8px;
373
+ }
374
+
375
+ .voice-select {
376
+ width: 100%;
377
+ padding: 12px 15px;
378
+ background: rgba(255, 255, 255, 0.07);
379
+ border: 1px solid rgba(255, 255, 255, 0.1);
380
+ border-radius: 10px;
381
+ color: white;
382
+ font-family: 'Segoe UI', system-ui, sans-serif;
383
+ font-size: 0.95em;
384
+ cursor: pointer;
385
+ transition: all 0.3s ease;
386
+ }
387
+
388
+ .voice-select:focus {
389
+ outline: none;
390
+ border-color: #5a6cff;
391
+ background: rgba(255, 255, 255, 0.1);
392
+ box-shadow: 0 0 0 2px rgba(90, 108, 255, 0.2);
393
+ }
394
+
395
+ .voice-select option {
396
+ background: #1a1a2a;
397
+ color: white;
398
+ padding: 10px;
399
+ }
400
+
401
+ .voice-slider-container {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 15px;
405
+ }
406
+
407
+ .voice-slider {
408
+ flex: 1;
409
+ height: 6px;
410
+ background: rgba(255, 255, 255, 0.1);
411
+ border-radius: 3px;
412
+ outline: none;
413
+ -webkit-appearance: none;
414
+ }
415
+
416
+ .voice-slider::-webkit-slider-thumb {
417
+ -webkit-appearance: none;
418
+ width: 20px;
419
+ height: 20px;
420
+ background: #5a6cff;
421
+ border-radius: 50%;
422
+ cursor: pointer;
423
+ transition: all 0.2s;
424
+ }
425
+
426
+ .voice-slider::-webkit-slider-thumb:hover {
427
+ background: #7a8aff;
428
+ transform: scale(1.1);
429
+ }
430
+
431
+ .voice-value {
432
+ min-width: 40px;
433
+ text-align: center;
434
+ font-family: 'Courier New', monospace;
435
+ color: #5a6cff;
436
+ font-weight: 500;
437
+ font-size: 0.9em;
438
+ }
439
+
440
+ .voice-control-buttons {
441
+ display: grid;
442
+ grid-template-columns: 1fr 1fr;
443
+ gap: 10px;
444
+ margin-top: 25px;
445
+ }
446
+
447
+ .voice-btn {
448
+ padding: 12px;
449
+ background: rgba(90, 108, 255, 0.1);
450
+ border: 1px solid rgba(90, 108, 255, 0.3);
451
+ color: #a0b0ff;
452
+ border-radius: 10px;
453
+ cursor: pointer;
454
+ transition: all 0.3s ease;
455
+ display: flex;
456
+ align-items: center;
457
+ justify-content: center;
458
+ gap: 8px;
459
+ font-size: 0.9em;
460
+ }
461
+
462
+ .voice-btn:hover {
463
+ background: rgba(90, 108, 255, 0.2);
464
+ transform: translateY(-2px);
465
+ }
466
+
467
+ .voice-btn.active {
468
+ background: rgba(90, 108, 255, 0.3);
469
+ color: white;
470
+ }
471
+
472
+ .voice-btn.stop {
473
+ background: rgba(255, 90, 90, 0.1);
474
+ border-color: rgba(255, 90, 90, 0.3);
475
+ color: #ff9d9d;
476
+ }
477
+
478
+ .voice-btn.stop:hover {
479
+ background: rgba(255, 90, 90, 0.2);
480
+ }
481
+
482
+ .voice-btn.stop.active {
483
+ background: rgba(255, 90, 90, 0.3);
484
+ }
485
+
486
+ .voice-preview-btn {
487
+ width: 100%;
488
+ padding: 12px;
489
+ background: rgba(255, 255, 255, 0.07);
490
+ border: 1px solid rgba(255, 255, 255, 0.1);
491
+ border-radius: 10px;
492
+ color: #a0b0ff;
493
+ cursor: pointer;
494
+ transition: all 0.3s ease;
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: center;
498
+ gap: 8px;
499
+ margin-top: 10px;
500
+ font-size: 0.9em;
501
+ }
502
+
503
+ .voice-preview-btn:hover {
504
+ background: rgba(255, 255, 255, 0.1);
505
+ transform: translateY(-2px);
506
+ }
507
+
508
+ .voice-status {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 8px;
512
+ margin-top: 15px;
513
+ padding: 10px;
514
+ background: rgba(0, 255, 157, 0.1);
515
+ border-radius: 10px;
516
+ font-size: 0.85em;
517
+ color: #00ff9d;
518
+ border: 1px solid rgba(0, 255, 157, 0.2);
519
+ }
520
+
521
+ .voice-status i {
522
+ animation: pulse 2s infinite;
523
+ }
524
+
525
+ /* === EXISTING STYLES (PRESERVED) === */
526
+ .control-panel {
527
+ position: absolute;
528
+ bottom: 30px;
529
+ left: 30px;
530
+ background: rgba(20, 20, 30, 0.8);
531
+ backdrop-filter: blur(10px);
532
+ border-radius: 20px;
533
+ padding: 25px;
534
+ width: 300px;
535
+ pointer-events: all;
536
+ border: 1px solid rgba(255, 255, 255, 0.1);
537
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
538
+ }
539
+
540
+ .control-title {
541
+ font-size: 1.2em;
542
+ margin-bottom: 20px;
543
+ font-weight: 300;
544
+ color: #a0b0ff;
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 10px;
548
+ }
549
+
550
+ .control-group {
551
+ margin-bottom: 20px;
552
+ }
553
+
554
+ label {
555
+ display: block;
556
+ margin-bottom: 8px;
557
+ font-size: 0.9em;
558
+ color: #8892b0;
559
+ }
560
+
561
+ .slider-container {
562
+ display: flex;
563
+ align-items: center;
564
+ gap: 15px;
565
+ }
566
+
567
+ input[type="range"] {
568
+ flex: 1;
569
+ height: 6px;
570
+ background: rgba(255, 255, 255, 0.1);
571
+ border-radius: 3px;
572
+ outline: none;
573
+ -webkit-appearance: none;
574
+ }
575
+
576
+ input[type="range"]::-webkit-slider-thumb {
577
+ -webkit-appearance: none;
578
+ width: 20px;
579
+ height: 20px;
580
+ background: #5a6cff;
581
+ border-radius: 50%;
582
+ cursor: pointer;
583
+ transition: all 0.2s;
584
+ }
585
+
586
+ input[type="range"]::-webkit-slider-thumb:hover {
587
+ background: #7a8aff;
588
+ transform: scale(1.1);
589
+ }
590
+
591
+ .value-display {
592
+ min-width: 40px;
593
+ text-align: center;
594
+ font-family: 'Courier New', monospace;
595
+ color: #5a6cff;
596
+ font-weight: 500;
597
+ }
598
+
599
+ .preset-buttons {
600
+ display: grid;
601
+ grid-template-columns: repeat(3, 1fr);
602
+ gap: 10px;
603
+ margin-top: 20px;
604
+ }
605
+
606
+ .preset-btn {
607
+ padding: 10px;
608
+ background: rgba(90, 108, 255, 0.1);
609
+ border: 1px solid rgba(90, 108, 255, 0.3);
610
+ color: #a0b0ff;
611
+ border-radius: 10px;
612
+ cursor: pointer;
613
+ transition: all 0.2s;
614
+ font-size: 0.9em;
615
+ text-align: center;
616
+ }
617
+
618
+ .preset-btn:hover {
619
+ background: rgba(90, 108, 255, 0.2);
620
+ transform: translateY(-2px);
621
+ }
622
+
623
+ .status-bar {
624
+ position: absolute;
625
+ top: 30px;
626
+ left: 30px;
627
+ background: rgba(20, 20, 30, 0.8);
628
+ backdrop-filter: blur(10px);
629
+ padding: 15px 25px;
630
+ border-radius: 15px;
631
+ font-size: 0.9em;
632
+ color: #8892b0;
633
+ border: 1px solid rgba(255, 255, 255, 0.1);
634
+ }
635
+
636
+ .pulse-indicator {
637
+ display: inline-block;
638
+ width: 12px;
639
+ height: 12px;
640
+ background: #00ff9d;
641
+ border-radius: 50%;
642
+ margin-right: 10px;
643
+ animation: pulse 2s infinite;
644
+ }
645
+
646
+ @keyframes pulse {
647
+ 0%, 100% { opacity: 1; }
648
+ 50% { opacity: 0.3; }
649
+ }
650
+
651
+ .loading-screen {
652
+ position: absolute;
653
+ top: 0;
654
+ left: 0;
655
+ width: 100%;
656
+ height: 100%;
657
+ background: #0a0a0f;
658
+ display: flex;
659
+ flex-direction: column;
660
+ justify-content: center;
661
+ align-items: center;
662
+ z-index: 1000;
663
+ }
664
+
665
+ .spinner {
666
+ width: 60px;
667
+ height: 60px;
668
+ border: 3px solid rgba(90, 108, 255, 0.3);
669
+ border-top-color: #5a6cff;
670
+ border-radius: 50%;
671
+ animation: spin 1s linear infinite;
672
+ margin-bottom: 20px;
673
+ }
674
+
675
+ @keyframes spin {
676
+ to { transform: rotate(360deg); }
677
+ }
678
+
679
+ .error-screen {
680
+ position: absolute;
681
+ top: 0;
682
+ left: 0;
683
+ width: 100%;
684
+ height: 100%;
685
+ background: #0a0a0f;
686
+ display: none;
687
+ flex-direction: column;
688
+ justify-content: center;
689
+ align-items: center;
690
+ z-index: 1001;
691
+ text-align: center;
692
+ padding: 20px;
693
+ }
694
+
695
+ .error-screen h2 {
696
+ color: #ff5a5a;
697
+ margin-bottom: 20px;
698
+ }
699
+
700
+ .error-screen p {
701
+ color: #8892b0;
702
+ margin-bottom: 30px;
703
+ max-width: 500px;
704
+ }
705
+
706
+ .retry-btn {
707
+ padding: 12px 30px;
708
+ background: #5a6cff;
709
+ color: white;
710
+ border: none;
711
+ border-radius: 10px;
712
+ cursor: pointer;
713
+ font-size: 1em;
714
+ transition: all 0.2s;
715
+ }
716
+
717
+ .retry-btn:hover {
718
+ background: #7a8aff;
719
+ transform: translateY(-2px);
720
+ }
721
+
722
+ /* Responsive adjustments */
723
+ @media (max-width: 1400px) {
724
+ .voice-controls {
725
+ right: 30px;
726
+ bottom: 400px;
727
+ }
728
+
729
+ .chat-interface {
730
+ width: 400px;
731
+ }
732
+ }
733
+
734
+ @media (max-height: 800px) {
735
+ .chat-interface {
736
+ height: calc(100vh - 80px);
737
+ }
738
+
739
+ .voice-controls {
740
+ padding: 20px;
741
+ width: 260px;
742
+ }
743
+ }
744
+ </style>
745
+ </head>
746
+ <body>
747
+ <div id="canvasContainer">
748
+ <canvas id="mainCanvas"></canvas>
749
+
750
+ <!-- Existing UI Overlay -->
751
+ <div class="ui-overlay">
752
+ <div class="status-bar">
753
+ <span class="pulse-indicator"></span>
754
+ <span id="statusText">AI Tutor System Ready</span>
755
+ </div>
756
+ </div>
757
+
758
+ <!-- Voice Controls Panel -->
759
+ <div class="voice-controls">
760
+ <div class="voice-header">
761
+ <i class="fas fa-robot"></i>
762
+ <span>Voice Synthesis</span>
763
+ </div>
764
+
765
+ <div class="voice-control-group">
766
+ <label class="voice-label">
767
+ <i class="fas fa-user-circle"></i>
768
+ Voice Selection
769
+ </label>
770
+ <select class="voice-select" id="voiceSelect">
771
+ <option value="">Loading voices...</option>
772
+ </select>
773
+ </div>
774
+
775
+ <div class="voice-control-group">
776
+ <label class="voice-label">
777
+ <i class="fas fa-tachometer-alt"></i>
778
+ Speech Rate
779
+ </label>
780
+ <div class="voice-slider-container">
781
+ <input type="range" class="voice-slider" id="rateSlider" min="0.5" max="2" step="0.1" value="1">
782
+ <span class="voice-value" id="rateValue">1.0x</span>
783
+ </div>
784
+ </div>
785
+
786
+ <div class="voice-control-group">
787
+ <label class="voice-label">
788
+ <i class="fas fa-wave-square"></i>
789
+ Pitch Variation
790
+ </label>
791
+ <div class="voice-slider-container">
792
+ <input type="range" class="voice-slider" id="pitchSlider" min="0.5" max="2" step="0.1" value="1">
793
+ <span class="voice-value" id="pitchValue">1.0</span>
794
+ </div>
795
+ </div>
796
+
797
+ <div class="voice-control-group">
798
+ <label class="voice-label">
799
+ <i class="fas fa-volume-up"></i>
800
+ Volume Level
801
+ </label>
802
+ <div class="voice-slider-container">
803
+ <input type="range" class="voice-slider" id="volumeSlider" min="0.1" max="1" step="0.1" value="0.8">
804
+ <span class="voice-value" id="volumeValue">80%</span>
805
+ </div>
806
+ </div>
807
+
808
+ <div class="voice-control-buttons">
809
+ <button class="voice-btn" id="autoSpeakBtn" data-enabled="true">
810
+ <i class="fas fa-bullhorn"></i>
811
+ Auto-Speak
812
+ </button>
813
+ <button class="voice-btn stop" id="stopBtn">
814
+ <i class="fas fa-stop"></i>
815
+ Stop
816
+ </button>
817
+ </div>
818
+
819
+ <button class="voice-preview-btn" id="previewBtn">
820
+ <i class="fas fa-play"></i>
821
+ Preview Voice
822
+ </button>
823
+
824
+ <div class="voice-status" id="voiceStatus" style="display: none;">
825
+ <i class="fas fa-comment-dots"></i>
826
+ <span id="statusMessage">Speaking...</span>
827
+ </div>
828
+ </div>
829
+
830
+ <!-- Chat Interface -->
831
+ <div class="chat-interface">
832
+ <div class="chat-header">
833
+ <div class="chat-title">
834
+ <i class="fas fa-robot"></i>
835
+ AI Tutor Assistant
836
+ </div>
837
+ <div class="chat-status">
838
+ <span class="status-indicator"></span>
839
+ <span id="chatStatusText">Online</span>
840
+ </div>
841
+ </div>
842
+
843
+ <div class="chat-container">
844
+ <div class="chat-messages" id="chatMessages">
845
+ <!-- Messages will be dynamically added here -->
846
+ <div class="message message-ai">
847
+ <div class="message-content">
848
+ Hello! I'm your AI Tutor. I can help explain concepts, answer questions, and guide your learning. How can I assist you today?
849
+ </div>
850
+ <div class="message-time">Just now</div>
851
+ </div>
852
+
853
+ <div class="message message-ai">
854
+ <div class="message-content">
855
+ You can type your questions or click the microphone icon to speak. I'll respond with explanations, examples, and follow-up questions to enhance your understanding.
856
+ </div>
857
+ <div class="message-time">Just now</div>
858
+ </div>
859
+
860
+ <!-- Typing indicator (hidden by default) -->
861
+ <div class="message-typing" id="typingIndicator" style="display: none;">
862
+ <div class="typing-dot"></div>
863
+ <div class="typing-dot"></div>
864
+ <div class="typing-dot"></div>
865
+ </div>
866
+ </div>
867
+
868
+ <div class="chat-input-container">
869
+ <div class="input-wrapper">
870
+ <textarea
871
+ class="chat-input"
872
+ id="chatInput"
873
+ placeholder="Type your question or click the microphone to speak..."
874
+ rows="1"
875
+ ></textarea>
876
+
877
+ <button class="voice-input-btn" id="voiceBtn" title="Start voice input">
878
+ <i class="fas fa-microphone"></i>
879
+ </button>
880
+
881
+ <button class="send-btn" id="sendBtn" title="Send message">
882
+ <i class="fas fa-paper-plane"></i>
883
+ </button>
884
+ </div>
885
+
886
+ <div class="voice-visualizer" id="voiceVisualizer">
887
+ <div class="voice-bar"></div>
888
+ <div class="voice-bar"></div>
889
+ <div class="voice-bar"></div>
890
+ <div class="voice-bar"></div>
891
+ <div class="voice-bar"></div>
892
+ <div class="voice-bar"></div>
893
+ <div class="voice-bar"></div>
894
+ <div class="voice-bar"></div>
895
+ <div class="voice-bar"></div>
896
+ <div class="voice-bar"></div>
897
+ </div>
898
+ </div>
899
+ </div>
900
+ </div>
901
+
902
+ <!-- Existing Control Panel -->
903
+ <div class="control-panel">
904
+ <div class="control-title">
905
+ <span>🧠 AI Tutor Interface</span>
906
+ </div>
907
+
908
+ <div class="control-group">
909
+ <label>Animation Intensity</label>
910
+ <div class="slider-container">
911
+ <input type="range" id="intensity" min="0" max="100" value="50">
912
+ <span class="value-display" id="intensityValue">50</span>
913
+ </div>
914
+ </div>
915
+
916
+ <div class="control-group">
917
+ <label>Particle Count</label>
918
+ <div class="slider-container">
919
+ <input type="range" id="particleCount" min="100" max="5000" value="2000" step="100">
920
+ <span class="value-display" id="particleCountValue">2000</span>
921
+ </div>
922
+ </div>
923
+
924
+ <div class="control-group">
925
+ <label>Energy Level</label>
926
+ <div class="slider-container">
927
+ <input type="range" id="energy" min="0" max="100" value="30">
928
+ <span class="value-display" id="energyValue">30</span>
929
+ </div>
930
+ </div>
931
+
932
+ <div class="preset-buttons">
933
+ <button class="preset-btn" data-preset="listening">🎤 Listening</button>
934
+ <button class="preset-btn" data-preset="processing">⚡ Processing</button>
935
+ <button class="preset-btn" data-preset="responding">💬 Responding</button>
936
+ <button class="preset-btn" data-preset="exploring">🔍 Exploring</button>
937
+ <button class="preset-btn" data-preset="teaching">📚 Teaching</button>
938
+ <button class="preset-btn" data-preset="idle">🌀 Idle</button>
939
+ </div>
940
+ </div>
941
+
942
+ <!-- Existing Loading Screen -->
943
+ <div class="loading-screen" id="loadingScreen">
944
+ <div class="spinner"></div>
945
+ <div id="loadingText">Initializing AI Interface...</div>
946
+ </div>
947
+
948
+ <!-- Existing Error Screen -->
949
+ <div class="error-screen" id="errorScreen">
950
+ <h2>⚠️ Initialization Failed</h2>
951
+ <p id="errorMessage">Three.js library failed to load. Please check your internet connection or try again.</p>
952
+ <button class="retry-btn" id="retryBtn">Retry</button>
953
+ </div>
954
+ </div>
955
+
956
+ <script>
957
+ // Check if Three.js loaded properly
958
+ function checkThreeJS() {
959
+ if (typeof THREE === 'undefined') {
960
+ console.error('Three.js failed to load');
961
+ showError('Three.js library failed to load. Please check your internet connection.');
962
+ return false;
963
+ }
964
+ return true;
965
+ }
966
+
967
+ function showError(message) {
968
+ const errorScreen = document.getElementById('errorScreen');
969
+ const errorMessage = document.getElementById('errorMessage');
970
+ const loadingScreen = document.getElementById('loadingScreen');
971
+
972
+ loadingScreen.style.display = 'none';
973
+ errorMessage.textContent = message;
974
+ errorScreen.style.display = 'flex';
975
+ }
976
+
977
+ function hideLoading() {
978
+ const loadingScreen = document.getElementById('loadingScreen');
979
+ loadingScreen.style.opacity = '0';
980
+ setTimeout(() => {
981
+ loadingScreen.style.display = 'none';
982
+ }, 500);
983
+ }
984
+
985
+ function updateStatus(text) {
986
+ document.getElementById('statusText').textContent = text;
987
+ document.getElementById('loadingText').textContent = text;
988
+ }
989
+
990
+ // Voice Synthesis System
991
+ class VoiceSynthesis {
992
+ constructor() {
993
+ this.synth = window.speechSynthesis;
994
+ this.voices = [];
995
+ this.currentVoice = null;
996
+ this.isAutoSpeak = true;
997
+ this.isSpeaking = false;
998
+ this.currentUtterance = null;
999
+
1000
+ this.settings = {
1001
+ rate: 1.0,
1002
+ pitch: 1.0,
1003
+ volume: 0.8
1004
+ };
1005
+
1006
+ // Hardcoded longer, simpler test script for voice testing
1007
+ this.testScript = "Hello there! This is a test to see how natural and fluid my voice sounds. I'm going to tell you a little story about learning and discovery. Imagine you're walking through a beautiful forest on a sunny day. The light filters through the leaves, creating patterns on the ground. Birds are singing in the trees, and there's a gentle breeze carrying the scent of flowers. You come across a small stream, and you sit on a rock to listen to the water flowing. It's in moments like these that we often have our best ideas and clearest thoughts. Learning is like this forest walk - it's a journey of discovery where each step reveals something new and wonderful. The key is to stay curious, keep exploring, and enjoy the process. Now, how does my voice sound to you? Is it clear, natural, and pleasant to listen to?";
1008
+
1009
+ this.initVoices();
1010
+ this.setupEventListeners();
1011
+ }
1012
+
1013
+ initVoices() {
1014
+ // Wait for voices to be loaded
1015
+ const loadVoices = () => {
1016
+ this.voices = this.synth.getVoices();
1017
+ this.populateVoiceList();
1018
+
1019
+ // Select a high-quality natural-sounding English voice
1020
+ this.selectBestVoice();
1021
+ };
1022
+
1023
+ if (this.synth.onvoiceschanged !== undefined) {
1024
+ this.synth.onvoiceschanged = loadVoices;
1025
+ }
1026
+
1027
+ // Load immediately if voices are already available
1028
+ if (this.synth.getVoices().length > 0) {
1029
+ loadVoices();
1030
+ }
1031
+ }
1032
+
1033
+ selectBestVoice() {
1034
+ // Filter for English voices only
1035
+ const englishVoices = this.voices.filter(voice =>
1036
+ voice.lang.startsWith('en-')
1037
+ );
1038
+
1039
+ if (englishVoices.length === 0) {
1040
+ console.warn('No English voices found');
1041
+ return;
1042
+ }
1043
+
1044
+ // Prioritize natural-sounding voices (Google, Microsoft, Apple premium voices)
1045
+ const preferredVoices = [
1046
+ 'Google UK English Male',
1047
+ 'Google US English',
1048
+ 'Microsoft David - English (United States)',
1049
+ 'Microsoft Zira - English (United States)',
1050
+ 'Alex',
1051
+ 'Samantha',
1052
+ 'Daniel',
1053
+ 'Karen',
1054
+ 'Moira',
1055
+ 'Tessa',
1056
+ 'Fred',
1057
+ 'Victoria'
1058
+ ];
1059
+
1060
+ // Try to find a preferred voice
1061
+ for (const voiceName of preferredVoices) {
1062
+ const voice = englishVoices.find(v => v.name.includes(voiceName));
1063
+ if (voice) {
1064
+ this.currentVoice = voice;
1065
+ this.updateVoiceSelect(voice);
1066
+ this.showStatus(`Selected: ${voice.name}`, 3000);
1067
+ break;
1068
+ }
1069
+ }
1070
+
1071
+ // Fallback to first English voice
1072
+ if (!this.currentVoice && englishVoices.length > 0) {
1073
+ this.currentVoice = englishVoices[0];
1074
+ this.updateVoiceSelect(englishVoices[0]);
1075
+ this.showStatus(`Selected: ${englishVoices[0].name}`, 3000);
1076
+ }
1077
+ }
1078
+
1079
+ populateVoiceList() {
1080
+ const voiceSelect = document.getElementById('voiceSelect');
1081
+ voiceSelect.innerHTML = '';
1082
+
1083
+ // Filter for English voices only
1084
+ const englishVoices = this.voices.filter(voice =>
1085
+ voice.lang.startsWith('en-')
1086
+ );
1087
+
1088
+ if (englishVoices.length === 0) {
1089
+ const option = document.createElement('option');
1090
+ option.textContent = 'No English voices found';
1091
+ option.disabled = true;
1092
+ voiceSelect.appendChild(option);
1093
+ return;
1094
+ }
1095
+
1096
+ // Sort English voices by dialect preference
1097
+ const sortedVoices = englishVoices.sort((a, b) => {
1098
+ const langOrder = ['en-US', 'en-GB', 'en-AU', 'en-CA', 'en-IN'];
1099
+ const indexA = langOrder.indexOf(a.lang);
1100
+ const indexB = langOrder.indexOf(b.lang);
1101
+
1102
+ if (indexA !== -1 && indexB !== -1) return indexA - indexB;
1103
+ if (indexA !== -1) return -1;
1104
+ if (indexB !== -1) return 1;
1105
+ return a.name.localeCompare(b.name);
1106
+ });
1107
+
1108
+ // Add voices to select
1109
+ sortedVoices.forEach(voice => {
1110
+ const option = document.createElement('option');
1111
+ option.value = voice.name;
1112
+ option.textContent = `${voice.name} (${this.getDialectName(voice.lang)})`;
1113
+ option.dataset.lang = voice.lang;
1114
+ voiceSelect.appendChild(option);
1115
+ });
1116
+ }
1117
+
1118
+ getDialectName(langCode) {
1119
+ const dialects = {
1120
+ 'en-US': 'US English',
1121
+ 'en-GB': 'UK English',
1122
+ 'en-AU': 'Australian English',
1123
+ 'en-CA': 'Canadian English',
1124
+ 'en-IN': 'Indian English',
1125
+ 'en-IE': 'Irish English',
1126
+ 'en-ZA': 'South African English',
1127
+ 'en-NZ': 'New Zealand English'
1128
+ };
1129
+
1130
+ return dialects[langCode] || langCode;
1131
+ }
1132
+
1133
+ updateVoiceSelect(voice) {
1134
+ const voiceSelect = document.getElementById('voiceSelect');
1135
+ const options = voiceSelect.querySelectorAll('option');
1136
+
1137
+ options.forEach(option => {
1138
+ if (option.value === voice.name) {
1139
+ option.selected = true;
1140
+ }
1141
+ });
1142
+ }
1143
+
1144
+ setupEventListeners() {
1145
+ // Voice selection
1146
+ document.getElementById('voiceSelect').addEventListener('change', (e) => {
1147
+ const selectedVoice = this.voices.find(v => v.name === e.target.value);
1148
+ if (selectedVoice) {
1149
+ this.currentVoice = selectedVoice;
1150
+ this.showStatus(`Voice changed to: ${selectedVoice.name}`, 2000);
1151
+ }
1152
+ });
1153
+
1154
+ // Rate slider
1155
+ document.getElementById('rateSlider').addEventListener('input', (e) => {
1156
+ const value = parseFloat(e.target.value);
1157
+ this.settings.rate = value;
1158
+ document.getElementById('rateValue').textContent = value.toFixed(1) + 'x';
1159
+ });
1160
+
1161
+ // Pitch slider
1162
+ document.getElementById('pitchSlider').addEventListener('input', (e) => {
1163
+ const value = parseFloat(e.target.value);
1164
+ this.settings.pitch = value;
1165
+ document.getElementById('pitchValue').textContent = value.toFixed(1);
1166
+ });
1167
+
1168
+ // Volume slider
1169
+ document.getElementById('volumeSlider').addEventListener('input', (e) => {
1170
+ const value = parseFloat(e.target.value);
1171
+ this.settings.volume = value;
1172
+ document.getElementById('volumeValue').textContent = Math.round(value * 100) + '%';
1173
+ });
1174
+
1175
+ // Auto-speak toggle
1176
+ document.getElementById('autoSpeakBtn').addEventListener('click', (e) => {
1177
+ this.isAutoSpeak = !this.isAutoSpeak;
1178
+ const btn = e.target.closest('.voice-btn');
1179
+ btn.dataset.enabled = this.isAutoSpeak;
1180
+ btn.innerHTML = this.isAutoSpeak
1181
+ ? '<i class="fas fa-bullhorn"></i> Auto-Speak'
1182
+ : '<i class="fas fa-volume-mute"></i> Auto-Speak';
1183
+
1184
+ btn.classList.toggle('active', this.isAutoSpeak);
1185
+
1186
+ // Show status message
1187
+ this.showStatus(this.isAutoSpeak ? 'Auto-speak enabled' : 'Auto-speak disabled', 2000);
1188
+ });
1189
+
1190
+ // Stop button
1191
+ document.getElementById('stopBtn').addEventListener('click', () => {
1192
+ this.stopSpeaking();
1193
+ });
1194
+
1195
+ // Preview button - uses the hardcoded test script
1196
+ document.getElementById('previewBtn').addEventListener('click', () => {
1197
+ this.previewCurrentVoice();
1198
+ });
1199
+
1200
+ // Update status displays when speaking
1201
+ this.synth.onstart = () => {
1202
+ this.isSpeaking = true;
1203
+ document.getElementById('stopBtn').classList.add('active');
1204
+ this.showStatus('Speaking...');
1205
+
1206
+ // Update 3D visualization
1207
+ if (window.visualization) {
1208
+ window.visualization.setPreset('teaching');
1209
+ }
1210
+ };
1211
+
1212
+ this.synth.onend = () => {
1213
+ this.isSpeaking = false;
1214
+ document.getElementById('stopBtn').classList.remove('active');
1215
+ this.hideStatus();
1216
+
1217
+ // Update 3D visualization
1218
+ if (window.visualization) {
1219
+ setTimeout(() => {
1220
+ window.visualization.setPreset('idle');
1221
+ }, 1000);
1222
+ }
1223
+ };
1224
+ }
1225
+
1226
+ speak(text) {
1227
+ if (!this.isAutoSpeak || !text || this.isSpeaking) return;
1228
+
1229
+ this.stopSpeaking();
1230
+
1231
+ const utterance = new SpeechSynthesisUtterance(text);
1232
+
1233
+ if (this.currentVoice) {
1234
+ utterance.voice = this.currentVoice;
1235
+ }
1236
+
1237
+ utterance.rate = this.settings.rate;
1238
+ utterance.pitch = this.settings.pitch;
1239
+ utterance.volume = this.settings.volume;
1240
+
1241
+ // Add natural pauses for better cadence
1242
+ utterance.text = this.addNaturalPauses(text);
1243
+
1244
+ // Event listeners for this utterance
1245
+ utterance.onerror = (event) => {
1246
+ console.error('Speech synthesis error:', event);
1247
+ this.hideStatus();
1248
+ };
1249
+
1250
+ this.currentUtterance = utterance;
1251
+ this.synth.speak(utterance);
1252
+ }
1253
+
1254
+ addNaturalPauses(text) {
1255
+ // Add slight pauses for commas and periods for more natural speech
1256
+ return text
1257
+ .replace(/,/g, ',<break time="200ms"/>')
1258
+ .replace(/\./g, '.<break time="300ms"/>')
1259
+ .replace(/\?/g, '?<break time="400ms"/>')
1260
+ .replace(/\!/g, '!<break time="400ms"/>');
1261
+ }
1262
+
1263
+ stopSpeaking() {
1264
+ if (this.isSpeaking) {
1265
+ this.synth.cancel();
1266
+ this.isSpeaking = false;
1267
+ this.currentUtterance = null;
1268
+ document.getElementById('stopBtn').classList.remove('active');
1269
+ this.hideStatus();
1270
+ }
1271
+ }
1272
+
1273
+ previewCurrentVoice() {
1274
+ // Always use the hardcoded test script for voice testing
1275
+ this.speak(this.testScript);
1276
+ this.showStatus('Testing voice with story script...', 3000);
1277
+ }
1278
+
1279
+ showStatus(message, duration = null) {
1280
+ const statusEl = document.getElementById('voiceStatus');
1281
+ const messageEl = document.getElementById('statusMessage');
1282
+
1283
+ messageEl.textContent = message;
1284
+ statusEl.style.display = 'flex';
1285
+
1286
+ if (duration) {
1287
+ setTimeout(() => {
1288
+ this.hideStatus();
1289
+ }, duration);
1290
+ }
1291
+ }
1292
+
1293
+ hideStatus() {
1294
+ const statusEl = document.getElementById('voiceStatus');
1295
+ statusEl.style.display = 'none';
1296
+ }
1297
+
1298
+ updateSettingsFromUI() {
1299
+ this.settings.rate = parseFloat(document.getElementById('rateSlider').value);
1300
+ this.settings.pitch = parseFloat(document.getElementById('pitchSlider').value);
1301
+ this.settings.volume = parseFloat(document.getElementById('volumeSlider').value);
1302
+ }
1303
+ }
1304
+
1305
+ // Chat Interface Functionality
1306
+ class ChatInterface {
1307
+ constructor() {
1308
+ this.isListening = false;
1309
+ this.voiceAnimationInterval = null;
1310
+ this.initChat();
1311
+ }
1312
+
1313
+ initChat() {
1314
+ this.chatInput = document.getElementById('chatInput');
1315
+ this.chatMessages = document.getElementById('chatMessages');
1316
+ this.voiceBtn = document.getElementById('voiceBtn');
1317
+ this.sendBtn = document.getElementById('sendBtn');
1318
+ this.voiceVisualizer = document.getElementById('voiceVisualizer');
1319
+ this.typingIndicator = document.getElementById('typingIndicator');
1320
+ this.chatStatusText = document.getElementById('chatStatusText');
1321
+
1322
+ this.setupEventListeners();
1323
+ this.autoResizeTextarea();
1324
+ }
1325
+
1326
+ setupEventListeners() {
1327
+ // Send button click
1328
+ this.sendBtn.addEventListener('click', () => this.sendMessage());
1329
+
1330
+ // Enter key to send (Shift+Enter for new line)
1331
+ this.chatInput.addEventListener('keydown', (e) => {
1332
+ if (e.key === 'Enter' && !e.shiftKey) {
1333
+ e.preventDefault();
1334
+ this.sendMessage();
1335
+ }
1336
+ });
1337
+
1338
+ // Voice button click
1339
+ this.voiceBtn.addEventListener('click', () => this.toggleVoiceInput());
1340
+
1341
+ // Auto-resize textarea
1342
+ this.chatInput.addEventListener('input', () => this.autoResizeTextarea());
1343
+ }
1344
+
1345
+ autoResizeTextarea() {
1346
+ const textarea = this.chatInput;
1347
+ textarea.style.height = 'auto';
1348
+ const newHeight = Math.min(textarea.scrollHeight, 120);
1349
+ textarea.style.height = newHeight + 'px';
1350
+ }
1351
+
1352
+ toggleVoiceInput() {
1353
+ this.isListening = !this.isListening;
1354
+
1355
+ if (this.isListening) {
1356
+ // Start "listening" state
1357
+ this.voiceBtn.classList.add('listening');
1358
+ this.voiceBtn.innerHTML = '<i class="fas fa-stop"></i>';
1359
+ this.voiceVisualizer.classList.add('active');
1360
+ this.chatStatusText.textContent = 'Listening...';
1361
+ this.startVoiceAnimation();
1362
+
1363
+ // Update 3D visualization to listening mode
1364
+ if (window.visualization) {
1365
+ window.visualization.setPreset('listening');
1366
+ }
1367
+
1368
+ // Simulate voice input (in a real app, this would capture actual audio)
1369
+ setTimeout(() => {
1370
+ this.simulateVoiceInput();
1371
+ }, 1500);
1372
+ } else {
1373
+ // Stop "listening" state
1374
+ this.voiceBtn.classList.remove('listening');
1375
+ this.voiceBtn.innerHTML = '<i class="fas fa-microphone"></i>';
1376
+ this.voiceVisualizer.classList.remove('active');
1377
+ this.chatStatusText.textContent = 'Online';
1378
+ this.stopVoiceAnimation();
1379
+
1380
+ // Update 3D visualization to processing mode
1381
+ if (window.visualization) {
1382
+ window.visualization.setPreset('processing');
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ startVoiceAnimation() {
1388
+ const bars = this.voiceVisualizer.querySelectorAll('.voice-bar');
1389
+ bars.forEach(bar => bar.classList.add('listening'));
1390
+
1391
+ this.voiceAnimationInterval = setInterval(() => {
1392
+ bars.forEach(bar => {
1393
+ const randomHeight = 10 + Math.random() * 30;
1394
+ bar.style.height = `${randomHeight}px`;
1395
+ });
1396
+ }, 100);
1397
+ }
1398
+
1399
+ stopVoiceAnimation() {
1400
+ if (this.voiceAnimationInterval) {
1401
+ clearInterval(this.voiceAnimationInterval);
1402
+ this.voiceAnimationInterval = null;
1403
+
1404
+ const bars = this.voiceVisualizer.querySelectorAll('.voice-bar');
1405
+ bars.forEach(bar => {
1406
+ bar.classList.remove('listening');
1407
+ bar.style.height = '10px';
1408
+ });
1409
+ }
1410
+ }
1411
+
1412
+ simulateVoiceInput() {
1413
+ if (!this.isListening) return;
1414
+
1415
+ // Simulate capturing voice input
1416
+ const simulatedQuestions = [
1417
+ "Can you explain quantum computing?",
1418
+ "How do neural networks learn?",
1419
+ "What's the difference between AI and machine learning?",
1420
+ "Help me understand blockchain technology",
1421
+ "Explain the concept of derivatives in calculus"
1422
+ ];
1423
+
1424
+ const randomQuestion = simulatedQuestions[Math.floor(Math.random() * simulatedQuestions.length)];
1425
+
1426
+ // Add user message
1427
+ this.addMessage(randomQuestion, 'user');
1428
+
1429
+ // Process and respond
1430
+ this.processAIResponse(randomQuestion);
1431
+
1432
+ // Stop listening after receiving input
1433
+ setTimeout(() => {
1434
+ this.isListening = false;
1435
+ this.voiceBtn.classList.remove('listening');
1436
+ this.voiceBtn.innerHTML = '<i class="fas fa-microphone"></i>';
1437
+ this.voiceVisualizer.classList.remove('active');
1438
+ this.stopVoiceAnimation();
1439
+ this.chatStatusText.textContent = 'Online';
1440
+ }, 500);
1441
+ }
1442
+
1443
+ sendMessage() {
1444
+ const message = this.chatInput.value.trim();
1445
+ if (message === '') return;
1446
+
1447
+ // Add user message
1448
+ this.addMessage(message, 'user');
1449
+
1450
+ // Clear input
1451
+ this.chatInput.value = '';
1452
+ this.autoResizeTextarea();
1453
+
1454
+ // Process and respond
1455
+ this.processAIResponse(message);
1456
+ }
1457
+
1458
+ addMessage(content, sender) {
1459
+ const messageDiv = document.createElement('div');
1460
+ messageDiv.className = `message message-${sender}`;
1461
+
1462
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1463
+
1464
+ messageDiv.innerHTML = `
1465
+ <div class="message-content">${this.escapeHtml(content)}</div>
1466
+ <div class="message-time">${time}</div>
1467
+ `;
1468
+
1469
+ this.chatMessages.appendChild(messageDiv);
1470
+
1471
+ // Scroll to bottom
1472
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
1473
+
1474
+ // Update 3D visualization
1475
+ if (window.visualization) {
1476
+ if (sender === 'user') {
1477
+ window.visualization.setPreset('processing');
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ escapeHtml(text) {
1483
+ const div = document.createElement('div');
1484
+ div.textContent = text;
1485
+ return div.innerHTML;
1486
+ }
1487
+
1488
+ processAIResponse(userMessage) {
1489
+ // Show typing indicator
1490
+ this.showTypingIndicator();
1491
+
1492
+ // Update 3D visualization
1493
+ if (window.visualization) {
1494
+ window.visualization.setPreset('processing');
1495
+ }
1496
+
1497
+ // Simulate AI processing time
1498
+ const processingTime = 1000 + Math.random() * 2000;
1499
+
1500
+ setTimeout(() => {
1501
+ this.hideTypingIndicator();
1502
+
1503
+ // Generate AI response
1504
+ const response = this.generateAIResponse(userMessage);
1505
+
1506
+ // Add AI response
1507
+ this.addMessage(response, 'ai');
1508
+
1509
+ // Speak the response if voice synthesis is available
1510
+ if (window.voiceSynthesis) {
1511
+ setTimeout(() => {
1512
+ window.voiceSynthesis.speak(response);
1513
+ }, 300);
1514
+ }
1515
+
1516
+ // Update 3D visualization
1517
+ if (window.visualization) {
1518
+ window.visualization.setPreset('responding');
1519
+ }
1520
+
1521
+ // Return to idle after a delay
1522
+ setTimeout(() => {
1523
+ if (window.visualization) {
1524
+ window.visualization.setPreset('idle');
1525
+ }
1526
+ }, 3000);
1527
+ }, processingTime);
1528
+ }
1529
+
1530
+ showTypingIndicator() {
1531
+ this.typingIndicator.style.display = 'flex';
1532
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
1533
+ }
1534
+
1535
+ hideTypingIndicator() {
1536
+ this.typingIndicator.style.display = 'none';
1537
+ }
1538
+
1539
+ generateAIResponse(userMessage) {
1540
+ // Simple AI response generator
1541
+ const responses = {
1542
+ quantum: "Quantum computing leverages quantum mechanics to process information. Unlike classical bits (0 or 1), quantum bits (qubits) can exist in superposition, enabling parallel computation. Key concepts include superposition, entanglement, and quantum interference.",
1543
+ neural: "Neural networks learn through backpropagation and gradient descent. They adjust weights based on prediction errors, minimizing loss functions. Deep learning uses multiple layers to extract hierarchical features from data.",
1544
+ blockchain: "Blockchain is a decentralized, distributed ledger technology. Each block contains cryptographic hashes linking to previous blocks, ensuring immutability. Smart contracts enable self-executing agreements on blockchain networks.",
1545
+ calculus: "Derivatives measure instantaneous rate of change. The derivative of f(x) at point a is the slope of the tangent line. Differentiation rules include power, product, quotient, and chain rules for various functions.",
1546
+ default: "I understand you're asking about " + userMessage.substring(0, 30) + "... This is a complex topic that requires careful explanation. Could you specify which aspect you'd like me to focus on? I can provide examples, applications, or fundamental principles."
1547
+ };
1548
+
1549
+ const lowerMessage = userMessage.toLowerCase();
1550
+
1551
+ if (lowerMessage.includes('quantum')) return responses.quantum;
1552
+ if (lowerMessage.includes('neural') || lowerMessage.includes('network')) return responses.neural;
1553
+ if (lowerMessage.includes('blockchain')) return responses.blockchain;
1554
+ if (lowerMessage.includes('calculus') || lowerMessage.includes('derivative')) return responses.calculus;
1555
+
1556
+ return responses.default;
1557
+ }
1558
+ }
1559
+
1560
+ // Main application
1561
+ class AITutorVisualization {
1562
+ constructor() {
1563
+ if (!checkThreeJS()) return;
1564
+
1565
+ this.scene = null;
1566
+ this.camera = null;
1567
+ this.renderer = null;
1568
+ this.particles = null;
1569
+ this.controls = null;
1570
+
1571
+ this.params = {
1572
+ intensity: 0.5,
1573
+ particleCount: 2000,
1574
+ energy: 0.3,
1575
+ mode: 'idle'
1576
+ };
1577
+
1578
+ this.audioData = new Array(32).fill(0);
1579
+ this.time = 0;
1580
+ this.animationFrameId = null;
1581
+
1582
+ try {
1583
+ this.init();
1584
+ this.createParticles();
1585
+ this.setupControls();
1586
+ this.animate();
1587
+ updateStatus('AI Interface Ready');
1588
+ setTimeout(hideLoading, 500);
1589
+ } catch (error) {
1590
+ console.error('Initialization error:', error);
1591
+ showError('Failed to initialize 3D scene: ' + error.message);
1592
+ }
1593
+ }
1594
+
1595
+ init() {
1596
+ updateStatus('Creating 3D scene...');
1597
+
1598
+ // Scene
1599
+ this.scene = new THREE.Scene();
1600
+ this.scene.background = new THREE.Color(0x0a0a0f);
1601
+
1602
+ // Camera
1603
+ this.camera = new THREE.PerspectiveCamera(
1604
+ 75,
1605
+ window.innerWidth / window.innerHeight,
1606
+ 0.1,
1607
+ 1000
1608
+ );
1609
+ this.camera.position.z = 5;
1610
+
1611
+ // Renderer
1612
+ const canvas = document.getElementById('mainCanvas');
1613
+ this.renderer = new THREE.WebGLRenderer({
1614
+ canvas: canvas,
1615
+ antialias: true,
1616
+ alpha: true
1617
+ });
1618
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1619
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1620
+
1621
+ // Add fog
1622
+ this.scene.fog = new THREE.Fog(0x0a0a0f, 10, 25);
1623
+
1624
+ // Add lights
1625
+ const ambientLight = new THREE.AmbientLight(0x222244, 0.5);
1626
+ this.scene.add(ambientLight);
1627
+
1628
+ const directionalLight = new THREE.DirectionalLight(0x5a6cff, 1);
1629
+ directionalLight.position.set(5, 3, 5);
1630
+ this.scene.add(directionalLight);
1631
+
1632
+ // Orbit controls
1633
+ updateStatus('Setting up controls...');
1634
+ this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
1635
+ this.controls.enableDamping = true;
1636
+ this.controls.dampingFactor = 0.05;
1637
+ }
1638
+
1639
+ createParticles() {
1640
+ updateStatus('Creating particles...');
1641
+
1642
+ const geometry = new THREE.BufferGeometry();
1643
+ const positions = new Float32Array(this.params.particleCount * 3);
1644
+ const colors = new Float32Array(this.params.particleCount * 3);
1645
+
1646
+ for (let i = 0; i < this.params.particleCount; i++) {
1647
+ const i3 = i * 3;
1648
+
1649
+ // Fibonacci sphere distribution
1650
+ const phi = Math.acos(-1 + (2 * i) / this.params.particleCount);
1651
+ const theta = Math.sqrt(this.params.particleCount * Math.PI) * phi;
1652
+
1653
+ const x = Math.cos(theta) * Math.sin(phi);
1654
+ const y = Math.sin(theta) * Math.sin(phi);
1655
+ const z = Math.cos(phi);
1656
+
1657
+ positions[i3] = x;
1658
+ positions[i3 + 1] = y;
1659
+ positions[i3 + 2] = z;
1660
+
1661
+ // Color gradient based on position
1662
+ colors[i3] = 0.5 + x * 0.5; // Red
1663
+ colors[i3 + 1] = 0.3 + y * 0.7; // Green
1664
+ colors[i3 + 2] = 0.8 + z * 0.2; // Blue
1665
+ }
1666
+
1667
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1668
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
1669
+
1670
+ const material = new THREE.PointsMaterial({
1671
+ size: 0.03,
1672
+ vertexColors: true,
1673
+ transparent: true,
1674
+ opacity: 0.8,
1675
+ blending: THREE.AdditiveBlending
1676
+ });
1677
+
1678
+ this.particles = new THREE.Points(geometry, material);
1679
+ this.scene.add(this.particles);
1680
+
1681
+ // Add a test sphere to verify rendering
1682
+ const testGeometry = new THREE.SphereGeometry(0.5, 32, 32);
1683
+ const testMaterial = new THREE.MeshBasicMaterial({
1684
+ color: 0x5a6cff,
1685
+ wireframe: true,
1686
+ transparent: true,
1687
+ opacity: 0.1
1688
+ });
1689
+ const testSphere = new THREE.Mesh(testGeometry, testMaterial);
1690
+ this.scene.add(testSphere);
1691
+ }
1692
+
1693
+ setupControls() {
1694
+ // Slider controls
1695
+ const intensitySlider = document.getElementById('intensity');
1696
+ const particleCountSlider = document.getElementById('particleCount');
1697
+ const energySlider = document.getElementById('energy');
1698
+
1699
+ const intensityValue = document.getElementById('intensityValue');
1700
+ const particleCountValue = document.getElementById('particleCountValue');
1701
+ const energyValue = document.getElementById('energyValue');
1702
+
1703
+ intensitySlider.addEventListener('input', (e) => {
1704
+ this.params.intensity = e.target.value / 100;
1705
+ intensityValue.textContent = e.target.value;
1706
+ });
1707
+
1708
+ particleCountSlider.addEventListener('input', (e) => {
1709
+ this.params.particleCount = parseInt(e.target.value);
1710
+ particleCountValue.textContent = e.target.value;
1711
+ this.updateParticleCount();
1712
+ });
1713
+
1714
+ energySlider.addEventListener('input', (e) => {
1715
+ this.params.energy = e.target.value / 100;
1716
+ energyValue.textContent = e.target.value;
1717
+ });
1718
+
1719
+ // Preset buttons
1720
+ document.querySelectorAll('.preset-btn').forEach(btn => {
1721
+ btn.addEventListener('click', (e) => {
1722
+ const preset = e.target.dataset.preset;
1723
+ this.setPreset(preset);
1724
+ });
1725
+ });
1726
+
1727
+ // Retry button
1728
+ document.getElementById('retryBtn').addEventListener('click', () => {
1729
+ location.reload();
1730
+ });
1731
+ }
1732
+
1733
+ setPreset(preset) {
1734
+ this.params.mode = preset;
1735
+
1736
+ // Update UI to show active preset
1737
+ document.querySelectorAll('.preset-btn').forEach(btn => {
1738
+ btn.style.background = btn.dataset.preset === preset
1739
+ ? 'rgba(90, 108, 255, 0.4)'
1740
+ : 'rgba(90, 108, 255, 0.1)';
1741
+ });
1742
+
1743
+ // Update status
1744
+ const statusMap = {
1745
+ 'listening': '🎤 Listening for input...',
1746
+ 'processing': '⚡ Processing thoughts...',
1747
+ 'responding': '💬 Formulating response...',
1748
+ 'exploring': '🔍 Exploring concepts...',
1749
+ 'teaching': '📚 Teaching mode active',
1750
+ 'idle': '🌀 AI Tutor Idle'
1751
+ };
1752
+
1753
+ if (statusMap[preset]) {
1754
+ updateStatus(statusMap[preset]);
1755
+ }
1756
+ }
1757
+
1758
+ updateParticleCount() {
1759
+ if (this.particles) {
1760
+ this.scene.remove(this.particles);
1761
+ }
1762
+ this.createParticles();
1763
+ }
1764
+
1765
+ simulateAudioData() {
1766
+ // Simulate audio data for visualization
1767
+ const time = performance.now() * 0.001;
1768
+ const frequency = 0.5 + this.params.energy * 2;
1769
+
1770
+ for (let i = 0; i < this.audioData.length; i++) {
1771
+ const base = Math.sin(time * frequency + i * 0.3) * 0.5 + 0.5;
1772
+ const variation = Math.sin(time * 2 + i * 0.5) * 0.2;
1773
+ this.audioData[i] = base + variation * this.params.intensity;
1774
+ }
1775
+ }
1776
+
1777
+ updateParticles() {
1778
+ if (!this.particles) return;
1779
+
1780
+ const positions = this.particles.geometry.attributes.position.array;
1781
+ const originalPositions = this.particles.geometry.attributes.originalPosition;
1782
+
1783
+ // Store original positions if not already stored
1784
+ if (!originalPositions) {
1785
+ const origPos = new Float32Array(positions.length);
1786
+ origPos.set(positions);
1787
+ this.particles.geometry.setAttribute('originalPosition', new THREE.BufferAttribute(origPos, 3));
1788
+ }
1789
+
1790
+ const origPositions = this.particles.geometry.attributes.originalPosition.array;
1791
+ const time = this.time;
1792
+
1793
+ for (let i = 0; i < this.params.particleCount; i++) {
1794
+ const i3 = i * 3;
1795
+ const x = origPositions[i3];
1796
+ const y = origPositions[i3 + 1];
1797
+ const z = origPositions[i3 + 2];
1798
+
1799
+ let radius = 2.0;
1800
+ let intensity = this.params.intensity;
1801
+
1802
+ // Apply different behaviors based on mode
1803
+ switch(this.params.mode) {
1804
+ case 'listening':
1805
+ radius += Math.sin(time * 3 + i * 0.01) * 0.5 * intensity;
1806
+ break;
1807
+ case 'processing':
1808
+ radius += Math.sin(time * 5 + i * 0.02) * 0.6 * intensity;
1809
+ break;
1810
+ case 'responding':
1811
+ radius += (Math.sin(time * 2 + i * 0.005) + 1) * 0.4 * intensity;
1812
+ break;
1813
+ case 'exploring':
1814
+ radius += Math.sin(time * 1.5 + i * 0.015) * 0.7 * intensity;
1815
+ break;
1816
+ case 'teaching':
1817
+ radius += Math.sin(time * 4 + i * 0.008) * 0.55 * intensity;
1818
+ break;
1819
+ default: // idle
1820
+ radius += Math.sin(time * 0.5 + i * 0.01) * 0.2 * intensity;
1821
+ }
1822
+
1823
+ // Add energy-based pulsation
1824
+ radius += Math.sin(time * 8) * 0.2 * this.params.energy;
1825
+
1826
+ // Add audio data influence
1827
+ const audioIndex = Math.floor((i / this.params.particleCount) * this.audioData.length);
1828
+ radius += this.audioData[audioIndex] * 0.5 * intensity;
1829
+
1830
+ positions[i3] = x * radius;
1831
+ positions[i3 + 1] = y * radius;
1832
+ positions[i3 + 2] = z * radius;
1833
+ }
1834
+
1835
+ this.particles.geometry.attributes.position.needsUpdate = true;
1836
+
1837
+ // Rotate particles slowly
1838
+ this.particles.rotation.y = time * 0.1;
1839
+ this.particles.rotation.x = Math.sin(time * 0.05) * 0.1;
1840
+ }
1841
+
1842
+ animate() {
1843
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
1844
+
1845
+ this.time += 0.016;
1846
+
1847
+ this.simulateAudioData();
1848
+ this.updateParticles();
1849
+
1850
+ this.controls.update();
1851
+ this.renderer.render(this.scene, this.camera);
1852
+ }
1853
+
1854
+ onWindowResize() {
1855
+ this.camera.aspect = window.innerWidth / window.innerHeight;
1856
+ this.camera.updateProjectionMatrix();
1857
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1858
+ }
1859
+
1860
+ destroy() {
1861
+ if (this.animationFrameId) {
1862
+ cancelAnimationFrame(this.animationFrameId);
1863
+ }
1864
+ if (this.controls) {
1865
+ this.controls.dispose();
1866
+ }
1867
+ if (this.renderer) {
1868
+ this.renderer.dispose();
1869
+ }
1870
+ }
1871
+ }
1872
+
1873
+ // Initialize when page loads
1874
+ window.addEventListener('load', () => {
1875
+ updateStatus('Loading AI Interface...');
1876
+
1877
+ // Wait a moment for libraries to load
1878
+ setTimeout(() => {
1879
+ // Initialize 3D visualization
1880
+ window.visualization = new AITutorVisualization();
1881
+
1882
+ // Initialize chat interface
1883
+ window.chatInterface = new ChatInterface();
1884
+
1885
+ // Initialize voice synthesis
1886
+ if ('speechSynthesis' in window) {
1887
+ window.voiceSynthesis = new VoiceSynthesis();
1888
+ updateStatus('Voice synthesis initialized');
1889
+ } else {
1890
+ console.warn('Speech synthesis not supported in this browser');
1891
+ document.getElementById('voiceStatus').style.display = 'flex';
1892
+ document.getElementById('statusMessage').textContent = 'Voice synthesis not supported';
1893
+ document.getElementById('statusMessage').style.color = '#ff5a5a';
1894
+ }
1895
+
1896
+ if (window.visualization && window.visualization.scene) {
1897
+ // Handle window resize
1898
+ window.addEventListener('resize', () => window.visualization.onWindowResize());
1899
+
1900
+ // Handle key controls
1901
+ window.addEventListener('keydown', (e) => {
1902
+ if (e.code === 'Space') {
1903
+ window.visualization.setPreset(
1904
+ window.visualization.params.mode === 'idle' ? 'listening' : 'idle'
1905
+ );
1906
+ }
1907
+ // Ctrl+Space to stop speech
1908
+ if (e.code === 'Space' && e.ctrlKey && window.voiceSynthesis) {
1909
+ window.voiceSynthesis.stopSpeaking();
1910
+ }
1911
+ });
1912
+
1913
+ // Set initial preset
1914
+ window.visualization.setPreset('idle');
1915
+ }
1916
+ }, 100);
1917
+ });
1918
+
1919
+ // Handle page unload
1920
+ window.addEventListener('beforeunload', () => {
1921
+ if (window.visualization) {
1922
+ window.visualization.destroy();
1923
+ }
1924
+ if (window.voiceSynthesis) {
1925
+ window.voiceSynthesis.stopSpeaking();
1926
+ }
1927
+ });
1928
+ </script>
1929
+ </body>
1930
+ </html>