Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Surf Spot Finder (Animated Annotation)</title> | |
| <style> | |
| /* Basic simulation of Streamlit layout and theme */ | |
| body { | |
| font-family: "Source Sans Pro", sans-serif; /* Simulating Streamlit default */ | |
| margin: 0; | |
| padding: 0; | |
| background-color: #FFFFFF; /* White background */ | |
| color: #161616; /* Text color */ | |
| display: flex; | |
| min-height: 100vh; | |
| } | |
| .sidebar { | |
| width: 300px; /* Typical sidebar width */ | |
| background-color: #F0F2F6; /* Secondary background color */ | |
| padding: 20px; | |
| overflow-y: auto; /* Allow scrolling if content overflows */ | |
| flex-shrink: 0; /* Prevent sidebar from shrinking */ | |
| position: sticky; | |
| top: 0; | |
| height: 100vh; /* Make it full height */ | |
| box-sizing: border-box; /* Include padding in width */ | |
| position: relative; /* Important: Needed for z-index to create a stacking context */ | |
| z-index: 10; /* Important: Ensure sidebar content is above main content */ | |
| } | |
| .main-content { | |
| flex-grow: 1; /* Main content takes remaining space */ | |
| padding: 20px; | |
| max-width: 900px; /* Limit main content width */ | |
| margin: 0 auto; /* Center main content */ | |
| box-sizing: border-box; /* Include padding in width */ | |
| /* Main content doesn't strictly need z-index here, | |
| as sidebar's higher z-index handles stacking */ | |
| } | |
| h1, h2, h3, h4, h5, h6 { | |
| color: #161616; /* Dark text for headings */ | |
| } | |
| button { | |
| background-color: #00d230; /* Primary color */ | |
| color: white; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| margin-top: 15px; | |
| width: 100%; /* Make button fill sidebar width */ | |
| box-sizing: border-box; | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| button:disabled { | |
| background-color: #cccccc; | |
| cursor: not-allowed; | |
| } | |
| input[type="text"], | |
| input[type="number"], | |
| input[type="date"], | |
| select { | |
| width: 100%; | |
| padding: 8px; | |
| margin-top: 5px; | |
| margin-bottom: 10px; | |
| border: 1px solid #cccccc; | |
| border-radius: 4px; | |
| box-sizing: border-box; /* Include padding and border in the element's total width and height */ | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| .st-columns { | |
| display: flex; | |
| gap: 10px; /* Space between columns */ | |
| /* Removed margin-bottom from here, added to annotated-field */ | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| .st-columns > div { | |
| flex: 1; /* Distribute space */ | |
| } | |
| .st-columns > div:first-child { | |
| flex: 3; /* Simulate 3:1 ratio */ | |
| } | |
| .st-expander { | |
| border: 1px solid #cccccc; | |
| border-radius: 5px; | |
| margin-bottom: 15px; | |
| overflow: hidden; /* Hide overflow from inner content like data editor */ | |
| } | |
| .st-expander-header { | |
| padding: 10px; | |
| background-color: #F8F9FA; /* Slightly different background for header */ | |
| cursor: pointer; | |
| font-weight: bold; | |
| } | |
| .st-expander-content { | |
| padding: 10px; | |
| border-top: 1px solid #cccccc; | |
| /* By default, show content as if expander is open */ | |
| display: block; | |
| } | |
| .st-info { | |
| background-color: #e6f7ff; /* Light blue */ | |
| border-left: 5px solid #2196F3; /* Blue border */ | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| border-radius: 4px; | |
| } | |
| .st-success { | |
| color: green; | |
| font-weight: bold; | |
| margin-left: 5px; | |
| } | |
| .st-error { | |
| color: red; | |
| font-weight: bold; | |
| margin-left: 5px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 10px; | |
| margin-bottom: 10px; | |
| font-size: 0.9em; /* Smaller text in tables */ | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| th, td { | |
| border: 1px solid #dddddd; | |
| text-align: left; | |
| padding: 8px; | |
| vertical-align: top; /* Align content to top */ | |
| } | |
| th { | |
| background-color: #f2f2f2; | |
| } | |
| td:first-child { | |
| width: 80%; /* Criteria column wider */ | |
| } | |
| td:last-child { | |
| width: 20%; /* Points column narrower */ | |
| } | |
| textarea { | |
| width: 100%; | |
| padding: 8px; | |
| margin-top: 0; /* Adjust spacing */ | |
| margin-bottom: 0; /* Adjust spacing */ | |
| border: 1px solid #cccccc; | |
| border-radius: 4px; | |
| box-sizing: border-box; | |
| min-height: 50px; /* Make textareas smaller in table */ | |
| resize: vertical; /* Allow vertical resize */ | |
| font-size: 1em; /* Reset font size for input */ | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| td input[type="number"] { | |
| width: 60px; /* Smaller width for point input */ | |
| padding: 4px; | |
| margin: 0; | |
| transition: border-color 0.5s ease; /* Transition for highlight */ | |
| } | |
| /* Simulate data editor add row button */ | |
| .add-row-button { | |
| display: inline-block; | |
| background-color: #e9e9e9; | |
| border: 1px solid #cccccc; | |
| border-radius: 4px; | |
| padding: 4px 8px; | |
| font-size: 0.9em; | |
| cursor: pointer; | |
| margin-top: 5px; | |
| margin-bottom: 10px; | |
| } | |
| a { | |
| color: #00d230; /* Primary color for links */ | |
| text-decoration: none; | |
| } | |
| a:hover { | |
| text-decoration: underline; | |
| } | |
| .st-checkbox { | |
| margin-top: 10px; | |
| margin-bottom: 10px; | |
| position: relative; /* Needed for absolute positioning inside */ | |
| display: inline-block; /* Make it an inline block to contain bubble */ | |
| } | |
| /* Highlight the checkbox input itself */ | |
| .st-checkbox input[type="checkbox"] { | |
| transition: outline-color 0.5s ease; /* Highlight checkbox outline */ | |
| } | |
| .st-checkbox input[type="checkbox"].highlight { | |
| outline: 2px solid red; /* Use outline for checkbox */ | |
| outline-offset: 2px; /* Keep outline separate from element */ | |
| border-color: red; /* Also highlight border just in case */ | |
| } | |
| label { | |
| font-weight: bold; | |
| display: block; /* Make labels block elements */ | |
| margin-bottom: 5px; | |
| } | |
| /* Highlighting Style */ | |
| .highlight { | |
| border-color: red ; /* Use important to override default styles */ | |
| } | |
| .highlight.st-columns { | |
| /* Highlight borders of inputs within st-columns */ | |
| } | |
| .highlight.st-columns input, | |
| .highlight.st-columns select { | |
| border-color: red ; | |
| } | |
| /* Container for Annotated Fields */ | |
| .annotated-field { | |
| position: relative; /* Parent for absolute positioning of bubble */ | |
| margin-bottom: 25px; /* Add space below each field group to make room for bubble */ | |
| } | |
| /* Adjust margin for the very last annotated field if needed */ | |
| .sidebar .annotated-field:last-of-type { | |
| margin-bottom: 15px; /* Less margin after the button */ | |
| } | |
| /* Speech Bubble Style */ | |
| .speech-bubble { | |
| position: absolute; | |
| background-color: #ffffcc; /* Light yellow */ | |
| border: 1px solid #ffcc00; /* Orange border */ | |
| padding: 8px 12px; /* More padding */ | |
| border-radius: 8px; /* Rounder corners */ | |
| font-size: 0.85em; /* Slightly larger font */ | |
| z-index: 10; /* Higher z-index within the sidebar's stacking context */ | |
| width: 180px; /* Fixed width */ | |
| box-shadow: 2px 2px 5px rgba(0,0,0,0.2); | |
| pointer-events: none; /* Allow clicks to pass through */ | |
| line-height: 1.4; | |
| /* Position bubble to the right */ | |
| left: calc(100% + 15px); /* 15px space from the right edge of parent */ | |
| top: 50%; /* Vertically center */ | |
| transform: translateY(-50%); /* Adjust vertical centering */ | |
| opacity: 0; /* Initially hidden */ | |
| visibility: hidden; /* Initially hidden */ | |
| transition: opacity 0.5s ease, visibility 0.5s ease; /* Animation */ | |
| } | |
| /* Speech Bubble Pointer (Points Left) */ | |
| .speech-bubble::before { | |
| content: ''; | |
| position: absolute; | |
| width: 0; | |
| height: 0; | |
| border-top: 8px solid transparent; | |
| border-bottom: 8px solid transparent; | |
| border-right: 8px solid #ffcc00; /* Border color */ | |
| left: -8px; /* Position at the edge */ | |
| top: 50%; | |
| transform: translateY(-50%); /* Vertically center pointer */ | |
| } | |
| .speech-bubble::after { | |
| content: ''; | |
| position: absolute; | |
| width: 0; | |
| height: 0; | |
| /* Inner pointer */ | |
| border-top: 7px solid transparent; | |
| border-bottom: 7px solid transparent; | |
| border-right: 7px solid #ffffcc; /* Background color */ | |
| left: -7px; /* Position slightly inside border */ | |
| top: 50%; | |
| transform: translateY(-50%); /* Vertically center pointer */ | |
| } | |
| /* Adjust bubble position for date/time columns (positioned below) */ | |
| .annotated-field .st-columns + .speech-bubble { | |
| top: auto; /* Override top: 50% */ | |
| bottom: -20px; /* Position below the container */ | |
| left: 50%; /* Center horizontally */ | |
| transform: translateX(-50%); | |
| width: 280px; /* Wider bubble */ | |
| } | |
| .annotated-field .st-columns + .speech-bubble::before, | |
| .annotated-field .st-columns + .speech-bubble::after { | |
| left: 50%; | |
| top: -8px; /* Position above */ | |
| bottom: auto; /* Override bottom */ | |
| transform: translateX(-50%) rotate(90deg); /* Rotate to point up */ | |
| border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid #ffcc00; border-top: none; /* Redefine border for upward pointer */ | |
| } | |
| .annotated-field .st-columns + .speech-bubble::after { | |
| left: 50%; | |
| top: -7px; /* Position above */ | |
| bottom: auto; | |
| transform: translateX(-50%) rotate(90deg); | |
| border-left: 7px solid transparent; border-right: 7px solid transparent; border-bottom: 7px solid #ffffcc; border-top: none; | |
| } | |
| /* Position bubble for Data Editor Table (positioned to the right, slightly down) */ | |
| .annotated-field table + .speech-bubble { | |
| top: 10px; /* Position slightly below the table top */ | |
| left: calc(100% + 15px); transform: none; width: 200px; | |
| } | |
| /* Position bubble for the checkbox (positioned to the right) */ | |
| .annotated-field .st-checkbox + .speech-bubble { | |
| top: 50%; /* Vertically center with the checkbox */ | |
| left: calc(100% + 15px); | |
| transform: translateY(-50%); | |
| width: 150px; | |
| } | |
| /* Position bubble for the button (positioned to the right) */ | |
| .annotated-field button.highlight + .speech-bubble { | |
| top: 50%; /* Vertically center with the button */ | |
| left: calc(100% + 15px); | |
| transform: translateY(-50%); | |
| width: 150px; | |
| } | |
| /* Styling for tool expanders */ | |
| .tool-description { | |
| font-size: 0.9em; | |
| color: #555; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Sidebar Simulation --> | |
| <div class="sidebar"> | |
| <h3>Configuration</h3> | |
| <p style="font-size: 0.9em;">Built using <a href="https://github.com/mozilla-ai/any-agent">Any-Agent</a></p> | |
| <div class="annotated-field"> | |
| <label for="location">Enter a location</label> | |
| <div class="st-columns"> | |
| <div><input type="text" id="location" value="Los Angeles California, US"></div> | |
| <div><span class="st-success">✅</span></div> | |
| </div> | |
| <div class="speech-bubble">Enter the surf spot location (e.g., city, state, country). Used to find nearby surf spots.</div> | |
| </div> | |
| <div class="annotated-field"> | |
| <label for="max-driving-hours">Enter the maximum driving hours</label> | |
| <input type="number" id="max-driving-hours" value="2" min="1"> | |
| <div class="speech-bubble">Sets the maximum driving distance from the location, in hours.</div> | |
| </div> | |
| <div class="annotated-field"> | |
| <div class="st-columns" id="date-time-inputs"> | |
| <div> | |
| <label for="date-input">Select a date in the future</label> | |
| <input type="date" id="date-input" value="2024-12-25"> | |
| </div> | |
| <div> | |
| <label for="time-select">Select a time</label> | |
| <select id="time-select"> | |
| <option value="09:00" selected>09:00</option> | |
| <option value="10:00">10:00</option> | |
| <!-- ... other times ... --> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="speech-bubble"> | |
| <span class="speech-bubble-pointer"></span> | |
| Select the date and time for which you want the surf and wind forecasts. Must be in the future. | |
| </div> | |
| </div> | |
| <div class="annotated-field"> | |
| <label for="framework-select">Select the agent framework to use</label> | |
| <select id="framework-select"> | |
| <option>OPENAI</option> | |
| <option>LANGCHAIN</option> | |
| <option selected>ANYAGENT</option> | |
| <!-- ... other frameworks ... --> | |
| </select> | |
| <div class="speech-bubble">Choose the underlying AI agent framework (e.g., LangChain, OpenAI Assistants, Any-Agent).</div> | |
| </div> | |
| <div class="annotated-field"> | |
| <label for="model-select">Select the model to use</label> | |
| <select id="model-select"> | |
| <option>openai/gpt-4.1-nano</option> | |
| <option selected>openai/gpt-4.1-mini</option> | |
| <option>openai/gpt-4o</option> | |
| <option>gemini/gemini-2.0-flash-lite</option> | |
| <!-- ... other models ... --> | |
| </select> | |
| <div class="speech-bubble">Select the specific large language model the agent will use for reasoning and responses.</div> | |
| </div> | |
| <!-- Custom Evaluation Expander Simulation --> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">Custom Evaluation</div> | |
| <div class="st-expander-content"> | |
| <div class="annotated-field"> | |
| <label for="eval-model-select">Select the model to use for LLM-as-a-Judge evaluation</label> | |
| <select id="eval-model-select"> | |
| <option>openai/gpt-4.1-nano</option> | |
| <option>openai/gpt-4.1-mini</option> | |
| <option selected>openai/gpt-4o</option> | |
| <!-- ... other models ... --> | |
| </select> | |
| <div class="speech-bubble">Choose the LLM that will act as the judge to evaluate the agent's performance based on your criteria.</div> | |
| </div> | |
| <p style="font-weight: bold;">Checkpoints</p> | |
| <div class="annotated-field"> | |
| <table id="checkpoints-table"> | |
| <thead> | |
| <tr> | |
| <th>Criteria</th> | |
| <th>Points</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td>Check if the agent considered at least three surf spot options</td> | |
| <td>1</td> | |
| </tr> | |
| <tr> | |
| <td>Check if the agent gathered wind forecasts for each surf spot being evaluated.</td> | |
| <td>1</td> | |
| </tr> | |
| <tr> | |
| <td>Check if the agent used any web search tools to explore which surf spots should be considered</td> | |
| <td>1</td> | |
| </tr> | |
| <!-- ... simulate more rows ... --> | |
| <tr> | |
| <td><textarea placeholder="Enter criteria here..."></textarea></td> | |
| <td><input type="number" value="1" min="0"></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div class="speech-bubble"> | |
| Define specific checkpoints the agent should meet and assign points. The judge LLM scores the trace. | |
| </div> | |
| <div class="add-row-button"> | |
| + Add row <!-- This button is part of the data editor simulation --> | |
| </div> | |
| </div> | |
| <div class="annotated-field"> | |
| <div class="st-checkbox" id="run-evaluation-checkbox"> | |
| <label><input type="checkbox" checked> Run Evaluation</label> | |
| </div> | |
| <div class="speech-bubble">Toggle whether the custom evaluation runs after the agent finishes.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="annotated-field"> | |
| <button type="button" id="run-button">Run Agent 🤖</button> | |
| <div class="speech-bubble">Click to start the agent with the current configuration.</div> | |
| </div> | |
| </div> | |
| <!-- Main Content Simulation (Initial State) --> | |
| <div class="main-content"> | |
| <h1>🏄 Surf Spot Finder</h1> | |
| <p>Find the best surfing spots based on your location and preferences! <a href="https://github.com/mozilla-ai/surf-spot-finder">Github Repo</a></p> | |
| <div class="st-info"> | |
| 👈 Configure your search parameters in the sidebar and click Run to start! | |
| </div> | |
| <h3>🛠️ Available Tools</h3> | |
| <p> | |
| The AI Agent built for this project has a few tools available for use in order to find the perfect surf spot. | |
| The agent is given the freedom to use (or not use) these tools in order to accomplish the task. | |
| </p> | |
| <!-- Simulated Tool Expanders --> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">🌤️ get_wave_forecast</div> | |
| <div class="st-expander-content tool-description"> | |
| Fetches the wave forecast for a given latitude and longitude. | |
| Uses the <a href="https://open-meteo.com/en/docs/marine-weather-api">Open-Meteo Marine Weather API</a>. | |
| It provides data like wave height, direction, and period. | |
| </div> | |
| </div> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">🌤️ get_wind_forecast</div> | |
| <div class="st-expander-content tool-description"> | |
| Fetches the wind forecast for a given latitude and longitude. | |
| Uses the <a href="https://open-meteo.com/en/docs/forecast-api">Open-Meteo Forecast API</a>. | |
| It provides data like wind speed, direction, and gusts. | |
| </div> | |
| </div> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">📍 get_area_lat_lon</div> | |
| <div class="st-expander-content tool-description"> | |
| Gets the latitude and longitude for a given area name. | |
| Uses the <a href="https://nominatim.org/release-docs/develop/api/Search/">Nominatim API</a>. | |
| </div> | |
| </div> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">🌐 search_web</div> | |
| <div class="st-expander-content tool-description"> | |
| Search the web for information. Returns a list of snippets from the search results. | |
| </div> | |
| </div> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">🌐 visit_webpage</div> | |
| <div class="st-expander-content tool-description"> | |
| Visit a webpage and extract its main content. | |
| </div> | |
| </div> | |
| <!-- Note about potentially missing tools --> | |
| <p style="font-size: 0.8em; color: #888;"> | |
| <span style="color: orange;">▲</span> Some tools may not be listed depending on configuration. Please check the code for more details. | |
| </p> | |
| <h3>📊 Custom Evaluation</h3> | |
| <p> | |
| The Surf Spot Finder includes a powerful evaluation system that allows you to customize how the agent's performance is assessed. | |
| You can find these settings in the sidebar under the "Custom Evaluation" expander. | |
| </p> | |
| <!-- Learn More Expander Simulation --> | |
| <div class="st-expander"> | |
| <div class="st-expander-header">Learn more about Custom Evaluation</div> | |
| <div class="st-expander-content"> | |
| <h4>What is Custom Evaluation?</h4> | |
| <p class="tool-description"> | |
| The Custom Evaluation feature uses an LLM-as-a-Judge approach to evaluate how well the agent performs its task. | |
| An LLM will be given the complete agent trace (not just the final answer), and will assess the agent's performance based on the criteria you set. | |
| You can customize: | |
| </p> | |
| <ul> | |
| <li class="tool-description"><b>Evaluation Model</b>: Choose which LLM should act as the judge</li> | |
| <li class="tool-description"><b>Evaluation Criteria</b>: Define specific checkpoints that the agent should meet</li> | |
| <li class="tool-description"><b>Scoring System</b>: Assign points to each criterion</li> | |
| </ul> | |
| <h4>How to Use Custom Evaluation</h4> | |
| <p class="tool-description"> | |
| 1. <b>Select an Evaluation Model</b>: Choose which LLM you want to use as the judge<br> | |
| 2. <b>Edit Checkpoints</b>: Use the data editor to: | |
| </p> | |
| <ul> | |
| <li class="tool-description">Add new evaluation criteria</li> | |
| <li class="tool-description">Modify existing criteria</li> | |
| <li class="tool-description">Adjust point values</li> | |
| <li class="tool-description">Remove criteria you don't want to evaluate</li> | |
| </ul> | |
| <h4>Example Criteria</h4> | |
| <p class="tool-description">You can evaluate things like:</p> | |
| <ul> | |
| <li class="tool-description">Tool usage and success</li> | |
| <li class="tool-description">Order of operations</li> | |
| <li class="tool-description">Quality of final recommendations</li> | |
| <li class="tool-description">Response completeness</li> | |
| <li class="tool-description">Number of steps taken</li> | |
| </ul> | |
| <p class="tool-description">The evaluation results will be displayed after each agent run, showing how well the agent met your custom criteria.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| async function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| async function animateSidebarFields() { | |
| const annotatedFields = document.querySelectorAll('.sidebar .annotated-field'); | |
| const animationDelay = 3000; // Duration each bubble/highlight stays visible | |
| const fadeDelay = 500; // Duration of fade transition | |
| for (const field of annotatedFields) { | |
| // Find the input element(s) or container to highlight | |
| // Include specific inputs within columns/tables | |
| const highlightTargets = []; | |
| const directInputs = field.querySelectorAll('input, select, button'); | |
| directInputs.forEach(input => highlightTargets.push(input)); | |
| const columns = field.querySelector('.st-columns'); | |
| if (columns) highlightTargets.push(columns); // Highlight the container | |
| const table = field.querySelector('table'); | |
| if (table) highlightTargets.push(table); // Highlight the table container | |
| const bubble = field.querySelector('.speech-bubble'); | |
| if (!highlightTargets.length && !bubble) { | |
| console.warn("Annotated field found without highlight target or bubble:", field); | |
| continue; // Skip if nothing to highlight or show | |
| } | |
| // Add highlight class(es) | |
| highlightTargets.forEach(target => { | |
| // Special handling for checkbox input within the label | |
| if (target.closest('.st-checkbox')) { | |
| target.querySelector('input[type="checkbox"]').classList.add('highlight'); | |
| } else { | |
| target.classList.add('highlight'); | |
| // If it's st-columns, highlight children inputs/selects too | |
| if (target.classList.contains('st-columns')) { | |
| target.querySelectorAll('input, select').forEach(childInput => childInput.classList.add('highlight')); | |
| } | |
| /* | |
| // If it's a table, highlight textareas/inputs within td - handled by highlighting the table container itself | |
| if (target.tagName === 'TABLE') { | |
| target.querySelectorAll('td textarea, td input[type="number"]').forEach(cellInput => cellInput.classList.add('highlight')); | |
| } | |
| */ | |
| } | |
| }); | |
| // Show speech bubble | |
| if (bubble) { | |
| bubble.style.opacity = '1'; | |
| bubble.style.visibility = 'visible'; | |
| } | |
| // Simulate user interaction delay | |
| await sleep(animationDelay); | |
| // Remove highlight class(es) | |
| highlightTargets.forEach(target => { | |
| if (target.closest('.st-checkbox')) { | |
| target.querySelector('input[type="checkbox"]').classList.remove('highlight'); | |
| } else { | |
| target.classList.remove('highlight'); | |
| if (target.classList.contains('st-columns')) { | |
| target.querySelectorAll('input, select').forEach(childInput => childInput.classList.remove('highlight')); | |
| } | |
| /* | |
| // If it's a table, remove highlight from children - Handled by removing from table container | |
| if (target.tagName === 'TABLE') { | |
| target.querySelectorAll('td textarea, td input[type="number"]').forEach(cellInput => cellInput.classList.remove('highlight')); | |
| } | |
| */ | |
| } | |
| }); | |
| // Hide speech bubble | |
| if (bubble) { | |
| bubble.style.opacity = '0'; | |
| // No need to set visibility: 'hidden' immediately, transition handles it | |
| } | |
| await sleep(fadeDelay); // Delay for fade out transition before moving to next field | |
| } | |
| // Optional: Loop the animation or stop | |
| // animateSidebarFields(); // Uncomment to loop | |
| } | |
| // Start the animation when the page loads | |
| window.addEventListener('load', animateSidebarFields); | |
| </script> | |
| </body> | |
| </html> |