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 id="full-view" class="view-button active" onclick="switchView('full')">Full View</button>
328            <button id="compact-view" class="view-button" onclick="switchView('compact')">Compact View</button>
329            <button
330                id="export-button"
331                class="view-button"
332                onclick="exportThreadAsJson()"
333                title="Export current thread as JSON"
334            >
335                Export
336            </button>
337            <div class="theme-switcher">
338                <button id="theme-toggle" class="theme-button" onclick="toggleTheme()">
339                    <span id="theme-icon" class="theme-icon">☀️</span>
340                    <span id="theme-text">Light</span>
341                </button>
342            </div>
343        </div>
344        <div class="thread-navigation">
345            <button
346                id="prev-thread"
347                class="nav-button"
348                onclick="previousThread()"
349                title="Previous thread (Ctrl+←, k, or h)"
350                disabled
351            >
352                ← Previous
353            </button>
354            <div class="thread-indicator">
355                Thread <span id="current-thread-index">1</span> of <span id="total-threads">1</span>:
356                <span id="thread-id">Default Thread</span>
357            </div>
358            <button
359                id="next-thread"
360                class="nav-button"
361                onclick="nextThread()"
362                title="Next thread (Ctrl+→, j, or l)"
363                disabled
364            >
365                Next →
366            </button>
367        </div>
368        <table id="thread-table">
369            <thead>
370                <tr>
371                    <th class="turn-column">Turn</th>
372                    <th class="text-column">Text</th>
373                    <th class="tool-column">Tool</th>
374                    <th class="result-column">Result</th>
375                </tr>
376            </thead>
377            <tbody id="thread-body">
378                <!-- Content will be filled dynamically -->
379            </tbody>
380        </table>
381
382        <script>
383            // View mode - 'full' or 'compact'
384            let viewMode = "full";
385
386            // Theme mode - 'light', 'dark', or 'system'
387            let themeMode = localStorage.getItem("theme") || "system";
388
389            // Function to apply theme
390            function applyTheme(theme) {
391                const themeIcon = document.getElementById("theme-icon");
392                const themeText = document.getElementById("theme-text");
393
394                if (theme === "dark") {
395                    document.documentElement.setAttribute("data-theme", "dark");
396                    themeIcon.textContent = "🌙";
397                    themeText.textContent = "Dark";
398                } else {
399                    document.documentElement.removeAttribute("data-theme");
400                    themeIcon.textContent = "☀️";
401                    themeText.textContent = "Light";
402                }
403            }
404
405            // Function to toggle between light and dark themes
406            function toggleTheme() {
407                // If currently system or light, switch to dark
408                if (themeMode === "system") {
409                    const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
410                    themeMode = systemDark ? "light" : "dark";
411                } else {
412                    themeMode = themeMode === "light" ? "dark" : "light";
413                }
414
415                // Save preference
416                localStorage.setItem("theme", themeMode);
417
418                // Apply theme
419                applyTheme(themeMode);
420            }
421
422            // Initialize theme based on system or saved preference
423            function initTheme() {
424                if (themeMode === "system") {
425                    // Use system preference
426                    const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
427                    applyTheme(systemDark ? "dark" : "light");
428
429                    // Listen for system theme changes
430                    window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
431                        if (themeMode === "system") {
432                            applyTheme(e.matches ? "dark" : "light");
433                        }
434                    });
435                } else {
436                    // Use saved preference
437                    applyTheme(themeMode);
438                }
439            }
440
441            // Function to switch between view modes
442            function switchView(mode) {
443                viewMode = mode;
444
445                // Update button states
446                document.getElementById("full-view").classList.toggle("active", mode === "full");
447                document.getElementById("compact-view").classList.toggle("active", mode === "compact");
448
449                // Add or remove compact-mode class on the body
450                document.body.classList.toggle("compact-mode", mode === "compact");
451
452                // Re-render the thread with the new view mode
453                renderThread();
454            }
455
456            // Function to export the current thread as a JSON file
457            function exportThreadAsJson() {
458                // Clone the thread to avoid modifying the original
459                const threadToExport = JSON.parse(JSON.stringify(thread));
460
461                // Create a Blob with the JSON data
462                const blob = new Blob([JSON.stringify(threadToExport, null, 2)], { type: "application/json" });
463
464                // Create a download link
465                const url = URL.createObjectURL(blob);
466                const a = document.createElement("a");
467                a.href = url;
468
469                // Generate filename based on thread ID or index
470                const filename =
471                    threadToExport.thread_id || threadToExport.filename || `thread-${currentThreadIndex + 1}.json`;
472                a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
473
474                // Trigger the download
475                document.body.appendChild(a);
476                a.click();
477
478                // Clean up
479                setTimeout(() => {
480                    document.body.removeChild(a);
481                    URL.revokeObjectURL(url);
482                }, 0);
483            }
484            // Default dummy thread data for preview purposes
485            let dummyThread = {
486                messages: [
487                    {
488                        role: "system",
489                        content: [{ Text: "System prompt..." }],
490                    },
491                    {
492                        role: "user",
493                        content: [{ Text: "Fix the bug: kwargs not passed..." }],
494                    },
495                    {
496                        role: "assistant",
497                        content: [
498                            { Text: "I'll help you fix that bug." },
499                            {
500                                ToolUse: {
501                                    name: "list_directory",
502                                    input: { path: "fastmcp" },
503                                    is_input_complete: true,
504                                },
505                            },
506                        ],
507                    },
508                    {
509                        role: "user",
510                        content: [
511                            {
512                                ToolResult: {
513                                    tool_name: "list_directory",
514                                    is_error: false,
515                                    content:
516                                        "fastmcp/src\nfastmcp/tests\nfastmcp/README.md\nfastmcp/pyproject.toml\nfastmcp/.gitignore\nfastmcp/setup.py\nfastmcp/examples\nfastmcp/LICENSE",
517                                },
518                            },
519                        ],
520                    },
521                    {
522                        role: "assistant",
523                        content: [
524                            { Text: "Let's examine the code." },
525                            {
526                                ToolUse: {
527                                    name: "read_file",
528                                    input: {
529                                        path: "fastmcp/main.py",
530                                        start_line: 253,
531                                        end_line: 360,
532                                    },
533                                    is_input_complete: true,
534                                },
535                            },
536                        ],
537                    },
538                    {
539                        role: "user",
540                        content: [
541                            {
542                                ToolResult: {
543                                    tool_name: "read_file",
544                                    is_error: false,
545                                    content:
546                                        "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",
547                                },
548                            },
549                        ],
550                    },
551                    {
552                        role: "assistant",
553                        content: [
554                            { Text: "I found the issue." },
555                            {
556                                ToolUse: {
557                                    name: "edit_file",
558                                    input: {
559                                        path: "fastmcp/core.py",
560                                        old_string: "def start_server(app):\n    anyio.run(app)",
561                                        new_string: "def start_server(app, **kwargs):\n    anyio.run(app, **kwargs)",
562                                        display_description: "Fix kwargs passing to anyio.run",
563                                    },
564                                    is_input_complete: true,
565                                },
566                            },
567                        ],
568                    },
569                    {
570                        role: "user",
571                        content: [
572                            {
573                                ToolResult: {
574                                    tool_name: "edit_file",
575                                    is_error: false,
576                                    content: "Made edit to fastmcp/core.py",
577                                },
578                            },
579                        ],
580                    },
581                    {
582                        role: "assistant",
583                        content: [
584                            { Text: "Let's check if there are any errors." },
585                            {
586                                ToolUse: {
587                                    name: "diagnostics",
588                                    input: {},
589                                    is_input_complete: true,
590                                },
591                            },
592                        ],
593                    },
594                    {
595                        role: "user",
596                        content: [
597                            {
598                                ToolResult: {
599                                    tool_name: "diagnostics",
600                                    is_error: false,
601                                    content: "No errors found",
602                                },
603                            },
604                        ],
605                    },
606                ],
607            };
608
609            // The actual thread data will be injected here when opened by eval
610            let threadsData = window.threadsData || { threads: [dummyThread] };
611
612            // Initialize thread variables
613            let threads = threadsData.threads;
614            let currentThreadIndex = 0;
615            let thread = threads[currentThreadIndex];
616
617            // Function to navigate to the previous thread
618            function previousThread() {
619                if (currentThreadIndex > 0) {
620                    currentThreadIndex--;
621                    switchToThread(currentThreadIndex);
622                }
623            }
624
625            // Function to navigate to the next thread
626            function nextThread() {
627                if (currentThreadIndex < threads.length - 1) {
628                    currentThreadIndex++;
629                    switchToThread(currentThreadIndex);
630                }
631            }
632
633            // Function to switch to a specific thread by index
634            function switchToThread(index) {
635                if (index >= 0 && index < threads.length) {
636                    currentThreadIndex = index;
637                    thread = threads[currentThreadIndex];
638                    updateNavigationButtons();
639                    renderThread();
640                }
641            }
642
643            // Function to update the navigation buttons state
644            function updateNavigationButtons() {
645                document.getElementById("prev-thread").disabled = currentThreadIndex <= 0;
646                document.getElementById("next-thread").disabled = currentThreadIndex >= threads.length - 1;
647                document.getElementById("current-thread-index").textContent = currentThreadIndex + 1;
648                document.getElementById("total-threads").textContent = threads.length;
649            }
650
651            function renderThread() {
652                const tbody = document.getElementById("thread-body");
653                tbody.innerHTML = ""; // Clear existing content
654
655                // Set thread name if available
656                const threadId = thread.thread_id || `Thread ${currentThreadIndex + 1}`;
657                document.getElementById("thread-id").textContent = threadId;
658
659                // Set filename in the header if available
660                const filename = thread.filename || `Thread ${currentThreadIndex + 1}`;
661                document.getElementById("current-filename").textContent = filename;
662
663                // Skip system message
664                const nonSystemMessages = thread.messages.filter((msg) => msg.role !== "system");
665
666                let turnNumber = 0;
667                processMessages(nonSystemMessages, tbody, turnNumber);
668            }
669
670            function processMessages(messages, tbody) {
671                let turnNumber = 0;
672
673                for (let i = 0; i < messages.length; i++) {
674                    const msg = messages[i];
675
676                    if (isUserQuery(msg)) {
677                        // User message starts a new turn
678                        turnNumber++;
679                        renderUserMessage(msg, turnNumber, tbody);
680                    } else if (msg.role === "assistant") {
681                        // Each assistant message is one turn
682                        turnNumber++;
683
684                        // Collect all text content and tool uses for this turn
685                        let assistantText = "";
686                        let toolUses = [];
687
688                        // First, collect all text content
689                        for (const content of msg.content) {
690                            if (content.hasOwnProperty("Text")) {
691                                if (assistantText) {
692                                    assistantText += "<br><br>" + formatContent(content.Text);
693                                } else {
694                                    assistantText = formatContent(content.Text);
695                                }
696                            } else if (content.hasOwnProperty("ToolUse")) {
697                                toolUses.push(content.ToolUse);
698                            }
699                        }
700
701                        // Create a single row for this turn with text and tools
702                        const row = document.createElement("tr");
703                        row.id = `assistant-turn-${turnNumber}`;
704
705                        // Start with the turn number and assistant text
706                        row.innerHTML = `
707                            <td class="text-content">${turnNumber}</td>
708                            <td class="text-content"><!--Assistant: <br/ -->${assistantText}</td>
709                            <td id="tools-${turnNumber}"></td>
710                            <td id="results-${turnNumber}"></td>
711                        `;
712
713                        tbody.appendChild(row);
714
715                        // Add all tool calls to the tools cell
716                        const toolsCell = document.getElementById(`tools-${turnNumber}`);
717                        const resultsCell = document.getElementById(`results-${turnNumber}`);
718
719                        // Process all tools and their results
720                        for (let j = 0; j < toolUses.length; j++) {
721                            const toolUse = toolUses[j];
722                            const toolCall = formatToolCall(toolUse.name, toolUse.input);
723
724                            // Add the tool call to the tools cell
725                            if (j > 0) toolsCell.innerHTML += "<hr>";
726                            toolsCell.innerHTML += toolCall;
727
728                            // Look for corresponding tool result
729                            if (hasMatchingToolResult(messages, i, toolUse.name)) {
730                                const resultMsg = messages[i + 1];
731                                const toolResult = findToolResult(resultMsg, toolUse.name);
732
733                                if (toolResult) {
734                                    // Add the result to the results cell
735                                    if (j > 0) resultsCell.innerHTML += "<hr>";
736
737                                    // Create a container for the result
738                                    const resultDiv = document.createElement("div");
739                                    resultDiv.className = "tool-result";
740
741                                    // Format and display the tool result
742                                    formatToolResultInline(toolResult.content.Text, resultDiv);
743                                    resultsCell.appendChild(resultDiv);
744
745                                    // Skip the result message in the next iteration
746                                    if (j === toolUses.length - 1) {
747                                        i++;
748                                    }
749                                }
750                            }
751                        }
752                    } else if (msg.role === "user" && msg.content.some((c) => c.hasOwnProperty("ToolResult"))) {
753                        // Skip tool result messages as they are handled with their corresponding tool use
754                        continue;
755                    }
756                }
757            }
758
759            function isUserQuery(message) {
760                return message.role === "user" && !message.content.some((c) => c.hasOwnProperty("ToolResult"));
761            }
762
763            function renderUserMessage(message, turnNumber, tbody) {
764                const row = document.createElement("tr");
765                row.innerHTML = `
766                    <td>${turnNumber}</td>
767                    <td class="text-content"><b>[User]:</b><br/> ${formatContent(message.content[0].Text)}</td>
768                    <td></td>
769                    <td></td>
770                `;
771                tbody.appendChild(row);
772            }
773
774            function hasMatchingToolResult(messages, currentIndex, toolName) {
775                return (
776                    currentIndex + 1 < messages.length &&
777                    messages[currentIndex + 1].role === "user" &&
778                    messages[currentIndex + 1].content.some(
779                        (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
780                    )
781                );
782            }
783
784            function findToolResult(resultMessage, toolName) {
785                const toolResultContent = resultMessage.content.find(
786                    (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
787                );
788
789                return toolResultContent ? toolResultContent.ToolResult : null;
790            }
791            function formatToolCall(name, input) {
792                // In compact mode, format tool calls on a single line
793                if (viewMode === "compact") {
794                    const params = [];
795                    const fullParams = [];
796
797                    // Process all parameters
798                    for (const [key, value] of Object.entries(input)) {
799                        if (value !== null && value !== undefined) {
800                            // Store full parameter for expanded view
801                            let fullValue = typeof value === "string" ? `"${value}"` : value;
802                            fullParams.push([key, fullValue]);
803
804                            // Abbreviated value for compact view
805                            let displayValue = fullValue;
806                            if (typeof value === "string" && value.length > 30) {
807                                displayValue = `"${value.substring(0, 30)}..."`;
808                            }
809                            params.push(`${key}=${displayValue}`);
810                        }
811                    }
812
813                    const paramString = params.join(", ");
814                    const fullLine = `<span class="tool-name">${name}</span>(${paramString})`;
815
816                    // If the line is too long, add a [more] link
817                    if (fullLine.length > 80 || params.length > 1) {
818                        // Create a container with the compact and full views
819                        const compactView = `<span class="tool-name">${name}</span>(${params[0]}, <span class="more-inline" onclick="toggleActionVisibility(this)">[...]</span>)`;
820
821                        // For the full view, use the original untruncated values
822                        let result = `<span class="tool-name">${name}</span>(`;
823                        const formattedParams = fullParams
824                            .map((p) => `    ${p[0]}=${p[1]}`)
825                            .join(",<br/>");
826                        const fullView = `${result}<br/>${formattedParams}<br/>)`;
827
828                        return `<div class="action-container">
829                            <div class="action-preview">${compactView}</div>
830                            <div class="action-full hidden">${fullView}</div>
831                        </div>`;
832                    }
833
834                    return fullLine;
835                }
836
837                // Regular (full) view formatting with multiple lines
838                let result = `<span class="tool-name">${name}</span>(`;
839                const params = [];
840                for (const [key, value] of Object.entries(input)) {
841                    if (value !== null && value !== undefined) {
842                        // Format different types of values
843                        let formattedValue = typeof value === "string" ? `"${value}"` : value;
844                        params.push([key, formattedValue]);
845                    }
846                }
847
848                if (params.length === 0) {
849                    return `${result})`;
850                } else if (params.length === 1) {
851                    // For single parameter, just show the value without the parameter name
852                    return `${result}${params[0][1]})`;
853                } else {
854                    // Format parameters
855                    const formattedParams = params.map((p) => `    ${p[0]}=${p[1]}`).join(",<br/>");
856                    return `${result}<br/>${formattedParams}<br/>)`;
857                }
858            }
859
860            function toggleActionVisibility(element, remainingLines) {
861                const container = element.closest(".action-container");
862                const preview = container.querySelector(".action-preview");
863                const full = container.querySelector(".action-full");
864
865                // Once expanded, keep it expanded
866                full.classList.remove("hidden");
867                preview.classList.add("hidden");
868            }
869
870            function formatToolResultInline(content, targetElement) {
871                // Count lines
872                const lines = content.split("\n");
873
874                // In compact mode, show only 1 line with [more] link
875                if (viewMode === "compact" && lines.length > 1) {
876                    // Create container
877                    const container = document.createElement("div");
878
879                    // Preview content
880                    const previewDiv = document.createElement("div");
881                    previewDiv.className = "preview-content";
882
883                    // Add the first line of content plus [more] link
884                    const previewContent = lines[0];
885                    previewDiv.innerHTML =
886                        escapeHtml(previewContent) +
887                        ` <span class="more-inline" onclick="toggleResultVisibility(this)">[...]</span>`;
888
889                    // Full content (initially hidden)
890                    const contentDiv = document.createElement("pre");
891                    contentDiv.className = "hidden";
892                    contentDiv.innerHTML = escapeHtml(content);
893
894                    container.appendChild(previewDiv);
895                    container.appendChild(contentDiv);
896                    targetElement.appendChild(container);
897                } else {
898                    // For full view or short results, display everything
899                    const preElement = document.createElement("pre");
900                    preElement.textContent = content;
901                    targetElement.appendChild(preElement);
902                }
903            }
904
905            function toggleResultVisibility(element, remainingLines) {
906                const container = element.parentElement.parentElement;
907                const preview = container.querySelector(".preview-content");
908                const full = container.querySelector("pre");
909
910                // Once expanded, keep it expanded
911                full.classList.remove("hidden");
912                preview.classList.add("hidden");
913            }
914
915            function formatContent(text) {
916                return escapeHtml(text);
917            }
918
919            function escapeHtml(text) {
920                const div = document.createElement("div");
921                div.textContent = text;
922                return div.innerHTML;
923            }
924
925            // Keyboard navigation handler
926            document.addEventListener("keydown", function (event) {
927                // previous thread
928                if ((event.ctrlKey && event.key === "ArrowLeft") || event.key === "h" || event.key === "k") {
929                    if (!document.getElementById("prev-thread").disabled) {
930                        previousThread();
931                    }
932                }
933                // next thread
934                else if ((event.ctrlKey && event.key === "ArrowRight") || event.key === "j" || event.key === "l") {
935                    if (!document.getElementById("next-thread").disabled) {
936                        nextThread();
937                    }
938                }
939            });
940
941            // Initialize the page
942            document.addEventListener("DOMContentLoaded", function () {
943                initTheme();
944                updateNavigationButtons();
945                renderThread();
946            });
947        </script>
948    </body>
949</html>