DreamyDetective commited on
Commit
69ac033
·
verified ·
1 Parent(s): bf9c161

feat: added application files

Browse files
.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
+ ![High-Level User Flow Diagram](./img/gradio-ui-user-flow.jpg)
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
+ ![MCP Server Flow Diagram](./img/mcp-server-flow.jpg)
61
+
62
+ **MCP Architecture Diagram:**
63
+
64
+ ![MCP Architecture Diagram](./img/architecture-diagram.jpg)
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: red
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: '**Transform Static Math Problems into Living, Interactive St'
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ![](./img/hero.jpg)
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
+ [![MCP's 1st Birthday Hackathon](https://img.shields.io/badge/MCP%27s%201st%20Birthday-Hackathon-blue)](https://github.com/modelcontextprotocol)
31
+ [![Track 1: Building MCP](https://img.shields.io/badge/Track%201-Building%20MCP%20(Consumer)-blue)](https://huggingface.co/MCP-1st-Birthday)
32
+ [![Powered by Google Gemini](https://img.shields.io/badge/Powered%20by-Google%20Gemini%202.5%20Flash-blue)](https://ai.google.dev/)
33
+ [![Powered by Google Gemini](https://img.shields.io/badge/Powered%20by-Google%20Gemini%203.0%20Pro-blue)](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
+ ![architecture](./img/architecture.jpg)
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
+ ![programmatic-flow](./img/programmatic-flow.jpg)
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('"', '&quot;')
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> &times; BD &times; AO</div>\n <div class=\"math-row highlight-pink\">Area(△BCD) = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> &times; BD &times; 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> &cdot; BD &cdot; AO</span> + <span class=\"highlight-pink\"><div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> &cdot; BD &cdot; 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> &cdot; BD &cdot; (AO + OC)</div>\n <div class=\"math-row highlight-green\">Area = <div class=\"fraction\"><span class=\"numerator\">1</span><span>2</span></div> &cdot; BD &cdot; AC</div>\n <div class=\"math-row\" style=\"font-size: 0.9em; color: #94a3b8;\">(or &frac12; &cdot; d<sub>1</sub> &cdot; 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

  • SHA256: 759ff903eb98aa87fd4d84c84725501a4701c6c4108b64b97364c1f5e3be279b
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB
img/gradio-ui-user-flow.jpg ADDED

Git LFS Details

  • SHA256: 2dfdbf11c6208f7fbe89253c6abc1f092c3df9358dd7310a06574b3ae73270bf
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
img/hero.jpg ADDED

Git LFS Details

  • SHA256: 86576d1930caf9fef2797f4eb9cc08f730f2ce8eff5ce3c9dc6091ba415346d3
  • Pointer size: 131 Bytes
  • Size of remote file: 110 kB
img/mcp-server-flow.jpg ADDED

Git LFS Details

  • SHA256: 74e49fb7ebb935e3ccc755665fb91699ca2bf5acff6923182d0f4f0a016458c6
  • Pointer size: 131 Bytes
  • Size of remote file: 112 kB
img/programmatic-flow.jpg ADDED

Git LFS Details

  • SHA256: e81752d4dabcb8188fbf05ac3eeb6e5110f7cdcb9aada3835bd1087109cd0757
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
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