Spaces:
Running
Running
Update index.html
Browse files- 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>
|