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 ← 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 →
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 ` ${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) => ` ${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>