feat: added application files
Browse files- .gitattributes +5 -0
- Dockerfile +39 -0
- PRD.md +776 -0
- README.md +244 -12
- app.py +1417 -0
- examples/001-visual-proof-probability-of-an-odd-sum.json +62 -0
- examples/002-visual-proof-pythagorean-theorem.json +70 -0
- examples/003-visual-proof-area-of-quadrilaterals-with-perpendicular-diagonals.json +56 -0
- examples/004-visual-proof-area-ratios-in-quadrilaterals.json +56 -0
- examples/005-visual-proof-sum-of-rhombus-diagonals.json +51 -0
- examples/006-visual-proof-altitude-to-hypotenuse-ratio.json +110 -0
- img/architecture-diagram.jpg +0 -0
- img/architecture.jpg +3 -0
- img/gradio-ui-user-flow.jpg +3 -0
- img/hero.jpg +3 -0
- img/mcp-server-flow.jpg +3 -0
- img/programmatic-flow.jpg +3 -0
- requirements.txt +4 -0
- saved_proofs/.gitkeep +1 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
img/architecture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
img/gradio-ui-user-flow.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
img/hero.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
img/mcp-server-flow.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
img/programmatic-flow.jpg filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
git \
|
| 9 |
+
git-lfs \
|
| 10 |
+
ffmpeg \
|
| 11 |
+
libsm6 \
|
| 12 |
+
libxext6 \
|
| 13 |
+
cmake \
|
| 14 |
+
rsync \
|
| 15 |
+
libgl1 \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 17 |
+
&& git lfs install
|
| 18 |
+
|
| 19 |
+
# Copy requirements first (for better caching)
|
| 20 |
+
COPY requirements.txt .
|
| 21 |
+
|
| 22 |
+
# Fresh install of Gradio 6 stable (released Nov 21, 2025)
|
| 23 |
+
# Install in a single command to avoid conflicts
|
| 24 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 25 |
+
pip install --no-cache-dir "gradio[mcp,oauth]==6.0.0" && \
|
| 26 |
+
pip install --no-cache-dir -r requirements.txt
|
| 27 |
+
|
| 28 |
+
# Copy application code
|
| 29 |
+
COPY . .
|
| 30 |
+
|
| 31 |
+
# Expose port
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
# Set environment variables
|
| 35 |
+
ENV GRADIO_SERVER_NAME="0.0.0.0"
|
| 36 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 37 |
+
|
| 38 |
+
# Run the application
|
| 39 |
+
CMD ["python", "app.py"]
|
PRD.md
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **Product Requirements Document: StepWise Math (Gradio MCP Implementation)**
|
| 2 |
+
|
| 3 |
+
| Document Info | Details |
|
| 4 |
+
| :--------------------- | :------------------------------------------------------------------------------- |
|
| 5 |
+
| **Product Name** | StepWise Math - Gradio MCP Framework |
|
| 6 |
+
| **Version** | 2.0 (MCP Server with Two-Stage Pipeline) |
|
| 7 |
+
| **Status** | Active |
|
| 8 |
+
| **Target Demographic** | Middle School (Grades 6-8) to High School (Grades 9-10) |
|
| 9 |
+
| **Tech Stack** | **Gradio 6.0+**, Python, **Google Gemini 2.5 Flash & 3.0 Pro**, **MCP Protocol** |
|
| 10 |
+
| **Deployment** | Hugging Face Spaces, Docker, Local Development |
|
| 11 |
+
|
| 12 |
+
## **1. Executive Summary**
|
| 13 |
+
|
| 14 |
+
**StepWise Math** is a Gradio-based web application with **Model Context Protocol (MCP) server capabilities** that converts static mathematical problems—supplied via text, screenshots, or URLs—into **interactive, step-by-step visual proofs**.
|
| 15 |
+
|
| 16 |
+
Unlike a calculator that just gives the answer, StepWise Math builds a bespoke HTML5 application that guides students through the logical stages of a proof or concept. It breaks down complex ideas into incremental steps (e.g., "Step 1: Construct the shape", "Step 2: Apply the transformation", "Step 3: Observe the result"), allowing students to manipulate variables at each stage to internalize the logic.
|
| 17 |
+
|
| 18 |
+
### **Key Architecture Features**
|
| 19 |
+
|
| 20 |
+
* **MCP Server Integration:** Exposes proof generation tools via the Model Context Protocol, enabling AI agents and external tools to programmatically generate mathematical proofs
|
| 21 |
+
* **Two-Stage AI Pipeline:** Separates concept analysis (fast, ~10-15s) from code generation (moderate, ~20-30s) to comply with MCP timeout constraints
|
| 22 |
+
* **Gradio Framework:** Provides both web UI and MCP server endpoints through a single application instance
|
| 23 |
+
* **Docker-Ready:** Fully containerized for deployment to Hugging Face Spaces or any Docker-compatible environment
|
| 24 |
+
|
| 25 |
+
## **2. Target Audience**
|
| 26 |
+
|
| 27 |
+
* **Students (Grades 6-10):** Visual learners who need structured guidance to understand abstract concepts in Geometry, Algebra, and Trigonometry.
|
| 28 |
+
* **Math Teachers:** Need "digital manipulatives" that walk the class through a concept phase-by-phase.
|
| 29 |
+
* **Tutors:** Need to generate custom step-by-step explanations for specific homework problems.
|
| 30 |
+
* **AI Agents & Developers:** Can programmatically generate mathematical proofs via MCP protocol integration for educational tools, chatbots, or automated tutoring systems.
|
| 31 |
+
|
| 32 |
+
## **3. User Flow & Experience**
|
| 33 |
+
|
| 34 |
+
### **3.1 High-Level Flow (Gradio Web UI)**
|
| 35 |
+
|
| 36 |
+
1. **Initial Load:** Application automatically loads first example proof to demonstrate capabilities
|
| 37 |
+
2. **Input Selection:** User chooses input mode (Text, URL, or Image) via Gradio interface
|
| 38 |
+
3. **Content Entry:** User provides the mathematical concept or problem
|
| 39 |
+
4. **API Key (Optional):** User can override default API key in settings
|
| 40 |
+
5. **Two-Stage Generation:**
|
| 41 |
+
- **Stage 1:** Concept Analysis (Gemini 2.5 Flash, ~10-15s) → JSON Specification
|
| 42 |
+
- **Stage 2:** Code Generation (Gemini 3.0 Pro, ~20-30s) → Interactive HTML/JS Application
|
| 43 |
+
6. **Visualization:** Generated proof displays in iframe with step navigation
|
| 44 |
+
7. **Actions:** Save to library, export as JSON, or refine with feedback
|
| 45 |
+
|
| 46 |
+

|
| 47 |
+
### **3.2 MCP Server Flow (Programmatic Access)**
|
| 48 |
+
|
| 49 |
+
1. **Tool Discovery:** AI agent connects to Gradio MCP server endpoint
|
| 50 |
+
2. **Available Tools:**
|
| 51 |
+
- `analyze_concept_from_text`: Analyzes text-based mathematical concept → returns JSON spec
|
| 52 |
+
- `analyze_concept_from_url`: Analyzes concept from URL → returns JSON spec
|
| 53 |
+
- `analyze_concept_from_image`: Analyzes concept from image → returns JSON spec
|
| 54 |
+
- `generate_code_from_concept`: Generates interactive proof code from JSON spec → returns HTML/JS
|
| 55 |
+
3. **Two-Step Invocation:**
|
| 56 |
+
- **Step 1:** Agent calls `analyze_concept_from_text/url/image` with input
|
| 57 |
+
- **Step 2:** Agent calls `generate_code_from_concept` with JSON from Step 1
|
| 58 |
+
4. **Output Handling:** Agent receives HTML/JS code for rendering or further processing
|
| 59 |
+
|
| 60 |
+

|
| 61 |
+
|
| 62 |
+
**MCP Architecture Diagram:**
|
| 63 |
+
|
| 64 |
+

|
| 65 |
+
|
| 66 |
+
### **3.3 Feedback & Iteration Flow**
|
| 67 |
+
|
| 68 |
+
The user can view the generated proof and provide text feedback (e.g., "Make the triangle red" or "Add a step for area calculation").
|
| 69 |
+
1. **User Feedback:** User enters text in the "Refinement" panel (Gradio Textbox)
|
| 70 |
+
2. **Intent Analysis:** The system determines if the request requires a structural change (new steps) or just a visual update
|
| 71 |
+
3. **Regeneration:**
|
| 72 |
+
- **Stage 1 (Refine Spec):** Gemini 2.5 Flash updates the JSON spec based on feedback
|
| 73 |
+
- **Stage 2 (Refine Code):** Gemini 3.0 Pro rewrites the application code using the new spec and specific user instructions
|
| 74 |
+
4. **Update:** The Gradio HTML component refreshes with the modified application
|
| 75 |
+
|
| 76 |
+
## **4. Functional Requirements**
|
| 77 |
+
|
| 78 |
+
### **4.1 Multi-Modal Input Handling**
|
| 79 |
+
|
| 80 |
+
The app must accept three distinct types of input:
|
| 81 |
+
1. **Natural Language Text:** e.g., "Prove the Pythagorean Theorem."
|
| 82 |
+
2. **Image/Screenshot:** A photo of a textbook problem.
|
| 83 |
+
3. **URL:** A link to a math concept video or page.
|
| 84 |
+
* **Validation:** The system must strictly validate inputs (e.g., check for empty text, valid URL format, or missing image files) before communicating with the AI to prevent "hallucinated" default responses.
|
| 85 |
+
|
| 86 |
+
### **4.2 The "Thinking" Engine (Gemini Integration)**
|
| 87 |
+
|
| 88 |
+
**Two-Stage Pipeline Architecture (MCP-Optimized)**
|
| 89 |
+
|
| 90 |
+
The application uses a **two-stage AI pipeline** specifically designed to comply with MCP protocol timeout constraints (typically <30 seconds per tool call):
|
| 91 |
+
|
| 92 |
+
* **Stage 1: Concept Decomposition (The Teacher)**
|
| 93 |
+
* **Model:** `gemini-2.5-flash`
|
| 94 |
+
* **Role:** Identifies the mathematical concept and breaks it down into a logical teaching sequence
|
| 95 |
+
* **Execution Time:** ~10-15 seconds
|
| 96 |
+
* **Output:** JSON Spec containing a list of **Steps**. Each step defines what the user should do and what they should see
|
| 97 |
+
* **MCP Exposure:** Three separate tools based on input type:
|
| 98 |
+
- `analyze_concept_from_text(text_input, api_key)`
|
| 99 |
+
- `analyze_concept_from_url(url_input, api_key)`
|
| 100 |
+
- `analyze_concept_from_image(image_input, api_key)`
|
| 101 |
+
* **Return Value:** JSON string containing the `MathSpec`
|
| 102 |
+
|
| 103 |
+
* **Stage 2: Implementation (The Engineer)**
|
| 104 |
+
* **Model:** `gemini-3-pro-preview`
|
| 105 |
+
* **Config:** `thinkingConfig: { thinkingBudget: 4096 }`
|
| 106 |
+
* **Role:** Writes the HTML5/Canvas code based on the JSON specification
|
| 107 |
+
* **Execution Time:** ~20-30 seconds
|
| 108 |
+
* **Requirement:** The generated app must include a **Step Navigation System** (Next/Previous buttons, Progress bar) and distinct visual states for each step
|
| 109 |
+
* **MCP Exposure:** Single tool for code generation:
|
| 110 |
+
- `generate_code_from_concept(concept_json, api_key)`
|
| 111 |
+
* **Return Value:** HTML string containing complete interactive application
|
| 112 |
+
|
| 113 |
+
**Why Two Stages for MCP?**
|
| 114 |
+
|
| 115 |
+
MCP protocol has strict timeout limitations. The original single-step `generate_proof` operation took 30-60 seconds, exceeding MCP timeout windows. By splitting into two independent operations:
|
| 116 |
+
- Each operation completes within timeout constraints
|
| 117 |
+
- AI agents can cache the JSON spec and regenerate code multiple times without re-analyzing
|
| 118 |
+
- More granular control over the generation process
|
| 119 |
+
- Better error recovery (if Stage 2 fails, Stage 1 results are preserved)
|
| 120 |
+
|
| 121 |
+
### **4.3 Output & Interaction**
|
| 122 |
+
|
| 123 |
+
* **Guided Experience:** The app starts at "Step 1". The user reads an instruction, interacts with the visual, and clicks "Next" to proceed
|
| 124 |
+
* **Interactive Canvas:** Graphics update based on the current step. For example, construction lines might appear only in Step 2
|
| 125 |
+
* **Live Feedback:** Equations and values update in real-time as user drags elements
|
| 126 |
+
* **Gradio Components:**
|
| 127 |
+
- **HTML Component:** Displays the generated interactive proof application
|
| 128 |
+
- **JSON Component:** Shows the MathSpec structure for debugging/inspection
|
| 129 |
+
- **Textbox Components:** Display process logs and thinking streams
|
| 130 |
+
- **Accordion/Tab Layouts:** Organize different views (Proof, Spec, Code, Logs)
|
| 131 |
+
|
| 132 |
+
### **4.4 Feedback Loop**
|
| 133 |
+
|
| 134 |
+
* **Refinement Interface:** A text input field below the simulation area allows users to request changes.
|
| 135 |
+
* **Context Awareness:** The AI must receive the *previous* JSON specification and the *new* user feedback to generate a delta or a completely new version.
|
| 136 |
+
* **Logic:**
|
| 137 |
+
* If the feedback changes the math concept (e.g., "Switch to Isosceles"), Stage 1 must regenerate the steps.
|
| 138 |
+
* If the feedback is cosmetic (e.g., "Dark mode"), Stage 2 must implement it while preserving the logic.
|
| 139 |
+
|
| 140 |
+
### **4.5 Export & Sharing** [Coming Soon]
|
| 141 |
+
|
| 142 |
+
* **Export Button:** A Gradio Button to download the current session
|
| 143 |
+
* **Format:** A JSON file containing:
|
| 144 |
+
* **Input Data:** The original problem text, URL, or image data
|
| 145 |
+
* **Math Concept:** The JSON Specification (Steps, Explanation)
|
| 146 |
+
* **Source Code:** The Generated HTML/JS
|
| 147 |
+
* **Metadata:** Timestamp, input mode
|
| 148 |
+
* **Exclusion:** Process Logs are **not** included in the export file to keep it clean
|
| 149 |
+
* **Import Capability:** A Gradio File Upload component to restore a previously exported JSON file. This restores the input fields, the concept specification, and the interactive proof code
|
| 150 |
+
* **Implementation:** Uses Gradio's `gr.DownloadButton` and `gr.File` components
|
| 151 |
+
|
| 152 |
+
### **4.6 Persistence (Local Storage via ProofLibrary)** [Coming Soon]
|
| 153 |
+
|
| 154 |
+
* **Save Capability:** Users can save the currently generated proof (Math Spec + Code) using a Gradio Button
|
| 155 |
+
* **Backend Storage:** Python-based `ProofLibrary` class manages proof persistence:
|
| 156 |
+
- Saves to `saved_proofs/` directory as JSON files
|
| 157 |
+
- Each proof file contains: title, timestamp, input data, concept spec, generated code
|
| 158 |
+
- Filename format: `proof_YYYYMMDD_HHMMSS.json`
|
| 159 |
+
* **Library View:** A Gradio component (Dropdown or Gallery) lists previously saved items with timestamps and concept titles
|
| 160 |
+
* **Load Capability:** Users can instantly restore a previously generated proof from the library without re-querying the AI
|
| 161 |
+
* **File System:** Unlike browser Local Storage, Gradio implementation uses server-side file system storage for better reliability and Docker compatibility
|
| 162 |
+
|
| 163 |
+
### **4.7 Configuration & API Key Management**
|
| 164 |
+
|
| 165 |
+
* **Configuration Interface:** Gradio Accordion component in settings panel
|
| 166 |
+
* **API Key Management:**
|
| 167 |
+
* The app defaults to using the `GEMINI_API_KEY` from environment variables (`os.getenv("GEMINI_API_KEY")`)
|
| 168 |
+
* Users can optionally provide their own API Key via a Gradio Textbox (type="password")
|
| 169 |
+
* **Logic:** If a custom key is provided to MCP tools or web UI, it takes precedence over the environment variable
|
| 170 |
+
* **MCP Tools:** API key is an optional parameter in all MCP-exposed functions:
|
| 171 |
+
- `analyze_concept_from_text(text_input, api_key: Optional[str] = None)`
|
| 172 |
+
- `generate_code_from_concept(concept_json, api_key: Optional[str] = None)`
|
| 173 |
+
* **Security:** Custom API keys are passed per-request and not persisted server-side
|
| 174 |
+
* **Environment Variable Setup:**
|
| 175 |
+
```bash
|
| 176 |
+
# Linux/Mac
|
| 177 |
+
export GEMINI_API_KEY="your-api-key-here"
|
| 178 |
+
|
| 179 |
+
# Windows PowerShell
|
| 180 |
+
$env:GEMINI_API_KEY="your-api-key-here"
|
| 181 |
+
|
| 182 |
+
# Docker
|
| 183 |
+
docker run -e GEMINI_API_KEY="your-key" -p 7860:7860 hf-stepwise-math
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
### **4.8 Thinking Process Streaming (Enhanced UI)** [Coming Soon]
|
| 187 |
+
|
| 188 |
+
* **Streaming Thoughts:** The application utilizes Gemini's `thinkingConfig` with `includeThoughts: true` to capture the model's internal reasoning process
|
| 189 |
+
* **Gradio Implementation:**
|
| 190 |
+
* **Textbox Component:** Displays thinking stream with `max_lines=20` for scrollable content
|
| 191 |
+
* **Real-time Updates:** Uses Gradio's streaming capabilities to update UI as thoughts arrive
|
| 192 |
+
* **Markdown Rendering:** Gradio automatically renders Markdown in textboxes when configured
|
| 193 |
+
* **Display Features:**
|
| 194 |
+
* **Timer:** Progress message showing elapsed time (e.g., "Running for 12s")
|
| 195 |
+
* **Structured Layout:** Separate sections for "Analysis Phase" and "Code Generation Phase"
|
| 196 |
+
* **Collapsible Accordions:** Users can expand/collapse thought details to focus on results
|
| 197 |
+
* **Process Logs:**
|
| 198 |
+
* Separate from thinking stream
|
| 199 |
+
* Shows high-level pipeline progress: "Starting Stage 1...", "Concept analyzed", "Generating code..."
|
| 200 |
+
* Stored in `GeminiPipeline.process_logs` list for debugging
|
| 201 |
+
|
| 202 |
+
### **4.9 Pre-loaded Examples Library**
|
| 203 |
+
|
| 204 |
+
* **Examples Section:** Gradio Dropdown component populated from `examples/` directory
|
| 205 |
+
* **File Format:** Each example is a JSON file containing:
|
| 206 |
+
```json
|
| 207 |
+
{
|
| 208 |
+
"title": "Visual Proof: Pythagorean Theorem",
|
| 209 |
+
"input_mode": "Text",
|
| 210 |
+
"input_data": "Prove the Pythagorean theorem...",
|
| 211 |
+
"concept": { /* MathSpec JSON */ },
|
| 212 |
+
"code": "<!DOCTYPE html>..."
|
| 213 |
+
}
|
| 214 |
+
```
|
| 215 |
+
* **Initial Load Behavior:** On application startup, automatically load the first example (or a designated default example) to:
|
| 216 |
+
- Provide immediate visual demonstration of app capabilities
|
| 217 |
+
- Avoid empty/blank initial state
|
| 218 |
+
- Give users instant understanding of the output format
|
| 219 |
+
- Enable immediate interaction without waiting for AI generation
|
| 220 |
+
* **One-Click Loading:** Selecting an example from dropdown triggers a Gradio event handler that:
|
| 221 |
+
- Populates input fields with example data
|
| 222 |
+
- Loads the pre-generated concept spec into JSON viewer
|
| 223 |
+
- Renders the code in the HTML iframe
|
| 224 |
+
- Bypasses AI generation for instant loading
|
| 225 |
+
* **Content:** The library covers diverse topics:
|
| 226 |
+
- Geometry: Pythagorean Theorem, Area of Quadrilaterals, Altitude-Hypotenuse Ratios
|
| 227 |
+
- Probability: Probability of Odd Sums
|
| 228 |
+
- Algebra: Diagonals in Rhombus
|
| 229 |
+
* **Example Files:** Located in `examples/` directory with naming convention `001-visual-proof-{topic}.json`
|
| 230 |
+
* **Default Example:** First example in alphabetical order (`001-visual-proof-probability-of-an-odd-sum.json`) loads automatically on app initialization
|
| 231 |
+
|
| 232 |
+
### **4.10 MCP Server Integration**
|
| 233 |
+
|
| 234 |
+
* **Gradio MCP Support:** Application launches with `mcp_server=True` flag to enable MCP protocol endpoints
|
| 235 |
+
* **Tool Exposure Mechanism:**
|
| 236 |
+
- Gradio only exposes methods that are connected to UI components as MCP tools
|
| 237 |
+
- Hidden UI components (created with `visible=False` in a `gr.Group`) are used to expose MCP-specific methods
|
| 238 |
+
- Event handlers connect methods to hidden buttons/textboxes for MCP discovery
|
| 239 |
+
* **Exposed MCP Tools (4 Total):**
|
| 240 |
+
|
| 241 |
+
1. **analyze_concept_from_text**
|
| 242 |
+
- **Parameters:** `text_input: str`, `api_key: Optional[str]`
|
| 243 |
+
- **Returns:** JSON string containing MathSpec
|
| 244 |
+
- **Purpose:** Fast concept analysis for text input
|
| 245 |
+
- **Timeout:** ~10-15 seconds
|
| 246 |
+
|
| 247 |
+
2. **analyze_concept_from_url**
|
| 248 |
+
- **Parameters:** `url_input: str`, `api_key: Optional[str]`
|
| 249 |
+
- **Returns:** JSON string containing MathSpec
|
| 250 |
+
- **Purpose:** Fast concept analysis from URL content
|
| 251 |
+
- **Timeout:** ~10-15 seconds
|
| 252 |
+
|
| 253 |
+
3. **analyze_concept_from_image**
|
| 254 |
+
- **Parameters:** `image_input: str` (base64 or file path), `api_key: Optional[str]`
|
| 255 |
+
- **Returns:** JSON string containing MathSpec
|
| 256 |
+
- **Purpose:** Fast concept analysis from image
|
| 257 |
+
- **Timeout:** ~10-15 seconds
|
| 258 |
+
|
| 259 |
+
4. **generate_code_from_concept**
|
| 260 |
+
- **Parameters:** `concept_json: str`, `api_key: Optional[str]`
|
| 261 |
+
- **Returns:** HTML string containing interactive proof application
|
| 262 |
+
- **Purpose:** Generate code from previously analyzed concept
|
| 263 |
+
- **Timeout:** ~20-30 seconds
|
| 264 |
+
|
| 265 |
+
* **MCP Usage Pattern:**
|
| 266 |
+
```python
|
| 267 |
+
# Step 1: Analyze concept
|
| 268 |
+
concept_json = mcp_client.call_tool(
|
| 269 |
+
"analyze_concept_from_text",
|
| 270 |
+
{"text_input": "Prove Pythagorean theorem", "api_key": "optional-key"}
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Step 2: Generate code
|
| 274 |
+
html_code = mcp_client.call_tool(
|
| 275 |
+
"generate_code_from_concept",
|
| 276 |
+
{"concept_json": concept_json, "api_key": "optional-key"}
|
| 277 |
+
)
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
* **Testing MCP Server:**
|
| 281 |
+
```bash
|
| 282 |
+
# Launch MCP Inspector
|
| 283 |
+
npx @modelcontextprotocol/inspector
|
| 284 |
+
|
| 285 |
+
# Connect to: http://localhost:7860/mcp
|
| 286 |
+
# Available tools will appear in inspector UI
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
* **Hidden UI Components (MCP Exposure):**
|
| 290 |
+
```python
|
| 291 |
+
with gr.Group(visible=False) as mcp_hidden_group:
|
| 292 |
+
# Analysis tools
|
| 293 |
+
mcp_analyze_text_input = gr.Textbox()
|
| 294 |
+
mcp_analyze_text_btn = gr.Button()
|
| 295 |
+
mcp_analyze_text_output = gr.Textbox()
|
| 296 |
+
|
| 297 |
+
# Code generation tool
|
| 298 |
+
mcp_generate_concept_input = gr.Textbox()
|
| 299 |
+
mcp_generate_concept_btn = gr.Button()
|
| 300 |
+
mcp_generate_concept_output = gr.Textbox()
|
| 301 |
+
|
| 302 |
+
# Event handlers connect methods for MCP
|
| 303 |
+
mcp_analyze_text_btn.click(
|
| 304 |
+
fn=analyze_concept_from_text,
|
| 305 |
+
inputs=[mcp_analyze_text_input],
|
| 306 |
+
outputs=[mcp_analyze_text_output]
|
| 307 |
+
)
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
## **5. Feature Specifications (Examples)**
|
| 311 |
+
|
| 312 |
+
### **Example 1: The Pythagorean Theorem**
|
| 313 |
+
* **Generated App Steps:**
|
| 314 |
+
* **Step 1: Setup:** Display a right triangle. User drags vertices to resize. "Observe the legs a, b and hypotenuse c."
|
| 315 |
+
* **Step 2: Geometric Construction:** Squares appear on each side. "We build a square on each side of the triangle."
|
| 316 |
+
* **Step 3: Area Calculation:** The app calculates the area of each square. "Note the values: A = a², B = b², C = c²."
|
| 317 |
+
* **Step 4: The Proof:** The app rearranges the areas or shows the equation `Area A + Area B = Area C`. User drags vertices to verify it holds true for *any* right triangle.
|
| 318 |
+
|
| 319 |
+
### **Example 2: Slope Intercept Form**
|
| 320 |
+
|
| 321 |
+
* **Input:** Text: "Explain y = mx + b"
|
| 322 |
+
* **Generated App Steps:**
|
| 323 |
+
* **Step 1: The Grid:** Shows a coordinate plane.
|
| 324 |
+
* **Step 2: The Y-Intercept:** User adjusts slider `b`. The line moves up/down. "b controls where the line crosses the Y-axis."
|
| 325 |
+
* **Step 3: The Slope:** User adjusts slider `m`. The line rotates. "m controls the steepness."
|
| 326 |
+
* **Step 4: Prediction:** User is asked to set sliders to match a target line.
|
| 327 |
+
|
| 328 |
+
## **9. Development & Testing**
|
| 329 |
+
|
| 330 |
+
### **9.1 Local Development**
|
| 331 |
+
```bash
|
| 332 |
+
# Clone repository
|
| 333 |
+
git clone <repo-url>
|
| 334 |
+
cd hf-StepWise-Math/gradio-app
|
| 335 |
+
|
| 336 |
+
# Install dependencies
|
| 337 |
+
pip install -r requirements.txt
|
| 338 |
+
|
| 339 |
+
# Set API key
|
| 340 |
+
export GEMINI_API_KEY="your-api-key" # Linux/Mac
|
| 341 |
+
$env:GEMINI_API_KEY="your-api-key" # Windows PowerShell
|
| 342 |
+
|
| 343 |
+
# Run application
|
| 344 |
+
python app.py
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
### **9.2 Testing MCP Integration**
|
| 348 |
+
```bash
|
| 349 |
+
# Terminal 1: Launch Gradio app with MCP server
|
| 350 |
+
cd gradio-app
|
| 351 |
+
python app.py
|
| 352 |
+
|
| 353 |
+
# Terminal 2: Launch MCP Inspector
|
| 354 |
+
npx @modelcontextprotocol/inspector
|
| 355 |
+
|
| 356 |
+
# In Inspector UI:
|
| 357 |
+
# 1. Connect to: http://localhost:7860/mcp
|
| 358 |
+
# 2. Verify 4 tools appear: analyze_concept_from_text/url/image, generate_code_from_concept
|
| 359 |
+
# 3. Test two-step workflow:
|
| 360 |
+
# - Call analyze_concept_from_text with test input
|
| 361 |
+
# - Copy JSON result
|
| 362 |
+
# - Call generate_code_from_concept with JSON
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
### **9.3 Docker Testing**
|
| 366 |
+
```bash
|
| 367 |
+
# Build image
|
| 368 |
+
docker build -t hf-stepwise-math .
|
| 369 |
+
|
| 370 |
+
# Run container
|
| 371 |
+
docker run --rm -it -e GEMINI_API_KEY="your-key" -p 7860:7860 hf-stepwise-math
|
| 372 |
+
|
| 373 |
+
# Access at: http://localhost:7860
|
| 374 |
+
# MCP endpoint: http://localhost:7860/mcp
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
### **9.4 Test Files**
|
| 378 |
+
* `test_app.py`: Unit tests for Gradio components
|
| 379 |
+
* `test_generate_proof.py`: Integration tests for AI pipeline
|
| 380 |
+
* `test_logging.py`: Logging system validation
|
| 381 |
+
* Example files in `examples/`: Pre-generated proofs for UI testing
|
| 382 |
+
|
| 383 |
+
### **9.5 Deployment Checklist**
|
| 384 |
+
- [ ] GEMINI_API_KEY environment variable configured
|
| 385 |
+
- [ ] Docker image builds successfully
|
| 386 |
+
- [ ] MCP server accessible at `/mcp` endpoint
|
| 387 |
+
- [ ] All 4 MCP tools discoverable
|
| 388 |
+
- [ ] Example library loads correctly
|
| 389 |
+
- [ ] **Default example auto-loads on app initialization**
|
| 390 |
+
- [ ] Save/Load functionality works with file system
|
| 391 |
+
- [ ] Export/Import produces valid JSON
|
| 392 |
+
- [ ] Two-stage pipeline completes within timeout constraints
|
| 393 |
+
|
| 394 |
+
**MathSpec (JSON)**
|
| 395 |
+
|
| 396 |
+
```json
|
| 397 |
+
{
|
| 398 |
+
"conceptTitle": "Pythagorean Theorem",
|
| 399 |
+
"educationalGoal": "Prove a^2 + b^2 = c^2",
|
| 400 |
+
"explanation": "In a right-angled triangle...",
|
| 401 |
+
"steps": [
|
| 402 |
+
{
|
| 403 |
+
"stepTitle": "The Triangle",
|
| 404 |
+
"instruction": "Drag the red dots to change the shape of the right triangle.",
|
| 405 |
+
"visualFocus": "Triangle ABC"
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"stepTitle": "Adding Squares",
|
| 409 |
+
"instruction": "Click Next to visualize squares attached to each side.",
|
| 410 |
+
"visualFocus": "Squares on sides a, b, c"
|
| 411 |
+
}
|
| 412 |
+
],
|
| 413 |
+
"visualSpec": {
|
| 414 |
+
"elements": ["Triangle", "Squares", "Grid"],
|
| 415 |
+
"interactions": ["Drag Vertex", "Hover info"],
|
| 416 |
+
"mathLogic": "Calculate distances..."
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
```
|
| 420 |
+
|
| 421 |
+
## **7. UI/UX Design (Gradio Components)**
|
| 422 |
+
|
| 423 |
+
* **Input Panel (Left Column):**
|
| 424 |
+
- **Radio Buttons:** Select input mode (Text/URL/Image)
|
| 425 |
+
- **Conditional Components:** Show relevant input field based on mode
|
| 426 |
+
- **Textbox:** For text input
|
| 427 |
+
- **Textbox:** For URL input
|
| 428 |
+
- **Image Upload:** For image input
|
| 429 |
+
- **Textbox (Password):** Optional API key override
|
| 430 |
+
- **Button:** "Generate Proof"
|
| 431 |
+
- **Dropdown:** Example selection
|
| 432 |
+
- **Accordion:** Settings panel
|
| 433 |
+
|
| 434 |
+
* **Output Panel (Right Column):**
|
| 435 |
+
- **Tabs Component:**
|
| 436 |
+
1. **Interactive Proof:** `gr.HTML()` component displaying generated application
|
| 437 |
+
2. **Concept Spec:** `gr.JSON()` component showing MathSpec structure
|
| 438 |
+
3. **Source Code:** `gr.Code(language="html")` for viewing/editing generated code
|
| 439 |
+
4. **Process Logs:** `gr.Textbox()` with process execution details
|
| 440 |
+
5. **Thinking Stream:** `gr.Textbox()` with AI reasoning (collapsible accordion)
|
| 441 |
+
|
| 442 |
+
* **Action Buttons:**
|
| 443 |
+
- **Save to Library:** Stores proof to `saved_proofs/` directory
|
| 444 |
+
- **Export/Download:** `gr.DownloadButton()` for JSON export
|
| 445 |
+
- **Import/Upload:** `gr.File()` for JSON import
|
| 446 |
+
- **Load from Library:** `gr.Dropdown()` with saved proofs
|
| 447 |
+
|
| 448 |
+
* **Refinement Panel (Below Output):**
|
| 449 |
+
- **Textbox:** Multi-line input for feedback
|
| 450 |
+
- **Button:** "Refine Proof" to trigger regeneration
|
| 451 |
+
|
| 452 |
+
## **8. Technical Constraints & Requirements**
|
| 453 |
+
|
| 454 |
+
### **8.1 MCP Protocol Constraints**
|
| 455 |
+
* **Timeout Limitation:** Each MCP tool call must complete within ~30 seconds
|
| 456 |
+
* **Solution:** Two-stage pipeline splits 30-60s operation into 10-15s + 20-30s stages
|
| 457 |
+
* **Tool Discovery:** Tools must be connected to Gradio UI components (even if hidden) to be exposed via MCP
|
| 458 |
+
* **Parameter Handling:** All MCP tool parameters must be either required or properly typed with `Optional[]`
|
| 459 |
+
* **No Conditional Parameters:** Cannot require different parameters based on conditions (e.g., url_input required when mode="URL")
|
| 460 |
+
|
| 461 |
+
### **8.2 Gemini API Configuration**
|
| 462 |
+
* **Thinking Budget:** Uses high reasoning budget (4096) for Stage 2 (code generation) to ensure robust logic
|
| 463 |
+
* **Model Selection:**
|
| 464 |
+
- Stage 1: `gemini-2.5-flash` for fast analysis
|
| 465 |
+
- Stage 2: `gemini-3-pro-preview` for extended thinking during code generation
|
| 466 |
+
* **API Key Management:** Supports environment variable fallback with optional per-request override
|
| 467 |
+
|
| 468 |
+
### **8.3 Gradio Framework**
|
| 469 |
+
* **Version:** Gradio 6.0+ (supports `mcp_server=True` flag)
|
| 470 |
+
* **Launch Command:** `demo.launch(mcp_server=True, share=False, server_port=7860)`
|
| 471 |
+
* **Hidden Components:** Required for MCP tool exposure without cluttering UI
|
| 472 |
+
* **Event Handlers:** Connect Python methods to Gradio components for both UI and MCP access
|
| 473 |
+
|
| 474 |
+
### **8.4 Visual Clarity (Generated Code)**
|
| 475 |
+
* **Layout Requirements:** Generated code **MUST** prioritize visual clarity
|
| 476 |
+
* **Separation:** Overlapping elements are strictly prohibited
|
| 477 |
+
* **CSS/Layout:** Must use Flexbox/Grid to strictly separate graphics area (Canvas/SVG) from controls and textual instructions
|
| 478 |
+
* **Step Navigation:** Generated apps must include prominent navigation UI (buttons, progress indicators)
|
| 479 |
+
|
| 480 |
+
### **8.5 Docker & Deployment**
|
| 481 |
+
* **Containerization:** Application is fully Docker-ready with `Dockerfile` and `requirements.txt`
|
| 482 |
+
* **Port Mapping:** Exposes port 7860 for web UI and MCP server
|
| 483 |
+
* **Environment Variables:** API key passed via `-e GEMINI_API_KEY="..."`
|
| 484 |
+
* **Hugging Face Spaces:** Compatible with HF Spaces deployment (uses `gradio` template)
|
| 485 |
+
* **Build Command:** `docker build -t hf-stepwise-math .`
|
| 486 |
+
* **Run Command:** `docker run -e GEMINI_API_KEY="key" -p 7860:7860 hf-stepwise-math`
|
| 487 |
+
|
| 488 |
+
### **8.6 Security & Safety**
|
| 489 |
+
* **Content Filtering:** Image inputs should be validated for appropriate educational content
|
| 490 |
+
* **API Key Security:** Custom keys are per-request only, not persisted server-side
|
| 491 |
+
* **CORS:** Gradio handles CORS automatically for web UI and MCP endpoints
|
| 492 |
+
* **Rate Limiting:** Consider implementing rate limits for MCP tool calls in production
|
| 493 |
+
|
| 494 |
+
## **10. Data Models (Python Implementation)**
|
| 495 |
+
|
| 496 |
+
**MathSpec (Python Class)**
|
| 497 |
+
|
| 498 |
+
```python
|
| 499 |
+
class MathSpec:
|
| 500 |
+
"""Structured mathematical concept specification"""
|
| 501 |
+
def __init__(self, data: dict):
|
| 502 |
+
self.concept_title = data.get("conceptTitle", "")
|
| 503 |
+
self.educational_goal = data.get("educationalGoal", "")
|
| 504 |
+
self.explanation = data.get("explanation", "")
|
| 505 |
+
self.steps = data.get("steps", [])
|
| 506 |
+
self.visual_spec = data.get("visualSpec", {})
|
| 507 |
+
|
| 508 |
+
def to_dict(self):
|
| 509 |
+
return {
|
| 510 |
+
"conceptTitle": self.concept_title,
|
| 511 |
+
"educationalGoal": self.educational_goal,
|
| 512 |
+
"explanation": self.explanation,
|
| 513 |
+
"steps": self.steps,
|
| 514 |
+
"visualSpec": self.visual_spec
|
| 515 |
+
}
|
| 516 |
+
```
|
| 517 |
+
|
| 518 |
+
**MathSpec JSON Format**
|
| 519 |
+
|
| 520 |
+
```json
|
| 521 |
+
{
|
| 522 |
+
"conceptTitle": "Pythagorean Theorem",
|
| 523 |
+
"educationalGoal": "Prove a^2 + b^2 = c^2",
|
| 524 |
+
"explanation": "In a right-angled triangle...",
|
| 525 |
+
"steps": [
|
| 526 |
+
{
|
| 527 |
+
"stepTitle": "The Triangle",
|
| 528 |
+
"instruction": "Drag the red dots to change the shape of the right triangle.",
|
| 529 |
+
"visualFocus": "Triangle ABC"
|
| 530 |
+
},
|
| 531 |
+
{
|
| 532 |
+
"stepTitle": "Adding Squares",
|
| 533 |
+
"instruction": "Click Next to visualize squares attached to each side.",
|
| 534 |
+
"visualFocus": "Squares on sides a, b, c"
|
| 535 |
+
}
|
| 536 |
+
],
|
| 537 |
+
"visualSpec": {
|
| 538 |
+
"elements": ["Triangle", "Squares", "Grid"],
|
| 539 |
+
"interactions": ["Drag Vertex", "Hover info"],
|
| 540 |
+
"mathLogic": "Calculate distances..."
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
```
|
| 544 |
+
|
| 545 |
+
**Export/Save Format**
|
| 546 |
+
|
| 547 |
+
```json
|
| 548 |
+
{
|
| 549 |
+
"title": "Visual Proof: Pythagorean Theorem",
|
| 550 |
+
"timestamp": "2025-11-25T10:30:00",
|
| 551 |
+
"input_mode": "Text",
|
| 552 |
+
"input_data": "Prove the Pythagorean theorem using visual methods",
|
| 553 |
+
"concept": {
|
| 554 |
+
"conceptTitle": "Pythagorean Theorem",
|
| 555 |
+
"educationalGoal": "...",
|
| 556 |
+
"steps": [...]
|
| 557 |
+
},
|
| 558 |
+
"code": "<!DOCTYPE html>...",
|
| 559 |
+
"metadata": {
|
| 560 |
+
"generated_by": "StepWise Math Gradio MCP",
|
| 561 |
+
"version": "2.0"
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
## **11. File Structure**
|
| 567 |
+
|
| 568 |
+
```
|
| 569 |
+
gradio-app/
|
| 570 |
+
├── app.py # Main Gradio application with MCP server
|
| 571 |
+
├── requirements.txt # Python dependencies
|
| 572 |
+
├── Dockerfile # Container configuration
|
| 573 |
+
├── setup.sh / setup.bat # Environment setup scripts
|
| 574 |
+
├── README.md # Documentation
|
| 575 |
+
├── saved_proofs/ # User-generated proofs (persistent storage)
|
| 576 |
+
│ ├── proof_20251125_103000.json
|
| 577 |
+
│ └── proof_20251125_154500.json
|
| 578 |
+
├── examples/ # Pre-loaded example proofs
|
| 579 |
+
│ ├── 001-visual-proof-pythagorean-theorem.json
|
| 580 |
+
│ ├── 002-visual-proof-probability-odd-sum.json
|
| 581 |
+
│ └── ...
|
| 582 |
+
├── test_app.py # Unit tests
|
| 583 |
+
├── test_generate_proof.py # Integration tests
|
| 584 |
+
└── docs/ # Additional documentation
|
| 585 |
+
├── DEPLOYMENT.md
|
| 586 |
+
├── TEST_GUIDE.md
|
| 587 |
+
└── MCP_SCHEMA_README.md
|
| 588 |
+
```
|
| 589 |
+
|
| 590 |
+
## **12. Appendix: MCP Tool Schema**
|
| 591 |
+
|
| 592 |
+
### **Tool 1: analyze_concept_from_text**
|
| 593 |
+
```json
|
| 594 |
+
{
|
| 595 |
+
"name": "analyze_concept_from_text",
|
| 596 |
+
"description": "Analyze a text-based mathematical concept and generate structured JSON specification",
|
| 597 |
+
"inputSchema": {
|
| 598 |
+
"type": "object",
|
| 599 |
+
"properties": {
|
| 600 |
+
"text_input": {
|
| 601 |
+
"type": "string",
|
| 602 |
+
"description": "Mathematical concept or problem description"
|
| 603 |
+
},
|
| 604 |
+
"api_key": {
|
| 605 |
+
"type": "string",
|
| 606 |
+
"description": "Optional Gemini API key (uses environment variable if not provided)"
|
| 607 |
+
}
|
| 608 |
+
},
|
| 609 |
+
"required": ["text_input"]
|
| 610 |
+
},
|
| 611 |
+
"outputSchema": {
|
| 612 |
+
"type": "string",
|
| 613 |
+
"description": "JSON string containing MathSpec structure"
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
```
|
| 617 |
+
|
| 618 |
+
### **Tool 2: analyze_concept_from_url**
|
| 619 |
+
```json
|
| 620 |
+
{
|
| 621 |
+
"name": "analyze_concept_from_url",
|
| 622 |
+
"description": "Analyze mathematical concept from URL and generate structured JSON specification",
|
| 623 |
+
"inputSchema": {
|
| 624 |
+
"type": "object",
|
| 625 |
+
"properties": {
|
| 626 |
+
"url_input": {
|
| 627 |
+
"type": "string",
|
| 628 |
+
"description": "URL to mathematical concept page or video"
|
| 629 |
+
},
|
| 630 |
+
"api_key": {
|
| 631 |
+
"type": "string",
|
| 632 |
+
"description": "Optional Gemini API key"
|
| 633 |
+
}
|
| 634 |
+
},
|
| 635 |
+
"required": ["url_input"]
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
```
|
| 639 |
+
|
| 640 |
+
### **Tool 3: analyze_concept_from_image**
|
| 641 |
+
```json
|
| 642 |
+
{
|
| 643 |
+
"name": "analyze_concept_from_image",
|
| 644 |
+
"description": "Analyze mathematical concept from image and generate structured JSON specification",
|
| 645 |
+
"inputSchema": {
|
| 646 |
+
"type": "object",
|
| 647 |
+
"properties": {
|
| 648 |
+
"image_input": {
|
| 649 |
+
"type": "string",
|
| 650 |
+
"description": "Base64-encoded image or file path"
|
| 651 |
+
},
|
| 652 |
+
"api_key": {
|
| 653 |
+
"type": "string",
|
| 654 |
+
"description": "Optional Gemini API key"
|
| 655 |
+
}
|
| 656 |
+
},
|
| 657 |
+
"required": ["image_input"]
|
| 658 |
+
}
|
| 659 |
+
}
|
| 660 |
+
```
|
| 661 |
+
|
| 662 |
+
### **Tool 4: generate_code_from_concept**
|
| 663 |
+
```json
|
| 664 |
+
{
|
| 665 |
+
"name": "generate_code_from_concept",
|
| 666 |
+
"description": "Generate interactive HTML/JS proof application from JSON concept specification",
|
| 667 |
+
"inputSchema": {
|
| 668 |
+
"type": "object",
|
| 669 |
+
"properties": {
|
| 670 |
+
"concept_json": {
|
| 671 |
+
"type": "string",
|
| 672 |
+
"description": "JSON string containing MathSpec (from analyze_concept_* tools)"
|
| 673 |
+
},
|
| 674 |
+
"api_key": {
|
| 675 |
+
"type": "string",
|
| 676 |
+
"description": "Optional Gemini API key"
|
| 677 |
+
}
|
| 678 |
+
},
|
| 679 |
+
"required": ["concept_json"]
|
| 680 |
+
},
|
| 681 |
+
"outputSchema": {
|
| 682 |
+
"type": "string",
|
| 683 |
+
"description": "HTML string containing complete interactive proof application"
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
```
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
## **13. Model Context Protocol (MCP) Integration**
|
| 690 |
+
|
| 691 |
+
StepWise Math functions as a complete **MCP Server**, exposing its capabilities to external AI agents and automation tools. This enables programmatic access to the visual proof generation pipeline.
|
| 692 |
+
|
| 693 |
+
### **13.1 MCP Tools**
|
| 694 |
+
|
| 695 |
+
The application exposes **4 primary MCP tools** organized in a two-step workflow:
|
| 696 |
+
|
| 697 |
+
#### **Step 1: Specification Creation Tools**
|
| 698 |
+
1. **`create_math_specification_from_text`**
|
| 699 |
+
- Creates a structured teaching specification from natural language descriptions
|
| 700 |
+
- Input: Text description of the math problem
|
| 701 |
+
- Output: JSON specification with teaching steps
|
| 702 |
+
- Processing time: ~10-15 seconds
|
| 703 |
+
|
| 704 |
+
2. **`create_math_specification_from_url`**
|
| 705 |
+
- Creates a specification from web resources (Wikipedia, Khan Academy, etc.)
|
| 706 |
+
- Input: URL pointing to math content
|
| 707 |
+
- Output: JSON specification with teaching steps
|
| 708 |
+
- Processing time: ~10-15 seconds
|
| 709 |
+
|
| 710 |
+
3. **`create_math_specification_from_image`**
|
| 711 |
+
- Creates a specification from uploaded images (textbook problems, screenshots, handwritten notes)
|
| 712 |
+
- Input: PIL Image object
|
| 713 |
+
- Output: JSON specification with teaching steps
|
| 714 |
+
- Processing time: ~10-15 seconds
|
| 715 |
+
|
| 716 |
+
#### **Step 2: Application Building Tool**
|
| 717 |
+
4. **`build_interactive_proof_from_specification`**
|
| 718 |
+
- Builds a complete HTML/JavaScript application from a specification
|
| 719 |
+
- Input: JSON specification from any Step 1 tool
|
| 720 |
+
- Output: Self-contained HTML document
|
| 721 |
+
- Processing time: ~20-30 seconds
|
| 722 |
+
|
| 723 |
+
### **13.2 MCP Prompts**
|
| 724 |
+
|
| 725 |
+
Pre-defined prompts guide agents on effective tool usage:
|
| 726 |
+
|
| 727 |
+
1. **`create_visual_math_proof`** - Complete workflow for creating visual proofs
|
| 728 |
+
2. **`create_math_specification`** - Focus on pedagogical design
|
| 729 |
+
3. **`build_from_specification`** - Focus on implementation with customization
|
| 730 |
+
|
| 731 |
+
### **13.3 MCP Resources**
|
| 732 |
+
|
| 733 |
+
The server provides helpful templates and examples:
|
| 734 |
+
|
| 735 |
+
| Resource URI | Description | Type |
|
| 736 |
+
| ----------------------------------- | ------------------------------------- | -------- |
|
| 737 |
+
| `stepwise://specification-template` | JSON template for math specifications | JSON |
|
| 738 |
+
| `stepwise://example-pythagorean` | Complete Pythagorean theorem example | JSON |
|
| 739 |
+
| `stepwise://example-probability` | Probability visualization example | JSON |
|
| 740 |
+
| `stepwise://workflow-guide` | Two-step workflow documentation | Markdown |
|
| 741 |
+
|
| 742 |
+
### **13.4 MCP Use Cases**
|
| 743 |
+
|
| 744 |
+
**For AI Agents:**
|
| 745 |
+
- Automatically generate visual proofs from student questions
|
| 746 |
+
- Create custom teaching materials on-demand
|
| 747 |
+
- Build interactive homework help applications
|
| 748 |
+
|
| 749 |
+
**For Automation:**
|
| 750 |
+
- Batch-process textbook problems into interactive visualizations
|
| 751 |
+
- Convert curriculum PDFs into step-by-step interactive lessons
|
| 752 |
+
- Generate proof variations for different learning styles
|
| 753 |
+
|
| 754 |
+
**For Integration:**
|
| 755 |
+
- Embed in learning management systems (LMS)
|
| 756 |
+
- Connect to homework platforms
|
| 757 |
+
- Integrate with educational chatbots
|
| 758 |
+
|
| 759 |
+
### **13.5 MCP Server Configuration**
|
| 760 |
+
|
| 761 |
+
The application launches with MCP server enabled:
|
| 762 |
+
|
| 763 |
+
```python
|
| 764 |
+
demo.launch(
|
| 765 |
+
server_name="0.0.0.0",
|
| 766 |
+
server_port=7860,
|
| 767 |
+
mcp_server=True, # Enable MCP protocol
|
| 768 |
+
theme=theme,
|
| 769 |
+
debug=True
|
| 770 |
+
)
|
| 771 |
+
```
|
| 772 |
+
|
| 773 |
+
**Access Points:**
|
| 774 |
+
- **Web UI:** `http://localhost:7860`
|
| 775 |
+
- **MCP Inspector:** Compatible with `@modelcontextprotocol/inspector`
|
| 776 |
+
- **API Endpoints:** Auto-generated for all 4 tools + resource endpoints
|
README.md
CHANGED
|
@@ -1,12 +1,244 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: StepWise Math AI
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: StepWise Math AI
|
| 3 |
+
emoji: 🎓
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
tags:
|
| 11 |
+
- building-mcp-track-consumer
|
| 12 |
+
- building-mcp-track-creative
|
| 13 |
+
- mcp
|
| 14 |
+
- gradio
|
| 15 |
+
- gemini
|
| 16 |
+
- education
|
| 17 |
+
- mathematics
|
| 18 |
+
- ai
|
| 19 |
+
- visualization
|
| 20 |
+
- interactive-learning
|
| 21 |
+
---
|
| 22 |
+
# StepWise Math
|
| 23 |
+
|
| 24 |
+
**Transform Static Math Problems into Living, Interactive Step-by-Step Visual Proofs**
|
| 25 |
+
|
| 26 |
+

|
| 27 |
+
|
| 28 |
+
This is a **Gradio MCP Framework** implementation of the StepWise Math React app, providing the same powerful features in a Python-based web interface.
|
| 29 |
+
|
| 30 |
+
[](https://github.com/modelcontextprotocol)
|
| 31 |
+
[-blue)](https://huggingface.co/MCP-1st-Birthday)
|
| 32 |
+
[](https://ai.google.dev/)
|
| 33 |
+
[](https://ai.google.dev/)
|
| 34 |
+
|
| 35 |
+
## Overview
|
| 36 |
+
|
| 37 |
+
### What This Project Does
|
| 38 |
+
|
| 39 |
+
StepWise Math is an **MCP-capable service** that transforms static math problems into interactive, step-by-step visual proofs. Built as a Gradio-based MCP server, it provides both a user-friendly web interface and programmatic MCP endpoints for AI agents and developer tools.
|
| 40 |
+
|
| 41 |
+
The system operates through a **two-stage AI pipeline**:
|
| 42 |
+
|
| 43 |
+
- **Stage 1 — Concept Analysis** (Gemini 2.5 Flash): Generates a pedagogical JSON specification from text, URL, or image input
|
| 44 |
+
- **Stage 2 — Code Generation** (Gemini 3.0 Pro): Synthesizes a self-contained HTML/JS interactive proof application
|
| 45 |
+
|
| 46 |
+
### Why This Project Matters
|
| 47 |
+
|
| 48 |
+
- **Educational Impact**: Empowers teachers and students to visualize mathematical reasoning step-by-step, transforming abstract concepts into concrete, interactive experiences
|
| 49 |
+
- **MCP Showcase**: Demonstrates best practices for building MCP servers that integrate multi-step LLM workflows, streaming thoughts, and developer-facing prompts/resources
|
| 50 |
+
- **Reference Implementation**: Provides a complete example of combining AI-powered analysis with code generation in an MCP-compliant architecture
|
| 51 |
+
|
| 52 |
+
## Documentation
|
| 53 |
+
|
| 54 |
+
For complete product specifications, feature requirements, and technical implementation details, see the **[Product Requirements Document (PRD.md)](./PRD.md)**.
|
| 55 |
+
|
| 56 |
+
The PRD covers:
|
| 57 |
+
- Target audience and user personas (Grades 6-10 students, teachers, tutors)
|
| 58 |
+
- Detailed functional requirements and data models
|
| 59 |
+
- UI/UX design specifications
|
| 60 |
+
- Example use cases (Pythagorean Theorem, Slope-Intercept Form)
|
| 61 |
+
- System constraints and technical architecture
|
| 62 |
+
|
| 63 |
+
## Quick Start
|
| 64 |
+
|
| 65 |
+
### Using Gradio UI
|
| 66 |
+
1. Enter your **Gemini API Key** in the Configuration section (get one free at [ai.google.dev](https://ai.google.dev/)). This is needed only when the embedded API key is out of credits.
|
| 67 |
+
2. Choose your input method (Text, Image, or URL)
|
| 68 |
+
3. Describe a math problem or concept
|
| 69 |
+
4. Click **Generate Guided Proof**
|
| 70 |
+
5. Explore the interactive visualization!
|
| 71 |
+
|
| 72 |
+
### Using MCP Clients
|
| 73 |
+
|
| 74 |
+
1. Point your MCP client (e.g., Claude Desktop, VSCode) to the deployed MCP server URL: https://mcp-1st-birthday-stepwise-math-ai.hf.space/gradio_api/mcp/
|
| 75 |
+
2. Configure the MCP server settings in your client as follows:
|
| 76 |
+
<details>
|
| 77 |
+
<summary><strong>Claude Desktop</strong> (<code>claude_desktop_config.json</code>)</summary>
|
| 78 |
+
|
| 79 |
+
```json
|
| 80 |
+
{
|
| 81 |
+
"mcpServers": {
|
| 82 |
+
"tracemind": {
|
| 83 |
+
"url": "https://mcp-1st-birthday-stepwise-math-ai.hf.space/gradio_api/mcp/sse",
|
| 84 |
+
"transport": "sse"
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
After updating, restart Claude Desktop.
|
| 91 |
+
|
| 92 |
+
</details>
|
| 93 |
+
|
| 94 |
+
<details>
|
| 95 |
+
<summary><strong>VSCode</strong> (<code>settings.json</code>)</summary>
|
| 96 |
+
|
| 97 |
+
```json
|
| 98 |
+
{
|
| 99 |
+
"servers": {
|
| 100 |
+
"stepwise": {
|
| 101 |
+
"url": "https://mcp-1st-birthday-stepwise-math-ai.hf.space/gradio_api/mcp/",
|
| 102 |
+
"type": "http"
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
</details>
|
| 109 |
+
|
| 110 |
+
3. Open your MCP client and discover the available prompts and tools.
|
| 111 |
+
4. Invoke the `create_visual_math_proof` prompt or the underlying tools to generate interactive proofs programmatically.
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
## Features
|
| 115 |
+
|
| 116 |
+
### MCP Tools, Prompts & Resources
|
| 117 |
+
- **Prompts**: High-level conversational wrappers
|
| 118 |
+
- **Tools**: Programmatic calls
|
| 119 |
+
- **Resources**: JSON templates and examples
|
| 120 |
+
- **Discovery**: All prompts/tools are registered in the server schema for MCP client access
|
| 121 |
+
- **Authentication**: Use API keys (`GEMINI_API_KEY`)
|
| 122 |
+
|
| 123 |
+
### Multi-Modal Input
|
| 124 |
+
- **Text Input**: Describe any math problem in natural language
|
| 125 |
+
- **Image Upload**: Upload photos of textbook problems or handwritten equations
|
| 126 |
+
- **URL Import**: Reference YouTube videos, Khan Academy lessons, or web resources
|
| 127 |
+
|
| 128 |
+
### Dual-Stage AI Pipeline
|
| 129 |
+
- **Stage 1 - The Teacher** (Gemini 2.5 Flash): Analyzes concepts and designs pedagogical step sequences
|
| 130 |
+
- **Stage 2 - The Engineer** (Gemini 3.0 Pro + Extended Thinking): Generates production-ready interactive visualizations
|
| 131 |
+
|
| 132 |
+
### Interactive Step Navigation
|
| 133 |
+
- Progressive disclosure of mathematical concepts
|
| 134 |
+
- Back/Forward buttons to review at your own pace
|
| 135 |
+
- Visual state changes synchronized with each step
|
| 136 |
+
- Real-time equation updates as you interact
|
| 137 |
+
|
| 138 |
+
## System Architecture
|
| 139 |
+
|
| 140 |
+
The system utilizes a **Two-Stage AI Pipeline** orchestrated by a Python/Gradio core to transform abstract math concepts into interactive HTML5 applications.
|
| 141 |
+
|
| 142 |
+
1. **Ingestion (MCP & Web):** Users submit requests via MCP-enabled clients (Claude Desktop, VSCode) or the direct Web UI.
|
| 143 |
+
2. **Stage 1 - Analysis (Gemini 2.5 Flash):** The "Architect" model decomposes the mathematical concept into a structured `MathSpec` JSON, defining learning steps and visual logic.
|
| 144 |
+
3. **Stage 2 - Implementation (Gemini 3.0 Pro):** The "Builder" model consumes the spec to generate self-contained, interactive HTML5/Canvas code.
|
| 145 |
+
4. **Delivery:** The final executable app is rendered in the UI or returned to the MCP client for immediate use.
|
| 146 |
+
|
| 147 |
+

|
| 148 |
+
|
| 149 |
+
**What's Included:**
|
| 150 |
+
- Functioning **MCP server** exposing tools for creating math specifications and building interactive proofs
|
| 151 |
+
- **Gradio UI** (`app.py`) for submitting text, URL, or image inputs and viewing generated proofs
|
| 152 |
+
- MCP **prompts** and **resources** accessible to MCP clients (Claude Desktop, VSCode, etc.)
|
| 153 |
+
|
| 154 |
+
## Usage Guide
|
| 155 |
+
|
| 156 |
+
### Generating Your First Proof
|
| 157 |
+
|
| 158 |
+
1. **Select Input Method**: Choose Text, Image, or URL
|
| 159 |
+
2. **Provide Your Problem**:
|
| 160 |
+
- Text: "Prove that the sum of angles in a triangle is 180 degrees"
|
| 161 |
+
- Image: Upload a photo of a textbook problem or handwritten equation
|
| 162 |
+
- URL: Paste a link to a Math problem. Example - https://cemc.uwaterloo.ca/sites/default/files/documents/2025/POTWC-25-G-11-P.html
|
| 163 |
+
3. **Click Generate**: The AI will analyze and create your interactive proof
|
| 164 |
+
4. **Explore**: Navigate through the steps in the Guided Proof tab
|
| 165 |
+
|
| 166 |
+
### Refining Your Proof
|
| 167 |
+
|
| 168 |
+
1. **View the Generated Proof**: Check the interactive simulation
|
| 169 |
+
2. **Provide Feedback**: Type suggestions like "Make the triangle larger" or "Add labels to the vertices"
|
| 170 |
+
3. **Apply Refinement**: The AI will regenerate with your feedback
|
| 171 |
+
|
| 172 |
+
## Technical Details
|
| 173 |
+
|
| 174 |
+
### MCP Integration Architecture & Programmatic Flow
|
| 175 |
+
|
| 176 |
+
The diagram illustrates how the Gradio application exposes its functionality for programmatic access via the Model Context Protocol (MCP). The **Gradio App Server** acts as the central hub, exposing capabilities in two categories:
|
| 177 |
+
|
| 178 |
+
* **MCP Prompts:** High-level, conversational wrappers (e.g., `create_visual_math_proof`) for guiding multi-step workflows.
|
| 179 |
+
* **MCP Resources / Tools:** Direct, callable functions (e.g., `create_math_specification_from_text`, `build_from_specification`) for specific tasks.
|
| 180 |
+
|
| 181 |
+
MCP-aware clients or agents can interact with these components by first **discovering** the available tools through the server schema and then **authenticating** with an API key.
|
| 182 |
+
|
| 183 |
+
The bottom section details a typical **programmatic flow**:
|
| 184 |
+
1. **Discover** available prompts and resources.
|
| 185 |
+
2. **Call** a `create_math_specification_from_*` tool with the appropriate input (Text, URL, or Image) to generate a structured `MathSpec JSON`.
|
| 186 |
+
3. **Call** the `build_interactive_proof_from_specification` tool with the generated JSON to produce the final, self-contained `index.html` interactive proof.
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+

|
| 190 |
+
|
| 191 |
+
***Note**: All data is processed securely. Your API key is only used to communicate with Google's Gemini API.*
|
| 192 |
+
|
| 193 |
+
## Resources & Links
|
| 194 |
+
|
| 195 |
+
### Live Demo & Demo Video (Coming Soon)
|
| 196 |
+
|
| 197 |
+
- **Live Demo**: Coming Soon
|
| 198 |
+
- **Demo Video**: Coming Soon (TODO)
|
| 199 |
+
|
| 200 |
+
### Social Media (Coming Soon)
|
| 201 |
+
|
| 202 |
+
Read the announcement and join the discussion:
|
| 203 |
+
- **Blog Post**: Coming Soon (TODO)
|
| 204 |
+
- **Twitter/X**: Coming Soon (TODO)
|
| 205 |
+
- **LinkedIn**: Coming Soon (TODO)
|
| 206 |
+
- **Discord**: [HuggingFace Discord](https://discord.com/channels/879548962464493619/1439001549492719726/1442838638307180656) (TODO)
|
| 207 |
+
|
| 208 |
+
## Contributing & Support
|
| 209 |
+
|
| 210 |
+
**Contributions Welcome!** Areas of interest:
|
| 211 |
+
- Performance optimizations
|
| 212 |
+
- UI/UX improvements
|
| 213 |
+
- Additional mathematical domains
|
| 214 |
+
- Documentation and examples
|
| 215 |
+
|
| 216 |
+
**Support Channels:**
|
| 217 |
+
- **Discussions**: [HuggingFace Discussions](https://huggingface.co/spaces/MCP-1st-Birthday/StepWise-Math-AI/discussions)
|
| 218 |
+
- **Contact**: TODO - Project lead contact information
|
| 219 |
+
|
| 220 |
+
## Acknowledgments
|
| 221 |
+
|
| 222 |
+
- **Google AI** for the incredible Gemini models
|
| 223 |
+
- **Gradio** for the amazing Python web framework
|
| 224 |
+
- **Nano Banana** for image assets
|
| 225 |
+
- **GitHub Copilot** for Vibe Coding
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
## License
|
| 229 |
+
|
| 230 |
+
This project is licensed under the MIT License. Original work created for the MCP's 1st Birthday Hackathon (November 2025).
|
| 231 |
+
|
| 232 |
+
For attributions of third-party assets and libraries, see [ACKNOWLEDGMENTS](#-acknowledgments).
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
<div align="center">
|
| 237 |
+
|
| 238 |
+
**Built with ❤️ for visual learners everywhere**
|
| 239 |
+
|
| 240 |
+
*Making abstract math concrete, one proof at a time*
|
| 241 |
+
|
| 242 |
+
**Like this space if you found it helpful!**
|
| 243 |
+
|
| 244 |
+
</div>
|
app.py
ADDED
|
@@ -0,0 +1,1417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
StepWise Math - Gradio MCP Framework Version
|
| 3 |
+
Transform Static Math Problems into Living, Interactive Step-by-Step Visual Proofs
|
| 4 |
+
Powered by Google Gemini 2.5 Flash & Gemini 3.0 Pro with Extended Thinking
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import base64
|
| 12 |
+
import re
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 16 |
+
from io import BytesIO
|
| 17 |
+
from google import genai
|
| 18 |
+
from google.genai import types
|
| 19 |
+
import logging
|
| 20 |
+
|
| 21 |
+
# Configure basic logger
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
logging.basicConfig(level=logging.INFO)
|
| 24 |
+
|
| 25 |
+
# ==================== Configuration ====================
|
| 26 |
+
|
| 27 |
+
class Config:
|
| 28 |
+
"""Application configuration"""
|
| 29 |
+
DEFAULT_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
| 30 |
+
LIBRARY_PATH = Path("saved_proofs")
|
| 31 |
+
EXAMPLES_PATH = Path("examples")
|
| 32 |
+
|
| 33 |
+
# Create directories if they don't exist
|
| 34 |
+
LIBRARY_PATH.mkdir(exist_ok=True)
|
| 35 |
+
EXAMPLES_PATH.mkdir(exist_ok=True)
|
| 36 |
+
|
| 37 |
+
# ==================== Data Models ====================
|
| 38 |
+
|
| 39 |
+
class MathSpec:
|
| 40 |
+
"""Structured mathematical concept specification"""
|
| 41 |
+
def __init__(self, data: dict):
|
| 42 |
+
self.concept_title = data.get("conceptTitle", "")
|
| 43 |
+
self.educational_goal = data.get("educationalGoal", "")
|
| 44 |
+
self.explanation = data.get("explanation", "")
|
| 45 |
+
self.steps = data.get("steps", [])
|
| 46 |
+
self.visual_spec = data.get("visualSpec", {})
|
| 47 |
+
|
| 48 |
+
def to_dict(self):
|
| 49 |
+
return {
|
| 50 |
+
"conceptTitle": self.concept_title,
|
| 51 |
+
"educationalGoal": self.educational_goal,
|
| 52 |
+
"explanation": self.explanation,
|
| 53 |
+
"steps": self.steps,
|
| 54 |
+
"visualSpec": self.visual_spec
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# ==================== AI Pipeline ====================
|
| 58 |
+
|
| 59 |
+
class GeminiPipeline:
|
| 60 |
+
"""Two-stage AI pipeline for concept decomposition and code generation"""
|
| 61 |
+
|
| 62 |
+
def __init__(self, api_key: str):
|
| 63 |
+
self.api_key = api_key
|
| 64 |
+
self.client = genai.Client(api_key=api_key)
|
| 65 |
+
self.current_thought = ""
|
| 66 |
+
self.process_logs = []
|
| 67 |
+
|
| 68 |
+
def add_log(self, message: str, log_type: str = "info"):
|
| 69 |
+
"""Add a log entry with timestamp"""
|
| 70 |
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
| 71 |
+
self.process_logs.append({
|
| 72 |
+
"timestamp": timestamp,
|
| 73 |
+
"message": message,
|
| 74 |
+
"type": log_type
|
| 75 |
+
})
|
| 76 |
+
return f"[{timestamp}] {message}"
|
| 77 |
+
|
| 78 |
+
async def process_stream(self, stream):
|
| 79 |
+
"""Process streaming response and extract thoughts"""
|
| 80 |
+
full_text = ""
|
| 81 |
+
self.current_thought = ""
|
| 82 |
+
|
| 83 |
+
for chunk in stream:
|
| 84 |
+
if not chunk.candidates or not chunk.candidates[0].content:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
for part in chunk.candidates[0].content.parts:
|
| 88 |
+
# Handle thoughts
|
| 89 |
+
if hasattr(part, 'thought') and part.thought:
|
| 90 |
+
thought_text = getattr(part, 'text', '')
|
| 91 |
+
self.current_thought += thought_text
|
| 92 |
+
else:
|
| 93 |
+
# This is content
|
| 94 |
+
text = getattr(part, 'text', '')
|
| 95 |
+
full_text += text
|
| 96 |
+
|
| 97 |
+
return full_text
|
| 98 |
+
|
| 99 |
+
def clean_json_output(self, text: str) -> str:
|
| 100 |
+
"""Remove markdown code blocks from JSON output and fix common JSON issues"""
|
| 101 |
+
cleaned = text.replace('```json', '').replace('```', '')
|
| 102 |
+
|
| 103 |
+
# Find first '{' and last '}'
|
| 104 |
+
start = cleaned.find('{')
|
| 105 |
+
end = cleaned.rfind('}')
|
| 106 |
+
|
| 107 |
+
if start != -1 and end != -1 and end > start:
|
| 108 |
+
cleaned = cleaned[start:end + 1]
|
| 109 |
+
|
| 110 |
+
cleaned = cleaned.strip()
|
| 111 |
+
|
| 112 |
+
# Try to fix common JSON issues
|
| 113 |
+
try:
|
| 114 |
+
# Validate JSON first
|
| 115 |
+
json.loads(cleaned)
|
| 116 |
+
return cleaned
|
| 117 |
+
except json.JSONDecodeError as e:
|
| 118 |
+
logger.warning(f"Initial JSON parse failed: {e}. Attempting to fix...")
|
| 119 |
+
|
| 120 |
+
# Try to fix trailing commas before ] or }
|
| 121 |
+
import re
|
| 122 |
+
cleaned = re.sub(r',(\s*[}\]])', r'\1', cleaned)
|
| 123 |
+
|
| 124 |
+
# Try to fix missing commas between properties (common in AI output)
|
| 125 |
+
# This is a best-effort fix
|
| 126 |
+
try:
|
| 127 |
+
json.loads(cleaned)
|
| 128 |
+
logger.info("Fixed JSON with comma cleanup")
|
| 129 |
+
return cleaned
|
| 130 |
+
except json.JSONDecodeError:
|
| 131 |
+
logger.error(f"Could not auto-fix JSON. Returning original: {cleaned[:500]}...")
|
| 132 |
+
return cleaned
|
| 133 |
+
|
| 134 |
+
def stage1_analyze_concept(self, input_text: str = "", input_url: str = "",
|
| 135 |
+
input_image: Optional[Any] = None,
|
| 136 |
+
input_mode: str = "text") -> Tuple[MathSpec, List[str]]:
|
| 137 |
+
"""
|
| 138 |
+
Stage 1: Concept Decomposition (Gemini 2.5 Flash)
|
| 139 |
+
Analyzes the math problem and creates a teaching plan
|
| 140 |
+
"""
|
| 141 |
+
logger.info("="*60)
|
| 142 |
+
logger.info("STAGE 1: CONCEPT ANALYSIS - Starting Gemini 2.5 Flash call")
|
| 143 |
+
logger.info(f"Input Mode: {input_mode}")
|
| 144 |
+
if input_mode == "text":
|
| 145 |
+
logger.info(f"Text Input Length: {len(input_text)} characters")
|
| 146 |
+
elif input_mode == "url":
|
| 147 |
+
logger.info(f"URL Input: {input_url}")
|
| 148 |
+
elif input_mode == "image":
|
| 149 |
+
logger.info(f"Image Input: {type(input_image)}")
|
| 150 |
+
self.add_log("Stage 1: Analyzing concept with Gemini 2.5 Flash...", "thinking")
|
| 151 |
+
|
| 152 |
+
system_instruction = """You are a world-class mathematics educator and visual designer.
|
| 153 |
+
Your goal is to translate user inputs into a "Step-by-Step Interactive Visual Proof".
|
| 154 |
+
|
| 155 |
+
Do not just solve the problem. Design a web application that guides the student through the concept incrementally.
|
| 156 |
+
|
| 157 |
+
CRITICAL DESIGN CONSTRAINT: Ensure the visual specification prioritizes clarity. Avoid clutter. Request layouts where controls, text, and diagrams are separated to prevent overlapping.
|
| 158 |
+
|
| 159 |
+
Return a JSON object with:
|
| 160 |
+
- conceptTitle: Short name (e.g., "Pythagorean Theorem").
|
| 161 |
+
- educationalGoal: What the student learns.
|
| 162 |
+
- explanation: Friendly markdown explanation.
|
| 163 |
+
- steps: An array of 3-6 logical steps.
|
| 164 |
+
- stepTitle: Title of this phase.
|
| 165 |
+
- instruction: What the user should do or observe (e.g., "Drag vertex A", "Click Next to see the area").
|
| 166 |
+
- visualFocus: What part of the visual changes or is highlighted.
|
| 167 |
+
- visualSpec: Technical details for the engineer.
|
| 168 |
+
- elements: List of visual objects.
|
| 169 |
+
- interactions: User actions.
|
| 170 |
+
- mathLogic: Formulas needed.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
parts = []
|
| 174 |
+
config = {
|
| 175 |
+
"thinking_config": types.ThinkingConfig(
|
| 176 |
+
include_thoughts=True,
|
| 177 |
+
thinking_budget=2048 # Limited budget to prevent response truncation
|
| 178 |
+
)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Build request based on input mode
|
| 182 |
+
if input_mode == "url" and input_url:
|
| 183 |
+
self.add_log(f"Processing URL: {input_url}", "info")
|
| 184 |
+
prompt = f"""Analyze the math concept at this URL: {input_url}.
|
| 185 |
+
Design a step-by-step visual proof and return the specification in strict JSON format.
|
| 186 |
+
The JSON must match this structure exactly:
|
| 187 |
+
{{
|
| 188 |
+
"conceptTitle": "string",
|
| 189 |
+
"educationalGoal": "string",
|
| 190 |
+
"explanation": "string",
|
| 191 |
+
"steps": [ {{ "stepTitle": "string", "instruction": "string", "visualFocus": "string" }} ],
|
| 192 |
+
"visualSpec": {{ "elements": ["string"], "interactions": ["string"], "mathLogic": "string" }}
|
| 193 |
+
}}
|
| 194 |
+
IMPORTANT: Return ONLY the raw JSON string. Do not include markdown formatting, code blocks, or conversational text. Start the response with '{{'."""
|
| 195 |
+
parts.append({"text": prompt})
|
| 196 |
+
# Use both google_search and url_context for comprehensive URL processing
|
| 197 |
+
config["tools"] = [{"google_search": {}}, {"url_context": {}}]
|
| 198 |
+
# NOTE: Do NOT use response_mime_type or response_schema with URL grounding tools
|
| 199 |
+
# The model needs prompt-based guidance for JSON format when using these tools
|
| 200 |
+
|
| 201 |
+
elif input_mode == "image" and input_image is not None:
|
| 202 |
+
self.add_log("Processing uploaded image...", "info")
|
| 203 |
+
# Convert PIL Image to base64
|
| 204 |
+
buffered = BytesIO()
|
| 205 |
+
input_image.save(buffered, format="JPEG")
|
| 206 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
| 207 |
+
|
| 208 |
+
prompt = """Analyze the math problem in this image and design a step-by-step visual proof.
|
| 209 |
+
Return a complete, valid JSON object following the exact structure specified in the system instruction.
|
| 210 |
+
Ensure all JSON fields are properly closed and the response is a valid, parseable JSON."""
|
| 211 |
+
parts.append({"inline_data": {"mime_type": "image/jpeg", "data": img_base64}})
|
| 212 |
+
parts.append({"text": prompt})
|
| 213 |
+
config["response_mime_type"] = "application/json"
|
| 214 |
+
config["response_schema"] = self._get_math_spec_schema()
|
| 215 |
+
|
| 216 |
+
else: # text mode
|
| 217 |
+
self.add_log(f"Processing text input...", "info")
|
| 218 |
+
prompt = f"""Analyze this math problem/concept and design a step-by-step visual proof: {input_text}
|
| 219 |
+
Return a complete, valid JSON object following the exact structure specified in the system instruction.
|
| 220 |
+
Ensure all JSON fields are properly closed and the response is a valid, parseable JSON."""
|
| 221 |
+
parts.append({"text": prompt})
|
| 222 |
+
config["response_mime_type"] = "application/json"
|
| 223 |
+
config["response_schema"] = self._get_math_spec_schema()
|
| 224 |
+
|
| 225 |
+
# Generate response
|
| 226 |
+
logger.info("Sending API request to Gemini 2.5 Flash...")
|
| 227 |
+
logger.debug(f"Config: {config}")
|
| 228 |
+
try:
|
| 229 |
+
response = self.client.models.generate_content(
|
| 230 |
+
model="gemini-2.5-flash",
|
| 231 |
+
contents={"parts": parts},
|
| 232 |
+
config=types.GenerateContentConfig(
|
| 233 |
+
system_instruction=system_instruction,
|
| 234 |
+
**config
|
| 235 |
+
)
|
| 236 |
+
)
|
| 237 |
+
logger.info("✓ API response received successfully")
|
| 238 |
+
logger.debug(f"Response length: {len(response.text)} characters")
|
| 239 |
+
except Exception as api_error:
|
| 240 |
+
logger.error(f"API call failed: {str(api_error)}", exc_info=True)
|
| 241 |
+
raise
|
| 242 |
+
|
| 243 |
+
# Parse response
|
| 244 |
+
logger.info("Parsing API response...")
|
| 245 |
+
spec_text = response.text
|
| 246 |
+
logger.debug(f"Raw API response (first 500 chars): {spec_text[:500]}")
|
| 247 |
+
|
| 248 |
+
spec_text = self.clean_json_output(spec_text)
|
| 249 |
+
logger.debug(f"Cleaned JSON (first 500 chars): {spec_text[:500]}")
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
logger.info("Parsing JSON specification...")
|
| 253 |
+
spec_data = json.loads(spec_text)
|
| 254 |
+
spec = MathSpec(spec_data)
|
| 255 |
+
logger.info(f"✓ Concept Title: {spec.concept_title}")
|
| 256 |
+
logger.info(f"✓ Educational Goal: {spec.educational_goal}")
|
| 257 |
+
logger.info(f"✓ Number of Steps: {len(spec.steps)}")
|
| 258 |
+
logger.info(f"Visual Elements: {spec.visual_spec.get('elements', [])}")
|
| 259 |
+
logger.info("STAGE 1: COMPLETE")
|
| 260 |
+
logger.info("="*60)
|
| 261 |
+
self.add_log(f"✓ Concept Identified: {spec.concept_title}", "success")
|
| 262 |
+
self.add_log(f"Planned {len(spec.steps)} interactive steps", "info")
|
| 263 |
+
return spec, self.process_logs
|
| 264 |
+
except json.JSONDecodeError as e:
|
| 265 |
+
logger.error(f"JSON Parse Error: {str(e)}", exc_info=True)
|
| 266 |
+
logger.error(f"Failed response text (first 1000 chars): {spec_text[:1000]}")
|
| 267 |
+
logger.error(f"Failed response text (around error position): {spec_text[max(0, e.pos-100):min(len(spec_text), e.pos+100)]}")
|
| 268 |
+
self.add_log(f"JSON Parse Error: {str(e)}", "error")
|
| 269 |
+
|
| 270 |
+
# Save the problematic response for debugging
|
| 271 |
+
debug_file = Config.LIBRARY_PATH / f"debug_response_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
| 272 |
+
with open(debug_file, 'w', encoding='utf-8') as f:
|
| 273 |
+
f.write(f"Error: {str(e)}\n")
|
| 274 |
+
f.write(f"Position: {e.pos}\n")
|
| 275 |
+
f.write("="*60 + "\n")
|
| 276 |
+
f.write(spec_text)
|
| 277 |
+
logger.error(f"Full response saved to: {debug_file}")
|
| 278 |
+
|
| 279 |
+
raise ValueError(f"Failed to parse AI response: {str(e)}")
|
| 280 |
+
|
| 281 |
+
def stage2_generate_code(self, spec: MathSpec, feedback: str = "") -> Tuple[str, List[str]]:
|
| 282 |
+
"""
|
| 283 |
+
Stage 2: Code Generation (Gemini 3 Pro Preview)
|
| 284 |
+
Generates the complete HTML5 application
|
| 285 |
+
"""
|
| 286 |
+
logger.info("="*60)
|
| 287 |
+
logger.info("STAGE 2: CODE GENERATION - Starting Gemini 3 Pro Preview call")
|
| 288 |
+
logger.info(f"Concept: {spec.concept_title}")
|
| 289 |
+
logger.info(f"Steps to Implement: {len(spec.steps)}")
|
| 290 |
+
if feedback:
|
| 291 |
+
logger.info(f"User Feedback: {feedback}")
|
| 292 |
+
self.add_log("Stage 2: Engineering simulation with Gemini 3 Pro Preview (Thinking Enabled)...", "thinking")
|
| 293 |
+
|
| 294 |
+
system_instruction = """You are an expert Senior Frontend Engineer specializing in Educational Technology.
|
| 295 |
+
Your task is to write a SINGLE, self-contained HTML file that implements the provided "Step-by-Step Visual Proof".
|
| 296 |
+
|
| 297 |
+
Rules:
|
| 298 |
+
1. The file must include all HTML, CSS, and JavaScript internally.
|
| 299 |
+
2. Use HTML5 Canvas API or SVG for graphics.
|
| 300 |
+
3. Design: Modern, dark theme (background #0f172a, text #e2e8f0).
|
| 301 |
+
4. **Interaction**: Implement a "Step Navigation" system.
|
| 302 |
+
- Include "Previous" and "Next" buttons.
|
| 303 |
+
- Display the current Step Title and Instruction.
|
| 304 |
+
- The visualization must change state based on the current step.
|
| 305 |
+
5. Ensure math logic is accurate.
|
| 306 |
+
6. Do NOT include markdown blocks. Return raw code only.
|
| 307 |
+
7. Handle resize events.
|
| 308 |
+
8. **VISUAL CLARITY - CRITICAL**:
|
| 309 |
+
- PREVENT OVERLAPPING ELEMENTS.
|
| 310 |
+
- Use a standard HTML layout (Flexbox/Grid) to separate the Canvas/SVG area from the Controls/Instructions.
|
| 311 |
+
"""
|
| 312 |
+
|
| 313 |
+
coding_prompt = f"""
|
| 314 |
+
Implement the following Step-by-Step Math App:
|
| 315 |
+
|
| 316 |
+
Concept: {spec.concept_title}
|
| 317 |
+
Goal: {spec.educational_goal}
|
| 318 |
+
|
| 319 |
+
Steps to Implement (State Machine):
|
| 320 |
+
{chr(10).join([f"{i+1}. [{step['stepTitle']}] {step['instruction']} (Focus: {step['visualFocus']})" for i, step in enumerate(spec.steps)])}
|
| 321 |
+
|
| 322 |
+
Technical Requirements:
|
| 323 |
+
- Visual Elements: {', '.join(spec.visual_spec.get('elements', []))}
|
| 324 |
+
- Interactions: {', '.join(spec.visual_spec.get('interactions', []))}
|
| 325 |
+
- Math Logic: {spec.visual_spec.get('mathLogic', '')}
|
| 326 |
+
|
| 327 |
+
{f"USER FEEDBACK / REFINEMENT REQUEST: {feedback}" if feedback else ""}
|
| 328 |
+
|
| 329 |
+
Generate the full index.html content now.
|
| 330 |
+
"""
|
| 331 |
+
|
| 332 |
+
logger.info("Sending API request to Gemini 3 Pro Preview...")
|
| 333 |
+
try:
|
| 334 |
+
response = self.client.models.generate_content(
|
| 335 |
+
model="gemini-3-pro-preview",
|
| 336 |
+
contents=coding_prompt,
|
| 337 |
+
config=types.GenerateContentConfig(
|
| 338 |
+
system_instruction=system_instruction,
|
| 339 |
+
thinking_config=types.ThinkingConfig(
|
| 340 |
+
include_thoughts=True,
|
| 341 |
+
thinking_budget=-1
|
| 342 |
+
)
|
| 343 |
+
)
|
| 344 |
+
)
|
| 345 |
+
logger.info("✓ API response received successfully")
|
| 346 |
+
logger.debug(f"Response length: {len(response.text)} characters")
|
| 347 |
+
except Exception as api_error:
|
| 348 |
+
logger.error(f"API call failed: {str(api_error)}", exc_info=True)
|
| 349 |
+
raise
|
| 350 |
+
|
| 351 |
+
code = response.text
|
| 352 |
+
code = code.replace('```html', '').replace('```', '').strip()
|
| 353 |
+
|
| 354 |
+
logger.info(f"Generated HTML code length: {len(code)} characters")
|
| 355 |
+
logger.info(f"Code starts with: {code[:100]}...")
|
| 356 |
+
logger.info("STAGE 2: COMPLETE")
|
| 357 |
+
logger.info("="*60)
|
| 358 |
+
self.add_log("✓ Code generated successfully", "success")
|
| 359 |
+
return code, self.process_logs
|
| 360 |
+
|
| 361 |
+
def _get_math_spec_schema(self):
|
| 362 |
+
"""Get JSON schema for MathSpec"""
|
| 363 |
+
return types.Schema(
|
| 364 |
+
type=types.Type.OBJECT,
|
| 365 |
+
properties={
|
| 366 |
+
"conceptTitle": types.Schema(type=types.Type.STRING),
|
| 367 |
+
"educationalGoal": types.Schema(type=types.Type.STRING),
|
| 368 |
+
"explanation": types.Schema(type=types.Type.STRING),
|
| 369 |
+
"steps": types.Schema(
|
| 370 |
+
type=types.Type.ARRAY,
|
| 371 |
+
items=types.Schema(
|
| 372 |
+
type=types.Type.OBJECT,
|
| 373 |
+
properties={
|
| 374 |
+
"stepTitle": types.Schema(type=types.Type.STRING),
|
| 375 |
+
"instruction": types.Schema(type=types.Type.STRING),
|
| 376 |
+
"visualFocus": types.Schema(type=types.Type.STRING)
|
| 377 |
+
},
|
| 378 |
+
required=["stepTitle", "instruction", "visualFocus"]
|
| 379 |
+
)
|
| 380 |
+
),
|
| 381 |
+
"visualSpec": types.Schema(
|
| 382 |
+
type=types.Type.OBJECT,
|
| 383 |
+
properties={
|
| 384 |
+
"elements": types.Schema(type=types.Type.ARRAY, items=types.Schema(type=types.Type.STRING)),
|
| 385 |
+
"interactions": types.Schema(type=types.Type.ARRAY, items=types.Schema(type=types.Type.STRING)),
|
| 386 |
+
"mathLogic": types.Schema(type=types.Type.STRING)
|
| 387 |
+
},
|
| 388 |
+
required=["elements", "interactions", "mathLogic"]
|
| 389 |
+
)
|
| 390 |
+
},
|
| 391 |
+
required=["conceptTitle", "educationalGoal", "explanation", "steps", "visualSpec"]
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
# ==================== Library Management ====================
|
| 395 |
+
|
| 396 |
+
class ProofLibrary:
|
| 397 |
+
"""Manage saved proofs"""
|
| 398 |
+
|
| 399 |
+
@staticmethod
|
| 400 |
+
def save_proof(spec: MathSpec, code: str, input_data: dict) -> str:
|
| 401 |
+
"""Save a proof to the library"""
|
| 402 |
+
proof_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 403 |
+
filename = f"{proof_id}_{spec.concept_title.replace(' ', '_').lower()}.json"
|
| 404 |
+
filepath = Config.LIBRARY_PATH / filename
|
| 405 |
+
|
| 406 |
+
proof_data = {
|
| 407 |
+
"id": proof_id,
|
| 408 |
+
"timestamp": datetime.now().isoformat(),
|
| 409 |
+
"conceptTitle": spec.concept_title,
|
| 410 |
+
"input": input_data,
|
| 411 |
+
"concept": spec.to_dict(),
|
| 412 |
+
"sourceCode": code
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 416 |
+
json.dump(proof_data, f, indent=2)
|
| 417 |
+
|
| 418 |
+
return str(filepath)
|
| 419 |
+
|
| 420 |
+
@staticmethod
|
| 421 |
+
def load_proof(filepath: str) -> dict:
|
| 422 |
+
"""Load a proof from the library"""
|
| 423 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 424 |
+
return json.load(f)
|
| 425 |
+
|
| 426 |
+
@staticmethod
|
| 427 |
+
def list_proofs() -> List[Tuple[str, str]]:
|
| 428 |
+
"""List all saved proofs"""
|
| 429 |
+
proofs = []
|
| 430 |
+
for filepath in Config.LIBRARY_PATH.glob("*.json"):
|
| 431 |
+
try:
|
| 432 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 433 |
+
data = json.load(f)
|
| 434 |
+
title = data.get("conceptTitle", filepath.stem)
|
| 435 |
+
timestamp = data.get("timestamp", "")
|
| 436 |
+
proofs.append((str(filepath), f"{title} ({timestamp})"))
|
| 437 |
+
except Exception:
|
| 438 |
+
continue
|
| 439 |
+
return sorted(proofs, key=lambda x: x[0], reverse=True)
|
| 440 |
+
|
| 441 |
+
@staticmethod
|
| 442 |
+
def export_proof(spec: MathSpec, code: str, input_data: dict) -> str:
|
| 443 |
+
"""Export proof to downloadable JSON"""
|
| 444 |
+
export_data = {
|
| 445 |
+
"appName": "StepWise Math Export",
|
| 446 |
+
"exportedAt": datetime.now().isoformat(),
|
| 447 |
+
"input": input_data,
|
| 448 |
+
"concept": spec.to_dict(),
|
| 449 |
+
"sourceCode": code
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
filename = f"visual-proof-{spec.concept_title.replace(' ', '-').lower()}.json"
|
| 453 |
+
filepath = Config.LIBRARY_PATH / filename
|
| 454 |
+
|
| 455 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 456 |
+
json.dump(export_data, f, indent=2)
|
| 457 |
+
|
| 458 |
+
return str(filepath)
|
| 459 |
+
|
| 460 |
+
# ==================== Gradio Application ====================
|
| 461 |
+
|
| 462 |
+
class StepWiseMathApp:
|
| 463 |
+
"""Main Gradio application"""
|
| 464 |
+
|
| 465 |
+
def __init__(self):
|
| 466 |
+
self.current_spec: Optional[MathSpec] = None
|
| 467 |
+
self.current_code: str = ""
|
| 468 |
+
self.current_logs: List[dict] = []
|
| 469 |
+
self.api_key: str = Config.DEFAULT_API_KEY
|
| 470 |
+
|
| 471 |
+
@staticmethod
|
| 472 |
+
def wrap_html_for_iframe(html_code: str) -> str:
|
| 473 |
+
"""Wrap HTML code in an iframe for proper rendering in Gradio"""
|
| 474 |
+
if not html_code or not html_code.strip():
|
| 475 |
+
return ""
|
| 476 |
+
|
| 477 |
+
# Escape only quotes for srcdoc attribute - do NOT escape HTML tags
|
| 478 |
+
# We need to preserve HTML structure but escape the quotes for attribute value
|
| 479 |
+
escaped_html = html_code.replace('\\', '\\\\').replace('"', '"')
|
| 480 |
+
|
| 481 |
+
# Create iframe with the HTML
|
| 482 |
+
iframe_html = f'''<iframe
|
| 483 |
+
style="width: 100%; height: 600px; border: none; border-radius: 8px;"
|
| 484 |
+
srcdoc="{escaped_html}"
|
| 485 |
+
sandbox="allow-scripts allow-same-origin"
|
| 486 |
+
></iframe>'''
|
| 487 |
+
|
| 488 |
+
return iframe_html
|
| 489 |
+
|
| 490 |
+
def _generate_proof_internal(self, text_input: str = "", url_input: str = "",
|
| 491 |
+
image_input: Any = None, input_mode: str = "text",
|
| 492 |
+
api_key: str = "") -> Tuple[str, str, str, str, str]:
|
| 493 |
+
"""Internal method for generating proofs - shared logic for all three MCP tools"""
|
| 494 |
+
try:
|
| 495 |
+
logger.info("\n" + "#"*60)
|
| 496 |
+
logger.info("# GENERATE_PROOF INITIATED")
|
| 497 |
+
logger.info("#"*60)
|
| 498 |
+
|
| 499 |
+
# Validate inputs
|
| 500 |
+
logger.info(f"Input Validation - Mode: {input_mode}")
|
| 501 |
+
if input_mode == "text" and not text_input.strip():
|
| 502 |
+
logger.warning("Validation failed: Empty text input")
|
| 503 |
+
return "", "", "❌ Error: Please enter a math problem description", "", ""
|
| 504 |
+
elif input_mode == "url" and not url_input.strip():
|
| 505 |
+
logger.warning("Validation failed: Empty URL input")
|
| 506 |
+
return "", "", "❌ Error: Please enter a valid URL", "", ""
|
| 507 |
+
elif input_mode == "image" and image_input is None:
|
| 508 |
+
logger.warning("Validation failed: No image provided")
|
| 509 |
+
return "", "", "❌ Error: Please upload an image", "", ""
|
| 510 |
+
logger.info("✓ Input validation passed")
|
| 511 |
+
|
| 512 |
+
# Use provided API key or default
|
| 513 |
+
logger.info("Checking API key configuration...")
|
| 514 |
+
key = api_key.strip() if api_key.strip() else Config.DEFAULT_API_KEY
|
| 515 |
+
if not key:
|
| 516 |
+
logger.error("No API key configured")
|
| 517 |
+
return "", "", "❌ Error: No API key configured. Please set GEMINI_API_KEY or provide one.", "", ""
|
| 518 |
+
logger.info("✓ API key found")
|
| 519 |
+
|
| 520 |
+
self.api_key = key
|
| 521 |
+
pipeline = GeminiPipeline(key)
|
| 522 |
+
|
| 523 |
+
logger.info("Pipeline initialized")
|
| 524 |
+
start_time = time.time()
|
| 525 |
+
|
| 526 |
+
# Stage 1: Analyze concept
|
| 527 |
+
logger.info("\nExecuting STAGE 1: Concept Analysis...")
|
| 528 |
+
self.current_spec, logs = pipeline.stage1_analyze_concept(
|
| 529 |
+
input_text=text_input,
|
| 530 |
+
input_url=url_input,
|
| 531 |
+
input_image=image_input,
|
| 532 |
+
input_mode=input_mode
|
| 533 |
+
)
|
| 534 |
+
logger.info(f"✓ Stage 1 complete - Concept: {self.current_spec.concept_title}")
|
| 535 |
+
|
| 536 |
+
# Stage 2: Generate code
|
| 537 |
+
logger.info("\nExecuting STAGE 2: Code Generation...")
|
| 538 |
+
self.current_code, logs = pipeline.stage2_generate_code(self.current_spec)
|
| 539 |
+
self.current_logs = logs
|
| 540 |
+
logger.info(f"✓ Stage 2 complete - Generated {len(self.current_code)} characters of HTML")
|
| 541 |
+
|
| 542 |
+
# Format outputs
|
| 543 |
+
elapsed = time.time() - start_time
|
| 544 |
+
|
| 545 |
+
# Concept details
|
| 546 |
+
concept_md = f"""# {self.current_spec.concept_title}
|
| 547 |
+
|
| 548 |
+
**Educational Goal:** {self.current_spec.educational_goal}
|
| 549 |
+
|
| 550 |
+
## Explanation
|
| 551 |
+
|
| 552 |
+
{self.current_spec.explanation}
|
| 553 |
+
|
| 554 |
+
## Proof Steps
|
| 555 |
+
|
| 556 |
+
"""
|
| 557 |
+
for i, step in enumerate(self.current_spec.steps, 1):
|
| 558 |
+
concept_md += f"### Step {i}: {step['stepTitle']}\n\n"
|
| 559 |
+
concept_md += f"**Instruction:** {step['instruction']}\n\n"
|
| 560 |
+
concept_md += f"**Visual Focus:** {step['visualFocus']}\n\n"
|
| 561 |
+
|
| 562 |
+
# Logs
|
| 563 |
+
logs_text = "\n".join([f"[{log['timestamp']}] {log['message']}" for log in logs])
|
| 564 |
+
logs_text += f"\n\n✓ Process Complete in {elapsed:.2f}s"
|
| 565 |
+
|
| 566 |
+
# Status
|
| 567 |
+
status = f"✅ Generated '{self.current_spec.concept_title}' successfully in {elapsed:.2f}s"
|
| 568 |
+
|
| 569 |
+
logger.info(f"\n✓ GENERATE_PROOF COMPLETED SUCCESSFULLY")
|
| 570 |
+
logger.info(f" - Concept: {self.current_spec.concept_title}")
|
| 571 |
+
logger.info(f" - Steps: {len(self.current_spec.steps)}")
|
| 572 |
+
logger.info(f" - HTML Size: {len(self.current_code)} bytes")
|
| 573 |
+
logger.info(f" - Total Time: {elapsed:.2f}s")
|
| 574 |
+
logger.info("#"*60 + "\n")
|
| 575 |
+
|
| 576 |
+
# Wrap HTML in iframe for proper rendering
|
| 577 |
+
rendered_html = self.wrap_html_for_iframe(self.current_code)
|
| 578 |
+
|
| 579 |
+
return rendered_html, concept_md, status, logs_text, self.current_code
|
| 580 |
+
|
| 581 |
+
except Exception as e:
|
| 582 |
+
error_msg = f"❌ Error: {str(e)}"
|
| 583 |
+
logger.error(f"GENERATE_PROOF FAILED: {str(e)}", exc_info=True)
|
| 584 |
+
logger.error("#"*60 + "\n")
|
| 585 |
+
return "", "", error_msg, str(e), ""
|
| 586 |
+
|
| 587 |
+
def create_math_specification_from_text(self, text_input: str, api_key: str = "") -> str:
|
| 588 |
+
"""
|
| 589 |
+
Creates a structured mathematical teaching specification from a natural language problem description.
|
| 590 |
+
This is the first step in creating an interactive visual proof. Use this when you have a text description of a math problem
|
| 591 |
+
and need to transform it into a pedagogical framework with step-by-step learning objectives. Returns a JSON specification
|
| 592 |
+
that can be passed to build_interactive_proof_from_specification.
|
| 593 |
+
|
| 594 |
+
Args:
|
| 595 |
+
text_input (str): Natural language description of the mathematical problem or theorem to analyze.
|
| 596 |
+
Example: "Prove that the sum of angles in a triangle equals 180 degrees" or
|
| 597 |
+
"Explain the Pythagorean theorem visually".
|
| 598 |
+
api_key (str, optional): Google Gemini API key for authentication. If empty or not provided, falls back to the GEMINI_API_KEY environment variable. Defaults to "".
|
| 599 |
+
|
| 600 |
+
Returns:
|
| 601 |
+
str: A JSON-formatted string containing the complete mathematical specification with fields:
|
| 602 |
+
- conceptTitle: The name of the mathematical concept
|
| 603 |
+
- educationalGoal: Learning objective for students
|
| 604 |
+
- explanation: Detailed markdown explanation
|
| 605 |
+
- steps: Array of interactive teaching steps
|
| 606 |
+
- visualSpec: Technical requirements for visualization
|
| 607 |
+
Returns a JSON object with an "error" field if the creation fails.
|
| 608 |
+
"""
|
| 609 |
+
try:
|
| 610 |
+
key = api_key.strip() if api_key.strip() else Config.DEFAULT_API_KEY
|
| 611 |
+
if not key:
|
| 612 |
+
return json.dumps({"error": "No API key configured"})
|
| 613 |
+
if not text_input.strip():
|
| 614 |
+
return json.dumps({"error": "Empty text input"})
|
| 615 |
+
|
| 616 |
+
pipeline = GeminiPipeline(key)
|
| 617 |
+
spec, logs = pipeline.stage1_analyze_concept(input_text=text_input, input_mode="text")
|
| 618 |
+
self.current_spec = spec
|
| 619 |
+
|
| 620 |
+
return json.dumps(spec.to_dict(), indent=2)
|
| 621 |
+
except Exception as e:
|
| 622 |
+
return json.dumps({"error": str(e)})
|
| 623 |
+
|
| 624 |
+
def create_math_specification_from_url(self, url_input: str, api_key: str = "") -> str:
|
| 625 |
+
"""
|
| 626 |
+
Creates a structured mathematical teaching specification from a web URL containing a math problem.
|
| 627 |
+
This is the first step in creating an interactive visual proof. Use this when you have a webpage, article, or
|
| 628 |
+
online resource containing a math problem that needs to be transformed into an educational framework.
|
| 629 |
+
The tool fetches and analyzes the content from the URL automatically. Returns a JSON specification that can be
|
| 630 |
+
passed to build_interactive_proof_from_specification.
|
| 631 |
+
|
| 632 |
+
Args:
|
| 633 |
+
url_input (str): Complete URL pointing to a webpage or resource containing the mathematical problem.
|
| 634 |
+
Example: "https://en.wikipedia.org/wiki/Pythagorean_theorem" or
|
| 635 |
+
"https://mathworld.wolfram.com/Circle.html". Must be a valid http:// or https:// URL.
|
| 636 |
+
api_key (str, optional): Google Gemini API key for authentication. If empty or not provided, falls back to the GEMINI_API_KEY environment variable. Defaults to "".
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
str: A JSON-formatted string containing the complete mathematical specification with fields:
|
| 640 |
+
- conceptTitle: The name of the mathematical concept
|
| 641 |
+
- educationalGoal: Learning objective for students
|
| 642 |
+
- explanation: Detailed markdown explanation
|
| 643 |
+
- steps: Array of interactive teaching steps
|
| 644 |
+
- visualSpec: Technical requirements for visualization
|
| 645 |
+
Returns a JSON object with an "error" field if the URL is invalid or creation fails.
|
| 646 |
+
"""
|
| 647 |
+
try:
|
| 648 |
+
key = api_key.strip() if api_key.strip() else Config.DEFAULT_API_KEY
|
| 649 |
+
if not key:
|
| 650 |
+
return json.dumps({"error": "No API key configured"})
|
| 651 |
+
if not url_input.strip():
|
| 652 |
+
return json.dumps({"error": "Empty URL input"})
|
| 653 |
+
|
| 654 |
+
pipeline = GeminiPipeline(key)
|
| 655 |
+
spec, logs = pipeline.stage1_analyze_concept(input_url=url_input, input_mode="url")
|
| 656 |
+
self.current_spec = spec
|
| 657 |
+
|
| 658 |
+
return json.dumps(spec.to_dict(), indent=2)
|
| 659 |
+
except Exception as e:
|
| 660 |
+
return json.dumps({"error": str(e)})
|
| 661 |
+
|
| 662 |
+
def create_math_specification_from_image(self, image_input: Any, api_key: str = "") -> str:
|
| 663 |
+
"""
|
| 664 |
+
Creates a structured mathematical teaching specification from an uploaded image containing a math problem.
|
| 665 |
+
This is the first step in creating an interactive visual proof. Use this when you have a photo, screenshot, or
|
| 666 |
+
diagram of a math problem (from textbooks, whiteboards, handwritten notes, etc.) that needs to be interpreted
|
| 667 |
+
and transformed into an educational framework. The AI performs optical character recognition and mathematical
|
| 668 |
+
reasoning on the image. Returns a JSON specification that can be passed to build_interactive_proof_from_specification.
|
| 669 |
+
|
| 670 |
+
Args:
|
| 671 |
+
image_input (Any): A PIL Image object containing the mathematical problem. The image should clearly show
|
| 672 |
+
the problem text, equations, or diagrams. Supported formats include JPEG, PNG, and other
|
| 673 |
+
common image formats. For best results, ensure good lighting and contrast.
|
| 674 |
+
api_key (str, optional): Google Gemini API key for authentication. If empty or not provided, falls back to the GEMINI_API_KEY environment variable. Defaults to "".
|
| 675 |
+
|
| 676 |
+
Returns:
|
| 677 |
+
str: A JSON-formatted string containing the complete mathematical specification with fields:
|
| 678 |
+
- conceptTitle: The name of the mathematical concept
|
| 679 |
+
- educationalGoal: Learning objective for students
|
| 680 |
+
- explanation: Detailed markdown explanation
|
| 681 |
+
- steps: Array of interactive teaching steps
|
| 682 |
+
- visualSpec: Technical requirements for visualization
|
| 683 |
+
Returns a JSON object with an "error" field if the image cannot be processed or creation fails.
|
| 684 |
+
"""
|
| 685 |
+
try:
|
| 686 |
+
key = api_key.strip() if api_key.strip() else Config.DEFAULT_API_KEY
|
| 687 |
+
if not key:
|
| 688 |
+
return json.dumps({"error": "No API key configured"})
|
| 689 |
+
if image_input is None:
|
| 690 |
+
return json.dumps({"error": "No image provided"})
|
| 691 |
+
|
| 692 |
+
pipeline = GeminiPipeline(key)
|
| 693 |
+
spec, logs = pipeline.stage1_analyze_concept(input_image=image_input, input_mode="image")
|
| 694 |
+
self.current_spec = spec
|
| 695 |
+
|
| 696 |
+
return json.dumps(spec.to_dict(), indent=2)
|
| 697 |
+
except Exception as e:
|
| 698 |
+
return json.dumps({"error": str(e)})
|
| 699 |
+
|
| 700 |
+
def build_interactive_proof_from_specification(self, specification_json: str, api_key: str = "") -> str:
|
| 701 |
+
"""
|
| 702 |
+
Builds a complete, self-contained HTML/JavaScript application from a mathematical teaching specification.
|
| 703 |
+
This is the second step in creating an interactive visual proof. Use this after obtaining a specification
|
| 704 |
+
from any of the create_math_specification_from_* methods. The tool produces production-ready code with step-by-step navigation,
|
| 705 |
+
interactive visualizations using Canvas/SVG, and a modern dark-themed UI. The output is a single HTML file with all
|
| 706 |
+
CSS and JavaScript embedded, ready to be saved and opened in any web browser.
|
| 707 |
+
|
| 708 |
+
Args:
|
| 709 |
+
specification_json (str): A JSON-formatted string containing the mathematical specification. This should be the exact
|
| 710 |
+
output from create_math_specification_from_text, create_math_specification_from_url, or create_math_specification_from_image.
|
| 711 |
+
The JSON must include: conceptTitle, educationalGoal, explanation, steps array, and visualSpec.
|
| 712 |
+
Example: '{"conceptTitle": "Pythagorean Theorem", "steps": [...], ...}'.
|
| 713 |
+
api_key (str, optional): Google Gemini API key for authentication. If empty or not provided,
|
| 714 |
+
falls back to the GEMINI_API_KEY environment variable or the key used in the
|
| 715 |
+
previous specification creation step. Defaults to "".
|
| 716 |
+
|
| 717 |
+
Returns:
|
| 718 |
+
str: A complete HTML document as a string, containing all HTML structure, CSS styling, and JavaScript code
|
| 719 |
+
needed for the interactive mathematical proof. The code includes step navigation buttons, dynamic
|
| 720 |
+
visualizations, and responsive design. Returns an error message string (starting with "Error:") if
|
| 721 |
+
the specification JSON is invalid or build fails.
|
| 722 |
+
"""
|
| 723 |
+
try:
|
| 724 |
+
key = api_key.strip() if api_key.strip() else Config.DEFAULT_API_KEY
|
| 725 |
+
if not key:
|
| 726 |
+
return "Error: No API key configured"
|
| 727 |
+
|
| 728 |
+
# Parse the specification JSON
|
| 729 |
+
concept_data = json.loads(specification_json)
|
| 730 |
+
if "error" in concept_data:
|
| 731 |
+
return f"Error: {concept_data['error']}"
|
| 732 |
+
|
| 733 |
+
spec = MathSpec(concept_data)
|
| 734 |
+
self.current_spec = spec
|
| 735 |
+
|
| 736 |
+
pipeline = GeminiPipeline(key)
|
| 737 |
+
code, logs = pipeline.stage2_generate_code(spec)
|
| 738 |
+
self.current_code = code
|
| 739 |
+
|
| 740 |
+
return code
|
| 741 |
+
except json.JSONDecodeError as e:
|
| 742 |
+
return f"Error: Invalid concept JSON - {str(e)}"
|
| 743 |
+
except Exception as e:
|
| 744 |
+
return f"Error: {str(e)}"
|
| 745 |
+
|
| 746 |
+
def generate_proof(self, text_input: str = "", url_input: str = "",
|
| 747 |
+
image_input: Any = None, input_mode: str = "Text",
|
| 748 |
+
api_key: str = "") -> Tuple[str, str, str, str, str]:
|
| 749 |
+
"""
|
| 750 |
+
Generate a guided, interactive visual proof (UI version with mode selector).
|
| 751 |
+
This method is for the Gradio UI and delegates to the appropriate specific method.
|
| 752 |
+
"""
|
| 753 |
+
mode_map = {"Text": "text", "Image": "image", "URL": "url"}
|
| 754 |
+
return self._generate_proof_internal(
|
| 755 |
+
text_input=text_input,
|
| 756 |
+
url_input=url_input,
|
| 757 |
+
image_input=image_input,
|
| 758 |
+
input_mode=mode_map.get(input_mode, "text"),
|
| 759 |
+
api_key=api_key
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
def refine_proof(self, feedback: str, api_key: str = "") -> Tuple[str, str, str, str]:
|
| 763 |
+
"""Refine the current proof based on feedback"""
|
| 764 |
+
if not self.current_spec or not feedback.strip():
|
| 765 |
+
rendered_html = self.wrap_html_for_iframe(self.current_code)
|
| 766 |
+
return rendered_html, "⚠️ No proof loaded or no feedback provided", "", self.current_code
|
| 767 |
+
|
| 768 |
+
try:
|
| 769 |
+
key = api_key.strip() if api_key.strip() else self.api_key
|
| 770 |
+
pipeline = GeminiPipeline(key)
|
| 771 |
+
|
| 772 |
+
# Regenerate with feedback
|
| 773 |
+
self.current_code, logs = pipeline.stage2_generate_code(
|
| 774 |
+
self.current_spec,
|
| 775 |
+
feedback=feedback
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
logs_text = "\n".join([f"[{log['timestamp']}] {log['message']}" for log in logs])
|
| 779 |
+
status = f"✅ Refinement applied based on feedback"
|
| 780 |
+
|
| 781 |
+
# Wrap HTML in iframe for proper rendering
|
| 782 |
+
rendered_html = self.wrap_html_for_iframe(self.current_code)
|
| 783 |
+
|
| 784 |
+
return rendered_html, status, logs_text, self.current_code
|
| 785 |
+
|
| 786 |
+
except Exception as e:
|
| 787 |
+
rendered_html = self.wrap_html_for_iframe(self.current_code)
|
| 788 |
+
return rendered_html, f"❌ Refinement failed: {str(e)}", str(e), self.current_code
|
| 789 |
+
|
| 790 |
+
def save_to_library(self, text_input: str, url_input: str,
|
| 791 |
+
image_input: Any, input_mode: str) -> str:
|
| 792 |
+
"""Save current proof to library"""
|
| 793 |
+
if not self.current_spec or not self.current_code:
|
| 794 |
+
return "⚠️ No proof to save"
|
| 795 |
+
|
| 796 |
+
try:
|
| 797 |
+
input_data = {
|
| 798 |
+
"mode": input_mode.lower(),
|
| 799 |
+
"text": text_input if input_mode == "Text" else None,
|
| 800 |
+
"url": url_input if input_mode == "URL" else None,
|
| 801 |
+
"image": image_input if input_mode == "Image" else None
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
filepath = ProofLibrary.save_proof(
|
| 805 |
+
self.current_spec,
|
| 806 |
+
self.current_code,
|
| 807 |
+
input_data
|
| 808 |
+
)
|
| 809 |
+
return f"✅ Proof saved to library: {filepath}"
|
| 810 |
+
except Exception as e:
|
| 811 |
+
return f"❌ Save failed: {str(e)}"
|
| 812 |
+
|
| 813 |
+
def export_proof_file(self, text_input: str, url_input: str,
|
| 814 |
+
image_input: Any, input_mode: str) -> Tuple[str, str]:
|
| 815 |
+
"""Export proof as downloadable file"""
|
| 816 |
+
if not self.current_spec or not self.current_code:
|
| 817 |
+
return None, "⚠️ No proof to export"
|
| 818 |
+
|
| 819 |
+
try:
|
| 820 |
+
input_data = {
|
| 821 |
+
"mode": input_mode.lower(),
|
| 822 |
+
"text": text_input if input_mode == "Text" else None,
|
| 823 |
+
"url": url_input if input_mode == "URL" else None
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
filepath = ProofLibrary.export_proof(
|
| 827 |
+
self.current_spec,
|
| 828 |
+
self.current_code,
|
| 829 |
+
input_data
|
| 830 |
+
)
|
| 831 |
+
return filepath, f"✅ Proof exported: {filepath}"
|
| 832 |
+
except Exception as e:
|
| 833 |
+
return None, f"❌ Export failed: {str(e)}"
|
| 834 |
+
|
| 835 |
+
def load_from_library(self, filepath: str) -> Tuple[str, str, str, str, str]:
|
| 836 |
+
"""Load a proof from library"""
|
| 837 |
+
if not filepath:
|
| 838 |
+
return "", "", "", "⚠️ No proof selected", ""
|
| 839 |
+
|
| 840 |
+
try:
|
| 841 |
+
data = ProofLibrary.load_proof(filepath)
|
| 842 |
+
self.current_spec = MathSpec(data["concept"])
|
| 843 |
+
self.current_code = data["sourceCode"]
|
| 844 |
+
|
| 845 |
+
# Format concept
|
| 846 |
+
concept_md = f"""# {self.current_spec.concept_title}
|
| 847 |
+
|
| 848 |
+
**Educational Goal:** {self.current_spec.educational_goal}
|
| 849 |
+
|
| 850 |
+
## Explanation
|
| 851 |
+
|
| 852 |
+
{self.current_spec.explanation}
|
| 853 |
+
"""
|
| 854 |
+
|
| 855 |
+
# Wrap HTML in iframe for proper rendering
|
| 856 |
+
rendered_html = self.wrap_html_for_iframe(self.current_code)
|
| 857 |
+
|
| 858 |
+
return (
|
| 859 |
+
rendered_html,
|
| 860 |
+
concept_md,
|
| 861 |
+
f"✅ Loaded '{self.current_spec.concept_title}' from library",
|
| 862 |
+
"",
|
| 863 |
+
self.current_code
|
| 864 |
+
)
|
| 865 |
+
except Exception as e:
|
| 866 |
+
return "", "", f"❌ Load failed: {str(e)}", "", ""
|
| 867 |
+
|
| 868 |
+
def load_example(self, example_name: str) -> Tuple[str, str, str, str, str]:
|
| 869 |
+
"""Load a pre-built example"""
|
| 870 |
+
example_files = {
|
| 871 |
+
"Probability of an Odd Sum": "001-visual-proof-probability-of-an-odd-sum.json",
|
| 872 |
+
"Pythagorean Theorem": "002-visual-proof-pythagorean-theorem.json",
|
| 873 |
+
"Orthodiagonal Quads": "003-visual-proof-area-of-quadrilaterals-with-perpendicular-diagonals.json"
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
if example_name not in example_files:
|
| 877 |
+
return "", "", "", "⚠️ Example not found", ""
|
| 878 |
+
|
| 879 |
+
filepath = Config.EXAMPLES_PATH / example_files[example_name]
|
| 880 |
+
if not filepath.exists():
|
| 881 |
+
# Try React app examples folder
|
| 882 |
+
filepath = Path("../react-app/public/examples") / example_files[example_name]
|
| 883 |
+
|
| 884 |
+
if not filepath.exists():
|
| 885 |
+
return "", "", "", f"⚠️ Example file not found: {filepath}", ""
|
| 886 |
+
|
| 887 |
+
return self.load_from_library(str(filepath))
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
# ==================== MCP Prompts & Resources ====================
|
| 891 |
+
|
| 892 |
+
# MCP Prompt functions using @gr.mcp.prompt() decorator
|
| 893 |
+
@gr.mcp.prompt()
|
| 894 |
+
def create_visual_math_proof(problem_description: str, input_type: str = "text") -> str:
|
| 895 |
+
"""Create a complete interactive visual proof for any math problem in two steps.
|
| 896 |
+
|
| 897 |
+
This prompt guides you through the two-step workflow:
|
| 898 |
+
1. Create a mathematical specification from your input
|
| 899 |
+
2. Build an interactive HTML/JS proof application
|
| 900 |
+
|
| 901 |
+
Args:
|
| 902 |
+
problem_description: The mathematical problem, theorem, or concept to visualize
|
| 903 |
+
input_type: Type of input - 'text' for natural language, 'url' for web resources, or 'image' for uploaded pictures
|
| 904 |
+
|
| 905 |
+
Returns:
|
| 906 |
+
A structured prompt for creating the visual proof
|
| 907 |
+
"""
|
| 908 |
+
input_types = {
|
| 909 |
+
"text": "create_math_specification_from_text",
|
| 910 |
+
"url": "create_math_specification_from_url",
|
| 911 |
+
"image": "create_math_specification_from_image"
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
tool_name = input_types.get(input_type, input_types["text"])
|
| 915 |
+
|
| 916 |
+
return f"""Please create an interactive visual proof for this mathematical concept: {problem_description}
|
| 917 |
+
|
| 918 |
+
Follow this two-step process:
|
| 919 |
+
|
| 920 |
+
**Step 1: Create Specification**
|
| 921 |
+
Use the appropriate tool based on input type '{input_type}':
|
| 922 |
+
- For text: {input_types["text"]}
|
| 923 |
+
- For URL: {input_types["url"]}
|
| 924 |
+
- For image: {input_types["image"]}
|
| 925 |
+
|
| 926 |
+
Recommended tool for this request: {tool_name}
|
| 927 |
+
|
| 928 |
+
**Step 2: Build Interactive Proof**
|
| 929 |
+
Once you have the JSON specification, use:
|
| 930 |
+
- build_interactive_proof_from_specification
|
| 931 |
+
|
| 932 |
+
The result will be a complete, self-contained HTML application with:
|
| 933 |
+
- Step-by-step navigation
|
| 934 |
+
- Interactive visualizations
|
| 935 |
+
- Real-time mathematical updates
|
| 936 |
+
- Modern dark-themed UI
|
| 937 |
+
"""
|
| 938 |
+
|
| 939 |
+
@gr.mcp.prompt()
|
| 940 |
+
def create_math_specification(problem_input: str, educational_focus: str = "step-by-step visual understanding") -> str:
|
| 941 |
+
"""Analyze a math problem and create a pedagogical specification with teaching steps.
|
| 942 |
+
|
| 943 |
+
This prompt helps create a detailed teaching plan for any mathematical concept,
|
| 944 |
+
breaking it down into interactive learning steps.
|
| 945 |
+
|
| 946 |
+
Args:
|
| 947 |
+
problem_input: The mathematical problem as text, URL, or image description
|
| 948 |
+
educational_focus: Specific learning objectives or teaching approach to emphasize
|
| 949 |
+
|
| 950 |
+
Returns:
|
| 951 |
+
A structured prompt for specification creation
|
| 952 |
+
"""
|
| 953 |
+
return f"""Please analyze this mathematical problem and create a pedagogical specification: {problem_input}
|
| 954 |
+
|
| 955 |
+
Educational Focus: {educational_focus}
|
| 956 |
+
|
| 957 |
+
The specification should include:
|
| 958 |
+
1. **Concept Title**: Clear name of the mathematical concept
|
| 959 |
+
2. **Educational Goal**: What students should learn
|
| 960 |
+
3. **Explanation**: Detailed markdown explanation
|
| 961 |
+
4. **Steps**: 3-6 interactive teaching steps, each with:
|
| 962 |
+
- Step title and instruction
|
| 963 |
+
- Visual focus (what changes in the visualization)
|
| 964 |
+
5. **Visual Spec**: Technical requirements including:
|
| 965 |
+
- Visual elements needed (shapes, graphs, etc.)
|
| 966 |
+
- Interactions (drag, click, sliders)
|
| 967 |
+
- Mathematical logic and formulas
|
| 968 |
+
|
| 969 |
+
Use create_math_specification_from_text, create_math_specification_from_url, or create_math_specification_from_image based on your input type.
|
| 970 |
+
"""
|
| 971 |
+
|
| 972 |
+
@gr.mcp.prompt()
|
| 973 |
+
def build_from_specification(specification: str, customization: str = "standard") -> str:
|
| 974 |
+
"""Build an interactive HTML/JS application from a math teaching specification.
|
| 975 |
+
|
| 976 |
+
This prompt guides building a production-ready interactive proof application
|
| 977 |
+
from a mathematical specification JSON.
|
| 978 |
+
|
| 979 |
+
Args:
|
| 980 |
+
specification: JSON specification from create_math_specification_from_* tools
|
| 981 |
+
customization: Additional visual or interactive customizations to apply
|
| 982 |
+
|
| 983 |
+
Returns:
|
| 984 |
+
A structured prompt for building the application
|
| 985 |
+
"""
|
| 986 |
+
return f"""Please build an interactive proof application from this specification:
|
| 987 |
+
|
| 988 |
+
{specification}
|
| 989 |
+
|
| 990 |
+
Customization requests: {customization}
|
| 991 |
+
|
| 992 |
+
Use the build_interactive_proof_from_specification tool to generate a complete HTML/JavaScript application with:
|
| 993 |
+
- Self-contained single file (all CSS/JS embedded)
|
| 994 |
+
- Step navigation system (Previous/Next buttons)
|
| 995 |
+
- Interactive Canvas/SVG visualizations
|
| 996 |
+
- Real-time mathematical updates
|
| 997 |
+
- Modern dark theme (#0f172a background, #e2e8f0 text)
|
| 998 |
+
- Responsive design that prevents overlapping elements
|
| 999 |
+
- Clear separation of controls, text, and diagrams
|
| 1000 |
+
|
| 1001 |
+
The output will be ready to save as an .html file and open in any browser.
|
| 1002 |
+
"""
|
| 1003 |
+
|
| 1004 |
+
# MCP Resource functions using @gr.mcp.resource() decorator
|
| 1005 |
+
@gr.mcp.resource("stepwise://specification-template", mime_type="application/json")
|
| 1006 |
+
def get_specification_template() -> str:
|
| 1007 |
+
"""JSON template for mathematical teaching specifications.
|
| 1008 |
+
|
| 1009 |
+
Returns the standard structure for creating math concept specifications
|
| 1010 |
+
that can be used with build_interactive_proof_from_specification.
|
| 1011 |
+
"""
|
| 1012 |
+
return json.dumps({
|
| 1013 |
+
"conceptTitle": "Name of the mathematical concept",
|
| 1014 |
+
"educationalGoal": "What students should learn from this proof",
|
| 1015 |
+
"explanation": "Detailed markdown explanation of the concept",
|
| 1016 |
+
"steps": [
|
| 1017 |
+
{
|
| 1018 |
+
"stepTitle": "Step name",
|
| 1019 |
+
"instruction": "What the student should do or observe",
|
| 1020 |
+
"visualFocus": "What part of the visualization changes"
|
| 1021 |
+
}
|
| 1022 |
+
],
|
| 1023 |
+
"visualSpec": {
|
| 1024 |
+
"elements": ["List of visual objects needed"],
|
| 1025 |
+
"interactions": ["User actions like drag, click, slider"],
|
| 1026 |
+
"mathLogic": "Formulas and calculations needed"
|
| 1027 |
+
}
|
| 1028 |
+
}, indent=2)
|
| 1029 |
+
|
| 1030 |
+
@gr.mcp.resource("stepwise://example-pythagorean", mime_type="application/json")
|
| 1031 |
+
def get_pythagorean_example() -> str:
|
| 1032 |
+
"""Complete example of Pythagorean theorem visual proof specification.
|
| 1033 |
+
|
| 1034 |
+
Returns a real working example showing how to structure a mathematical
|
| 1035 |
+
proof specification for the Pythagorean theorem.
|
| 1036 |
+
"""
|
| 1037 |
+
example_path = Config.EXAMPLES_PATH / "002-visual-proof-pythagorean-theorem.json"
|
| 1038 |
+
if example_path.exists():
|
| 1039 |
+
with open(example_path, 'r', encoding='utf-8') as f:
|
| 1040 |
+
data = json.load(f)
|
| 1041 |
+
return json.dumps(data.get("concept", {}), indent=2)
|
| 1042 |
+
return json.dumps({"error": "Example file not found"})
|
| 1043 |
+
|
| 1044 |
+
@gr.mcp.resource("stepwise://example-probability", mime_type="application/json")
|
| 1045 |
+
def get_probability_example() -> str:
|
| 1046 |
+
"""Complete example of probability concept visualization.
|
| 1047 |
+
|
| 1048 |
+
Returns a real working example showing how to structure a mathematical
|
| 1049 |
+
proof specification for probability concepts.
|
| 1050 |
+
"""
|
| 1051 |
+
example_path = Config.EXAMPLES_PATH / "001-visual-proof-probability-of-an-odd-sum.json"
|
| 1052 |
+
if example_path.exists():
|
| 1053 |
+
with open(example_path, 'r', encoding='utf-8') as f:
|
| 1054 |
+
data = json.load(f)
|
| 1055 |
+
return json.dumps(data.get("concept", {}), indent=2)
|
| 1056 |
+
return json.dumps({"error": "Example file not found"})
|
| 1057 |
+
|
| 1058 |
+
@gr.mcp.resource("stepwise://workflow-guide", mime_type="text/markdown")
|
| 1059 |
+
def get_workflow_guide() -> str:
|
| 1060 |
+
"""Guide for using the two-step process: specification creation → proof building.
|
| 1061 |
+
|
| 1062 |
+
Returns comprehensive documentation on how to use the StepWise Math
|
| 1063 |
+
two-step workflow effectively.
|
| 1064 |
+
"""
|
| 1065 |
+
return """# StepWise Math: Two-Step Workflow Guide
|
| 1066 |
+
|
| 1067 |
+
## Overview
|
| 1068 |
+
StepWise Math uses a two-step process to create interactive visual proofs:
|
| 1069 |
+
|
| 1070 |
+
### Step 1: Create Mathematical Specification
|
| 1071 |
+
Use one of these tools based on your input type:
|
| 1072 |
+
- `create_math_specification_from_text` - For natural language descriptions
|
| 1073 |
+
- `create_math_specification_from_url` - For web resources (Wikipedia, Khan Academy, etc.)
|
| 1074 |
+
- `create_math_specification_from_image` - For photos/screenshots of problems
|
| 1075 |
+
|
| 1076 |
+
**Output**: JSON specification with teaching steps and visual requirements
|
| 1077 |
+
**Processing time**: ~10-15 seconds
|
| 1078 |
+
|
| 1079 |
+
### Step 2: Build Interactive Proof
|
| 1080 |
+
Use the specification from Step 1:
|
| 1081 |
+
- `build_interactive_proof_from_specification` - Takes the JSON specification
|
| 1082 |
+
|
| 1083 |
+
**Output**: Complete HTML/JavaScript application
|
| 1084 |
+
**Processing time**: ~20-30 seconds
|
| 1085 |
+
|
| 1086 |
+
## Example Workflow
|
| 1087 |
+
|
| 1088 |
+
```python
|
| 1089 |
+
# Step 1: Create specification from text
|
| 1090 |
+
specification = create_math_specification_from_text(
|
| 1091 |
+
text_input="Prove that the angles in a triangle sum to 180 degrees"
|
| 1092 |
+
)
|
| 1093 |
+
|
| 1094 |
+
# Step 2: Build interactive proof
|
| 1095 |
+
html_code = build_interactive_proof_from_specification(
|
| 1096 |
+
specification_json=specification
|
| 1097 |
+
)
|
| 1098 |
+
|
| 1099 |
+
# Result: Save or display the HTML application
|
| 1100 |
+
```
|
| 1101 |
+
|
| 1102 |
+
## Tips
|
| 1103 |
+
- Each tool can work independently or as part of the two-step pipeline
|
| 1104 |
+
- Specifications are reusable - create once, build multiple times with different customizations
|
| 1105 |
+
- All tools support optional API key parameter for using custom Gemini API keys
|
| 1106 |
+
- The specification includes 3-6 interactive teaching steps
|
| 1107 |
+
- Generated apps include step navigation, interactive visualizations, and real-time updates
|
| 1108 |
+
"""
|
| 1109 |
+
|
| 1110 |
+
# ==================== Gradio Interface ====================
|
| 1111 |
+
|
| 1112 |
+
def create_gradio_app():
|
| 1113 |
+
"""Create and configure the Gradio interface"""
|
| 1114 |
+
app = StepWiseMathApp()
|
| 1115 |
+
|
| 1116 |
+
# Load default example on initialization
|
| 1117 |
+
default_example = "Probability of an Odd Sum"
|
| 1118 |
+
default_html, default_concept, default_status, default_logs, default_code = app.load_example(default_example)
|
| 1119 |
+
|
| 1120 |
+
with gr.Blocks(
|
| 1121 |
+
title="StepWise Math - Gradio Edition"
|
| 1122 |
+
) as demo:
|
| 1123 |
+
|
| 1124 |
+
# Header
|
| 1125 |
+
gr.HTML("""
|
| 1126 |
+
<div class="main-header">
|
| 1127 |
+
<h1>🎓 StepWise Math</h1>
|
| 1128 |
+
<p style="font-size: 1.2rem; margin-top: 0.5rem;">Transform Static Math Problems into Living, Interactive Step-by-Step Visual Proofs</p>
|
| 1129 |
+
<p style="opacity: 0.9; margin-top: 0.5rem;">Powered by Google Gemini 2.5 Flash & Gemini 3.0 Pro with Extended Thinking</p>
|
| 1130 |
+
</div>
|
| 1131 |
+
""")
|
| 1132 |
+
|
| 1133 |
+
with gr.Row():
|
| 1134 |
+
# Left Panel - Input
|
| 1135 |
+
with gr.Column(scale=1):
|
| 1136 |
+
gr.Markdown("## 📝 Input Method")
|
| 1137 |
+
|
| 1138 |
+
input_mode = gr.Radio(
|
| 1139 |
+
["Text", "Image", "URL"],
|
| 1140 |
+
value="Text",
|
| 1141 |
+
label="Select Input Type"
|
| 1142 |
+
)
|
| 1143 |
+
|
| 1144 |
+
with gr.Group():
|
| 1145 |
+
text_input = gr.Textbox(
|
| 1146 |
+
label="Describe the Math Problem",
|
| 1147 |
+
placeholder="e.g., Prove that the sum of angles in a triangle is 180 degrees...",
|
| 1148 |
+
lines=5,
|
| 1149 |
+
visible=True
|
| 1150 |
+
)
|
| 1151 |
+
|
| 1152 |
+
image_input = gr.Image(
|
| 1153 |
+
label="Upload Problem Image",
|
| 1154 |
+
type="pil",
|
| 1155 |
+
visible=False
|
| 1156 |
+
)
|
| 1157 |
+
|
| 1158 |
+
url_input = gr.Textbox(
|
| 1159 |
+
label="Enter Resource URL",
|
| 1160 |
+
placeholder="https://example.com/math-problem",
|
| 1161 |
+
visible=False
|
| 1162 |
+
)
|
| 1163 |
+
|
| 1164 |
+
# Toggle visibility based on mode
|
| 1165 |
+
def update_inputs(mode):
|
| 1166 |
+
return {
|
| 1167 |
+
text_input: gr.update(visible=mode == "Text"),
|
| 1168 |
+
image_input: gr.update(visible=mode == "Image"),
|
| 1169 |
+
url_input: gr.update(visible=mode == "URL")
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
input_mode.change(
|
| 1173 |
+
update_inputs,
|
| 1174 |
+
input_mode,
|
| 1175 |
+
[text_input, image_input, url_input],
|
| 1176 |
+
api_visibility="private"
|
| 1177 |
+
)
|
| 1178 |
+
|
| 1179 |
+
generate_btn = gr.Button("🚀 Generate Guided Proof", variant="primary", size="lg")
|
| 1180 |
+
|
| 1181 |
+
gr.Markdown("---")
|
| 1182 |
+
gr.Markdown("## ⚙️ Configuration")
|
| 1183 |
+
api_key_input = gr.Textbox(
|
| 1184 |
+
label="Gemini API Key (Optional)",
|
| 1185 |
+
placeholder="Leave empty to use environment variable",
|
| 1186 |
+
type="password"
|
| 1187 |
+
)
|
| 1188 |
+
|
| 1189 |
+
gr.Markdown("---")
|
| 1190 |
+
gr.Markdown("## 📚 Examples")
|
| 1191 |
+
example_selector = gr.Dropdown(
|
| 1192 |
+
choices=["Probability of an Odd Sum", "Pythagorean Theorem", "Orthodiagonal Quads"],
|
| 1193 |
+
value=default_example, # Set default selected example
|
| 1194 |
+
label="Load Example",
|
| 1195 |
+
interactive=True
|
| 1196 |
+
)
|
| 1197 |
+
load_example_btn = gr.Button("Load Example")
|
| 1198 |
+
|
| 1199 |
+
# gr.Markdown("---")
|
| 1200 |
+
# gr.Markdown("## 💾 Library")
|
| 1201 |
+
# library_selector = gr.Dropdown(
|
| 1202 |
+
# choices=ProofLibrary.list_proofs(),
|
| 1203 |
+
# label="Saved Proofs",
|
| 1204 |
+
# interactive=True
|
| 1205 |
+
# )
|
| 1206 |
+
# refresh_library_btn = gr.Button("🔄 Refresh")
|
| 1207 |
+
# load_library_btn = gr.Button("Load from Library")
|
| 1208 |
+
|
| 1209 |
+
# Right Panel - Output
|
| 1210 |
+
with gr.Column(scale=2):
|
| 1211 |
+
status_display = gr.Markdown(default_status, elem_classes="status-box")
|
| 1212 |
+
|
| 1213 |
+
with gr.Tabs():
|
| 1214 |
+
with gr.Tab("🎬 Guided Proof"):
|
| 1215 |
+
html_output = gr.HTML(value=default_html, label="Interactive Simulation")
|
| 1216 |
+
|
| 1217 |
+
with gr.Group():
|
| 1218 |
+
gr.Markdown("### 💬 Refinement Feedback")
|
| 1219 |
+
feedback_input = gr.Textbox(
|
| 1220 |
+
placeholder="e.g., 'Make the triangle red' or 'Add a step to show area'",
|
| 1221 |
+
label="Feedback"
|
| 1222 |
+
)
|
| 1223 |
+
refine_btn = gr.Button("Apply Refinement")
|
| 1224 |
+
|
| 1225 |
+
with gr.Tab("📖 Concept Details"):
|
| 1226 |
+
concept_output = gr.Markdown(value=default_concept)
|
| 1227 |
+
|
| 1228 |
+
with gr.Tab("💻 Source Code"):
|
| 1229 |
+
code_output = gr.Code(value=default_code, language="html", label="Generated HTML/JS")
|
| 1230 |
+
|
| 1231 |
+
with gr.Tab("📊 Process Logs"):
|
| 1232 |
+
logs_output = gr.Textbox(value=default_logs, label="Execution Logs", lines=20)
|
| 1233 |
+
|
| 1234 |
+
# with gr.Row():
|
| 1235 |
+
# save_btn = gr.Button("💾 Save to Library")
|
| 1236 |
+
# export_btn = gr.Button("📥 Export")
|
| 1237 |
+
# export_file = gr.File(label="Download", visible=False)
|
| 1238 |
+
|
| 1239 |
+
# Hidden components for MCP tool exposure (Two-step process)
|
| 1240 |
+
with gr.Group(visible=False):
|
| 1241 |
+
# Step 1: Analyze concept from text
|
| 1242 |
+
mcp_analyze_text_input = gr.Textbox()
|
| 1243 |
+
mcp_analyze_text_api_key = gr.Textbox()
|
| 1244 |
+
mcp_analyze_text_btn = gr.Button("MCP Analyze Concept from Text")
|
| 1245 |
+
mcp_analyze_text_output = gr.Textbox()
|
| 1246 |
+
|
| 1247 |
+
# Step 1: Analyze concept from URL
|
| 1248 |
+
mcp_analyze_url_input = gr.Textbox()
|
| 1249 |
+
mcp_analyze_url_api_key = gr.Textbox()
|
| 1250 |
+
mcp_analyze_url_btn = gr.Button("MCP Analyze Concept from URL")
|
| 1251 |
+
mcp_analyze_url_output = gr.Textbox()
|
| 1252 |
+
|
| 1253 |
+
# Step 1: Analyze concept from image
|
| 1254 |
+
mcp_analyze_image_input = gr.Image(type="pil")
|
| 1255 |
+
mcp_analyze_image_api_key = gr.Textbox()
|
| 1256 |
+
mcp_analyze_image_btn = gr.Button("MCP Analyze Concept from Image")
|
| 1257 |
+
mcp_analyze_image_output = gr.Textbox()
|
| 1258 |
+
|
| 1259 |
+
# Step 2: Generate code from concept
|
| 1260 |
+
mcp_generate_code_concept_json = gr.Textbox()
|
| 1261 |
+
mcp_generate_code_api_key = gr.Textbox()
|
| 1262 |
+
mcp_generate_code_btn = gr.Button("MCP Generate Code from Concept")
|
| 1263 |
+
mcp_generate_code_output = gr.Textbox()
|
| 1264 |
+
|
| 1265 |
+
# Event handlers
|
| 1266 |
+
generate_btn.click(
|
| 1267 |
+
fn=app.generate_proof,
|
| 1268 |
+
inputs=[text_input, url_input, image_input, input_mode, api_key_input],
|
| 1269 |
+
outputs=[html_output, concept_output, status_display, logs_output, code_output],
|
| 1270 |
+
api_visibility="private" # UI-only, not exposed to MCP
|
| 1271 |
+
)
|
| 1272 |
+
|
| 1273 |
+
refine_btn.click(
|
| 1274 |
+
fn=app.refine_proof,
|
| 1275 |
+
inputs=[feedback_input, api_key_input],
|
| 1276 |
+
outputs=[html_output, status_display, logs_output, code_output],
|
| 1277 |
+
api_visibility="private" # UI-only, not exposed to MCP
|
| 1278 |
+
)
|
| 1279 |
+
|
| 1280 |
+
# save_btn.click(
|
| 1281 |
+
# fn=app.save_to_library,
|
| 1282 |
+
# inputs=[text_input, url_input, image_input, input_mode],
|
| 1283 |
+
# outputs=[status_display],
|
| 1284 |
+
# api_visibility="private" # UI-only, not exposed to MCP
|
| 1285 |
+
# )
|
| 1286 |
+
|
| 1287 |
+
# export_btn.click(
|
| 1288 |
+
# fn=app.export_proof_file,
|
| 1289 |
+
# inputs=[text_input, url_input, image_input, input_mode],
|
| 1290 |
+
# outputs=[export_file, status_display],
|
| 1291 |
+
# api_visibility="private" # UI-only, not exposed to MCP
|
| 1292 |
+
# )
|
| 1293 |
+
|
| 1294 |
+
# load_library_btn.click(
|
| 1295 |
+
# fn=app.load_from_library,
|
| 1296 |
+
# inputs=[library_selector],
|
| 1297 |
+
# outputs=[html_output, concept_output, status_display, logs_output, code_output],
|
| 1298 |
+
# api_visibility="private" # UI-only, not exposed to MCP
|
| 1299 |
+
# )
|
| 1300 |
+
|
| 1301 |
+
# refresh_library_btn.click(
|
| 1302 |
+
# fn=lambda: gr.update(choices=ProofLibrary.list_proofs()),
|
| 1303 |
+
# outputs=[library_selector],
|
| 1304 |
+
# api_visibility="private" # UI-only, not exposed to MCP
|
| 1305 |
+
# )
|
| 1306 |
+
|
| 1307 |
+
load_example_btn.click(
|
| 1308 |
+
fn=app.load_example,
|
| 1309 |
+
inputs=[example_selector],
|
| 1310 |
+
outputs=[html_output, concept_output, status_display, logs_output, code_output],
|
| 1311 |
+
api_visibility="private" # UI-only, not exposed to MCP
|
| 1312 |
+
)
|
| 1313 |
+
|
| 1314 |
+
# MCP tool event handlers (Two-step process)
|
| 1315 |
+
mcp_analyze_text_btn.click(
|
| 1316 |
+
fn=app.create_math_specification_from_text,
|
| 1317 |
+
inputs=[mcp_analyze_text_input, mcp_analyze_text_api_key],
|
| 1318 |
+
outputs=[mcp_analyze_text_output]
|
| 1319 |
+
)
|
| 1320 |
+
|
| 1321 |
+
mcp_analyze_url_btn.click(
|
| 1322 |
+
fn=app.create_math_specification_from_url,
|
| 1323 |
+
inputs=[mcp_analyze_url_input, mcp_analyze_url_api_key],
|
| 1324 |
+
outputs=[mcp_analyze_url_output]
|
| 1325 |
+
)
|
| 1326 |
+
|
| 1327 |
+
mcp_analyze_image_btn.click(
|
| 1328 |
+
fn=app.create_math_specification_from_image,
|
| 1329 |
+
inputs=[mcp_analyze_image_input, mcp_analyze_image_api_key],
|
| 1330 |
+
outputs=[mcp_analyze_image_output]
|
| 1331 |
+
)
|
| 1332 |
+
|
| 1333 |
+
mcp_generate_code_btn.click(
|
| 1334 |
+
fn=app.build_interactive_proof_from_specification,
|
| 1335 |
+
inputs=[mcp_generate_code_concept_json, mcp_generate_code_api_key],
|
| 1336 |
+
outputs=[mcp_generate_code_output]
|
| 1337 |
+
)
|
| 1338 |
+
|
| 1339 |
+
# Register MCP prompts and resources as API endpoints so they appear in the server schema
|
| 1340 |
+
gr.api(
|
| 1341 |
+
create_visual_math_proof,
|
| 1342 |
+
api_name="create_visual_math_proof_prompt",
|
| 1343 |
+
api_description=create_visual_math_proof.__doc__
|
| 1344 |
+
)
|
| 1345 |
+
gr.api(
|
| 1346 |
+
create_math_specification,
|
| 1347 |
+
api_name="create_math_specification_prompt",
|
| 1348 |
+
api_description=create_math_specification.__doc__
|
| 1349 |
+
)
|
| 1350 |
+
gr.api(
|
| 1351 |
+
build_from_specification,
|
| 1352 |
+
api_name="build_from_specification_prompt",
|
| 1353 |
+
api_description=build_from_specification.__doc__
|
| 1354 |
+
)
|
| 1355 |
+
gr.api(
|
| 1356 |
+
get_specification_template,
|
| 1357 |
+
api_name="specification_template_resource",
|
| 1358 |
+
api_description=get_specification_template.__doc__
|
| 1359 |
+
)
|
| 1360 |
+
gr.api(
|
| 1361 |
+
get_pythagorean_example,
|
| 1362 |
+
api_name="example_pythagorean_resource",
|
| 1363 |
+
api_description=get_pythagorean_example.__doc__
|
| 1364 |
+
)
|
| 1365 |
+
gr.api(
|
| 1366 |
+
get_probability_example,
|
| 1367 |
+
api_name="example_probability_resource",
|
| 1368 |
+
api_description=get_probability_example.__doc__
|
| 1369 |
+
)
|
| 1370 |
+
gr.api(
|
| 1371 |
+
get_workflow_guide,
|
| 1372 |
+
api_name="workflow_guide_resource",
|
| 1373 |
+
api_description=get_workflow_guide.__doc__
|
| 1374 |
+
)
|
| 1375 |
+
|
| 1376 |
+
return demo
|
| 1377 |
+
|
| 1378 |
+
# ==================== Main Entry Point ====================
|
| 1379 |
+
|
| 1380 |
+
if __name__ == "__main__":
|
| 1381 |
+
try:
|
| 1382 |
+
|
| 1383 |
+
# Create Gradio interface
|
| 1384 |
+
demo = create_gradio_app()
|
| 1385 |
+
|
| 1386 |
+
# Theme configuration (Gradio 6 style)
|
| 1387 |
+
theme = gr.themes.Base(
|
| 1388 |
+
primary_hue="indigo",
|
| 1389 |
+
secondary_hue="purple",
|
| 1390 |
+
neutral_hue="slate",
|
| 1391 |
+
font=gr.themes.GoogleFont("Inter"),
|
| 1392 |
+
).set(
|
| 1393 |
+
body_background_fill="*neutral_50",
|
| 1394 |
+
body_background_fill_dark="*neutral_900",
|
| 1395 |
+
button_primary_background_fill="*primary_500",
|
| 1396 |
+
button_primary_background_fill_hover="*primary_600",
|
| 1397 |
+
button_primary_text_color="white",
|
| 1398 |
+
)
|
| 1399 |
+
|
| 1400 |
+
# Launch with MCP server enabled
|
| 1401 |
+
demo.launch(
|
| 1402 |
+
server_name="0.0.0.0",
|
| 1403 |
+
server_port=7860,
|
| 1404 |
+
mcp_server=True,
|
| 1405 |
+
theme=theme,
|
| 1406 |
+
debug=True,
|
| 1407 |
+
show_error=True,
|
| 1408 |
+
quiet=False
|
| 1409 |
+
)
|
| 1410 |
+
|
| 1411 |
+
except Exception as e:
|
| 1412 |
+
logger.error(f"Failed to start server: {e}")
|
| 1413 |
+
logger.error("Check that:")
|
| 1414 |
+
logger.error(" 1. GEMINI_API_KEY environment variable is set")
|
| 1415 |
+
logger.error(" 2. Port 7860 is available")
|
| 1416 |
+
logger.error(" 3. All dependencies are installed")
|
| 1417 |
+
raise
|
examples/001-visual-proof-probability-of-an-odd-sum.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-25T00:42:01.682Z",
|
| 4 |
+
"input": {
|
| 5 |
+
"mode": "url",
|
| 6 |
+
"url": "https://cemc.uwaterloo.ca/sites/default/files/documents/2025/POTWC-25-D-N-10-P.html"
|
| 7 |
+
},
|
| 8 |
+
"concept": {
|
| 9 |
+
"conceptTitle": "Probability of an Odd Sum",
|
| 10 |
+
"educationalGoal": "Students will learn to calculate probabilities involving combinations and the parity of numbers by systematically identifying total and favorable outcomes.",
|
| 11 |
+
"explanation": "We are given five cards, numbered 1 through 5. If we randomly flip over three cards, what is the probability that the sum of the numbers on those three cards is odd? To solve this, we'll first categorize the cards by whether they are odd or even. Then, we'll explore the rules of addition for odd and even numbers to determine which combinations of three cards will result in an odd sum. Finally, we'll count all possible three-card combinations and the combinations that give an odd sum to calculate the probability.",
|
| 12 |
+
"steps": [
|
| 13 |
+
{
|
| 14 |
+
"stepTitle": "Categorize Cards by Parity",
|
| 15 |
+
"instruction": "Observe the five cards (1-5) and how they are separated into odd and even groups. Click 'Next' to continue.",
|
| 16 |
+
"visualFocus": "The initial five cards, with clear visual distinction/grouping for odd (1, 3, 5) and even (2, 4) numbers."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stepTitle": "Understand Parity Sum Rules",
|
| 20 |
+
"instruction": "Drag and drop 'Odd' and 'Even' markers into the three slots to see how their sum's parity changes. Find the combinations that result in an 'Odd Sum'.",
|
| 21 |
+
"visualFocus": "Interactive display showing three input slots for 'Odd'/'Even' markers and a dynamic output for the 'Sum Parity'. Highlighting the resulting 'Odd Sum' outcome."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"stepTitle": "Identify Favorable Combinations",
|
| 25 |
+
"instruction": "Based on the parity rules you discovered, identify which specific combinations of three cards from our original set (1, 2, 3, 4, 5) will result in an odd sum. Click 'Next' to reveal them.",
|
| 26 |
+
"visualFocus": "All possible unique combinations of three cards from (1,2,3,4,5) are displayed. The combinations that result in an odd sum (e.g., O+O+O, O+E+E) will be highlighted."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"stepTitle": "Count Outcomes",
|
| 30 |
+
"instruction": "Count the total number of ways to choose 3 cards from 5 (Total Outcomes) and the number of ways that result in an odd sum (Favorable Outcomes).",
|
| 31 |
+
"visualFocus": "A visual display summarizing: 'Total Possible Combinations = C(5,3)' and 'Favorable Combinations (Odd Sum) = Counted from previous step'. Show the calculated numbers."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"stepTitle": "Calculate Probability",
|
| 35 |
+
"instruction": "Drag the 'Favorable Outcomes' and 'Total Outcomes' into the probability formula. See the final probability.",
|
| 36 |
+
"visualFocus": "A visual representation of the probability formula: 'Probability = Favorable Outcomes / Total Outcomes', with input fields for the numbers and a display for the calculated fractional and simplified probability."
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"visualSpec": {
|
| 40 |
+
"elements": [
|
| 41 |
+
"cards_1_to_5: Rectangular card objects labeled 1, 2, 3, 4, 5.",
|
| 42 |
+
"parity_groups: Visually distinct containers/colors for odd (1, 3, 5) and even (2, 4) cards.",
|
| 43 |
+
"parity_markers: Draggable 'Odd' and 'Even' labels/icons.",
|
| 44 |
+
"sum_calculator_slots: Three empty rectangular slots to receive parity_markers.",
|
| 45 |
+
"sum_parity_display: Text area showing 'Odd Sum' or 'Even Sum' dynamically.",
|
| 46 |
+
"all_combinations_grid: A grid or list displaying all C(5,3) unique combinations of 3 cards.",
|
| 47 |
+
"favorable_highlight: A visual overlay or border to highlight combinations with an odd sum in the grid.",
|
| 48 |
+
"outcome_counters: Text labels displaying 'Total Outcomes: X' and 'Favorable Outcomes: Y'.",
|
| 49 |
+
"probability_formula: A visual fraction bar with input fields for numerator and denominator, and a result field."
|
| 50 |
+
],
|
| 51 |
+
"interactions": [
|
| 52 |
+
"next_button_click: User clicks a 'Next' button to advance through steps.",
|
| 53 |
+
"drag_parity_to_slot: User drags 'Odd'/'Even' markers into the sum_calculator_slots.",
|
| 54 |
+
"observe_sum_parity_update: System updates sum_parity_display based on user drag-and-drop.",
|
| 55 |
+
"click_to_reveal_favorable: User clicks a button to reveal highlighted favorable combinations.",
|
| 56 |
+
"drag_counts_to_formula: User drags numerical values from outcome_counters to probability_formula input fields."
|
| 57 |
+
],
|
| 58 |
+
"mathLogic": "1. Identify odd/even numbers: Odds = {1,3,5}, Evens = {2,4}. 2. Parity rules for sum of three numbers: O+O+O = O; O+E+E = O; O+O+E = E; E+E+E = E (not possible here). 3. Total combinations: C(n, k) = n! / (k!(n-k)!), specifically C(5,3) = 10. 4. Favorable combinations (odd sum): C(3 odd, 3 chosen) + C(3 odd, 1 chosen) * C(2 even, 2 chosen) = C(3,3) + C(3,1)*C(2,2) = 1 + 3*1 = 4. 5. Probability = Favorable Outcomes / Total Outcomes = 4/10 = 2/5."
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"sourceCode": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Probability of an Odd Sum</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-color: #e2e8f0;\n --accent-blue: #38bdf8;\n --accent-red: #fb7185;\n --accent-green: #4ade80;\n --accent-yellow: #facc15;\n --border-color: #334155;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n }\n\n body {\n background-color: var(--bg-color);\n color: var(--text-color);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* Layout */\n header {\n padding: 1rem 2rem;\n border-bottom: 1px solid var(--border-color);\n display: flex;\n justify-content: space-between;\n align-items: center;\n background-color: var(--panel-bg);\n }\n\n h1 { font-size: 1.5rem; color: var(--accent-blue); }\n .step-indicator { font-size: 0.9rem; color: #94a3b8; }\n\n main {\n flex: 1;\n display: grid;\n grid-template-columns: 300px 1fr;\n gap: 0;\n overflow: hidden;\n }\n\n /* Sidebar for Instructions */\n .sidebar {\n background-color: #172033;\n border-right: 1px solid var(--border-color);\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n overflow-y: auto;\n }\n\n .sidebar h2 { font-size: 1.2rem; margin-bottom: 0.5rem; color: var(--accent-yellow); }\n .sidebar p { line-height: 1.6; font-size: 0.95rem; color: #cbd5e1; }\n .instruction-highlight { background: #334155; padding: 10px; border-radius: 6px; margin-top: 10px; border-left: 4px solid var(--accent-blue); }\n\n /* Visualization Area */\n .viz-container {\n background-color: var(--bg-color);\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n overflow-y: auto;\n }\n\n /* Footer Controls */\n footer {\n padding: 1rem 2rem;\n border-top: 1px solid var(--border-color);\n background-color: var(--panel-bg);\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n button {\n background-color: var(--accent-blue);\n color: #0f172a;\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 6px;\n font-weight: bold;\n cursor: pointer;\n transition: transform 0.1s, opacity 0.2s;\n }\n\n button:hover { opacity: 0.9; }\n button:active { transform: scale(0.98); }\n button:disabled { background-color: #475569; color: #94a3b8; cursor: not-allowed; }\n\n /* Shared Components */\n .card {\n width: 60px;\n height: 80px;\n background: white;\n color: #0f172a;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n font-weight: bold;\n box-shadow: 0 4px 6px rgba(0,0,0,0.3);\n user-select: none;\n transition: all 0.3s ease;\n }\n \n .card.odd { border-bottom: 5px solid var(--accent-blue); }\n .card.even { border-bottom: 5px solid var(--accent-red); }\n\n .area-box {\n border: 2px dashed var(--border-color);\n border-radius: 12px;\n padding: 1rem;\n margin: 1rem;\n background: rgba(255,255,255,0.02);\n }\n\n /* Step 1 Styles */\n .sorting-area { display: flex; gap: 4rem; width: 100%; justify-content: center; margin-top: 2rem; }\n .group-container { display: flex; flex-direction: column; align-items: center; gap: 1rem; min-width: 150px; }\n .group-box { display: flex; gap: 10px; padding: 20px; background: rgba(56, 189, 248, 0.1); border-radius: 12px; min-height: 100px; border: 1px solid var(--border-color); }\n .group-box.even-box { background: rgba(251, 113, 133, 0.1); }\n\n /* Step 2 Styles */\n .drag-source { display: flex; gap: 20px; margin-bottom: 2rem; }\n .token {\n padding: 10px 20px;\n border-radius: 20px;\n font-weight: bold;\n cursor: grab;\n color: #0f172a;\n user-select: none;\n }\n .token.odd { background-color: var(--accent-blue); }\n .token.even { background-color: var(--accent-red); }\n \n .slots-container { display: flex; gap: 10px; align-items: center; }\n .plus-sign { font-size: 2rem; color: #64748b; }\n .slot {\n width: 100px;\n height: 60px;\n border: 2px dashed #64748b;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0,0,0,0.2);\n color: #64748b;\n }\n .slot.filled { border-style: solid; color: #0f172a; }\n .result-display { \n margin-top: 2rem; \n font-size: 1.5rem; \n padding: 1rem 2rem; \n background: #334155; \n border-radius: 8px; \n opacity: 0;\n transition: opacity 0.5s;\n }\n .result-display.visible { opacity: 1; }\n\n /* Step 3 Styles */\n .grid-container {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 15px;\n width: 100%;\n max-width: 800px;\n }\n .combo-item {\n background: var(--panel-bg);\n border: 1px solid var(--border-color);\n padding: 10px;\n border-radius: 8px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n transition: all 0.3s;\n opacity: 0.6;\n }\n .combo-item.highlight {\n border-color: var(--accent-green);\n box-shadow: 0 0 15px rgba(74, 222, 128, 0.2);\n opacity: 1;\n transform: scale(1.05);\n }\n .mini-cards { display: flex; gap: 5px; }\n .mini-card {\n width: 25px;\n height: 35px;\n background: #fff;\n color: #000;\n display: flex;\n justify-content: center;\n align-items: center;\n font-weight: bold;\n border-radius: 3px;\n font-size: 0.8rem;\n }\n .combo-sum { font-size: 0.8rem; color: #94a3b8; }\n .highlight .combo-sum { color: var(--accent-green); font-weight: bold; }\n\n /* Step 4 Styles */\n .count-container { display: flex; gap: 3rem; align-items: center; margin-top: 2rem; }\n .count-box {\n background: var(--panel-bg);\n padding: 2rem;\n border-radius: 12px;\n text-align: center;\n border: 1px solid var(--border-color);\n position: relative;\n }\n .count-number { font-size: 3rem; font-weight: bold; margin: 10px 0; }\n .count-label { color: #94a3b8; font-size: 0.9rem; }\n .count-draggable { \n cursor: grab; \n background: var(--accent-yellow); \n color: black; \n width: 40px; \n height: 40px; \n border-radius: 50%; \n display: flex; \n align-items: center; \n justify-content: center;\n font-weight: bold;\n position: absolute;\n top: -10px;\n right: -10px;\n box-shadow: 0 2px 5px rgba(0,0,0,0.5);\n }\n\n /* Step 5 Styles */\n .formula-area {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2rem;\n font-size: 1.5rem;\n }\n .fraction-builder {\n display: flex;\n align-items: center;\n gap: 20px;\n }\n .fraction {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n }\n .fraction-line { width: 100px; height: 4px; background: white; }\n .drop-zone {\n width: 60px;\n height: 60px;\n border: 2px dashed #64748b;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.2rem;\n background: rgba(255,255,255,0.05);\n transition: all 0.2s;\n }\n .drop-zone.hover { border-color: var(--accent-green); background: rgba(74, 222, 128, 0.1); }\n .final-result { font-size: 2rem; color: var(--accent-green); font-weight: bold; opacity: 0; }\n\n @media (max-width: 768px) {\n main { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }\n .sidebar { padding: 1rem; max-height: 200px; }\n .viz-container { padding: 1rem; }\n .card { width: 40px; height: 56px; font-size: 1rem; }\n .sorting-area { gap: 1rem; flex-direction: column; align-items: center; }\n }\n </style>\n</head>\n<body>\n\n <header>\n <h1>Probability: Odd Sum</h1>\n <div class=\"step-indicator\">Step <span id=\"step-num\">1</span> of 5</div>\n </header>\n\n <main>\n <div class=\"sidebar\">\n <h2 id=\"step-title\">Loading...</h2>\n <p id=\"step-desc\">Loading instructions...</p>\n <div id=\"step-instruction-extra\" class=\"instruction-highlight\"></div>\n </div>\n <div class=\"viz-container\" id=\"viz-root\">\n <!-- Content injected by JS -->\n </div>\n </main>\n\n <footer>\n <button id=\"prev-btn\" disabled>Previous</button>\n <button id=\"next-btn\">Next</button>\n </footer>\n\n <script>\n // --- Application State ---\n const state = {\n step: 0,\n totalSteps: 5,\n cards: [1, 2, 3, 4, 5],\n paritySlotValues: [null, null, null], // For Step 2\n probabilityInputs: { num: null, den: null } // For Step 5\n };\n\n // --- Data ---\n const stepsData = [\n {\n title: \"1. Categorize Cards by Parity\",\n desc: \"We have 5 cards numbered 1 through 5. To analyze the sum, we first need to separate them into Odd and Even numbers.\",\n extra: \"Observe how the numbers are grouped. Odds are 1, 3, 5. Evens are 2, 4.\",\n render: renderStep1\n },\n {\n title: \"2. Understand Parity Sum Rules\",\n desc: \"How do we get an ODD sum from three numbers? Let's experiment. Drag 'Odd' and 'Even' markers into the slots.\",\n extra: \"Hint: Adding two Odd numbers equals an Even number (1+3=4). Adding an Even to an Odd keeps it Odd.\",\n render: renderStep2\n },\n {\n title: \"3. Identify Favorable Combinations\",\n desc: \"We are choosing 3 cards. Here are all possible combinations (Total = 10).\",\n extra: \"The combinations resulting in an Odd Sum are highlighted. These are our Favorable Outcomes.\",\n render: renderStep3\n },\n {\n title: \"4. Count Outcomes\",\n desc: \"Let's formalize the count. We found the Total Outcomes using Combinations C(5,3) and counted the Favorable ones.\",\n extra: \"Favorable = 4, Total = 10.\",\n render: renderStep4\n },\n {\n title: \"5. Calculate Probability\",\n desc: \"Probability is the ratio of Favorable Outcomes to Total Outcomes.\",\n extra: \"Drag the 'Favorable' count to the top and 'Total' count to the bottom.\",\n render: renderStep5\n }\n ];\n\n // --- DOM Elements ---\n const dom = {\n stepNum: document.getElementById('step-num'),\n title: document.getElementById('step-title'),\n desc: document.getElementById('step-desc'),\n extra: document.getElementById('step-instruction-extra'),\n viz: document.getElementById('viz-root'),\n prevBtn: document.getElementById('prev-btn'),\n nextBtn: document.getElementById('next-btn')\n };\n\n // --- Initialization ---\n function init() {\n updateUI();\n dom.nextBtn.addEventListener('click', () => changeStep(1));\n dom.prevBtn.addEventListener('click', () => changeStep(-1));\n }\n\n function changeStep(delta) {\n const newStep = state.step + delta;\n if (newStep >= 0 && newStep < state.totalSteps) {\n state.step = newStep;\n updateUI();\n }\n }\n\n function updateUI() {\n const current = stepsData[state.step];\n \n // Update Sidebar\n dom.stepNum.textContent = state.step + 1;\n dom.title.textContent = current.title;\n dom.desc.textContent = current.desc;\n dom.extra.textContent = current.extra;\n\n // Update Buttons\n dom.prevBtn.disabled = state.step === 0;\n dom.nextBtn.disabled = state.step === state.totalSteps - 1;\n if(state.step === 1 || state.step === 4) {\n // In interactive steps, we might optionally disable next until complete, \n // but for exploration flow, we keep it open or just style it.\n // keeping enabled for smooth flow as requested.\n }\n\n // Render Visualization\n dom.viz.innerHTML = ''; // Clear previous\n current.render(dom.viz);\n }\n\n // --- Step 1: Categorize ---\n function renderStep1(container) {\n const wrapper = document.createElement('div');\n wrapper.style.width = \"100%\";\n \n // Initial Lineup\n const initialTitle = document.createElement('h3');\n initialTitle.textContent = \"Original Set\";\n initialTitle.style.textAlign = 'center';\n initialTitle.style.marginBottom = '1rem';\n wrapper.appendChild(initialTitle);\n\n const initialSet = document.createElement('div');\n initialSet.style.display = 'flex';\n initialSet.style.justifyContent = 'center';\n initialSet.style.gap = '10px';\n initialSet.style.marginBottom = '2rem';\n\n state.cards.forEach(num => {\n const card = createCard(num);\n initialSet.appendChild(card);\n });\n wrapper.appendChild(initialSet);\n\n // Split Groups\n const sortingArea = document.createElement('div');\n sortingArea.className = 'sorting-area';\n\n // Odd Box\n const oddGroup = document.createElement('div');\n oddGroup.className = 'group-container';\n oddGroup.innerHTML = '<div style=\"color: var(--accent-blue); font-weight:bold;\">ODD (1, 3, 5)</div>';\n const oddBox = document.createElement('div');\n oddBox.className = 'group-box';\n oddBox.id = 'odd-box';\n oddGroup.appendChild(oddBox);\n\n // Even Box\n const evenGroup = document.createElement('div');\n evenGroup.className = 'group-container';\n evenGroup.innerHTML = '<div style=\"color: var(--accent-red); font-weight:bold;\">EVEN (2, 4)</div>';\n const evenBox = document.createElement('div');\n evenBox.className = 'group-box even-box';\n evenBox.id = 'even-box';\n evenGroup.appendChild(evenBox);\n\n sortingArea.appendChild(oddGroup);\n sortingArea.appendChild(evenGroup);\n wrapper.appendChild(sortingArea);\n container.appendChild(wrapper);\n\n // Animation Logic\n setTimeout(() => {\n state.cards.forEach(num => {\n const card = createCard(num);\n card.style.opacity = '0';\n card.style.transform = 'translateY(-20px)';\n if (num % 2 !== 0) {\n card.classList.add('odd');\n document.getElementById('odd-box').appendChild(card);\n } else {\n card.classList.add('even');\n document.getElementById('even-box').appendChild(card);\n }\n // Staggered fade in\n setTimeout(() => {\n card.style.opacity = '1';\n card.style.transform = 'translateY(0)';\n }, num * 200);\n });\n }, 500);\n }\n\n // --- Step 2: Parity Rules ---\n function renderStep2(container) {\n state.paritySlotValues = [null, null, null];\n\n const wrapper = document.createElement('div');\n wrapper.style.display = 'flex';\n wrapper.style.flexDirection = 'column';\n wrapper.style.alignItems = 'center';\n\n // Source Tokens\n const source = document.createElement('div');\n source.className = 'drag-source';\n \n const oddToken = createDraggableToken('Odd', 'odd');\n const evenToken = createDraggableToken('Even', 'even');\n source.appendChild(oddToken);\n source.appendChild(evenToken);\n wrapper.appendChild(source);\n\n const hint = document.createElement('div');\n hint.textContent = \"Drag tokens to slots below:\";\n hint.style.marginBottom = '1rem';\n wrapper.appendChild(hint);\n\n // Slots\n const slotsContainer = document.createElement('div');\n slotsContainer.className = 'slots-container';\n\n for (let i = 0; i < 3; i++) {\n const slot = document.createElement('div');\n slot.className = 'slot';\n slot.dataset.index = i;\n slot.textContent = \"Drop\";\n \n // Drop Events\n slot.addEventListener('dragover', e => e.preventDefault());\n slot.addEventListener('drop', handleDropStep2);\n \n slotsContainer.appendChild(slot);\n \n if (i < 2) {\n const plus = document.createElement('div');\n plus.className = 'plus-sign';\n plus.textContent = '+';\n slotsContainer.appendChild(plus);\n }\n }\n wrapper.appendChild(slotsContainer);\n\n // Result\n const resultDisplay = document.createElement('div');\n resultDisplay.id = 'parity-result';\n resultDisplay.className = 'result-display';\n resultDisplay.textContent = \"Sum Parity: ?\";\n wrapper.appendChild(resultDisplay);\n\n container.appendChild(wrapper);\n }\n\n function createDraggableToken(text, type) {\n const el = document.createElement('div');\n el.className = `token ${type}`;\n el.textContent = text;\n el.draggable = true;\n el.addEventListener('dragstart', (e) => {\n e.dataTransfer.setData('text/plain', type);\n e.dataTransfer.effectAllowed = 'copy';\n });\n return el;\n }\n\n function handleDropStep2(e) {\n e.preventDefault();\n const type = e.dataTransfer.getData('text/plain');\n const index = e.target.dataset.index;\n \n if (!type || index === undefined) return;\n\n // Update visual\n e.target.className = `slot filled`;\n e.target.textContent = type === 'odd' ? 'ODD' : 'EVEN';\n e.target.style.backgroundColor = type === 'odd' ? 'var(--accent-blue)' : 'var(--accent-red)';\n e.target.style.borderColor = 'transparent';\n\n // Update state\n state.paritySlotValues[index] = type;\n\n // Check logic\n if (state.paritySlotValues.every(v => v !== null)) {\n checkParityLogic();\n }\n }\n\n function checkParityLogic() {\n const oddCount = state.paritySlotValues.filter(v => v === 'odd').length;\n const resultEl = document.getElementById('parity-result');\n const isOddSum = oddCount % 2 !== 0;\n\n resultEl.textContent = isOddSum ? \"Result: ODD Sum\" : \"Result: EVEN Sum\";\n resultEl.style.border = isOddSum ? \"2px solid var(--accent-green)\" : \"2px solid var(--accent-red)\";\n resultEl.style.color = isOddSum ? \"var(--accent-green)\" : \"#e2e8f0\";\n resultEl.classList.add('visible');\n\n // Update text based on combination\n let explanation = \"\";\n if (oddCount === 3) explanation = \"(Odd + Odd) + Odd = Even + Odd = Odd\";\n else if (oddCount === 2) explanation = \"(Odd + Odd) + Even = Even + Even = Even\";\n else if (oddCount === 1) explanation = \"(Even + Even) + Odd = Even + Odd = Odd\";\n else explanation = \"Even + Even + Even = Even\";\n \n const subText = document.createElement('div');\n subText.style.fontSize = \"0.9rem\";\n subText.style.marginTop = \"5px\";\n subText.style.color = \"#94a3b8\";\n subText.textContent = explanation;\n \n // Clear previous subtext if any\n if(resultEl.children.length > 0) resultEl.removeChild(resultEl.lastChild);\n resultEl.appendChild(subText);\n }\n\n // --- Step 3: All Combinations ---\n function renderStep3(container) {\n // Generate combinations\n const arr = [1, 2, 3, 4, 5];\n const combinations = [];\n for (let i = 0; i < arr.length; i++) {\n for (let j = i + 1; j < arr.length; j++) {\n for (let k = j + 1; k < arr.length; k++) {\n combinations.push([arr[i], arr[j], arr[k]]);\n }\n }\n }\n\n const grid = document.createElement('div');\n grid.className = 'grid-container';\n\n combinations.forEach((combo) => {\n const sum = combo.reduce((a, b) => a + b, 0);\n const isOdd = sum % 2 !== 0;\n\n const item = document.createElement('div');\n item.className = 'combo-item';\n if (isOdd) item.classList.add('highlight');\n\n const miniCards = document.createElement('div');\n miniCards.className = 'mini-cards';\n combo.forEach(num => {\n const span = document.createElement('div');\n span.className = 'mini-card';\n span.textContent = num;\n span.style.borderBottom = (num % 2 !== 0) ? \"3px solid var(--accent-blue)\" : \"3px solid var(--accent-red)\";\n miniCards.appendChild(span);\n });\n\n const sumText = document.createElement('div');\n sumText.className = 'combo-sum';\n sumText.textContent = `Sum: ${sum} (${isOdd ? 'Odd' : 'Even'})`;\n\n item.appendChild(miniCards);\n item.appendChild(sumText);\n grid.appendChild(item);\n });\n\n container.appendChild(grid);\n }\n\n // --- Step 4: Count Outcomes ---\n function renderStep4(container) {\n const wrapper = document.createElement('div');\n wrapper.className = 'count-container';\n\n // Favorable\n const favorableBox = document.createElement('div');\n favorableBox.className = 'count-box';\n favorableBox.innerHTML = `\n <div class=\"count-label\">Favorable Outcomes</div>\n <div class=\"count-label\">(Odd Sums)</div>\n <div class=\"count-number\" style=\"color: var(--accent-green)\">4</div>\n <div class=\"count-draggable\" draggable=\"true\" id=\"drag-fav\">4</div>\n `;\n \n // Total\n const totalBox = document.createElement('div');\n totalBox.className = 'count-box';\n totalBox.innerHTML = `\n <div class=\"count-label\">Total Outcomes</div>\n <div class=\"count-label\">C(5,3)</div>\n <div class=\"count-number\" style=\"color: var(--accent-blue)\">10</div>\n <div class=\"count-draggable\" draggable=\"true\" id=\"drag-total\">10</div>\n `;\n\n // Instructions for next step preview\n const info = document.createElement('div');\n info.style.textAlign = 'center';\n info.innerHTML = `\n <p style=\"margin-bottom:10px\">We found:</p>\n <ul style=\"list-style:none; text-align:left; display:inline-block; color: #cbd5e1;\">\n <li>• 1 way to pick 3 Odds</li>\n <li>• 3 ways to pick 1 Odd & 2 Evens</li>\n <li style=\"margin-top:5px; font-weight:bold; color:white;\">= 4 Favorable Outcomes</li>\n </ul>\n `;\n\n wrapper.appendChild(favorableBox);\n wrapper.appendChild(info);\n wrapper.appendChild(totalBox);\n container.appendChild(wrapper);\n\n // We are preparing for drag in step 5, but visual is enough here.\n }\n\n // --- Step 5: Formula ---\n function renderStep5(container) {\n state.probabilityInputs = { num: null, den: null };\n\n const wrapper = document.createElement('div');\n wrapper.className = 'formula-area';\n\n // Draggables Area\n const sourceArea = document.createElement('div');\n sourceArea.style.display = 'flex';\n sourceArea.style.gap = '2rem';\n sourceArea.style.marginBottom = '2rem';\n sourceArea.style.padding = '1rem';\n sourceArea.style.background = 'rgba(255,255,255,0.05)';\n sourceArea.style.borderRadius = '10px';\n\n const dragFav = document.createElement('div');\n dragFav.className = 'count-draggable';\n dragFav.style.position = 'relative';\n dragFav.style.top = '0';\n dragFav.style.right = '0';\n dragFav.textContent = \"4\";\n dragFav.id = \"source-4\";\n dragFav.draggable = true;\n dragFav.addEventListener('dragstart', (e) => {\n e.dataTransfer.setData('text/plain', '4');\n e.dataTransfer.effectAllowed = 'copy';\n });\n\n const dragTotal = document.createElement('div');\n dragTotal.className = 'count-draggable';\n dragTotal.style.position = 'relative';\n dragTotal.style.top = '0';\n dragTotal.style.right = '0';\n dragTotal.style.backgroundColor = 'var(--accent-blue)';\n dragTotal.textContent = \"10\";\n dragTotal.id = \"source-10\";\n dragTotal.draggable = true;\n dragTotal.addEventListener('dragstart', (e) => {\n e.dataTransfer.setData('text/plain', '10');\n e.dataTransfer.effectAllowed = 'copy';\n });\n\n const labelFav = document.createElement('div');\n labelFav.innerHTML = \"Favorable <br> (4)\";\n labelFav.style.textAlign = 'center';\n labelFav.style.fontSize = '0.8rem';\n \n const labelTot = document.createElement('div');\n labelTot.innerHTML = \"Total <br> (10)\";\n labelTot.style.textAlign = 'center';\n labelTot.style.fontSize = '0.8rem';\n\n const grp1 = document.createElement('div');\n grp1.style.display='flex'; grp1.style.flexDirection='column'; grp1.style.alignItems='center'; grp1.style.gap='5px';\n grp1.appendChild(dragFav); grp1.appendChild(labelFav);\n\n const grp2 = document.createElement('div');\n grp2.style.display='flex'; grp2.style.flexDirection='column'; grp2.style.alignItems='center'; grp2.style.gap='5px';\n grp2.appendChild(dragTotal); grp2.appendChild(labelTot);\n\n sourceArea.appendChild(grp1);\n sourceArea.appendChild(grp2);\n wrapper.appendChild(sourceArea);\n\n // Formula\n const formulaTitle = document.createElement('div');\n formulaTitle.textContent = \"P(Odd Sum) = \";\n \n const fractionBuilder = document.createElement('div');\n fractionBuilder.className = 'fraction-builder';\n\n const fraction = document.createElement('div');\n fraction.className = 'fraction';\n\n const numSlot = document.createElement('div');\n numSlot.className = 'drop-zone';\n numSlot.dataset.target = 'num';\n \n const line = document.createElement('div');\n line.className = 'fraction-line';\n \n const denSlot = document.createElement('div');\n denSlot.className = 'drop-zone';\n denSlot.dataset.target = 'den';\n\n fraction.appendChild(numSlot);\n fraction.appendChild(line);\n fraction.appendChild(denSlot);\n\n const equals = document.createElement('div');\n equals.textContent = \"=\";\n \n const finalRes = document.createElement('div');\n finalRes.className = 'final-result';\n finalRes.id = 'final-res';\n finalRes.innerHTML = \"2/5 <span style='font-size:1rem; color:#ccc'> (or 40%)</span>\";\n\n fractionBuilder.appendChild(formulaTitle);\n fractionBuilder.appendChild(fraction);\n fractionBuilder.appendChild(equals);\n fractionBuilder.appendChild(finalRes);\n\n wrapper.appendChild(fractionBuilder);\n container.appendChild(wrapper);\n\n // Events\n [numSlot, denSlot].forEach(slot => {\n slot.addEventListener('dragover', e => {\n e.preventDefault();\n slot.classList.add('hover');\n });\n slot.addEventListener('dragleave', () => slot.classList.remove('hover'));\n slot.addEventListener('drop', handleDropStep5);\n });\n }\n\n function handleDropStep5(e) {\n e.preventDefault();\n e.target.classList.remove('hover');\n const val = e.dataTransfer.getData('text/plain');\n const target = e.target.dataset.target;\n\n // Basic Validation\n if (target === 'num' && val !== '4') {\n alert(\"The numerator must be the Favorable Outcomes (4).\");\n return;\n }\n if (target === 'den' && val !== '10') {\n alert(\"The denominator must be the Total Outcomes (10).\");\n return;\n }\n\n // Update UI\n e.target.textContent = val;\n e.target.style.border = \"2px solid var(--accent-green)\";\n e.target.style.backgroundColor = \"rgba(74, 222, 128, 0.2)\";\n \n // Hide source\n document.getElementById(`source-${val}`).style.opacity = '0.3';\n document.getElementById(`source-${val}`).draggable = false;\n\n state.probabilityInputs[target] = val;\n\n if (state.probabilityInputs.num && state.probabilityInputs.den) {\n document.getElementById('final-res').style.opacity = '1';\n launchConfetti();\n }\n }\n\n // --- Utilities ---\n function createCard(num) {\n const el = document.createElement('div');\n el.className = 'card';\n el.textContent = num;\n return el;\n }\n\n function launchConfetti() {\n // Simple CSS animation or just visual feedback\n const colors = ['#fb7185', '#38bdf8', '#4ade80', '#facc15'];\n for(let i=0; i<30; i++) {\n const dot = document.createElement('div');\n dot.style.position = 'fixed';\n dot.style.left = '50%';\n dot.style.top = '50%';\n dot.style.width = '10px';\n dot.style.height = '10px';\n dot.style.backgroundColor = colors[Math.floor(Math.random()*colors.length)];\n dot.style.borderRadius = '50%';\n dot.style.transition = 'all 1s ease-out';\n document.body.appendChild(dot);\n\n // Animate\n setTimeout(() => {\n const x = (Math.random() - 0.5) * window.innerWidth * 0.5;\n const y = (Math.random() - 0.5) * window.innerHeight * 0.5;\n dot.style.transform = `translate(${x}px, ${y}px)`;\n dot.style.opacity = '0';\n }, 10);\n\n setTimeout(() => dot.remove(), 1000);\n }\n }\n\n // Start\n init();\n\n </script>\n</body>\n</html>"
|
| 62 |
+
}
|
examples/002-visual-proof-pythagorean-theorem.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-24T20:58:36.471Z",
|
| 4 |
+
"input": {
|
| 5 |
+
"mode": "text",
|
| 6 |
+
"text": "explain pythagoras theroem"
|
| 7 |
+
},
|
| 8 |
+
"concept": {
|
| 9 |
+
"conceptTitle": "Pythagorean Theorem",
|
| 10 |
+
"educationalGoal": "To visually understand and prove the relationship between the sides of a right-angled triangle: a² + b² = c².",
|
| 11 |
+
"explanation": "The Pythagorean Theorem is a fundamental principle in geometry that describes the relationship between the three sides of a **right-angled triangle**. A right-angled triangle is a triangle with one angle measuring 90 degrees.\n\nLet's label the two shorter sides (legs) as `a` and `b`, and the longest side (hypotenuse), which is opposite the right angle, as `c`. The theorem states that the square of the hypotenuse (`c²`) is equal to the sum of the squares of the other two sides (`a² + b²`).\n\nWe'll explore a classic visual proof that demonstrates this relationship by rearranging shapes!",
|
| 12 |
+
"steps": [
|
| 13 |
+
{
|
| 14 |
+
"stepTitle": "1. Introducing the Right Triangle",
|
| 15 |
+
"instruction": "Observe the right-angled triangle. Its two shorter sides are labeled `a` and `b`, and the longest side, the hypotenuse, is labeled `c`. The square symbol marks the 90-degree angle.",
|
| 16 |
+
"visualFocus": "A single right-angled triangle with sides a, b, c labeled, and the right angle marked with a square."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stepTitle": "2. Squares on the Sides",
|
| 20 |
+
"instruction": "Drag the slider to visualize the squares built on each side of the triangle. The area of each square corresponds to a², b², and c² respectively.",
|
| 21 |
+
"visualFocus": "The right triangle, with squares built outwards from each side (a, b, and c). The areas a², b², c² are displayed next to their respective squares."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"stepTitle": "3. Proof Setup: First Arrangement",
|
| 25 |
+
"instruction": "To prove a² + b² = c², we'll use a larger square with side length (a+b). Observe how four copies of our original right triangle are arranged inside this large square. They form a smaller square in the center.",
|
| 26 |
+
"visualFocus": "A large square (side a+b). Inside, four identical right triangles are arranged, forming a central square with side c. The central square and the four triangles should be distinct."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"stepTitle": "4. Area Calculation: First Arrangement",
|
| 30 |
+
"instruction": "The total area of the large square can be expressed as the sum of its internal parts: the central square and the four triangles. Click 'Next' to see the calculation.",
|
| 31 |
+
"visualFocus": "Text: 'Area_large = Area(central square) + Area(4 triangles)'. Then, 'Area_large = c² + 4 * (1/2 * a * b)' simplifies to 'Area_large = c² + 2ab'. The central square and triangles are highlighted during calculation."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"stepTitle": "5. Proof Setup: Second Arrangement",
|
| 35 |
+
"instruction": "Now, let's take the *exact same four triangles* and arrange them differently within a second identical large square (also with side a+b). This time, they form two smaller squares (a² and b²) and two rectangles.",
|
| 36 |
+
"visualFocus": "A second large square (side a+b) appears next to the first. Inside it, the four identical right triangles are rearranged to form a square of side 'a', a square of side 'b', and two rectangles of area 'ab' each. The a² and b² squares should be distinct."
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"stepTitle": "6. Area Calculation: Second Arrangement",
|
| 40 |
+
"instruction": "Similar to before, the total area of this second large square can also be expressed as the sum of *its* internal parts. Click 'Next' to see the calculation.",
|
| 41 |
+
"visualFocus": "Text: 'Area_large = Area(square a) + Area(square b) + Area(2 rectangles)'. Then, 'Area_large = a² + b² + 2 * (a * b)' is displayed. The a² and b² squares and the 'ab' rectangles are highlighted during calculation."
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"stepTitle": "7. The Conclusion",
|
| 45 |
+
"instruction": "Since both large squares have the same side length (a+b) and contain the exact same four triangles, their total areas must be equal. Equate the two expressions for the large square's area to reveal the Pythagorean Theorem!",
|
| 46 |
+
"visualFocus": "Display: 'c² + 2ab = a² + b² + 2ab'. Then, animate the cancellation of '2ab' from both sides, leaving: 'c² = a² + b²'. This final equation is highlighted prominently."
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"visualSpec": {
|
| 50 |
+
"elements": [
|
| 51 |
+
"Right-angled triangle (vertices A, B, C; right angle at C)",
|
| 52 |
+
"Sides labeled: 'a' (opposite A), 'b' (opposite B), 'c' (opposite C - hypotenuse)",
|
| 53 |
+
"Squares built on sides a, b, c (areas a², b², c²)",
|
| 54 |
+
"Large square (side a+b)",
|
| 55 |
+
"Four identical right triangles (copies of the original)",
|
| 56 |
+
"Central square (side c) formed by the first arrangement of triangles",
|
| 57 |
+
"Two smaller squares (sides a, b) formed by the second arrangement of triangles",
|
| 58 |
+
"Two rectangles (dimensions a x b) formed by the second arrangement of triangles",
|
| 59 |
+
"Text labels for areas and equations"
|
| 60 |
+
],
|
| 61 |
+
"interactions": [
|
| 62 |
+
"Slider to control visibility/size of squares in Step 2",
|
| 63 |
+
"Next button to advance through steps and trigger animations/text reveals",
|
| 64 |
+
"Highlighting of specific visual elements (e.g., sides, squares, triangles) as they are discussed"
|
| 65 |
+
],
|
| 66 |
+
"mathLogic": "Area of a square = side * side; Area of a triangle = 1/2 * base * height; (a+b)² = a² + 2ab + b²; Equating areas: c² + 2ab = a² + b² + 2ab => c² = a² + b²"
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"sourceCode": "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Pythagorean Theorem Visual Proof</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-main: #e2e8f0;\n --text-muted: #94a3b8;\n --accent-blue: #3b82f6;\n --accent-red: #ef4444; /* Side a */\n --accent-green: #22c55e; /* Side b */\n --accent-yellow: #eab308;/* Side c */\n --btn-hover: #334155;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-main);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* Header */\n header {\n padding: 1rem 1.5rem;\n border-bottom: 1px solid #334155;\n flex-shrink: 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n letter-spacing: 0.05em;\n }\n\n /* Main Layout */\n main {\n flex: 1;\n display: flex;\n flex-direction: row;\n overflow: hidden;\n }\n\n /* Canvas Area */\n #canvas-container {\n flex: 2;\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: #020617;\n overflow: hidden;\n }\n\n canvas {\n display: block;\n max-width: 100%;\n max-height: 100%;\n }\n\n /* Info & Controls Panel */\n #info-panel {\n flex: 1;\n min-width: 320px;\n max-width: 500px;\n background-color: var(--panel-bg);\n border-left: 1px solid #334155;\n display: flex;\n flex-direction: column;\n padding: 2rem;\n box-shadow: -5px 0 15px rgba(0,0,0,0.3);\n z-index: 10;\n }\n\n /* Text Content */\n .step-indicator {\n text-transform: uppercase;\n font-size: 0.75rem;\n color: var(--accent-blue);\n font-weight: bold;\n margin-bottom: 0.5rem;\n }\n\n h2 {\n font-size: 1.5rem;\n margin-bottom: 1rem;\n color: #fff;\n }\n\n p {\n line-height: 1.6;\n color: var(--text-muted);\n margin-bottom: 1.5rem;\n font-size: 1rem;\n }\n\n /* Math Display Area */\n .math-display {\n background: #0f172a;\n border: 1px solid #334155;\n border-radius: 8px;\n padding: 1.5rem;\n margin-bottom: auto; /* Push controls to bottom */\n font-family: 'Courier New', Courier, monospace;\n font-size: 1.1rem;\n text-align: center;\n min-height: 100px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n transition: all 0.3s ease;\n }\n\n .math-line {\n margin: 5px 0;\n opacity: 0;\n transform: translateY(10px);\n animation: fadeInUp 0.5s forwards;\n }\n\n .highlight-eqn {\n color: var(--accent-yellow);\n font-weight: bold;\n font-size: 1.3rem;\n }\n\n .strike {\n text-decoration: line-through;\n text-decoration-color: var(--accent-red);\n text-decoration-thickness: 2px;\n opacity: 0.5;\n }\n\n /* Controls */\n .controls {\n margin-top: 2rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\n\n .slider-container {\n display: none; /* Hidden by default */\n width: 100%;\n margin-bottom: 1rem;\n }\n \n .slider-container label {\n display: block;\n margin-bottom: 0.5rem;\n font-size: 0.9rem;\n color: var(--text-muted);\n }\n\n input[type=range] {\n width: 100%;\n cursor: pointer;\n accent-color: var(--accent-blue);\n }\n\n .nav-buttons {\n display: flex;\n gap: 1rem;\n }\n\n button {\n flex: 1;\n padding: 0.75rem;\n border: none;\n border-radius: 6px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s;\n font-size: 1rem;\n }\n\n button.btn-primary {\n background-color: var(--accent-blue);\n color: white;\n }\n\n button.btn-primary:hover {\n background-color: #2563eb;\n }\n\n button.btn-secondary {\n background-color: #334155;\n color: var(--text-main);\n }\n\n button.btn-secondary:hover {\n background-color: #475569;\n }\n\n button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Color Spans for Text */\n .c-a { color: var(--accent-red); font-weight: bold; }\n .c-b { color: var(--accent-green); font-weight: bold; }\n .c-c { color: var(--accent-yellow); font-weight: bold; }\n\n /* Animations */\n @keyframes fadeInUp {\n to { opacity: 1; transform: translateY(0); }\n }\n\n /* Responsive */\n @media (max-width: 800px) {\n main {\n flex-direction: column;\n }\n \n #canvas-container {\n flex: 1;\n min-height: 50vh;\n }\n\n #info-panel {\n flex: 1;\n width: 100%;\n max-width: 100%;\n border-left: none;\n border-top: 1px solid #334155;\n padding: 1.5rem;\n }\n\n h2 { font-size: 1.2rem; }\n }\n </style>\n</head>\n<body>\n\n<header>\n <h1>MathVisuals <span style=\"color:var(--accent-blue)\">//</span> Pythagorean Theorem</h1>\n</header>\n\n<main>\n <div id=\"canvas-container\">\n <canvas id=\"vizCanvas\"></canvas>\n </div>\n\n <div id=\"info-panel\">\n <div>\n <div class=\"step-indicator\" id=\"step-indicator\">Step 1 of 7</div>\n <h2 id=\"step-title\">Introduction</h2>\n <div id=\"step-desc\"></div>\n </div>\n\n <div class=\"slider-container\" id=\"slider-group\">\n <label for=\"square-slider\">Resize Squares</label>\n <input type=\"range\" id=\"square-slider\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0\">\n </div>\n\n <div class=\"math-display\" id=\"math-display\">\n <!-- Dynamic Math Content -->\n </div>\n\n <div class=\"controls\">\n <div class=\"nav-buttons\">\n <button id=\"prev-btn\" class=\"btn-secondary\">Previous</button>\n <button id=\"next-btn\" class=\"btn-primary\">Next</button>\n </div>\n </div>\n </div>\n</main>\n\n<script>\n/**\n * Pythagorean Theorem Visualization Logic\n */\n\n// Configuration\nconst COLORS = {\n bg: '#0f172a',\n a: '#ef4444',\n b: '#22c55e',\n c: '#eab308',\n tri: 'rgba(59, 130, 246, 0.6)', // Blue with opacity\n triStroke: '#60a5fa',\n text: '#e2e8f0',\n grid: '#1e293b'\n};\n\n// Triangle properties (Base unit logic)\n// We use a 3-4-5 triangle for clean integer visuals, but code handles generic.\nconst TRI = {\n a: 3, // vertical leg\n b: 4, // horizontal leg\n c: 5\n};\n\nconst STEPS = [\n {\n id: 0,\n title: \"Introducing the Right Triangle\",\n desc: `Here is a right-angled triangle. The two shorter sides are labeled <span class=\"c-a\">a</span> and <span class=\"c-b\">b</span>. The longest side, opposite the right angle, is the hypotenuse, labeled <span class=\"c-c\">c</span>.`,\n math: [],\n hasSlider: false\n },\n {\n id: 1,\n title: \"Squares on the Sides\",\n desc: `Drag the slider to build squares on each side. The area of each square is the side length squared: <span class=\"c-a\">a²</span>, <span class=\"c-b\">b²</span>, and <span class=\"c-c\">c²</span>.`,\n math: [],\n hasSlider: true\n },\n {\n id: 2,\n title: \"Proof Setup: First Arrangement\",\n desc: `Let's build a large square with side length <strong>(a + b)</strong>. Inside, we arrange four copies of our triangle. Notice the empty space in the center forms a tilted square of side <span class=\"c-c\">c</span>.`,\n math: [],\n hasSlider: false\n },\n {\n id: 3,\n title: \"Area Calculation: First Arrangement\",\n desc: `The total area is the sum of the inner parts: the central square <span class=\"c-c\">(c²)</span> and the four triangles.`,\n math: [\n `Area<sub>Total</sub> = Area(<span class=\"c-c\">Square C</span>) + Area(4 Triangles)`,\n `Area<sub>Total</sub> = <span class=\"c-c\">c²</span> + 4 × (½ × <span class=\"c-a\">a</span> × <span class=\"c-b\">b</span>)`,\n `Area<sub>Total</sub> = <span class=\"c-c\">c²</span> + 2<span class=\"c-a\">a</span><span class=\"c-b\">b</span>`\n ],\n hasSlider: false\n },\n {\n id: 4,\n title: \"Proof Setup: Second Arrangement\",\n desc: `Now, take a second identical large square (side <strong>a + b</strong>). We rearrange the <em>same four triangles</em> differently. This leaves two smaller squares: <span class=\"c-a\">a²</span> and <span class=\"c-b\">b²</span>.`,\n math: [],\n hasSlider: false\n },\n {\n id: 5,\n title: \"Area Calculation: Second Arrangement\",\n desc: `Again, calculate the total area. It is the sum of the two squares (<span class=\"c-a\">a²</span>, <span class=\"c-b\">b²</span>) and the four triangles (which form two rectangles).`,\n math: [\n `Area<sub>Total</sub> = Area(<span class=\"c-a\">Sq A</span>) + Area(<span class=\"c-b\">Sq B</span>) + Area(4 Triangles)`,\n `Area<sub>Total</sub> = <span class=\"c-a\">a²</span> + <span class=\"c-b\">b²</span> + 2 × (<span class=\"c-a\">a</span> × <span class=\"c-b\">b</span>)`,\n `Area<sub>Total</sub> = <span class=\"c-a\">a²</span> + <span class=\"c-b\">b²</span> + 2<span class=\"c-a\">a</span><span class=\"c-b\">b</span>`\n ],\n hasSlider: false\n },\n {\n id: 6,\n title: \"The Conclusion\",\n desc: `Since both large squares are the same size, their area formulas must be equal. We can cancel out the triangles (2ab) from both sides.`,\n math: [\n `<span class=\"c-c\">c²</span> + <span class=\"strike\">2ab</span> = <span class=\"c-a\">a²</span> + <span class=\"c-b\">b²</span> + <span class=\"strike\">2ab</span>`,\n `<span class=\"highlight-eqn\"><span class=\"c-c\">c²</span> = <span class=\"c-a\">a²</span> + <span class=\"c-b\">b²</span></span>`\n ],\n hasSlider: false\n }\n];\n\n// Application State\nlet currentState = {\n step: 0,\n sliderValue: 0, // 0 to 1\n canvasWidth: 0,\n canvasHeight: 0,\n pixelsPerUnit: 40 // Scale factor\n};\n\n// DOM Elements\nconst canvas = document.getElementById('vizCanvas');\nconst ctx = canvas.getContext('2d');\nconst stepTitle = document.getElementById('step-title');\nconst stepDesc = document.getElementById('step-desc');\nconst stepIndicator = document.getElementById('step-indicator');\nconst mathDisplay = document.getElementById('math-display');\nconst sliderGroup = document.getElementById('slider-group');\nconst slider = document.getElementById('square-slider');\nconst prevBtn = document.getElementById('prev-btn');\nconst nextBtn = document.getElementById('next-btn');\n\n// Initialization\nfunction init() {\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n \n prevBtn.addEventListener('click', () => changeStep(-1));\n nextBtn.addEventListener('click', () => changeStep(1));\n \n slider.addEventListener('input', (e) => {\n currentState.sliderValue = parseFloat(e.target.value);\n draw();\n });\n\n updateUI();\n draw();\n}\n\nfunction resizeCanvas() {\n const container = document.getElementById('canvas-container');\n canvas.width = container.clientWidth;\n canvas.height = container.clientHeight;\n currentState.canvasWidth = canvas.width;\n currentState.canvasHeight = canvas.height;\n \n // Calculate scale based on a+b+padding\n const totalUnits = TRI.a + TRI.b + 2; // +2 for padding\n const minDim = Math.min(canvas.width, canvas.height);\n currentState.pixelsPerUnit = (minDim * 0.8) / totalUnits;\n \n draw();\n}\n\nfunction changeStep(delta) {\n const newStep = currentState.step + delta;\n if (newStep >= 0 && newStep < STEPS.length) {\n currentState.step = newStep;\n \n // Reset slider for step 2 interactions\n if (newStep === 1) {\n currentState.sliderValue = 0;\n slider.value = 0;\n } else {\n currentState.sliderValue = 1; // Full visualization for other steps\n }\n\n updateUI();\n draw();\n }\n}\n\nfunction updateUI() {\n const stepData = STEPS[currentState.step];\n \n // Text Updates\n stepIndicator.textContent = `Step ${stepData.id + 1} of ${STEPS.length}`;\n stepTitle.textContent = stepData.title;\n stepDesc.innerHTML = stepData.desc;\n \n // Math Display\n mathDisplay.innerHTML = '';\n stepData.math.forEach((line, index) => {\n const div = document.createElement('div');\n div.className = 'math-line';\n div.style.animationDelay = `${index * 0.5}s`;\n div.innerHTML = line;\n mathDisplay.appendChild(div);\n });\n\n // Controls Visibility\n sliderGroup.style.display = stepData.hasSlider ? 'block' : 'none';\n \n // Button States\n prevBtn.disabled = currentState.step === 0;\n nextBtn.disabled = currentState.step === STEPS.length - 1;\n}\n\n// --- Drawing Functions ---\n\nfunction draw() {\n // Clear Canvas\n ctx.fillStyle = COLORS.bg;\n ctx.fillRect(0, 0, currentState.canvasWidth, currentState.canvasHeight);\n \n ctx.save();\n \n // Center the coordinate system\n ctx.translate(currentState.canvasWidth / 2, currentState.canvasHeight / 2);\n \n const ppu = currentState.pixelsPerUnit;\n const { a, b, c } = TRI;\n \n // Switch based on step\n switch (currentState.step) {\n case 0: // Intro\n case 1: // Slider Squares\n drawSingleTriangleScene(ppu, a, b, c);\n break;\n \n case 2: // Arr 1 Setup\n case 3: // Arr 1 Calc\n case 6: // Conclusion (Comparing, usually shows Arr 1 or equation emphasis. Let's show Arr 1)\n drawArrangementOne(ppu, a, b, c);\n break;\n\n case 4: // Arr 2 Setup\n case 5: // Arr 2 Calc\n drawArrangementTwo(ppu, a, b, c);\n break;\n }\n\n ctx.restore();\n}\n\nfunction drawSingleTriangleScene(ppu, a, b, c) {\n // Center the triangle roughly\n const offsetX = -(b * ppu) / 2;\n const offsetY = (a * ppu) / 2;\n ctx.translate(offsetX, offsetY);\n\n // Draw Squares if Slider is active\n if (currentState.step === 1) {\n const t = currentState.sliderValue;\n \n // Square A (Left side)\n ctx.fillStyle = COLORS.a;\n ctx.globalAlpha = 0.2 + (0.6 * t);\n // Grow out to left: x from 0 to -a*t, width a*t\n // Actually simpler: Draw full square, scale it, or mask it.\n // Let's just scale the size of the square based on t\n const sA = a * ppu * t;\n if (sA > 1) {\n ctx.fillRect(-sA, -a * ppu, sA, sA); // Left of A leg\n ctx.globalAlpha = 1;\n if(t > 0.8) drawLabel(\"a²\", -sA/2, -a*ppu/2, COLORS.text);\n }\n\n // Square B (Bottom side)\n ctx.fillStyle = COLORS.b;\n ctx.globalAlpha = 0.2 + (0.6 * t);\n const sB = b * ppu * t;\n if (sB > 1) {\n ctx.fillRect(0, 0, sB, sB); // Below B leg\n ctx.globalAlpha = 1;\n if(t > 0.8) drawLabel(\"b²\", sB/2, sB/2, COLORS.text);\n }\n\n // Square C (Hypotenuse)\n ctx.save();\n // Rotate to align with hypotenuse\n // Angle of hypotenuse relative to x-axis (A is top (0, -a*ppu), B is right (b*ppu, 0))\n // Vector AB = (b, a). Angle = atan(a/b)\n const angle = Math.atan(a/b);\n // Move to top point\n ctx.translate(0, -a * ppu);\n ctx.rotate(angle); \n // Square projects \"up\" from the line A-B relative to the triangle? \n // Normal is perpendicular. \n // Let's simplify: Hypotenuse connects (0, -a) and (b, 0).\n // We want the square to grow outwards.\n ctx.fillStyle = COLORS.c;\n ctx.globalAlpha = 0.2 + (0.6 * t);\n const sC = c * ppu * t;\n if (sC > 1) {\n ctx.fillRect(0, -sC, c * ppu, sC);\n ctx.globalAlpha = 1;\n if(t > 0.8) {\n ctx.save();\n ctx.translate(c*ppu/2, -sC/2);\n ctx.rotate(-angle); // Unrotate text\n drawLabel(\"c²\", 0, 0, COLORS.text);\n ctx.restore();\n }\n }\n ctx.restore();\n }\n\n // Draw Triangle\n drawRightTriangle(ctx, 0, 0, a * ppu, b * ppu, COLORS.tri);\n \n // Labels\n ctx.font = \"bold 16px sans-serif\";\n \n // Side a\n ctx.fillStyle = COLORS.a;\n ctx.fillText(\"a\", -20, - (a * ppu) / 2);\n \n // Side b\n ctx.fillStyle = COLORS.b;\n ctx.fillText(\"b\", (b * ppu) / 2, 20);\n \n // Side c\n ctx.fillStyle = COLORS.c;\n ctx.fillText(\"c\", (b * ppu) / 2 + 10, - (a * ppu) / 2 - 10);\n}\n\nfunction drawArrangementOne(ppu, a, b, c) {\n const size = (a + b) * ppu;\n const startX = -size / 2;\n const startY = -size / 2;\n\n // Draw Big Square Container\n ctx.strokeStyle = COLORS.text;\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, size, size);\n\n // 4 Triangles\n const triColor = COLORS.tri;\n\n // Top-Left Triangle\n drawRightTriangleStandard(ctx, startX, startY, b*ppu, a*ppu, triColor, 0); // Base b, Height a ?? \n // Actually, standard proof:\n // Top-left corner: Go right 'b', down 'a' -> Hypotenuse c\n // Top-Right corner: Go down 'b', left 'a'\n \n // Let's do the standard coordinates explicitly for clarity\n // 1. Top Left (Rotated 0) - Vertical side 'a' on left wall, horizontal 'b' on top wall\n // Wait, if a=3, b=4.\n // Top Left Triangle: Vertices at (startX, startY), (startX + a, startY), (startX, startY + b). Hypotenuse internal.\n // No, the arrangement that forms C^2 in center:\n // T1: Top-Left corner. Leg 'a' along top edge. Leg 'b' along left edge. \n // T2: Top-Right corner. Leg 'b' along top edge. Leg 'a' along right edge.\n // ... This forms a small square (b-a) in center. Not c^2.\n \n // Correct Arrangement for C^2 in center (Bhaskara / Chinese):\n // T1: Base b along bottom, Height a along left. (Bottom-Left)\n // T2: Base b along top, Height a along right. (Top-Right)\n // T3: Base b along right... \n // \n // Let's use the outer square method:\n // Square side (a+b).\n // Point P1 on Top Edge at distance 'a' from TopLeft.\n // Point P2 on Right Edge at distance 'a' from TopRight.\n // Point P3 on Bottom Edge at distance 'a' from BottomRight.\n // Point P4 on Left Edge at distance 'a' from BottomLeft.\n // Connect P1-P2-P3-P4 -> This forms Square C.\n // The corners are the 4 triangles.\n \n const sa = a * ppu;\n const sb = b * ppu;\n \n // Triangle 1 (Top Left corner)\n // Vertices: (startX, startY), (startX+sa, startY), (startX, startY+sb). \n // This creates hypotenuse length c. \n // Let's fill the corners.\n \n // Corner 1: Top Left. Width 'a', Height 'b'.\n drawTrianglePoly(ctx, startX, startY, startX + sa, startY, startX, startY + sb, triColor);\n \n // Corner 2: Top Right. Width 'b', Height 'a'.\n drawTrianglePoly(ctx, startX + size, startY, startX + size - sb, startY, startX + size, startY + sa, triColor);\n\n // Corner 3: Bottom Right. Width 'a', Height 'b'.\n drawTrianglePoly(ctx, startX + size, startY + size, startX + size - sa, startY + size, startX + size, startY + size - sb, triColor);\n\n // Corner 4: Bottom Left. Width 'b', Height 'a'.\n drawTrianglePoly(ctx, startX, startY + size, startX + sb, startY + size, startX, startY + size - sa, triColor);\n\n // Label Center Square\n ctx.fillStyle = COLORS.c;\n ctx.globalAlpha = 0.2;\n ctx.beginPath();\n ctx.moveTo(startX + sa, startY);\n ctx.lineTo(startX + size, startY + sa);\n ctx.lineTo(startX + size - sa, startY + size);\n ctx.lineTo(startX, startY + size - sa);\n ctx.closePath();\n ctx.fill();\n \n ctx.globalAlpha = 1;\n drawLabel(\"c²\", 0, 0, COLORS.c);\n\n // Draw Side Labels on the outer box\n ctx.fillStyle = COLORS.text;\n ctx.font = \"14px sans-serif\";\n // Top Edge\n ctx.fillText(\"a\", startX + sa/2, startY - 10);\n ctx.fillText(\"b\", startX + sa + sb/2, startY - 10);\n}\n\nfunction drawArrangementTwo(ppu, a, b, c) {\n const size = (a + b) * ppu;\n const startX = -size / 2;\n const startY = -size / 2;\n const sa = a * ppu;\n const sb = b * ppu;\n\n // Outer Box\n ctx.strokeStyle = COLORS.text;\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, size, size);\n\n const triColor = COLORS.tri;\n\n // In this arrangement, we form a square a^2 and b^2.\n // Usually a^2 is top-left, b^2 is bottom-right.\n // The rectangles (a*b) are top-right and bottom-left.\n // Each rectangle is split into two triangles.\n\n // Square a^2 (Top Left)\n ctx.fillStyle = COLORS.a;\n ctx.globalAlpha = 0.3;\n ctx.fillRect(startX, startY, sa, sa);\n ctx.globalAlpha = 1;\n drawLabel(\"a²\", startX + sa/2, startY + sa/2, COLORS.text);\n\n // Square b^2 (Bottom Right)\n ctx.fillStyle = COLORS.b;\n ctx.globalAlpha = 0.3;\n ctx.fillRect(startX + sa, startY + sa, sb, sb);\n ctx.globalAlpha = 1;\n drawLabel(\"b²\", startX + sa + sb/2, startY + sa + sb/2, COLORS.text);\n\n // Rectangle 1 (Top Right) - Split into 2 triangles\n // Coords: x: startX+sa, y: startY, w: sb, h: sa\n drawTrianglePoly(ctx, startX + sa, startY, startX + size, startY, startX + size, startY + sa, triColor); // Top half\n drawTrianglePoly(ctx, startX + sa, startY, startX + sa, startY + sa, startX + size, startY + sa, triColor); // Bottom half\n // Draw diagonal line to show separation\n ctx.beginPath();\n ctx.moveTo(startX + sa, startY);\n ctx.lineTo(startX + size, startY + sa);\n ctx.strokeStyle = COLORS.bg;\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Rectangle 2 (Bottom Left) - Split into 2 triangles\n // Coords: x: startX, y: startY+sa, w: sa, h: sb\n drawTrianglePoly(ctx, startX, startY + sa, startX + sa, startY + sa, startX + sa, startY + size, triColor);\n drawTrianglePoly(ctx, startX, startY + sa, startX, startY + size, startX + sa, startY + size, triColor);\n // Draw diagonal\n ctx.beginPath();\n ctx.moveTo(startX, startY + sa);\n ctx.lineTo(startX + sa, startY + size);\n ctx.strokeStyle = COLORS.bg;\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Labels for sides\n ctx.fillStyle = COLORS.text;\n ctx.fillText(\"a\", startX + sa/2, startY - 10);\n ctx.fillText(\"b\", startX - 15, startY + sa + sb/2);\n}\n\n// --- Helpers ---\n\n// Draws triangle with right-angle at (x,y), vertical leg h (up negative), horizontal w (right positive)\nfunction drawRightTriangle(ctx, x, y, heightPixels, widthPixels, color) {\n ctx.beginPath();\n ctx.moveTo(x, y); // C (Right Angle)\n ctx.lineTo(x, y - heightPixels); // A\n ctx.lineTo(x + widthPixels, y); // B\n ctx.closePath();\n \n ctx.fillStyle = color;\n ctx.fill();\n ctx.strokeStyle = COLORS.triStroke;\n ctx.lineWidth = 2;\n ctx.stroke();\n\n // Right angle marker\n const m = 15;\n ctx.beginPath();\n ctx.moveTo(x, y - m);\n ctx.lineTo(x + m, y - m);\n ctx.lineTo(x + m, y);\n ctx.strokeStyle = COLORS.text;\n ctx.lineWidth = 1;\n ctx.stroke();\n}\n\n// Generic Triangle Polygon\nfunction drawTrianglePoly(ctx, x1, y1, x2, y2, x3, y3, color) {\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.lineTo(x3, y3);\n ctx.closePath();\n ctx.fillStyle = color;\n ctx.fill();\n ctx.strokeStyle = COLORS.bg; // Separator\n ctx.lineWidth = 1;\n ctx.stroke();\n}\n\n// Standard visual placeholder\nfunction drawRightTriangleStandard(ctx, x, y, w, h, color, rotationDeg) {\n ctx.save();\n ctx.translate(x, y);\n ctx.rotate(rotationDeg * Math.PI / 180);\n drawRightTriangle(ctx, 0, 0, h, w, color);\n ctx.restore();\n}\n\nfunction drawLabel(text, x, y, color) {\n ctx.fillStyle = color;\n ctx.font = \"bold 20px Courier New\";\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(text, x, y);\n}\n\n// Start\ninit();\n\n</script>\n</body>\n</html>\n"
|
| 70 |
+
}
|
examples/003-visual-proof-area-of-quadrilaterals-with-perpendicular-diagonals.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-24T20:51:42.020Z",
|
| 4 |
+
"input": {
|
| 5 |
+
"mode": "url",
|
| 6 |
+
"url": "https://cemc.uwaterloo.ca/sites/default/files/documents/2025/POTWC-25-D-N-10-P.html"
|
| 7 |
+
},
|
| 8 |
+
"concept": {
|
| 9 |
+
"conceptTitle": "Area of Quadrilaterals with Perpendicular Diagonals",
|
| 10 |
+
"educationalGoal": "Understand and visually prove that the area of any quadrilateral with perpendicular diagonals is half the product of its diagonals.",
|
| 11 |
+
"explanation": "Have you ever wondered if there's a simple way to find the area of a shape like a kite or a rhombus? These shapes have a special property: their diagonals are perpendicular! This proof will show you how to find the area of *any* quadrilateral where the diagonals intersect at a right angle, using a clever division into triangles.",
|
| 12 |
+
"steps": [
|
| 13 |
+
{
|
| 14 |
+
"stepTitle": "Introducing the Quadrilateral",
|
| 15 |
+
"instruction": "Observe the quadrilateral ABCD and its diagonals AC and BD. Notice they intersect at a right angle at point O. Drag vertices A, B, C, or D to see how the shape changes while maintaining perpendicular diagonals.",
|
| 16 |
+
"visualFocus": "Quadrilateral ABCD, diagonals AC and BD, intersection point O, right angle symbol at O. Labels A, B, C, D, O."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stepTitle": "Decomposing into Triangles",
|
| 20 |
+
"instruction": "The perpendicular diagonals divide the quadrilateral into two main triangles, sharing a common base BD. Click 'Next' to highlight these triangles and their corresponding altitudes.",
|
| 21 |
+
"visualFocus": "Highlight △ABD and △BCD. Show altitude from A to BD (segment AO) and altitude from C to BD (segment CO)."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"stepTitle": "Area of Each Main Triangle",
|
| 25 |
+
"instruction": "The area of a triangle is (1/2) * base * height. For △ABD, the base is BD and the height is AO. For △BCD, the base is BD and the height is OC. Observe the formulas.",
|
| 26 |
+
"visualFocus": "Display 'Area(△ABD) = (1/2) * BD * AO' and 'Area(△BCD) = (1/2) * BD * OC'. Highlight segments BD, AO, OC."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"stepTitle": "Summing the Areas",
|
| 30 |
+
"instruction": "The total area of the quadrilateral ABCD is the sum of the areas of △ABD and △BCD. Click 'Next' to see the combined expression.",
|
| 31 |
+
"visualFocus": "Display 'Area(ABCD) = Area(△ABD) + Area(△BCD)' then 'Area(ABCD) = (1/2) * BD * AO + (1/2) * BD * OC'. Highlight the entire equation."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"stepTitle": "Factoring and Simplifying",
|
| 35 |
+
"instruction": "Notice that (1/2) * BD is a common factor in both terms. Factor it out. Then, observe that (AO + OC) is simply the entire diagonal AC. Click 'Next' to see the final simplified formula.",
|
| 36 |
+
"visualFocus": "First, highlight and show 'Area(ABCD) = (1/2) * BD * (AO + OC)'. Then, highlight (AO + OC) and show 'AC'. Finally, display 'Area(ABCD) = (1/2) * BD * AC'. Also, label AC as d1 and BD as d2, and display 'Area(ABCD) = (1/2) * d1 * d2'."
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"visualSpec": {
|
| 40 |
+
"elements": [
|
| 41 |
+
"A quadrilateral ABCD with vertices A, B, C, D.",
|
| 42 |
+
"Diagonals AC and BD, intersecting at point O.",
|
| 43 |
+
"A right angle symbol at point O, indicating perpendicularity.",
|
| 44 |
+
"Labels for points A, B, C, D, O.",
|
| 45 |
+
"Labels for segments: AC (d1), BD (d2), AO, BO, CO, DO.",
|
| 46 |
+
"Formulas displayed as text overlays or in a dedicated math display area."
|
| 47 |
+
],
|
| 48 |
+
"interactions": [
|
| 49 |
+
"Drag vertices A, B, C, D: Quadrilateral shape should change, but diagonals must remain perpendicular (O remains 90 degrees). Diagonal lengths (d1, d2) and segment lengths (AO, BO, CO, DO) should update dynamically.",
|
| 50 |
+
"Click 'Next' button: Progresses through the steps, triggering visual highlights and formula displays."
|
| 51 |
+
],
|
| 52 |
+
"mathLogic": "1. Area(△ABD) = (1/2) * base * height = (1/2) * BD * AO\n2. Area(△BCD) = (1/2) * base * height = (1/2) * BD * OC\n3. Area(ABCD) = Area(△ABD) + Area(△BCD)\n4. Area(ABCD) = (1/2) * BD * AO + (1/2) * BD * OC\n5. Area(ABCD) = (1/2) * BD * (AO + OC)\n6. Since AO + OC = AC (length of diagonal d1), and BD = d2 (length of diagonal d2),\n7. Area(ABCD) = (1/2) * d1 * d2"
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"sourceCode": "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Visual Proof: Area of Orthodiagonal Quadrilateral</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-color: #e2e8f0;\n --accent-color: #38bdf8;\n --secondary-accent: #f472b6;\n --highlight-tri-1: rgba(56, 189, 248, 0.25); /* Light Blue */\n --highlight-tri-2: rgba(244, 114, 182, 0.25); /* Pink */\n --btn-bg: #334155;\n --btn-hover: #475569;\n --border-color: #475569;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n }\n\n body {\n background-color: var(--bg-color);\n color: var(--text-color);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* Header */\n header {\n padding: 1rem 2rem;\n border-bottom: 1px solid var(--border-color);\n display: flex;\n justify-content: space-between;\n align-items: center;\n background-color: var(--panel-bg);\n flex-shrink: 0;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n color: #fff;\n }\n\n /* Main Layout */\n main {\n display: flex;\n flex: 1;\n overflow: hidden;\n position: relative;\n }\n\n /* Canvas Area */\n #canvas-container {\n flex: 2;\n position: relative;\n background-color: #0b1120;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow: hidden;\n cursor: crosshair;\n }\n\n canvas {\n box-shadow: inset 0 0 20px rgba(0,0,0,0.5);\n }\n\n /* Controls & Info Panel */\n #info-panel {\n flex: 1;\n min-width: 350px;\n max-width: 500px;\n background-color: var(--panel-bg);\n border-left: 1px solid var(--border-color);\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n overflow-y: auto;\n z-index: 10;\n }\n\n /* Step Indicator */\n .step-indicator {\n font-size: 0.875rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #94a3b8;\n margin-bottom: 0.5rem;\n }\n\n .step-title {\n font-size: 1.5rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 1rem;\n }\n\n .instruction-text {\n font-size: 1rem;\n line-height: 1.6;\n color: #cbd5e1;\n }\n\n /* Math Display */\n .math-box {\n background-color: rgba(0, 0, 0, 0.2);\n padding: 1rem;\n border-radius: 8px;\n border: 1px solid var(--border-color);\n font-family: 'Courier New', Courier, monospace;\n margin-top: 1rem;\n }\n\n .math-row {\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0.5rem 0;\n font-size: 1.1rem;\n flex-wrap: wrap;\n }\n\n .fraction {\n display: inline-block;\n text-align: center;\n vertical-align: middle;\n margin: 0 0.2rem;\n font-size: 0.9em;\n }\n\n .fraction > span {\n display: block;\n padding: 0.1rem;\n }\n\n .fraction span.numerator {\n border-bottom: 1px solid currentColor;\n }\n\n .highlight-blue { color: var(--accent-color); font-weight: bold; }\n .highlight-pink { color: var(--secondary-accent); font-weight: bold; }\n .highlight-green { color: #4ade80; font-weight: bold; }\n\n /* Navigation Buttons */\n .nav-controls {\n margin-top: auto;\n display: flex;\n justify-content: space-between;\n gap: 1rem;\n padding-top: 1rem;\n border-top: 1px solid var(--border-color);\n }\n\n button {\n background-color: var(--btn-bg);\n color: #fff;\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 6px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s ease;\n flex: 1;\n }\n\n button:hover:not(:disabled) {\n background-color: var(--btn-hover);\n }\n\n button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Responsive */\n @media (max-width: 800px) {\n main {\n flex-direction: column;\n }\n #canvas-container {\n min-height: 50vh;\n }\n #info-panel {\n min-width: 100%;\n border-left: none;\n border-top: 1px solid var(--border-color);\n }\n }\n\n /* Drag Hint */\n .drag-hint {\n position: absolute;\n top: 1rem;\n left: 1rem;\n background: rgba(0,0,0,0.6);\n color: #fff;\n padding: 0.5rem 1rem;\n border-radius: 20px;\n font-size: 0.85rem;\n pointer-events: none;\n }\n </style>\n</head>\n<body>\n\n<header>\n <h1>Math Visualizer</h1>\n <div style=\"font-size: 0.9rem; color: #94a3b8;\">Quadrilaterals with Perpendicular Diagonals</div>\n</header>\n\n<main>\n <div id=\"canvas-container\">\n <canvas id=\"geometryCanvas\"></canvas>\n <div class=\"drag-hint\">Drag points A, B, C, D to resize</div>\n </div>\n\n <div id=\"info-panel\">\n <div id=\"step-content\">\n <!-- Content injected by JS -->\n </div>\n \n <div class=\"nav-controls\">\n <button id=\"btn-prev\" disabled>Previous</button>\n <button id=\"btn-next\">Next Step</button>\n </div>\n </div>\n</main>\n\n<script>\n/**\n * Logic for \"Area of Quadrilaterals with Perpendicular Diagonals\"\n */\n\n// --- Constants & Config ---\nconst COLORS = {\n bg: '#0f172a',\n text: '#e2e8f0',\n line: '#cbd5e1',\n dashed: '#64748b',\n accent: '#38bdf8', // Blue (Tri 1)\n accent2: '#f472b6', // Pink (Tri 2)\n highlight: '#facc15', // Yellow/Gold for active focus\n point: '#fff',\n pointFill: '#1e293b'\n};\n\n// --- State Management ---\nconst state = {\n step: 0,\n // Geometry: Defined by intersection O at (0,0) locally, \n // and distances to vertices along perpendicular axes.\n // We apply a global rotation to make it look less like a \"+\" and more like a shape.\n geom: {\n rotation: -Math.PI / 12, // -15 degrees tilt\n oa: 120, // Distance Up (visually)\n oc: 160, // Distance Down\n ob: 100, // Distance Left\n od: 100 // Distance Right\n },\n // Canvas dimensions\n width: 0,\n height: 0,\n cx: 0,\n cy: 0,\n // Interaction\n dragging: null, // 'A', 'B', 'C', 'D'\n};\n\n// --- Step Definitions ---\nconst steps = [\n {\n title: \"1. Introducing the Quadrilateral\",\n text: \"Observe the quadrilateral <strong>ABCD</strong>. Its diagonals, <strong>AC</strong> and <strong>BD</strong>, intersect at point <strong>O</strong> at a <strong>right angle</strong> (90°).<br><br>Try dragging the vertices to change the shape. Notice that no matter how you move them, the diagonals remain perpendicular.\",\n math: null,\n drawFlags: { showQuads: true, showDiagonals: true, showRightAngle: true, highlightAlt: false, highlightBase: false, highlightFullDiag: false }\n },\n {\n title: \"2. Decomposing into Triangles\",\n text: \"The perpendicular diagonals divide the shape into two main triangles: <span class='highlight-blue'>△ABD</span> (top/left) and <span class='highlight-pink'>△BCD</span> (bottom/right).<br><br>Notice they share a common base, the diagonal <strong>BD</strong>. Their heights correspond to the segments <strong>AO</strong> and <strong>OC</strong>.\",\n math: null,\n drawFlags: { fillTriangles: true, showAltitudes: true }\n },\n {\n title: \"3. Area of Each Main Triangle\",\n text: \"Recall the area of a triangle is half the base times the height.<br>For <span class='highlight-blue'>△ABD</span>, Base = <strong>BD</strong>, Height = <strong>AO</strong>.<br>For <span class='highlight-pink'>△BCD</span>, Base = <strong>BD</strong>, Height = <strong>OC</strong>.\",\n math: `\n <div class=\"math-row highlight-blue\">Area(△ABD) = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> × BD × AO</div>\n <div class=\"math-row highlight-pink\">Area(△BCD) = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> × BD × OC</div>\n `,\n drawFlags: { fillTriangles: true, highlightFormulas: true }\n },\n {\n title: \"4. Summing the Areas\",\n text: \"The total area of quadrilateral <strong>ABCD</strong> is simply the sum of the areas of these two triangles.\",\n math: `\n <div class=\"math-row\">Area(ABCD) = <span class=\"highlight-blue\">Area(△ABD)</span> + <span class=\"highlight-pink\">Area(△BCD)</span></div>\n <div class=\"math-row\">Area(ABCD) = <span class=\"highlight-blue\"><div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> ċ BD ċ AO</span> + <span class=\"highlight-pink\"><div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> ċ BD ċ OC</span></div>\n `,\n drawFlags: { fillTriangles: true }\n },\n {\n title: \"5. Factoring and Simplifying\",\n text: \"We can factor out <strong>1/2</strong> and <strong>BD</strong>. Notice that <strong>(AO + OC)</strong> is equal to the length of the entire diagonal <strong>AC</strong>.<br><br>Thus, the area is half the product of the diagonals.\",\n math: `\n <div class=\"math-row\">Area = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> ċ BD ċ (AO + OC)</div>\n <div class=\"math-row highlight-green\">Area = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> ċ BD ċ AC</div>\n <div class=\"math-row\" style=\"font-size: 0.9em; color: #94a3b8;\">(or ½ ċ d<sub>1</sub> ċ d<sub>2</sub>)</div>\n `,\n drawFlags: { highlightFullDiag: true, showFinalLabels: true }\n }\n];\n\n// --- DOM Elements ---\nconst canvas = document.getElementById('geometryCanvas');\nconst ctx = canvas.getContext('2d');\nconst stepContent = document.getElementById('step-content');\nconst btnPrev = document.getElementById('btn-prev');\nconst btnNext = document.getElementById('btn-next');\n\n// --- Initialization ---\nfunction init() {\n window.addEventListener('resize', handleResize);\n canvas.addEventListener('mousedown', handleMouseDown);\n canvas.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n \n // Touch support\n canvas.addEventListener('touchstart', handleTouchStart, {passive: false});\n canvas.addEventListener('touchmove', handleTouchMove, {passive: false});\n window.addEventListener('touchend', handleMouseUp);\n\n btnPrev.addEventListener('click', () => changeStep(-1));\n btnNext.addEventListener('click', () => changeStep(1));\n\n handleResize();\n updateStepUI();\n animate();\n}\n\nfunction handleResize() {\n const container = document.getElementById('canvas-container');\n state.width = container.clientWidth;\n state.height = container.clientHeight;\n canvas.width = state.width;\n canvas.height = state.height;\n \n // Center point\n state.cx = state.width / 2;\n state.cy = state.height / 2;\n\n // Ensure geometry fits if screen is small\n const minDim = Math.min(state.width, state.height);\n if (minDim < 400) {\n const scale = minDim / 400;\n state.geom.oa = 100 * scale;\n state.geom.oc = 140 * scale;\n state.geom.ob = 80 * scale;\n state.geom.od = 80 * scale;\n }\n\n draw();\n}\n\n// --- Step Logic ---\nfunction changeStep(delta) {\n state.step = Math.max(0, Math.min(steps.length - 1, state.step + delta));\n updateStepUI();\n draw();\n}\n\nfunction updateStepUI() {\n const currentStep = steps[state.step];\n \n // Update Buttons\n btnPrev.disabled = state.step === 0;\n btnNext.textContent = state.step === steps.length - 1 ? \"Finish\" : \"Next Step\";\n btnNext.disabled = state.step === steps.length - 1;\n\n // Update Content\n let html = `<div class=\"step-indicator\">Step ${state.step + 1} of ${steps.length}</div>`;\n html += `<div class=\"step-title\">${currentStep.title}</div>`;\n html += `<div class=\"instruction-text\">${currentStep.text}</div>`;\n \n if (currentStep.math) {\n html += `<div class=\"math-box\">${currentStep.math}</div>`;\n }\n\n // Dynamic value display (Area calc)\n if (state.step >= 2) {\n const d1 = Math.round(state.geom.oa + state.geom.oc);\n const d2 = Math.round(state.geom.ob + state.geom.od);\n const area = 0.5 * d1 * d2;\n html += `<div style=\"margin-top:1rem; padding-top:1rem; border-top:1px solid #334155; font-size:0.9rem; color:#94a3b8;\">\n Current Dimensions:<br>\n d<sub>1</sub> (AC) = ${d1} px<br>\n d<sub>2</sub> (BD) = ${d2} px<br>\n Area = ${area.toLocaleString()} px²\n </div>`;\n }\n\n stepContent.innerHTML = html;\n}\n\n// --- Geometry & Drawing ---\n\n// Helper to get coordinates of vertices based on current state\nfunction getVertices() {\n const { rotation, oa, ob, oc, od } = state.geom;\n const cx = state.cx;\n const cy = state.cy;\n\n // Axis unit vectors\n // Diagonal AC is along the \"Y-ish\" axis (rotated)\n // Diagonal BD is along the \"X-ish\" axis (rotated)\n // To make it intuitive: Let's define AC along vector U, BD along vector V.\n // U = (cos(rot - 90), sin(rot - 90)) -> standard Up direction rotated\n // V = (cos(rot), sin(rot)) -> standard Right direction rotated\n \n // Actually simpler: \n // A is 'Up' (negative Y relative to rotation), C is 'Down'\n // B is 'Left', D is 'Right'\n \n const cos = Math.cos(rotation);\n const sin = Math.sin(rotation);\n\n // Vector for AC direction (Vertical-ish)\n // In standard canvas: Y increases downwards. \n // Let's define local Up as vector (-sin, cos) ? No, let's use standard rotation matrix.\n \n // A is at distance OA in direction (rotation - 90 deg)\n const aAngle = rotation - Math.PI / 2;\n const A = { x: cx + Math.cos(aAngle) * oa, y: cy + Math.sin(aAngle) * oa };\n\n // C is at distance OC in direction (rotation + 90 deg)\n const cAngle = rotation + Math.PI / 2;\n const C = { x: cx + Math.cos(cAngle) * oc, y: cy + Math.sin(cAngle) * oc };\n\n // B is at distance OB in direction (rotation + 180 deg)\n const bAngle = rotation + Math.PI;\n const B = { x: cx + Math.cos(bAngle) * state.geom.ob, y: cy + Math.sin(bAngle) * state.geom.ob };\n\n // D is at distance OD in direction (rotation)\n const dAngle = rotation;\n const D = { x: cx + Math.cos(dAngle) * state.geom.od, y: cy + Math.sin(dAngle) * state.geom.od };\n\n const O = { x: cx, y: cy };\n\n return { A, B, C, D, O };\n}\n\nfunction draw() {\n // Clear\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n \n const coords = getVertices();\n const flags = steps[state.step].drawFlags || {};\n\n // 1. Fill Triangles if needed\n if (flags.fillTriangles) {\n // ABD (Top)\n ctx.fillStyle = COLORS.highlight_tri_1 || 'rgba(56, 189, 248, 0.15)';\n ctx.beginPath();\n ctx.moveTo(coords.A.x, coords.A.y);\n ctx.lineTo(coords.B.x, coords.B.y);\n ctx.lineTo(coords.D.x, coords.D.y);\n ctx.closePath();\n ctx.fill();\n\n // BCD (Bottom)\n ctx.fillStyle = COLORS.highlight_tri_2 || 'rgba(244, 114, 182, 0.15)';\n ctx.beginPath();\n ctx.moveTo(coords.C.x, coords.C.y);\n ctx.lineTo(coords.B.x, coords.B.y);\n ctx.lineTo(coords.D.x, coords.D.y);\n ctx.closePath();\n ctx.fill();\n }\n\n // 2. Diagonals (Dashed)\n ctx.beginPath();\n ctx.setLineDash([5, 5]);\n ctx.strokeStyle = COLORS.dashed;\n ctx.lineWidth = 2;\n \n // AC\n ctx.moveTo(coords.A.x, coords.A.y);\n ctx.lineTo(coords.C.x, coords.C.y);\n \n // BD\n ctx.moveTo(coords.B.x, coords.B.y);\n ctx.lineTo(coords.D.x, coords.D.y);\n ctx.stroke();\n ctx.setLineDash([]);\n\n // 3. Quadrilateral Outline\n ctx.beginPath();\n ctx.strokeStyle = COLORS.line;\n ctx.lineWidth = 3;\n ctx.moveTo(coords.A.x, coords.A.y);\n ctx.lineTo(coords.B.x, coords.B.y);\n ctx.lineTo(coords.C.x, coords.C.y);\n ctx.lineTo(coords.D.x, coords.D.y);\n ctx.closePath();\n ctx.stroke();\n\n // 4. Right Angle Symbol at O\n if (flags.showRightAngle || state.step === 0) {\n drawRightAngle(coords.O, state.geom.rotation);\n }\n\n // 5. Highlights based on Step\n if (flags.highlightFormulas) {\n // Highlight Base BD\n drawSegment(coords.B, coords.D, '#fff', 4);\n // Highlight Height AO\n drawSegment(coords.A, coords.O, COLORS.accent, 4);\n // Highlight Height OC\n drawSegment(coords.O, coords.C, COLORS.accent2, 4);\n }\n\n if (flags.highlightFullDiag) {\n // Highlight AC\n drawSegment(coords.A, coords.C, '#4ade80', 4);\n // Highlight BD\n drawSegment(coords.B, coords.D, '#fff', 4);\n }\n\n // 6. Labels\n drawPoint(coords.A, 'A');\n drawPoint(coords.B, 'B');\n drawPoint(coords.C, 'C');\n drawPoint(coords.D, 'D');\n drawPoint(coords.O, 'O', true);\n\n // Distance Labels (Optional / Contextual)\n if (state.step >= 2) {\n drawText(\"d2 (Base)\", coords.D.x + 10, coords.D.y + 20, '#94a3b8', '12px');\n if(state.step === 4) {\n drawText(\"d1\", coords.A.x - 20, coords.A.y, '#4ade80', 'bold 14px');\n }\n }\n}\n\n// --- Drawing Helpers ---\nfunction drawPoint(pt, label, isSmall = false) {\n ctx.beginPath();\n ctx.arc(pt.x, pt.y, isSmall ? 4 : 6, 0, Math.PI * 2);\n ctx.fillStyle = COLORS.pointFill;\n ctx.fill();\n ctx.strokeStyle = COLORS.point;\n ctx.lineWidth = 2;\n ctx.stroke();\n\n // Label offset calculation\n const dx = pt.x - state.cx;\n const dy = pt.y - state.cy;\n const dist = Math.sqrt(dx*dx + dy*dy);\n \n let lx = pt.x;\n let ly = pt.y;\n\n if (dist > 0 && !isSmall) {\n lx += (dx / dist) * 25;\n ly += (dy / dist) * 25;\n } else if (isSmall) {\n lx += 15;\n ly -= 15;\n }\n\n ctx.fillStyle = COLORS.text;\n ctx.font = 'bold 16px Segoe UI';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(label, lx, ly);\n}\n\nfunction drawSegment(p1, p2, color, width) {\n ctx.beginPath();\n ctx.moveTo(p1.x, p1.y);\n ctx.lineTo(p2.x, p2.y);\n ctx.strokeStyle = color;\n ctx.lineWidth = width;\n ctx.stroke();\n}\n\nfunction drawRightAngle(center, rotation) {\n const size = 15;\n ctx.beginPath();\n // We need points offset by rotation relative to the axes\n // Axis 1: rotation\n // Axis 2: rotation - 90\n const angle1 = rotation;\n const angle2 = rotation - Math.PI/2;\n \n // Corner point\n const p1 = { \n x: center.x + Math.cos(angle1) * size,\n y: center.y + Math.sin(angle1) * size\n };\n \n const p2 = {\n x: center.x + Math.cos(angle1) * size + Math.cos(angle2) * size,\n y: center.y + Math.sin(angle1) * size + Math.sin(angle2) * size\n };\n \n const p3 = {\n x: center.x + Math.cos(angle2) * size,\n y: center.y + Math.sin(angle2) * size\n };\n \n ctx.moveTo(p1.x, p1.y);\n ctx.lineTo(p2.x, p2.y);\n ctx.lineTo(p3.x, p3.y);\n ctx.strokeStyle = COLORS.text;\n ctx.lineWidth = 1.5;\n ctx.stroke();\n}\n\nfunction drawText(text, x, y, color, font) {\n ctx.fillStyle = color;\n ctx.font = font || '12px Segoe UI';\n ctx.fillText(text, x, y);\n}\n\n// --- Interaction Logic ---\n\nfunction getMousePos(evt) {\n const rect = canvas.getBoundingClientRect();\n return {\n x: evt.clientX - rect.left,\n y: evt.clientY - rect.top\n };\n}\n\nfunction getTouchPos(evt) {\n const rect = canvas.getBoundingClientRect();\n return {\n x: evt.touches[0].clientX - rect.left,\n y: evt.touches[0].clientY - rect.top\n };\n}\n\n// Hit test\nfunction getHitPoint(pos) {\n const coords = getVertices();\n const threshold = 20;\n \n if (dist(pos, coords.A) < threshold) return 'A';\n if (dist(pos, coords.B) < threshold) return 'B';\n if (dist(pos, coords.C) < threshold) return 'C';\n if (dist(pos, coords.D) < threshold) return 'D';\n return null;\n}\n\nfunction dist(p1, p2) {\n return Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2);\n}\n\nfunction handleMouseDown(evt) {\n const pos = getMousePos(evt);\n state.dragging = getHitPoint(pos);\n if (state.dragging) {\n canvas.style.cursor = 'grabbing';\n }\n}\n\nfunction handleTouchStart(evt) {\n const pos = getTouchPos(evt);\n state.dragging = getHitPoint(pos);\n if (state.dragging) {\n evt.preventDefault(); // prevent scroll\n }\n}\n\nfunction handleMouseMove(evt) {\n if (!state.dragging) {\n const pos = getMousePos(evt);\n const hit = getHitPoint(pos);\n canvas.style.cursor = hit ? 'grab' : 'crosshair';\n return;\n }\n \n const pos = getMousePos(evt);\n updateGeometry(pos);\n}\n\nfunction handleTouchMove(evt) {\n if (!state.dragging) return;\n evt.preventDefault();\n const pos = getTouchPos(evt);\n updateGeometry(pos);\n}\n\nfunction handleMouseUp() {\n state.dragging = null;\n canvas.style.cursor = 'crosshair';\n}\n\n// Core logic for constraint preservation\nfunction updateGeometry(mousePos) {\n const dx = mousePos.x - state.cx;\n const dy = mousePos.y - state.cy;\n \n // We need to project the mouse vector onto the specific axis of the point being dragged.\n // Rotation R.\n // A axis: R - 90 deg\n // C axis: R + 90 deg (Same line as A, opp direction)\n // B axis: R + 180 deg\n // D axis: R deg\n \n const rot = state.geom.rotation;\n let axisAngle = 0;\n \n // Identify the axis angle for the dragged point\n switch(state.dragging) {\n case 'A': axisAngle = rot - Math.PI/2; break;\n case 'C': axisAngle = rot + Math.PI/2; break;\n case 'B': axisAngle = rot + Math.PI; break; // Left-ish\n case 'D': axisAngle = rot; break; // Right-ish\n }\n \n // Unit vector for axis\n const u = { x: Math.cos(axisAngle), y: Math.sin(axisAngle) };\n \n // Project mouse vector (dx, dy) onto u\n // Dot product\n let len = dx * u.x + dy * u.y;\n \n // Clamp minimum length to avoid shape inverting or becoming 0\n len = Math.max(20, len);\n \n // Update specific length\n switch(state.dragging) {\n case 'A': state.geom.oa = len; break;\n case 'C': state.geom.oc = len; break;\n case 'B': state.geom.ob = len; break;\n case 'D': state.geom.od = len; break;\n }\n \n // Force redraw and UI update (for area calculation text)\n draw();\n updateStepUI();\n}\n\nfunction animate() {\n requestAnimationFrame(animate);\n // We are using event-driven drawing mostly, but a loop ensures smooth transitions if we add animations later\n // For now, just ensuring state consistency.\n}\n\n// Start\ninit();\n\n</script>\n</body>\n</html>\n"
|
| 56 |
+
}
|
examples/004-visual-proof-area-ratios-in-quadrilaterals.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-24T20:36:54.093Z",
|
| 4 |
+
"input": {
|
| 5 |
+
"mode": "url",
|
| 6 |
+
"url": "https://cemc.uwaterloo.ca/sites/default/files/documents/2025/POTWC-25-G-11-P.html"
|
| 7 |
+
},
|
| 8 |
+
"concept": {
|
| 9 |
+
"conceptTitle": "Area Ratios in Quadrilaterals",
|
| 10 |
+
"educationalGoal": "Understand and apply the relationship between the areas of the four triangles formed by the intersecting diagonals of a convex quadrilateral.",
|
| 11 |
+
"explanation": "When the diagonals of a convex quadrilateral intersect, they divide the quadrilateral into four triangles. There's a powerful and often surprising relationship between the areas of these four triangles: the product of the areas of opposite triangles is always equal! This property is incredibly useful for solving various geometry problems, especially when some areas are known, and an unknown area needs to be found.",
|
| 12 |
+
"steps": [
|
| 13 |
+
{
|
| 14 |
+
"stepTitle": "The Quadrilateral and its Diagonals",
|
| 15 |
+
"instruction": "Observe the convex quadrilateral ABCD and its diagonals AC and BD, intersecting at point E. Note the four triangles formed: ABE, BCE, CDE, and DAE.",
|
| 16 |
+
"visualFocus": "The entire quadrilateral ABCD with its diagonals AC and BD, and the intersection point E. Highlight the four constituent triangles ABE, BCE, CDE, DAE."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stepTitle": "Shared Altitude from Vertex A",
|
| 20 |
+
"instruction": "Focus on triangles ABE and DAE. They share the same altitude from vertex A to the line segment BD. The ratio of their areas is equal to the ratio of their bases, BE and DE. Drag vertex A to see how the altitude changes, while the relationship holds.",
|
| 21 |
+
"visualFocus": "Highlight triangles ABE and DAE. Show a dashed line representing the common altitude from A to BD. Display text: Area(ABE) / Area(DAE) = BE / DE."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"stepTitle": "Shared Altitude from Vertex C",
|
| 25 |
+
"instruction": "Now, consider triangles BCE and CDE. Similar to the previous step, they share the same altitude from vertex C to the line segment BD. Observe how their area ratio is also determined by the ratio of bases BE and DE. Drag vertex C to explore this relationship.",
|
| 26 |
+
"visualFocus": "Highlight triangles BCE and CDE. Show a dashed line representing the common altitude from C to BD. Display text: Area(BCE) / Area(CDE) = BE / DE."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"stepTitle": "Deriving the Key Relationship",
|
| 30 |
+
"instruction": "Since both pairs of triangles have area ratios equal to the same base ratio (BE/DE), we can equate these ratios. Observe the algebraic derivation that leads to a remarkable product relationship.",
|
| 31 |
+
"visualFocus": "Display the step-by-step algebraic derivation:\n1. Area(ABE) / Area(DAE) = BE / DE\n2. Area(BCE) / Area(CDE) = BE / DE\n3. From (1) and (2): Area(ABE) / Area(DAE) = Area(BCE) / Area(CDE)\n4. Cross-multiply: Area(ABE) * Area(CDE) = Area(BCE) * Area(DAE)\nHighlight the final product relationship."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"stepTitle": "Applying the Relationship",
|
| 35 |
+
"instruction": "Now, let's apply the derived property to the given problem. We know Area(ABE) = 6, Area(BCE) = 7, and Area(CDE) = 8. Substitute these values into the product relationship to find Area(DAE).",
|
| 36 |
+
"visualFocus": "Display the problem's given areas and the application of the formula:\nGiven: Area(ABE) = 6, Area(BCE) = 7, Area(CDE) = 8, Area(DAE) = ?\nFormula: Area(ABE) * Area(CDE) = Area(BCE) * Area(DAE)\nSubstitution: 6 * 8 = 7 * Area(DAE)\nCalculation: 48 = 7 * Area(DAE)\nResult: Area(DAE) = 48/7. Highlight the final answer."
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"visualSpec": {
|
| 40 |
+
"elements": [
|
| 41 |
+
"A convex quadrilateral ABCD with vertices A, B, C, D.",
|
| 42 |
+
"Diagonals AC and BD intersecting at point E.",
|
| 43 |
+
"Labels for points A, B, C, D, E.",
|
| 44 |
+
"Text labels for Area(ABE), Area(BCE), Area(CDE), Area(DAE).",
|
| 45 |
+
"Dashed lines representing altitudes from A to BD and C to BD.",
|
| 46 |
+
"Dynamic display of formulas and calculations."
|
| 47 |
+
],
|
| 48 |
+
"interactions": [
|
| 49 |
+
"Drag points A and C to dynamically change the shape of the quadrilateral and altitudes.",
|
| 50 |
+
"Click 'Next' button to advance through the steps."
|
| 51 |
+
],
|
| 52 |
+
"mathLogic": "Area of a triangle = (1/2) * base * height.\nRatio of areas of triangles sharing a common altitude is equal to the ratio of their bases.\nSpecifically, for triangles ABE and DAE sharing altitude from A to BD: Area(ABE)/Area(DAE) = BE/DE.\nFor triangles BCE and CDE sharing altitude from C to BD: Area(BCE)/Area(CDE) = BE/DE.\nTherefore, Area(ABE)/Area(DAE) = Area(BCE)/Area(CDE).\nThis implies Area(ABE) * Area(CDE) = Area(BCE) * Area(DAE).\nGiven values: Area(ABE)=6, Area(BCE)=7, Area(CDE)=8.\nCalculation: 6 * 8 = 7 * Area(DAE) => 48 = 7 * Area(DAE) => Area(DAE) = 48/7."
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"sourceCode": "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Area Ratios in Quadrilaterals</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-color: #e2e8f0;\n --accent-primary: #38bdf8;\n --accent-secondary: #f472b6;\n --border-color: #334155;\n --btn-hover: #475569;\n \n /* Triangle Colors */\n --col-abe: rgba(56, 189, 248, 0.3); /* Sky */\n --col-bce: rgba(74, 222, 128, 0.3); /* Green */\n --col-cde: rgba(248, 113, 113, 0.3); /* Red */\n --col-dae: rgba(250, 204, 21, 0.3); /* Yellow */\n }\n\n body {\n margin: 0;\n padding: 0;\n font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-color);\n display: flex;\n flex-direction: column;\n height: 100vh;\n overflow: hidden;\n }\n\n /* Layout */\n header {\n padding: 1rem 2rem;\n background-color: var(--panel-bg);\n border-bottom: 1px solid var(--border-color);\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-shrink: 0;\n }\n\n h1 {\n font-size: 1.25rem;\n margin: 0;\n font-weight: 600;\n color: #fff;\n }\n\n .container {\n display: flex;\n flex: 1;\n overflow: hidden;\n position: relative;\n }\n\n /* Canvas Area */\n .canvas-wrapper {\n flex: 2;\n position: relative;\n background-color: #0f172a;\n overflow: hidden;\n cursor: crosshair;\n }\n\n canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /* UI Panel */\n .ui-panel {\n flex: 1;\n min-width: 320px;\n max-width: 450px;\n background-color: var(--panel-bg);\n border-left: 1px solid var(--border-color);\n display: flex;\n flex-direction: column;\n padding: 1.5rem;\n box-sizing: border-box;\n overflow-y: auto;\n z-index: 10;\n }\n\n /* Steps & Content */\n .step-indicator {\n font-size: 0.85rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--accent-primary);\n margin-bottom: 0.5rem;\n font-weight: 700;\n }\n\n .step-title {\n font-size: 1.5rem;\n margin: 0 0 1rem 0;\n color: #fff;\n line-height: 1.2;\n }\n\n .instruction-box {\n background-color: rgba(255,255,255,0.05);\n border-radius: 8px;\n padding: 1rem;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-color);\n }\n\n .instruction-text {\n line-height: 1.6;\n font-size: 1rem;\n margin-bottom: 1rem;\n }\n\n .math-block {\n font-family: 'Courier New', monospace;\n background-color: #00000040;\n padding: 0.75rem;\n border-radius: 4px;\n margin: 0.5rem 0;\n border-left: 3px solid var(--accent-secondary);\n }\n\n .math-row {\n display: block;\n margin-bottom: 0.5rem;\n }\n .math-row:last-child { margin-bottom: 0; }\n\n .highlight { color: var(--accent-primary); font-weight: bold; }\n .highlight-res { color: var(--accent-secondary); font-weight: bold; }\n\n /* Controls */\n .controls {\n margin-top: auto;\n display: flex;\n gap: 1rem;\n padding-top: 1rem;\n border-top: 1px solid var(--border-color);\n }\n\n button {\n flex: 1;\n padding: 0.75rem;\n border: none;\n border-radius: 6px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s;\n }\n\n button.primary {\n background-color: var(--accent-primary);\n color: #0f172a;\n }\n button.primary:hover { background-color: #7dd3fc; }\n\n button.secondary {\n background-color: transparent;\n border: 1px solid var(--border-color);\n color: var(--text-color);\n }\n button.secondary:hover { background-color: var(--btn-hover); }\n\n button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Responsive */\n @media (max-width: 800px) {\n .container {\n flex-direction: column;\n }\n .canvas-wrapper {\n flex: 1;\n min-height: 40vh;\n }\n .ui-panel {\n flex: 1;\n max-width: 100%;\n min-width: 100%;\n border-left: none;\n border-top: 1px solid var(--border-color);\n }\n }\n \n /* Floating label styles on canvas */\n .tooltip {\n position: absolute;\n background: rgba(15, 23, 42, 0.8);\n padding: 4px 8px;\n border-radius: 4px;\n pointer-events: none;\n font-size: 12px;\n color: white;\n transform: translate(-50%, -100%);\n margin-top: -10px;\n white-space: nowrap;\n }\n\n </style>\n</head>\n<body>\n\n<header>\n <h1>Geometric Proofs: Area Ratios</h1>\n</header>\n\n<div class=\"container\">\n <!-- Graphics Area -->\n <div class=\"canvas-wrapper\" id=\"canvasContainer\">\n <canvas id=\"geoCanvas\"></canvas>\n <!-- Tooltips can be injected here dynamically -->\n </div>\n\n <!-- Instructions & Controls -->\n <div class=\"ui-panel\">\n <div>\n <div class=\"step-indicator\" id=\"stepIndicator\">Step 1 of 5</div>\n <h2 class=\"step-title\" id=\"stepTitle\">The Quadrilateral</h2>\n \n <div class=\"instruction-box\">\n <div id=\"stepContent\" class=\"instruction-text\">\n <!-- Dynamic Content -->\n </div>\n </div>\n </div>\n\n <div class=\"controls\">\n <button class=\"secondary\" id=\"prevBtn\" disabled>Previous</button>\n <button class=\"primary\" id=\"nextBtn\">Next</button>\n </div>\n </div>\n</div>\n\n<script>\n/**\n * APP STATE & CONFIGURATION\n */\nconst state = {\n step: 0,\n points: {\n A: { x: 200, y: 100, label: 'A', color: '#38bdf8' },\n B: { x: 150, y: 400, label: 'B', color: '#e2e8f0' },\n C: { x: 500, y: 350, label: 'C', color: '#f472b6' },\n D: { x: 450, y: 150, label: 'D', color: '#e2e8f0' }\n },\n draggingPoint: null,\n intersection: { x: 0, y: 0, label: 'E' },\n canvas: null,\n ctx: null,\n width: 0,\n height: 0,\n dpr: 1\n};\n\n// Specific Problem Data for Step 5\nconst problemData = {\n areaABE: 6,\n areaBCE: 7,\n areaCDE: 8,\n areaDAE: 48/7 // approx 6.86\n};\n\nconst steps = [\n {\n title: \"The Quadrilateral and Diagonals\",\n content: `\n <p>Observe the convex quadrilateral <strong>ABCD</strong>.</p>\n <p>The diagonals <span style=\"color:orange\">AC</span> and <span style=\"color:orange\">BD</span> intersect at point <strong>E</strong>.</p>\n <p>This intersection divides the quadrilateral into four triangles: <br>\n <span style=\"color:#38bdf8\">ABE</span>, \n <span style=\"color:#4ade80\">BCE</span>, \n <span style=\"color:#f87171\">CDE</span>, and \n <span style=\"color:#facc15\">DAE</span>.</p>\n <p><em>Interaction: Drag vertices A, B, C, or D to resize.</em></p>\n `\n },\n {\n title: \"Shared Altitude from Vertex A\",\n content: `\n <p>Focus on <span style=\"color:#38bdf8\">△ABE</span> and <span style=\"color:#facc15\">△DAE</span>.</p>\n <p>Both triangles share the same altitude ($h_a$) from vertex <strong>A</strong> to the diagonal line segment BD.</p>\n <p>Because they have the same height, the ratio of their areas equals the ratio of their bases:</p>\n <div class=\"math-block\">\n Area(ABE) / Area(DAE) = BE / DE\n </div>\n <p><em>Interaction: Drag Vertex A to see the altitude change.</em></p>\n `\n },\n {\n title: \"Shared Altitude from Vertex C\",\n content: `\n <p>Now look at <span style=\"color:#4ade80\">△BCE</span> and <span style=\"color:#f87171\">△CDE</span>.</p>\n <p>These triangles share a common altitude ($h_c$) from vertex <strong>C</strong> to the diagonal BD.</p>\n <p>Similarly, their area ratio depends only on their bases:</p>\n <div class=\"math-block\">\n Area(BCE) / Area(CDE) = BE / DE\n </div>\n <p><em>Interaction: Drag Vertex C.</em></p>\n `\n },\n {\n title: \"Deriving the Key Relationship\",\n content: `\n <p>Both pairs of triangles have area ratios equal to the same base ratio ($BE / DE$).</p>\n <div class=\"math-block\">\n <span class=\"math-row\">1. Area(ABE)/Area(DAE) = BE/DE</span>\n <span class=\"math-row\">2. Area(BCE)/Area(CDE) = BE/DE</span>\n <span class=\"math-row\" style=\"border-top: 1px solid #777; margin-top:4px; padding-top:4px\">Therefore:</span>\n <span class=\"math-row\">Area(ABE)/Area(DAE) = Area(BCE)/Area(CDE)</span>\n </div>\n <p>Cross-multiplying gives us the <span class=\"highlight\">Product Property</span>:</p>\n <div class=\"math-block\" style=\"border-color: #facc15;\">\n Area(ABE) × Area(CDE) = Area(BCE) × Area(DAE)\n </div>\n <p><em>Opposite triangles have equal area products!</em></p>\n `\n },\n {\n title: \"Applying the Relationship\",\n content: `\n <p>Let's solve a problem using this property.</p>\n <p><strong>Given:</strong><br> \n Area(ABE) = 6, Area(BCE) = 7, Area(CDE) = 8.</p>\n <div class=\"math-block\">\n <span class=\"math-row\">Formula: ABE × CDE = BCE × DAE</span>\n <span class=\"math-row\">Sub: 6 × 8 = 7 × DAE</span>\n <span class=\"math-row\">48 = 7 × DAE</span>\n <span class=\"math-row\" class=\"highlight-res\">DAE = 48/7 ≈ 6.86</span>\n </div>\n `\n }\n];\n\n/**\n * INITIALIZATION\n */\nfunction init() {\n state.canvas = document.getElementById('geoCanvas');\n state.ctx = state.canvas.getContext('2d');\n \n // Set up event listeners\n window.addEventListener('resize', handleResize);\n state.canvas.addEventListener('mousedown', handleMouseDown);\n state.canvas.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n \n // Touch support\n state.canvas.addEventListener('touchstart', handleTouchStart, {passive: false});\n state.canvas.addEventListener('touchmove', handleTouchMove, {passive: false});\n window.addEventListener('touchend', handleMouseUp);\n\n document.getElementById('nextBtn').addEventListener('click', () => setStep(state.step + 1));\n document.getElementById('prevBtn').addEventListener('click', () => setStep(state.step - 1));\n\n // Initial sizing\n handleResize();\n \n // Initial Layout setup (center the quad approximately)\n centerQuadrilateral();\n \n updateUI();\n requestAnimationFrame(draw);\n}\n\nfunction handleResize() {\n const container = document.getElementById('canvasContainer');\n state.width = container.clientWidth;\n state.height = container.clientHeight;\n state.dpr = window.devicePixelRatio || 1;\n \n state.canvas.width = state.width * state.dpr;\n state.canvas.height = state.height * state.dpr;\n \n state.ctx.scale(state.dpr, state.dpr);\n \n // Re-draw immediately\n draw();\n}\n\nfunction centerQuadrilateral() {\n // Center points relative to canvas size initially\n const cx = state.width / 2;\n const cy = state.height / 2;\n const scale = Math.min(state.width, state.height) / 3;\n\n state.points.A = { x: cx - 0.5*scale, y: cy - 0.8*scale, label: 'A', color: '#38bdf8' };\n state.points.B = { x: cx - 0.8*scale, y: cy + 0.6*scale, label: 'B', color: '#e2e8f0' };\n state.points.C = { x: cx + 0.8*scale, y: cy + 0.4*scale, label: 'C', color: '#f472b6' };\n state.points.D = { x: cx + 0.6*scale, y: cy - 0.6*scale, label: 'D', color: '#e2e8f0' };\n}\n\n/**\n * LOGIC & MATH\n */\n\nfunction getIntersection(p1, p2, p3, p4) {\n // Line AB represented as a1x + b1y = c1\n const a1 = p2.y - p1.y;\n const b1 = p1.x - p2.x;\n const c1 = a1 * p1.x + b1 * p1.y;\n\n // Line CD represented as a2x + b2y = c2\n const a2 = p4.y - p3.y;\n const b2 = p3.x - p4.x;\n const c2 = a2 * p3.x + b2 * p3.y;\n\n const determinant = a1 * b2 - a2 * b1;\n\n if (Math.abs(determinant) < 0.001) {\n return null; // Parallel lines\n } else {\n const x = (b2 * c1 - b1 * c2) / determinant;\n const y = (a1 * c2 - a2 * c1) / determinant;\n return { x: x, y: y };\n }\n}\n\nfunction projectPointOnLine(p, lineStart, lineEnd) {\n const dx = lineEnd.x - lineStart.x;\n const dy = lineEnd.y - lineStart.y;\n const lenSq = dx*dx + dy*dy;\n if (lenSq === 0) return lineStart;\n \n const t = ((p.x - lineStart.x) * dx + (p.y - lineStart.y) * dy) / lenSq;\n return {\n x: lineStart.x + t * dx,\n y: lineStart.y + t * dy\n };\n}\n\n// Standard Shoelace formula for triangle area\nfunction triangleArea(p1, p2, p3) {\n return 0.5 * Math.abs(\n p1.x * (p2.y - p3.y) +\n p2.x * (p3.y - p1.y) +\n p3.x * (p1.y - p2.y)\n );\n}\n\n/**\n * DRAWING ENGINE\n */\nfunction draw() {\n const ctx = state.ctx;\n const w = state.width;\n const h = state.height;\n\n // Clear\n ctx.clearRect(0, 0, w, h);\n\n const A = state.points.A;\n const B = state.points.B;\n const C = state.points.C;\n const D = state.points.D;\n\n // Calculate Intersection E\n const E_pt = getIntersection(A, C, B, D);\n \n // If quadrilateral is not convex or bowtie causing E to be outside segments, \n // we handle strictly, but for visualization, we just need E on the lines.\n // Checking if E is within the bounds of AC and BD segments\n let valid = false;\n if (E_pt) {\n const minX = Math.min(A.x, C.x) - 0.1; const maxX = Math.max(A.x, C.x) + 0.1;\n const minY = Math.min(A.y, C.y) - 0.1; const maxY = Math.max(A.y, C.y) + 0.1;\n if(E_pt.x >= minX && E_pt.x <= maxX && E_pt.y >= minY && E_pt.y <= maxY) {\n valid = true;\n state.intersection = { x: E_pt.x, y: E_pt.y, label: 'E' };\n }\n }\n\n // Draw Diagonals\n ctx.strokeStyle = '#64748b';\n ctx.lineWidth = 1;\n ctx.setLineDash([5, 5]);\n ctx.beginPath();\n ctx.moveTo(A.x, A.y); ctx.lineTo(C.x, C.y);\n ctx.moveTo(B.x, B.y); ctx.lineTo(D.x, D.y);\n ctx.stroke();\n ctx.setLineDash([]);\n\n // Draw Quadrilateral Outline\n ctx.strokeStyle = '#fff';\n ctx.lineWidth = 2;\n ctx.beginPath();\n ctx.moveTo(A.x, A.y);\n ctx.lineTo(B.x, B.y);\n ctx.lineTo(C.x, C.y);\n ctx.lineTo(D.x, D.y);\n ctx.closePath();\n ctx.stroke();\n\n if (!valid) {\n ctx.fillStyle = 'rgba(255, 50, 50, 0.2)';\n ctx.fillText(\"Quadrilateral must be convex\", 20, 30);\n drawPoints([A, B, C, D]);\n requestAnimationFrame(draw);\n return;\n }\n\n const E = state.intersection;\n\n // Define Triangles\n const tris = [\n { p: [A, B, E], color: 'rgba(56, 189, 248, 0.3)', id: 'ABE', val: triangleArea(A, B, E) },\n { p: [B, C, E], color: 'rgba(74, 222, 128, 0.3)', id: 'BCE', val: triangleArea(B, C, E) },\n { p: [C, D, E], color: 'rgba(248, 113, 113, 0.3)', id: 'CDE', val: triangleArea(C, D, E) },\n { p: [D, A, E], color: 'rgba(250, 204, 21, 0.3)', id: 'DAE', val: triangleArea(D, A, E) }\n ];\n\n // Step Specific Rendering\n \n // Step 0: Show all\n if (state.step === 0) {\n tris.forEach(t => fillTri(ctx, t.p, t.color));\n tris.forEach(t => drawAreaLabel(ctx, t.p, `Area(${t.id})`));\n }\n\n // Step 1: ABE and DAE (Altitude A)\n if (state.step === 1) {\n // Highlight\n fillTri(ctx, tris[0].p, tris[0].color); // ABE\n fillTri(ctx, tris[3].p, tris[3].color); // DAE\n // Dim others\n fillTri(ctx, tris[1].p, 'rgba(255,255,255,0.03)');\n fillTri(ctx, tris[2].p, 'rgba(255,255,255,0.03)');\n\n // Altitude\n const foot = projectPointOnLine(A, B, D);\n drawDashedLine(ctx, A, foot, '#38bdf8');\n drawPoint(ctx, foot, '', 3, '#38bdf8');\n \n // Label Altitude\n ctx.fillStyle = '#38bdf8';\n ctx.font = 'italic 14px serif';\n ctx.fillText('h', (A.x + foot.x)/2 + 10, (A.y + foot.y)/2);\n ctx.font = '10px sans-serif';\n ctx.fillText('a', (A.x + foot.x)/2 + 18, (A.y + foot.y)/2 + 5);\n\n // Labels\n drawAreaLabel(ctx, tris[0].p, \"Area(ABE)\");\n drawAreaLabel(ctx, tris[3].p, \"Area(DAE)\");\n }\n\n // Step 2: BCE and CDE (Altitude C)\n if (state.step === 2) {\n fillTri(ctx, tris[1].p, tris[1].color); // BCE\n fillTri(ctx, tris[2].p, tris[2].color); // CDE\n fillTri(ctx, tris[0].p, 'rgba(255,255,255,0.03)');\n fillTri(ctx, tris[3].p, 'rgba(255,255,255,0.03)');\n\n const foot = projectPointOnLine(C, B, D);\n drawDashedLine(ctx, C, foot, '#f472b6');\n drawPoint(ctx, foot, '', 3, '#f472b6');\n\n ctx.fillStyle = '#f472b6';\n ctx.font = 'italic 14px serif';\n ctx.fillText('h', (C.x + foot.x)/2 + 10, (C.y + foot.y)/2);\n ctx.font = '10px sans-serif';\n ctx.fillText('c', (C.x + foot.x)/2 + 18, (C.y + foot.y)/2 + 5);\n\n drawAreaLabel(ctx, tris[1].p, \"Area(BCE)\");\n drawAreaLabel(ctx, tris[2].p, \"Area(CDE)\");\n }\n\n // Step 3: Derivation (Show all, highlight opposites)\n if (state.step === 3) {\n tris.forEach(t => fillTri(ctx, t.p, t.color));\n // Draw arrows or just static?\n // Just label nicely\n tris.forEach(t => drawAreaLabel(ctx, t.p, t.id));\n }\n\n // Step 4: Problem Solving\n if (state.step === 4) {\n tris.forEach(t => fillTri(ctx, t.p, t.color));\n // Override real area labels with Problem Data\n drawAreaLabel(ctx, tris[0].p, problemData.areaABE.toString(), true); // ABE\n drawAreaLabel(ctx, tris[1].p, problemData.areaBCE.toString(), true); // BCE\n drawAreaLabel(ctx, tris[2].p, problemData.areaCDE.toString(), true); // CDE\n drawAreaLabel(ctx, tris[3].p, \"?\", true); // DAE\n }\n\n // Draw Points Labels (Always on top)\n drawPoints([A, B, C, D, E]);\n\n requestAnimationFrame(draw);\n}\n\nfunction fillTri(ctx, pts, color) {\n ctx.fillStyle = color;\n ctx.beginPath();\n ctx.moveTo(pts[0].x, pts[0].y);\n ctx.lineTo(pts[1].x, pts[1].y);\n ctx.lineTo(pts[2].x, pts[2].y);\n ctx.closePath();\n ctx.fill();\n}\n\nfunction drawDashedLine(ctx, p1, p2, color) {\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.setLineDash([4, 4]);\n ctx.beginPath();\n ctx.moveTo(p1.x, p1.y);\n ctx.lineTo(p2.x, p2.y);\n ctx.stroke();\n ctx.restore();\n}\n\nfunction drawPoint(ctx, p, label, r=5, color='#fff') {\n ctx.beginPath();\n ctx.arc(p.x, p.y, r, 0, Math.PI * 2);\n ctx.fillStyle = color;\n ctx.fill();\n ctx.strokeStyle = '#0f172a';\n ctx.lineWidth = 2;\n ctx.stroke();\n\n if (label) {\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 14px sans-serif';\n const offset = 15;\n // Simple offset logic to push label away from center\n let dx = 0, dy = 0;\n if (p.x < state.width/2) dx = -offset; else dx = offset;\n if (p.y < state.height/2) dy = -offset; else dy = offset;\n \n // Specific overrides for clear E label\n if (label === 'E') { dy = -15; dx = 0; }\n\n ctx.fillText(label, p.x + dx - 4, p.y + dy + 4);\n }\n}\n\nfunction drawPoints(points) {\n points.forEach(p => {\n drawPoint(state.ctx, p, p.label, 6, p.color || '#fff');\n });\n}\n\nfunction drawAreaLabel(ctx, pts, text, isBold = false) {\n // Centroid\n const cx = (pts[0].x + pts[1].x + pts[2].x) / 3;\n const cy = (pts[0].y + pts[1].y + pts[2].y) / 3;\n \n ctx.fillStyle = '#fff';\n ctx.font = isBold ? 'bold 16px sans-serif' : '12px sans-serif';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n \n // Shadow for readability\n ctx.shadowColor = 'rgba(0,0,0,0.8)';\n ctx.shadowBlur = 4;\n ctx.fillText(text, cx, cy);\n ctx.shadowBlur = 0;\n}\n\n\n/**\n * INTERACTION HANDLERS\n */\n\nfunction getMousePos(evt) {\n const rect = state.canvas.getBoundingClientRect();\n const clientX = evt.clientX || (evt.touches ? evt.touches[0].clientX : 0);\n const clientY = evt.clientY || (evt.touches ? evt.touches[0].clientY : 0);\n return {\n x: (clientX - rect.left) * (state.canvas.width / rect.width / state.dpr),\n y: (clientY - rect.top) * (state.canvas.height / rect.height / state.dpr)\n };\n}\n\nfunction dist(p1, p2) {\n return Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2);\n}\n\nfunction handleMouseDown(e) {\n const m = getMousePos(e);\n const points = [state.points.A, state.points.B, state.points.C, state.points.D];\n \n // Find closest point within radius\n for (let p of points) {\n if (dist(m, p) < 20) {\n state.draggingPoint = p;\n break;\n }\n }\n}\n\nfunction handleMouseMove(e) {\n if (state.draggingPoint) {\n const m = getMousePos(e);\n \n // Keep inside canvas roughly\n m.x = Math.max(10, Math.min(state.width - 10, m.x));\n m.y = Math.max(10, Math.min(state.height - 10, m.y));\n\n state.draggingPoint.x = m.x;\n state.draggingPoint.y = m.y;\n } else {\n // Hover effect cursor\n const m = getMousePos(e);\n const points = [state.points.A, state.points.B, state.points.C, state.points.D];\n let hovering = false;\n for (let p of points) {\n if (dist(m, p) < 20) hovering = true;\n }\n state.canvas.style.cursor = hovering ? 'move' : 'default';\n }\n}\n\nfunction handleMouseUp() {\n state.draggingPoint = null;\n}\n\nfunction handleTouchStart(e) {\n if(e.target === state.canvas) e.preventDefault();\n handleMouseDown(e);\n}\n\nfunction handleTouchMove(e) {\n if(e.target === state.canvas) e.preventDefault();\n handleMouseMove(e);\n}\n\n\n/**\n * UI CONTROL\n */\nfunction setStep(n) {\n if (n < 0 || n >= steps.length) return;\n state.step = n;\n updateUI();\n}\n\nfunction updateUI() {\n // Update Text\n document.getElementById('stepIndicator').innerText = `Step ${state.step + 1} of ${steps.length}`;\n document.getElementById('stepTitle').innerText = steps[state.step].title;\n document.getElementById('stepContent').innerHTML = steps[state.step].content;\n\n // Update Buttons\n const prevBtn = document.getElementById('prevBtn');\n const nextBtn = document.getElementById('nextBtn');\n \n prevBtn.disabled = state.step === 0;\n \n if (state.step === steps.length - 1) {\n nextBtn.innerText = 'Restart';\n nextBtn.onclick = () => setStep(0);\n } else {\n nextBtn.innerText = 'Next';\n nextBtn.onclick = () => setStep(state.step + 1);\n }\n}\n\n// Start\ninit();\n\n</script>\n</body>\n</html>\n"
|
| 56 |
+
}
|
examples/005-visual-proof-sum-of-rhombus-diagonals.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-24T19:41:49.518Z",
|
| 4 |
+
"input": {
|
| 5 |
+
"mode": "text",
|
| 6 |
+
"text": "Area of a Rhombus of perimeter 56 cms is 100 sq cms. Find the sum of the lengths of its diagonals."
|
| 7 |
+
},
|
| 8 |
+
"concept": {
|
| 9 |
+
"conceptTitle": "Sum of Rhombus Diagonals",
|
| 10 |
+
"educationalGoal": "To understand how to calculate the sum of the diagonals of a rhombus given its perimeter and area, by applying properties of a rhombus, the Pythagorean theorem, and an algebraic identity.",
|
| 11 |
+
"explanation": "A **rhombus** is a fascinating four-sided shape where all four sides are equal in length. Its diagonals have a special relationship: they **bisect each other at right angles**. This creates four identical right-angled triangles inside the rhombus! We're going to use this property, along with the area formula and a key algebraic identity, to solve our problem.",
|
| 12 |
+
"steps": [
|
| 13 |
+
{
|
| 14 |
+
"stepTitle": "1. Understand the Rhombus and its Side",
|
| 15 |
+
"instruction": "A rhombus has four equal sides. Given the perimeter is 56 cm, calculate the length of one side. Click Next to reveal the side length.",
|
| 16 |
+
"visualFocus": "A rhombus with all four sides labeled 's'. Highlight one side 's' and the perimeter calculation (Perimeter = 4s)."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"stepTitle": "2. Area and the Product of Diagonals",
|
| 20 |
+
"instruction": "The area of a rhombus is half the product of its diagonals. Given the area is 100 sq cm, find the product of the diagonals (d1 * d2). Click Next to see the calculation.",
|
| 21 |
+
"visualFocus": "The rhombus with its two diagonals d1 and d2 drawn. Display the area formula: Area = (1/2) * d1 * d2. Highlight the calculation of d1 * d2."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"stepTitle": "3. Diagonals, Side, and Pythagoras",
|
| 25 |
+
"instruction": "The diagonals of a rhombus bisect each other at right angles. This creates four right-angled triangles. Focus on one of these triangles: its legs are half the diagonals (d1/2, d2/2) and its hypotenuse is the side (s) of the rhombus. Apply the Pythagorean theorem. Click Next to see how this relates d1, d2, and s.",
|
| 26 |
+
"visualFocus": "The rhombus with diagonals. Highlight one of the four right-angled triangles formed at the center. Label its legs as d1/2 and d2/2, and its hypotenuse as 's'. Show the Pythagorean theorem: (d1/2)^2 + (d2/2)^2 = s^2, and its simplified form: d1^2 + d2^2 = 4s^2."
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"stepTitle": "4. Connecting with Algebra: Finding the Sum",
|
| 30 |
+
"instruction": "Now we have values for 'd1 * d2' and 'd1^2 + d2^2'. We want to find 'd1 + d2'. Recall the algebraic identity: (a + b)^2 = a^2 + b^2 + 2ab. Substitute our values for 'a' as d1 and 'b' as d2. Click Next to complete the final calculation!",
|
| 31 |
+
"visualFocus": "Display the algebraic identity (d1 + d2)^2 = d1^2 + d2^2 + 2d1d2. Show the substitution of the calculated values for d1^2 + d2^2 and 2 * d1 * d2. Highlight the final calculation steps leading to d1 + d2."
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"visualSpec": {
|
| 35 |
+
"elements": [
|
| 36 |
+
"A dynamic rhombus shape, labeled ABCD.",
|
| 37 |
+
"Sides of the rhombus labeled 's'.",
|
| 38 |
+
"Diagonals of the rhombus, labeled 'd1' (longer) and 'd2' (shorter).",
|
| 39 |
+
"A right-angle symbol at the intersection of the diagonals.",
|
| 40 |
+
"Highlighting capability for one of the four right-angled triangles formed by the diagonals and sides.",
|
| 41 |
+
"Text display areas for: Perimeter, Area, Side Length (s), d1*d2, d1^2+d2^2, (d1+d2)^2, and d1+d2."
|
| 42 |
+
],
|
| 43 |
+
"interactions": [
|
| 44 |
+
"A 'Next' button to advance through the steps.",
|
| 45 |
+
"Potentially a 'Reset' button to restart the proof from step 1."
|
| 46 |
+
],
|
| 47 |
+
"mathLogic": "1. Side Length (s): s = Perimeter / 4. 2. Product of Diagonals: d1 * d2 = 2 * Area. 3. Pythagorean Theorem: (d1/2)^2 + (d2/2)^2 = s^2, which simplifies to d1^2 + d2^2 = 4s^2. 4. Algebraic Identity: (d1 + d2)^2 = d1^2 + d2^2 + 2 * (d1 * d2). 5. Final Answer: d1 + d2 = sqrt((d1 + d2)^2)."
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"sourceCode": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Sum of Rhombus Diagonals Visualization</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-color: #e2e8f0;\n --accent-cyan: #38bdf8;\n --accent-pink: #f472b6;\n --accent-yellow: #facc15;\n --accent-green: #4ade80;\n --border-color: #334155;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-color);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n header {\n padding: 1rem 2rem;\n border-bottom: 1px solid var(--border-color);\n background-color: var(--panel-bg);\n flex-shrink: 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--accent-cyan);\n }\n\n .container {\n display: flex;\n flex: 1;\n overflow: hidden;\n flex-direction: row; /* Side by side layout */\n }\n\n /* Canvas Area */\n #canvas-wrapper {\n flex: 3;\n position: relative;\n background-color: #0f172a; /* Darker distinct from panel */\n display: flex;\n justify-content: center;\n align-items: center;\n overflow: hidden;\n }\n\n canvas {\n display: block;\n }\n\n /* Info Panel Area */\n #info-panel {\n flex: 2;\n background-color: var(--panel-bg);\n border-left: 1px solid var(--border-color);\n padding: 2rem;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n overflow-y: auto;\n min-width: 320px;\n box-shadow: -4px 0 15px rgba(0,0,0,0.3);\n }\n\n .step-indicator {\n font-size: 0.85rem;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: #94a3b8;\n margin-bottom: 0.5rem;\n }\n\n .step-title {\n font-size: 1.5rem;\n font-weight: 700;\n margin-bottom: 1.5rem;\n color: var(--text-color);\n line-height: 1.3;\n }\n\n .instruction-box {\n background-color: rgba(255, 255, 255, 0.05);\n border-radius: 8px;\n padding: 1.5rem;\n margin-bottom: 2rem;\n border: 1px solid var(--border-color);\n }\n\n .math-block {\n font-family: 'Courier New', Courier, monospace;\n background-color: rgba(0, 0, 0, 0.3);\n padding: 1rem;\n border-radius: 4px;\n margin: 1rem 0;\n border-left: 4px solid var(--accent-cyan);\n font-size: 1.1rem;\n line-height: 1.6;\n }\n\n .highlight-text {\n color: var(--accent-yellow);\n font-weight: bold;\n }\n\n .variable {\n font-style: italic;\n font-family: serif;\n }\n\n /* Controls */\n .controls {\n margin-top: auto;\n display: flex;\n gap: 1rem;\n padding-top: 1rem;\n border-top: 1px solid var(--border-color);\n }\n\n button {\n flex: 1;\n padding: 0.75rem 1.5rem;\n border: none;\n border-radius: 6px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n }\n\n button.prev {\n background-color: transparent;\n border: 1px solid var(--border-color);\n color: var(--text-color);\n }\n\n button.prev:hover:not(:disabled) {\n background-color: rgba(255, 255, 255, 0.1);\n }\n\n button.next {\n background-color: var(--accent-cyan);\n color: #0f172a;\n }\n\n button.next:hover:not(:disabled) {\n background-color: #7dd3fc;\n transform: translateY(-1px);\n }\n\n button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n }\n\n /* Responsive Adjustments */\n @media (max-width: 768px) {\n .container {\n flex-direction: column;\n }\n \n #canvas-wrapper {\n flex: 1;\n min-height: 300px;\n }\n\n #info-panel {\n flex: 1;\n border-left: none;\n border-top: 1px solid var(--border-color);\n min-width: 100%;\n }\n }\n </style>\n</head>\n<body>\n\n <header>\n <h1>Math Visualizer</h1>\n <div style=\"font-size: 0.9rem; color: #94a3b8;\">Rhombus Properties</div>\n </header>\n\n <div class=\"container\">\n <div id=\"canvas-wrapper\">\n <canvas id=\"geometryCanvas\"></canvas>\n </div>\n\n <aside id=\"info-panel\">\n <div class=\"step-indicator\">Step <span id=\"step-number\">1</span> of 4</div>\n <h2 class=\"step-title\" id=\"step-title\">Title</h2>\n \n <div class=\"instruction-box\">\n <p id=\"step-desc\">Description goes here.</p>\n <div id=\"math-content\" class=\"math-block\">\n <!-- Math formulas inserted via JS -->\n </div>\n </div>\n\n <div class=\"controls\">\n <button id=\"btn-prev\" class=\"prev\">Previous</button>\n <button id=\"btn-next\" class=\"next\">Next</button>\n </div>\n </aside>\n </div>\n\n <script>\n /**\n * APPLICATION STATE\n */\n const state = {\n currentStep: 0,\n perimeter: 56,\n area: 100,\n side: 14, // 56 / 4\n productDiagonals: 200, // 100 * 2\n sumSquares: 784, // 4 * 14^2\n sumDiagonalsSq: 1184, // 784 + 2*200\n finalAnswer: 34.41 // sqrt(1184)\n };\n\n /**\n * STEP DATA\n */\n const steps = [\n {\n title: \"Understand the Rhombus and its Side\",\n desc: \"A rhombus has four equal sides. We are given the Perimeter = 56 cm. Since all sides are equal, we can find the length of one side (s).\",\n math: `Perimeter = 4<span class=\"variable\">s</span> = 56<br>\n <span class=\"variable\">s</span> = 56 / 4<br>\n <span class=\"highlight-text\"><span class=\"variable\">s</span> = 14 cm</span>`,\n drawState: 'step1'\n },\n {\n title: \"Area and the Product of Diagonals\",\n desc: \"The area of a rhombus is half the product of its diagonals (<span class='variable'>d₁</span> and <span class='variable'>d₂</span>). Given Area = 100 sq cm, let's find <span class='variable'>d₁</span> × <span class='variable'>d₂</span>.\",\n math: `Area = ½ × <span class=\"variable\">d₁</span> × <span class=\"variable\">d₂</span> = 100<br>\n <span class=\"variable\">d₁</span> × <span class=\"variable\">d₂</span> = 100 × 2<br>\n <span class=\"highlight-text\"><span class=\"variable\">d₁</span><span class=\"variable\">d₂</span> = 200</span>`,\n drawState: 'step2'\n },\n {\n title: \"Diagonals, Side, and Pythagoras\",\n desc: \"Diagonals bisect each other at 90°. This forms four right-angled triangles. Looking at one triangle, the legs are half the diagonals, and the hypotenuse is side <span class='variable'>s</span>.\",\n math: `(<span class=\"variable\">d₁</span>/2)² + (<span class=\"variable\">d₂</span>/2)² = <span class=\"variable\">s</span>²<br>\n <span class=\"variable\">d₁</span>²/4 + <span class=\"variable\">d₂</span>²/4 = <span class=\"variable\">s</span>²<br>\n <span class=\"variable\">d₁</span>² + <span class=\"variable\">d₂</span>² = 4<span class=\"variable\">s</span>²<br>\n Substitute <span class=\"variable\">s</span>=14: 4(196)<br>\n <span class=\"highlight-text\"><span class=\"variable\">d₁</span>² + <span class=\"variable\">d₂</span>² = 784</span>`,\n drawState: 'step3'\n },\n {\n title: \"Connecting with Algebra: Finding the Sum\",\n desc: \"We need <span class='variable'>d₁</span> + <span class='variable'>d₂</span>. We know <span class='variable'>d₁</span>² + <span class='variable'>d₂</span>² and <span class='variable'>d₁</span><span class='variable'>d₂</span>. We use the algebraic identity <span class='variable'>(a+b)² = a² + b² + 2ab</span>.\",\n math: `(<span class=\"variable\">d₁</span>+<span class=\"variable\">d₂</span>)² = (<span class=\"variable\">d₁</span>² + <span class=\"variable\">d₂</span>²) + 2(<span class=\"variable\">d₁</span><span class=\"variable\">d₂</span>)<br>\n Substitute values:<br>\n (<span class=\"variable\">d₁</span>+<span class=\"variable\">d₂</span>)² = 784 + 2(200) = 1184<br>\n <span class=\"variable\">d₁</span>+<span class=\"variable\">d₂</span> = √1184<br>\n <span class=\"highlight-text\">Sum ≈ 34.41 cm</span>`,\n drawState: 'step4'\n }\n ];\n\n /**\n * DOM ELEMENTS\n */\n const canvas = document.getElementById('geometryCanvas');\n const ctx = canvas.getContext('2d');\n const wrapper = document.getElementById('canvas-wrapper');\n \n // UI Elements\n const els = {\n stepNum: document.getElementById('step-number'),\n title: document.getElementById('step-title'),\n desc: document.getElementById('step-desc'),\n math: document.getElementById('math-content'),\n prev: document.getElementById('btn-prev'),\n next: document.getElementById('btn-next')\n };\n\n /**\n * INITIALIZATION\n */\n function init() {\n window.addEventListener('resize', handleResize);\n els.prev.addEventListener('click', () => changeStep(-1));\n els.next.addEventListener('click', () => changeStep(1));\n handleResize(); // Initial draw\n updateUI();\n }\n\n function handleResize() {\n const rect = wrapper.getBoundingClientRect();\n canvas.width = rect.width;\n canvas.height = rect.height;\n draw();\n }\n\n function changeStep(delta) {\n const newStep = state.currentStep + delta;\n if (newStep >= 0 && newStep < steps.length) {\n state.currentStep = newStep;\n updateUI();\n draw();\n }\n }\n\n function updateUI() {\n const step = steps[state.currentStep];\n els.stepNum.textContent = state.currentStep + 1;\n els.title.textContent = step.title;\n els.desc.innerHTML = step.desc;\n els.math.innerHTML = step.math;\n\n els.prev.disabled = state.currentStep === 0;\n els.next.disabled = state.currentStep === steps.length - 1;\n \n // Change button text on last step\n if (state.currentStep === steps.length - 1) {\n els.next.textContent = \"Done\";\n } else {\n els.next.textContent = \"Next\";\n }\n }\n\n /**\n * DRAWING LOGIC\n */\n function draw() {\n const width = canvas.width;\n const height = canvas.height;\n const cx = width / 2;\n const cy = height / 2;\n\n // Clear\n ctx.clearRect(0, 0, width, height);\n \n // Config for the rhombus visualization\n // To make it look nice, we pick a scale that fits. \n // Given s=14, let's assume d1 is roughly 24 and d2 roughly 11 (fits math approx).\n // We scale these logic units to pixels.\n \n const scale = Math.min(width, height) / 30; \n // Visual dimensions (not strictly 1:1 to math to ensure visibility of labels)\n const d1_vis = 24; \n const d2_vis = 12; \n \n const halfW = (d1_vis * scale) / 2;\n const halfH = (d2_vis * scale) / 2;\n\n // Vertices\n const top = { x: cx, y: cy - halfH };\n const right = { x: cx + halfW, y: cy };\n const bottom = { x: cx, y: cy + halfH };\n const left = { x: cx - halfW, y: cy };\n\n // Save context for transformations\n ctx.save();\n\n // 1. Draw Rhombus Shape\n ctx.beginPath();\n ctx.moveTo(top.x, top.y);\n ctx.lineTo(right.x, right.y);\n ctx.lineTo(bottom.x, bottom.y);\n ctx.lineTo(left.x, left.y);\n ctx.closePath();\n \n ctx.lineWidth = 3;\n ctx.strokeStyle = '#38bdf8'; // cyan\n ctx.stroke();\n ctx.fillStyle = 'rgba(56, 189, 248, 0.05)';\n ctx.fill();\n\n // 2. Corner Labels (A, B, C, D)\n drawLabel(\"A\", top.x, top.y - 20, \"#94a3b8\");\n drawLabel(\"B\", right.x + 20, right.y, \"#94a3b8\");\n drawLabel(\"C\", bottom.x, bottom.y + 20, \"#94a3b8\");\n drawLabel(\"D\", left.x - 20, left.y, \"#94a3b8\");\n\n const s = state.currentStep;\n\n // Step 1: Side Lengths\n if (s === 0) {\n drawLabel(\"s\", (top.x + right.x)/2 + 10, (top.y + right.y)/2 - 10, \"#facc15\");\n drawLabel(\"s\", (right.x + bottom.x)/2 + 10, (right.y + bottom.y)/2 + 10, \"#facc15\");\n drawLabel(\"s\", (bottom.x + left.x)/2 - 10, (bottom.y + left.y)/2 + 10, \"#facc15\");\n drawLabel(\"s\", (left.x + top.x)/2 - 10, (left.y + top.y)/2 - 10, \"#facc15\");\n }\n\n // Step 2, 3, 4: Diagonals\n if (s >= 1) {\n // Draw Diagonals\n ctx.beginPath();\n ctx.moveTo(top.x, top.y);\n ctx.lineTo(bottom.x, bottom.y);\n ctx.moveTo(left.x, left.y);\n ctx.lineTo(right.x, right.y);\n ctx.lineWidth = 2;\n ctx.strokeStyle = '#f472b6'; // pink\n ctx.setLineDash([5, 5]);\n ctx.stroke();\n ctx.setLineDash([]);\n\n // Labels for Diagonals\n // d1 is horizontal (left-right), d2 is vertical (top-bottom)\n if (s === 1) {\n drawLabel(\"d₁\", left.x + 20, left.y - 10, \"#f472b6\");\n drawLabel(\"d₂\", top.x + 10, top.y + 30, \"#f472b6\");\n }\n }\n\n // Step 3: Highlight Triangle & Pythagoras\n if (s >= 2) {\n // Highlight top-right triangle\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.lineTo(cx, top.y);\n ctx.lineTo(right.x, right.y);\n ctx.lineTo(cx, cy);\n ctx.fillStyle = 'rgba(250, 204, 21, 0.2)'; // Yellow transparent\n ctx.fill();\n ctx.strokeStyle = '#facc15';\n ctx.stroke();\n\n // Right Angle Symbol\n ctx.beginPath();\n ctx.strokeStyle = '#e2e8f0';\n ctx.lineWidth = 1;\n const raSize = 15;\n ctx.moveTo(cx + raSize, cy);\n ctx.lineTo(cx + raSize, cy - raSize);\n ctx.lineTo(cx, cy - raSize);\n ctx.stroke();\n\n // Labels for Triangle parts\n drawLabel(\"s=14\", (top.x + right.x)/2 + 20, (top.y + right.y)/2 - 10, \"#facc15\");\n \n // Legs\n // Horizontal leg\n drawLabel(\"d₁/2\", cx + halfW/2, cy + 20, \"#f472b6\");\n // Vertical leg\n drawLabel(\"d₂/2\", cx - 35, cy - halfH/2, \"#f472b6\");\n }\n\n // Step 4: Just reinforce the highlighting\n if (s === 3) {\n // Maybe pulse the whole shape or specific parts?\n // For now, the static visual with the equation updates is sufficient.\n }\n\n ctx.restore();\n }\n\n /**\n * Helper to draw text with background for readability\n */\n function drawLabel(text, x, y, color) {\n ctx.font = \"bold 16px sans-serif\";\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n \n const metrics = ctx.measureText(text);\n const bgPadding = 4;\n \n ctx.fillStyle = \"rgba(15, 23, 42, 0.8)\"; // Semi-transparent dark bg\n ctx.fillRect(\n x - metrics.width/2 - bgPadding, \n y - 10 - bgPadding, \n metrics.width + bgPadding*2, \n 20 + bgPadding*2\n );\n\n ctx.fillStyle = color;\n ctx.fillText(text, x, y);\n }\n\n // Run\n init();\n\n </script>\n</body>\n</html>"
|
| 51 |
+
}
|
examples/006-visual-proof-altitude-to-hypotenuse-ratio.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appName": "VisualMath AI Export",
|
| 3 |
+
"exportedAt": "2025-11-24T17:56:07.994Z",
|
| 4 |
+
"concept": {
|
| 5 |
+
"conceptTitle": "Altitude to Hypotenuse Ratio",
|
| 6 |
+
"educationalGoal": "To understand how the altitude to the hypotenuse of a right triangle creates similar triangles, and to use these similarities to prove that the ratio of the segments of the hypotenuse (AD:DC) is equal to the ratio of the squares of the adjacent legs (AB²:BC²).",
|
| 7 |
+
"explanation": "Welcome! This interactive proof will explore a fascinating property of right triangles. When you draw an altitude from the right angle to the hypotenuse, it divides the original triangle into two smaller triangles that are similar to the original one, and to each other! We'll use this powerful concept of similar triangles to discover the ratio in which the hypotenuse is divided.",
|
| 8 |
+
"steps": [
|
| 9 |
+
{
|
| 10 |
+
"stepTitle": "The Setup: Right Triangle and Altitude",
|
| 11 |
+
"instruction": "Observe the right triangle ABC, with the right angle at B. You can drag points A and C to change its shape. Click 'Draw Altitude' to construct the altitude BD to the hypotenuse AC. Notice how angle B is divided and two new right angles are formed at D.",
|
| 12 |
+
"visualFocus": "Initial display of triangle ABC with 90-degree marker at B. Draggable points A and C. Upon 'Draw Altitude' click, line segment BD appears, and 90-degree markers at D appear. Text labels A, B, C, D."
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"stepTitle": "Similarity 1: ΔABC and ΔADB",
|
| 16 |
+
"instruction": "Let's compare the big triangle ABC with the smaller triangle ADB. Click 'Highlight Angles' to see that they share Angle A and both have a right angle (Angle ABC and Angle ADB). This means they are similar!",
|
| 17 |
+
"visualFocus": "Triangles ABC and ADB are highlighted. Angle A is highlighted with a shared color/marker for both triangles. Angle B (in ABC) and Angle D (in ADB) are highlighted with a different shared color/marker. Text 'ΔABC ~ ΔADB' appears."
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"stepTitle": "Proportionality from Similarity 1",
|
| 21 |
+
"instruction": "Because ΔABC ~ ΔADB, the ratio of their corresponding sides is equal. Observe how this leads to the relationship between AB, AD, and AC.",
|
| 22 |
+
"visualFocus": "The highlighted triangles remain. A text box appears showing the proportionality: 'AD/AB = AB/AC' and then '=> AB² = AD * AC'. Sides AB, AD, AC are emphasized."
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"stepTitle": "Similarity 2: ΔABC and ΔBDC",
|
| 26 |
+
"instruction": "Now, let's compare the original triangle ABC with the other small triangle, BDC. Click 'Highlight Angles' again to see they share Angle C and both have a right angle (Angle ABC and Angle BDC). They are also similar!",
|
| 27 |
+
"visualFocus": "Triangles ABC and BDC are highlighted. Angle C is highlighted with a shared color/marker. Angle B (in ABC) and Angle D (in BDC) are highlighted with a different shared color/marker. Text 'ΔABC ~ ΔBDC' appears."
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"stepTitle": "Proportionality from Similarity 2",
|
| 31 |
+
"instruction": "Similarly, since ΔABC ~ ΔBDC, another set of corresponding sides are proportional. See how this gives us a relationship between BC, DC, and AC.",
|
| 32 |
+
"visualFocus": "The highlighted triangles remain. A text box appears showing the proportionality: 'DC/BC = BC/AC' and then '=> BC² = DC * AC'. Sides BC, DC, AC are emphasized."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"stepTitle": "The Final Ratio AD:DC",
|
| 36 |
+
"instruction": "We now have expressions for AB² and BC². To find the ratio AD:DC, we can divide these two equations. Observe the final derivation.",
|
| 37 |
+
"visualFocus": "The two derived equations (AB² = AD * AC and BC² = DC * AC) are displayed. Then, the division is shown: 'AB²/BC² = (AD * AC) / (DC * AC)'. Finally, the simplified ratio is displayed: 'AD/DC = AB²/BC²'. All segments involved (AD, DC, AB, BC) are highlighted."
|
| 38 |
+
}
|
| 39 |
+
],
|
| 40 |
+
"visualSpec": {
|
| 41 |
+
"elements": [
|
| 42 |
+
"A canvas for geometric drawing.",
|
| 43 |
+
"Points A, B, C (draggable to maintain right angle at B, or allow A and C to be dragged while B is fixed for simplicity).",
|
| 44 |
+
"Lines AB, BC, AC forming a right triangle.",
|
| 45 |
+
"Altitude line BD, perpendicular to AC (drawn upon interaction).",
|
| 46 |
+
"Right angle markers at B, D.",
|
| 47 |
+
"Angle arc markers for common angles (e.g., Angle A, Angle C), color-coded for similarity steps.",
|
| 48 |
+
"Text labels for A, B, C, D and for side lengths (AB, BC, AC, AD, DC).",
|
| 49 |
+
"Text display area for derived equations and ratios.",
|
| 50 |
+
"Buttons: 'Draw Altitude', 'Highlight Angles', 'Next Step' (to cycle through ratios)."
|
| 51 |
+
],
|
| 52 |
+
"interactions": [
|
| 53 |
+
"drag(point_A): Changes the length of leg AB while maintaining angle B at 90 degrees.",
|
| 54 |
+
"drag(point_C): Changes the length of leg BC while maintaining angle B at 90 degrees.",
|
| 55 |
+
"click(button: 'Draw Altitude'): Renders line segment BD and point D.",
|
| 56 |
+
"click(button: 'Highlight Angles'): Toggles angle highlights for the current step's similar triangles.",
|
| 57 |
+
"click(button: 'Next Step'): Advances the displayed proportionality/ratio."
|
| 58 |
+
],
|
| 59 |
+
"mathLogic": "Given a right triangle ABC with right angle at B. BD is the altitude to hypotenuse AC. Point D lies on AC. 1. Angle properties: Angle ABC = Angle ADB = Angle BDC = 90 degrees. Angle A + Angle C = 90 degrees. Angle ABD + Angle A = 90 degrees => Angle ABD = Angle C. Angle DBC + Angle C = 90 degrees => Angle DBC = Angle A. 2. Similarity 1: Triangle ABC ~ Triangle ADB (by AA similarity: Angle A is common, Angle ABC = Angle ADB = 90 degrees). From this, the ratio of corresponding sides: AD/AB = AB/AC => AB² = AD * AC. 3. Similarity 2: Triangle ABC ~ Triangle BDC (by AA similarity: Angle C is common, Angle ABC = Angle BDC = 90 degrees). From this, the ratio of corresponding sides: DC/BC = BC/AC => BC² = DC * AC. 4. Final Ratio: Divide the two derived equations: (AB²)/(BC²) = (AD * AC) / (DC * AC). Since AC is common in numerator and denominator, it cancels out: AD/DC = AB²/BC²."
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"sourceCode": "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Visual Proof: Altitude to Hypotenuse Ratio</title>\n <style>\n :root {\n --bg-color: #0f172a;\n --panel-bg: #1e293b;\n --text-color: #e2e8f0;\n --accent-primary: #38bdf8; /* Sky Blue */\n --accent-secondary: #34d399; /* Emerald */\n --accent-tertiary: #a78bfa; /* Violet */\n --danger: #fb7185;\n --border-color: #334155;\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-color);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* Header */\n header {\n padding: 1rem 2rem;\n background-color: var(--panel-bg);\n border-bottom: 1px solid var(--border-color);\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-shrink: 0;\n }\n\n h1 { font-size: 1.2rem; font-weight: 600; color: var(--accent-primary); }\n\n /* Main Layout */\n main {\n display: flex;\n flex: 1;\n overflow: hidden;\n flex-direction: row;\n }\n\n /* Canvas Area */\n #canvas-container {\n flex: 2;\n position: relative;\n background-color: #020617;\n overflow: hidden;\n cursor: crosshair;\n }\n\n canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /* Sidebar / Controls */\n #sidebar {\n flex: 1;\n min-width: 320px;\n max-width: 450px;\n background-color: var(--panel-bg);\n border-left: 1px solid var(--border-color);\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n overflow-y: auto;\n box-shadow: -4px 0 15px rgba(0,0,0,0.3);\n }\n\n /* Step Indicator */\n .step-indicator {\n font-size: 0.85rem;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: #94a3b8;\n margin-bottom: 0.5rem;\n }\n\n .step-title {\n font-size: 1.5rem;\n font-weight: 700;\n margin-bottom: 1rem;\n color: white;\n line-height: 1.2;\n }\n\n .instruction-box {\n background: rgba(255,255,255,0.05);\n padding: 1rem;\n border-radius: 8px;\n border-left: 4px solid var(--accent-primary);\n font-size: 0.95rem;\n line-height: 1.6;\n }\n\n /* Math / Equation Display */\n .math-display {\n background: #0f172a;\n padding: 1rem;\n border-radius: 6px;\n border: 1px solid var(--border-color);\n font-family: 'Courier New', monospace;\n font-size: 1rem;\n text-align: center;\n min-height: 60px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 8px;\n }\n \n .math-row { display: block; }\n .highlight-math { color: var(--accent-primary); font-weight: bold; }\n sup { font-size: 0.7em; }\n\n /* Controls Area */\n .controls-area {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\n\n .action-btn {\n background-color: var(--accent-secondary);\n color: #064e3b;\n border: none;\n padding: 0.75rem;\n border-radius: 6px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n display: none; /* Hidden by default, shown via JS */\n }\n\n .action-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }\n .action-btn:active { transform: translateY(0); }\n\n .nav-buttons {\n display: flex;\n gap: 1rem;\n border-top: 1px solid var(--border-color);\n padding-top: 1.5rem;\n }\n\n .nav-btn {\n flex: 1;\n padding: 0.75rem;\n border: 1px solid var(--border-color);\n background: transparent;\n color: var(--text-color);\n border-radius: 6px;\n cursor: pointer;\n font-weight: 600;\n transition: background 0.2s;\n }\n\n .nav-btn:hover:not(:disabled) { background-color: rgba(255,255,255,0.1); }\n .nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n .nav-btn.primary { background-color: var(--accent-primary); color: #0c4a6e; border: none; }\n .nav-btn.primary:hover:not(:disabled) { background-color: #7dd3fc; }\n\n /* Responsive Mobile */\n @media (max-width: 768px) {\n main { flex-direction: column; }\n #canvas-container { height: 50vh; flex: none; }\n #sidebar { width: 100%; max-width: none; flex: 1; min-width: auto; }\n }\n </style>\n</head>\n<body>\n\n <header>\n <h1>Math Visualizer</h1>\n <div style=\"font-size: 0.9rem; color: #94a3b8;\">Altitude to Hypotenuse Proof</div>\n </header>\n\n <main>\n <div id=\"canvas-container\">\n <canvas id=\"geoCanvas\"></canvas>\n </div>\n\n <aside id=\"sidebar\">\n <div>\n <div class=\"step-indicator\">Step <span id=\"step-num\">1</span> / 6</div>\n <h2 class=\"step-title\" id=\"step-title\">The Setup</h2>\n \n <div class=\"instruction-box\" id=\"step-desc\">\n Observe the right triangle ABC. Drag points <strong>A</strong> (top) and <strong>C</strong> (right) to resize legs. Point B remains fixed at 90°.\n </div>\n\n <br>\n \n <div class=\"math-display\" id=\"math-output\">\n <!-- Dynamic Math Content -->\n <span style=\"color: #64748b; font-style: italic;\">Equations will appear here...</span>\n </div>\n </div>\n\n <div class=\"controls-area\">\n <!-- Contextual Actions -->\n <button id=\"btn-draw-altitude\" class=\"action-btn\">Draw Altitude</button>\n <button id=\"btn-highlight\" class=\"action-btn\">Highlight Angles</button>\n\n <!-- Navigation -->\n <div class=\"nav-buttons\">\n <button id=\"btn-prev\" class=\"nav-btn\">Previous</button>\n <button id=\"btn-next\" class=\"nav-btn primary\">Next</button>\n </div>\n </div>\n </aside>\n </main>\n\n<script>\n/**\n * Geometry App Logic\n * Handles State, Rendering, and Interaction\n */\nconst App = (() => {\n // --- Configuration ---\n const COLORS = {\n bg: '#020617',\n white: '#e2e8f0',\n blue: '#38bdf8',\n green: '#34d399',\n orange: '#fb923c',\n red: '#f43f5e',\n purple: '#a78bfa',\n highlightFill: 'rgba(56, 189, 248, 0.15)',\n highlightFill2: 'rgba(52, 211, 153, 0.15)'\n };\n\n // --- State ---\n let state = {\n step: 0, // 0 to 5 corresponding to Steps 1-6\n width: 0,\n height: 0,\n points: {\n A: { x: 150, y: 100 }, // Top\n B: { x: 150, y: 450 }, // Corner (Right Angle)\n C: { x: 650, y: 450 }, // Right\n D: { x: 0, y: 0 } // Calculated\n },\n dragging: null,\n altitudeDrawn: false,\n anglesHighlighted: false,\n animationFrame: null\n };\n\n // --- DOM Elements ---\n const canvas = document.getElementById('geoCanvas');\n const ctx = canvas.getContext('2d');\n const els = {\n stepNum: document.getElementById('step-num'),\n stepTitle: document.getElementById('step-title'),\n stepDesc: document.getElementById('step-desc'),\n mathOutput: document.getElementById('math-output'),\n btnDrawAlt: document.getElementById('btn-draw-altitude'),\n btnHighlight: document.getElementById('btn-highlight'),\n btnPrev: document.getElementById('btn-prev'),\n btnNext: document.getElementById('btn-next')\n };\n\n // --- Math Helpers ---\n function distance(p1, p2) {\n return Math.hypot(p2.x - p1.x, p2.y - p1.y);\n }\n\n // Calculate Point D: Projection of B onto line AC\n // A = (x1, y1), C = (x2, y2), B = (x3, y3)\n // D lies on AC such that BD is perpendicular to AC\n function calculateD() {\n const A = state.points.A;\n const B = state.points.B;\n const C = state.points.C;\n\n // Vector AC\n const ACx = C.x - A.x;\n const ACy = C.y - A.y;\n \n // Vector AB\n const ABx = B.x - A.x;\n const ABy = B.y - A.y;\n\n // Projection scalar t = (AB . AC) / (AC . AC)\n const dot = ABx * ACx + ABy * ACy;\n const lenSq = ACx * ACx + ACy * ACy;\n const t = dot / lenSq;\n\n state.points.D = {\n x: A.x + t * ACx,\n y: A.y + t * ACy\n };\n }\n\n // --- Content Data ---\n const stepsData = [\n {\n title: \"The Setup\",\n desc: \"We have a Right Triangle ABC (∠B = 90°). Drag points <strong>A</strong> and <strong>C</strong> to adjust dimensions. Click 'Draw Altitude' to construct segment BD perpendicular to the hypotenuse AC.\",\n math: \"\",\n hasAltitudeBtn: true\n },\n {\n title: \"Similarity: ΔABC ~ ΔADB\",\n desc: \"Let's compare the whole triangle (ABC) with the top small triangle (ADB). Click 'Highlight Angles' to see the matching angles. Both have a right angle, and they share Angle A.\",\n math: \"<span class='math-row'>ΔABC ~ ΔADB (AA Similarity)</span>\",\n hasHighlightBtn: true\n },\n {\n title: \"Ratio: Legs and Segment AD\",\n desc: \"Because ΔABC ~ ΔADB, their corresponding sides are proportional. We focus on the hypotenuse and the adjacent leg.\",\n math: \"<span class='math-row'>AD / AB = AB / AC</span><span class='math-row highlight-math'>AB² = AD × AC</span>\"\n },\n {\n title: \"Similarity: ΔABC ~ ΔBDC\",\n desc: \"Now compare the whole triangle (ABC) with the bottom/right small triangle (BDC). Click 'Highlight Angles'. They share Angle C and both have 90° angles.\",\n math: \"<span class='math-row'>ΔABC ~ ΔBDC (AA Similarity)</span>\",\n hasHighlightBtn: true\n },\n {\n title: \"Ratio: Legs and Segment DC\",\n desc: \"Since ΔABC ~ ΔBDC, their sides are proportional. This gives us a relationship for the other leg, BC.\",\n math: \"<span class='math-row'>DC / BC = BC / AC</span><span class='math-row highlight-math'>BC² = DC × AC</span>\"\n },\n {\n title: \"The Final Ratio\",\n desc: \"We divide the two equations derived (AB² and BC²) to find the ratio of the hypotenuse segments AD to DC.\",\n math: \"<span class='math-row'>AB² / BC² = (AD × AC) / (DC × AC)</span><span class='math-row highlight-math'>AD / DC = AB² / BC²</span>\"\n }\n ];\n\n // --- Drawing Functions ---\n function drawLine(p1, p2, color, width = 2, dashed = false) {\n ctx.beginPath();\n ctx.moveTo(p1.x, p1.y);\n ctx.lineTo(p2.x, p2.y);\n ctx.strokeStyle = color;\n ctx.lineWidth = width;\n if(dashed) ctx.setLineDash([5, 5]);\n else ctx.setLineDash([]);\n ctx.stroke();\n ctx.setLineDash([]);\n }\n\n function drawPoint(p, label, color = COLORS.white) {\n ctx.beginPath();\n ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);\n ctx.fillStyle = color;\n ctx.fill();\n \n // Label\n ctx.font = \"bold 16px sans-serif\";\n ctx.fillStyle = COLORS.white;\n let offsetX = 15, offsetY = -15;\n if (label === 'B') { offsetX = -20; offsetY = 20; }\n if (label === 'A') { offsetX = -20; offsetY = -10; }\n if (label === 'C') { offsetX = 15; offsetY = 10; }\n if (label === 'D') { offsetX = 10; offsetY = -10; }\n ctx.fillText(label, p.x + offsetX, p.y + offsetY);\n }\n\n function drawRightAngleMarker(center, p1, p2, size = 15) {\n // Vector C->P1\n const v1x = p1.x - center.x;\n const v1y = p1.y - center.y;\n // Vector C->P2\n const v2x = p2.x - center.x;\n const v2y = p2.y - center.y;\n \n // Normalize\n const l1 = Math.hypot(v1x, v1y);\n const l2 = Math.hypot(v2x, v2y);\n \n const u1x = (v1x / l1) * size;\n const u1y = (v1y / l1) * size;\n const u2x = (v2x / l2) * size;\n const u2y = (v2y / l2) * size;\n\n ctx.beginPath();\n ctx.moveTo(center.x + u1x, center.y + u1y);\n ctx.lineTo(center.x + u1x + u2x, center.y + u1y + u2y);\n ctx.lineTo(center.x + u2x, center.y + u2y);\n ctx.strokeStyle = COLORS.white;\n ctx.lineWidth = 1;\n ctx.stroke();\n }\n\n function drawAngleArc(center, startP, endP, color, radius = 30, fill = false) {\n const startAngle = Math.atan2(startP.y - center.y, startP.x - center.x);\n const endAngle = Math.atan2(endP.y - center.y, endP.x - center.x);\n \n ctx.beginPath();\n ctx.arc(center.x, center.y, radius, startAngle, endAngle, (endAngle < startAngle && Math.abs(startAngle - endAngle) < Math.PI)); // Simple direction check logic might need tweaking for arbitrary angles but works for this triangle setup\n \n // Correction for counter-clockwise vs clockwise drawing dependent on point order\n // For this specific triangle setup (B bottom-left), standard arc works ok.\n // A more robust way draws lines to center for fill.\n \n if (fill) {\n ctx.lineTo(center.x, center.y);\n ctx.closePath();\n ctx.fillStyle = color;\n ctx.fill();\n } else {\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.stroke();\n }\n }\n \n function drawPolygon(points, color) {\n ctx.beginPath();\n ctx.moveTo(points[0].x, points[0].y);\n for(let i=1; i<points.length; i++) ctx.lineTo(points[i].x, points[i].y);\n ctx.closePath();\n ctx.fillStyle = color;\n ctx.fill();\n }\n\n // --- Main Render Loop ---\n function render() {\n // Clear\n ctx.fillStyle = COLORS.bg;\n ctx.fillRect(0, 0, state.width, state.height);\n\n calculateD();\n const { A, B, C, D } = state.points;\n\n // Visual Logic based on Step\n const step = state.step;\n const showAlt = step > 0 || state.altitudeDrawn;\n const highlightAngles = state.anglesHighlighted;\n\n // 1. Draw Highlights (Behind lines)\n if (step === 1 && highlightAngles) {\n // ABC and ADB\n drawPolygon([A, B, C], COLORS.highlightFill); \n drawPolygon([A, D, B], COLORS.highlightFill2);\n } else if (step === 3 && highlightAngles) {\n // ABC and BDC\n drawPolygon([A, B, C], COLORS.highlightFill);\n drawPolygon([B, D, C], COLORS.highlightFill2);\n }\n\n // 2. Draw Triangle Lines\n // Base Triangle ABC\n let widthAB = 2, widthBC = 2, widthAC = 2;\n let colorAB = COLORS.white, colorBC = COLORS.white, colorAC = COLORS.white;\n\n if (step === 2 || step === 5) { // Highlight sides for formulas\n if(step === 2) { widthAB = 4; colorAB = COLORS.blue; widthAC = 4; colorAC = COLORS.orange; } // AB^2 = AD*AC\n if(step === 5) { widthAB = 4; colorAB = COLORS.blue; widthBC = 4; colorBC = COLORS.green; } // AD/DC\n }\n\n // 3. Draw Segments\n drawLine(A, B, colorAB, widthAB);\n drawLine(B, C, colorBC, widthBC);\n drawLine(A, C, colorAC, widthAC);\n\n // 4. Draw Altitude\n if (showAlt) {\n let colorBD = COLORS.red;\n let widthBD = 2;\n if (step === 2) colorBD = COLORS.red; // Emphasize for AD segment logic? No, AD is on AC.\n drawLine(B, D, colorBD, widthBD);\n \n // Right angle at D (two sides)\n drawRightAngleMarker(D, B, A, 12); // towards A\n drawRightAngleMarker(D, B, C, 12); // towards C\n \n drawPoint(D, 'D', COLORS.red);\n \n // Draw AD and DC segments highlighted if needed\n if (step === 2) { // AD * AC\n drawLine(A, D, COLORS.purple, 4);\n }\n if (step === 4) { // DC * AC\n drawLine(D, C, COLORS.purple, 4);\n }\n if (step === 5) { // AD and DC\n drawLine(A, D, COLORS.blue, 4);\n drawLine(D, C, COLORS.green, 4);\n }\n }\n\n // 5. Draw Angle Markers\n drawRightAngleMarker(B, A, C, 20);\n\n if (highlightAngles) {\n if (step === 1) {\n // ABC ~ ADB. Common Angle A.\n drawAngleArc(A, B, C, COLORS.orange, 35, true); // Angle A\n // Right angles are already marked.\n // Angle ABD = Angle C.\n drawAngleArc(B, D, A, COLORS.green, 45, false); // part of B\n }\n if (step === 3) {\n // ABC ~ BDC. Common Angle C.\n drawAngleArc(C, B, A, COLORS.orange, 35, true); // Angle C\n // Angle DBC = Angle A.\n }\n }\n\n // 6. Draw Points\n drawPoint(A, 'A', COLORS.blue);\n drawPoint(B, 'B', COLORS.white); // Fixed\n drawPoint(C, 'C', COLORS.green);\n\n // Labels for Lengths\n // Midpoints\n if (step >= 2) {\n ctx.fillStyle = COLORS.white;\n ctx.font = \"12px monospace\";\n // AB\n ctx.fillText(\"AB\", (A.x+B.x)/2 - 20, (A.y+B.y)/2);\n // BC\n ctx.fillText(\"BC\", (C.x+B.x)/2 + 10, (C.y+B.y)/2);\n // AC\n // ctx.fillText(\"AC\", (A.x+C.x)/2, (A.y+C.y)/2 - 10);\n }\n if (showAlt && step >= 2) {\n // AD\n // DC\n }\n }\n\n // --- UI Logic ---\n function updateUI() {\n const data = stepsData[state.step];\n els.stepNum.textContent = state.step + 1;\n els.stepTitle.textContent = data.title;\n els.stepDesc.innerHTML = data.desc;\n els.mathOutput.innerHTML = data.math || \"<span style='color: #64748b'>...</span>\";\n\n // Buttons Visibility\n els.btnDrawAlt.style.display = (data.hasAltitudeBtn && !state.altitudeDrawn) ? 'block' : 'none';\n \n if (data.hasHighlightBtn) {\n els.btnHighlight.style.display = 'block';\n els.btnHighlight.textContent = state.anglesHighlighted ? 'Hide Angles' : 'Highlight Angles';\n els.btnHighlight.style.backgroundColor = state.anglesHighlighted ? '#475569' : COLORS.green;\n els.btnHighlight.style.color = state.anglesHighlighted ? '#fff' : '#064e3b';\n } else {\n els.btnHighlight.style.display = 'none';\n }\n\n // Nav Buttons\n els.btnPrev.disabled = state.step === 0;\n els.btnNext.disabled = state.step === stepsData.length - 1;\n \n // Special check: Block next if altitude not drawn in step 1? \n // The prompt suggests freedom, but visual logic breaks if altitude missing.\n // We will auto-draw if user clicks next.\n }\n\n function nextStep() {\n if (state.step < stepsData.length - 1) {\n // Transition Logic\n if (state.step === 0 && !state.altitudeDrawn) {\n state.altitudeDrawn = true; // Auto draw\n }\n state.step++;\n state.anglesHighlighted = false; // Reset highlights on step change\n updateUI();\n requestAnimationFrame(render);\n }\n }\n\n function prevStep() {\n if (state.step > 0) {\n state.step--;\n state.anglesHighlighted = false;\n updateUI();\n requestAnimationFrame(render);\n }\n }\n\n // --- Interaction Handlers ---\n function handleResize() {\n const rect = canvas.parentElement.getBoundingClientRect();\n state.width = rect.width;\n state.height = rect.height;\n canvas.width = state.width;\n canvas.height = state.height;\n \n // Reset positions relative to new size if needed, \n // but simply clamping B to bottom left area is better.\n state.points.B = { x: 100, y: state.height - 100 };\n state.points.A = { x: 100, y: 100 };\n state.points.C = { x: state.width - 100, y: state.height - 100 };\n \n render();\n }\n\n function getMousePos(evt) {\n const rect = canvas.getBoundingClientRect();\n return {\n x: evt.clientX - rect.left,\n y: evt.clientY - rect.top\n };\n }\n\n function handleMouseDown(e) {\n const m = getMousePos(e);\n // Check A (Drag Vertical only mostly, but let's allow user to mess up slightly? No, enforce Right Angle at B)\n // B is fixed. A can move along Y axis above B. C can move along X axis right of B.\n \n if (distance(m, state.points.A) < 20) state.dragging = 'A';\n else if (distance(m, state.points.C) < 20) state.dragging = 'C';\n }\n\n function handleMouseMove(e) {\n const m = getMousePos(e);\n // Cursor style\n if (distance(m, state.points.A) < 20 || distance(m, state.points.C) < 20) {\n canvas.style.cursor = 'pointer';\n } else {\n canvas.style.cursor = 'crosshair';\n }\n\n if (!state.dragging) return;\n\n if (state.dragging === 'A') {\n // Constrain A to be directly above B\n let newY = m.y;\n if (newY > state.points.B.y - 50) newY = state.points.B.y - 50; // Don't go below B\n if (newY < 20) newY = 20; // Don't go offscreen top\n state.points.A.y = newY;\n state.points.A.x = state.points.B.x; // Ensure vertical alignment\n } else if (state.dragging === 'C') {\n // Constrain C to be directly right of B\n let newX = m.x;\n if (newX < state.points.B.x + 50) newX = state.points.B.x + 50; // Don't go left of B\n if (newX > state.width - 20) newX = state.width - 20; // Don't go offscreen right\n state.points.C.x = newX;\n state.points.C.y = state.points.B.y; // Ensure horizontal alignment\n }\n render();\n }\n\n function handleMouseUp() {\n state.dragging = null;\n }\n\n function init() {\n window.addEventListener('resize', handleResize);\n handleResize(); // Set initial size\n\n // Interaction Events\n canvas.addEventListener('mousedown', handleMouseDown);\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n \n // Touch support\n canvas.addEventListener('touchstart', (e) => {\n e.preventDefault();\n handleMouseDown(e.touches[0]);\n }, {passive: false});\n canvas.addEventListener('touchmove', (e) => {\n e.preventDefault();\n handleMouseMove(e.touches[0]);\n }, {passive: false});\n canvas.addEventListener('touchend', handleMouseUp);\n\n // Buttons\n els.btnNext.addEventListener('click', nextStep);\n els.btnPrev.addEventListener('click', prevStep);\n \n els.btnDrawAlt.addEventListener('click', () => {\n state.altitudeDrawn = true;\n updateUI();\n render();\n });\n\n els.btnHighlight.addEventListener('click', () => {\n state.anglesHighlighted = !state.anglesHighlighted;\n updateUI();\n render();\n });\n\n updateUI();\n render();\n }\n\n return { init };\n\n})();\n\n// Start App\nApp.init();\n</script>\n</body>\n</html>\n",
|
| 63 |
+
"logs": [
|
| 64 |
+
{
|
| 65 |
+
"timestamp": "12:53:07",
|
| 66 |
+
"message": "Starting process...",
|
| 67 |
+
"type": "info"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"timestamp": "12:53:07",
|
| 71 |
+
"message": "Stage 1: Analyzing concept with Gemini 2.5 Flash...",
|
| 72 |
+
"type": "thinking"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"timestamp": "12:53:07",
|
| 76 |
+
"message": "Input (Text): \"In right triangle ABC, what is the ratio in which ...\"",
|
| 77 |
+
"type": "info"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"timestamp": "12:53:29",
|
| 81 |
+
"message": "Concept Identified: Altitude to Hypotenuse Ratio",
|
| 82 |
+
"type": "success"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"timestamp": "12:53:29",
|
| 86 |
+
"message": "Planned 6 interactive steps.",
|
| 87 |
+
"type": "info"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"timestamp": "12:53:29",
|
| 91 |
+
"message": "Stage 2: Engineering simulation with Gemini 3.0 Pro (Thinking Enabled)...",
|
| 92 |
+
"type": "thinking"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"timestamp": "12:53:29",
|
| 96 |
+
"message": "Generating code with high thinking budget...",
|
| 97 |
+
"type": "info"
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"timestamp": "12:55:30",
|
| 101 |
+
"message": "Code generated successfully.",
|
| 102 |
+
"type": "success"
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"timestamp": "12:55:30",
|
| 106 |
+
"message": "Process Complete in 143.06s",
|
| 107 |
+
"type": "success"
|
| 108 |
+
}
|
| 109 |
+
]
|
| 110 |
+
}
|
img/architecture-diagram.jpg
ADDED
|
img/architecture.jpg
ADDED
|
Git LFS Details
|
img/gradio-ui-user-flow.jpg
ADDED
|
Git LFS Details
|
img/hero.jpg
ADDED
|
Git LFS Details
|
img/mcp-server-flow.jpg
ADDED
|
Git LFS Details
|
img/programmatic-flow.jpg
ADDED
|
Git LFS Details
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gradio[oauth,mcp]==6.0.1
|
| 2 |
+
google-genai>=1.0.0
|
| 3 |
+
Pillow>=10.0.0
|
| 4 |
+
python-dotenv>=1.0.0
|
saved_proofs/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Saved proofs will be stored here
|