Spaces:
Sleeping
Sleeping
Commit ·
0b5541b
0
Parent(s):
Echo Light initial Docker Space
Browse files- .dockerignore +20 -0
- .env +1 -0
- Dockerfile +25 -0
- ai_server.py +38 -0
- app.js +1373 -0
- development_guide.md +229 -0
- index.html +276 -0
- requirements.txt +5 -0
- style.css +1647 -0
- test_echo_light_app.py +89 -0
.dockerignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .dockerignore
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.env
|
| 7 |
+
.venv/
|
| 8 |
+
build/
|
| 9 |
+
dist/
|
| 10 |
+
node_modules/
|
| 11 |
+
.DS_Store
|
| 12 |
+
*.db
|
| 13 |
+
*.sqlite3
|
| 14 |
+
selenium/
|
| 15 |
+
chromedriver*
|
| 16 |
+
*.log
|
| 17 |
+
*.swp
|
| 18 |
+
.idea/
|
| 19 |
+
.vscode/
|
| 20 |
+
.git/
|
.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
NVIDIA_API_KEY=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
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Create and switch to a new user
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
USER user
|
| 7 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 8 |
+
|
| 9 |
+
# Set work directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Copy requirements
|
| 13 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 14 |
+
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy application code
|
| 19 |
+
COPY --chown=user . /app
|
| 20 |
+
|
| 21 |
+
# Expose port for Flask
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
|
| 24 |
+
# Start the application
|
| 25 |
+
CMD ["python", "ai_server.py"]
|
ai_server.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Flask, request, jsonify
|
| 3 |
+
from flask_cors import CORS
|
| 4 |
+
import openai
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
app = Flask(__name__)
|
| 10 |
+
CORS(app)
|
| 11 |
+
|
| 12 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("NVIDIA_API_KEY")
|
| 13 |
+
openai.api_key = OPENAI_API_KEY
|
| 14 |
+
|
| 15 |
+
@app.route("/chat", methods=["POST"])
|
| 16 |
+
def chat():
|
| 17 |
+
data = request.get_json(force=True)
|
| 18 |
+
user_message = data.get("message", "")
|
| 19 |
+
if not user_message:
|
| 20 |
+
return jsonify({"reply": "Please say something!"})
|
| 21 |
+
try:
|
| 22 |
+
# Compatible with openai>=1.0.0
|
| 23 |
+
response = openai.chat.completions.create(
|
| 24 |
+
model="gpt-3.5-turbo", # or your NVIDIA LLM if available
|
| 25 |
+
messages=[{"role": "user", "content": user_message}]
|
| 26 |
+
)
|
| 27 |
+
reply = response.choices[0].message.content
|
| 28 |
+
return jsonify({"reply": reply})
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return jsonify({"reply": f"AI error: {str(e)}"})
|
| 31 |
+
|
| 32 |
+
@app.route("/")
|
| 33 |
+
def root():
|
| 34 |
+
return {"Hello": "World!", "status": "Echo Light API is running"}
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
port = int(os.environ.get("PORT", 7860))
|
| 38 |
+
app.run(host="0.0.0.0", port=port)
|
app.js
ADDED
|
@@ -0,0 +1,1373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Application Data
|
| 2 |
+
const appData = {
|
| 3 |
+
modes: [
|
| 4 |
+
{
|
| 5 |
+
id: "conversation",
|
| 6 |
+
name: "المحادثة الحرة",
|
| 7 |
+
icon: "💬",
|
| 8 |
+
description: "دردشة مفتوحة مع Echo Light لممارسة المهارات اللغوية"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
id: "vocabulary",
|
| 12 |
+
name: "بناء المفردات",
|
| 13 |
+
icon: "📚",
|
| 14 |
+
description: "نظام بطاقات تعليمية ذكي مع تكرار مباعد"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
id: "pronunciation",
|
| 18 |
+
name: "تدريب النطق",
|
| 19 |
+
icon: "🎤",
|
| 20 |
+
description: "تقييم النطق الفوري مع تعليقات مفصلة"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
id: "grammar",
|
| 24 |
+
name: "ألعاب القواعد",
|
| 25 |
+
icon: "🎯",
|
| 26 |
+
description: "تمارين تفاعلية لتعلم القواعد بطريقة ممتعة"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
id: "scenarios",
|
| 30 |
+
name: "سيناريوهات واقعية",
|
| 31 |
+
icon: "🌍",
|
| 32 |
+
description: "محاكاة مواقف حياتية مثل المطاعم والمقابلات"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: "daily",
|
| 36 |
+
name: "التحدي اليومي",
|
| 37 |
+
icon: "⭐",
|
| 38 |
+
description: "تمارين يومية قصيرة للحفاظ على استمرارية التعلم"
|
| 39 |
+
}
|
| 40 |
+
],
|
| 41 |
+
achievements: [
|
| 42 |
+
{
|
| 43 |
+
id: "first_conversation",
|
| 44 |
+
name: "المحادثة الأولى",
|
| 45 |
+
description: "أكمل أول محادثة مع Echo Light",
|
| 46 |
+
icon: "🎉"
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
id: "vocabulary_master",
|
| 50 |
+
name: "خبير المفردات",
|
| 51 |
+
description: "تعلم 50 كلمة جديدة",
|
| 52 |
+
icon: "🏆"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
id: "pronunciation_expert",
|
| 56 |
+
name: "خبير النطق",
|
| 57 |
+
description: "احصل على تقييم ممتاز في النطق 10 مرات",
|
| 58 |
+
icon: "🎯"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: "grammar_guru",
|
| 62 |
+
name: "معلم القواعد",
|
| 63 |
+
description: "أكمل جميع تمارين القواعد بنجاح",
|
| 64 |
+
icon: "📖"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: "daily_streak",
|
| 68 |
+
name: "المواظبة",
|
| 69 |
+
description: "أكمل التحدي اليومي لمدة 7 أيام متتالية",
|
| 70 |
+
icon: "🔥"
|
| 71 |
+
}
|
| 72 |
+
],
|
| 73 |
+
vocabulary_cards: [
|
| 74 |
+
{
|
| 75 |
+
word: "Hello",
|
| 76 |
+
translation: "مرحبا",
|
| 77 |
+
pronunciation: "/həˈloʊ/",
|
| 78 |
+
example: "Hello, how are you?"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
word: "Thank you",
|
| 82 |
+
translation: "شكراً لك",
|
| 83 |
+
pronunciation: "/θæŋk juː/",
|
| 84 |
+
example: "Thank you for your help."
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
word: "Beautiful",
|
| 88 |
+
translation: "جميل",
|
| 89 |
+
pronunciation: "/ˈbjuːtɪfəl/",
|
| 90 |
+
example: "The sunset is beautiful."
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
word: "Important",
|
| 94 |
+
translation: "مهم",
|
| 95 |
+
pronunciation: "/ɪmˈpɔːrtənt/",
|
| 96 |
+
example: "This is very important."
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
word: "Learning",
|
| 100 |
+
translation: "تعلم",
|
| 101 |
+
pronunciation: "/ˈlɜːrnɪŋ/",
|
| 102 |
+
example: "Learning English is fun."
|
| 103 |
+
}
|
| 104 |
+
],
|
| 105 |
+
grammar_exercises: [
|
| 106 |
+
{
|
| 107 |
+
question: "Choose the correct form: I ___ to school every day.",
|
| 108 |
+
options: ["go", "goes", "going", "went"],
|
| 109 |
+
correct: 0,
|
| 110 |
+
explanation: "نستخدم 'go' مع الضمير 'I' في المضارع البسيط"
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
question: "Complete: She ___ reading a book now.",
|
| 114 |
+
options: ["is", "are", "was", "were"],
|
| 115 |
+
correct: 0,
|
| 116 |
+
explanation: "نستخدم 'is' مع الضمير 'She' في المضارع المستمر"
|
| 117 |
+
}
|
| 118 |
+
],
|
| 119 |
+
scenarios: [
|
| 120 |
+
{
|
| 121 |
+
title: "في المطعم",
|
| 122 |
+
description: "تعلم كيفية طلب الطعام في المطعم",
|
| 123 |
+
dialogue: [
|
| 124 |
+
{
|
| 125 |
+
speaker: "waiter",
|
| 126 |
+
text: "Good evening! Welcome to our restaurant. How can I help you?"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
speaker: "customer",
|
| 130 |
+
text: "Good evening! I'd like to see the menu, please."
|
| 131 |
+
}
|
| 132 |
+
]
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
title: "مقابلة عمل",
|
| 136 |
+
description: "تدرب على أسئلة مقابلة العمل الشائعة",
|
| 137 |
+
dialogue: [
|
| 138 |
+
{
|
| 139 |
+
speaker: "interviewer",
|
| 140 |
+
text: "Tell me about yourself."
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
speaker: "candidate",
|
| 144 |
+
text: "I'm a motivated professional with experience in..."
|
| 145 |
+
}
|
| 146 |
+
]
|
| 147 |
+
}
|
| 148 |
+
],
|
| 149 |
+
echoResponses: [
|
| 150 |
+
"That's wonderful! Your English is getting better every day.",
|
| 151 |
+
"Great job! Can you tell me more about that topic?",
|
| 152 |
+
"Excellent! I love how you expressed that idea.",
|
| 153 |
+
"Perfect! Let's try using some advanced vocabulary now.",
|
| 154 |
+
"Amazing progress! What would you like to talk about next?",
|
| 155 |
+
"Fantastic! Your grammar is really improving.",
|
| 156 |
+
"Well done! That was a complete and clear sentence.",
|
| 157 |
+
"Impressive! You're becoming more confident in English.",
|
| 158 |
+
"Brilliant! Let's explore this topic further.",
|
| 159 |
+
"Outstanding! Keep up the great work!"
|
| 160 |
+
]
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
// Application State
|
| 164 |
+
const appState = {
|
| 165 |
+
currentScreen: 'homeScreen',
|
| 166 |
+
userProgress: {
|
| 167 |
+
streak: 5,
|
| 168 |
+
totalPoints: 125,
|
| 169 |
+
wordsLearned: 12,
|
| 170 |
+
conversationsCompleted: 3,
|
| 171 |
+
pronunciationAccuracy: 78,
|
| 172 |
+
grammarScore: 85,
|
| 173 |
+
unlockedAchievements: ['first_conversation']
|
| 174 |
+
},
|
| 175 |
+
currentVocabIndex: 0,
|
| 176 |
+
currentGrammarIndex: 0,
|
| 177 |
+
currentScenario: null,
|
| 178 |
+
isRecording: false,
|
| 179 |
+
soundEnabled: true,
|
| 180 |
+
dailyProgress: 1,
|
| 181 |
+
selectedChallengeWords: [],
|
| 182 |
+
recordingTimer: null
|
| 183 |
+
};
|
| 184 |
+
|
| 185 |
+
// Character Animation Controller
|
| 186 |
+
class CharacterController {
|
| 187 |
+
constructor() {
|
| 188 |
+
this.character = document.getElementById('echoCharacter');
|
| 189 |
+
this.mouth = document.getElementById('characterMouth');
|
| 190 |
+
this.soundWaves = document.getElementById('soundWaves');
|
| 191 |
+
this.isAnimating = false;
|
| 192 |
+
this.mouthStates = ['happy', 'speaking', 'surprised', 'neutral'];
|
| 193 |
+
this.currentMoodIndex = 0;
|
| 194 |
+
|
| 195 |
+
this.startIdleAnimation();
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
startIdleAnimation() {
|
| 199 |
+
// Eye blinking animation
|
| 200 |
+
setInterval(() => {
|
| 201 |
+
this.blink();
|
| 202 |
+
}, 3000 + Math.random() * 2000);
|
| 203 |
+
|
| 204 |
+
// Mood changes
|
| 205 |
+
setInterval(() => {
|
| 206 |
+
this.changeMood();
|
| 207 |
+
}, 10000);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
blink() {
|
| 211 |
+
const eyes = document.querySelectorAll('.eye');
|
| 212 |
+
eyes.forEach(eye => {
|
| 213 |
+
eye.style.transform = 'scaleY(0.1)';
|
| 214 |
+
setTimeout(() => {
|
| 215 |
+
eye.style.transform = 'scaleY(1)';
|
| 216 |
+
}, 150);
|
| 217 |
+
});
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
changeMood() {
|
| 221 |
+
if (this.isAnimating) return;
|
| 222 |
+
|
| 223 |
+
const moods = ['happy', 'excited', 'thinking', 'encouraging'];
|
| 224 |
+
const randomMood = moods[Math.floor(Math.random() * moods.length)];
|
| 225 |
+
this.setMood(randomMood);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
setMood(mood) {
|
| 229 |
+
const character = this.character;
|
| 230 |
+
const mouth = this.mouth;
|
| 231 |
+
|
| 232 |
+
// Remove existing mood classes
|
| 233 |
+
character.classList.remove('happy', 'excited', 'thinking', 'encouraging');
|
| 234 |
+
character.classList.add(mood);
|
| 235 |
+
|
| 236 |
+
switch(mood) {
|
| 237 |
+
case 'happy':
|
| 238 |
+
mouth.style.borderRadius = '0 0 40px 40px';
|
| 239 |
+
mouth.style.width = '40px';
|
| 240 |
+
mouth.style.background = '#ff6b9d';
|
| 241 |
+
break;
|
| 242 |
+
case 'excited':
|
| 243 |
+
mouth.style.borderRadius = '50%';
|
| 244 |
+
mouth.style.width = '30px';
|
| 245 |
+
mouth.style.background = '#32b8c6';
|
| 246 |
+
break;
|
| 247 |
+
case 'thinking':
|
| 248 |
+
mouth.style.borderRadius = '40px 40px 0 0';
|
| 249 |
+
mouth.style.width = '20px';
|
| 250 |
+
mouth.style.background = '#8a2be2';
|
| 251 |
+
break;
|
| 252 |
+
case 'encouraging':
|
| 253 |
+
mouth.style.borderRadius = '0 0 50px 50px';
|
| 254 |
+
mouth.style.width = '50px';
|
| 255 |
+
mouth.style.background = '#ff6b9d';
|
| 256 |
+
break;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
speak(text) {
|
| 261 |
+
this.isAnimating = true;
|
| 262 |
+
this.setMood('happy');
|
| 263 |
+
|
| 264 |
+
// Animate sound waves
|
| 265 |
+
if (this.soundWaves) {
|
| 266 |
+
this.soundWaves.style.display = 'block';
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// Simulate mouth movement
|
| 270 |
+
let speakingInterval = setInterval(() => {
|
| 271 |
+
const randomWidth = 20 + Math.random() * 30;
|
| 272 |
+
if (this.mouth) {
|
| 273 |
+
this.mouth.style.width = randomWidth + 'px';
|
| 274 |
+
}
|
| 275 |
+
}, 150);
|
| 276 |
+
|
| 277 |
+
// Use Web Speech API if available and enabled
|
| 278 |
+
if ('speechSynthesis' in window && appState.soundEnabled) {
|
| 279 |
+
const utterance = new SpeechSynthesisUtterance(text);
|
| 280 |
+
utterance.lang = 'en-US';
|
| 281 |
+
utterance.rate = 0.9;
|
| 282 |
+
utterance.pitch = 1.1;
|
| 283 |
+
|
| 284 |
+
utterance.onend = () => {
|
| 285 |
+
clearInterval(speakingInterval);
|
| 286 |
+
this.stopSpeaking();
|
| 287 |
+
};
|
| 288 |
+
|
| 289 |
+
utterance.onerror = () => {
|
| 290 |
+
clearInterval(speakingInterval);
|
| 291 |
+
this.stopSpeaking();
|
| 292 |
+
};
|
| 293 |
+
|
| 294 |
+
speechSynthesis.speak(utterance);
|
| 295 |
+
} else {
|
| 296 |
+
// Fallback animation
|
| 297 |
+
setTimeout(() => {
|
| 298 |
+
clearInterval(speakingInterval);
|
| 299 |
+
this.stopSpeaking();
|
| 300 |
+
}, Math.min(text.length * 50, 3000));
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
stopSpeaking() {
|
| 305 |
+
this.isAnimating = false;
|
| 306 |
+
if (this.soundWaves) {
|
| 307 |
+
this.soundWaves.style.display = 'none';
|
| 308 |
+
}
|
| 309 |
+
if (this.mouth) {
|
| 310 |
+
this.mouth.style.width = '40px';
|
| 311 |
+
}
|
| 312 |
+
this.setMood('happy');
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
celebrate() {
|
| 316 |
+
this.setMood('excited');
|
| 317 |
+
if (this.character) {
|
| 318 |
+
this.character.style.animation = 'float 0.5s ease-in-out 3';
|
| 319 |
+
|
| 320 |
+
setTimeout(() => {
|
| 321 |
+
this.character.style.animation = 'float 3s ease-in-out infinite';
|
| 322 |
+
this.setMood('happy');
|
| 323 |
+
}, 1500);
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
encourage() {
|
| 328 |
+
this.setMood('encouraging');
|
| 329 |
+
setTimeout(() => {
|
| 330 |
+
this.setMood('happy');
|
| 331 |
+
}, 2000);
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Initialize character controller
|
| 336 |
+
let characterController;
|
| 337 |
+
|
| 338 |
+
// Screen Management
|
| 339 |
+
function showScreen(screenId) {
|
| 340 |
+
// Hide all screens
|
| 341 |
+
document.querySelectorAll('.screen').forEach(screen => {
|
| 342 |
+
screen.classList.remove('active');
|
| 343 |
+
});
|
| 344 |
+
|
| 345 |
+
// Show target screen
|
| 346 |
+
const targetScreen = document.getElementById(screenId);
|
| 347 |
+
if (targetScreen) {
|
| 348 |
+
targetScreen.classList.add('active');
|
| 349 |
+
appState.currentScreen = screenId;
|
| 350 |
+
|
| 351 |
+
// Update character mood based on screen
|
| 352 |
+
if (characterController) {
|
| 353 |
+
switch(screenId) {
|
| 354 |
+
case 'homeScreen':
|
| 355 |
+
characterController.setMood('happy');
|
| 356 |
+
break;
|
| 357 |
+
case 'conversationScreen':
|
| 358 |
+
characterController.setMood('encouraging');
|
| 359 |
+
break;
|
| 360 |
+
case 'vocabularyScreen':
|
| 361 |
+
case 'grammarScreen':
|
| 362 |
+
characterController.setMood('thinking');
|
| 363 |
+
break;
|
| 364 |
+
case 'pronunciationScreen':
|
| 365 |
+
characterController.setMood('excited');
|
| 366 |
+
break;
|
| 367 |
+
case 'progressScreen':
|
| 368 |
+
initializeAchievements();
|
| 369 |
+
break;
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Mode Management
|
| 376 |
+
function initializeModes() {
|
| 377 |
+
const modesGrid = document.getElementById('modesGrid');
|
| 378 |
+
if (!modesGrid) return;
|
| 379 |
+
|
| 380 |
+
modesGrid.innerHTML = '';
|
| 381 |
+
|
| 382 |
+
appData.modes.forEach(mode => {
|
| 383 |
+
const modeCard = document.createElement('div');
|
| 384 |
+
modeCard.className = 'mode-card';
|
| 385 |
+
modeCard.onclick = () => openMode(mode.id);
|
| 386 |
+
|
| 387 |
+
modeCard.innerHTML = `
|
| 388 |
+
<span class="mode-icon">${mode.icon}</span>
|
| 389 |
+
<h3 class="mode-title">${mode.name}</h3>
|
| 390 |
+
<p class="mode-description">${mode.description}</p>
|
| 391 |
+
`;
|
| 392 |
+
|
| 393 |
+
modesGrid.appendChild(modeCard);
|
| 394 |
+
});
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function openMode(modeId) {
|
| 398 |
+
const screenMap = {
|
| 399 |
+
'conversation': 'conversationScreen',
|
| 400 |
+
'vocabulary': 'vocabularyScreen',
|
| 401 |
+
'pronunciation': 'pronunciationScreen',
|
| 402 |
+
'grammar': 'grammarScreen',
|
| 403 |
+
'scenarios': 'scenariosScreen',
|
| 404 |
+
'daily': 'dailyScreen'
|
| 405 |
+
};
|
| 406 |
+
|
| 407 |
+
const screenId = screenMap[modeId];
|
| 408 |
+
if (screenId) {
|
| 409 |
+
showScreen(screenId);
|
| 410 |
+
|
| 411 |
+
// Initialize mode-specific content
|
| 412 |
+
switch(modeId) {
|
| 413 |
+
case 'vocabulary':
|
| 414 |
+
initializeVocabulary();
|
| 415 |
+
break;
|
| 416 |
+
case 'grammar':
|
| 417 |
+
initializeGrammar();
|
| 418 |
+
break;
|
| 419 |
+
case 'scenarios':
|
| 420 |
+
initializeScenarios();
|
| 421 |
+
break;
|
| 422 |
+
case 'daily':
|
| 423 |
+
initializeDailyChallenge();
|
| 424 |
+
break;
|
| 425 |
+
case 'pronunciation':
|
| 426 |
+
initializePronunciation();
|
| 427 |
+
break;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// Conversation Mode
|
| 433 |
+
function sendMessage() {
|
| 434 |
+
const input = document.getElementById('messageInput');
|
| 435 |
+
if (!input) return;
|
| 436 |
+
const message = input.value.trim();
|
| 437 |
+
if (!message) return;
|
| 438 |
+
addMessage(message, 'user');
|
| 439 |
+
input.value = '';
|
| 440 |
+
|
| 441 |
+
// استدعاء الذكاء الاصطناعي عبر الخادم
|
| 442 |
+
fetch('http://localhost:5000/chat', {
|
| 443 |
+
method: 'POST',
|
| 444 |
+
headers: {'Content-Type': 'application/json'},
|
| 445 |
+
body: JSON.stringify({message})
|
| 446 |
+
})
|
| 447 |
+
.then(res => res.json())
|
| 448 |
+
.then(data => {
|
| 449 |
+
addMessage(data.reply, 'echo');
|
| 450 |
+
if (characterController) characterController.speak(data.reply);
|
| 451 |
+
})
|
| 452 |
+
.catch(() => {
|
| 453 |
+
addMessage("Sorry, I couldn't connect to the AI server.", 'echo');
|
| 454 |
+
});
|
| 455 |
+
|
| 456 |
+
// تحديث النقاط كما هو
|
| 457 |
+
appState.userProgress.conversationsCompleted++;
|
| 458 |
+
appState.userProgress.totalPoints += 25;
|
| 459 |
+
updateProgressDisplay();
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
function addMessage(text, sender) {
|
| 463 |
+
const messagesContainer = document.getElementById('chatMessages');
|
| 464 |
+
if (!messagesContainer) return;
|
| 465 |
+
|
| 466 |
+
const messageDiv = document.createElement('div');
|
| 467 |
+
messageDiv.className = `message ${sender}`;
|
| 468 |
+
messageDiv.textContent = text;
|
| 469 |
+
|
| 470 |
+
messagesContainer.appendChild(messageDiv);
|
| 471 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
function startVoiceInput() {
|
| 475 |
+
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
| 476 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 477 |
+
const recognition = new SpeechRecognition();
|
| 478 |
+
|
| 479 |
+
recognition.lang = 'en-US';
|
| 480 |
+
recognition.continuous = false;
|
| 481 |
+
recognition.interimResults = false;
|
| 482 |
+
|
| 483 |
+
recognition.onresult = function(event) {
|
| 484 |
+
const transcript = event.results[0][0].transcript;
|
| 485 |
+
const input = document.getElementById('messageInput');
|
| 486 |
+
if (input) {
|
| 487 |
+
input.value = transcript;
|
| 488 |
+
}
|
| 489 |
+
};
|
| 490 |
+
|
| 491 |
+
recognition.onerror = function(event) {
|
| 492 |
+
console.log('Speech recognition error:', event.error);
|
| 493 |
+
alert('حدث خطأ في التعرف على الصوت. تأكد من السماح بالوصول للميكروفون.');
|
| 494 |
+
};
|
| 495 |
+
|
| 496 |
+
recognition.start();
|
| 497 |
+
} else {
|
| 498 |
+
alert('متصفحك لا يدعم تقنية التعرف على الصوت.');
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
// Vocabulary Mode
|
| 503 |
+
function initializeVocabulary() {
|
| 504 |
+
appState.currentVocabIndex = 0;
|
| 505 |
+
updateVocabularyCard();
|
| 506 |
+
updateVocabularyProgress();
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
function updateVocabularyCard() {
|
| 510 |
+
const card = appData.vocabulary_cards[appState.currentVocabIndex];
|
| 511 |
+
|
| 512 |
+
const elements = {
|
| 513 |
+
'cardWord': card.word,
|
| 514 |
+
'cardPronunciation': card.pronunciation,
|
| 515 |
+
'cardTranslation': card.translation,
|
| 516 |
+
'cardExample': card.example
|
| 517 |
+
};
|
| 518 |
+
|
| 519 |
+
Object.entries(elements).forEach(([id, value]) => {
|
| 520 |
+
const element = document.getElementById(id);
|
| 521 |
+
if (element) {
|
| 522 |
+
element.textContent = value;
|
| 523 |
+
}
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
// Reset card flip
|
| 527 |
+
const flashcard = document.getElementById('flashcard');
|
| 528 |
+
if (flashcard) {
|
| 529 |
+
flashcard.classList.remove('flipped');
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function updateVocabularyProgress() {
|
| 534 |
+
const progress = ((appState.currentVocabIndex + 1) / appData.vocabulary_cards.length) * 100;
|
| 535 |
+
const progressFill = document.getElementById('vocabProgress');
|
| 536 |
+
const progressText = document.getElementById('vocabProgressText');
|
| 537 |
+
|
| 538 |
+
if (progressFill) {
|
| 539 |
+
progressFill.style.width = progress + '%';
|
| 540 |
+
}
|
| 541 |
+
if (progressText) {
|
| 542 |
+
progressText.textContent = `${appState.currentVocabIndex + 1}/${appData.vocabulary_cards.length}`;
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
function flipCard() {
|
| 547 |
+
const flashcard = document.getElementById('flashcard');
|
| 548 |
+
if (flashcard) {
|
| 549 |
+
flashcard.classList.toggle('flipped');
|
| 550 |
+
}
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
function nextCard() {
|
| 554 |
+
appState.currentVocabIndex = (appState.currentVocabIndex + 1) % appData.vocabulary_cards.length;
|
| 555 |
+
updateVocabularyCard();
|
| 556 |
+
updateVocabularyProgress();
|
| 557 |
+
|
| 558 |
+
// Update progress
|
| 559 |
+
appState.userProgress.wordsLearned++;
|
| 560 |
+
appState.userProgress.totalPoints += 10;
|
| 561 |
+
updateProgressDisplay();
|
| 562 |
+
|
| 563 |
+
if (characterController) {
|
| 564 |
+
characterController.encourage();
|
| 565 |
+
}
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
function playPronunciation() {
|
| 569 |
+
const card = appData.vocabulary_cards[appState.currentVocabIndex];
|
| 570 |
+
if (characterController && appState.soundEnabled) {
|
| 571 |
+
characterController.speak(card.word);
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
// Pronunciation Mode
|
| 576 |
+
function initializePronunciation() {
|
| 577 |
+
const words = appData.vocabulary_cards;
|
| 578 |
+
const randomWord = words[Math.floor(Math.random() * words.length)];
|
| 579 |
+
|
| 580 |
+
const practiceWord = document.getElementById('practiceWord');
|
| 581 |
+
const practicePronunciation = document.getElementById('practicePronunciation');
|
| 582 |
+
|
| 583 |
+
if (practiceWord) practiceWord.textContent = randomWord.word;
|
| 584 |
+
if (practicePronunciation) practicePronunciation.textContent = randomWord.pronunciation;
|
| 585 |
+
|
| 586 |
+
// Clear previous feedback
|
| 587 |
+
const feedback = document.getElementById('pronunciationFeedback');
|
| 588 |
+
if (feedback) {
|
| 589 |
+
feedback.innerHTML = '';
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
function playTargetPronunciation() {
|
| 594 |
+
const word = document.getElementById('practiceWord');
|
| 595 |
+
if (word && characterController && appState.soundEnabled) {
|
| 596 |
+
characterController.speak(word.textContent);
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
function toggleRecording() {
|
| 601 |
+
const recordBtn = document.getElementById('recordBtn');
|
| 602 |
+
const visualizer = document.getElementById('recordVisualizer');
|
| 603 |
+
|
| 604 |
+
if (!recordBtn || !visualizer) return;
|
| 605 |
+
|
| 606 |
+
if (!appState.isRecording) {
|
| 607 |
+
// Start recording
|
| 608 |
+
appState.isRecording = true;
|
| 609 |
+
recordBtn.textContent = '⏹️ إيقاف التسجيل';
|
| 610 |
+
recordBtn.classList.add('recording');
|
| 611 |
+
|
| 612 |
+
// Clear previous content
|
| 613 |
+
visualizer.innerHTML = '';
|
| 614 |
+
|
| 615 |
+
// Create recording visualization
|
| 616 |
+
const bars = [];
|
| 617 |
+
for (let i = 0; i < 10; i++) {
|
| 618 |
+
const bar = document.createElement('div');
|
| 619 |
+
bar.style.cssText = `
|
| 620 |
+
width: 15px;
|
| 621 |
+
background: #32b8c6;
|
| 622 |
+
margin: 2px;
|
| 623 |
+
border-radius: 2px;
|
| 624 |
+
display: inline-block;
|
| 625 |
+
height: 20px;
|
| 626 |
+
animation: record-bar 0.8s infinite alternate;
|
| 627 |
+
animation-delay: ${i * 0.1}s;
|
| 628 |
+
`;
|
| 629 |
+
bars.push(bar);
|
| 630 |
+
visualizer.appendChild(bar);
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
// Add CSS for bars animation if not exists
|
| 634 |
+
if (!document.getElementById('recordBarStyles')) {
|
| 635 |
+
const style = document.createElement('style');
|
| 636 |
+
style.id = 'recordBarStyles';
|
| 637 |
+
style.textContent = `
|
| 638 |
+
@keyframes record-bar {
|
| 639 |
+
0% { height: 10px; opacity: 0.5; }
|
| 640 |
+
100% { height: 50px; opacity: 1; }
|
| 641 |
+
}
|
| 642 |
+
`;
|
| 643 |
+
document.head.appendChild(style);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
// Auto-stop after 5 seconds
|
| 647 |
+
appState.recordingTimer = setTimeout(() => {
|
| 648 |
+
if (appState.isRecording) {
|
| 649 |
+
toggleRecording();
|
| 650 |
+
}
|
| 651 |
+
}, 5000);
|
| 652 |
+
|
| 653 |
+
} else {
|
| 654 |
+
// Stop recording
|
| 655 |
+
appState.isRecording = false;
|
| 656 |
+
recordBtn.textContent = '🎤 ابدأ التسجيل';
|
| 657 |
+
recordBtn.classList.remove('recording');
|
| 658 |
+
|
| 659 |
+
// Clear timer
|
| 660 |
+
if (appState.recordingTimer) {
|
| 661 |
+
clearTimeout(appState.recordingTimer);
|
| 662 |
+
appState.recordingTimer = null;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
// Clear visualizer
|
| 666 |
+
visualizer.innerHTML = '';
|
| 667 |
+
|
| 668 |
+
// Show processing message then feedback
|
| 669 |
+
const feedback = document.getElementById('pronunciationFeedback');
|
| 670 |
+
if (feedback) {
|
| 671 |
+
feedback.innerHTML = '<p>جاري تحليل النطق...</p>';
|
| 672 |
+
|
| 673 |
+
setTimeout(() => {
|
| 674 |
+
showPronunciationFeedback();
|
| 675 |
+
}, 1500);
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
function showPronunciationFeedback() {
|
| 681 |
+
const feedback = document.getElementById('pronunciationFeedback');
|
| 682 |
+
if (!feedback) return;
|
| 683 |
+
|
| 684 |
+
const accuracy = 65 + Math.random() * 30; // Simulate accuracy between 65-95%
|
| 685 |
+
const roundedAccuracy = Math.round(accuracy);
|
| 686 |
+
|
| 687 |
+
let message, color;
|
| 688 |
+
if (roundedAccuracy >= 85) {
|
| 689 |
+
message = 'ممتاز! نطقك رائع 🎉';
|
| 690 |
+
color = '#32b8c6';
|
| 691 |
+
} else if (roundedAccuracy >= 70) {
|
| 692 |
+
message = 'جيد جداً! يمكنك تحسينه أكثر 👍';
|
| 693 |
+
color = '#8a2be2';
|
| 694 |
+
} else {
|
| 695 |
+
message = 'يحتاج تحسين، استمع للنطق الصحيح 🎯';
|
| 696 |
+
color = '#ff6b9d';
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
feedback.innerHTML = `
|
| 700 |
+
<h4>نتيجة النطق</h4>
|
| 701 |
+
<div style="font-size: 3em; color: ${color}; margin: 16px 0; text-shadow: 0 0 10px ${color}40;">
|
| 702 |
+
${roundedAccuracy}%
|
| 703 |
+
</div>
|
| 704 |
+
<p style="color: ${color}; font-weight: bold;">${message}</p>
|
| 705 |
+
<button class="btn btn--primary" onclick="initializePronunciation()">كلمة جديدة</button>
|
| 706 |
+
`;
|
| 707 |
+
|
| 708 |
+
// Update progress
|
| 709 |
+
appState.userProgress.pronunciationAccuracy = Math.round(
|
| 710 |
+
(appState.userProgress.pronunciationAccuracy + accuracy) / 2
|
| 711 |
+
);
|
| 712 |
+
appState.userProgress.totalPoints += Math.round(accuracy / 10);
|
| 713 |
+
updateProgressDisplay();
|
| 714 |
+
|
| 715 |
+
if (characterController) {
|
| 716 |
+
if (roundedAccuracy >= 85) {
|
| 717 |
+
characterController.celebrate();
|
| 718 |
+
} else {
|
| 719 |
+
characterController.encourage();
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
// Grammar Mode
|
| 725 |
+
function initializeGrammar() {
|
| 726 |
+
appState.currentGrammarIndex = 0;
|
| 727 |
+
showGrammarQuestion();
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
function showGrammarQuestion() {
|
| 731 |
+
const exercise = appData.grammar_exercises[appState.currentGrammarIndex];
|
| 732 |
+
|
| 733 |
+
const questionElement = document.getElementById('grammarQuestion');
|
| 734 |
+
if (questionElement) {
|
| 735 |
+
questionElement.textContent = exercise.question;
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
const optionsContainer = document.getElementById('grammarOptions');
|
| 739 |
+
if (optionsContainer) {
|
| 740 |
+
optionsContainer.innerHTML = '';
|
| 741 |
+
|
| 742 |
+
exercise.options.forEach((option, index) => {
|
| 743 |
+
const optionBtn = document.createElement('button');
|
| 744 |
+
optionBtn.className = 'option-btn';
|
| 745 |
+
optionBtn.textContent = option;
|
| 746 |
+
optionBtn.onclick = () => selectGrammarOption(index);
|
| 747 |
+
|
| 748 |
+
optionsContainer.appendChild(optionBtn);
|
| 749 |
+
});
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
// Clear previous feedback
|
| 753 |
+
const feedback = document.getElementById('grammarFeedback');
|
| 754 |
+
if (feedback) {
|
| 755 |
+
feedback.innerHTML = '';
|
| 756 |
+
}
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
function selectGrammarOption(selectedIndex) {
|
| 760 |
+
const exercise = appData.grammar_exercises[appState.currentGrammarIndex];
|
| 761 |
+
const options = document.querySelectorAll('.option-btn');
|
| 762 |
+
const feedback = document.getElementById('grammarFeedback');
|
| 763 |
+
|
| 764 |
+
if (!feedback) return;
|
| 765 |
+
|
| 766 |
+
// Disable all options
|
| 767 |
+
options.forEach((btn, index) => {
|
| 768 |
+
btn.disabled = true;
|
| 769 |
+
if (index === exercise.correct) {
|
| 770 |
+
btn.classList.add('correct');
|
| 771 |
+
} else if (index === selectedIndex && index !== exercise.correct) {
|
| 772 |
+
btn.classList.add('incorrect');
|
| 773 |
+
}
|
| 774 |
+
});
|
| 775 |
+
|
| 776 |
+
// Show feedback
|
| 777 |
+
const isCorrect = selectedIndex === exercise.correct;
|
| 778 |
+
feedback.innerHTML = `
|
| 779 |
+
<h4>${isCorrect ? '✅ إجابة صحيحة!' : '❌ إجابة خاطئة'}</h4>
|
| 780 |
+
<p>${exercise.explanation}</p>
|
| 781 |
+
<button class="btn btn--primary" onclick="nextGrammarQuestion()">السؤال التالي</button>
|
| 782 |
+
`;
|
| 783 |
+
|
| 784 |
+
// Update progress
|
| 785 |
+
if (isCorrect) {
|
| 786 |
+
appState.userProgress.grammarScore += 10;
|
| 787 |
+
appState.userProgress.totalPoints += 15;
|
| 788 |
+
updateProgressDisplay();
|
| 789 |
+
|
| 790 |
+
if (characterController) {
|
| 791 |
+
characterController.celebrate();
|
| 792 |
+
}
|
| 793 |
+
} else {
|
| 794 |
+
if (characterController) {
|
| 795 |
+
characterController.encourage();
|
| 796 |
+
}
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
function nextGrammarQuestion() {
|
| 801 |
+
appState.currentGrammarIndex = (appState.currentGrammarIndex + 1) % appData.grammar_exercises.length;
|
| 802 |
+
showGrammarQuestion();
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
// Scenarios Mode
|
| 806 |
+
function initializeScenarios() {
|
| 807 |
+
const buttonsContainer = document.getElementById('scenarioButtons');
|
| 808 |
+
if (!buttonsContainer) return;
|
| 809 |
+
|
| 810 |
+
buttonsContainer.innerHTML = '';
|
| 811 |
+
|
| 812 |
+
appData.scenarios.forEach((scenario, index) => {
|
| 813 |
+
const btn = document.createElement('button');
|
| 814 |
+
btn.className = 'scenario-btn';
|
| 815 |
+
btn.textContent = scenario.title;
|
| 816 |
+
btn.onclick = () => showScenario(index);
|
| 817 |
+
|
| 818 |
+
buttonsContainer.appendChild(btn);
|
| 819 |
+
});
|
| 820 |
+
|
| 821 |
+
// Show first scenario by default
|
| 822 |
+
if (appData.scenarios.length > 0) {
|
| 823 |
+
showScenario(0);
|
| 824 |
+
}
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
function showScenario(scenarioIndex) {
|
| 828 |
+
const scenario = appData.scenarios[scenarioIndex];
|
| 829 |
+
appState.currentScenario = scenarioIndex;
|
| 830 |
+
|
| 831 |
+
// Update button states
|
| 832 |
+
document.querySelectorAll('.scenario-btn').forEach((btn, index) => {
|
| 833 |
+
btn.classList.toggle('active', index === scenarioIndex);
|
| 834 |
+
});
|
| 835 |
+
|
| 836 |
+
// Show dialogue
|
| 837 |
+
const dialogueContainer = document.getElementById('scenarioDialogue');
|
| 838 |
+
if (dialogueContainer) {
|
| 839 |
+
dialogueContainer.innerHTML = `
|
| 840 |
+
<h4>${scenario.title}</h4>
|
| 841 |
+
<p>${scenario.description}</p>
|
| 842 |
+
<div class="dialogue-content">
|
| 843 |
+
${scenario.dialogue.map(line => `
|
| 844 |
+
<div class="dialogue-line ${line.speaker}">
|
| 845 |
+
<strong>${line.speaker === 'waiter' ? 'النادل:' :
|
| 846 |
+
line.speaker === 'customer' ? 'الزبون:' :
|
| 847 |
+
line.speaker === 'interviewer' ? 'المحاور:' : 'المرشح:'}</strong>
|
| 848 |
+
<p>${line.text}</p>
|
| 849 |
+
</div>
|
| 850 |
+
`).join('')}
|
| 851 |
+
</div>
|
| 852 |
+
<button class="btn btn--primary" onclick="practiceScenario()">🎤 تدرب على هذا السيناريو</button>
|
| 853 |
+
`;
|
| 854 |
+
}
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
function practiceScenario() {
|
| 858 |
+
if (characterController && appState.currentScenario !== null) {
|
| 859 |
+
const scenario = appData.scenarios[appState.currentScenario];
|
| 860 |
+
const firstLine = scenario.dialogue[0].text;
|
| 861 |
+
characterController.speak(firstLine);
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
// Show practice message
|
| 865 |
+
const dialogueContainer = document.getElementById('scenarioDialogue');
|
| 866 |
+
if (dialogueContainer) {
|
| 867 |
+
const practiceDiv = document.createElement('div');
|
| 868 |
+
practiceDiv.style.cssText = `
|
| 869 |
+
background: rgba(50, 184, 198, 0.1);
|
| 870 |
+
border: 1px solid #32b8c6;
|
| 871 |
+
border-radius: 12px;
|
| 872 |
+
padding: 16px;
|
| 873 |
+
margin-top: 16px;
|
| 874 |
+
text-align: center;
|
| 875 |
+
`;
|
| 876 |
+
practiceDiv.innerHTML = '<p>🎤 الآن دورك! تدرب على الرد باللغة الإنجليزية</p>';
|
| 877 |
+
dialogueContainer.appendChild(practiceDiv);
|
| 878 |
+
}
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
// Daily Challenge Mode
|
| 882 |
+
function initializeDailyChallenge() {
|
| 883 |
+
appState.dailyProgress = 1;
|
| 884 |
+
appState.selectedChallengeWords = [];
|
| 885 |
+
updateDailyProgress();
|
| 886 |
+
startChallengeTimer();
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
function updateDailyProgress() {
|
| 890 |
+
const progress = (appState.dailyProgress / 3) * 100;
|
| 891 |
+
const progressFill = document.getElementById('dailyProgress');
|
| 892 |
+
const progressText = document.getElementById('dailyProgressText');
|
| 893 |
+
|
| 894 |
+
if (progressFill) {
|
| 895 |
+
progressFill.style.width = progress + '%';
|
| 896 |
+
}
|
| 897 |
+
if (progressText) {
|
| 898 |
+
progressText.textContent = `${appState.dailyProgress}/3`;
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
function selectWord(wordElement) {
|
| 903 |
+
const word = wordElement.textContent;
|
| 904 |
+
|
| 905 |
+
// Toggle selection
|
| 906 |
+
if (wordElement.classList.contains('selected')) {
|
| 907 |
+
wordElement.classList.remove('selected');
|
| 908 |
+
appState.selectedChallengeWords = appState.selectedChallengeWords.filter(w => w !== word);
|
| 909 |
+
} else {
|
| 910 |
+
// Limit to 2 selections
|
| 911 |
+
if (appState.selectedChallengeWords.length < 2) {
|
| 912 |
+
wordElement.classList.add('selected');
|
| 913 |
+
appState.selectedChallengeWords.push(word);
|
| 914 |
+
}
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
// Check if challenge is complete
|
| 918 |
+
if (appState.selectedChallengeWords.length === 2) {
|
| 919 |
+
setTimeout(() => {
|
| 920 |
+
checkChallengeAnswer();
|
| 921 |
+
}, 500);
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
function checkChallengeAnswer() {
|
| 926 |
+
const correctWords = ['study', 'important'];
|
| 927 |
+
const isCorrect = correctWords.every(word => appState.selectedChallengeWords.includes(word));
|
| 928 |
+
|
| 929 |
+
if (isCorrect) {
|
| 930 |
+
appState.dailyProgress++;
|
| 931 |
+
updateDailyProgress();
|
| 932 |
+
|
| 933 |
+
if (characterController) {
|
| 934 |
+
characterController.celebrate();
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
// Update streak and points
|
| 938 |
+
appState.userProgress.streak++;
|
| 939 |
+
appState.userProgress.totalPoints += 20;
|
| 940 |
+
updateProgressDisplay();
|
| 941 |
+
|
| 942 |
+
if (appState.dailyProgress >= 3) {
|
| 943 |
+
setTimeout(() => {
|
| 944 |
+
alert('🎉 أكملت التحدي اليومي! أحسنت!');
|
| 945 |
+
checkAchievements();
|
| 946 |
+
}, 1000);
|
| 947 |
+
} else {
|
| 948 |
+
setTimeout(() => {
|
| 949 |
+
alert('✅ إجابة صحيحة! استمر...');
|
| 950 |
+
}, 500);
|
| 951 |
+
}
|
| 952 |
+
} else {
|
| 953 |
+
if (characterController) {
|
| 954 |
+
characterController.encourage();
|
| 955 |
+
}
|
| 956 |
+
setTimeout(() => {
|
| 957 |
+
alert('❌ حاول مرة أخرى!');
|
| 958 |
+
}, 500);
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// Reset selections
|
| 962 |
+
document.querySelectorAll('.word-option').forEach(option => {
|
| 963 |
+
option.classList.remove('selected');
|
| 964 |
+
});
|
| 965 |
+
appState.selectedChallengeWords = [];
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
function startChallengeTimer() {
|
| 969 |
+
let timeLeft = 300; // 5 minutes in seconds
|
| 970 |
+
const timerElement = document.getElementById('challengeTimer');
|
| 971 |
+
if (!timerElement) return;
|
| 972 |
+
|
| 973 |
+
const timer = setInterval(() => {
|
| 974 |
+
const minutes = Math.floor(timeLeft / 60);
|
| 975 |
+
const seconds = timeLeft % 60;
|
| 976 |
+
timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 977 |
+
|
| 978 |
+
timeLeft--;
|
| 979 |
+
|
| 980 |
+
if (timeLeft < 0) {
|
| 981 |
+
clearInterval(timer);
|
| 982 |
+
alert('⏰ انتهى الوقت! حاول مرة أخرى غداً.');
|
| 983 |
+
}
|
| 984 |
+
}, 1000);
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
// Progress Tracking
|
| 988 |
+
function updateProgressDisplay() {
|
| 989 |
+
const elements = {
|
| 990 |
+
'streakCount': appState.userProgress.streak,
|
| 991 |
+
'totalPoints': appState.userProgress.totalPoints,
|
| 992 |
+
'wordsLearned': appState.userProgress.wordsLearned,
|
| 993 |
+
'conversationsCompleted': appState.userProgress.conversationsCompleted,
|
| 994 |
+
'pronunciationAccuracy': appState.userProgress.pronunciationAccuracy + '%',
|
| 995 |
+
'grammarScore': appState.userProgress.grammarScore
|
| 996 |
+
};
|
| 997 |
+
|
| 998 |
+
Object.entries(elements).forEach(([id, value]) => {
|
| 999 |
+
const element = document.getElementById(id);
|
| 1000 |
+
if (element) {
|
| 1001 |
+
element.textContent = value;
|
| 1002 |
+
}
|
| 1003 |
+
});
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
// Achievements System
|
| 1007 |
+
function checkAchievements() {
|
| 1008 |
+
const progress = appState.userProgress;
|
| 1009 |
+
const achievements = [
|
| 1010 |
+
{
|
| 1011 |
+
id: 'first_conversation',
|
| 1012 |
+
condition: progress.conversationsCompleted >= 1
|
| 1013 |
+
},
|
| 1014 |
+
{
|
| 1015 |
+
id: 'vocabulary_master',
|
| 1016 |
+
condition: progress.wordsLearned >= 50
|
| 1017 |
+
},
|
| 1018 |
+
{
|
| 1019 |
+
id: 'pronunciation_expert',
|
| 1020 |
+
condition: progress.pronunciationAccuracy >= 85
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
id: 'grammar_guru',
|
| 1024 |
+
condition: progress.grammarScore >= 100
|
| 1025 |
+
},
|
| 1026 |
+
{
|
| 1027 |
+
id: 'daily_streak',
|
| 1028 |
+
condition: progress.streak >= 7
|
| 1029 |
+
}
|
| 1030 |
+
];
|
| 1031 |
+
|
| 1032 |
+
achievements.forEach(achievement => {
|
| 1033 |
+
if (achievement.condition && !progress.unlockedAchievements.includes(achievement.id)) {
|
| 1034 |
+
unlockAchievement(achievement.id);
|
| 1035 |
+
}
|
| 1036 |
+
});
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
function unlockAchievement(achievementId) {
|
| 1040 |
+
const achievement = appData.achievements.find(a => a.id === achievementId);
|
| 1041 |
+
if (!achievement) return;
|
| 1042 |
+
|
| 1043 |
+
appState.userProgress.unlockedAchievements.push(achievementId);
|
| 1044 |
+
appState.userProgress.totalPoints += 50;
|
| 1045 |
+
updateProgressDisplay();
|
| 1046 |
+
|
| 1047 |
+
// Show achievement notification
|
| 1048 |
+
showAchievementNotification(achievement);
|
| 1049 |
+
|
| 1050 |
+
if (characterController) {
|
| 1051 |
+
characterController.celebrate();
|
| 1052 |
+
}
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
function showAchievementNotification(achievement) {
|
| 1056 |
+
const notification = document.createElement('div');
|
| 1057 |
+
notification.style.cssText = `
|
| 1058 |
+
position: fixed;
|
| 1059 |
+
top: 20px;
|
| 1060 |
+
right: 20px;
|
| 1061 |
+
background: linear-gradient(135deg, #32b8c6, #8a2be2);
|
| 1062 |
+
color: white;
|
| 1063 |
+
padding: 20px;
|
| 1064 |
+
border-radius: 12px;
|
| 1065 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 1066 |
+
z-index: 1000;
|
| 1067 |
+
animation: slideIn 0.5s ease-out;
|
| 1068 |
+
max-width: 300px;
|
| 1069 |
+
`;
|
| 1070 |
+
notification.innerHTML = `
|
| 1071 |
+
<div style="font-size: 2em; margin-bottom: 8px; text-align: center;">${achievement.icon}</div>
|
| 1072 |
+
<h4 style="margin: 0 0 8px 0; text-align: center;">إنجاز جديد!</h4>
|
| 1073 |
+
<p style="margin: 0; font-size: 14px; text-align: center;">${achievement.name}</p>
|
| 1074 |
+
`;
|
| 1075 |
+
|
| 1076 |
+
document.body.appendChild(notification);
|
| 1077 |
+
|
| 1078 |
+
setTimeout(() => {
|
| 1079 |
+
notification.remove();
|
| 1080 |
+
}, 5000);
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
function initializeAchievements() {
|
| 1084 |
+
const achievementsGrid = document.getElementById('achievementsGrid');
|
| 1085 |
+
if (!achievementsGrid) return;
|
| 1086 |
+
|
| 1087 |
+
achievementsGrid.innerHTML = '';
|
| 1088 |
+
|
| 1089 |
+
appData.achievements.forEach(achievement => {
|
| 1090 |
+
const isUnlocked = appState.userProgress.unlockedAchievements.includes(achievement.id);
|
| 1091 |
+
|
| 1092 |
+
const achievementCard = document.createElement('div');
|
| 1093 |
+
achievementCard.className = `achievement-card ${isUnlocked ? 'unlocked' : ''}`;
|
| 1094 |
+
achievementCard.innerHTML = `
|
| 1095 |
+
<span class="achievement-icon">${achievement.icon}</span>
|
| 1096 |
+
<div class="achievement-name">${achievement.name}</div>
|
| 1097 |
+
<div class="achievement-description">${achievement.description}</div>
|
| 1098 |
+
`;
|
| 1099 |
+
|
| 1100 |
+
achievementsGrid.appendChild(achievementCard);
|
| 1101 |
+
});
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
// Sound Management
|
| 1105 |
+
function toggleSound() {
|
| 1106 |
+
appState.soundEnabled = !appState.soundEnabled;
|
| 1107 |
+
const soundToggle = document.getElementById('soundToggle');
|
| 1108 |
+
|
| 1109 |
+
if (soundToggle) {
|
| 1110 |
+
soundToggle.textContent = appState.soundEnabled ? '🔊' : '🔇';
|
| 1111 |
+
soundToggle.title = appState.soundEnabled ? 'إيقاف الصوت' : 'تشغيل الصوت';
|
| 1112 |
+
|
| 1113 |
+
// Visual feedback
|
| 1114 |
+
soundToggle.style.color = appState.soundEnabled ? '#32b8c6' : '#ff6b9d';
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
if (!appState.soundEnabled && 'speechSynthesis' in window) {
|
| 1118 |
+
speechSynthesis.cancel();
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
// Show feedback message
|
| 1122 |
+
const message = appState.soundEnabled ? 'تم تشغيل الصوت' : 'تم إيقاف الصوت';
|
| 1123 |
+
showTemporaryMessage(message);
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
function showTemporaryMessage(message) {
|
| 1127 |
+
const messageDiv = document.createElement('div');
|
| 1128 |
+
messageDiv.style.cssText = `
|
| 1129 |
+
position: fixed;
|
| 1130 |
+
bottom: 100px;
|
| 1131 |
+
right: 20px;
|
| 1132 |
+
background: rgba(0, 0, 0, 0.8);
|
| 1133 |
+
color: white;
|
| 1134 |
+
padding: 12px 16px;
|
| 1135 |
+
border-radius: 8px;
|
| 1136 |
+
font-size: 14px;
|
| 1137 |
+
z-index: 1000;
|
| 1138 |
+
animation: slideIn 0.5s ease-out;
|
| 1139 |
+
`;
|
| 1140 |
+
messageDiv.textContent = message;
|
| 1141 |
+
|
| 1142 |
+
document.body.appendChild(messageDiv);
|
| 1143 |
+
|
| 1144 |
+
setTimeout(() => {
|
| 1145 |
+
messageDiv.remove();
|
| 1146 |
+
}, 2000);
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
// Add slide-in animation CSS
|
| 1150 |
+
const style = document.createElement('style');
|
| 1151 |
+
style.textContent = `
|
| 1152 |
+
@keyframes slideIn {
|
| 1153 |
+
from { transform: translateX(100%); opacity: 0; }
|
| 1154 |
+
to { transform: translateX(0); opacity: 1; }
|
| 1155 |
+
}
|
| 1156 |
+
`;
|
| 1157 |
+
document.head.appendChild(style);
|
| 1158 |
+
|
| 1159 |
+
// === Conversational Voice Loop & Avatar State ===
|
| 1160 |
+
let isMuted = false;
|
| 1161 |
+
let isConversationActive = false;
|
| 1162 |
+
let isAutoLoop = false;
|
| 1163 |
+
let recognition = null;
|
| 1164 |
+
let isSpeakingVoiceLoop = false; // حماية من التكرار
|
| 1165 |
+
|
| 1166 |
+
function setVoiceControlButtonsState() {
|
| 1167 |
+
const btnStart = document.querySelector('.voice-controls .btn--primary');
|
| 1168 |
+
const btnMute = document.querySelector('.voice-controls .btn--secondary');
|
| 1169 |
+
const btnEnd = document.querySelector('.voice-controls .btn--danger');
|
| 1170 |
+
if (!btnStart || !btnMute || !btnEnd) return;
|
| 1171 |
+
btnStart.disabled = isConversationActive || isSpeakingVoiceLoop;
|
| 1172 |
+
btnEnd.disabled = !isConversationActive && !isSpeakingVoiceLoop;
|
| 1173 |
+
btnMute.disabled = !isConversationActive && !isSpeakingVoiceLoop;
|
| 1174 |
+
btnMute.textContent = isMuted ? '🔇 كتم' : '🔊 كتم';
|
| 1175 |
+
btnMute.classList.toggle('muted', isMuted);
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
function setAvatarState(state) {
|
| 1179 |
+
if (!characterController) return;
|
| 1180 |
+
switch(state) {
|
| 1181 |
+
case 'idle':
|
| 1182 |
+
characterController.setMood('happy');
|
| 1183 |
+
break;
|
| 1184 |
+
case 'listening':
|
| 1185 |
+
characterController.setMood('encouraging');
|
| 1186 |
+
break;
|
| 1187 |
+
case 'speaking':
|
| 1188 |
+
characterController.setMood('excited');
|
| 1189 |
+
break;
|
| 1190 |
+
}
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
function startVoiceLoop() {
|
| 1194 |
+
if (isMuted || isConversationActive || isSpeakingVoiceLoop) return;
|
| 1195 |
+
isConversationActive = true;
|
| 1196 |
+
isAutoLoop = true;
|
| 1197 |
+
setAvatarState('listening');
|
| 1198 |
+
setVoiceControlButtonsState();
|
| 1199 |
+
startListening();
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
function stopVoiceLoop() {
|
| 1203 |
+
isConversationActive = false;
|
| 1204 |
+
isAutoLoop = false;
|
| 1205 |
+
if (recognition) recognition.stop();
|
| 1206 |
+
if ('speechSynthesis' in window) speechSynthesis.cancel();
|
| 1207 |
+
isSpeakingVoiceLoop = false;
|
| 1208 |
+
setAvatarState('idle');
|
| 1209 |
+
setVoiceControlButtonsState();
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
function toggleMute() {
|
| 1213 |
+
isMuted = !isMuted;
|
| 1214 |
+
if (isMuted) {
|
| 1215 |
+
if (recognition) recognition.stop();
|
| 1216 |
+
if ('speechSynthesis' in window) speechSynthesis.cancel();
|
| 1217 |
+
setAvatarState('idle');
|
| 1218 |
+
isConversationActive = false;
|
| 1219 |
+
isAutoLoop = false;
|
| 1220 |
+
isSpeakingVoiceLoop = false;
|
| 1221 |
+
} else if (!isConversationActive && !isSpeakingVoiceLoop) {
|
| 1222 |
+
isConversationActive = true;
|
| 1223 |
+
isAutoLoop = true;
|
| 1224 |
+
setAvatarState('listening');
|
| 1225 |
+
startListening();
|
| 1226 |
+
}
|
| 1227 |
+
setVoiceControlButtonsState();
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
function startListening() {
|
| 1231 |
+
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
|
| 1232 |
+
alert('Speech recognition not supported.');
|
| 1233 |
+
return;
|
| 1234 |
+
}
|
| 1235 |
+
if (isSpeakingVoiceLoop) return; // لا تستمع أثناء النطق
|
| 1236 |
+
setAvatarState('listening');
|
| 1237 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 1238 |
+
recognition = new SpeechRecognition();
|
| 1239 |
+
recognition.lang = 'en-US';
|
| 1240 |
+
recognition.continuous = false;
|
| 1241 |
+
recognition.interimResults = false;
|
| 1242 |
+
recognition.onresult = function(event) {
|
| 1243 |
+
const transcript = event.results[0][0].transcript;
|
| 1244 |
+
addMessage(transcript, 'user');
|
| 1245 |
+
handleAIResponse(transcript);
|
| 1246 |
+
};
|
| 1247 |
+
recognition.onerror = function(event) {
|
| 1248 |
+
setAvatarState('idle');
|
| 1249 |
+
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1250 |
+
setTimeout(startListening, 1000);
|
| 1251 |
+
}
|
| 1252 |
+
};
|
| 1253 |
+
recognition.onend = function() {
|
| 1254 |
+
// Do nothing, will be restarted after AI response
|
| 1255 |
+
};
|
| 1256 |
+
recognition.start();
|
| 1257 |
+
}
|
| 1258 |
+
|
| 1259 |
+
function handleAIResponse(userText) {
|
| 1260 |
+
setAvatarState('thinking');
|
| 1261 |
+
isSpeakingVoiceLoop = true;
|
| 1262 |
+
setVoiceControlButtonsState();
|
| 1263 |
+
fetch('http://127.0.0.1:5000/chat', {
|
| 1264 |
+
method: 'POST',
|
| 1265 |
+
headers: {'Content-Type': 'application/json'},
|
| 1266 |
+
body: JSON.stringify({message: userText})
|
| 1267 |
+
})
|
| 1268 |
+
.then(res => res.json())
|
| 1269 |
+
.then(data => {
|
| 1270 |
+
addMessage(data.reply, 'echo');
|
| 1271 |
+
setAvatarState('speaking');
|
| 1272 |
+
if (characterController && appState.soundEnabled) {
|
| 1273 |
+
// استخدم SpeechSynthesisUtterance مع حدث onend
|
| 1274 |
+
const utterance = new SpeechSynthesisUtterance(data.reply);
|
| 1275 |
+
utterance.lang = 'en-US';
|
| 1276 |
+
utterance.onend = () => {
|
| 1277 |
+
isSpeakingVoiceLoop = false;
|
| 1278 |
+
setAvatarState('idle');
|
| 1279 |
+
setVoiceControlButtonsState();
|
| 1280 |
+
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1281 |
+
setTimeout(startListening, 500);
|
| 1282 |
+
}
|
| 1283 |
+
};
|
| 1284 |
+
utterance.onerror = () => {
|
| 1285 |
+
isSpeakingVoiceLoop = false;
|
| 1286 |
+
setAvatarState('idle');
|
| 1287 |
+
setVoiceControlButtonsState();
|
| 1288 |
+
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1289 |
+
setTimeout(startListening, 500);
|
| 1290 |
+
}
|
| 1291 |
+
};
|
| 1292 |
+
speechSynthesis.speak(utterance);
|
| 1293 |
+
characterController.speak(data.reply); // لتحريك الفم
|
| 1294 |
+
} else {
|
| 1295 |
+
isSpeakingVoiceLoop = false;
|
| 1296 |
+
setAvatarState('idle');
|
| 1297 |
+
setVoiceControlButtonsState();
|
| 1298 |
+
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1299 |
+
setTimeout(startListening, 500);
|
| 1300 |
+
}
|
| 1301 |
+
}
|
| 1302 |
+
})
|
| 1303 |
+
.catch(() => {
|
| 1304 |
+
addMessage("Sorry, I couldn't connect to the AI server.", 'echo');
|
| 1305 |
+
isSpeakingVoiceLoop = false;
|
| 1306 |
+
setAvatarState('idle');
|
| 1307 |
+
setVoiceControlButtonsState();
|
| 1308 |
+
if (isAutoLoop && !isMuted && isConversationActive) {
|
| 1309 |
+
setTimeout(startListening, 1500);
|
| 1310 |
+
}
|
| 1311 |
+
});
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1315 |
+
// Initialize character controller
|
| 1316 |
+
characterController = new CharacterController();
|
| 1317 |
+
|
| 1318 |
+
// Initialize modes
|
| 1319 |
+
initializeModes();
|
| 1320 |
+
|
| 1321 |
+
// Initialize achievements
|
| 1322 |
+
initializeAchievements();
|
| 1323 |
+
|
| 1324 |
+
// Update progress display
|
| 1325 |
+
updateProgressDisplay();
|
| 1326 |
+
|
| 1327 |
+
// Welcome message
|
| 1328 |
+
setTimeout(() => {
|
| 1329 |
+
if (characterController) {
|
| 1330 |
+
characterController.speak("Welcome to Echo Light! I'm here to help you learn English in a fun and interactive way.");
|
| 1331 |
+
}
|
| 1332 |
+
}, 2000);
|
| 1333 |
+
|
| 1334 |
+
// Set up Enter key for chat
|
| 1335 |
+
const messageInput = document.getElementById('messageInput');
|
| 1336 |
+
if (messageInput) {
|
| 1337 |
+
messageInput.addEventListener('keypress', (e) => {
|
| 1338 |
+
if (e.key === 'Enter') {
|
| 1339 |
+
sendMessage();
|
| 1340 |
+
}
|
| 1341 |
+
});
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
// Check for achievements periodically
|
| 1345 |
+
setInterval(() => {
|
| 1346 |
+
checkAchievements();
|
| 1347 |
+
}, 10000);
|
| 1348 |
+
|
| 1349 |
+
// ربط الأزرار يدويًا بعد تحميل الصفحة
|
| 1350 |
+
const btnStart = document.querySelector('.voice-controls .btn--primary');
|
| 1351 |
+
const btnMute = document.querySelector('.voice-controls .btn--secondary');
|
| 1352 |
+
const btnEnd = document.querySelector('.voice-controls .btn--danger');
|
| 1353 |
+
if (btnStart) btnStart.onclick = startVoiceLoop;
|
| 1354 |
+
if (btnMute) btnMute.onclick = toggleMute;
|
| 1355 |
+
if (btnEnd) btnEnd.onclick = stopVoiceLoop;
|
| 1356 |
+
setVoiceControlButtonsState();
|
| 1357 |
+
});
|
| 1358 |
+
|
| 1359 |
+
// Export functions for global access
|
| 1360 |
+
window.showScreen = showScreen;
|
| 1361 |
+
window.sendMessage = sendMessage;
|
| 1362 |
+
window.startVoiceInput = startVoiceInput;
|
| 1363 |
+
window.flipCard = flipCard;
|
| 1364 |
+
window.nextCard = nextCard;
|
| 1365 |
+
window.playPronunciation = playPronunciation;
|
| 1366 |
+
window.playTargetPronunciation = playTargetPronunciation;
|
| 1367 |
+
window.toggleRecording = toggleRecording;
|
| 1368 |
+
window.selectGrammarOption = selectGrammarOption;
|
| 1369 |
+
window.nextGrammarQuestion = nextGrammarQuestion;
|
| 1370 |
+
window.showScenario = showScenario;
|
| 1371 |
+
window.practiceScenario = practiceScenario;
|
| 1372 |
+
window.selectWord = selectWord;
|
| 1373 |
+
window.toggleSound = toggleSound;
|
development_guide.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# دليل المطور لتطبيق Echo Light المتقدم
|
| 2 |
+
|
| 3 |
+
## نظرة عامة على البنية التقنية
|
| 4 |
+
|
| 5 |
+
### 1. شخصية Echo Light التفاعلية
|
| 6 |
+
|
| 7 |
+
#### الحركة العائمة المنتظمة
|
| 8 |
+
```css
|
| 9 |
+
@keyframes float {
|
| 10 |
+
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
| 11 |
+
50% { transform: translateY(-20px) rotate(2deg); }
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.echo-character {
|
| 15 |
+
animation: float 4s ease-in-out infinite;
|
| 16 |
+
will-change: transform;
|
| 17 |
+
}
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
#### نظام الحالات التفاعلية
|
| 21 |
+
```javascript
|
| 22 |
+
const characterStates = {
|
| 23 |
+
idle: { animation: 'float', expression: 'neutral' },
|
| 24 |
+
listening: { animation: 'float-focus', expression: 'attentive' },
|
| 25 |
+
speaking: { animation: 'float-talk', expression: 'animated' },
|
| 26 |
+
thinking: { animation: 'float-think', expression: 'thoughtful' }
|
| 27 |
+
};
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### 2. تقنيات تحسين الأداء للهواتف
|
| 31 |
+
|
| 32 |
+
#### CSS Transforms للحركات السلسة
|
| 33 |
+
- استخدام `transform` بدلاً من `left/top`
|
| 34 |
+
- تفعيل `will-change` للعناصر المتحركة
|
| 35 |
+
- استخدام `translate3d` لتفعيل تسريع GPU
|
| 36 |
+
|
| 37 |
+
#### إدارة الذاكرة
|
| 38 |
+
```javascript
|
| 39 |
+
// تحسين استهلاك الذاكرة
|
| 40 |
+
const performanceOptimizer = {
|
| 41 |
+
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
| 42 |
+
lowPowerMode: navigator.hardwareConcurrency <= 2,
|
| 43 |
+
|
| 44 |
+
adaptAnimations() {
|
| 45 |
+
if (this.reducedMotion || this.lowPowerMode) {
|
| 46 |
+
document.body.classList.add('reduced-motion');
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 3. نظام التعرف على الصوت المتقدم
|
| 53 |
+
|
| 54 |
+
#### تكامل Web Speech API
|
| 55 |
+
```javascript
|
| 56 |
+
class VoiceRecognition {
|
| 57 |
+
constructor() {
|
| 58 |
+
this.recognition = new webkitSpeechRecognition();
|
| 59 |
+
this.recognition.continuous = true;
|
| 60 |
+
this.recognition.interimResults = true;
|
| 61 |
+
this.recognition.lang = 'en-US';
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
startListening() {
|
| 65 |
+
this.recognition.start();
|
| 66 |
+
this.updateCharacterState('listening');
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
assessPronunciation(text, audio) {
|
| 70 |
+
// تقييم النطق باستخدام خوارزميات متقدمة
|
| 71 |
+
return this.calculateAccuracyScore(text, audio);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### 4. خوارزمية التكرار المباعد
|
| 77 |
+
|
| 78 |
+
#### تنفيذ نظام SM-2 المحسن
|
| 79 |
+
```javascript
|
| 80 |
+
class SpacedRepetition {
|
| 81 |
+
calculateNextReview(quality, interval, easeFactor) {
|
| 82 |
+
if (quality >= 3) {
|
| 83 |
+
if (interval === 0) {
|
| 84 |
+
interval = 1;
|
| 85 |
+
} else if (interval === 1) {
|
| 86 |
+
interval = 6;
|
| 87 |
+
} else {
|
| 88 |
+
interval = Math.round(interval * easeFactor);
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
interval = 1;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
easeFactor = easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
|
| 95 |
+
|
| 96 |
+
return { interval, easeFactor };
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### 5. نظام التحليل والإحصائيات
|
| 102 |
+
|
| 103 |
+
#### تتبع التقدم في الوقت الفعلي
|
| 104 |
+
```javascript
|
| 105 |
+
class ProgressTracker {
|
| 106 |
+
constructor() {
|
| 107 |
+
this.metrics = {
|
| 108 |
+
vocabularyLearned: 0,
|
| 109 |
+
pronunciationAccuracy: [],
|
| 110 |
+
grammarScores: [],
|
| 111 |
+
dailyStreaks: 0,
|
| 112 |
+
timeSpent: 0
|
| 113 |
+
};
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
updateProgress(category, value) {
|
| 117 |
+
this.metrics[category] = value;
|
| 118 |
+
this.generateInsights();
|
| 119 |
+
this.updateCharacterFeedback();
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
generateInsights() {
|
| 123 |
+
// تحليل البيانات وإنشاء رؤى تعليمية
|
| 124 |
+
const insights = this.analyzePerformance();
|
| 125 |
+
return insights;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### 6. نظام الإنجازات والتحفيز
|
| 131 |
+
|
| 132 |
+
#### آلية الشارات الذكية
|
| 133 |
+
```javascript
|
| 134 |
+
class AchievementSystem {
|
| 135 |
+
checkAchievements(userStats) {
|
| 136 |
+
const achievements = [];
|
| 137 |
+
|
| 138 |
+
// فحص الإنجازات المختلفة
|
| 139 |
+
if (userStats.vocabularyLearned >= 50) {
|
| 140 |
+
achievements.push('vocabulary_master');
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
if (userStats.dailyStreaks >= 7) {
|
| 144 |
+
achievements.push('daily_streak');
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return achievements;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
celebrateAchievement(achievementId) {
|
| 151 |
+
// تفعيل الاحتفال بالإنجاز
|
| 152 |
+
this.showAchievementAnimation(achievementId);
|
| 153 |
+
this.updateCharacterExpression('celebrating');
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### 7. التكيف مع مستوى المتعلم
|
| 159 |
+
|
| 160 |
+
#### نظام التعلم المكيف
|
| 161 |
+
```javascript
|
| 162 |
+
class AdaptiveLearning {
|
| 163 |
+
assessLearnerLevel(userHistory) {
|
| 164 |
+
const level = this.calculateLevel(userHistory);
|
| 165 |
+
return {
|
| 166 |
+
vocabulary: level.vocabulary,
|
| 167 |
+
grammar: level.grammar,
|
| 168 |
+
pronunciation: level.pronunciation,
|
| 169 |
+
comprehension: level.comprehension
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
adaptContent(currentLevel, performance) {
|
| 174 |
+
// تكييف المحتوى حسب مستوى المتعلم
|
| 175 |
+
if (performance.accuracy < 0.7) {
|
| 176 |
+
return this.getEasierContent(currentLevel);
|
| 177 |
+
} else if (performance.accuracy > 0.9) {
|
| 178 |
+
return this.getHarderContent(currentLevel);
|
| 179 |
+
}
|
| 180 |
+
return this.getCurrentLevelContent(currentLevel);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
### 8. التحسينات الأمنية
|
| 186 |
+
|
| 187 |
+
#### حماية البيانات الشخصية
|
| 188 |
+
```javascript
|
| 189 |
+
class DataSecurity {
|
| 190 |
+
encryptUserData(data) {
|
| 191 |
+
// تشفير بيانات المستخدم
|
| 192 |
+
return CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString();
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
validateInput(userInput) {
|
| 196 |
+
// التحقق من صحة المدخلات
|
| 197 |
+
return this.sanitizeInput(userInput);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
### 9. إرشادات التطوير المستقبلي
|
| 203 |
+
|
| 204 |
+
#### إضافة ميزات جديدة
|
| 205 |
+
1. **تعلم الآلة المحلي**: استخدام TensorFlow.js للتحليل المحلي
|
| 206 |
+
2. **الواقع المعزز**: دمج تقنيات AR للتعلم التفاعلي
|
| 207 |
+
3. **التعلم الاجتماعي**: إضافة ميزات التعلم الجماعي
|
| 208 |
+
4. **دعم لغات متعددة**: توسيع التطبيق ليشمل لغات أخرى
|
| 209 |
+
|
| 210 |
+
#### أفضل الممارسات
|
| 211 |
+
- استخدام مبدأ Progressive Web App
|
| 212 |
+
- تنفيذ Service Workers للعمل دون اتصال
|
| 213 |
+
- تحسين تجربة المستخدم على الشاشات المختلفة
|
| 214 |
+
- إجراء اختبارات الأداء المنتظمة
|
| 215 |
+
|
| 216 |
+
### 10. متطلبات النشر
|
| 217 |
+
|
| 218 |
+
#### البيئة المطلوبة
|
| 219 |
+
- Node.js 16+
|
| 220 |
+
- متصفح يدعم Web Speech API
|
| 221 |
+
- اتصال إنترنت للميزات المتقدمة
|
| 222 |
+
- ذاكرة RAM: 2GB على الأقل للهواتف
|
| 223 |
+
|
| 224 |
+
#### التوافق
|
| 225 |
+
- iOS Safari 12+
|
| 226 |
+
- Android Chrome 80+
|
| 227 |
+
- Desktop Chrome/Firefox/Edge الأحدث
|
| 228 |
+
|
| 229 |
+
هذا الدليل يوفر الأساس التقني لتطوير وتحسين تطبيق Echo Light المتقدم مع ضمان الأداء الأمثل والتجربة المتميزة للمستخدمين.
|
index.html
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Echo Light - تعلم الإنجليزية التفاعلي</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<!-- Animated Background -->
|
| 11 |
+
<div class="space-background">
|
| 12 |
+
<div class="stars"></div>
|
| 13 |
+
<div class="moving-particles"></div>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<!-- Main Application Container -->
|
| 17 |
+
<div class="app-container">
|
| 18 |
+
<!-- Header -->
|
| 19 |
+
<header class="app-header">
|
| 20 |
+
<div class="container flex items-center justify-between">
|
| 21 |
+
<div class="logo">
|
| 22 |
+
<h1 class="glow-text">Echo Light</h1>
|
| 23 |
+
<span class="tagline">معلمك الذكي لتعلم الإنجليزية</span>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="user-stats">
|
| 26 |
+
<div class="stat-item">
|
| 27 |
+
<span class="stat-value" id="streakCount">0</span>
|
| 28 |
+
<span class="stat-label">أيام متتالية</span>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="stat-item">
|
| 31 |
+
<span class="stat-value" id="totalPoints">0</span>
|
| 32 |
+
<span class="stat-label">نقاط</span>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</header>
|
| 37 |
+
|
| 38 |
+
<!-- Main Content -->
|
| 39 |
+
<main class="main-content">
|
| 40 |
+
<div class="container">
|
| 41 |
+
<!-- Home Screen -->
|
| 42 |
+
<section id="homeScreen" class="screen active">
|
| 43 |
+
<div class="home-layout">
|
| 44 |
+
<!-- Echo Light Character -->
|
| 45 |
+
<div class="character-container">
|
| 46 |
+
<div class="echo-character" id="echoCharacter">
|
| 47 |
+
<div class="character-body">
|
| 48 |
+
<div class="character-head">
|
| 49 |
+
<div class="eyes">
|
| 50 |
+
<div class="eye left-eye">
|
| 51 |
+
<div class="pupil"></div>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="eye right-eye">
|
| 54 |
+
<div class="pupil"></div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="mouth" id="characterMouth"></div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="character-glow"></div>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="sound-waves" id="soundWaves"></div>
|
| 62 |
+
<div class="light-particles"></div>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="welcome-message">
|
| 65 |
+
<p id="welcomeText">مرحباً! أنا Echo Light، معلمك الذكي لتعلم الإنجليزية</p>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<!-- Learning Modes -->
|
| 70 |
+
<div class="learning-modes">
|
| 71 |
+
<h2 class="section-title glow-text">اختر طريقة التعلم</h2>
|
| 72 |
+
<div class="modes-grid" id="modesGrid">
|
| 73 |
+
<!-- Modes will be generated by JavaScript -->
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</section>
|
| 78 |
+
|
| 79 |
+
<!-- Conversation Mode -->
|
| 80 |
+
<section id="conversationScreen" class="screen">
|
| 81 |
+
<div class="mode-header">
|
| 82 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 83 |
+
<h2 class="glow-text">المحادثة الحرة</h2>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="conversation-layout">
|
| 86 |
+
<div class="character-side">
|
| 87 |
+
<div class="echo-character mini">
|
| 88 |
+
<div class="character-body">
|
| 89 |
+
<div class="character-head">
|
| 90 |
+
<div class="eyes">
|
| 91 |
+
<div class="eye left-eye"><div class="pupil"></div></div>
|
| 92 |
+
<div class="eye right-eye"><div class="pupil"></div></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="mouth"></div>
|
| 95 |
+
</div>
|
| 96 |
+
<div class="character-glow"></div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
<div class="chat-area">
|
| 101 |
+
<div class="chat-messages" id="chatMessages"></div>
|
| 102 |
+
<div class="chat-input">
|
| 103 |
+
<input type="text" id="messageInput" placeholder="اكتب رسالتك هنا..." class="form-control">
|
| 104 |
+
<button class="btn btn--primary" onclick="sendMessage()">إرسال</button>
|
| 105 |
+
<button class="btn btn--outline voice-btn" onclick="startVoiceInput()">🎤</button>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<!-- Voice Conversation Controls -->
|
| 111 |
+
<div class="voice-controls" style="display: flex; gap: 10px; justify-content: center; margin: 20px 0;">
|
| 112 |
+
<button class="btn btn--primary" onclick="startVoiceLoop()" title="Start Voice Conversation">🎤 ابدأ المحادثة</button>
|
| 113 |
+
<button class="btn btn--secondary" onclick="toggleMute()" title="Mute/Unmute">🔇 كتم/إلغاء كتم</button>
|
| 114 |
+
<button class="btn btn--danger" onclick="stopVoiceLoop()" title="End Conversation">⏹️ إنهاء المحادثة</button>
|
| 115 |
+
</div>
|
| 116 |
+
</section>
|
| 117 |
+
|
| 118 |
+
<!-- Vocabulary Mode -->
|
| 119 |
+
<section id="vocabularyScreen" class="screen">
|
| 120 |
+
<div class="mode-header">
|
| 121 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 122 |
+
<h2 class="glow-text">بناء المفردات</h2>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="vocabulary-layout">
|
| 125 |
+
<div class="progress-bar">
|
| 126 |
+
<div class="progress-fill" id="vocabProgress"></div>
|
| 127 |
+
<span class="progress-text" id="vocabProgressText">0/5</span>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="flashcard" id="flashcard">
|
| 130 |
+
<div class="card-inner">
|
| 131 |
+
<div class="card-front">
|
| 132 |
+
<h3 id="cardWord">Hello</h3>
|
| 133 |
+
<p id="cardPronunciation">/həˈloʊ/</p>
|
| 134 |
+
<button class="btn btn--outline" onclick="playPronunciation()">🔊 استمع</button>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="card-back">
|
| 137 |
+
<h3 id="cardTranslation">مرحبا</h3>
|
| 138 |
+
<p id="cardExample">Hello, how are you?</p>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="card-controls">
|
| 143 |
+
<button class="btn btn--secondary" onclick="flipCard()">قلب البطاقة</button>
|
| 144 |
+
<button class="btn btn--primary" onclick="nextCard()">التالي</button>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</section>
|
| 148 |
+
|
| 149 |
+
<!-- Pronunciation Mode -->
|
| 150 |
+
<section id="pronunciationScreen" class="screen">
|
| 151 |
+
<div class="mode-header">
|
| 152 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 153 |
+
<h2 class="glow-text">تدريب النطق</h2>
|
| 154 |
+
</div>
|
| 155 |
+
<div class="pronunciation-layout">
|
| 156 |
+
<div class="practice-word">
|
| 157 |
+
<h3 id="practiceWord">Beautiful</h3>
|
| 158 |
+
<p id="practicePronunciation">/ˈbjuːtɪfəl/</p>
|
| 159 |
+
<button class="btn btn--outline" onclick="playTargetPronunciation()">🔊 النطق الصحيح</button>
|
| 160 |
+
</div>
|
| 161 |
+
<div class="recording-area">
|
| 162 |
+
<div class="record-visualizer" id="recordVisualizer"></div>
|
| 163 |
+
<button class="btn btn--primary record-btn" id="recordBtn" onclick="toggleRecording()">🎤 ابدأ التسجيل</button>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="pronunciation-feedback" id="pronunciationFeedback"></div>
|
| 166 |
+
</div>
|
| 167 |
+
</section>
|
| 168 |
+
|
| 169 |
+
<!-- Grammar Mode -->
|
| 170 |
+
<section id="grammarScreen" class="screen">
|
| 171 |
+
<div class="mode-header">
|
| 172 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 173 |
+
<h2 class="glow-text">ألعاب القواعد</h2>
|
| 174 |
+
</div>
|
| 175 |
+
<div class="grammar-layout">
|
| 176 |
+
<div class="question-card">
|
| 177 |
+
<h3 id="grammarQuestion">Choose the correct form: I ___ to school every day.</h3>
|
| 178 |
+
<div class="options-grid" id="grammarOptions"></div>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="grammar-feedback" id="grammarFeedback"></div>
|
| 181 |
+
</div>
|
| 182 |
+
</section>
|
| 183 |
+
|
| 184 |
+
<!-- Scenarios Mode -->
|
| 185 |
+
<section id="scenariosScreen" class="screen">
|
| 186 |
+
<div class="mode-header">
|
| 187 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 188 |
+
<h2 class="glow-text">سيناريوهات واقعية</h2>
|
| 189 |
+
</div>
|
| 190 |
+
<div class="scenarios-layout">
|
| 191 |
+
<div class="scenario-selector">
|
| 192 |
+
<h3>اختر السيناريو:</h3>
|
| 193 |
+
<div class="scenario-buttons" id="scenarioButtons"></div>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="scenario-dialogue" id="scenarioDialogue"></div>
|
| 196 |
+
</div>
|
| 197 |
+
</section>
|
| 198 |
+
|
| 199 |
+
<!-- Daily Challenge Mode -->
|
| 200 |
+
<section id="dailyScreen" class="screen">
|
| 201 |
+
<div class="mode-header">
|
| 202 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 203 |
+
<h2 class="glow-text">التحدي اليومي</h2>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="daily-layout">
|
| 206 |
+
<div class="challenge-header">
|
| 207 |
+
<h3>تحدي اليوم</h3>
|
| 208 |
+
<div class="challenge-timer" id="challengeTimer">05:00</div>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="challenge-content" id="challengeContent">
|
| 211 |
+
<p>أكمل الجملة التالية:</p>
|
| 212 |
+
<p class="challenge-sentence">"I ____ English every day because it's ____."</p>
|
| 213 |
+
<div class="word-bank">
|
| 214 |
+
<span class="word-option" onclick="selectWord(this)">study</span>
|
| 215 |
+
<span class="word-option" onclick="selectWord(this)">important</span>
|
| 216 |
+
<span class="word-option" onclick="selectWord(this)">learn</span>
|
| 217 |
+
<span class="word-option" onclick="selectWord(this)">interesting</span>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
<div class="daily-progress">
|
| 221 |
+
<div class="progress-bar">
|
| 222 |
+
<div class="progress-fill" id="dailyProgress"></div>
|
| 223 |
+
</div>
|
| 224 |
+
<p>التقدم: <span id="dailyProgressText">0/3</span></p>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
</section>
|
| 228 |
+
|
| 229 |
+
<!-- Progress Screen -->
|
| 230 |
+
<section id="progressScreen" class="screen">
|
| 231 |
+
<div class="mode-header">
|
| 232 |
+
<button class="btn btn--secondary back-btn" onclick="showScreen('homeScreen')">← العودة</button>
|
| 233 |
+
<h2 class="glow-text">تتبع التقدم</h2>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="progress-layout">
|
| 236 |
+
<div class="stats-grid">
|
| 237 |
+
<div class="stat-card">
|
| 238 |
+
<h3>الكلمات المتعلمة</h3>
|
| 239 |
+
<div class="stat-number" id="wordsLearned">0</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="stat-card">
|
| 242 |
+
<h3>المحادثات المكتملة</h3>
|
| 243 |
+
<div class="stat-number" id="conversationsCompleted">0</div>
|
| 244 |
+
</div>
|
| 245 |
+
<div class="stat-card">
|
| 246 |
+
<h3>دقة النطق</h3>
|
| 247 |
+
<div class="stat-number" id="pronunciationAccuracy">0%</div>
|
| 248 |
+
</div>
|
| 249 |
+
<div class="stat-card">
|
| 250 |
+
<h3>نقاط القواعد</h3>
|
| 251 |
+
<div class="stat-number" id="grammarScore">0</div>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="achievements-section">
|
| 255 |
+
<h3>الإنجازات</h3>
|
| 256 |
+
<div class="achievements-grid" id="achievementsGrid"></div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</section>
|
| 260 |
+
</div>
|
| 261 |
+
</main>
|
| 262 |
+
|
| 263 |
+
<!-- Quick Actions -->
|
| 264 |
+
<div class="quick-actions">
|
| 265 |
+
<button class="quick-btn" onclick="showScreen('progressScreen')" title="تتبع التقدم">
|
| 266 |
+
📊
|
| 267 |
+
</button>
|
| 268 |
+
<button class="quick-btn" onclick="toggleSound()" title="الصوت" id="soundToggle">
|
| 269 |
+
🔊
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<script src="app.js"></script>
|
| 275 |
+
</body>
|
| 276 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# requirements.txt for Docker/HuggingFace deployment
|
| 2 |
+
Flask
|
| 3 |
+
flask-cors
|
| 4 |
+
openai
|
| 5 |
+
python-dotenv
|
style.css
ADDED
|
@@ -0,0 +1,1647 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
:root {
|
| 3 |
+
/* Colors */
|
| 4 |
+
--color-background: rgba(252, 252, 249, 1);
|
| 5 |
+
--color-surface: rgba(255, 255, 253, 1);
|
| 6 |
+
--color-text: rgba(19, 52, 59, 1);
|
| 7 |
+
--color-text-secondary: rgba(98, 108, 113, 1);
|
| 8 |
+
--color-primary: rgba(33, 128, 141, 1);
|
| 9 |
+
--color-primary-hover: rgba(29, 116, 128, 1);
|
| 10 |
+
--color-primary-active: rgba(26, 104, 115, 1);
|
| 11 |
+
--color-secondary: rgba(94, 82, 64, 0.12);
|
| 12 |
+
--color-secondary-hover: rgba(94, 82, 64, 0.2);
|
| 13 |
+
--color-secondary-active: rgba(94, 82, 64, 0.25);
|
| 14 |
+
--color-border: rgba(94, 82, 64, 0.2);
|
| 15 |
+
--color-btn-primary-text: rgba(252, 252, 249, 1);
|
| 16 |
+
--color-card-border: rgba(94, 82, 64, 0.12);
|
| 17 |
+
--color-card-border-inner: rgba(94, 82, 64, 0.12);
|
| 18 |
+
--color-error: rgba(192, 21, 47, 1);
|
| 19 |
+
--color-success: rgba(33, 128, 141, 1);
|
| 20 |
+
--color-warning: rgba(168, 75, 47, 1);
|
| 21 |
+
--color-info: rgba(98, 108, 113, 1);
|
| 22 |
+
--color-focus-ring: rgba(33, 128, 141, 0.4);
|
| 23 |
+
--color-select-caret: rgba(19, 52, 59, 0.8);
|
| 24 |
+
|
| 25 |
+
/* Common style patterns */
|
| 26 |
+
--focus-ring: 0 0 0 3px var(--color-focus-ring);
|
| 27 |
+
--focus-outline: 2px solid var(--color-primary);
|
| 28 |
+
--status-bg-opacity: 0.15;
|
| 29 |
+
--status-border-opacity: 0.25;
|
| 30 |
+
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 31 |
+
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 32 |
+
|
| 33 |
+
/* RGB versions for opacity control */
|
| 34 |
+
--color-success-rgb: 33, 128, 141;
|
| 35 |
+
--color-error-rgb: 192, 21, 47;
|
| 36 |
+
--color-warning-rgb: 168, 75, 47;
|
| 37 |
+
--color-info-rgb: 98, 108, 113;
|
| 38 |
+
|
| 39 |
+
/* Typography */
|
| 40 |
+
--font-family-base: "FKGroteskNeue", "Geist", "Inter", -apple-system,
|
| 41 |
+
BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 42 |
+
--font-family-mono: "Berkeley Mono", ui-monospace, SFMono-Regular, Menlo,
|
| 43 |
+
Monaco, Consolas, monospace;
|
| 44 |
+
--font-size-xs: 11px;
|
| 45 |
+
--font-size-sm: 12px;
|
| 46 |
+
--font-size-base: 14px;
|
| 47 |
+
--font-size-md: 14px;
|
| 48 |
+
--font-size-lg: 16px;
|
| 49 |
+
--font-size-xl: 18px;
|
| 50 |
+
--font-size-2xl: 20px;
|
| 51 |
+
--font-size-3xl: 24px;
|
| 52 |
+
--font-size-4xl: 30px;
|
| 53 |
+
--font-weight-normal: 400;
|
| 54 |
+
--font-weight-medium: 500;
|
| 55 |
+
--font-weight-semibold: 550;
|
| 56 |
+
--font-weight-bold: 600;
|
| 57 |
+
--line-height-tight: 1.2;
|
| 58 |
+
--line-height-normal: 1.5;
|
| 59 |
+
--letter-spacing-tight: -0.01em;
|
| 60 |
+
|
| 61 |
+
/* Spacing */
|
| 62 |
+
--space-0: 0;
|
| 63 |
+
--space-1: 1px;
|
| 64 |
+
--space-2: 2px;
|
| 65 |
+
--space-4: 4px;
|
| 66 |
+
--space-6: 6px;
|
| 67 |
+
--space-8: 8px;
|
| 68 |
+
--space-10: 10px;
|
| 69 |
+
--space-12: 12px;
|
| 70 |
+
--space-16: 16px;
|
| 71 |
+
--space-20: 20px;
|
| 72 |
+
--space-24: 24px;
|
| 73 |
+
--space-32: 32px;
|
| 74 |
+
|
| 75 |
+
/* Border Radius */
|
| 76 |
+
--radius-sm: 6px;
|
| 77 |
+
--radius-base: 8px;
|
| 78 |
+
--radius-md: 10px;
|
| 79 |
+
--radius-lg: 12px;
|
| 80 |
+
--radius-full: 9999px;
|
| 81 |
+
|
| 82 |
+
/* Shadows */
|
| 83 |
+
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.02);
|
| 84 |
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
|
| 85 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04),
|
| 86 |
+
0 2px 4px -1px rgba(0, 0, 0, 0.02);
|
| 87 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04),
|
| 88 |
+
0 4px 6px -2px rgba(0, 0, 0, 0.02);
|
| 89 |
+
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
| 90 |
+
inset 0 -1px 0 rgba(0, 0, 0, 0.03);
|
| 91 |
+
|
| 92 |
+
/* Animation */
|
| 93 |
+
--duration-fast: 150ms;
|
| 94 |
+
--duration-normal: 250ms;
|
| 95 |
+
--ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
|
| 96 |
+
|
| 97 |
+
/* Layout */
|
| 98 |
+
--container-sm: 640px;
|
| 99 |
+
--container-md: 768px;
|
| 100 |
+
--container-lg: 1024px;
|
| 101 |
+
--container-xl: 1280px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* Dark mode colors */
|
| 105 |
+
@media (prefers-color-scheme: dark) {
|
| 106 |
+
:root {
|
| 107 |
+
--color-background: rgba(31, 33, 33, 1);
|
| 108 |
+
--color-surface: rgba(38, 40, 40, 1);
|
| 109 |
+
--color-text: rgba(245, 245, 245, 1);
|
| 110 |
+
--color-text-secondary: rgba(167, 169, 169, 0.7);
|
| 111 |
+
--color-primary: rgba(50, 184, 198, 1);
|
| 112 |
+
--color-primary-hover: rgba(45, 166, 178, 1);
|
| 113 |
+
--color-primary-active: rgba(41, 150, 161, 1);
|
| 114 |
+
--color-secondary: rgba(119, 124, 124, 0.15);
|
| 115 |
+
--color-secondary-hover: rgba(119, 124, 124, 0.25);
|
| 116 |
+
--color-secondary-active: rgba(119, 124, 124, 0.3);
|
| 117 |
+
--color-border: rgba(119, 124, 124, 0.3);
|
| 118 |
+
--color-error: rgba(255, 84, 89, 1);
|
| 119 |
+
--color-success: rgba(50, 184, 198, 1);
|
| 120 |
+
--color-warning: rgba(230, 129, 97, 1);
|
| 121 |
+
--color-info: rgba(167, 169, 169, 1);
|
| 122 |
+
--color-focus-ring: rgba(50, 184, 198, 0.4);
|
| 123 |
+
--color-btn-primary-text: rgba(19, 52, 59, 1);
|
| 124 |
+
--color-card-border: rgba(119, 124, 124, 0.2);
|
| 125 |
+
--color-card-border-inner: rgba(119, 124, 124, 0.15);
|
| 126 |
+
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
| 127 |
+
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
| 128 |
+
--button-border-secondary: rgba(119, 124, 124, 0.2);
|
| 129 |
+
--color-border-secondary: rgba(119, 124, 124, 0.2);
|
| 130 |
+
--color-select-caret: rgba(245, 245, 245, 0.8);
|
| 131 |
+
|
| 132 |
+
/* Common style patterns - updated for dark mode */
|
| 133 |
+
--focus-ring: 0 0 0 3px var(--color-focus-ring);
|
| 134 |
+
--focus-outline: 2px solid var(--color-primary);
|
| 135 |
+
--status-bg-opacity: 0.15;
|
| 136 |
+
--status-border-opacity: 0.25;
|
| 137 |
+
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 138 |
+
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 139 |
+
|
| 140 |
+
/* RGB versions for dark mode */
|
| 141 |
+
--color-success-rgb: 50, 184, 198;
|
| 142 |
+
--color-error-rgb: 255, 84, 89;
|
| 143 |
+
--color-warning-rgb: 230, 129, 97;
|
| 144 |
+
--color-info-rgb: 167, 169, 169;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Data attribute for manual theme switching */
|
| 149 |
+
[data-color-scheme="dark"] {
|
| 150 |
+
--color-background: rgba(31, 33, 33, 1);
|
| 151 |
+
--color-surface: rgba(38, 40, 40, 1);
|
| 152 |
+
--color-text: rgba(245, 245, 245, 1);
|
| 153 |
+
--color-text-secondary: rgba(167, 169, 169, 0.7);
|
| 154 |
+
--color-primary: rgba(50, 184, 198, 1);
|
| 155 |
+
--color-primary-hover: rgba(45, 166, 178, 1);
|
| 156 |
+
--color-primary-active: rgba(41, 150, 161, 1);
|
| 157 |
+
--color-secondary: rgba(119, 124, 124, 0.15);
|
| 158 |
+
--color-secondary-hover: rgba(119, 124, 124, 0.25);
|
| 159 |
+
--color-secondary-active: rgba(119, 124, 124, 0.3);
|
| 160 |
+
--color-border: rgba(119, 124, 124, 0.3);
|
| 161 |
+
--color-error: rgba(255, 84, 89, 1);
|
| 162 |
+
--color-success: rgba(50, 184, 198, 1);
|
| 163 |
+
--color-warning: rgba(230, 129, 97, 1);
|
| 164 |
+
--color-info: rgba(167, 169, 169, 1);
|
| 165 |
+
--color-focus-ring: rgba(50, 184, 198, 0.4);
|
| 166 |
+
--color-btn-primary-text: rgba(19, 52, 59, 1);
|
| 167 |
+
--color-card-border: rgba(119, 124, 124, 0.15);
|
| 168 |
+
--color-card-border-inner: rgba(119, 124, 124, 0.15);
|
| 169 |
+
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
| 170 |
+
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
| 171 |
+
--color-border-secondary: rgba(119, 124, 124, 0.2);
|
| 172 |
+
--color-select-caret: rgba(245, 245, 245, 0.8);
|
| 173 |
+
|
| 174 |
+
/* Common style patterns - updated for dark mode */
|
| 175 |
+
--focus-ring: 0 0 0 3px var(--color-focus-ring);
|
| 176 |
+
--focus-outline: 2px solid var(--color-primary);
|
| 177 |
+
--status-bg-opacity: 0.15;
|
| 178 |
+
--status-border-opacity: 0.25;
|
| 179 |
+
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 180 |
+
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 181 |
+
|
| 182 |
+
/* RGB versions for dark mode */
|
| 183 |
+
--color-success-rgb: 50, 184, 198;
|
| 184 |
+
--color-error-rgb: 255, 84, 89;
|
| 185 |
+
--color-warning-rgb: 230, 129, 97;
|
| 186 |
+
--color-info-rgb: 167, 169, 169;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
[data-color-scheme="light"] {
|
| 190 |
+
--color-background: rgba(252, 252, 249, 1);
|
| 191 |
+
--color-surface: rgba(255, 255, 253, 1);
|
| 192 |
+
--color-text: rgba(19, 52, 59, 1);
|
| 193 |
+
--color-text-secondary: rgba(98, 108, 113, 1);
|
| 194 |
+
--color-primary: rgba(33, 128, 141, 1);
|
| 195 |
+
--color-primary-hover: rgba(29, 116, 128, 1);
|
| 196 |
+
--color-primary-active: rgba(26, 104, 115, 1);
|
| 197 |
+
--color-secondary: rgba(94, 82, 64, 0.12);
|
| 198 |
+
--color-secondary-hover: rgba(94, 82, 64, 0.2);
|
| 199 |
+
--color-secondary-active: rgba(94, 82, 64, 0.25);
|
| 200 |
+
--color-border: rgba(94, 82, 64, 0.2);
|
| 201 |
+
--color-btn-primary-text: rgba(252, 252, 249, 1);
|
| 202 |
+
--color-card-border: rgba(94, 82, 64, 0.12);
|
| 203 |
+
--color-card-border-inner: rgba(94, 82, 64, 0.12);
|
| 204 |
+
--color-error: rgba(192, 21, 47, 1);
|
| 205 |
+
--color-success: rgba(33, 128, 141, 1);
|
| 206 |
+
--color-warning: rgba(168, 75, 47, 1);
|
| 207 |
+
--color-info: rgba(98, 108, 113, 1);
|
| 208 |
+
--color-focus-ring: rgba(33, 128, 141, 0.4);
|
| 209 |
+
|
| 210 |
+
/* RGB versions for light mode */
|
| 211 |
+
--color-success-rgb: 33, 128, 141;
|
| 212 |
+
--color-error-rgb: 192, 21, 47;
|
| 213 |
+
--color-warning-rgb: 168, 75, 47;
|
| 214 |
+
--color-info-rgb: 98, 108, 113;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/* Base styles */
|
| 218 |
+
html {
|
| 219 |
+
font-size: var(--font-size-base);
|
| 220 |
+
font-family: var(--font-family-base);
|
| 221 |
+
line-height: var(--line-height-normal);
|
| 222 |
+
color: var(--color-text);
|
| 223 |
+
background-color: var(--color-background);
|
| 224 |
+
-webkit-font-smoothing: antialiased;
|
| 225 |
+
box-sizing: border-box;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
body {
|
| 229 |
+
margin: 0;
|
| 230 |
+
padding: 0;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
*,
|
| 234 |
+
*::before,
|
| 235 |
+
*::after {
|
| 236 |
+
box-sizing: inherit;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Typography */
|
| 240 |
+
h1,
|
| 241 |
+
h2,
|
| 242 |
+
h3,
|
| 243 |
+
h4,
|
| 244 |
+
h5,
|
| 245 |
+
h6 {
|
| 246 |
+
margin: 0;
|
| 247 |
+
font-weight: var(--font-weight-semibold);
|
| 248 |
+
line-height: var(--line-height-tight);
|
| 249 |
+
color: var(--color-text);
|
| 250 |
+
letter-spacing: var(--letter-spacing-tight);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
h1 {
|
| 254 |
+
font-size: var(--font-size-4xl);
|
| 255 |
+
}
|
| 256 |
+
h2 {
|
| 257 |
+
font-size: var(--font-size-3xl);
|
| 258 |
+
}
|
| 259 |
+
h3 {
|
| 260 |
+
font-size: var(--font-size-2xl);
|
| 261 |
+
}
|
| 262 |
+
h4 {
|
| 263 |
+
font-size: var(--font-size-xl);
|
| 264 |
+
}
|
| 265 |
+
h5 {
|
| 266 |
+
font-size: var(--font-size-lg);
|
| 267 |
+
}
|
| 268 |
+
h6 {
|
| 269 |
+
font-size: var(--font-size-md);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
p {
|
| 273 |
+
margin: 0 0 var(--space-16) 0;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
a {
|
| 277 |
+
color: var(--color-primary);
|
| 278 |
+
text-decoration: none;
|
| 279 |
+
transition: color var(--duration-fast) var(--ease-standard);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
a:hover {
|
| 283 |
+
color: var(--color-primary-hover);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
code,
|
| 287 |
+
pre {
|
| 288 |
+
font-family: var(--font-family-mono);
|
| 289 |
+
font-size: calc(var(--font-size-base) * 0.95);
|
| 290 |
+
background-color: var(--color-secondary);
|
| 291 |
+
border-radius: var(--radius-sm);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
code {
|
| 295 |
+
padding: var(--space-1) var(--space-4);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
pre {
|
| 299 |
+
padding: var(--space-16);
|
| 300 |
+
margin: var(--space-16) 0;
|
| 301 |
+
overflow: auto;
|
| 302 |
+
border: 1px solid var(--color-border);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
pre code {
|
| 306 |
+
background: none;
|
| 307 |
+
padding: 0;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* Buttons */
|
| 311 |
+
.btn {
|
| 312 |
+
display: inline-flex;
|
| 313 |
+
align-items: center;
|
| 314 |
+
justify-content: center;
|
| 315 |
+
padding: var(--space-8) var(--space-16);
|
| 316 |
+
border-radius: var(--radius-base);
|
| 317 |
+
font-size: var(--font-size-base);
|
| 318 |
+
font-weight: 500;
|
| 319 |
+
line-height: 1.5;
|
| 320 |
+
cursor: pointer;
|
| 321 |
+
transition: all var(--duration-normal) var(--ease-standard);
|
| 322 |
+
border: none;
|
| 323 |
+
text-decoration: none;
|
| 324 |
+
position: relative;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.btn:focus-visible {
|
| 328 |
+
outline: none;
|
| 329 |
+
box-shadow: var(--focus-ring);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.btn--primary {
|
| 333 |
+
background: var(--color-primary);
|
| 334 |
+
color: var(--color-btn-primary-text);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.btn--primary:hover {
|
| 338 |
+
background: var(--color-primary-hover);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.btn--primary:active {
|
| 342 |
+
background: var(--color-primary-active);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.btn--secondary {
|
| 346 |
+
background: var(--color-secondary);
|
| 347 |
+
color: var(--color-text);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.btn--secondary:hover {
|
| 351 |
+
background: var(--color-secondary-hover);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.btn--secondary:active {
|
| 355 |
+
background: var(--color-secondary-active);
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.btn--outline {
|
| 359 |
+
background: transparent;
|
| 360 |
+
border: 1px solid var(--color-border);
|
| 361 |
+
color: var(--color-text);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.btn--outline:hover {
|
| 365 |
+
background: var(--color-secondary);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.btn--sm {
|
| 369 |
+
padding: var(--space-4) var(--space-12);
|
| 370 |
+
font-size: var(--font-size-sm);
|
| 371 |
+
border-radius: var(--radius-sm);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.btn--lg {
|
| 375 |
+
padding: var(--space-10) var(--space-20);
|
| 376 |
+
font-size: var(--font-size-lg);
|
| 377 |
+
border-radius: var(--radius-md);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.btn--full-width {
|
| 381 |
+
width: 100%;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.btn:disabled {
|
| 385 |
+
opacity: 0.5;
|
| 386 |
+
cursor: not-allowed;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/* Form elements */
|
| 390 |
+
.form-control {
|
| 391 |
+
display: block;
|
| 392 |
+
width: 100%;
|
| 393 |
+
padding: var(--space-8) var(--space-12);
|
| 394 |
+
font-size: var(--font-size-md);
|
| 395 |
+
line-height: 1.5;
|
| 396 |
+
color: var(--color-text);
|
| 397 |
+
background-color: var(--color-surface);
|
| 398 |
+
border: 1px solid var(--color-border);
|
| 399 |
+
border-radius: var(--radius-base);
|
| 400 |
+
transition: border-color var(--duration-fast) var(--ease-standard),
|
| 401 |
+
box-shadow var(--duration-fast) var(--ease-standard);
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
textarea.form-control {
|
| 405 |
+
font-family: var(--font-family-base);
|
| 406 |
+
font-size: var(--font-size-base);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
select.form-control {
|
| 410 |
+
padding: var(--space-8) var(--space-12);
|
| 411 |
+
-webkit-appearance: none;
|
| 412 |
+
-moz-appearance: none;
|
| 413 |
+
appearance: none;
|
| 414 |
+
background-image: var(--select-caret-light);
|
| 415 |
+
background-repeat: no-repeat;
|
| 416 |
+
background-position: right var(--space-12) center;
|
| 417 |
+
background-size: 16px;
|
| 418 |
+
padding-right: var(--space-32);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* Add a dark mode specific caret */
|
| 422 |
+
@media (prefers-color-scheme: dark) {
|
| 423 |
+
select.form-control {
|
| 424 |
+
background-image: var(--select-caret-dark);
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
/* Also handle data-color-scheme */
|
| 429 |
+
[data-color-scheme="dark"] select.form-control {
|
| 430 |
+
background-image: var(--select-caret-dark);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
[data-color-scheme="light"] select.form-control {
|
| 434 |
+
background-image: var(--select-caret-light);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.form-control:focus {
|
| 438 |
+
border-color: var(--color-primary);
|
| 439 |
+
outline: var(--focus-outline);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.form-label {
|
| 443 |
+
display: block;
|
| 444 |
+
margin-bottom: var(--space-8);
|
| 445 |
+
font-weight: var(--font-weight-medium);
|
| 446 |
+
font-size: var(--font-size-sm);
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.form-group {
|
| 450 |
+
margin-bottom: var(--space-16);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Card component */
|
| 454 |
+
.card {
|
| 455 |
+
background-color: var(--color-surface);
|
| 456 |
+
border-radius: var(--radius-lg);
|
| 457 |
+
border: 1px solid var(--color-card-border);
|
| 458 |
+
box-shadow: var(--shadow-sm);
|
| 459 |
+
overflow: hidden;
|
| 460 |
+
transition: box-shadow var(--duration-normal) var(--ease-standard);
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.card:hover {
|
| 464 |
+
box-shadow: var(--shadow-md);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.card__body {
|
| 468 |
+
padding: var(--space-16);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.card__header,
|
| 472 |
+
.card__footer {
|
| 473 |
+
padding: var(--space-16);
|
| 474 |
+
border-bottom: 1px solid var(--color-card-border-inner);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/* Status indicators - simplified with CSS variables */
|
| 478 |
+
.status {
|
| 479 |
+
display: inline-flex;
|
| 480 |
+
align-items: center;
|
| 481 |
+
padding: var(--space-6) var(--space-12);
|
| 482 |
+
border-radius: var(--radius-full);
|
| 483 |
+
font-weight: var(--font-weight-medium);
|
| 484 |
+
font-size: var(--font-size-sm);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.status--success {
|
| 488 |
+
background-color: rgba(
|
| 489 |
+
var(--color-success-rgb, 33, 128, 141),
|
| 490 |
+
var(--status-bg-opacity)
|
| 491 |
+
);
|
| 492 |
+
color: var(--color-success);
|
| 493 |
+
border: 1px solid
|
| 494 |
+
rgba(var(--color-success-rgb, 33, 128, 141), var(--status-border-opacity));
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.status--error {
|
| 498 |
+
background-color: rgba(
|
| 499 |
+
var(--color-error-rgb, 192, 21, 47),
|
| 500 |
+
var(--status-bg-opacity)
|
| 501 |
+
);
|
| 502 |
+
color: var(--color-error);
|
| 503 |
+
border: 1px solid
|
| 504 |
+
rgba(var(--color-error-rgb, 192, 21, 47), var(--status-border-opacity));
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.status--warning {
|
| 508 |
+
background-color: rgba(
|
| 509 |
+
var(--color-warning-rgb, 168, 75, 47),
|
| 510 |
+
var(--status-bg-opacity)
|
| 511 |
+
);
|
| 512 |
+
color: var(--color-warning);
|
| 513 |
+
border: 1px solid
|
| 514 |
+
rgba(var(--color-warning-rgb, 168, 75, 47), var(--status-border-opacity));
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.status--info {
|
| 518 |
+
background-color: rgba(
|
| 519 |
+
var(--color-info-rgb, 98, 108, 113),
|
| 520 |
+
var(--status-bg-opacity)
|
| 521 |
+
);
|
| 522 |
+
color: var(--color-info);
|
| 523 |
+
border: 1px solid
|
| 524 |
+
rgba(var(--color-info-rgb, 98, 108, 113), var(--status-border-opacity));
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* Container layout */
|
| 528 |
+
.container {
|
| 529 |
+
width: 100%;
|
| 530 |
+
margin-right: auto;
|
| 531 |
+
margin-left: auto;
|
| 532 |
+
padding-right: var(--space-16);
|
| 533 |
+
padding-left: var(--space-16);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
@media (min-width: 640px) {
|
| 537 |
+
.container {
|
| 538 |
+
max-width: var(--container-sm);
|
| 539 |
+
}
|
| 540 |
+
}
|
| 541 |
+
@media (min-width: 768px) {
|
| 542 |
+
.container {
|
| 543 |
+
max-width: var(--container-md);
|
| 544 |
+
}
|
| 545 |
+
}
|
| 546 |
+
@media (min-width: 1024px) {
|
| 547 |
+
.container {
|
| 548 |
+
max-width: var(--container-lg);
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
@media (min-width: 1280px) {
|
| 552 |
+
.container {
|
| 553 |
+
max-width: var(--container-xl);
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
/* Utility classes */
|
| 558 |
+
.flex {
|
| 559 |
+
display: flex;
|
| 560 |
+
}
|
| 561 |
+
.flex-col {
|
| 562 |
+
flex-direction: column;
|
| 563 |
+
}
|
| 564 |
+
.items-center {
|
| 565 |
+
align-items: center;
|
| 566 |
+
}
|
| 567 |
+
.justify-center {
|
| 568 |
+
justify-content: center;
|
| 569 |
+
}
|
| 570 |
+
.justify-between {
|
| 571 |
+
justify-content: space-between;
|
| 572 |
+
}
|
| 573 |
+
.gap-4 {
|
| 574 |
+
gap: var(--space-4);
|
| 575 |
+
}
|
| 576 |
+
.gap-8 {
|
| 577 |
+
gap: var(--space-8);
|
| 578 |
+
}
|
| 579 |
+
.gap-16 {
|
| 580 |
+
gap: var(--space-16);
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.m-0 {
|
| 584 |
+
margin: 0;
|
| 585 |
+
}
|
| 586 |
+
.mt-8 {
|
| 587 |
+
margin-top: var(--space-8);
|
| 588 |
+
}
|
| 589 |
+
.mb-8 {
|
| 590 |
+
margin-bottom: var(--space-8);
|
| 591 |
+
}
|
| 592 |
+
.mx-8 {
|
| 593 |
+
margin-left: var(--space-8);
|
| 594 |
+
margin-right: var(--space-8);
|
| 595 |
+
}
|
| 596 |
+
.my-8 {
|
| 597 |
+
margin-top: var(--space-8);
|
| 598 |
+
margin-bottom: var(--space-8);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.p-0 {
|
| 602 |
+
padding: 0;
|
| 603 |
+
}
|
| 604 |
+
.py-8 {
|
| 605 |
+
padding-top: var(--space-8);
|
| 606 |
+
padding-bottom: var(--space-8);
|
| 607 |
+
}
|
| 608 |
+
.px-8 {
|
| 609 |
+
padding-left: var(--space-8);
|
| 610 |
+
padding-right: var(--space-8);
|
| 611 |
+
}
|
| 612 |
+
.py-16 {
|
| 613 |
+
padding-top: var(--space-16);
|
| 614 |
+
padding-bottom: var(--space-16);
|
| 615 |
+
}
|
| 616 |
+
.px-16 {
|
| 617 |
+
padding-left: var(--space-16);
|
| 618 |
+
padding-right: var(--space-16);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.block {
|
| 622 |
+
display: block;
|
| 623 |
+
}
|
| 624 |
+
.hidden {
|
| 625 |
+
display: none;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* Accessibility */
|
| 629 |
+
.sr-only {
|
| 630 |
+
position: absolute;
|
| 631 |
+
width: 1px;
|
| 632 |
+
height: 1px;
|
| 633 |
+
padding: 0;
|
| 634 |
+
margin: -1px;
|
| 635 |
+
overflow: hidden;
|
| 636 |
+
clip: rect(0, 0, 0, 0);
|
| 637 |
+
white-space: nowrap;
|
| 638 |
+
border-width: 0;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
:focus-visible {
|
| 642 |
+
outline: var(--focus-outline);
|
| 643 |
+
outline-offset: 2px;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
/* Dark mode specifics */
|
| 647 |
+
[data-color-scheme="dark"] .btn--outline {
|
| 648 |
+
border: 1px solid var(--color-border-secondary);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
@font-face {
|
| 652 |
+
font-family: 'FKGroteskNeue';
|
| 653 |
+
src: url('https://r2cdn.perplexity.ai/fonts/FKGroteskNeue.woff2')
|
| 654 |
+
format('woff2');
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
/* Space Background Animation */
|
| 658 |
+
.space-background {
|
| 659 |
+
position: fixed;
|
| 660 |
+
top: 0;
|
| 661 |
+
left: 0;
|
| 662 |
+
width: 100%;
|
| 663 |
+
height: 100%;
|
| 664 |
+
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 50%, #16213e 100%);
|
| 665 |
+
z-index: -1;
|
| 666 |
+
overflow: hidden;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.stars {
|
| 670 |
+
position: absolute;
|
| 671 |
+
width: 100%;
|
| 672 |
+
height: 100%;
|
| 673 |
+
background-image:
|
| 674 |
+
radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,0.8), transparent),
|
| 675 |
+
radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.6), transparent),
|
| 676 |
+
radial-gradient(1px 1px at 90px 40px, rgba(255,255,255,0.4), transparent),
|
| 677 |
+
radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.7), transparent),
|
| 678 |
+
radial-gradient(2px 2px at 160px 30px, rgba(255,255,255,0.5), transparent);
|
| 679 |
+
background-repeat: repeat;
|
| 680 |
+
background-size: 200px 100px;
|
| 681 |
+
animation: twinkle 4s linear infinite;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.moving-particles {
|
| 685 |
+
position: absolute;
|
| 686 |
+
width: 100%;
|
| 687 |
+
height: 100%;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.moving-particles::before,
|
| 691 |
+
.moving-particles::after {
|
| 692 |
+
content: '';
|
| 693 |
+
position: absolute;
|
| 694 |
+
width: 4px;
|
| 695 |
+
height: 4px;
|
| 696 |
+
background: rgba(50, 184, 198, 0.6);
|
| 697 |
+
border-radius: 50%;
|
| 698 |
+
animation: float-particle 8s linear infinite;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.moving-particles::before {
|
| 702 |
+
top: 20%;
|
| 703 |
+
left: 10%;
|
| 704 |
+
animation-delay: -2s;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.moving-particles::after {
|
| 708 |
+
top: 60%;
|
| 709 |
+
right: 15%;
|
| 710 |
+
animation-delay: -4s;
|
| 711 |
+
background: rgba(138, 43, 226, 0.6);
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
@keyframes twinkle {
|
| 715 |
+
0%, 100% { opacity: 1; }
|
| 716 |
+
50% { opacity: 0.5; }
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
@keyframes float-particle {
|
| 720 |
+
0% { transform: translateY(100vh) scale(0); }
|
| 721 |
+
10% { transform: translateY(90vh) scale(1); }
|
| 722 |
+
90% { transform: translateY(-10vh) scale(1); }
|
| 723 |
+
100% { transform: translateY(-20vh) scale(0); }
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
/* App Layout */
|
| 727 |
+
.app-container {
|
| 728 |
+
min-height: 100vh;
|
| 729 |
+
position: relative;
|
| 730 |
+
z-index: 1;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.app-header {
|
| 734 |
+
padding: var(--space-16) 0;
|
| 735 |
+
background: rgba(0, 0, 0, 0.3);
|
| 736 |
+
backdrop-filter: blur(10px);
|
| 737 |
+
border-bottom: 1px solid rgba(50, 184, 198, 0.3);
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
.logo h1 {
|
| 741 |
+
color: #32b8c6;
|
| 742 |
+
text-shadow: 0 0 20px rgba(50, 184, 198, 0.6);
|
| 743 |
+
margin: 0;
|
| 744 |
+
font-size: var(--font-size-3xl);
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.tagline {
|
| 748 |
+
color: var(--color-text-secondary);
|
| 749 |
+
font-size: var(--font-size-sm);
|
| 750 |
+
display: block;
|
| 751 |
+
margin-top: var(--space-4);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.user-stats {
|
| 755 |
+
display: flex;
|
| 756 |
+
gap: var(--space-24);
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.stat-item {
|
| 760 |
+
text-align: center;
|
| 761 |
+
color: var(--color-text);
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.stat-value {
|
| 765 |
+
display: block;
|
| 766 |
+
font-size: var(--font-size-2xl);
|
| 767 |
+
font-weight: var(--font-weight-bold);
|
| 768 |
+
color: #32b8c6;
|
| 769 |
+
text-shadow: 0 0 10px rgba(50, 184, 198, 0.4);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.stat-label {
|
| 773 |
+
font-size: var(--font-size-xs);
|
| 774 |
+
opacity: 0.8;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
/* Glow Text Effect */
|
| 778 |
+
.glow-text {
|
| 779 |
+
color: #32b8c6;
|
| 780 |
+
text-shadow: 0 0 20px rgba(50, 184, 198, 0.6);
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* Screen Management */
|
| 784 |
+
.screen {
|
| 785 |
+
display: none;
|
| 786 |
+
padding: var(--space-32) 0;
|
| 787 |
+
min-height: calc(100vh - 120px);
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.screen.active {
|
| 791 |
+
display: block;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
/* Home Screen Layout */
|
| 795 |
+
.home-layout {
|
| 796 |
+
display: grid;
|
| 797 |
+
grid-template-columns: 1fr 1fr;
|
| 798 |
+
gap: var(--space-32);
|
| 799 |
+
align-items: center;
|
| 800 |
+
min-height: 70vh;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
/* Echo Light Character */
|
| 804 |
+
.character-container {
|
| 805 |
+
display: flex;
|
| 806 |
+
flex-direction: column;
|
| 807 |
+
align-items: center;
|
| 808 |
+
justify-content: center;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.echo-character {
|
| 812 |
+
width: 300px;
|
| 813 |
+
height: 300px;
|
| 814 |
+
position: relative;
|
| 815 |
+
animation: float 3s ease-in-out infinite;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
.echo-character.mini {
|
| 819 |
+
width: 150px;
|
| 820 |
+
height: 150px;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.character-body {
|
| 824 |
+
width: 100%;
|
| 825 |
+
height: 100%;
|
| 826 |
+
position: relative;
|
| 827 |
+
background: linear-gradient(135deg, #32b8c6 0%, #8a2be2 100%);
|
| 828 |
+
border-radius: 50%;
|
| 829 |
+
box-shadow:
|
| 830 |
+
0 0 50px rgba(50, 184, 198, 0.4),
|
| 831 |
+
inset 0 0 50px rgba(138, 43, 226, 0.2);
|
| 832 |
+
animation: character-rotate 6s linear infinite;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.character-head {
|
| 836 |
+
position: absolute;
|
| 837 |
+
top: 20%;
|
| 838 |
+
left: 50%;
|
| 839 |
+
transform: translateX(-50%);
|
| 840 |
+
width: 80%;
|
| 841 |
+
height: 60%;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.eyes {
|
| 845 |
+
display: flex;
|
| 846 |
+
justify-content: space-between;
|
| 847 |
+
width: 60%;
|
| 848 |
+
margin: 0 auto;
|
| 849 |
+
padding-top: 20px;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.eye {
|
| 853 |
+
width: 30px;
|
| 854 |
+
height: 30px;
|
| 855 |
+
background: white;
|
| 856 |
+
border-radius: 50%;
|
| 857 |
+
position: relative;
|
| 858 |
+
animation: blink 4s infinite;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.pupil {
|
| 862 |
+
width: 15px;
|
| 863 |
+
height: 15px;
|
| 864 |
+
background: #1a0a2e;
|
| 865 |
+
border-radius: 50%;
|
| 866 |
+
position: absolute;
|
| 867 |
+
top: 50%;
|
| 868 |
+
left: 50%;
|
| 869 |
+
transform: translate(-50%, -50%);
|
| 870 |
+
animation: eye-movement 3s ease-in-out infinite;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
.mouth {
|
| 874 |
+
width: 40px;
|
| 875 |
+
height: 20px;
|
| 876 |
+
background: #ff6b9d;
|
| 877 |
+
border-radius: 0 0 40px 40px;
|
| 878 |
+
position: absolute;
|
| 879 |
+
bottom: 30%;
|
| 880 |
+
left: 50%;
|
| 881 |
+
transform: translateX(-50%);
|
| 882 |
+
animation: mouth-talk 2s ease-in-out infinite;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.character-glow {
|
| 886 |
+
position: absolute;
|
| 887 |
+
top: -20px;
|
| 888 |
+
left: -20px;
|
| 889 |
+
right: -20px;
|
| 890 |
+
bottom: -20px;
|
| 891 |
+
background: radial-gradient(circle, rgba(50, 184, 198, 0.3) 0%, transparent 70%);
|
| 892 |
+
border-radius: 50%;
|
| 893 |
+
animation: glow-pulse 2s ease-in-out infinite alternate;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.sound-waves {
|
| 897 |
+
position: absolute;
|
| 898 |
+
top: 50%;
|
| 899 |
+
left: 50%;
|
| 900 |
+
transform: translate(-50%, -50%);
|
| 901 |
+
width: 100%;
|
| 902 |
+
height: 100%;
|
| 903 |
+
pointer-events: none;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
.sound-waves::before,
|
| 907 |
+
.sound-waves::after {
|
| 908 |
+
content: '';
|
| 909 |
+
position: absolute;
|
| 910 |
+
border: 2px solid rgba(50, 184, 198, 0.4);
|
| 911 |
+
border-radius: 50%;
|
| 912 |
+
animation: sound-wave 2s ease-out infinite;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.sound-waves::before {
|
| 916 |
+
width: 120%;
|
| 917 |
+
height: 120%;
|
| 918 |
+
top: -10%;
|
| 919 |
+
left: -10%;
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
.sound-waves::after {
|
| 923 |
+
width: 140%;
|
| 924 |
+
height: 140%;
|
| 925 |
+
top: -20%;
|
| 926 |
+
left: -20%;
|
| 927 |
+
animation-delay: 0.5s;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.light-particles {
|
| 931 |
+
position: absolute;
|
| 932 |
+
width: 100%;
|
| 933 |
+
height: 100%;
|
| 934 |
+
pointer-events: none;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
.light-particles::before,
|
| 938 |
+
.light-particles::after {
|
| 939 |
+
content: '';
|
| 940 |
+
position: absolute;
|
| 941 |
+
width: 4px;
|
| 942 |
+
height: 4px;
|
| 943 |
+
background: rgba(255, 107, 157, 0.8);
|
| 944 |
+
border-radius: 50%;
|
| 945 |
+
animation: particle-orbit 4s linear infinite;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.light-particles::before {
|
| 949 |
+
top: 20%;
|
| 950 |
+
left: 20%;
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
.light-particles::after {
|
| 954 |
+
bottom: 20%;
|
| 955 |
+
right: 20%;
|
| 956 |
+
animation-delay: 2s;
|
| 957 |
+
background: rgba(138, 43, 226, 0.8);
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
/* Character Animations */
|
| 961 |
+
@keyframes float {
|
| 962 |
+
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
| 963 |
+
50% { transform: translateY(-20px) rotate(2deg); }
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
@keyframes character-rotate {
|
| 967 |
+
0% { transform: rotate(0deg); }
|
| 968 |
+
100% { transform: rotate(360deg); }
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
@keyframes blink {
|
| 972 |
+
0%, 90%, 100% { transform: scaleY(1); }
|
| 973 |
+
95% { transform: scaleY(0.1); }
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
@keyframes eye-movement {
|
| 977 |
+
0%, 100% { transform: translate(-50%, -50%); }
|
| 978 |
+
25% { transform: translate(-30%, -50%); }
|
| 979 |
+
75% { transform: translate(-70%, -50%); }
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
@keyframes mouth-talk {
|
| 983 |
+
0%, 100% { transform: translateX(-50%) scaleY(1); }
|
| 984 |
+
50% { transform: translateX(-50%) scaleY(0.5); }
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
@keyframes glow-pulse {
|
| 988 |
+
0% { opacity: 0.3; }
|
| 989 |
+
100% { opacity: 0.7; }
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
@keyframes sound-wave {
|
| 993 |
+
0% { transform: scale(1); opacity: 1; }
|
| 994 |
+
100% { transform: scale(1.5); opacity: 0; }
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
@keyframes particle-orbit {
|
| 998 |
+
0% { transform: rotate(0deg) translateX(150px) rotate(0deg); }
|
| 999 |
+
100% { transform: rotate(360deg) translateX(150px) rotate(-360deg); }
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
/* Welcome Message */
|
| 1003 |
+
.welcome-message {
|
| 1004 |
+
margin-top: var(--space-32);
|
| 1005 |
+
text-align: center;
|
| 1006 |
+
max-width: 400px;
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
.welcome-message p {
|
| 1010 |
+
color: var(--color-text);
|
| 1011 |
+
font-size: var(--font-size-lg);
|
| 1012 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1013 |
+
padding: var(--space-16);
|
| 1014 |
+
border-radius: var(--radius-lg);
|
| 1015 |
+
backdrop-filter: blur(10px);
|
| 1016 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
/* Learning Modes */
|
| 1020 |
+
.learning-modes {
|
| 1021 |
+
text-align: center;
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
.section-title {
|
| 1025 |
+
margin-bottom: var(--space-32);
|
| 1026 |
+
font-size: var(--font-size-3xl);
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
.modes-grid {
|
| 1030 |
+
display: grid;
|
| 1031 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 1032 |
+
gap: var(--space-24);
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
.mode-card {
|
| 1036 |
+
background: rgba(0, 0, 0, 0.4);
|
| 1037 |
+
backdrop-filter: blur(10px);
|
| 1038 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1039 |
+
border-radius: var(--radius-lg);
|
| 1040 |
+
padding: var(--space-24);
|
| 1041 |
+
cursor: pointer;
|
| 1042 |
+
transition: all var(--duration-normal) var(--ease-standard);
|
| 1043 |
+
text-align: center;
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
.mode-card:hover {
|
| 1047 |
+
transform: translateY(-5px);
|
| 1048 |
+
border-color: rgba(50, 184, 198, 0.6);
|
| 1049 |
+
box-shadow: 0 10px 30px rgba(50, 184, 198, 0.2);
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
.mode-icon {
|
| 1053 |
+
font-size: var(--font-size-4xl);
|
| 1054 |
+
margin-bottom: var(--space-16);
|
| 1055 |
+
display: block;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
.mode-title {
|
| 1059 |
+
color: #32b8c6;
|
| 1060 |
+
font-size: var(--font-size-xl);
|
| 1061 |
+
margin-bottom: var(--space-8);
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
.mode-description {
|
| 1065 |
+
color: var(--color-text-secondary);
|
| 1066 |
+
font-size: var(--font-size-sm);
|
| 1067 |
+
line-height: 1.6;
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
/* Mode Headers */
|
| 1071 |
+
.mode-header {
|
| 1072 |
+
display: flex;
|
| 1073 |
+
align-items: center;
|
| 1074 |
+
gap: var(--space-16);
|
| 1075 |
+
margin-bottom: var(--space-32);
|
| 1076 |
+
padding-bottom: var(--space-16);
|
| 1077 |
+
border-bottom: 1px solid rgba(50, 184, 198, 0.3);
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
.mode-header h2 {
|
| 1081 |
+
flex: 1;
|
| 1082 |
+
text-align: center;
|
| 1083 |
+
margin: 0;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
.back-btn {
|
| 1087 |
+
background: rgba(0, 0, 0, 0.3) !important;
|
| 1088 |
+
border: 1px solid rgba(50, 184, 198, 0.3) !important;
|
| 1089 |
+
color: var(--color-text) !important;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
/* Conversation Layout */
|
| 1093 |
+
.conversation-layout {
|
| 1094 |
+
display: grid;
|
| 1095 |
+
grid-template-columns: 200px 1fr;
|
| 1096 |
+
gap: var(--space-32);
|
| 1097 |
+
height: 60vh;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
.character-side {
|
| 1101 |
+
display: flex;
|
| 1102 |
+
justify-content: center;
|
| 1103 |
+
align-items: flex-start;
|
| 1104 |
+
padding-top: var(--space-32);
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.chat-area {
|
| 1108 |
+
display: flex;
|
| 1109 |
+
flex-direction: column;
|
| 1110 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1111 |
+
border-radius: var(--radius-lg);
|
| 1112 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1113 |
+
overflow: hidden;
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
.chat-messages {
|
| 1117 |
+
flex: 1;
|
| 1118 |
+
padding: var(--space-16);
|
| 1119 |
+
overflow-y: auto;
|
| 1120 |
+
display: flex;
|
| 1121 |
+
flex-direction: column;
|
| 1122 |
+
gap: var(--space-12);
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
.message {
|
| 1126 |
+
max-width: 80%;
|
| 1127 |
+
padding: var(--space-12) var(--space-16);
|
| 1128 |
+
border-radius: var(--radius-lg);
|
| 1129 |
+
word-wrap: break-word;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
.message.user {
|
| 1133 |
+
align-self: flex-end;
|
| 1134 |
+
background: linear-gradient(135deg, #32b8c6, #8a2be2);
|
| 1135 |
+
color: white;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.message.echo {
|
| 1139 |
+
align-self: flex-start;
|
| 1140 |
+
background: rgba(50, 184, 198, 0.1);
|
| 1141 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1142 |
+
color: var(--color-text);
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
.chat-input {
|
| 1146 |
+
display: flex;
|
| 1147 |
+
gap: var(--space-8);
|
| 1148 |
+
padding: var(--space-16);
|
| 1149 |
+
border-top: 1px solid rgba(50, 184, 198, 0.3);
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.chat-input .form-control {
|
| 1153 |
+
flex: 1;
|
| 1154 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1155 |
+
border-color: rgba(50, 184, 198, 0.3);
|
| 1156 |
+
color: var(--color-text);
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
.voice-btn {
|
| 1160 |
+
min-width: 50px;
|
| 1161 |
+
}
|
| 1162 |
+
|
| 1163 |
+
/* Vocabulary Layout */
|
| 1164 |
+
.vocabulary-layout {
|
| 1165 |
+
display: flex;
|
| 1166 |
+
flex-direction: column;
|
| 1167 |
+
align-items: center;
|
| 1168 |
+
gap: var(--space-32);
|
| 1169 |
+
max-width: 600px;
|
| 1170 |
+
margin: 0 auto;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
.progress-bar {
|
| 1174 |
+
width: 100%;
|
| 1175 |
+
height: 10px;
|
| 1176 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1177 |
+
border-radius: var(--radius-full);
|
| 1178 |
+
overflow: hidden;
|
| 1179 |
+
position: relative;
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
.progress-fill {
|
| 1183 |
+
height: 100%;
|
| 1184 |
+
background: linear-gradient(90deg, #32b8c6, #8a2be2);
|
| 1185 |
+
border-radius: var(--radius-full);
|
| 1186 |
+
transition: width var(--duration-normal) var(--ease-standard);
|
| 1187 |
+
width: 0%;
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
.progress-text {
|
| 1191 |
+
position: absolute;
|
| 1192 |
+
top: 50%;
|
| 1193 |
+
left: 50%;
|
| 1194 |
+
transform: translate(-50%, -50%);
|
| 1195 |
+
color: var(--color-text);
|
| 1196 |
+
font-size: var(--font-size-sm);
|
| 1197 |
+
font-weight: var(--font-weight-bold);
|
| 1198 |
+
}
|
| 1199 |
+
|
| 1200 |
+
.flashcard {
|
| 1201 |
+
width: 400px;
|
| 1202 |
+
height: 250px;
|
| 1203 |
+
perspective: 1000px;
|
| 1204 |
+
cursor: pointer;
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
.card-inner {
|
| 1208 |
+
position: relative;
|
| 1209 |
+
width: 100%;
|
| 1210 |
+
height: 100%;
|
| 1211 |
+
text-align: center;
|
| 1212 |
+
transition: transform 0.6s;
|
| 1213 |
+
transform-style: preserve-3d;
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
.flashcard.flipped .card-inner {
|
| 1217 |
+
transform: rotateY(180deg);
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
.card-front, .card-back {
|
| 1221 |
+
position: absolute;
|
| 1222 |
+
width: 100%;
|
| 1223 |
+
height: 100%;
|
| 1224 |
+
backface-visibility: hidden;
|
| 1225 |
+
background: rgba(0, 0, 0, 0.4);
|
| 1226 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1227 |
+
border-radius: var(--radius-lg);
|
| 1228 |
+
display: flex;
|
| 1229 |
+
flex-direction: column;
|
| 1230 |
+
align-items: center;
|
| 1231 |
+
justify-content: center;
|
| 1232 |
+
gap: var(--space-16);
|
| 1233 |
+
padding: var(--space-24);
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
.card-back {
|
| 1237 |
+
transform: rotateY(180deg);
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
.card-front h3, .card-back h3 {
|
| 1241 |
+
color: #32b8c6;
|
| 1242 |
+
font-size: var(--font-size-3xl);
|
| 1243 |
+
margin: 0;
|
| 1244 |
+
}
|
| 1245 |
+
|
| 1246 |
+
.card-controls {
|
| 1247 |
+
display: flex;
|
| 1248 |
+
gap: var(--space-16);
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
/* Pronunciation Layout */
|
| 1252 |
+
.pronunciation-layout {
|
| 1253 |
+
display: flex;
|
| 1254 |
+
flex-direction: column;
|
| 1255 |
+
align-items: center;
|
| 1256 |
+
gap: var(--space-32);
|
| 1257 |
+
max-width: 600px;
|
| 1258 |
+
margin: 0 auto;
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.practice-word {
|
| 1262 |
+
text-align: center;
|
| 1263 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1264 |
+
padding: var(--space-32);
|
| 1265 |
+
border-radius: var(--radius-lg);
|
| 1266 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
.practice-word h3 {
|
| 1270 |
+
color: #32b8c6;
|
| 1271 |
+
font-size: var(--font-size-4xl);
|
| 1272 |
+
margin-bottom: var(--space-16);
|
| 1273 |
+
}
|
| 1274 |
+
|
| 1275 |
+
.recording-area {
|
| 1276 |
+
text-align: center;
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
.record-visualizer {
|
| 1280 |
+
width: 200px;
|
| 1281 |
+
height: 100px;
|
| 1282 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1283 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1284 |
+
border-radius: var(--radius-lg);
|
| 1285 |
+
margin-bottom: var(--space-16);
|
| 1286 |
+
position: relative;
|
| 1287 |
+
overflow: hidden;
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
.record-btn {
|
| 1291 |
+
font-size: var(--font-size-lg);
|
| 1292 |
+
padding: var(--space-16) var(--space-32);
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
.record-btn.recording {
|
| 1296 |
+
background: #ff6b9d !important;
|
| 1297 |
+
animation: pulse 1s infinite;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
.pronunciation-feedback {
|
| 1301 |
+
width: 100%;
|
| 1302 |
+
padding: var(--space-16);
|
| 1303 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1304 |
+
border-radius: var(--radius-lg);
|
| 1305 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1306 |
+
text-align: center;
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
@keyframes pulse {
|
| 1310 |
+
0%, 100% { transform: scale(1); }
|
| 1311 |
+
50% { transform: scale(1.05); }
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
/* Grammar Layout */
|
| 1315 |
+
.grammar-layout {
|
| 1316 |
+
max-width: 800px;
|
| 1317 |
+
margin: 0 auto;
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
.question-card {
|
| 1321 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1322 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1323 |
+
border-radius: var(--radius-lg);
|
| 1324 |
+
padding: var(--space-32);
|
| 1325 |
+
margin-bottom: var(--space-32);
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
.question-card h3 {
|
| 1329 |
+
color: var(--color-text);
|
| 1330 |
+
font-size: var(--font-size-xl);
|
| 1331 |
+
margin-bottom: var(--space-24);
|
| 1332 |
+
text-align: center;
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
+
.options-grid {
|
| 1336 |
+
display: grid;
|
| 1337 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 1338 |
+
gap: var(--space-16);
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
.option-btn {
|
| 1342 |
+
padding: var(--space-16);
|
| 1343 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1344 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1345 |
+
color: var(--color-text);
|
| 1346 |
+
border-radius: var(--radius-base);
|
| 1347 |
+
cursor: pointer;
|
| 1348 |
+
transition: all var(--duration-fast) var(--ease-standard);
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
.option-btn:hover {
|
| 1352 |
+
background: rgba(50, 184, 198, 0.1);
|
| 1353 |
+
border-color: rgba(50, 184, 198, 0.6);
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
.option-btn.correct {
|
| 1357 |
+
background: rgba(46, 160, 67, 0.2);
|
| 1358 |
+
border-color: #2ea043;
|
| 1359 |
+
color: #2ea043;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.option-btn.incorrect {
|
| 1363 |
+
background: rgba(218, 54, 51, 0.2);
|
| 1364 |
+
border-color: #da3633;
|
| 1365 |
+
color: #da3633;
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
.grammar-feedback {
|
| 1369 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1370 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1371 |
+
border-radius: var(--radius-lg);
|
| 1372 |
+
padding: var(--space-24);
|
| 1373 |
+
text-align: center;
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
/* Quick Actions */
|
| 1377 |
+
.quick-actions {
|
| 1378 |
+
position: fixed;
|
| 1379 |
+
bottom: var(--space-24);
|
| 1380 |
+
right: var(--space-24);
|
| 1381 |
+
display: flex;
|
| 1382 |
+
flex-direction: column;
|
| 1383 |
+
gap: var(--space-12);
|
| 1384 |
+
z-index: 1000;
|
| 1385 |
+
}
|
| 1386 |
+
|
| 1387 |
+
.quick-btn {
|
| 1388 |
+
width: 60px;
|
| 1389 |
+
height: 60px;
|
| 1390 |
+
border-radius: 50%;
|
| 1391 |
+
background: rgba(0, 0, 0, 0.6);
|
| 1392 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1393 |
+
color: var(--color-text);
|
| 1394 |
+
font-size: var(--font-size-xl);
|
| 1395 |
+
cursor: pointer;
|
| 1396 |
+
backdrop-filter: blur(10px);
|
| 1397 |
+
transition: all var(--duration-fast) var(--ease-standard);
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
.quick-btn:hover {
|
| 1401 |
+
background: rgba(50, 184, 198, 0.2);
|
| 1402 |
+
border-color: rgba(50, 184, 198, 0.6);
|
| 1403 |
+
transform: scale(1.1);
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
+
/* Responsive Design */
|
| 1407 |
+
@media (max-width: 768px) {
|
| 1408 |
+
.home-layout {
|
| 1409 |
+
grid-template-columns: 1fr;
|
| 1410 |
+
gap: var(--space-24);
|
| 1411 |
+
text-align: center;
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
.echo-character {
|
| 1415 |
+
width: 250px;
|
| 1416 |
+
height: 250px;
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
+
.modes-grid {
|
| 1420 |
+
grid-template-columns: 1fr;
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
.conversation-layout {
|
| 1424 |
+
grid-template-columns: 1fr;
|
| 1425 |
+
gap: var(--space-16);
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
.character-side {
|
| 1429 |
+
order: -1;
|
| 1430 |
+
padding-top: 0;
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
.flashcard {
|
| 1434 |
+
width: 100%;
|
| 1435 |
+
max-width: 350px;
|
| 1436 |
+
}
|
| 1437 |
+
|
| 1438 |
+
.user-stats {
|
| 1439 |
+
gap: var(--space-16);
|
| 1440 |
+
}
|
| 1441 |
+
|
| 1442 |
+
.quick-actions {
|
| 1443 |
+
bottom: var(--space-16);
|
| 1444 |
+
right: var(--space-16);
|
| 1445 |
+
}
|
| 1446 |
+
|
| 1447 |
+
.quick-btn {
|
| 1448 |
+
width: 50px;
|
| 1449 |
+
height: 50px;
|
| 1450 |
+
font-size: var(--font-size-lg);
|
| 1451 |
+
}
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
/* Additional Styles for Scenarios */
|
| 1455 |
+
.scenarios-layout {
|
| 1456 |
+
max-width: 800px;
|
| 1457 |
+
margin: 0 auto;
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
.scenario-selector {
|
| 1461 |
+
margin-bottom: var(--space-32);
|
| 1462 |
+
}
|
| 1463 |
+
|
| 1464 |
+
.scenario-buttons {
|
| 1465 |
+
display: flex;
|
| 1466 |
+
gap: var(--space-16);
|
| 1467 |
+
flex-wrap: wrap;
|
| 1468 |
+
justify-content: center;
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
.scenario-btn {
|
| 1472 |
+
padding: var(--space-12) var(--space-24);
|
| 1473 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1474 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1475 |
+
color: var(--color-text);
|
| 1476 |
+
border-radius: var(--radius-lg);
|
| 1477 |
+
cursor: pointer;
|
| 1478 |
+
transition: all var(--duration-fast) var(--ease-standard);
|
| 1479 |
+
}
|
| 1480 |
+
|
| 1481 |
+
.scenario-btn:hover, .scenario-btn.active {
|
| 1482 |
+
background: rgba(50, 184, 198, 0.2);
|
| 1483 |
+
border-color: rgba(50, 184, 198, 0.6);
|
| 1484 |
+
}
|
| 1485 |
+
|
| 1486 |
+
.scenario-dialogue {
|
| 1487 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1488 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1489 |
+
border-radius: var(--radius-lg);
|
| 1490 |
+
padding: var(--space-24);
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
.dialogue-line {
|
| 1494 |
+
margin-bottom: var(--space-16);
|
| 1495 |
+
padding: var(--space-12);
|
| 1496 |
+
border-radius: var(--radius-base);
|
| 1497 |
+
}
|
| 1498 |
+
|
| 1499 |
+
.dialogue-line.waiter, .dialogue-line.interviewer {
|
| 1500 |
+
background: rgba(50, 184, 198, 0.1);
|
| 1501 |
+
border-left: 3px solid #32b8c6;
|
| 1502 |
+
}
|
| 1503 |
+
|
| 1504 |
+
.dialogue-line.customer, .dialogue-line.candidate {
|
| 1505 |
+
background: rgba(138, 43, 226, 0.1);
|
| 1506 |
+
border-left: 3px solid #8a2be2;
|
| 1507 |
+
margin-left: var(--space-24);
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
/* Daily Challenge Styles */
|
| 1511 |
+
.daily-layout {
|
| 1512 |
+
max-width: 600px;
|
| 1513 |
+
margin: 0 auto;
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
.challenge-header {
|
| 1517 |
+
text-align: center;
|
| 1518 |
+
margin-bottom: var(--space-32);
|
| 1519 |
+
}
|
| 1520 |
+
|
| 1521 |
+
.challenge-timer {
|
| 1522 |
+
font-size: var(--font-size-3xl);
|
| 1523 |
+
color: #ff6b9d;
|
| 1524 |
+
font-weight: var(--font-weight-bold);
|
| 1525 |
+
text-shadow: 0 0 10px rgba(255, 107, 157, 0.4);
|
| 1526 |
+
}
|
| 1527 |
+
|
| 1528 |
+
.challenge-content {
|
| 1529 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1530 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1531 |
+
border-radius: var(--radius-lg);
|
| 1532 |
+
padding: var(--space-32);
|
| 1533 |
+
margin-bottom: var(--space-32);
|
| 1534 |
+
text-align: center;
|
| 1535 |
+
}
|
| 1536 |
+
|
| 1537 |
+
.challenge-sentence {
|
| 1538 |
+
font-size: var(--font-size-xl);
|
| 1539 |
+
color: #32b8c6;
|
| 1540 |
+
margin: var(--space-24) 0;
|
| 1541 |
+
}
|
| 1542 |
+
|
| 1543 |
+
.word-bank {
|
| 1544 |
+
display: flex;
|
| 1545 |
+
gap: var(--space-12);
|
| 1546 |
+
justify-content: center;
|
| 1547 |
+
flex-wrap: wrap;
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
.word-option {
|
| 1551 |
+
padding: var(--space-8) var(--space-16);
|
| 1552 |
+
background: rgba(138, 43, 226, 0.2);
|
| 1553 |
+
border: 1px solid #8a2be2;
|
| 1554 |
+
color: #8a2be2;
|
| 1555 |
+
border-radius: var(--radius-base);
|
| 1556 |
+
cursor: pointer;
|
| 1557 |
+
transition: all var(--duration-fast) var(--ease-standard);
|
| 1558 |
+
}
|
| 1559 |
+
|
| 1560 |
+
.word-option:hover {
|
| 1561 |
+
background: rgba(138, 43, 226, 0.3);
|
| 1562 |
+
transform: translateY(-2px);
|
| 1563 |
+
}
|
| 1564 |
+
|
| 1565 |
+
.word-option.selected {
|
| 1566 |
+
background: #8a2be2;
|
| 1567 |
+
color: white;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
/* Progress Screen Styles */
|
| 1571 |
+
.progress-layout {
|
| 1572 |
+
max-width: 800px;
|
| 1573 |
+
margin: 0 auto;
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
.stats-grid {
|
| 1577 |
+
display: grid;
|
| 1578 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 1579 |
+
gap: var(--space-24);
|
| 1580 |
+
margin-bottom: var(--space-32);
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
.stat-card {
|
| 1584 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1585 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1586 |
+
border-radius: var(--radius-lg);
|
| 1587 |
+
padding: var(--space-24);
|
| 1588 |
+
text-align: center;
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
.stat-card h3 {
|
| 1592 |
+
color: var(--color-text);
|
| 1593 |
+
margin-bottom: var(--space-16);
|
| 1594 |
+
font-size: var(--font-size-lg);
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
.stat-number {
|
| 1598 |
+
font-size: var(--font-size-4xl);
|
| 1599 |
+
font-weight: var(--font-weight-bold);
|
| 1600 |
+
color: #32b8c6;
|
| 1601 |
+
text-shadow: 0 0 10px rgba(50, 184, 198, 0.4);
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
.achievements-section h3 {
|
| 1605 |
+
color: #32b8c6;
|
| 1606 |
+
margin-bottom: var(--space-24);
|
| 1607 |
+
text-align: center;
|
| 1608 |
+
}
|
| 1609 |
+
|
| 1610 |
+
.achievements-grid {
|
| 1611 |
+
display: grid;
|
| 1612 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 1613 |
+
gap: var(--space-16);
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
.achievement-card {
|
| 1617 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1618 |
+
border: 1px solid rgba(50, 184, 198, 0.3);
|
| 1619 |
+
border-radius: var(--radius-lg);
|
| 1620 |
+
padding: var(--space-16);
|
| 1621 |
+
text-align: center;
|
| 1622 |
+
opacity: 0.5;
|
| 1623 |
+
transition: all var(--duration-fast) var(--ease-standard);
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
.achievement-card.unlocked {
|
| 1627 |
+
opacity: 1;
|
| 1628 |
+
border-color: rgba(255, 215, 0, 0.6);
|
| 1629 |
+
box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
|
| 1630 |
+
}
|
| 1631 |
+
|
| 1632 |
+
.achievement-icon {
|
| 1633 |
+
font-size: var(--font-size-3xl);
|
| 1634 |
+
margin-bottom: var(--space-8);
|
| 1635 |
+
display: block;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
.achievement-name {
|
| 1639 |
+
color: #32b8c6;
|
| 1640 |
+
font-weight: var(--font-weight-bold);
|
| 1641 |
+
margin-bottom: var(--space-4);
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
.achievement-description {
|
| 1645 |
+
color: var(--color-text-secondary);
|
| 1646 |
+
font-size: var(--font-size-sm);
|
| 1647 |
+
}
|
test_echo_light_app.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from selenium import webdriver
|
| 3 |
+
from selenium.webdriver.common.by import By
|
| 4 |
+
from selenium.webdriver.common.keys import Keys
|
| 5 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
| 6 |
+
from selenium.webdriver.support import expected_conditions as EC
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
class EchoLightAppTest(unittest.TestCase):
|
| 10 |
+
@classmethod
|
| 11 |
+
def setUpClass(cls):
|
| 12 |
+
# Adjust the path to your chromedriver if needed
|
| 13 |
+
cls.driver = webdriver.Chrome()
|
| 14 |
+
cls.driver.get('http://localhost:5500/index.html') # Use your local server/port
|
| 15 |
+
cls.driver.maximize_window()
|
| 16 |
+
cls.wait = WebDriverWait(cls.driver, 10)
|
| 17 |
+
|
| 18 |
+
@classmethod
|
| 19 |
+
def tearDownClass(cls):
|
| 20 |
+
cls.driver.quit()
|
| 21 |
+
|
| 22 |
+
def test_1_text_chat(self):
|
| 23 |
+
driver = self.driver
|
| 24 |
+
wait = self.wait
|
| 25 |
+
# Find input and send a message
|
| 26 |
+
input_box = wait.until(EC.presence_of_element_located((By.ID, 'messageInput')))
|
| 27 |
+
input_box.clear()
|
| 28 |
+
input_box.send_keys('Hello, how are you?')
|
| 29 |
+
input_box.send_keys(Keys.ENTER)
|
| 30 |
+
# Wait for echo reply
|
| 31 |
+
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, '.message.echo')) > 0)
|
| 32 |
+
messages = driver.find_elements(By.CSS_SELECTOR, '.message.echo')
|
| 33 |
+
self.assertTrue(any('how are you' in m.text.lower() or m.text for m in messages))
|
| 34 |
+
|
| 35 |
+
def test_2_empty_message(self):
|
| 36 |
+
driver = self.driver
|
| 37 |
+
wait = self.wait
|
| 38 |
+
input_box = wait.until(EC.presence_of_element_located((By.ID, 'messageInput')))
|
| 39 |
+
input_box.clear()
|
| 40 |
+
input_box.send_keys(' ')
|
| 41 |
+
input_box.send_keys(Keys.ENTER)
|
| 42 |
+
# No new user message should be added
|
| 43 |
+
time.sleep(1)
|
| 44 |
+
user_msgs = driver.find_elements(By.CSS_SELECTOR, '.message.user')
|
| 45 |
+
self.assertFalse(user_msgs[-1].text.strip() == '')
|
| 46 |
+
|
| 47 |
+
def test_3_modes_navigation(self):
|
| 48 |
+
driver = self.driver
|
| 49 |
+
wait = self.wait
|
| 50 |
+
# Modes grid
|
| 51 |
+
modes = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.mode-card')))
|
| 52 |
+
for mode in modes:
|
| 53 |
+
mode.click()
|
| 54 |
+
time.sleep(0.5)
|
| 55 |
+
# Check that the screen is active
|
| 56 |
+
active = driver.find_elements(By.CSS_SELECTOR, '.screen.active')
|
| 57 |
+
self.assertTrue(len(active) > 0)
|
| 58 |
+
|
| 59 |
+
def test_4_vocabulary_card(self):
|
| 60 |
+
driver = self.driver
|
| 61 |
+
wait = self.wait
|
| 62 |
+
# Go to vocabulary mode
|
| 63 |
+
vocab_mode = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.mode-card')))[1]
|
| 64 |
+
vocab_mode.click()
|
| 65 |
+
time.sleep(0.5)
|
| 66 |
+
# Check card content
|
| 67 |
+
word = driver.find_element(By.ID, 'cardWord').text
|
| 68 |
+
self.assertTrue(len(word) > 0)
|
| 69 |
+
# Next card
|
| 70 |
+
next_btn = driver.find_element(By.ID, 'nextCardBtn') if driver.find_elements(By.ID, 'nextCardBtn') else None
|
| 71 |
+
if next_btn:
|
| 72 |
+
next_btn.click()
|
| 73 |
+
time.sleep(0.5)
|
| 74 |
+
new_word = driver.find_element(By.ID, 'cardWord').text
|
| 75 |
+
self.assertNotEqual(word, new_word)
|
| 76 |
+
|
| 77 |
+
def test_5_achievements(self):
|
| 78 |
+
driver = self.driver
|
| 79 |
+
wait = self.wait
|
| 80 |
+
# Go to achievements/progress screen
|
| 81 |
+
progress_btn = driver.find_element(By.ID, 'progressBtn') if driver.find_elements(By.ID, 'progressBtn') else None
|
| 82 |
+
if progress_btn:
|
| 83 |
+
progress_btn.click()
|
| 84 |
+
time.sleep(0.5)
|
| 85 |
+
achievements = driver.find_elements(By.CSS_SELECTOR, '.achievement-card')
|
| 86 |
+
self.assertTrue(len(achievements) > 0)
|
| 87 |
+
|
| 88 |
+
if __name__ == '__main__':
|
| 89 |
+
unittest.main()
|