Download templates/decision_tree_game.html from deedrop1140/machinelearningalgorithms: direct link, hf CLI and curl.
- Browser
- Download file 35.3 kB
-
https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/decision_tree_game.html
- Command line
-
hf download hf://spaces/deedrop1140/machinelearningalgorithms/templates/decision_tree_game.html
-
curl -L -o decision_tree_game.html https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/decision_tree_game.html
35.3 kB
| {% extends "layout.html" %} | |
| {% block content %} | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Clever Critter Classifier</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom styles to enhance attractiveness and functionality */ | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: #e2e8f0; /* Light blue-gray background */ | |
| display: block; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| max-height: 100vh; | |
| padding: 20px; | |
| overflow-x: hidden; /* Prevent horizontal scroll */ | |
| } | |
| .main-wrapper { | |
| max-width: 1400px; /* Wider to accommodate 8 nodes in level 4 */ | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| .game-container { | |
| background-color: #ffffff; | |
| padding: 2.5rem; | |
| border-radius: 1rem; | |
| box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); | |
| text-align: center; | |
| position: relative; | |
| } | |
| h1 { | |
| font-size: 3rem; | |
| font-weight: 800; | |
| margin-bottom: 1rem; | |
| color: #1a202c; | |
| letter-spacing: -0.05em; | |
| } | |
| .tagline { | |
| font-size: 1.25rem; | |
| color: #4a5568; | |
| margin-bottom: 2rem; | |
| font-style: italic; | |
| } | |
| .tree-container { | |
| position: relative; | |
| margin-top: 3rem; | |
| min-height: 850px; /* Adjusted height for 4 levels + spacing */ | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding-top: 20px; | |
| } | |
| #tree-svg { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; /* Allows clicks to pass through to nodes */ | |
| overflow: visible; | |
| } | |
| .tree-row { | |
| display: flex; | |
| justify-content: center; | |
| width: 100%; | |
| margin-bottom: 7rem; /* Spacing between levels */ | |
| position: relative; /* For z-index stacking */ | |
| z-index: 1; /* Ensure nodes are above lines */ | |
| } | |
| .tree-node { | |
| background-color: #ebf8ff; /* Tailwind blue-100 */ | |
| border: 2px solid #63b3ed; /* Tailwind blue-400 */ | |
| border-radius: 0.75rem; | |
| padding: 0.8rem 0.8rem; /* Reduced padding */ | |
| margin: 0 1rem; /* Reduced space between sibling nodes */ | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
| transition: all 0.4s ease-in-out, transform 0.2s ease-out, opacity 0.4s ease-in-out; /* Smooth transitions */ | |
| width: 220px; /* Reduced fixed width */ | |
| min-height: 110px; /* Reduced min height */ | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; /* Center content vertically */ | |
| align-items: center; /* Center content horizontally */ | |
| position: relative; | |
| opacity: 0.3; /* Faded by default */ | |
| pointer-events: none; /* Not clickable by default */ | |
| text-align: center; /* Ensure text is centered */ | |
| } | |
| .tree-node.active { | |
| border-color: #3182ce; /* Tailwind blue-600 */ | |
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25); | |
| background-color: #bee3f8; /* Tailwind blue-200 */ | |
| opacity: 1; /* Fully visible when active */ | |
| pointer-events: all; /* Clickable when active */ | |
| transform: translateY(-5px); /* Subtle lift */ | |
| z-index: 2; /* Bring active node slightly to front */ | |
| } | |
| .tree-node.path-taken { | |
| border-color: #90cdf4; /* Lighter blue for path */ | |
| background-color: #e0f2fe; /* Even lighter blue */ | |
| opacity: 0.7; /* Still visible but not as prominent as active */ | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); | |
| pointer-events: none; /* Not clickable once passed */ | |
| transform: translateY(0); /* Reset lift */ | |
| z-index: 1; | |
| } | |
| .root-node { | |
| background-color: #38a169; /* Darker green for root */ | |
| border-color: #2f855a; /* Even darker green */ | |
| color: white; | |
| padding: 1.2rem; /* Adjusted padding for root */ | |
| font-size: 1.1rem; /* Adjusted font size for root */ | |
| opacity: 1; /* Root is always fully visible */ | |
| pointer-events: all; /* Root is always clickable (its buttons) */ | |
| width: 260px; /* Slightly wider root node for distinction */ | |
| min-height: 120px; /* Slightly taller root node */ | |
| } | |
| .root-node.active { | |
| background-color: #2f855a; | |
| } | |
| .leaf-node { | |
| background-color: #d6eaff; /* Light blue for leaves, softer than green */ | |
| border-color: #63b3ed; | |
| opacity: 0.3; /* Faded by default */ | |
| pointer-events: none; /* Not clickable by default */ | |
| } | |
| .leaf-node.active { | |
| background-color: #a7d3ff; | |
| box-shadow: 0 0 20px rgba(99, 179, 237, 0.7); /* Glow effect for leaf */ | |
| transform: scale(1.05); /* Slightly pop out */ | |
| opacity: 1; /* Fully visible when reached */ | |
| pointer-events: none; /* Not clickable once reached */ | |
| } | |
| .node-question { | |
| font-size: 1.1rem; /* Adjusted font size for internal nodes */ | |
| font-weight: 700; | |
| margin-bottom: 0.8rem; /* Adjusted margin */ | |
| color: inherit; | |
| } | |
| .classification-text { | |
| font-size: 1.5rem; /* Adjusted classification text size */ | |
| font-weight: bold; | |
| color: #2f855a; | |
| padding: 0.4rem; /* Adjusted padding for leaf node text */ | |
| } | |
| .node-buttons { | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.6rem; /* Adjusted gap */ | |
| flex-wrap: wrap; | |
| margin-top: auto; /* Push buttons to bottom if node grows */ | |
| } | |
| .node-button { | |
| background-color: #4299e1; | |
| color: white; | |
| padding: 0.6rem 1.2rem; /* Adjusted padding */ | |
| border-radius: 0.5rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| border: none; | |
| transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.2s ease-in-out; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| font-size: 0.9rem; /* Adjusted font size */ | |
| } | |
| .node-button:hover:not(:disabled) { | |
| background-color: #3182ce; | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| .node-button:active:not(:disabled) { | |
| transform: translateY(0); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .node-button:disabled { | |
| background-color: #a0aec0; | |
| cursor: not-allowed; | |
| opacity: 0.7; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* --- Line Styling --- */ | |
| .connector-line { | |
| stroke: #a0aec0; /* Gray for inactive lines */ | |
| stroke-width: 3; | |
| transition: stroke 0.4s ease-in-out, stroke-width 0.4s ease-in-out, opacity 0.4s ease-in-out; | |
| } | |
| .connector-line.active-path { | |
| stroke: #3182ce ; /* Deeper blue for active path */ | |
| stroke-width: 5 ; /* Thicker line */ | |
| opacity: 1 ; /* Fully visible */ | |
| } | |
| .connector-line.inactive-path { | |
| opacity: 0.3; /* Faded lines for unchosen paths */ | |
| stroke-width: 2; /* Thinner lines */ | |
| } | |
| /* --- Control Buttons --- */ | |
| .reset-button { | |
| background-color: #e53e3e; /* Tailwind red-600 */ | |
| color: white; | |
| padding: 0.9rem 2rem; | |
| border-radius: 0.5rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| border: none; | |
| transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.2s ease-in-out; | |
| margin-top: 2rem; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .reset-button:hover { | |
| background-color: #c53030; | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| .reset-button:active { | |
| transform: translateY(0); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .llm-explanation-container { | |
| margin-top: 2rem; | |
| background-color: #f0f8ff; | |
| border: 1px solid #a7d3ff; | |
| padding: 2rem; | |
| border-radius: 0.75rem; | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); | |
| text-align: left; | |
| } | |
| .llm-explanation-container h4 { | |
| font-size: 1.25rem; | |
| font-weight: bold; | |
| margin-bottom: 1rem; | |
| color: #2c5282; | |
| } | |
| .llm-explanation-container p { | |
| font-size: 1.05rem; | |
| line-height: 1.7; | |
| color: #4a5568; | |
| } | |
| .loading-indicator { | |
| text-align: center; | |
| font-style: italic; | |
| color: #718096; | |
| padding: 1rem; | |
| } | |
| /* Confetti animation (simple CSS based) */ | |
| .confetti-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .confetti { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background-color: #f06; /* Pink */ | |
| opacity: 0; | |
| animation: fall 3s ease-out forwards; | |
| } | |
| .confetti:nth-child(2n) { background-color: #6cf; /* Blue */ } | |
| .confetti:nth-child(3n) { background-color: #ffc; /* Yellow */ } | |
| .confetti:nth-child(4n) { background-color: #9c9; /* Green */ } | |
| .confetti:nth-child(5n) { background-color: #f60; /* Orange */ } | |
| @keyframes fall { | |
| 0% { transform: translateY(0) rotateZ(0deg); opacity: 1; } | |
| 100% { transform: translateY(100vh) rotateZ(720deg); opacity: 0; } | |
| } | |
| /* Media Queries for Responsiveness */ | |
| @media (max-width: 768px) { | |
| h1 { | |
| font-size: 2rem; | |
| } | |
| .tagline { | |
| font-size: 1rem; | |
| } | |
| .tree-node { | |
| width: 90%; /* Keep width responsive on small screens */ | |
| min-height: unset; /* Remove min-height for mobile to avoid overflow */ | |
| margin: 1rem auto; | |
| padding: 1rem; | |
| } | |
| .tree-row { | |
| flex-direction: column; | |
| align-items: center; | |
| margin-bottom: 3rem; | |
| } | |
| .node-buttons { | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .node-button { | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| } | |
| .root-node { | |
| width: 90%; /* Responsive root */ | |
| min-height: unset; /* Remove min-height for mobile */ | |
| padding: 1.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="main-wrapper"> | |
| <div class="game-container"> | |
| <h1 class="text-gray-900">πΎ Clever Critter Classifier! πΏ</h1> | |
| <p class="tagline text-gray-700"> | |
| Think of your favorite animal, answer wisely, and watch the tree reveal its identity! | |
| </p> | |
| <div class="tree-container" id="game-tree-container"> | |
| <svg id="tree-svg"></svg> | |
| <div class="tree-row level-1"> | |
| <div id="node-root" class="tree-node root-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-root" class="node-buttons"></div> | |
| </div> | |
| </div> | |
| <div class="tree-row level-2"> | |
| <div id="node-land-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-land-branch" class="node-buttons"></div> | |
| </div> | |
| <div id="node-water-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-water-branch" class="node-buttons"></div> | |
| </div> | |
| </div> | |
| <div class="tree-row level-3"> | |
| <div id="node-fur-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-fur-branch" class="node-buttons"></div> | |
| </div> | |
| <div id="node-no-fur-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-no-fur-branch" class="node-buttons"></div> | |
| </div> | |
| <div id="node-water-mammal-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-water-mammal-branch" class="node-buttons"></div> | |
| </div> | |
| <div id="node-water-non-mammal-branch" class="tree-node internal-node"> | |
| <p class="node-question"></p> | |
| <div id="buttons-water-non-mammal-branch" class="node-buttons"></div> | |
| </div> | |
| </div> | |
| <div class="tree-row level-4"> | |
| <div id="node-leaf-lion" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-cat" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-eagle" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-snake" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-dolphin" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-whale" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-fish" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| <div id="node-leaf-octopus" class="tree-node leaf-node"> | |
| <p class="classification-text"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <button id="explain-classification-button" class="node-button mt-6 hidden">β¨ Deep Dive into Classification β¨</button> | |
| <div id="llm-explanation-container" class="llm-explanation-container hidden"> | |
| <div id="llm-explanation-box" class="llm-explanation-box"></div> | |
| </div> | |
| <button id="reset-button" class="reset-button hidden">Start Over</button> | |
| </div> | |
| <div class="mt-8 p-6 bg-blue-50 rounded-lg border border-blue-200 shadow-md"> | |
| <h3 class="text-xl font-extrabold mb-4 text-center text-blue-800">Understanding Decision Trees</h3> | |
| <p class="text-gray-700 leading-relaxed mb-4"> | |
| This game is a simple, fun way to explore how a **Decision Tree** algorithm works in Machine Learning. Imagine a flowchart that helps make decisions! | |
| </p> | |
| <ul class="list-disc list-inside text-gray-800 space-y-2"> | |
| <li> | |
| <strong>π³ Root Node (Top):</strong> This is your starting point, like the first big question everyone asks. In real Decision Trees, this node considers all possible data and picks the best question to split it. | |
| </li> | |
| <li> | |
| <strong>πΏ Internal Nodes (Middle Questions):</strong> These are where you make choices based on features (like "Does it have fur?"). Each answer leads you down a specific branch, getting you closer to a final decision. | |
| </li> | |
| <li> | |
| <strong>πΈ Leaf Nodes (Bottom - Classifications):</strong> You've reached the end of a path! These are the final answers or predictions. In machine learning, this could be classifying an email as "Spam" or predicting if a customer will "Churn." | |
| </li> | |
| <li> | |
| <strong>π Paths (Decision Rules):</strong> The sequence of choices you make from the root to a leaf node creates a unique "rule." This rule can then be applied to new, unseen data to make a prediction! | |
| </li> | |
| </ul> | |
| <p class="mt-4 text-gray-700 leading-relaxed"> | |
| Just like your choices guide you through this game, Decision Trees learn these question-and-answer paths from large datasets to classify new information. | |
| </p> | |
| </div> | |
| </div> | |
| <script> | |
| const treeData = { | |
| 'root': { | |
| question: 'Does your favorite animal live on land or in water?', | |
| options: [ | |
| { text: 'Land ποΈ', next: 'land-branch', value: 'land' }, | |
| { text: 'Water π', next: 'water-branch', value: 'water' } | |
| ], | |
| level: 1 | |
| }, | |
| 'land-branch': { | |
| question: 'Does your favorite animal have fur?', | |
| options: [ | |
| { text: 'Yes π', next: 'fur-branch', value: 'fur' }, | |
| { text: 'No π', next: 'no-fur-branch', value: 'no_fur' } | |
| ], | |
| level: 2 | |
| }, | |
| 'water-branch': { | |
| question: 'Is your favorite animal a mammal?', | |
| options: [ | |
| { text: 'Yes π', next: 'water-mammal-branch', value: 'mammal' }, | |
| { text: 'No π', next: 'water-non-mammal-branch', value: 'not_mammal' } | |
| ], | |
| level: 2 | |
| }, | |
| 'fur-branch': { | |
| question: 'Is your favorite animal large?', | |
| options: [ | |
| { text: 'Yes π', next: 'leaf-lion', value: 'large' }, | |
| { text: 'No π', next: 'leaf-cat', value: 'small' } | |
| ], | |
| level: 3 | |
| }, | |
| 'no-fur-branch': { | |
| question: 'Can it fly?', | |
| options: [ | |
| { text: 'Yes π', next: 'leaf-eagle', value: 'flies' }, | |
| { text: 'No π', next: 'leaf-snake', value: 'no_fly' } | |
| ], | |
| level: 3 | |
| }, | |
| 'water-mammal-branch': { | |
| question: 'Is it found in saltwater or freshwater?', | |
| options: [ | |
| { text: 'Saltwater π§', next: 'leaf-dolphin', value: 'saltwater' }, | |
| { text: 'Freshwater π§', next: 'leaf-whale', value: 'freshwater_mammal' } // Example: Whale if freshwater | |
| ], | |
| level: 3 | |
| }, | |
| 'water-non-mammal-branch': { | |
| question: 'Does it have scales?', | |
| options: [ | |
| { text: 'Yes β ', next: 'leaf-fish', value: 'scales' }, | |
| { text: 'No β', next: 'leaf-octopus', value: 'no_scales' } // Example: Octopus if no scales | |
| ], | |
| level: 3 | |
| }, | |
| // Leaf Nodes (classifications) - all are now Level 4 | |
| 'leaf-lion': { classification: 'Lion π¦', explanation: 'Lions are large, furry land animals. They fit the criteria for this classification because they reside on land, possess fur, and are generally considered large mammals.', level: 4 }, | |
| 'leaf-cat': { classification: 'Cat π', explanation: 'Cats are small, furry land animals. This classification is reached as cats live on land, have fur, and are small in size.', level: 4 }, | |
| 'leaf-eagle': { classification: 'Eagle π¦ ', explanation: 'Eagles are non-furry land animals that can fly. They are classified here because they inhabit land, lack fur, and are characterized by their ability to fly.', level: 4 }, | |
| 'leaf-snake': { classification: 'Snake π', explanation: 'Snakes are non-furry land animals that cannot fly. This classification applies to snakes as they live on land, do not have fur, and are unable to fly.', level: 4 }, | |
| 'leaf-dolphin': { classification: 'Dolphin π¬', explanation: 'Dolphins are water mammals found in saltwater. They are classified here because they live in water, are mammals, and inhabit saltwater environments.', level: 4 }, | |
| 'leaf-whale': { classification: 'Whale π³', explanation: 'Whales are typically large water mammals. This classification applies to water animals that are mammals and are very large.', level: 4 }, | |
| 'leaf-fish': { classification: 'Fish π ', explanation: 'Fish are aquatic animals, often with scales, found in various water bodies. This classification is given to non-mammalian water creatures with scales.', level: 4 }, | |
| 'leaf-octopus': { classification: 'Octopus π', explanation: 'Octopuses are aquatic animals without scales. This classification is for water non-mammals that do not have scales.', level: 4 } | |
| }; | |
| let currentNodeLogicalId = 'root'; | |
| let currentPath = []; // Stores logical IDs of nodes in the current path | |
| let lastClassificationNode = null; // Stores the logical ID of the last classified leaf node | |
| // --- Helper Functions for DOM Manipulation and Tree Drawing --- | |
| function getElementById(id) { | |
| return document.getElementById(id); | |
| } | |
| function clearNodeButtons(nodeId) { | |
| const buttonsContainer = getElementById(`buttons-${nodeId}`); | |
| if (buttonsContainer) { | |
| buttonsContainer.innerHTML = ''; | |
| } | |
| } | |
| function drawLine(startX, startY, endX, endY, id, is_active_path = false) { | |
| const svg = getElementById('tree-svg'); | |
| let line = getElementById(id); | |
| if (!line) { | |
| line = document.createElementNS("http://www.w3.org/2000/svg", "line"); | |
| line.setAttribute('id', id); | |
| line.setAttribute('class', 'connector-line'); | |
| svg.appendChild(line); | |
| } | |
| line.setAttribute('x1', startX); | |
| line.setAttribute('y1', startY); | |
| line.setAttribute('x2', endX); | |
| line.setAttribute('y2', endY); | |
| // Apply classes based on active path | |
| if (is_active_path) { | |
| line.classList.add('active-path'); | |
| line.classList.remove('inactive-path'); | |
| } else { | |
| line.classList.remove('active-path'); | |
| line.classList.add('inactive-path'); | |
| } | |
| } | |
| // Gets absolute position relative to the SVG container (center bottom for parent, center top for child) | |
| function getAbsolutePosition(element, type = 'bottom') { | |
| if (!element) return { x: 0, y: 0, width: 0, height: 0 }; | |
| const rect = element.getBoundingClientRect(); | |
| const svg = getElementById('tree-svg'); | |
| const svgRect = svg.getBoundingClientRect(); | |
| let x = rect.left - svgRect.left + rect.width / 2; | |
| let y; | |
| if (type === 'bottom') { | |
| y = rect.top - svgRect.top + rect.height; // Bottom center | |
| } else { // 'top' | |
| y = rect.top - svgRect.top; // Top center | |
| } | |
| return { x, y, width: rect.width, height: rect.height }; | |
| } | |
| // Function to draw and update all lines based on the current path | |
| function drawAllLines() { | |
| const svg = getElementById('tree-svg'); | |
| // Clear existing lines to redraw all | |
| while (svg.firstChild) { | |
| svg.removeChild(svg.firstChild); | |
| } | |
| // Iterate through all possible connections in treeData | |
| for (const parentLogicalId in treeData) { | |
| const parentData = treeData[parentLogicalId]; | |
| if (parentData.options) { // If it's a node with options (not a leaf) | |
| parentData.options.forEach(option => { | |
| const childLogicalId = option.next; | |
| const parentNode = getElementById(`node-${parentLogicalId}`); | |
| const childNode = getElementById(`node-${childLogicalId}`); | |
| if (parentNode && childNode) { | |
| const parentPos = getAbsolutePosition(parentNode, 'bottom'); | |
| const childPos = getAbsolutePosition(childNode, 'top'); | |
| const lineId = `line-${parentLogicalId}-to-${childLogicalId}`; | |
| // Check if this connection is part of the current active path | |
| const isPathSegment = currentPath.includes(parentLogicalId) && currentPath.includes(childLogicalId); | |
| drawLine(parentPos.x, parentPos.y, childPos.x, childPos.y, lineId, isPathSegment); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| // Confetti effect | |
| function launchConfetti() { | |
| const confettiContainer = document.createElement('div'); | |
| confettiContainer.classList.add('confetti-container'); | |
| getElementById('game-tree-container').appendChild(confettiContainer); | |
| for (let i = 0; i < 50; i++) { | |
| const confetti = document.createElement('div'); | |
| confetti.classList.add('confetti'); | |
| confetti.style.left = `${Math.random() * 100}%`; | |
| confetti.style.backgroundColor = `hsl(${Math.random() * 360}, 70%, 60%)`; | |
| confetti.style.animationDelay = `${Math.random() * 0.5}s`; | |
| confetti.style.animationDuration = `${3 + Math.random() * 2}s`; | |
| confettiContainer.appendChild(confetti); | |
| } | |
| // Remove confetti after animation | |
| setTimeout(() => { | |
| confettiContainer.remove(); | |
| }, 5000); | |
| } | |
| // --- Game Logic Functions --- | |
| function initializeNode(nodeLogicalId) { | |
| const nodeData = treeData[nodeLogicalId]; | |
| const nodeElement = getElementById(`node-${nodeLogicalId}`); | |
| const buttonsContainer = getElementById(`buttons-${nodeLogicalId}`); | |
| if (!nodeElement || !nodeData) { | |
| console.error(`Node element or data not found for ID: ${nodeLogicalId}`); | |
| return; | |
| } | |
| // Reset all nodes to inactive/faded state first, and clear all buttons | |
| document.querySelectorAll('.tree-node').forEach(node => { | |
| node.classList.remove('active', 'path-taken'); | |
| // Set initial opacity and pointer-events for all nodes | |
| node.style.opacity = '0.3'; | |
| node.style.pointerEvents = 'none'; | |
| clearNodeButtons(node.id.replace('node-', '')); | |
| }); | |
| // Set 'path-taken' for all nodes in the current path (except the current active one) | |
| currentPath.forEach(pathId => { | |
| const pathNodeElement = getElementById(`node-${pathId}`); | |
| if (pathNodeElement && pathId !== nodeLogicalId) { | |
| pathNodeElement.classList.add('path-taken'); | |
| pathNodeElement.style.opacity = '0.7'; // Less faded | |
| pathNodeElement.style.pointerEvents = 'none'; // Still not clickable | |
| } | |
| }); | |
| // Set content and activate the current node | |
| nodeElement.classList.add('active'); | |
| nodeElement.style.opacity = '1'; | |
| nodeElement.style.pointerEvents = 'all'; | |
| if (nodeData.classification) { // It's a leaf node | |
| nodeElement.querySelector('.classification-text').textContent = nodeData.classification; | |
| clearNodeButtons(nodeLogicalId); | |
| lastClassificationNode = nodeLogicalId; | |
| getElementById('explain-classification-button').classList.remove('hidden'); | |
| getElementById('reset-button').classList.remove('hidden'); | |
| launchConfetti(); // Yay, classification! | |
| } else { // It's an internal/root node | |
| const questionElement = nodeElement.querySelector('.node-question'); | |
| questionElement.textContent = nodeData.question; | |
| buttonsContainer.innerHTML = ''; // Clear previous buttons | |
| nodeData.options.forEach(option => { | |
| const button = document.createElement('button'); | |
| button.classList.add('node-button'); | |
| button.textContent = option.text; | |
| button.onclick = () => chooseOption(nodeLogicalId, option.next); | |
| buttonsContainer.appendChild(button); | |
| }); | |
| getElementById('explain-classification-button').classList.add('hidden'); | |
| getElementById('llm-explanation-container').classList.add('hidden'); | |
| getElementById('reset-button').classList.add('hidden'); | |
| } | |
| // Always redraw all lines to reflect current path highlight | |
| drawAllLines(); | |
| } | |
| function chooseOption(parentNodeLogicalId, nextNodeLogicalId) { | |
| // Add current node to path if not already there (prevents duplicates on restart) | |
| if (!currentPath.includes(parentNodeLogicalId)) { | |
| currentPath.push(parentNodeLogicalId); | |
| } | |
| currentPath.push(nextNodeLogicalId); | |
| currentNodeLogicalId = nextNodeLogicalId; | |
| // Initialize the next node (which will apply active/path-taken styles and redraw lines) | |
| initializeNode(nextNodeLogicalId); | |
| } | |
| function startGame() { | |
| // Reset game state | |
| currentNodeLogicalId = 'root'; | |
| currentPath = []; | |
| lastClassificationNode = null; | |
| // Initialize all nodes with their content but set to faded/inactive state initially | |
| for (const id in treeData) { | |
| const nodeData = treeData[id]; | |
| const nodeElement = getElementById(`node-${id}`); | |
| if (nodeElement) { | |
| // Populate questions/classifications for all nodes first | |
| if (nodeData.classification) { | |
| const classificationText = nodeElement.querySelector('.classification-text'); | |
| if (classificationText) classificationText.textContent = nodeData.classification; | |
| } else if (nodeData.question) { | |
| const questionText = nodeElement.querySelector('.node-question'); | |
| if (questionText) questionText.textContent = nodeData.question; | |
| } | |
| // Reset styling | |
| nodeElement.classList.remove('active', 'path-taken'); | |
| nodeElement.style.opacity = '0.3'; | |
| nodeElement.style.pointerEvents = 'none'; | |
| clearNodeButtons(nodeElement.id.replace('node-', '')); // Corrected this line to pass proper ID | |
| } | |
| } | |
| // Activate the root node to start the game | |
| initializeNode(currentNodeLogicalId); | |
| } | |
| // Function to explain classification using LLM (placeholder for now) | |
| async function explainClassification() { | |
| const explanationContainer = getElementById('llm-explanation-container'); | |
| const explanationBox = getElementById('llm-explanation-box'); | |
| explanationContainer.classList.remove('hidden'); | |
| explanationBox.innerHTML = '<p class="loading-indicator">Loading explanation...</p>'; | |
| if (lastClassificationNode && treeData[lastClassificationNode]) { | |
| const animalName = treeData[lastClassificationNode].classification.split(' ')[0]; // Get just the name | |
| const explanationText = treeData[lastClassificationNode].explanation; | |
| // Simulate LLM response delay | |
| setTimeout(() => { | |
| explanationBox.innerHTML = ` | |
| <h4>Why ${animalName}?</h4> | |
| <p>${explanationText}</p> | |
| `; | |
| }, 1000); // 1 second delay | |
| } else { | |
| explanationBox.innerHTML = '<p class="text-red-500">No classification to explain yet. Please play the game!</p>'; | |
| } | |
| } | |
| // --- Event Listeners --- | |
| window.onload = () => { | |
| startGame(); | |
| // Re-draw lines on window resize to adjust for responsive layout changes | |
| window.addEventListener('resize', drawAllLines); | |
| }; | |
| getElementById('reset-button').addEventListener('click', startGame); | |
| getElementById('explain-classification-button').addEventListener('click', explainClassification); | |
| </script> | |
| <div class="back-button"> | |
| <a href="/decision_tree" class=" bg-gray-200 center hover:bg-gray-300 text-black-800 px-4 py-2 rounded shadow"> | |
| β Back to decision tree | |
| </a> | |
| </div> | |
| </body> | |
| </html> | |
| {% endblock %} |