explorer.html

   1<!doctype html>
   2<html lang="en">
   3    <head>
   4        <meta charset="UTF-8" />
   5        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   6        <title>Eval Explorer</title>
   7        <style>
   8            :root {
   9                /* Light theme (default) */
  10                --bg-color: #ffffff;
  11                --text-color: #333333;
  12                --header-bg: #f8f8f8;
  13                --border-color: #eaeaea;
  14                --code-bg: #f5f5f5;
  15                --link-color: #0066cc;
  16                --button-bg: #f5f5f5;
  17                --button-border: #ddd;
  18                --button-active-bg: #0066cc;
  19                --button-active-color: white;
  20                --button-active-border: #0055aa;
  21                --preview-bg: #f5f5f5;
  22                --table-line: #f0f0f0;
  23            }
  24
  25            /* Dark theme */
  26            [data-theme="dark"] {
  27                --bg-color: #1e1e1e;
  28                --text-color: #e0e0e0;
  29                --header-bg: #2d2d2d;
  30                --border-color: #444444;
  31                --code-bg: #2a2a2a;
  32                --link-color: #4da6ff;
  33                --button-bg: #333333;
  34                --button-border: #555555;
  35                --button-active-bg: #0066cc;
  36                --button-active-color: white;
  37                --button-active-border: #0055aa;
  38                --preview-bg: #2a2a2a;
  39                --table-line: #333333;
  40            }
  41
  42            /* Apply theme variables */
  43            body {
  44                font-family: monospace;
  45                line-height: 1.6;
  46                margin: 0;
  47                padding: 20px;
  48                color: var(--text-color);
  49                max-width: 1200px;
  50                margin: 0 auto;
  51                background-color: var(--bg-color);
  52            }
  53            h1 {
  54                margin-bottom: 20px;
  55                border-bottom: 1px solid var(--border-color);
  56                padding-bottom: 10px;
  57                font-family: monospace;
  58            }
  59            table {
  60                width: 100%;
  61                border-collapse: collapse;
  62                margin-bottom: 20px;
  63                table-layout: fixed; /* Ensure fixed width columns */
  64            }
  65            th,
  66            td {
  67                padding: 10px;
  68                text-align: left;
  69                border-bottom: 1px dotted var(--border-color);
  70                vertical-align: top;
  71                word-wrap: break-word; /* Ensure long content wraps */
  72                overflow-wrap: break-word;
  73            }
  74            th {
  75                background-color: var(--header-bg);
  76                font-weight: 600;
  77            }
  78            .collapsible {
  79                cursor: pointer;
  80                color: var(--link-color);
  81                text-decoration: underline;
  82            }
  83            .hidden {
  84                display: none;
  85            }
  86            .tool-name {
  87                font-weight: bold;
  88            }
  89            .tool-params {
  90                padding-left: 20px;
  91                color: #666;
  92            }
  93            pre {
  94                background-color: var(--code-bg);
  95                padding: 10px;
  96                border-radius: 5px;
  97                overflow-x: auto;
  98                max-height: 200px;
  99                margin: 10px 0;
 100                font-family: monospace;
 101                width: 100%;
 102                box-sizing: border-box;
 103                white-space: pre-wrap; /* Ensure text wraps */
 104                color: var(--text-color);
 105            }
 106            code {
 107                font-family: monospace;
 108            }
 109
 110            /* Column sizing */
 111            .turn-column {
 112                width: 3%;
 113                max-width: 3%;
 114            }
 115            .text-column {
 116                width: 22%;
 117                max-width: 22%;
 118            }
 119            .tool-column {
 120                width: 38%;
 121                max-width: 38%;
 122            }
 123            .result-column {
 124                width: 37%;
 125                max-width: 37%;
 126                overflow-x: auto;
 127            }
 128
 129            /* Content formatting */
 130            .text-content {
 131                font-family:
 132                    system-ui,
 133                    -apple-system,
 134                    BlinkMacSystemFont,
 135                    "Segoe UI",
 136                    Roboto,
 137                    Oxygen,
 138                    Ubuntu,
 139                    Cantarell,
 140                    "Open Sans",
 141                    "Helvetica Neue",
 142                    sans-serif;
 143                font-size: 0.7rem;
 144            }
 145            .action-container .action-preview,
 146            .action-container .action-full {
 147                margin-bottom: 5px;
 148            }
 149            .preview-content {
 150                white-space: pre-wrap;
 151                margin-bottom: 5px;
 152                background-color: var(--preview-bg);
 153                padding: 10px;
 154                border-radius: 5px;
 155                font-family: monospace;
 156                width: 100%;
 157                box-sizing: border-box;
 158                overflow-wrap: break-word;
 159                color: var(--text-color);
 160            }
 161            .show-more {
 162                color: var(--link-color);
 163                cursor: pointer;
 164                text-decoration: none;
 165                display: block;
 166                margin-top: 5px;
 167            }
 168            .more-inline {
 169                color: var(--link-color);
 170                cursor: pointer;
 171                text-decoration: none;
 172                display: inline;
 173                margin-left: 5px;
 174            }
 175
 176            /* Compact mode styles */
 177            .compact-mode td {
 178                padding: 5px; /* Reduced padding in compact mode */
 179            }
 180
 181            .compact-mode .preview-content {
 182                padding: 2px;
 183                margin-bottom: 2px;
 184            }
 185
 186            .compact-mode pre {
 187                padding: 5px;
 188                margin: 5px 0;
 189                white-space: pre; /* Don't wrap code in compact mode */
 190                overflow-x: auto; /* Add horizontal scrollbar */
 191            }
 192
 193            .compact-mode .result-column pre,
 194            .compact-mode .result-column .preview-content {
 195                max-width: 100%;
 196                overflow-x: auto;
 197                white-space: pre;
 198            }
 199
 200            /* Make action containers more compact */
 201            .compact-mode .action-container {
 202                margin-bottom: 2px;
 203            }
 204
 205            /* Reduce space between turns */
 206            .compact-mode tr {
 207                border-bottom: 1px solid var(--table-line);
 208            }
 209
 210            /* Tool params more compact */
 211            .compact-mode .tool-params {
 212                padding-left: 10px;
 213                margin-top: 2px;
 214            }
 215
 216            hr {
 217                margin: 10px 0;
 218                border: 0;
 219                height: 1px;
 220                background-color: var(--border-color);
 221            }
 222
 223            /* View switcher */
 224            .view-switcher {
 225                display: flex;
 226                gap: 10px;
 227                margin-bottom: 20px;
 228                align-items: center;
 229            }
 230
 231            .view-button {
 232                background-color: var(--button-bg);
 233                border: 1px solid var(--button-border);
 234                border-radius: 4px;
 235                padding: 5px 15px;
 236                cursor: pointer;
 237                font-family: monospace;
 238                font-size: 0.9rem;
 239                transition: all 0.2s ease;
 240                color: var(--text-color);
 241            }
 242
 243            .view-button:hover {
 244                background-color: var(--button-border);
 245            }
 246
 247            .view-button.active {
 248                background-color: var(--button-active-bg);
 249                color: var(--button-active-color);
 250                border-color: var(--button-active-border);
 251            }
 252
 253            /* Navigation bar styles */
 254            .thread-navigation {
 255                display: flex;
 256                align-items: center;
 257                margin-bottom: 20px;
 258                padding: 10px 0;
 259                border-bottom: 1px solid var(--border-color);
 260            }
 261
 262            .nav-button {
 263                background-color: var(--button-bg);
 264                border: 1px solid var(--button-border);
 265                border-radius: 4px;
 266                padding: 5px 15px;
 267                cursor: pointer;
 268                font-family: monospace;
 269                font-size: 0.9rem;
 270                transition: all 0.2s ease;
 271                color: var(--text-color);
 272            }
 273
 274            .nav-button:hover:not(:disabled) {
 275                background-color: var(--button-border);
 276            }
 277
 278            .nav-button:disabled {
 279                opacity: 0.5;
 280                cursor: not-allowed;
 281            }
 282
 283            .thread-indicator {
 284                margin: 0 15px;
 285                font-size: 1rem;
 286                flex-grow: 1;
 287                text-align: center;
 288            }
 289
 290            #thread-id {
 291                font-weight: bold;
 292            }
 293
 294            /* Theme switcher */
 295            .theme-switcher {
 296                margin-left: auto;
 297                display: flex;
 298                align-items: center;
 299            }
 300
 301            .theme-button {
 302                background-color: var(--button-bg);
 303                border: 1px solid var(--button-border);
 304                border-radius: 4px;
 305                padding: 5px 10px;
 306                cursor: pointer;
 307                font-size: 0.9rem;
 308                transition: all 0.2s ease;
 309                color: var(--text-color);
 310                display: flex;
 311                align-items: center;
 312            }
 313
 314            .theme-button:hover {
 315                background-color: var(--button-border);
 316            }
 317
 318            .theme-icon {
 319                margin-right: 5px;
 320                font-size: 1rem;
 321            }
 322        </style>
 323    </head>
 324    <body>
 325        <h1 id="current-filename">Thread Explorer</h1>
 326        <div class="view-switcher">
 327            <button
 328                id="full-view"
 329                class="view-button active"
 330                onclick="switchView('full')"
 331            >
 332                Full View
 333            </button>
 334            <button
 335                id="compact-view"
 336                class="view-button"
 337                onclick="switchView('compact')"
 338            >
 339                Compact View
 340            </button>
 341            <button
 342                id="export-button"
 343                class="view-button"
 344                onclick="exportThreadAsJson()"
 345                title="Export current thread as JSON"
 346            >
 347                Export
 348            </button>
 349            <div class="theme-switcher">
 350                <button
 351                    id="theme-toggle"
 352                    class="theme-button"
 353                    onclick="toggleTheme()"
 354                >
 355                    <span id="theme-icon" class="theme-icon">☀️</span>
 356                    <span id="theme-text">Light</span>
 357                </button>
 358            </div>
 359        </div>
 360        <div class="thread-navigation">
 361            <button
 362                id="prev-thread"
 363                class="nav-button"
 364                onclick="previousThread()"
 365                title="Previous thread (Ctrl+←, k, or h)"
 366                disabled
 367            >
 368                &larr; Previous
 369            </button>
 370            <div class="thread-indicator">
 371                Thread <span id="current-thread-index">1</span> of
 372                <span id="total-threads">1</span>:
 373                <span id="thread-id">Default Thread</span>
 374            </div>
 375            <button
 376                id="next-thread"
 377                class="nav-button"
 378                onclick="nextThread()"
 379                title="Next thread (Ctrl+→, j, or l)"
 380                disabled
 381            >
 382                Next &rarr;
 383            </button>
 384        </div>
 385        <table id="thread-table">
 386            <thead>
 387                <tr>
 388                    <th class="turn-column">Turn</th>
 389                    <th class="text-column">Text</th>
 390                    <th class="tool-column">Tool</th>
 391                    <th class="result-column">Result</th>
 392                </tr>
 393            </thead>
 394            <tbody id="thread-body">
 395                <!-- Content will be filled dynamically -->
 396            </tbody>
 397        </table>
 398
 399        <script>
 400            // View mode - 'full' or 'compact'
 401            let viewMode = "full";
 402
 403            // Theme mode - 'light', 'dark', or 'system'
 404            let themeMode = localStorage.getItem("theme") || "system";
 405
 406            // Function to apply theme
 407            function applyTheme(theme) {
 408                const themeIcon = document.getElementById("theme-icon");
 409                const themeText = document.getElementById("theme-text");
 410
 411                if (theme === "dark") {
 412                    document.documentElement.setAttribute("data-theme", "dark");
 413                    themeIcon.textContent = "🌙";
 414                    themeText.textContent = "Dark";
 415                } else {
 416                    document.documentElement.removeAttribute("data-theme");
 417                    themeIcon.textContent = "☀️";
 418                    themeText.textContent = "Light";
 419                }
 420            }
 421
 422            // Function to toggle between light and dark themes
 423            function toggleTheme() {
 424                // If currently system or light, switch to dark
 425                if (themeMode === "system") {
 426                    const systemDark = window.matchMedia(
 427                        "(prefers-color-scheme: dark)",
 428                    ).matches;
 429                    themeMode = systemDark ? "light" : "dark";
 430                } else {
 431                    themeMode = themeMode === "light" ? "dark" : "light";
 432                }
 433
 434                // Save preference
 435                localStorage.setItem("theme", themeMode);
 436
 437                // Apply theme
 438                applyTheme(themeMode);
 439            }
 440
 441            // Initialize theme based on system or saved preference
 442            function initTheme() {
 443                if (themeMode === "system") {
 444                    // Use system preference
 445                    const systemDark = window.matchMedia(
 446                        "(prefers-color-scheme: dark)",
 447                    ).matches;
 448                    applyTheme(systemDark ? "dark" : "light");
 449
 450                    // Listen for system theme changes
 451                    window
 452                        .matchMedia("(prefers-color-scheme: dark)")
 453                        .addEventListener("change", (e) => {
 454                            if (themeMode === "system") {
 455                                applyTheme(e.matches ? "dark" : "light");
 456                            }
 457                        });
 458                } else {
 459                    // Use saved preference
 460                    applyTheme(themeMode);
 461                }
 462            }
 463
 464            // Function to switch between view modes
 465            function switchView(mode) {
 466                viewMode = mode;
 467
 468                // Update button states
 469                document
 470                    .getElementById("full-view")
 471                    .classList.toggle("active", mode === "full");
 472                document
 473                    .getElementById("compact-view")
 474                    .classList.toggle("active", mode === "compact");
 475
 476                // Add or remove compact-mode class on the body
 477                document.body.classList.toggle(
 478                    "compact-mode",
 479                    mode === "compact",
 480                );
 481
 482                // Re-render the thread with the new view mode
 483                renderThread();
 484            }
 485            
 486            // Function to export the current thread as a JSON file
 487            function exportThreadAsJson() {
 488                // Clone the thread to avoid modifying the original
 489                const threadToExport = JSON.parse(JSON.stringify(thread));
 490                
 491                // Create a Blob with the JSON data
 492                const blob = new Blob(
 493                    [JSON.stringify(threadToExport, null, 2)],
 494                    { type: "application/json" }
 495                );
 496                
 497                // Create a download link
 498                const url = URL.createObjectURL(blob);
 499                const a = document.createElement("a");
 500                a.href = url;
 501                
 502                // Generate filename based on thread ID or index
 503                const filename = threadToExport.thread_id || 
 504                                threadToExport.filename || 
 505                                `thread-${currentThreadIndex + 1}.json`;
 506                a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
 507                
 508                // Trigger the download
 509                document.body.appendChild(a);
 510                a.click();
 511                
 512                // Clean up
 513                setTimeout(() => {
 514                    document.body.removeChild(a);
 515                    URL.revokeObjectURL(url);
 516                }, 0);
 517            }
 518            // Default dummy thread data for preview purposes
 519            let dummyThread = {
 520                messages: [
 521                    {
 522                        role: "system",
 523                        content: [{ Text: "System prompt..." }],
 524                    },
 525                    {
 526                        role: "user",
 527                        content: [
 528                            { Text: "Fix the bug: kwargs not passed..." },
 529                        ],
 530                    },
 531                    {
 532                        role: "assistant",
 533                        content: [
 534                            { Text: "I'll help you fix that bug." },
 535                            {
 536                                ToolUse: {
 537                                    name: "list_directory",
 538                                    input: { path: "fastmcp" },
 539                                    is_input_complete: true,
 540                                },
 541                            },
 542                        ],
 543                    },
 544                    {
 545                        role: "user",
 546                        content: [
 547                            {
 548                                ToolResult: {
 549                                    tool_name: "list_directory",
 550                                    is_error: false,
 551                                    content:
 552                                        "fastmcp/src\nfastmcp/tests\nfastmcp/README.md\nfastmcp/pyproject.toml\nfastmcp/.gitignore\nfastmcp/setup.py\nfastmcp/examples\nfastmcp/LICENSE",
 553                                },
 554                            },
 555                        ],
 556                    },
 557                    {
 558                        role: "assistant",
 559                        content: [
 560                            { Text: "Let's examine the code." },
 561                            {
 562                                ToolUse: {
 563                                    name: "read_file",
 564                                    input: {
 565                                        path: "fastmcp/main.py",
 566                                        start_line: 253,
 567                                        end_line: 360,
 568                                    },
 569                                    is_input_complete: true,
 570                                },
 571                            },
 572                        ],
 573                    },
 574                    {
 575                        role: "user",
 576                        content: [
 577                            {
 578                                ToolResult: {
 579                                    tool_name: "read_file",
 580                                    is_error: false,
 581                                    content:
 582                                        "def run_application(app, **kwargs):\n    return anyio.run(app, **kwargs)\n\nasync def start_server():\n    # More code...\n    # Multiple lines of code that would be displayed\n    # when clicking on the show more link\n    app = create_app()\n    await run_app(app)\n\ndef main():\n    # Initialize everything\n    anyio.run(start_server)\n    # Even more code here\n    # that would be shown when the user\n    # expands the content",
 583                                },
 584                            },
 585                        ],
 586                    },
 587                    {
 588                        role: "assistant",
 589                        content: [
 590                            { Text: "I found the issue." },
 591                            {
 592                                ToolUse: {
 593                                    name: "edit_file",
 594                                    input: {
 595                                        path: "fastmcp/core.py",
 596                                        old_string:
 597                                            "def start_server(app):\n    anyio.run(app)",
 598                                        new_string:
 599                                            "def start_server(app, **kwargs):\n    anyio.run(app, **kwargs)",
 600                                        display_description:
 601                                            "Fix kwargs passing to anyio.run",
 602                                    },
 603                                    is_input_complete: true,
 604                                },
 605                            },
 606                        ],
 607                    },
 608                    {
 609                        role: "user",
 610                        content: [
 611                            {
 612                                ToolResult: {
 613                                    tool_name: "edit_file",
 614                                    is_error: false,
 615                                    content: "Made edit to fastmcp/core.py",
 616                                },
 617                            },
 618                        ],
 619                    },
 620                    {
 621                        role: "assistant",
 622                        content: [
 623                            { Text: "Let's check if there are any errors." },
 624                            {
 625                                ToolUse: {
 626                                    name: "diagnostics",
 627                                    input: {},
 628                                    is_input_complete: true,
 629                                },
 630                            },
 631                        ],
 632                    },
 633                    {
 634                        role: "user",
 635                        content: [
 636                            {
 637                                ToolResult: {
 638                                    tool_name: "diagnostics",
 639                                    is_error: false,
 640                                    content: "No errors found",
 641                                },
 642                            },
 643                        ],
 644                    },
 645                ],
 646            };
 647
 648            // The actual thread data will be injected here when opened by eval
 649            let threadsData = window.threadsData || { threads: [dummyThread] };
 650
 651            // Initialize thread variables
 652            let threads = threadsData.threads;
 653            let currentThreadIndex = 0;
 654            let thread = threads[currentThreadIndex];
 655
 656            // Function to navigate to the previous thread
 657            function previousThread() {
 658                if (currentThreadIndex > 0) {
 659                    currentThreadIndex--;
 660                    switchToThread(currentThreadIndex);
 661                }
 662            }
 663
 664            // Function to navigate to the next thread
 665            function nextThread() {
 666                if (currentThreadIndex < threads.length - 1) {
 667                    currentThreadIndex++;
 668                    switchToThread(currentThreadIndex);
 669                }
 670            }
 671
 672            // Function to switch to a specific thread by index
 673            function switchToThread(index) {
 674                if (index >= 0 && index < threads.length) {
 675                    currentThreadIndex = index;
 676                    thread = threads[currentThreadIndex];
 677                    updateNavigationButtons();
 678                    renderThread();
 679                }
 680            }
 681
 682            // Function to update the navigation buttons state
 683            function updateNavigationButtons() {
 684                document.getElementById("prev-thread").disabled =
 685                    currentThreadIndex <= 0;
 686                document.getElementById("next-thread").disabled =
 687                    currentThreadIndex >= threads.length - 1;
 688                document.getElementById("current-thread-index").textContent =
 689                    currentThreadIndex + 1;
 690                document.getElementById("total-threads").textContent =
 691                    threads.length;
 692            }
 693
 694            function renderThread() {
 695                const tbody = document.getElementById("thread-body");
 696                tbody.innerHTML = ""; // Clear existing content
 697
 698                // Set thread name if available
 699                const threadId =
 700                    thread.thread_id || `Thread ${currentThreadIndex + 1}`;
 701                document.getElementById("thread-id").textContent = threadId;
 702
 703                // Set filename in the header if available
 704                const filename =
 705                    thread.filename || `Thread ${currentThreadIndex + 1}`;
 706                document.getElementById("current-filename").textContent =
 707                    filename;
 708
 709                // Skip system message
 710                const nonSystemMessages = thread.messages.filter(
 711                    (msg) => msg.role !== "system",
 712                );
 713
 714                let turnNumber = 0;
 715                processMessages(nonSystemMessages, tbody, turnNumber);
 716            }
 717
 718            function processMessages(messages, tbody) {
 719                let turnNumber = 0;
 720
 721                for (let i = 0; i < messages.length; i++) {
 722                    const msg = messages[i];
 723
 724                    if (isUserQuery(msg)) {
 725                        // User message starts a new turn
 726                        turnNumber++;
 727                        renderUserMessage(msg, turnNumber, tbody);
 728                    } else if (msg.role === "assistant") {
 729                        // Each assistant message is one turn
 730                        turnNumber++;
 731
 732                        // Collect all text content and tool uses for this turn
 733                        let assistantText = "";
 734                        let toolUses = [];
 735
 736                        // First, collect all text content
 737                        for (const content of msg.content) {
 738                            if (content.hasOwnProperty("Text")) {
 739                                if (assistantText) {
 740                                    assistantText +=
 741                                        "<br><br>" +
 742                                        formatContent(content.Text);
 743                                } else {
 744                                    assistantText = formatContent(content.Text);
 745                                }
 746                            } else if (content.hasOwnProperty("ToolUse")) {
 747                                toolUses.push(content.ToolUse);
 748                            }
 749                        }
 750
 751                        // Create a single row for this turn with text and tools
 752                        const row = document.createElement("tr");
 753                        row.id = `assistant-turn-${turnNumber}`;
 754
 755                        // Start with the turn number and assistant text
 756                        row.innerHTML = `
 757                            <td class="text-content">${turnNumber}</td>
 758                            <td class="text-content"><!--Assistant: <br/ -->${assistantText}</td>
 759                            <td id="tools-${turnNumber}"></td>
 760                            <td id="results-${turnNumber}"></td>
 761                        `;
 762
 763                        tbody.appendChild(row);
 764
 765                        // Add all tool calls to the tools cell
 766                        const toolsCell = document.getElementById(
 767                            `tools-${turnNumber}`,
 768                        );
 769                        const resultsCell = document.getElementById(
 770                            `results-${turnNumber}`,
 771                        );
 772
 773                        // Process all tools and their results
 774                        for (let j = 0; j < toolUses.length; j++) {
 775                            const toolUse = toolUses[j];
 776                            const toolCall = formatToolCall(
 777                                toolUse.name,
 778                                toolUse.input,
 779                            );
 780
 781                            // Add the tool call to the tools cell
 782                            if (j > 0) toolsCell.innerHTML += "<hr>";
 783                            toolsCell.innerHTML += toolCall;
 784
 785                            // Look for corresponding tool result
 786                            if (
 787                                hasMatchingToolResult(messages, i, toolUse.name)
 788                            ) {
 789                                const resultMsg = messages[i + 1];
 790                                const toolResult = findToolResult(
 791                                    resultMsg,
 792                                    toolUse.name,
 793                                );
 794
 795                                if (toolResult) {
 796                                    // Add the result to the results cell
 797                                    if (j > 0) resultsCell.innerHTML += "<hr>";
 798
 799                                    // Create a container for the result
 800                                    const resultDiv =
 801                                        document.createElement("div");
 802                                    resultDiv.className = "tool-result";
 803
 804                                    // Format and display the tool result
 805                                    formatToolResultInline(
 806                                        toolResult.content,
 807                                        resultDiv,
 808                                    );
 809                                    resultsCell.appendChild(resultDiv);
 810
 811                                    // Skip the result message in the next iteration
 812                                    if (j === toolUses.length - 1) {
 813                                        i++;
 814                                    }
 815                                }
 816                            }
 817                        }
 818                    } else if (
 819                        msg.role === "user" &&
 820                        msg.content.some((c) => c.hasOwnProperty("ToolResult"))
 821                    ) {
 822                        // Skip tool result messages as they are handled with their corresponding tool use
 823                        continue;
 824                    }
 825                }
 826            }
 827
 828            function isUserQuery(message) {
 829                return (
 830                    message.role === "user" &&
 831                    !message.content.some((c) => c.hasOwnProperty("ToolResult"))
 832                );
 833            }
 834
 835            function renderUserMessage(message, turnNumber, tbody) {
 836                const row = document.createElement("tr");
 837                row.innerHTML = `
 838                    <td>${turnNumber}</td>
 839                    <td class="text-content"><b>[User]:</b><br/> ${formatContent(message.content[0].Text)}</td>
 840                    <td></td>
 841                    <td></td>
 842                `;
 843                tbody.appendChild(row);
 844            }
 845
 846            function hasMatchingToolResult(messages, currentIndex, toolName) {
 847                return (
 848                    currentIndex + 1 < messages.length &&
 849                    messages[currentIndex + 1].role === "user" &&
 850                    messages[currentIndex + 1].content.some(
 851                        (c) =>
 852                            c.hasOwnProperty("ToolResult") &&
 853                            c.ToolResult.tool_name === toolName,
 854                    )
 855                );
 856            }
 857
 858            function findToolResult(resultMessage, toolName) {
 859                const toolResultContent = resultMessage.content.find(
 860                    (c) =>
 861                        c.hasOwnProperty("ToolResult") &&
 862                        c.ToolResult.tool_name === toolName,
 863                );
 864
 865                return toolResultContent ? toolResultContent.ToolResult : null;
 866            }
 867            function formatToolCall(name, input) {
 868                // In compact mode, format tool calls on a single line
 869                if (viewMode === "compact") {
 870                    const params = [];
 871                    const fullParams = [];
 872
 873                    // Process all parameters
 874                    for (const [key, value] of Object.entries(input)) {
 875                        if (value !== null && value !== undefined) {
 876                            // Store full parameter for expanded view
 877                            let fullValue =
 878                                typeof value === "string"
 879                                    ? `"${value}"`
 880                                    : value;
 881                            fullParams.push([key, fullValue]);
 882
 883                            // Abbreviated value for compact view
 884                            let displayValue = fullValue;
 885                            if (
 886                                typeof value === "string" &&
 887                                value.length > 30
 888                            ) {
 889                                displayValue = `"${value.substring(0, 30)}..."`;
 890                            }
 891                            params.push(`${key}=${displayValue}`);
 892                        }
 893                    }
 894
 895                    const paramString = params.join(", ");
 896                    const fullLine = `<span class="tool-name">${name}</span>(${paramString})`;
 897
 898                    // If the line is too long, add a [more] link
 899                    if (fullLine.length > 80 || params.length > 1) {
 900                        // Create a container with the compact and full views
 901                        const compactView = `<span class="tool-name">${name}</span>(${params[0]}, <span class="more-inline" onclick="toggleActionVisibility(this)">[...]</span>)`;
 902
 903                        // For the full view, use the original untruncated values
 904                        let result = `<span class="tool-name">${name}</span>(`;
 905                        const formattedParams = fullParams
 906                            .map(
 907                                (p) =>
 908                                    `&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`,
 909                            )
 910                            .join(",<br/>");
 911                        const fullView = `${result}<br/>${formattedParams}<br/>)`;
 912
 913                        return `<div class="action-container">
 914                            <div class="action-preview">${compactView}</div>
 915                            <div class="action-full hidden">${fullView}</div>
 916                        </div>`;
 917                    }
 918
 919                    return fullLine;
 920                }
 921
 922                // Regular (full) view formatting with multiple lines
 923                let result = `<span class="tool-name">${name}</span>(`;
 924                const params = [];
 925                for (const [key, value] of Object.entries(input)) {
 926                    if (value !== null && value !== undefined) {
 927                        // Format different types of values
 928                        let formattedValue =
 929                            typeof value === "string" ? `"${value}"` : value;
 930                        params.push([key, formattedValue]);
 931                    }
 932                }
 933
 934                if (params.length === 0) {
 935                    return `${result})`;
 936                } else if (params.length === 1) {
 937                    // For single parameter, just show the value without the parameter name
 938                    return `${result}${params[0][1]})`;
 939                } else {
 940                    // Format parameters
 941                    const formattedParams = params
 942                        .map((p) => `&nbsp;&nbsp;&nbsp;&nbsp;${p[0]}=${p[1]}`)
 943                        .join(",<br/>");
 944                    return `${result}<br/>${formattedParams}<br/>)`;
 945                }
 946            }
 947
 948            function toggleActionVisibility(element, remainingLines) {
 949                const container = element.closest(".action-container");
 950                const preview = container.querySelector(".action-preview");
 951                const full = container.querySelector(".action-full");
 952
 953                // Once expanded, keep it expanded
 954                full.classList.remove("hidden");
 955                preview.classList.add("hidden");
 956            }
 957
 958            function formatToolResultInline(content, targetElement) {
 959                // Count lines
 960                const lines = content.split("\n");
 961
 962                // In compact mode, show only 1 line with [more] link
 963                if (viewMode === "compact" && lines.length > 1) {
 964                    // Create container
 965                    const container = document.createElement("div");
 966
 967                    // Preview content
 968                    const previewDiv = document.createElement("div");
 969                    previewDiv.className = "preview-content";
 970
 971                    // Add the first line of content plus [more] link
 972                    const previewContent = lines[0];
 973                    previewDiv.innerHTML =
 974                        escapeHtml(previewContent) +
 975                        ` <span class="more-inline" onclick="toggleResultVisibility(this)">[...]</span>`;
 976
 977                    // Full content (initially hidden)
 978                    const contentDiv = document.createElement("pre");
 979                    contentDiv.className = "hidden";
 980                    contentDiv.innerHTML = escapeHtml(content);
 981
 982                    container.appendChild(previewDiv);
 983                    container.appendChild(contentDiv);
 984                    targetElement.appendChild(container);
 985                } else {
 986                    // For full view or short results, display everything
 987                    const preElement = document.createElement("pre");
 988                    preElement.textContent = content;
 989                    targetElement.appendChild(preElement);
 990                }
 991            }
 992
 993            function toggleResultVisibility(element, remainingLines) {
 994                const container = element.parentElement.parentElement;
 995                const preview = container.querySelector(".preview-content");
 996                const full = container.querySelector("pre");
 997
 998                // Once expanded, keep it expanded
 999                full.classList.remove("hidden");
1000                preview.classList.add("hidden");
1001            }
1002
1003            function formatContent(text) {
1004                return escapeHtml(text);
1005            }
1006
1007            function escapeHtml(text) {
1008                const div = document.createElement("div");
1009                div.textContent = text;
1010                return div.innerHTML;
1011            }
1012
1013            // Keyboard navigation handler
1014            document.addEventListener("keydown", function (event) {
1015                // previous thread
1016                if (
1017                    (event.ctrlKey && event.key === "ArrowLeft") ||
1018                    event.key === "h" ||
1019                    event.key === "k"
1020                ) {
1021                    if (!document.getElementById("prev-thread").disabled) {
1022                        previousThread();
1023                    }
1024                }
1025                // next thread
1026                else if (
1027                    (event.ctrlKey && event.key === "ArrowRight") ||
1028                    event.key === "j" ||
1029                    event.key === "l"
1030                ) {
1031                    if (!document.getElementById("next-thread").disabled) {
1032                        nextThread();
1033                    }
1034                }
1035            });
1036
1037            // Initialize the page
1038            document.addEventListener("DOMContentLoaded", function () {
1039                initTheme();
1040                updateNavigationButtons();
1041                renderThread();
1042            });
1043        </script>
1044    </body>
1045</html>