Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Trace Viewer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: black; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 20px; | |
| color: white; | |
| } | |
| .container { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 30px; | |
| box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); | |
| border: 1px solid rgba(255, 255, 255, 0.18); | |
| width: 100%; | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| font-size: 2.5em; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .file-input-container { | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .file-input-label { | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| display: inline-block; | |
| transition: all 0.3s ease; | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| color: white; | |
| font-size: 16px; | |
| min-width: 80px; | |
| text-align: center; | |
| } | |
| .file-input-label:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.2); | |
| } | |
| .control-separator { | |
| width: 2px; | |
| height: 30px; | |
| background: rgba(255, 255, 255, 0.2); | |
| margin-top: 8px; | |
| } | |
| #fileInput { | |
| display: none; | |
| } | |
| .visualization-container { | |
| display: flex; | |
| flex-direction: row; | |
| align-items: center; | |
| gap: 30px; | |
| margin: 20px 0; | |
| padding-left: 20px; | |
| padding-right: 20px; | |
| } | |
| .canvas-container { | |
| display: flex; | |
| justify-content: center; | |
| } | |
| #gridCanvas { | |
| border-radius: 10px; | |
| } | |
| .action-bars { | |
| width: 100%; | |
| max-width: 600px; | |
| transition: opacity 0.3s ease; | |
| } | |
| .state-tracker-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 20px; | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .action-bars h3 { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| font-size: 1.3em; | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .bars-container { | |
| display: flex; | |
| justify-content: space-around; | |
| align-items: flex-end; | |
| height: 200px; | |
| gap: 20px; | |
| } | |
| .bar-wrapper { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .bar { | |
| width: 100%; | |
| max-width: 80px; | |
| background: linear-gradient(to top, #4CAF50, #8BC34A); | |
| border-radius: 5px 5px 0 0; | |
| transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: center; | |
| padding-top: 8px; | |
| min-height: 5px; | |
| } | |
| .bar-value { | |
| color: white; | |
| font-weight: bold; | |
| font-size: 0.9em; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.5); | |
| } | |
| .bar-label { | |
| font-weight: bold; | |
| font-size: 1em; | |
| color: rgba(255, 255, 255, 0.95); | |
| padding-top: 10px; | |
| } | |
| .controls { | |
| display: flex; | |
| justify-content: center; | |
| gap: 15px; | |
| margin-top: 30px; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| background: rgba(255, 255, 255, 0.2); | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| min-width: 80px; | |
| } | |
| button:hover:not(:disabled) { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.2); | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| button.active { | |
| background: rgba(76, 175, 80, 0.5); | |
| border-color: #4CAF50; | |
| } | |
| .speed-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| } | |
| #speedSlider { | |
| width: 100px; | |
| cursor: pointer; | |
| } | |
| .legend { | |
| display: flex; | |
| gap: 15px; | |
| justify-content: center; | |
| margin-top: 20px; | |
| padding: 10px; | |
| background: rgba(255, 255, 255,0.2); | |
| border-radius: 10px; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .legend-color { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 5px; | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| text-align: center; | |
| line-height: 25px; | |
| } | |
| .placeholder-message { | |
| text-align: center; | |
| padding: 40px; | |
| color: rgba(255, 255, 255, 0.7); | |
| font-size: 1.2em; | |
| } | |
| /* HuggingFace dataset loader */ | |
| .hf-loader { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .hf-loader select { | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| background: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .hf-loader label { | |
| font-size: 14px; | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .hf-loader button { | |
| min-width: auto; | |
| } | |
| .hf-loader button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .hf-loader .hf-icon { | |
| font-size: 16px; | |
| } | |
| /* Header section */ | |
| .header-section { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| #paramsMenuToggle { | |
| background: rgba(255, 255, 255, 0.2); | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| #paramsMenuToggle:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: translateY(-2px); | |
| } | |
| /* Collapsible params menu */ | |
| .params-menu { | |
| position: fixed; | |
| top: 80px; | |
| right: 20px; | |
| max-width: 400px; | |
| max-height: 60vh; | |
| overflow-y: auto; | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); | |
| z-index: 1000; | |
| transition: transform 0.3s, opacity 0.3s; | |
| color: #333; | |
| } | |
| .params-menu.collapsed { | |
| transform: translateY(-20px); | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .params-section { | |
| margin-bottom: 20px; | |
| } | |
| .params-section h3 { | |
| color: #667eea; | |
| margin-bottom: 10px; | |
| font-size: 1.1em; | |
| } | |
| #gridParamsContent, | |
| #modelParamsContent { | |
| font-size: 0.9em; | |
| line-height: 1.6; | |
| } | |
| #gridParamsContent div, | |
| #modelParamsContent div { | |
| margin: 4px 0; | |
| } | |
| /* Updated visualization container - side by side */ | |
| .visualization-container { | |
| display: flex; | |
| gap: 0; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin: 20px 0; | |
| min-height: 100%; | |
| } | |
| .canvas-container { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: flex-start; | |
| } | |
| .canvas-container h3 { | |
| text-align: center; | |
| margin-bottom: 50px; | |
| font-size: 1.2em; | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .canvas-container h3 #stepInfo { | |
| font-size: 0.9em; | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| /* Decoded grid container */ | |
| .decoded-grid-container { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: flex-start; | |
| transition: opacity 0.3s ease; | |
| } | |
| .decoded-grid-container.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| visibility: hidden; | |
| } | |
| .decoded-grid-container h3 { | |
| text-align: center; | |
| margin-bottom: 15px; | |
| font-size: 1.2em; | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| #decodedGridCanvas { | |
| border-radius: 10px; | |
| cursor: pointer; | |
| } | |
| .layer-selector-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| justify-content: center; | |
| } | |
| .layer-selector-container label { | |
| font-size: 0.9em; | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .layer-selector-container select { | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| background: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| border: 1px solid rgba(255, 255, 255, 0.5); | |
| cursor: pointer; | |
| font-size: 0.85em; | |
| } | |
| /* Controls and legend section */ | |
| .controls-legend-section { | |
| margin: 30px 0; | |
| } | |
| /* Token displays section */ | |
| .token-displays-section { | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 15px; | |
| } | |
| .token-display { | |
| margin-bottom: 25px; | |
| } | |
| .token-display h3 { | |
| margin-bottom: 10px; | |
| font-size: 1.2em; | |
| color: rgba(255, 255, 255, 0.95); | |
| } | |
| .collapsible-header { | |
| cursor: pointer; | |
| user-select: none; | |
| transition: color 0.2s; | |
| } | |
| .collapsible-header:hover { | |
| color: rgba(255, 255, 255, 1); | |
| } | |
| .collapse-icon { | |
| display: inline-block; | |
| transition: transform 0.2s; | |
| font-size: 0.8em; | |
| } | |
| .token-container { | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 15px; | |
| border-radius: 8px; | |
| line-height: 1.8; | |
| min-height: 60px; | |
| font-size: 0.85em; | |
| transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease; | |
| max-height: 2000px; | |
| overflow: hidden; | |
| } | |
| .token-container.collapsed { | |
| max-height: 0; | |
| padding: 0 15px; | |
| opacity: 0; | |
| min-height: 0; | |
| } | |
| /* Help section */ | |
| .help-section { | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 10px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .help-content { | |
| padding: 15px; | |
| line-height: 1.6; | |
| font-size: 0.9em; | |
| transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease; | |
| max-height: 2000px; | |
| overflow: hidden; | |
| } | |
| .help-content.collapsed { | |
| max-height: 0; | |
| padding: 0 15px; | |
| opacity: 0; | |
| } | |
| .help-content h4 { | |
| margin-top: 15px; | |
| margin-bottom: 10px; | |
| color: rgba(255, 255, 255, 0.9); | |
| font-size: 1.05em; | |
| } | |
| .help-content h4:first-child { | |
| margin-top: 0; | |
| } | |
| .help-content ul { | |
| margin: 10px 0; | |
| padding-left: 25px; | |
| } | |
| .help-content li { | |
| margin: 6px 0; | |
| } | |
| .help-content ul ul { | |
| margin: 5px 0; | |
| } | |
| .help-content p { | |
| margin: 10px 0; | |
| } | |
| /* Token styling */ | |
| .token-span { | |
| padding: 2px 2px; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| white-space: pre; | |
| transition: all 0.2s; | |
| display: inline-block; | |
| font-size: 1em; | |
| } | |
| .token-span:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 2px 8px rgba(255, 255, 255,0.3); | |
| } | |
| /* Token group underlines (no background) */ | |
| .token-span.group-analysis { | |
| border-bottom: 2px solid rgba(33, 150, 243, 0.8); | |
| } | |
| .token-span.group-final { | |
| border-bottom: 2px solid rgba(255, 152, 0, 0.8); | |
| } | |
| .token-span.group-action { | |
| border-bottom: 2px solid rgba(76, 175, 80, 0.8); | |
| } | |
| .token-span.group-template { | |
| border-bottom: 2px solid rgba(158, 158, 158, 0.8); | |
| } | |
| /* Highlight for tokens with probabilities */ | |
| .token-span.has-probabilities { | |
| box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.5); | |
| background: rgba(255, 235, 59, 0.15); | |
| } | |
| /* Highlight for tokens with probes */ | |
| .token-span.has-probe { | |
| box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.5); | |
| background: rgba(102, 126, 234, 0.15); | |
| } | |
| /* Combined: token with both group and probabilities */ | |
| .token-span.group-analysis.has-probabilities, | |
| .token-span.group-final.has-probabilities, | |
| .token-span.group-action.has-probabilities, | |
| .token-span.group-template.has-probabilities { | |
| box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.7); | |
| } | |
| /* Combined: token with both group and probes */ | |
| .token-span.group-analysis.has-probe, | |
| .token-span.group-final.has-probe, | |
| .token-span.group-action.has-probe, | |
| .token-span.group-template.has-probe { | |
| box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.7); | |
| } | |
| .token-span.pinned { | |
| border: 2px solid #667eea ; | |
| box-shadow: 0 0 8px rgba(102, 126, 234, 0.5) ; | |
| } | |
| /* Tooltips */ | |
| .token-tooltip { | |
| position: fixed; | |
| background: rgba(40, 40, 40, 0.95); | |
| color: white; | |
| padding: 12px; | |
| border-radius: 8px; | |
| z-index: 2000; | |
| min-width: 200px; | |
| max-width: 350px; | |
| font-size: 0.9em; | |
| box-shadow: 0 4px 12px rgba(255, 255, 255,0.5); | |
| pointer-events: none; | |
| } | |
| .token-tooltip .tooltip-section { | |
| margin: 8px 0; | |
| display: flex; | |
| flex-direction: row; | |
| } | |
| .token-tooltip .tooltip-label { | |
| font-weight: bold; | |
| margin-right: 4px; | |
| color: #a0c4ff; | |
| } | |
| .token-tooltip select { | |
| width: 100%; | |
| padding: 4px; | |
| border-radius: 4px; | |
| background: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| border: none; | |
| pointer-events: auto; | |
| cursor: pointer; | |
| } | |
| .tile-tooltip { | |
| position: fixed; | |
| background: rgba(40, 40, 40, 0.95); | |
| color: white; | |
| padding: 12px; | |
| border-radius: 8px; | |
| z-index: 2000; | |
| min-width: 200px; | |
| font-size: 0.9em; | |
| box-shadow: 0 4px 12px rgba(255, 255, 255,0.5); | |
| } | |
| .tile-tooltip .tooltip-title { | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| color: #a0c4ff; | |
| } | |
| /* Mini bar chart (for both token and tile tooltips) */ | |
| .tile-tooltip .bar-item, | |
| .token-tooltip .bar-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin: 6px 0; | |
| } | |
| .tile-tooltip .bar-label, | |
| .token-tooltip .bar-label { | |
| min-width: 60px; | |
| font-size: 0.85em; | |
| } | |
| .tile-tooltip .bar-wrapper, | |
| .token-tooltip .bar-wrapper { | |
| flex: 1; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| position: relative; | |
| height: 16px; | |
| } | |
| .tile-tooltip .bar-fill, | |
| .token-tooltip .bar-fill { | |
| height: 100%; | |
| background: black; | |
| border-radius: 3px; | |
| transition: width 0.3s ease; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| } | |
| .tile-tooltip .bar-wrapper::after, | |
| .token-tooltip .bar-wrapper::after { | |
| content: attr(data-percentage); | |
| position: absolute; | |
| right: 4px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: white; | |
| font-size: 0.75em; | |
| font-weight: bold; | |
| pointer-events: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header-section"> | |
| <h2>Project Telos Trace Viewer</h2> | |
| <button id="paramsMenuToggle">⚙️ Config</button> | |
| </div> | |
| <div id="paramsMenu" class="params-menu collapsed"> | |
| <div class="params-section"> | |
| <h3>Grid Parameters</h3> | |
| <div id="gridParamsContent"></div> | |
| </div> | |
| <div class="params-section"> | |
| <h3>Model Parameters</h3> | |
| <div id="modelParamsContent"></div> | |
| </div> | |
| </div> | |
| <div id="placeholderMessage" class="placeholder-message"> | |
| Select a trajectory from the 🤗 HuggingFace dataset or upload a JSON file to start | |
| </div> | |
| <div class="visualization-container"> | |
| <div class="canvas-container" id="canvasContainer" style="display: none;"> | |
| <h3>Grid State <span id="stepInfo"></span></h3> | |
| <canvas id="gridCanvas"></canvas> | |
| </div> | |
| <div id="decodedGridContainer" class="decoded-grid-container hidden"> | |
| <h3>Decoded Grid State</h3> | |
| <div class="layer-selector-container"> | |
| <label for="layerSelector">Probe Layer:</label> | |
| <select id="layerSelector"></select> | |
| </div> | |
| <canvas id="decodedGridCanvas"></canvas> | |
| </div> | |
| </div> | |
| <div class="controls-legend-section"> | |
| <div class="controls"> | |
| <div class="hf-loader"> | |
| <span class="hf-icon">🤗</span> | |
| <label>Probes:</label> | |
| <select id="hfProbeType"> | |
| <option value="pre_reasoning">Pre-reasoning</option> | |
| <option value="post_reasoning">Post-reasoning</option> | |
| </select> | |
| <label>Size:</label> | |
| <select id="hfSize"> | |
| <option value="7">7</option> | |
| <option value="11">11</option> | |
| <option value="15">15</option> | |
| </select> | |
| <label>Complexity:</label> | |
| <select id="hfComplexity"> | |
| <option value="0.0">0.0</option> | |
| <option value="0.2">0.2</option> | |
| <option value="0.4">0.4</option> | |
| <option value="0.6">0.6</option> | |
| <option value="0.8">0.8</option> | |
| <option value="1.0">1.0</option> | |
| </select> | |
| <label>#</label> | |
| <select id="hfIndex"> | |
| <option value="0">0</option> | |
| <option value="1">1</option> | |
| <option value="2">2</option> | |
| <option value="3">3</option> | |
| <option value="4">4</option> | |
| <option value="5">5</option> | |
| <option value="6">6</option> | |
| <option value="7">7</option> | |
| <option value="8">8</option> | |
| <option value="9">9</option> | |
| </select> | |
| <button id="hfLoadBtn">Load</button> | |
| </div> | |
| <div class="control-separator"></div> | |
| <label for="fileInput" class="file-input-label" id="fileInputLabel"> | |
| 📁 Load JSON | |
| </label> | |
| <input type="file" id="fileInput" accept=".json" style="display: none;"> | |
| <div class="control-separator"></div> | |
| <button id="playPauseBtn">▶️ Play</button> | |
| <button id="backStepBtn">⏪ Back</button> | |
| <button id="stepBtn">⏩ Step</button> | |
| <button id="resetBtn">🔄 Reset</button> | |
| <div class="speed-control"> | |
| <label>Speed:</label> | |
| <input type="range" id="speedSlider" min="100" max="2000" value="1000" step="100"> | |
| <span id="speedValue">1.0s</span> | |
| </div> | |
| </div> | |
| <div class="legend" id="dynamicLegend" style="display: none;"></div> | |
| </div> | |
| <div class="help-section"> | |
| <h3 class="collapsible-header" id="helpHeader"> | |
| <span class="collapse-icon">▶</span> How to Use | |
| </h3> | |
| <div id="helpContent" class="help-content collapsed"> | |
| <h4>Getting Started with Trace Viewer</h4> | |
| <p>To use Trace Viewer, select a trajectory from the <b>🤗 HuggingFace dataset</b> using the size, complexity, and index selectors, or upload a <b>JSON trace file</b> using the 📁 Load JSON button. Once loaded, the first grid state will become visible, alongside two prompt and model output sections containing input/output tokens for the current trace step.</p> | |
| <p>Use the buttons ▶️ Play, ⏸️ Pause, ⏩ Next, ⏪ Back and 🔄 Reset to move across trace steps, and use the slider to control animation speed. A white arrow on the agent tile (A) indicates the predicted action direction from the model. The arrow points in the direction predicted by the agent. A summary of grid and model parameters is available in the ⚙️ Config tab.</p> | |
| <h4>Token Visual Indicators</h4> | |
| <ul> | |
| <li><strong>Colored Underlines</strong> indicate token types: | |
| <ul> | |
| <li><span style="border-bottom: 2px solid rgba(33, 150, 243, 0.8);">Blue</span> = Reasoning tokens</li> | |
| <li><span style="border-bottom: 2px solid rgba(255, 152, 0, 0.8);">Orange</span> = Final answer tokens</li> | |
| <li><span style="border-bottom: 2px solid rgba(76, 175, 80, 0.8);">Green</span> = Tokens matching valid output actions</li> | |
| <li><span style="border-bottom: 2px solid rgba(158, 158, 158, 0.8);">Gray</span> = Special tokens used by the tokenizer</li> | |
| </ul> | |
| </li> | |
| <li><strong>Colored Highlights</strong> indicate the token has additional information visible upon hovering: | |
| <ul> | |
| <li><span style="box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.5);">Yellow</span> = Prediction probabilities</li> | |
| <li><span style="box-shadow: inset 0 0 0 2px rgba(102, 126, 234, 0.5);">Purple</span> = Probing results</li> | |
| </ul> | |
| </li> | |
| </ul> | |
| <p>Tokens can be hovered to show an info tooltip with next-token probabilities displayed as bar charts. Tokens with probe information will also display a decoded grid showing the model's internal representation of tile types. Tiles marked with "X" have no probe data available. Clicking on a token pins its position; when advancing steps, the decoded grid will update to show probe data for the same token position in the new step (if available). This allows tracking how the model's internal representation evolves across steps. Use the layer selector dropdown above the decoded grid to switch between different model layers. Hovering on decoded grid tiles shows the prediction probabilities for all tile classes as bar charts. Click the pinned token again to unpin.</p> | |
| </div> | |
| </div> | |
| <div class="token-displays-section" style="display: none;"> | |
| <div class="token-display"> | |
| <h3 class="collapsible-header" id="promptHeader"> | |
| <span class="collapse-icon">▶</span> Prompt Template | |
| </h3> | |
| <div id="promptTokens" class="token-container collapsed"></div> | |
| </div> | |
| <div class="token-display"> | |
| <h3 class="collapsible-header" id="outputHeader"> | |
| <span class="collapse-icon">▼</span> Model Output | |
| </h3> | |
| <div id="outputTokens" class="token-container"></div> | |
| </div> | |
| </div> | |
| <div style="height: 200px"></div> | |
| </div> | |
| <script> | |
| // DOM references | |
| const canvas = document.getElementById('gridCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const canvasContainer = document.getElementById('canvasContainer'); | |
| const decodedCanvas = document.getElementById('decodedGridCanvas'); | |
| const decodedCtx = decodedCanvas.getContext('2d'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const backStepBtn = document.getElementById('backStepBtn'); | |
| const stepBtn = document.getElementById('stepBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const speedSlider = document.getElementById('speedSlider'); | |
| const speedValue = document.getElementById('speedValue'); | |
| const stepInfo = document.getElementById('stepInfo'); | |
| const placeholderMessage = document.getElementById('placeholderMessage'); | |
| const paramsMenuToggle = document.getElementById('paramsMenuToggle'); | |
| const paramsMenu = document.getElementById('paramsMenu'); | |
| const gridParamsContent = document.getElementById('gridParamsContent'); | |
| const modelParamsContent = document.getElementById('modelParamsContent'); | |
| const dynamicLegend = document.getElementById('dynamicLegend'); | |
| const promptTokens = document.getElementById('promptTokens'); | |
| const outputTokens = document.getElementById('outputTokens'); | |
| const decodedGridContainer = document.getElementById('decodedGridContainer'); | |
| const tokenDisplaysSection = document.querySelector('.token-displays-section'); | |
| const fileInputLabel = document.getElementById('fileInputLabel'); | |
| const promptHeader = document.getElementById('promptHeader'); | |
| const outputHeader = document.getElementById('outputHeader'); | |
| const helpHeader = document.getElementById('helpHeader'); | |
| const helpContent = document.getElementById('helpContent'); | |
| const layerSelector = document.getElementById('layerSelector'); | |
| const hfProbeType = document.getElementById('hfProbeType'); | |
| const hfSize = document.getElementById('hfSize'); | |
| const hfComplexity = document.getElementById('hfComplexity'); | |
| const hfIndex = document.getElementById('hfIndex'); | |
| const hfLoadBtn = document.getElementById('hfLoadBtn'); | |
| // Global state | |
| let viewerData = null; | |
| let currentStepIndex = 0; | |
| let isPlaying = false; | |
| let animationTimeout = null; | |
| let uiState = { | |
| currentStepIndex: 0, | |
| isPlaying: false, | |
| pinnedToken: null, // { stepIndex, context, tokenIndex, element } | |
| hoveredToken: null, | |
| selectedLayer: {}, // Map: tokenKey -> layerName | |
| paramsMenuCollapsed: true, | |
| tokenGroupColors: { | |
| "analysis": "rgba(227, 242, 253, 0.6)", | |
| "final": "rgba(255, 243, 224, 0.6)", | |
| "action": "rgba(241, 248, 233, 0.6)", | |
| "template": "rgba(245, 245, 245, 0.6)" | |
| } | |
| }; | |
| let currentTooltip = null; | |
| let decodedGridTileProbes = null; | |
| // Tile color mapping (will be dynamically built from legend) | |
| let tileColorMap = {}; | |
| // Helper functions | |
| function getTileColor(tileType) { | |
| // Return color from dynamic legend mapping | |
| if (tileColorMap[tileType]) { | |
| return tileColorMap[tileType]; | |
| } | |
| return '#CCCCCC'; // Default gray | |
| } | |
| function buildTileColorMap() { | |
| if (!viewerData || !viewerData.grid_params || !viewerData.grid_params.legend) { | |
| return; | |
| } | |
| const legend = viewerData.grid_params.legend; | |
| const colorPalette = { | |
| 'wall': '#666666', | |
| 'empty': '#E8F5E9', | |
| 'agent': '#F44336', | |
| 'goal': '#4CAF50', | |
| 'start': '#2196F3', | |
| 'fog': '#444444', | |
| 'unknown_obj': '#FF9800' | |
| }; | |
| tileColorMap = {}; | |
| Object.keys(legend).forEach((tileType, index) => { | |
| tileColorMap[tileType] = colorPalette[tileType] || `hsl(${index * 45}, 70%, 60%)`; | |
| }); | |
| } | |
| function validateData(data) { | |
| if (!data.grid_params) throw new Error('Missing grid_params'); | |
| if (!data.model_params) throw new Error('Missing model_params'); | |
| if (!data.prompt) throw new Error('Missing prompt'); | |
| if (!data.steps || !Array.isArray(data.steps)) throw new Error('Invalid steps'); | |
| // Validate legend structure | |
| if (!data.grid_params.legend) throw new Error('Missing legend'); | |
| for (const [key, val] of Object.entries(data.grid_params.legend)) { | |
| if (!val.symbol || !val.description) { | |
| throw new Error(`Invalid legend entry: ${key}`); | |
| } | |
| } | |
| } | |
| // Event listeners | |
| speedSlider.addEventListener('input', (e) => { | |
| speedValue.textContent = (e.target.value / 1000).toFixed(1) + 's'; | |
| }); | |
| paramsMenuToggle.addEventListener('click', () => { | |
| uiState.paramsMenuCollapsed = !uiState.paramsMenuCollapsed; | |
| if (uiState.paramsMenuCollapsed) { | |
| paramsMenu.classList.add('collapsed'); | |
| } else { | |
| paramsMenu.classList.remove('collapsed'); | |
| } | |
| }); | |
| function loadTrajectoryData(data, label) { | |
| // Validate data structure | |
| validateData(data); | |
| // Store data | |
| viewerData = data; | |
| currentStepIndex = 0; | |
| uiState.currentStepIndex = 0; | |
| uiState.pinnedToken = null; | |
| uiState.selectedLayer = {}; | |
| // Build color map from legend | |
| buildTileColorMap(); | |
| // Update file label | |
| const displayLabel = label.length > 20 ? label.substring(0, 17) + '...' : label; | |
| fileInputLabel.textContent = `📁 ${displayLabel}`; | |
| // Render UI components | |
| renderParamsMenu(); | |
| renderLegend(); | |
| resetAnimation(); | |
| displayStep(0); | |
| // Show/hide UI elements | |
| placeholderMessage.style.display = 'none'; | |
| canvasContainer.style.display = 'flex'; | |
| dynamicLegend.style.display = 'flex'; | |
| tokenDisplaysSection.style.display = 'block'; | |
| } | |
| fileInput.addEventListener('change', async (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| try { | |
| const text = await file.text(); | |
| const data = JSON.parse(text); | |
| loadTrajectoryData(data, file.name); | |
| } catch (error) { | |
| alert('Error loading JSON file: ' + error.message); | |
| console.error(error); | |
| } | |
| } | |
| }); | |
| hfLoadBtn.addEventListener('click', async () => { | |
| const probeType = hfProbeType.value; | |
| const size = hfSize.value; | |
| const comp = hfComplexity.value; | |
| const idx = hfIndex.value; | |
| const fileName = `together_ai_openai_gpt-oss-20b_size${size}_comp${comp}_${idx}.json`; | |
| const url = `https://huggingface.co/datasets/project-telos/trajectories_with_cognitive_map_probe_examples/resolve/main/${probeType}/size${size}/${fileName}`; | |
| hfLoadBtn.disabled = true; | |
| hfLoadBtn.textContent = 'Loading...'; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch trajectory (${response.status})`); | |
| } | |
| const data = await response.json(); | |
| const shortProbe = probeType === 'pre_reasoning' ? 'pre' : 'post'; | |
| loadTrajectoryData(data, `${shortProbe}_s${size}_c${comp}_${idx}`); | |
| } catch (error) { | |
| alert('Error loading from HuggingFace: ' + error.message); | |
| console.error(error); | |
| } finally { | |
| hfLoadBtn.disabled = false; | |
| hfLoadBtn.textContent = 'Load'; | |
| } | |
| }); | |
| function renderParamsMenu() { | |
| if (!viewerData) return; | |
| // Render grid params (exclude legend) | |
| gridParamsContent.innerHTML = ''; | |
| const gridParams = viewerData.grid_params; | |
| for (const [key, value] of Object.entries(gridParams)) { | |
| if (key !== 'legend') { | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<strong>${key}:</strong> ${JSON.stringify(value)}`; | |
| gridParamsContent.appendChild(div); | |
| } | |
| } | |
| // Render model params | |
| modelParamsContent.innerHTML = ''; | |
| const modelParams = viewerData.model_params; | |
| for (const [key, value] of Object.entries(modelParams)) { | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<strong>${key}:</strong> ${JSON.stringify(value)}`; | |
| modelParamsContent.appendChild(div); | |
| } | |
| } | |
| function renderLegend() { | |
| if (!viewerData || !viewerData.grid_params || !viewerData.grid_params.legend) return; | |
| dynamicLegend.innerHTML = ''; | |
| const legend = viewerData.grid_params.legend; | |
| for (const [tileType, info] of Object.entries(legend)) { | |
| const legendItem = document.createElement('div'); | |
| legendItem.className = 'legend-item'; | |
| const colorBox = document.createElement('div'); | |
| colorBox.className = 'legend-color'; | |
| colorBox.style.background = getTileColor(tileType); | |
| colorBox.textContent = info.symbol; | |
| const label = document.createElement('span'); | |
| label.textContent = info.description; | |
| legendItem.appendChild(colorBox); | |
| legendItem.appendChild(label); | |
| dynamicLegend.appendChild(legendItem); | |
| } | |
| // Add special legend item for missing probe data | |
| const missingItem = document.createElement('div'); | |
| missingItem.className = 'legend-item'; | |
| const missingBox = document.createElement('canvas'); | |
| missingBox.className = 'legend-color'; | |
| missingBox.width = 30; | |
| missingBox.height = 30; | |
| const ctx = missingBox.getContext('2d'); | |
| // Draw gray background | |
| ctx.fillStyle = '#888888'; | |
| ctx.fillRect(0, 0, 30, 30); | |
| // Draw X | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(5, 5); | |
| ctx.lineTo(25, 25); | |
| ctx.moveTo(25, 5); | |
| ctx.lineTo(5, 25); | |
| ctx.stroke(); | |
| const missingLabel = document.createElement('span'); | |
| missingLabel.textContent = 'Missing Info'; | |
| missingItem.appendChild(missingBox); | |
| missingItem.appendChild(missingLabel); | |
| dynamicLegend.appendChild(missingItem); | |
| } | |
| function drawOriginalGrid(gridStateLines, agentAction = null) { | |
| if (!gridStateLines || gridStateLines.length === 0) return; | |
| // Grid state is an array of strings, first line is coordinates | |
| // Skip the first line (coordinate headers) and parse the rest | |
| const gridLines = gridStateLines.slice(1).map(line => line.trim()).filter(line => line.length > 0); | |
| const cellSize = 30; | |
| const padding = 20; | |
| const labelPadding = 20; | |
| // Parse grid to extract symbols (skip row number at start of each line) | |
| const grid = gridLines.map(line => { | |
| const parts = line.split(/\s+/); | |
| // First part is row number, rest are symbols | |
| return parts.slice(1); | |
| }); | |
| if (grid.length === 0 || !grid[0]) return; | |
| const numRows = grid.length; | |
| const numCols = grid[0].length; | |
| canvas.width = numCols * cellSize + padding * 2 + labelPadding; | |
| canvas.height = numRows * cellSize + padding * 2 + labelPadding; | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw coordinate labels | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; | |
| ctx.font = '11px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| // Column labels along top | |
| for (let c = 0; c < numCols; c++) { | |
| ctx.fillText(c, c * cellSize + padding + labelPadding + cellSize/2, padding + labelPadding/2); | |
| } | |
| // Row labels along left | |
| ctx.textAlign = 'right'; | |
| for (let r = 0; r < numRows; r++) { | |
| ctx.fillText(r, padding + labelPadding - 4, r * cellSize + padding + labelPadding + cellSize/2); | |
| } | |
| let agentPosition = null; | |
| grid.forEach((row, y) => { | |
| row.forEach((symbol, x) => { | |
| const xPos = x * cellSize + padding + labelPadding; | |
| const yPos = y * cellSize + padding + labelPadding; | |
| // Track agent position | |
| if (symbol === 'A') { | |
| agentPosition = { row: y, col: x, xPos, yPos }; | |
| } | |
| // Map symbol to tile type from legend | |
| let tileType = null; | |
| if (viewerData && viewerData.grid_params && viewerData.grid_params.legend) { | |
| for (const [type, info] of Object.entries(viewerData.grid_params.legend)) { | |
| if (info.symbol === symbol) { | |
| tileType = type; | |
| break; | |
| } | |
| } | |
| } | |
| // Draw cell background | |
| ctx.fillStyle = tileType ? getTileColor(tileType) : '#CCCCCC'; | |
| ctx.fillRect(xPos, yPos, cellSize, cellSize); | |
| // Draw grid lines | |
| ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; | |
| ctx.strokeRect(xPos, yPos, cellSize, cellSize); | |
| // Draw symbol | |
| if (symbol && symbol !== '_') { | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(symbol, xPos + cellSize/2, yPos + cellSize/2); | |
| } | |
| }); | |
| }); | |
| // Draw arrow on agent tile if agent_action is provided | |
| if (agentAction && agentPosition) { | |
| drawActionArrow(agentPosition, agentAction, cellSize); | |
| } | |
| } | |
| function drawActionArrow(agentPosition, agentAction, cellSize) { | |
| const centerX = agentPosition.xPos + cellSize / 2; | |
| const centerY = agentPosition.yPos + cellSize / 2; | |
| const arrowLength = cellSize * 0.50; | |
| const arrowHeadSize = 7; | |
| // Calculate arrow end point based on direction | |
| let endX = centerX; | |
| let endY = centerY; | |
| switch (agentAction) { | |
| case 'UP': | |
| endY -= arrowLength; | |
| break; | |
| case 'DOWN': | |
| endY += arrowLength; | |
| break; | |
| case 'LEFT': | |
| endX -= arrowLength; | |
| break; | |
| case 'RIGHT': | |
| endX += arrowLength; | |
| break; | |
| default: | |
| return; // Unknown action, don't draw | |
| } | |
| // Calculate arrowhead points | |
| const angle = Math.atan2(endY - centerY, endX - centerX); | |
| const arrowHead1X = endX - arrowHeadSize * Math.cos(angle - Math.PI / 6); | |
| const arrowHead1Y = endY - arrowHeadSize * Math.sin(angle - Math.PI / 6); | |
| const arrowHead2X = endX - arrowHeadSize * Math.cos(angle + Math.PI / 6); | |
| const arrowHead2Y = endY - arrowHeadSize * Math.sin(angle + Math.PI / 6); | |
| // Draw arrowhead | |
| ctx.fillStyle = 'rgba(255, 255, 255, 1)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(endX, endY); | |
| ctx.lineTo(arrowHead1X, arrowHead1Y); | |
| ctx.lineTo(arrowHead2X, arrowHead2Y); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Token rendering functions | |
| function renderPromptTokens(step) { | |
| promptTokens.innerHTML = ''; | |
| // Assemble full prompt: prefix + grid_state + suffix | |
| const allTokens = [ | |
| ...(viewerData.prompt.prompt_prefix_tokens || []), | |
| ...(step.grid_state_tokens || []), | |
| ...(step.prompt_suffix_tokens || []) | |
| ]; | |
| const fragment = document.createDocumentFragment(); | |
| allTokens.forEach((token, index) => { | |
| const tokenElement = createTokenSpan(token, 'prompt', index); | |
| fragment.appendChild(tokenElement); | |
| }); | |
| promptTokens.appendChild(fragment); | |
| } | |
| function renderOutputTokens(step) { | |
| outputTokens.innerHTML = ''; | |
| if (!step.output_tokens || step.output_tokens.length === 0) { | |
| outputTokens.textContent = '(No output)'; | |
| return; | |
| } | |
| const fragment = document.createDocumentFragment(); | |
| step.output_tokens.forEach((token, index) => { | |
| const tokenElement = createTokenSpan(token, 'output', index); | |
| fragment.appendChild(tokenElement); | |
| }); | |
| outputTokens.appendChild(fragment); | |
| } | |
| function createTokenSpan(token, context, index) { | |
| // Check if token contains newlines (Ċ) | |
| if (token.token.includes('Ċ')) { | |
| // Split by newline and create multiple elements | |
| const parts = token.token.split('Ċ'); | |
| const fragment = document.createDocumentFragment(); | |
| parts.forEach((part, i) => { | |
| if (part.length > 0) { | |
| // Create span for non-empty part | |
| const span = document.createElement('span'); | |
| span.className = 'token-span'; | |
| // Replace Ġ with space for display | |
| let displayText = part.replace(/Ġ/g, ' '); | |
| span.textContent = displayText; | |
| // Apply token group colors | |
| const groupColor = getTokenGroupColor(token.token_groups || []); | |
| if (groupColor) { | |
| const groupName = getTokenGroupName(token.token_groups || []); | |
| span.classList.add(`group-${groupName}`); | |
| } | |
| // Check if token has tile_identity_probes | |
| const hasProbes = hasTileIdentityProbes(token); | |
| if (hasProbes) { | |
| span.classList.add('has-probe'); | |
| } | |
| // Check if token has probabilities | |
| if (token.probabilities && Object.keys(token.probabilities).length > 0) { | |
| span.classList.add('has-probabilities'); | |
| } | |
| // Store token data in dataset | |
| span.dataset.tokenId = token.token_id; | |
| span.dataset.context = context; | |
| span.dataset.index = index; | |
| span.dataset.tokenData = JSON.stringify(token); | |
| // Attach event listeners | |
| span.addEventListener('mouseenter', (e) => handleTokenHover(e, token, context, index, span)); | |
| span.addEventListener('mouseleave', () => handleTokenUnhover()); | |
| span.addEventListener('click', (e) => handleTokenClick(e, token, context, index, span)); | |
| fragment.appendChild(span); | |
| } | |
| // Add <br> after each part except the last | |
| if (i < parts.length - 1) { | |
| const br = document.createElement('br'); | |
| fragment.appendChild(br); | |
| } | |
| }); | |
| return fragment; | |
| } | |
| // No newlines - create single span | |
| const span = document.createElement('span'); | |
| span.className = 'token-span'; | |
| // Replace Ġ with space for display | |
| let displayText = token.token.replace(/Ġ/g, ' '); | |
| span.textContent = displayText; | |
| // Apply token group colors (priority: action > final > analysis > template) | |
| const groupColor = getTokenGroupColor(token.token_groups || []); | |
| if (groupColor) { | |
| const groupName = getTokenGroupName(token.token_groups || []); | |
| span.classList.add(`group-${groupName}`); | |
| } | |
| // Check if token has tile_identity_probes | |
| const hasProbes = hasTileIdentityProbes(token); | |
| if (hasProbes) { | |
| span.classList.add('has-probe'); | |
| } | |
| // Check if token has probabilities | |
| if (token.probabilities && Object.keys(token.probabilities).length > 0) { | |
| span.classList.add('has-probabilities'); | |
| } | |
| // Store token data in dataset | |
| span.dataset.tokenId = token.token_id; | |
| span.dataset.context = context; | |
| span.dataset.index = index; | |
| span.dataset.tokenData = JSON.stringify(token); | |
| // Attach event listeners | |
| span.addEventListener('mouseenter', (e) => handleTokenHover(e, token, context, index, span)); | |
| span.addEventListener('mouseleave', () => handleTokenUnhover()); | |
| span.addEventListener('click', (e) => handleTokenClick(e, token, context, index, span)); | |
| return span; | |
| } | |
| function getTokenGroupColor(tokenGroups) { | |
| // Priority order: action > final > analysis > template | |
| const priority = ['action', 'final', 'analysis', 'template']; | |
| for (const group of priority) { | |
| if (tokenGroups.includes(group)) { | |
| return uiState.tokenGroupColors[group]; | |
| } | |
| } | |
| return null; | |
| } | |
| function getTokenGroupName(tokenGroups) { | |
| const priority = ['action', 'final', 'analysis', 'template']; | |
| for (const group of priority) { | |
| if (tokenGroups.includes(group)) { | |
| return group; | |
| } | |
| } | |
| return null; | |
| } | |
| function hasTileIdentityProbes(token) { | |
| if (!token.probes) return false; | |
| return Object.keys(token.probes).some(key => | |
| key.startsWith('tile_identity_probe') || key.startsWith('cognitive_map_probe') | |
| ); | |
| } | |
| function getAvailableLayers(token) { | |
| if (!token.probes) return []; | |
| const layers = new Set(); | |
| for (const probeKey of Object.keys(token.probes)) { | |
| if (probeKey.startsWith('tile_identity_probe') || probeKey.startsWith('cognitive_map_probe')) { | |
| const probeData = token.probes[probeKey]; | |
| Object.keys(probeData).forEach(layer => layers.add(layer)); | |
| } | |
| } | |
| return Array.from(layers); | |
| } | |
| function getTokenKey(tokenRef) { | |
| // Key based on context and position only (not step), so layer selection persists across steps | |
| return `${tokenRef.context}_${tokenRef.tokenIndex}`; | |
| } | |
| function getTokenAtPosition(stepIndex, context, tokenIndex) { | |
| // Get token at a specific position for any step | |
| const step = viewerData.steps[stepIndex]; | |
| if (!step) return null; | |
| if (context === 'prompt') { | |
| const allTokens = [ | |
| ...(viewerData.prompt.prompt_prefix_tokens || []), | |
| ...(step.grid_state_tokens || []), | |
| ...(step.prompt_suffix_tokens || []) | |
| ]; | |
| return allTokens[tokenIndex] || null; | |
| } else if (context === 'output') { | |
| return (step.output_tokens && step.output_tokens[tokenIndex]) || null; | |
| } | |
| return null; | |
| } | |
| function getPinnedTokenData() { | |
| if (!uiState.pinnedToken) return null; | |
| // Get the token at the pinned position for the CURRENT step | |
| return getTokenAtPosition(currentStepIndex, uiState.pinnedToken.context, uiState.pinnedToken.tokenIndex); | |
| } | |
| function unpinToken() { | |
| if (uiState.pinnedToken && uiState.pinnedToken.element) { | |
| uiState.pinnedToken.element.classList.remove('pinned'); | |
| } | |
| uiState.pinnedToken = null; | |
| hideDecodedGrid(); | |
| } | |
| function hideDecodedGrid() { | |
| decodedGridContainer.classList.add('hidden'); | |
| } | |
| function showDecodedGrid() { | |
| decodedGridContainer.classList.remove('hidden'); | |
| } | |
| // Token interaction handlers | |
| function handleTokenHover(event, token, context, index, element) { | |
| // Remove existing tooltip | |
| if (currentTooltip) { | |
| document.body.removeChild(currentTooltip); | |
| } | |
| // Create tooltip | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'token-tooltip'; | |
| let tooltipHTML = `<div class="tooltip-section"><div class="tooltip-label">ID:</div>${token.token_id}</div>`; | |
| // Show probabilities if present (using bar chart format like tile tooltip) | |
| if (token.probabilities && Object.keys(token.probabilities).length > 0) { | |
| tooltipHTML += `<div style="margin-top: 10px;"><div class="tooltip-label" style="margin-bottom: 8px;">Next Token Probabilities:</div>`; | |
| // Sort by probability descending | |
| const sorted = Object.entries(token.probabilities).sort((a, b) => b[1] - a[1]); | |
| for (const [tokenText, prob] of sorted) { | |
| const percentage = (prob * 100).toFixed(1); | |
| const barWidthPercent = Math.max(1, prob * 100); // Percentage width | |
| tooltipHTML += ` | |
| <div class="bar-item"> | |
| <div class="bar-label">${tokenText}</div> | |
| <div class="bar-wrapper" data-percentage="${percentage}%"> | |
| <div class="bar-fill" style="width: ${barWidthPercent}%;"></div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| tooltipHTML += `</div>`; | |
| } | |
| tooltip.innerHTML = tooltipHTML; | |
| // Position tooltip near cursor | |
| tooltip.style.left = (event.clientX + 10) + 'px'; | |
| tooltip.style.top = (event.clientY + 10) + 'px'; | |
| document.body.appendChild(tooltip); | |
| currentTooltip = tooltip; | |
| // If token has tile_identity_probes, show decoded grid | |
| if (hasTileIdentityProbes(token) && !uiState.pinnedToken) { | |
| const tokenKey = `${context}_${index}`; | |
| const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0]; | |
| updateDecodedGrid(token, selectedLayer); | |
| } | |
| } | |
| function handleTokenUnhover() { | |
| // Remove tooltip | |
| if (currentTooltip) { | |
| document.body.removeChild(currentTooltip); | |
| currentTooltip = null; | |
| } | |
| // Hide decoded grid if not pinned | |
| if (!uiState.pinnedToken) { | |
| hideDecodedGrid(); | |
| } | |
| } | |
| function handleTokenClick(event, token, context, index, element) { | |
| event.stopPropagation(); | |
| // Check if this position is already pinned (compare by position, not step) | |
| if (uiState.pinnedToken && | |
| uiState.pinnedToken.context === context && | |
| uiState.pinnedToken.tokenIndex === index) { | |
| // Unpin | |
| unpinToken(); | |
| } else { | |
| // Unpin previous token if any | |
| if (uiState.pinnedToken && uiState.pinnedToken.element) { | |
| uiState.pinnedToken.element.classList.remove('pinned'); | |
| } | |
| // Pin this position (persists across steps) | |
| uiState.pinnedToken = { | |
| context: context, | |
| tokenIndex: index, | |
| element: element | |
| }; | |
| element.classList.add('pinned'); | |
| // Show decoded grid if token has probes | |
| if (hasTileIdentityProbes(token)) { | |
| const tokenKey = getTokenKey(uiState.pinnedToken); | |
| const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0]; | |
| updateDecodedGrid(token, selectedLayer); | |
| showDecodedGrid(); | |
| } else { | |
| // Token is pinned but has no probes - hide decoded grid | |
| hideDecodedGrid(); | |
| } | |
| } | |
| } | |
| function handleLayerChange(newLayer) { | |
| if (!uiState.pinnedToken) return; | |
| const tokenKey = getTokenKey(uiState.pinnedToken); | |
| uiState.selectedLayer[tokenKey] = newLayer; | |
| // Get token and update decoded grid | |
| const token = getPinnedTokenData(); | |
| if (token) { | |
| updateDecodedGrid(token, newLayer); | |
| } | |
| } | |
| // Add event listener for layer selector | |
| layerSelector.addEventListener('change', (e) => { | |
| handleLayerChange(e.target.value); | |
| }); | |
| // Decoded grid logic | |
| function updateDecodedGrid(token, selectedLayer) { | |
| if (!token || !token.probes) { | |
| hideDecodedGrid(); | |
| return; | |
| } | |
| // Populate layer selector | |
| const layers = getAvailableLayers(token); | |
| layerSelector.innerHTML = ''; | |
| layers.forEach(layer => { | |
| const option = document.createElement('option'); | |
| option.value = layer; | |
| option.textContent = layer; | |
| if (layer === selectedLayer) { | |
| option.selected = true; | |
| } | |
| layerSelector.appendChild(option); | |
| }); | |
| // Extract tile identity probes | |
| const tileProbes = extractTileIdentityProbes(token.probes, selectedLayer); | |
| // Render decoded grid | |
| const n_rows = viewerData.grid_params.grid_height; | |
| const n_cols = viewerData.grid_params.grid_width; | |
| renderDecodedGrid(tileProbes, n_rows, n_cols); | |
| // Store for hover interactions | |
| decodedGridTileProbes = tileProbes; | |
| showDecodedGrid(); | |
| } | |
| function extractTileIdentityProbes(probes, selectedLayer) { | |
| const tileProbes = {}; | |
| for (const probeKey of Object.keys(probes)) { | |
| if (probeKey.startsWith('tile_identity_probe') || probeKey.startsWith('cognitive_map_probe')) { | |
| // Extract row and column from probe key | |
| const match = probeKey.match(/r(\d+)_c(\d+)/); | |
| if (match) { | |
| const row = parseInt(match[1]); | |
| const col = parseInt(match[2]); | |
| const key = `${row}_${col}`; | |
| // Get probabilities for the selected layer | |
| const probeData = probes[probeKey]; | |
| if (probeData[selectedLayer]) { | |
| tileProbes[key] = probeData[selectedLayer]; | |
| } | |
| } | |
| } | |
| } | |
| return tileProbes; | |
| } | |
| function renderDecodedGrid(tileProbes, n_rows, n_cols) { | |
| const cellSize = 30; | |
| const padding = 20; | |
| const labelPadding = 20; | |
| decodedCanvas.width = n_cols * cellSize + padding * 2 + labelPadding; | |
| decodedCanvas.height = n_rows * cellSize + padding * 2 + labelPadding; | |
| decodedCtx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| decodedCtx.fillRect(0, 0, decodedCanvas.width, decodedCanvas.height); | |
| // Draw coordinate labels | |
| decodedCtx.fillStyle = 'rgba(255, 255, 255, 0.6)'; | |
| decodedCtx.font = '11px Arial'; | |
| decodedCtx.textAlign = 'center'; | |
| decodedCtx.textBaseline = 'middle'; | |
| // Column labels along top | |
| for (let c = 0; c < n_cols; c++) { | |
| decodedCtx.fillText(c, c * cellSize + padding + labelPadding + cellSize/2, padding + labelPadding/2); | |
| } | |
| // Row labels along left | |
| decodedCtx.textAlign = 'right'; | |
| for (let r = 0; r < n_rows; r++) { | |
| decodedCtx.fillText(r, padding + labelPadding - 4, r * cellSize + padding + labelPadding + cellSize/2); | |
| } | |
| for (let row = 0; row < n_rows; row++) { | |
| for (let col = 0; col < n_cols; col++) { | |
| const xPos = col * cellSize + padding + labelPadding; | |
| const yPos = row * cellSize + padding + labelPadding; | |
| const key = `${row}_${col}`; | |
| if (tileProbes[key]) { | |
| // Find tile type with highest probability | |
| const probabilities = tileProbes[key]; | |
| const priorityTypes = ['wall', 'agent', 'goal']; | |
| let maxProb = -1; | |
| let maxTileType = null; | |
| let bestPriorityProb = -1; | |
| let bestPriorityType = null; | |
| for (const [tileType, prob] of Object.entries(probabilities)) { | |
| if (prob > maxProb) { | |
| maxProb = prob; | |
| maxTileType = tileType; | |
| } | |
| if (priorityTypes.includes(tileType) && prob > bestPriorityProb) { | |
| bestPriorityProb = prob; | |
| bestPriorityType = tileType; | |
| } | |
| } | |
| // Prefer priority types (wall/agent/goal) if any is >= 20%; otherwise fall back to overall max | |
| const usePriority = bestPriorityType && bestPriorityProb >= 0.2; | |
| const displayType = usePriority ? bestPriorityType : maxTileType; | |
| const displayProb = usePriority ? bestPriorityProb : maxProb; | |
| // Draw tile with confidence-based opacity | |
| if (displayType) { | |
| // Draw dark neutral background at full opacity | |
| decodedCtx.fillStyle = '#333333'; | |
| decodedCtx.fillRect(xPos, yPos, cellSize, cellSize); | |
| // Draw tile color with confidence-based opacity | |
| decodedCtx.globalAlpha = displayProb; | |
| decodedCtx.fillStyle = getTileColor(displayType); | |
| decodedCtx.fillRect(xPos, yPos, cellSize, cellSize); | |
| // Draw symbol | |
| const legend = viewerData.grid_params.legend; | |
| const symbol = legend[displayType]?.symbol || '?'; | |
| if (symbol && symbol !== '_') { | |
| decodedCtx.fillStyle = 'white'; | |
| decodedCtx.font = 'bold 16px Arial'; | |
| decodedCtx.textAlign = 'center'; | |
| decodedCtx.textBaseline = 'middle'; | |
| decodedCtx.fillText(symbol, xPos + cellSize/2, yPos + cellSize/2); | |
| } | |
| // Reset alpha | |
| decodedCtx.globalAlpha = 1.0; | |
| } | |
| } else { | |
| // No probe data - draw X (crossed out) | |
| decodedCtx.fillStyle = '#888888'; | |
| decodedCtx.fillRect(xPos, yPos, cellSize, cellSize); | |
| // Draw X | |
| decodedCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; | |
| decodedCtx.lineWidth = 2; | |
| decodedCtx.beginPath(); | |
| decodedCtx.moveTo(xPos + 5, yPos + 5); | |
| decodedCtx.lineTo(xPos + cellSize - 5, yPos + cellSize - 5); | |
| decodedCtx.moveTo(xPos + cellSize - 5, yPos + 5); | |
| decodedCtx.lineTo(xPos + 5, yPos + cellSize - 5); | |
| decodedCtx.stroke(); | |
| } | |
| // Draw grid lines | |
| decodedCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; | |
| decodedCtx.lineWidth = 1; | |
| decodedCtx.strokeRect(xPos, yPos, cellSize, cellSize); | |
| } | |
| } | |
| } | |
| // Add hover interaction for decoded grid | |
| decodedCanvas.addEventListener('mousemove', (e) => { | |
| if (!decodedGridTileProbes || !uiState.pinnedToken) return; | |
| handleDecodedGridHover(e, decodedGridTileProbes); | |
| }); | |
| decodedCanvas.addEventListener('mouseleave', () => { | |
| removeTileTooltip(); | |
| }); | |
| let currentTileTooltip = null; | |
| function handleDecodedGridHover(event, tileProbes) { | |
| const cellSize = 30; | |
| const padding = 20; | |
| const labelPadding = 20; | |
| const rect = decodedCanvas.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| // Calculate tile coordinates | |
| const col = Math.floor((x - padding - labelPadding) / cellSize); | |
| const row = Math.floor((y - padding - labelPadding) / cellSize); | |
| const n_rows = viewerData.grid_params.grid_height; | |
| const n_cols = viewerData.grid_params.grid_width; | |
| // Check if within grid bounds | |
| if (row < 0 || row >= n_rows || col < 0 || col >= n_cols) { | |
| removeTileTooltip(); | |
| return; | |
| } | |
| const key = `${row}_${col}`; | |
| const probabilities = tileProbes[key]; | |
| if (probabilities) { | |
| createTileTooltip(probabilities, row, col, event.clientX, event.clientY); | |
| } else { | |
| removeTileTooltip(); | |
| } | |
| } | |
| function createTileTooltip(probabilities, row, col, x, y) { | |
| // Remove existing tooltip | |
| removeTileTooltip(); | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'tile-tooltip'; | |
| let tooltipHTML = `<div class="tooltip-title">Tile (${row}, ${col}) Probabilities</div>`; | |
| // Keep consistent order (same as legend order) | |
| const legendOrder = viewerData.grid_params.legend; | |
| const entries = Object.keys(legendOrder).map(tileType => { | |
| return [tileType, probabilities[tileType] || 0]; | |
| }); | |
| for (const [tileType, prob] of entries) { | |
| const percentage = (prob * 100).toFixed(1); | |
| const barWidthPercent = Math.max(1, prob * 100); // Percentage width | |
| tooltipHTML += ` | |
| <div class="bar-item"> | |
| <div class="bar-label">${tileType}</div> | |
| <div class="bar-wrapper" data-percentage="${percentage}%"> | |
| <div class="bar-fill" style="width: ${barWidthPercent}%;"></div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| tooltip.innerHTML = tooltipHTML; | |
| // Position tooltip | |
| tooltip.style.left = (x + 15) + 'px'; | |
| tooltip.style.top = (y + 15) + 'px'; | |
| document.body.appendChild(tooltip); | |
| currentTileTooltip = tooltip; | |
| } | |
| function removeTileTooltip() { | |
| if (currentTileTooltip) { | |
| document.body.removeChild(currentTileTooltip); | |
| currentTileTooltip = null; | |
| } | |
| } | |
| function displayStep(stepIndex) { | |
| if (!viewerData || !viewerData.steps || stepIndex >= viewerData.steps.length) return; | |
| const step = viewerData.steps[stepIndex]; | |
| currentStepIndex = stepIndex; | |
| uiState.currentStepIndex = stepIndex; | |
| // Draw original grid with agent action arrow | |
| drawOriginalGrid(step.grid_state, step.agent_action); | |
| // Render token displays | |
| renderPromptTokens(step); | |
| renderOutputTokens(step); | |
| // Update step info | |
| updateInfo(); | |
| // If a position is pinned, update the pin for the new step | |
| if (uiState.pinnedToken) { | |
| // Clear old element reference (it's from the previous render) | |
| uiState.pinnedToken.element = null; | |
| // Find and mark the element at the pinned position in the new step | |
| const pinnedElement = findTokenElement(uiState.pinnedToken.context, uiState.pinnedToken.tokenIndex); | |
| if (pinnedElement) { | |
| pinnedElement.classList.add('pinned'); | |
| uiState.pinnedToken.element = pinnedElement; | |
| } | |
| // Get the token data at the pinned position for this step | |
| const token = getPinnedTokenData(); | |
| if (token && hasTileIdentityProbes(token)) { | |
| const tokenKey = getTokenKey(uiState.pinnedToken); | |
| const selectedLayer = uiState.selectedLayer[tokenKey] || getAvailableLayers(token)[0]; | |
| updateDecodedGrid(token, selectedLayer); | |
| showDecodedGrid(); | |
| } else { | |
| // Token exists but has no probes at this step | |
| hideDecodedGrid(); | |
| } | |
| } | |
| } | |
| function findTokenElement(context, tokenIndex) { | |
| // Find the DOM element for a token at a specific position | |
| const container = context === 'prompt' ? promptTokens : outputTokens; | |
| const tokenSpans = container.querySelectorAll('.token-span'); | |
| for (const span of tokenSpans) { | |
| if (span.dataset.context === context && parseInt(span.dataset.index) === tokenIndex) { | |
| return span; | |
| } | |
| } | |
| return null; | |
| } | |
| function animate() { | |
| if (!isPlaying || !viewerData || !viewerData.steps) { | |
| return; | |
| } | |
| if (currentStepIndex >= viewerData.steps.length - 1) { | |
| stopAnimation(); | |
| return; | |
| } | |
| currentStepIndex++; | |
| displayStep(currentStepIndex); | |
| const delay = parseInt(speedSlider.value); | |
| animationTimeout = setTimeout(animate, delay); | |
| } | |
| function step() { | |
| if (!viewerData || !viewerData.steps || currentStepIndex >= viewerData.steps.length - 1) return; | |
| stopAnimation(); | |
| currentStepIndex++; | |
| displayStep(currentStepIndex); | |
| } | |
| function backStep() { | |
| if (!viewerData || !viewerData.steps || currentStepIndex <= 0) return; | |
| stopAnimation(); | |
| currentStepIndex--; | |
| displayStep(currentStepIndex); | |
| } | |
| function startAnimation() { | |
| if (!viewerData || !viewerData.steps || viewerData.steps.length === 0) return; | |
| isPlaying = true; | |
| uiState.isPlaying = true; | |
| playPauseBtn.textContent = '⏸️ Pause'; | |
| playPauseBtn.classList.add('active'); | |
| animate(); | |
| } | |
| function stopAnimation() { | |
| isPlaying = false; | |
| uiState.isPlaying = false; | |
| playPauseBtn.textContent = '▶️ Play'; | |
| playPauseBtn.classList.remove('active'); | |
| if (animationTimeout) { | |
| clearTimeout(animationTimeout); | |
| animationTimeout = null; | |
| } | |
| } | |
| function resetAnimation() { | |
| stopAnimation(); | |
| currentStepIndex = 0; | |
| uiState.currentStepIndex = 0; | |
| if (viewerData && viewerData.steps && viewerData.steps.length > 0) { | |
| displayStep(0); | |
| } | |
| } | |
| function updateInfo() { | |
| if (!viewerData || !viewerData.steps) { | |
| stepInfo.innerHTML = ''; | |
| } else { | |
| stepInfo.innerHTML = `(${currentStepIndex + 1} / ${viewerData.steps.length})`; | |
| } | |
| } | |
| playPauseBtn.addEventListener('click', () => { | |
| if (!viewerData || !viewerData.steps) return; | |
| if (isPlaying) { | |
| stopAnimation(); | |
| } else { | |
| if (currentStepIndex >= viewerData.steps.length - 1) { | |
| resetAnimation(); | |
| } | |
| startAnimation(); | |
| } | |
| }); | |
| backStepBtn.addEventListener('click', backStep); | |
| stepBtn.addEventListener('click', step); | |
| resetBtn.addEventListener('click', resetAnimation); | |
| // Collapsible sections for prompt template and model output | |
| promptHeader.addEventListener('click', () => { | |
| promptTokens.classList.toggle('collapsed'); | |
| const icon = promptHeader.querySelector('.collapse-icon'); | |
| icon.textContent = promptTokens.classList.contains('collapsed') ? '▶' : '▼'; | |
| }); | |
| outputHeader.addEventListener('click', () => { | |
| outputTokens.classList.toggle('collapsed'); | |
| const icon = outputHeader.querySelector('.collapse-icon'); | |
| icon.textContent = outputTokens.classList.contains('collapsed') ? '▶' : '▼'; | |
| }); | |
| helpHeader.addEventListener('click', () => { | |
| helpContent.classList.toggle('collapsed'); | |
| const icon = helpHeader.querySelector('.collapse-icon'); | |
| icon.textContent = helpContent.classList.contains('collapsed') ? '▶' : '▼'; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |