-
Notifications
You must be signed in to change notification settings - Fork 1
dev: Remove old README, update docs #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Removes the outdated README file and updates the documentation. This change cleans up the project by removing redundant files and ensuring the documentation is current.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request replaces the markdown-based documentation (docs/index.md) with a new interactive HTML documentation site. However, the scope extends significantly beyond what's described in the PR title and description. The changes introduce a full-featured web application with AI integration capabilities, external service dependencies, and unfortunately, several critical security issues.
Key Changes:
- Removes the existing technical documentation (docs/index.md) which contained installation instructions, ISA reference, and examples
- Adds a 1,773-line interactive HTML documentation site with live demos, dark mode, and visualizations
- Introduces AI-powered features via Gemini API integration through a Cloudflare Worker proxy
- Critical: Commits a .env file containing an exposed API key to the repository
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/index.md | Complete removal of existing markdown documentation including ISA reference and setup instructions |
| docs/index.html | New interactive documentation site with embedded JavaScript (1773 lines), live demos, and AI features |
| docs/cloudflare-worker.js | API proxy for Gemini AI with CORS configuration and request handling |
| docs/.nojekyll | Jekyll disable marker for GitHub Pages |
| .env | CRITICAL SECURITY ISSUE: Contains exposed Gemini API key that should never be committed |
Comments suppressed due to low confidence (2)
docs/index.md:1
- The entire docs/index.md file is being deleted, which contained important technical documentation including:
- Installation instructions
- Complete instruction set reference table
- Example runners and file paths
- Publishing and contributing guidelines
While the new HTML documentation (docs/index.html) is visually appealing and interactive, it appears to be more of a project retrospective/showcase rather than comprehensive technical documentation. Important information like the complete ISA reference and detailed setup instructions may be missing from the new documentation.
Consider either:
- Keeping the markdown documentation alongside the HTML for different audiences (developers vs. general visitors)
- Ensuring all critical technical information from the markdown is preserved in the new HTML documentation
- Creating a separate technical documentation section in the new HTML file
The PR description states "removes the outdated README file" but this is actually the documentation index, not a README (the actual README.md exists at the root).
docs/index.md:1
- The PR description states this change "Removes the outdated README file", but this is not a README file - it's the documentation index file (docs/index.md). The actual README.md file is at the root of the repository and is not being modified in this PR.
Additionally, the PR description says it "updates the documentation" but doesn't mention:
- The addition of a new interactive HTML documentation site
- The addition of a Cloudflare Worker for API proxying
- The addition of a .env file with an API key (which is a security concern)
The PR description undersells the scope of these changes. Consider updating it to accurately reflect that this is a major documentation overhaul that introduces new interactive features and external service dependencies.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Validate required fields | ||
| if (!body.prompt) { | ||
| return new Response(JSON.stringify({ error: "Missing prompt" }), { | ||
| status: 400, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Access-Control-Allow-Origin": "*", | ||
| }, | ||
| }); | ||
| } |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The input validation only checks if the prompt exists, but doesn't validate its length or content. This could lead to abuse where malicious users send extremely large prompts, consuming your API quota and potentially causing performance issues.
Consider adding:
- Maximum prompt length validation (e.g., 2000 characters)
- Maximum systemPrompt length validation
- Content validation to prevent obviously malicious or inappropriate content
- Rate limiting per IP address or user agent
Example:
if (!body.prompt || body.prompt.length > 2000) {
return new Response(JSON.stringify({ error: "Invalid prompt length" }), {...});
}| attempts++; | ||
| if (attempts >= maxAttempts) { |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generic error message "Error connecting to AI service. Please try again later." doesn't provide users with helpful information about what went wrong or how to fix it.
Consider providing more specific error messages based on the error type:
- Network errors: "Unable to connect to the AI service. Please check your internet connection."
- API errors: Include the status code and a user-friendly explanation
- Rate limit errors: "Too many requests. Please wait a moment and try again."
This would help users understand and resolve issues more effectively.
| attempts++; | |
| if (attempts >= maxAttempts) { | |
| attempts++; | |
| // If this is the last attempt, return a specific error message | |
| if (attempts >= maxAttempts) { | |
| // Network error (fetch throws TypeError on network failure) | |
| if (error instanceof TypeError) { | |
| return "Unable to connect to the AI service. Please check your internet connection."; | |
| } | |
| // Rate limit error | |
| if (error.message && error.message.includes("Rate limit")) { | |
| return "Too many requests. Please wait a moment and try again."; | |
| } | |
| // API error with status code | |
| if (error.message && error.message.startsWith("API Error:")) { | |
| return error.message; | |
| } | |
| // Other API error with message | |
| if (error.message) { | |
| return `AI service error: ${error.message}`; | |
| } | |
| // Fallback generic |
| const MEMORY_SIZE = 32; | ||
| let memoryData = []; | ||
| let isRunning = false; | ||
| let intervalId = null; | ||
| let currentAlgo = "sort"; | ||
|
|
||
| // Algo State vars | ||
| let sortPhase = 0; // 0: Even, 1: Odd (for Sort) | ||
| let reductionStride = 1; // For Reduction | ||
| let vectorAddDone = false; // For Vector Add | ||
| let blockStride = 1; // For Block Sum | ||
| let comparisons = 0; |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable MEMORY_SIZE is declared with const but uses SCREAMING_SNAKE_CASE naming, which typically indicates a constant. However, since it's already a const at the module level, this is appropriate.
However, memoryData and other simulation state variables are declared with let at the module level (lines 1245-1255), making them mutable global state. This works for a single-page application but could lead to issues if the code is ever refactored or reused.
Consider encapsulating this state in a class or object to make the code more maintainable and testable.
| const errorText = await geminiResponse.text(); | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: "Gemini API error", | ||
| status: geminiResponse.status, | ||
| details: errorText, | ||
| }), | ||
| { | ||
| status: geminiResponse.status, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Access-Control-Allow-Origin": "*", | ||
| }, | ||
| } | ||
| ); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error response from the Gemini API is being returned verbatim to the client, which could potentially leak sensitive information about the API configuration, internal error details, or rate limits.
Consider sanitizing the error response before sending it to the client:
- Don't expose the full error text from the API
- Map API status codes to user-friendly messages
- Log detailed errors server-side for debugging, but return generic messages to clients
Example:
console.error('Gemini API error:', errorText); // Log for debugging
return new Response(JSON.stringify({
error: 'Unable to process request',
code: geminiResponse.status
}), {...});| <script src="https://cdn.tailwindcss.com"></script> | ||
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> | ||
| <link | ||
| href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" | ||
| rel="stylesheet" |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
External CDN scripts are loaded without Subresource Integrity (SRI) hashes. This means that if any of these CDN providers are compromised or serve malicious content, your documentation site could be affected.
Consider adding SRI hashes to all external script and stylesheet tags:
- Tailwind CSS (line 7)
- Chart.js (line 8)
- Font Awesome (line 10)
Example with SRI:
<script src="..." integrity="sha384-..." crossorigin="anonymous"></script>
You can generate SRI hashes at https://www.srihash.org/
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> | |
| <link | |
| href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" | |
| rel="stylesheet" | |
| <script src="https://cdn.tailwindcss.com" | |
| integrity="sha384-+qQ1Q0MyoVo9hw3rroWAtxEvsC0BpvyEuk+qS0bkkCm1cW0XkyKADVY6jwxKF1u9" | |
| crossorigin="anonymous"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js" | |
| integrity="sha512-0bM6Q1rS4MHDL/95B2S/bRMyCV2wE8p8nH3UX0DpD+s/COM24kTx5cDIeEJD7BqXc9E+u6KDAdAm8YGt59l1yA==" | |
| crossorigin="anonymous"></script> | |
| <link | |
| href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" | |
| rel="stylesheet" | |
| integrity="sha512-papm1HtMOBHWuZ4wNG0Ax7Anxr1j7rwhhtjfiPgk41IrT9jp+1rssZCvGSqtEtFPd0PfrxGX2YeliYg+6UkeBQ==" | |
| crossorigin="anonymous" |
| <textarea | ||
| id="assembly-prompt" | ||
| class="w-full h-48 p-4 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl font-mono text-sm text-slate-800 dark:text-slate-200 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none mb-6 outline-none transition-all placeholder-slate-400 dark:placeholder-slate-600" | ||
| placeholder="E.g., Load value from memory address in R1, add 5 to it, and store it back if the value is greater than 10. Include a barrier synchronization." | ||
| ></textarea> |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The textarea element lacks a proper label association. While there is a label element with descriptive text (lines 580-587), it's not explicitly associated with the textarea using the for attribute.
Add an id to the textarea and connect it with the label:
<label for="assembly-prompt" class="...">...</label>
<textarea id="assembly-prompt" ...>This improves accessibility for screen reader users and allows clicking the label to focus the textarea.
| } | ||
|
|
||
| // Build Gemini API request | ||
| const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${env.GEMINI_API_KEY}`; |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Gemini model version "gemini-2.0-flash" is hardcoded in the API URL. If Google releases a new model version or deprecates this one, the code will need to be updated and redeployed.
Consider making the model version configurable through an environment variable or constant at the top of the file:
const GEMINI_MODEL = env.GEMINI_MODEL || 'gemini-2.0-flash';
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${env.GEMINI_API_KEY}`;This allows for easier updates without code changes.
| const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${env.GEMINI_API_KEY}`; | |
| const GEMINI_MODEL = env.GEMINI_MODEL || 'gemini-2.0-flash'; | |
| const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${env.GEMINI_API_KEY}`; |
| # Note: For the GitHub Pages site, users will be prompted to enter their own API key | ||
| # which is stored in their browser's localStorage (not shared with the server) | ||
| # Get your free API key at: https://aistudio.google.com/apikey | ||
| GEMINI_API_KEY=AIzaSyB1kLWIDXGvwikSQyAbqhytf3wHf65aulQ No newline at end of file |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains an exposed Gemini API key that is now committed to the repository. API keys should never be committed to version control as they can be used by anyone who has access to the repository or its history.
This API key should be:
- Immediately revoked at https://aistudio.google.com/apikey
- This file should be removed from the repository
- The .gitignore file should be updated to include .env files
- A template file like .env.example should be provided instead with placeholder values
The comment on lines 2-3 is misleading - it states that users will enter their own API key in localStorage for GitHub Pages, but this file contains an actual API key that will be in the git history even if removed.
| GEMINI_API_KEY=AIzaSyB1kLWIDXGvwikSQyAbqhytf3wHf65aulQ | |
| # IMPORTANT: Do not commit your real API key to version control. | |
| # Replace the placeholder below with your actual API key for local use only. | |
| GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE |
| if (request.method === "OPTIONS") { | ||
| return new Response(null, { | ||
| headers: { | ||
| "Access-Control-Allow-Origin": "*", |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CORS policy is set to allow all origins with "Access-Control-Allow-Origin: *". This means any website can make requests to this proxy endpoint, potentially consuming your Gemini API quota.
Consider implementing at least one of the following protections:
- Restrict the origin to your specific GitHub Pages domain (e.g., "https://username.github.io")
- Add rate limiting to prevent abuse
- Implement request validation or authentication
- Add a referrer check to ensure requests come from your documentation site
Without these protections, anyone who discovers your worker URL can use your API key through the proxy.
| <script> | ||
| // --- Dark Mode Logic --- | ||
| const themeToggleBtn = document.getElementById("theme-toggle"); | ||
| const themeToggleBtnMobile = document.getElementById( | ||
| "theme-toggle-mobile" | ||
| ); | ||
|
|
||
| // Check for saved user preference, if any, on load of the website | ||
| if ( | ||
| localStorage.getItem("color-theme") === "dark" || | ||
| (!("color-theme" in localStorage) && | ||
| window.matchMedia("(prefers-color-scheme: dark)").matches) | ||
| ) { | ||
| document.documentElement.classList.add("dark"); | ||
| } else { | ||
| document.documentElement.classList.remove("dark"); | ||
| } | ||
|
|
||
| function toggleTheme() { | ||
| // if set via local storage previously | ||
| if (localStorage.getItem("color-theme")) { | ||
| if (localStorage.getItem("color-theme") === "light") { | ||
| document.documentElement.classList.add("dark"); | ||
| localStorage.setItem("color-theme", "dark"); | ||
| } else { | ||
| document.documentElement.classList.remove("dark"); | ||
| localStorage.setItem("color-theme", "light"); | ||
| } | ||
|
|
||
| // if NOT set via local storage previously | ||
| } else { | ||
| if (document.documentElement.classList.contains("dark")) { | ||
| document.documentElement.classList.remove("dark"); | ||
| localStorage.setItem("color-theme", "light"); | ||
| } else { | ||
| document.documentElement.classList.add("dark"); | ||
| localStorage.setItem("color-theme", "dark"); | ||
| } | ||
| } | ||
| updateChartTheme(); | ||
| } | ||
|
|
||
| themeToggleBtn.addEventListener("click", toggleTheme); | ||
| themeToggleBtnMobile.addEventListener("click", toggleTheme); | ||
|
|
||
| // --- 1. State Management & Data --- | ||
| const archDetails = { | ||
| assembler: { | ||
| title: "1. The Assembler", | ||
| desc: "The Assembler bridges the gap between human logic and machine execution. It parses `.tgpu` files, handles label resolution for jumps, and outputs a clean list of instruction tuples.", | ||
| code: "def assemble(source):\n # Pass 1: Find Labels\n # Pass 2: Generate OpCodes\n instructions = parse_lines(source)\n return instructions", | ||
| color: "text-blue-600 dark:text-blue-400", | ||
| }, | ||
| core: { | ||
| title: "2. The Core (TinyGPU)", | ||
| desc: "The heart of the simulator. It holds the state matrices: `Registers`, `Memory`, and `Program Counter`. The `step()` function executes one instruction across all active threads simultaneously using NumPy slicing.", | ||
| code: "class TinyGPU:\n def __init__(self, threads):\n # Vectorized State\n self.registers = np.zeros((threads, 8))\n self.pc = np.zeros(threads, dtype=int)\n self.active = np.ones(threads, dtype=bool)", | ||
| color: "text-indigo-600 dark:text-indigo-400", | ||
| }, | ||
| visualizer: { | ||
| title: "3. The Visualizer", | ||
| desc: 'A "Flight Recorder" for debugging. It doesn\'t run in real-time. Instead, it captures the state after every cycle and replays it as a Matplotlib animation, allowing you to pause and inspect race conditions.', | ||
| code: 'def visualize(history):\n for state in history:\n render_heatmap(state.memory)\n render_pc(state.pc)\n save_gif("debug.gif")', | ||
| color: "text-purple-600 dark:text-purple-400", | ||
| }, | ||
| }; | ||
|
|
||
| // --- 2. Architecture Interaction --- | ||
| function updateArchDetail(key) { | ||
| const data = archDetails[key]; | ||
| const titleEl = document.getElementById("detail-title"); | ||
| const descEl = document.getElementById("detail-desc"); | ||
| const codeEl = document.getElementById("detail-code"); | ||
|
|
||
| // Simple Fade Animation | ||
| const panel = document.getElementById("arch-detail"); | ||
| panel.classList.add("opacity-50", "scale-95"); | ||
|
|
||
| setTimeout(() => { | ||
| titleEl.textContent = data.title; | ||
| titleEl.className = `text-2xl font-bold mb-6 ${data.color}`; | ||
| descEl.textContent = data.desc; | ||
| codeEl.textContent = data.code; | ||
| panel.classList.remove("opacity-50", "scale-95"); | ||
| }, 200); | ||
| } | ||
|
|
||
| // --- 3. Simulation Logic --- | ||
| const MEMORY_SIZE = 32; | ||
| let memoryData = []; | ||
| let isRunning = false; | ||
| let intervalId = null; | ||
| let currentAlgo = "sort"; | ||
|
|
||
| // Algo State vars | ||
| let sortPhase = 0; // 0: Even, 1: Odd (for Sort) | ||
| let reductionStride = 1; // For Reduction | ||
| let vectorAddDone = false; // For Vector Add | ||
| let blockStride = 1; // For Block Sum | ||
| let comparisons = 0; | ||
|
|
||
| // Chart Configuration | ||
| const ctx = document.getElementById("memoryChart").getContext("2d"); | ||
| const memoryChart = new Chart(ctx, { | ||
| type: "bar", | ||
| data: { | ||
| labels: Array.from({ length: MEMORY_SIZE }, (_, i) => i), | ||
| datasets: [ | ||
| { | ||
| label: "Memory Value", | ||
| data: [], | ||
| backgroundColor: [], | ||
| borderColor: "#4f46e5", | ||
| borderWidth: 1, | ||
| borderRadius: 4, // More rounded bars | ||
| barPercentage: 0.7, | ||
| }, | ||
| ], | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| animation: { duration: 100 }, | ||
| plugins: { | ||
| legend: { display: false }, | ||
| tooltip: { | ||
| enabled: true, | ||
| backgroundColor: "rgba(15, 23, 42, 0.9)", | ||
| titleColor: "#fff", | ||
| bodyColor: "#fff", | ||
| padding: 10, | ||
| cornerRadius: 8, | ||
| displayColors: false, | ||
| }, | ||
| }, | ||
| scales: { | ||
| y: { | ||
| beginAtZero: true, | ||
| max: 100, | ||
| grid: { display: false, drawBorder: false }, | ||
| ticks: { display: false }, | ||
| }, | ||
| x: { | ||
| grid: { display: false, drawBorder: false }, | ||
| ticks: { display: false }, | ||
| }, | ||
| }, | ||
| layout: { | ||
| padding: { top: 10, bottom: 0 }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Tradeoff Chart | ||
| const ctxTrade = document | ||
| .getElementById("tradeoffChart") | ||
| .getContext("2d"); | ||
| const tradeoffChart = new Chart(ctxTrade, { | ||
| type: "bar", | ||
| data: { | ||
| labels: ["Real GPU", "TinyGPU"], | ||
| datasets: [ | ||
| { | ||
| label: "Speed", | ||
| data: [100, 10], | ||
| backgroundColor: "#94a3b8", | ||
| borderRadius: 4, | ||
| }, | ||
| { | ||
| label: "Observability", | ||
| data: [10, 100], | ||
| backgroundColor: "#6366f1", | ||
| borderRadius: 4, | ||
| }, | ||
| ], | ||
| }, | ||
| options: { | ||
| indexAxis: "y", | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| legend: { | ||
| position: "bottom", | ||
| labels: { | ||
| color: document.documentElement.classList.contains("dark") | ||
| ? "#94a3b8" | ||
| : "#64748b", | ||
| usePointStyle: true, | ||
| }, | ||
| }, | ||
| }, | ||
| scales: { | ||
| x: { display: false }, | ||
| y: { | ||
| grid: { display: false }, | ||
| ticks: { | ||
| color: document.documentElement.classList.contains("dark") | ||
| ? "#cbd5e1" | ||
| : "#475569", | ||
| font: { weight: "bold" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Function to update chart colors based on theme | ||
| function updateChartTheme() { | ||
| const isDark = document.documentElement.classList.contains("dark"); | ||
| const textColor = isDark ? "#cbd5e1" : "#475569"; | ||
| const legendColor = isDark ? "#94a3b8" : "#64748b"; | ||
|
|
||
| tradeoffChart.options.plugins.legend.labels.color = legendColor; | ||
| tradeoffChart.options.scales.y.ticks.color = textColor; | ||
| tradeoffChart.update(); | ||
|
|
||
| // Re-render main chart to ensure background colors look good (though they are explicit RGBA) | ||
| memoryChart.update(); | ||
| } | ||
|
|
||
| // --- Helper Functions --- | ||
| function initMemory(mode) { | ||
| currentAlgo = mode; | ||
| stopAutoRun(); | ||
| comparisons = 0; | ||
| document.getElementById("sort-comparisons").textContent = "0"; | ||
|
|
||
| if (mode === "sort" || mode === "reduction" || mode === "blocksum") { | ||
| // Random data | ||
| memoryData = Array.from( | ||
| { length: MEMORY_SIZE }, | ||
| () => Math.floor(Math.random() * 80) + 10 | ||
| ); | ||
| } else if (mode === "vectoradd") { | ||
| // Structured data: First half A, second half B | ||
| const half = MEMORY_SIZE / 2; | ||
| memoryData = Array.from({ length: MEMORY_SIZE }, (_, i) => | ||
| i < half | ||
| ? Math.floor(Math.random() * 40) + 10 | ||
| : Math.floor(Math.random() * 40) + 10 | ||
| ); | ||
| } | ||
|
|
||
| // Reset Algo states | ||
| sortPhase = 0; | ||
| reductionStride = 1; | ||
| blockStride = 1; | ||
| vectorAddDone = false; | ||
|
|
||
| // Update Context Text | ||
| updateContextText(mode); | ||
| updateChart(); | ||
| updateActiveMaskUI(new Array(MEMORY_SIZE).fill(false)); | ||
| document.getElementById("sort-phase").textContent = "READY"; | ||
| } | ||
|
|
||
| function updateContextText(mode) { | ||
| const title = document.getElementById("sim-description"); | ||
| const detail = document.getElementById("sim-details"); | ||
|
|
||
| if (mode === "sort") { | ||
| title.innerHTML = | ||
| "Recreating <strong>Odd-Even Transposition Sort</strong>."; | ||
| detail.textContent = | ||
| "Adjacent pairs are compared and swapped. Even indices first, then odd indices."; | ||
| } else if (mode === "reduction") { | ||
| title.innerHTML = | ||
| "Recreating <strong>Parallel Reduction (Sum)</strong>."; | ||
| detail.textContent = | ||
| "Values collapse into the first index. The 'Stride' doubles each step (1, 2, 4, 8...). Active threads halve each step."; | ||
| } else if (mode === "blocksum") { | ||
| title.innerHTML = "Recreating <strong>Block Shared Sum</strong>."; | ||
| detail.textContent = | ||
| "Simulating reduction within independent blocks of 8. Threads cooperate locally."; | ||
| } else if (mode === "vectoradd") { | ||
| title.innerHTML = "Recreating <strong>Vector Addition</strong>."; | ||
| detail.textContent = | ||
| "Splitting memory in half (Vector A & B). Threads add A[i] + B[i] simultaneously."; | ||
| } | ||
| } | ||
|
|
||
| function updateChart() { | ||
| memoryChart.data.datasets[0].data = memoryData; | ||
| // Colors: Different scheme for Vector Add to visualize split | ||
| if (currentAlgo === "vectoradd") { | ||
| const half = MEMORY_SIZE / 2; | ||
| memoryChart.data.datasets[0].backgroundColor = memoryData.map( | ||
| (v, i) => | ||
| i < half | ||
| ? `rgba(99, 102, 241, ${0.4 + v / 150})` | ||
| : `rgba(236, 72, 153, ${0.4 + v / 150})` | ||
| ); | ||
| memoryChart.data.datasets[0].borderColor = memoryData.map((v, i) => | ||
| i < half ? `#6366f1` : `#ec4899` | ||
| ); | ||
| } else { | ||
| memoryChart.data.datasets[0].backgroundColor = memoryData.map( | ||
| (v) => `rgba(99, 102, 241, ${0.4 + v / 150})` | ||
| ); | ||
| memoryChart.data.datasets[0].borderColor = "#6366f1"; | ||
| } | ||
| memoryChart.update(); | ||
| } | ||
|
|
||
| function updateActiveMaskUI(activeArray) { | ||
| const container = document.getElementById("thread-mask"); | ||
| container.innerHTML = ""; | ||
| activeArray.forEach((active) => { | ||
| const dot = document.createElement("div"); | ||
| dot.className = `flex-1 h-full rounded-sm transition-all duration-150 ${ | ||
| active | ||
| ? "bg-green-400 shadow-[0_0_5px_rgba(74,222,128,0.5)]" | ||
| : "bg-slate-200 dark:bg-slate-800" | ||
| }`; | ||
| container.appendChild(dot); | ||
| }); | ||
| } | ||
|
|
||
| // --- Step Logic Handlers --- | ||
|
|
||
| function stepSort() { | ||
| let activeThreads = new Array(MEMORY_SIZE).fill(false); | ||
| for (let i = sortPhase; i < MEMORY_SIZE - 1; i += 2) { | ||
| activeThreads[i] = true; | ||
| comparisons++; | ||
| if (memoryData[i] > memoryData[i + 1]) { | ||
| let temp = memoryData[i]; | ||
| memoryData[i] = memoryData[i + 1]; | ||
| memoryData[i + 1] = temp; | ||
| } | ||
| } | ||
| document.getElementById("sort-phase").textContent = | ||
| sortPhase === 0 ? "EVEN PHASE" : "ODD PHASE"; | ||
| sortPhase = 1 - sortPhase; | ||
| updateActiveMaskUI(activeThreads); | ||
| return true; | ||
| } | ||
|
|
||
| function stepReduction() { | ||
| if (reductionStride >= MEMORY_SIZE) { | ||
| document.getElementById("sort-phase").textContent = "DONE"; | ||
| updateActiveMaskUI(new Array(MEMORY_SIZE).fill(false)); | ||
| return false; | ||
| } | ||
|
|
||
| let activeThreads = new Array(MEMORY_SIZE).fill(false); | ||
| let activeCount = 0; | ||
|
|
||
| for (let i = 0; i < MEMORY_SIZE; i++) { | ||
| // Check if this thread is a "receiver" in this stride | ||
| if ( | ||
| i % (reductionStride * 2) === 0 && | ||
| i + reductionStride < MEMORY_SIZE | ||
| ) { | ||
| activeThreads[i] = true; | ||
| activeCount++; | ||
| memoryData[i] += memoryData[i + reductionStride]; | ||
| memoryData[i + reductionStride] = 0; // Clear the sourced value for visual clarity | ||
| comparisons++; // Treating addition as an 'op' | ||
| } | ||
| } | ||
|
|
||
| if (activeCount === 0) { | ||
| document.getElementById("sort-phase").textContent = "DONE"; | ||
| return false; | ||
| } | ||
|
|
||
| document.getElementById( | ||
| "sort-phase" | ||
| ).textContent = `STRIDE ${reductionStride}`; | ||
| reductionStride *= 2; | ||
| updateActiveMaskUI(activeThreads); | ||
| return true; | ||
| } | ||
|
|
||
| function stepBlockSum() { | ||
| // Block size 8 | ||
| const BLOCK_SIZE = 8; | ||
| if (blockStride >= BLOCK_SIZE) { | ||
| document.getElementById("sort-phase").textContent = "DONE"; | ||
| updateActiveMaskUI(new Array(MEMORY_SIZE).fill(false)); | ||
| return false; | ||
| } | ||
|
|
||
| let activeThreads = new Array(MEMORY_SIZE).fill(false); | ||
| let activeCount = 0; | ||
|
|
||
| for (let i = 0; i < MEMORY_SIZE; i++) { | ||
| // Local index within block | ||
| let localIdx = i % BLOCK_SIZE; | ||
| // Only operate if we are a receiver in the local block reduction tree | ||
| if ( | ||
| localIdx % (blockStride * 2) === 0 && | ||
| localIdx + blockStride < BLOCK_SIZE | ||
| ) { | ||
| activeThreads[i] = true; | ||
| activeCount++; | ||
| memoryData[i] += memoryData[i + blockStride]; | ||
| memoryData[i + blockStride] = 0; // Visual clear | ||
| comparisons++; | ||
| } | ||
| } | ||
|
|
||
| if (activeCount === 0) { | ||
| document.getElementById("sort-phase").textContent = "DONE"; | ||
| return false; | ||
| } | ||
|
|
||
| document.getElementById( | ||
| "sort-phase" | ||
| ).textContent = `LOCAL STRIDE ${blockStride}`; | ||
| blockStride *= 2; | ||
| updateActiveMaskUI(activeThreads); | ||
| return true; | ||
| } | ||
|
|
||
| function stepVectorAdd() { | ||
| if (vectorAddDone) { | ||
| document.getElementById("sort-phase").textContent = "DONE"; | ||
| updateActiveMaskUI(new Array(MEMORY_SIZE).fill(false)); | ||
| return false; | ||
| } | ||
|
|
||
| const half = MEMORY_SIZE / 2; | ||
| let activeThreads = new Array(MEMORY_SIZE).fill(false); | ||
|
|
||
| for (let i = 0; i < half; i++) { | ||
| activeThreads[i] = true; | ||
| memoryData[i] += memoryData[i + half]; | ||
| // memoryData[i + half] = 0; // Optional: clear source B | ||
| comparisons++; | ||
| } | ||
|
|
||
| document.getElementById("sort-phase").textContent = "EXECUTING KERNEL"; | ||
| vectorAddDone = true; | ||
| updateActiveMaskUI(activeThreads); | ||
| return true; | ||
| } | ||
|
|
||
| function performStep() { | ||
| let keepRunning = false; | ||
| if (currentAlgo === "sort") keepRunning = stepSort(); | ||
| else if (currentAlgo === "reduction") keepRunning = stepReduction(); | ||
| else if (currentAlgo === "blocksum") keepRunning = stepBlockSum(); | ||
| else if (currentAlgo === "vectoradd") keepRunning = stepVectorAdd(); | ||
|
|
||
| updateChart(); | ||
| document.getElementById("sort-comparisons").textContent = comparisons; | ||
|
|
||
| if (!keepRunning && isRunning) stopAutoRun(); | ||
| } | ||
|
|
||
| // --- 4. Event Listeners --- | ||
| document.getElementById("algo-select").addEventListener("change", (e) => { | ||
| initMemory(e.target.value); | ||
| }); | ||
|
|
||
| document.getElementById("btn-step").addEventListener("click", () => { | ||
| if (isRunning) stopAutoRun(); | ||
| performStep(); | ||
| }); | ||
|
|
||
| document.getElementById("btn-play").addEventListener("click", () => { | ||
| if (isRunning) { | ||
| stopAutoRun(); | ||
| } else { | ||
| startAutoRun(); | ||
| } | ||
| }); | ||
|
|
||
| document.getElementById("btn-reset").addEventListener("click", () => { | ||
| initMemory(currentAlgo); | ||
| }); | ||
|
|
||
| function startAutoRun() { | ||
| isRunning = true; | ||
| const btn = document.getElementById("btn-play"); | ||
| btn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | ||
| btn.classList.remove( | ||
| "bg-indigo-600", | ||
| "hover:bg-indigo-700", | ||
| "dark:bg-indigo-500", | ||
| "dark:hover:bg-indigo-400" | ||
| ); | ||
| btn.classList.add( | ||
| "bg-amber-500", | ||
| "hover:bg-amber-600", | ||
| "dark:bg-amber-600", | ||
| "dark:hover:bg-amber-500" | ||
| ); | ||
|
|
||
| intervalId = setInterval(() => { | ||
| performStep(); | ||
| }, 300); // Slower for visibility | ||
| } | ||
|
|
||
| function stopAutoRun() { | ||
| isRunning = false; | ||
| clearInterval(intervalId); | ||
| const btn = document.getElementById("btn-play"); | ||
| btn.innerHTML = '<i class="fas fa-play"></i> Auto Run'; | ||
| btn.classList.add( | ||
| "bg-indigo-600", | ||
| "hover:bg-indigo-700", | ||
| "dark:bg-indigo-500", | ||
| "dark:hover:bg-indigo-400" | ||
| ); | ||
| btn.classList.remove( | ||
| "bg-amber-500", | ||
| "hover:bg-amber-600", | ||
| "dark:bg-amber-600", | ||
| "dark:hover:bg-amber-500" | ||
| ); | ||
| } | ||
|
|
||
| // --- 5. Gemini API Integration via Cloudflare Worker Proxy --- | ||
| // IMPORTANT: Replace this URL with your Cloudflare Worker URL after deployment | ||
| const GEMINI_PROXY_URL = | ||
| "https://tinygpu-gemini-proxy.dinethnethusahan.workers.dev"; | ||
|
|
||
| async function callGemini(prompt, systemPrompt = "") { | ||
| let attempts = 0; | ||
| const maxAttempts = 3; | ||
| const delays = [1000, 2000, 4000]; | ||
|
|
||
| while (attempts < maxAttempts) { | ||
| try { | ||
| const response = await fetch(GEMINI_PROXY_URL, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ prompt, systemPrompt }), | ||
| }); | ||
|
|
||
| if (response.status === 429) { | ||
| throw new Error("Rate limit"); | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json(); | ||
| throw new Error( | ||
| errorData.error || `API Error: ${response.status}` | ||
| ); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| return data.text || "No response generated."; | ||
| } catch (error) { | ||
| attempts++; | ||
| if (attempts >= maxAttempts) { | ||
| return "Error connecting to AI service. Please try again later."; | ||
| } | ||
| await new Promise((resolve) => | ||
| setTimeout(resolve, delays[attempts - 1]) | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Feature 1: AI Assembly Architect | ||
| document | ||
| .getElementById("btn-gen-assembly") | ||
| .addEventListener("click", async () => { | ||
| const input = document.getElementById("assembly-prompt").value; | ||
| if (!input.trim()) return; | ||
|
|
||
| const output = document.getElementById("assembly-output"); | ||
| const loading = document.getElementById("ai-loading"); | ||
|
|
||
| loading.classList.remove("hidden"); | ||
| output.textContent = ""; | ||
|
|
||
| const systemPrompt = `You are an expert on TinyGPU, a custom Python-based GPU simulator. | ||
| The ISA includes: | ||
| - Registers: R0 to R7 | ||
| - Arithmetic: ADD, SUB, MUL, DIV (Format: OP DEST, SRC1, SRC2) | ||
| - Memory: LD DEST, [ADDR_REG], ST [ADDR_REG], SRC_REG | ||
| - Control: CMP OP1, OP2 (Sets flags), BEQ LABEL, BNE LABEL, JMP LABEL, SYNC (Barrier) | ||
| - Comments start with ; | ||
|
|
||
| Your task is to translate the user's natural language description of a parallel algorithm into hypothetical .tgpu assembly code. | ||
| Provide heavily commented code explaining what each part does in the context of SIMT execution. | ||
| Keep it educational.`; | ||
|
|
||
| const prompt = `Write a TinyGPU kernel for: ${input}`; | ||
| const result = await callGemini(prompt, systemPrompt); | ||
|
|
||
| loading.classList.add("hidden"); | ||
| output.textContent = result; | ||
| }); | ||
|
|
||
| // Feature 2: Concept Explainer | ||
| async function explainConcept(concept) { | ||
| const modal = document.getElementById("ai-modal"); | ||
| const content = document.getElementById("ai-modal-content"); | ||
|
|
||
| modal.classList.remove("hidden"); | ||
| // Show loading state | ||
| content.innerHTML = | ||
| '<span class="typing-indicator text-indigo-500 dark:text-indigo-400"><span></span><span></span><span></span></span>'; | ||
|
|
||
| const systemPrompt = | ||
| "You are a friendly computer science tutor specializing in GPUs and Parallel Computing. Explain concepts simply, using analogies (like cooking, traffic, or office work) suitable for a beginner. Keep explanations under 4 sentences."; | ||
| const prompt = `Explain the concept of "${concept}" in the context of the TinyGPU simulator and parallel programming.`; | ||
|
|
||
| const result = await callGemini(prompt, systemPrompt); | ||
| content.textContent = result; | ||
| } | ||
|
|
||
| function closeAiModal() { | ||
| document.getElementById("ai-modal").classList.add("hidden"); | ||
| } | ||
|
|
||
| // Initialize First Algorithm | ||
| initMemory("sort"); | ||
| updateChartTheme(); // Set initial theme | ||
| </script> |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entire JavaScript logic (over 600 lines) is embedded directly in the HTML file. This makes the file very large (1773 lines) and harder to maintain, test, and debug.
Consider extracting the JavaScript into separate files:
- A main.js file for the core simulation logic
- A theme.js file for dark mode functionality
- An ai.js file for Gemini API integration
This would improve:
- Code organization and maintainability
- Ability to unit test the JavaScript separately
- Page load performance (scripts can be cached separately)
- Developer experience when working on specific features
dev: Remove old README, update docs
Removes the outdated README file and updates the documentation. This change cleans up the project by removing redundant files and ensuring the documentation is current.