UNPKG

@buger/probe-chat

Version:

CLI and web interface for Probe code search (formerly @buger/probe-web and @buger/probe-chat)

1,738 lines (1,503 loc) 80.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Probe - AI-Native Code Understanding</title> <!-- Add Marked.js for Markdown rendering --> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Add Highlight.js for syntax highlighting --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <!-- Add Mermaid.js for diagram rendering --> <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; margin: 0; padding: 0; line-height: 1.5; color: #333; min-height: 100vh; overflow-x: hidden; overflow-y: auto; } #chat-container { max-width: 868px; /* 900px - 16px padding on each side */ margin: 0 auto; display: flex; flex-direction: column; min-height: 100vh; /* Changed from height to min-height to allow expansion */ position: relative; box-sizing: border-box; } .header { padding: 10px 0; border-bottom: 1px solid #eee; } .header-container { display: flex; justify-content: space-between; align-items: center; } .header-left { display: flex; align-items: center; } .header-logo { height: 30px; margin-right: 10px; } .new-chat-link { font-size: 15px; color: #555; text-decoration: none; font-weight: 500; padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px; margin-left: 5px; transition: all 0.2s ease; } .new-chat-link:hover { background-color: #f5f5f5; border-color: #ccc; } #messages { flex: 1; background-color: #fff; /* Removed overflow-y: auto to let the page handle scrolling */ margin-bottom: 80px; /* Keep margin for fixed input form */ margin-top: 20px; /* Space for fixed input form */ } .tool-call { margin: 10px 0; border: 1px solid #ddd; border-radius: 8px; background-color: #f8f9fa; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); transition: all 0.2s ease; } .tool-call:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .tool-call-header { background-color: #e9ecef; padding: 10px 14px; font-weight: bold; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; } .tool-call-name { color: #0066cc; font-size: 1.05em; } .tool-call-timestamp { font-size: 0.8em; color: #666; font-style: italic; } .tool-call-content { padding: 12px; } .tool-call-description { background-color: #ffffff; padding: 10px 12px; border-radius: 6px; border-left: 3px solid #44CDF3; margin-bottom: 10px; font-size: 0.95em; color: #333; } .tool-call-args { background-color: #ffffff; padding: 10px; border-radius: 6px; border: 1px solid #eee; font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; font-size: 0.9em; margin-bottom: 10px; white-space: pre-wrap; } .tool-call-result { background-color: #f0f8ff; padding: 10px; border-radius: 6px; border: 1px solid #e0e8ff; font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; font-size: 0.9em; white-space: pre-wrap; max-height: 300px; overflow-y: auto; } #input-form { position: fixed; display: flex; padding: 16px 0; background-color: white; border-top: 1px solid #ddd; z-index: 100; max-width: 868px; width: calc(100% - 32px); margin: 0 auto; left: 50%; transform: translateX(-50%); box-sizing: border-box; align-items: flex-end; /* Align items to the bottom */ } #input-form.centered { top: 50%; transform: translate(-50%, -50%); border-top: none; } .centered-logo-container { text-align: center; margin-bottom: 20px; position: fixed; top: 35%; left: 50%; transform: translate(-50%, -100%); z-index: 99; } .api-setup-mode .centered-logo-container { position: static; margin: 40px auto 20px; width: 100%; max-width: 800px; transform: none; } .centered-logo-container h1 { font-weight: 300; display: flex; align-items: center; justify-content: center; margin: 0; } .centered-logo-container h1 img { height: 80px; margin-right: 16px; } .centered-logo-container h1 { font-size: 38px; } #input-form.bottom { bottom: 0; } .search-suggestions { color: #999; font-size: 0.85em; display: none; text-align: left; padding: 0; position: fixed; max-width: 868px; width: calc(100% - 32px); margin: 0 auto; background-color: white; left: 50%; transform: translateX(-50%); z-index: 99; } .search-suggestions ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; } .search-suggestions li { padding: 6px 16px 6px 0; white-space: nowrap; cursor: pointer; } .search-suggestions li:hover { color: #44CDF3; } #input-form.centered .search-suggestions { display: block; } .folder-info { color: #666; font-size: 0.9em; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e0e0e0; font-style: italic; font-weight: 500; } #input-form.centered .folder-info { display: block; } #message-input { resize: none; flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); resize: none; overflow-y: auto; height: 1.5em; /* Changed from 44px to auto */ min-height: 44px; /* Ensures 1 row minimum */ max-height: 200px; /* Limits to ~10 rows */ line-height: 1.5em; box-sizing: border-box; } button { padding: 12px 24px; margin-left: 10px; background-color: #44CDF3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 18px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; align-self: flex-end; /* Ensure button aligns to bottom */ height: 44px; /* Match the height of the single-line textarea */ } button:hover { background-color: #2bb5db; } #folder-list ul { margin: 4px 0; padding-left: 20px; } #folder-list li { padding: 2px 0; } #folder-list strong { color: #333; font-weight: 600; } .footer { text-align: center; padding: 10px; font-size: 14px; color: #666; position: fixed; bottom: 0; left: 0; right: 0; background-color: white; border-top: 1px solid #eee; z-index: 50; } .footer a { color: #2196F3; text-decoration: none; } .footer a:hover { text-decoration: underline; } .header-container { display: flex; align-items: center; justify-content: space-between; } .example { font-style: italic; color: #666; margin: 8px 0; } /* Markdown styling */ .markdown-content { line-height: 1.6; } .markdown-content h1, .markdown-content h2, .markdown-content h3 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } .markdown-content h1 { font-size: 2em; } .markdown-content h2 { font-size: 1.5em; } .markdown-content h3 { font-size: 1.25em; } .markdown-content p, .markdown-content ul, .markdown-content ol { margin-top: 0; margin-bottom: 16px; } .markdown-content code { padding: 0.2em 0.4em; margin: 0; font-size: 85%; background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; } .markdown-content pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 3px; margin-top: 0; margin-bottom: 16px; } .markdown-content pre code { padding: 0; margin: 0; font-size: 100%; background-color: transparent; border: 0; } .user-message { background-color: #f1f1f1; padding: 10px 14px; border-radius: 18px; margin-bottom: 12px; max-width: 80%; align-self: flex-end; font-weight: 500; } .ai-message { background-color: transparent; padding: 10px 14px; margin-bottom: 4px; max-width: 90%; align-self: flex-start; } .message-container { display: flex; flex-direction: column; } .copy-button-container { align-self: flex-start; margin-bottom: 12px; margin-top: -20px; } .copy-button { background-color: white; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; color: #666; display: flex; align-items: center; border: none; } .copy-button:hover { background-color: #d0d0d0; } .copy-button svg { width: 16px; height: 16px; margin-right: 4px; } .message-container { display: flex; flex-direction: column; } /* Mermaid diagram styling */ .mermaid { background-color: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; overflow-x: auto; text-align: center; position: relative; } .mermaid svg, .mermaid-png { max-width: 100%; height: auto; transition: transform 0.2s ease; } /* Zoom icon overlay */ .mermaid-container { position: relative; display: inline-block; } .zoom-icon { position: absolute; top: 10px; right: 10px; background-color: rgba(255, 255, 255, 0.8); border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transition: opacity 0.2s ease; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); z-index: 10; } .mermaid-container:hover .zoom-icon { opacity: 1; } .zoom-icon svg { width: 18px; height: 18px; } /* Fullscreen dialog */ .diagram-dialog { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 40px; box-sizing: border-box; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; overflow: hidden; /* Prevent any scrolling */ } .diagram-dialog.active { opacity: 1; pointer-events: auto; } .diagram-dialog-content { max-width: 90%; max-height: 90%; background-color: white; border-radius: 8px; padding: 20px; position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; /* Prevent scrollbars */ } .diagram-dialog img, .diagram-dialog svg { max-width: 100%; max-height: 100%; object-fit: contain; /* Ensure image fits while maintaining aspect ratio */ display: block; } .close-dialog { position: absolute; top: 10px; right: 10px; background-color: rgba(255, 255, 255, 0.8); border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); z-index: 1001; } .close-dialog svg { width: 20px; height: 20px; } /* Token usage display styles */ .token-usage { position: fixed; bottom: 80px; right: 20px; background-color: rgba(255, 255, 255, 0.95); border: 1px solid #ddd; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #666; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); z-index: 100; display: none; /* Hidden by default, shown after first message */ } .token-usage-content { display: flex; flex-direction: column; gap: 6px; } .token-usage-table { display: table; width: 100%; border-collapse: collapse; } .token-usage-row { display: table-row; } .token-label { display: table-cell; font-weight: bold; color: #444; padding-right: 10px; text-align: left; white-space: nowrap; } .token-value { display: table-cell; text-align: right; white-space: nowrap; } .cache-info { color: #888; font-size: 11px; margin-left: 5px; } </style> <style> /* Styles for the API key setup message */ #api-key-setup { display: none; background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px auto; max-width: 800px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } #api-key-setup h2 { color: #333; margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; } #api-key-setup code { background-color: #f1f1f1; padding: 2px 5px; border-radius: 3px; font-family: monospace; } /* API Key Form Styles */ #api-key-form { background-color: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 20px; } #api-key-form h3 { margin-top: 0; color: #44CDF3; } #api-key-form .form-group { margin-bottom: 15px; } #api-key-form label { display: block; margin-bottom: 5px; font-weight: 500; } #api-key-form select, #api-key-form input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } #api-key-form .buttons { display: flex; justify-content: space-between; margin-top: 20px; } #api-key-form button { padding: 10px 15px; background-color: #44CDF3; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } #api-key-form button:hover { background-color: #2bb5db; } #reset-api-key { background-color: #f44336 !important; } #reset-api-key:hover { background-color: #d32f2f !important; } .api-key-status { margin-top: 10px; padding: 10px; border-radius: 4px; font-size: 14px; } .api-key-status.success { background-color: #e8f5e9; color: #2e7d32; border-left: 4px solid #4caf50; } .api-key-status.error { background-color: #ffebee; color: #c62828; border-left: 4px solid #f44336; } h1 a:hover { text-decoration: underline; } </style> </head> <body> <div id="chat-container"> <div class="header"> <div class="header-container"> <div class="header-left"> <img src="logo.png" alt="Probe Logo" class="header-logo"> <a href="#" class="new-chat-link">New chat</a> </div> <div id="api-settings"> <a href="#" id="header-reset-api-key" style="display: none; font-size: 12px; color: #f44336; text-decoration: none;">Reset API Key</a> </div> </div> </div> <div id="empty-state-logo" class="centered-logo-container"> <h1><img src="logo.png" alt="Probe Logo"><a href="https://probeai.dev/" style="color: inherit; text-decoration: none;">Probe </a>&nbsp;- AI-Native Code Understanding</h1> </div> <!-- API Key Setup Instructions --> <div id="api-key-setup"> <h2>API Key Setup Required</h2> <p>To use the Probe AI chat interface, you need to configure at least one API key. You have two options:</p> <!-- API Key Web Form --> <div id="api-key-form"> <h3>Option 1: Configure API Key in Browser</h3> <p>Enter your API key details below to start using the chat interface immediately:</p> <div class="form-group"> <label for="api-provider">API Provider:</label> <select id="api-provider"> <option value="anthropic">Anthropic Claude</option> <option value="openai">OpenAI</option> <option value="google">Google AI</option> </select> </div> <div class="form-group"> <label for="api-key">API Key:</label> <input type="password" id="api-key" placeholder="Enter your API key"> </div> <div class="form-group"> <label for="api-url">Custom API URL (Optional):</label> <input type="text" id="api-url" placeholder="Leave blank for default API URL"> </div> <div class="api-key-status" id="api-key-status" style="display: none;"></div> <div class="buttons"> <button type="button" id="save-api-key">Save API Key</button> </div> <p style="margin-top: 10px; font-size: 0.9em; color: #666;"> Your API key will be stored in your browser's local storage and sent with each request. No data is stored on our servers. </p> </div> <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;"> <h3>Option 2: Using Server-Side Configuration</h3> <p>Alternatively, you can configure API keys on the server:</p> <div style="background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 10px;"> <p><strong>Using a .env file:</strong></p> <ol style="margin-left: 20px;"> <li>Create a <code>.env</code> file in the current directory by copying <code>.env.example</code></li> <li>Add your API key to the <code>.env</code> file (uncomment and replace with your key)</li> <li>Restart the application</li> </ol> <p style="margin-top: 15px;"><strong>Using environment variables:</strong></p> <ul style="margin-left: 20px;"> <li>Anthropic: <code>ANTHROPIC_API_KEY=your_anthropic_api_key</code></li> <li>OpenAI: <code>OPENAI_API_KEY=your_openai_api_key</code></li> <li>Google AI: <code>GOOGLE_API_KEY=your_google_api_key</code></li> </ul> </div> </div> </div> <div id="messages" class="message-container"></div> </div> <form id="input-form" onsubmit="return false;"> <textarea id="message-input" placeholder="Ask about code..." required rows="1"></textarea> <button type="button" id="search-button">Search</button> </form> <div class="search-suggestions"> <ul> <li>Find functions that handle user authentication</li> <li>Search for database connection implementations</li> <li>Show error handling patterns in the codebase</li> <li>List all API endpoints in the project</li> <li>Find code that processes user input</li> <li>Show how configuration is loaded</li> <li>Find file parsing implementations</li> </ul> <div id="folder-info" class="folder-info"></div> </div> <div id="token-usage" class="token-usage"> <div class="token-usage-content"> <div class="token-usage-table"> <div class="token-usage-row"> <div class="token-label">Current:</div> <div class="token-value"> <span id="current-request">0</span> req / <span id="current-response">0</span> resp </div> </div> <div class="token-usage-row"> <div class="token-label">Cache:</div> <div class="token-value"> <span id="current-cache-read">0</span> read / <span id="current-cache-write">0</span> write </div> </div> <div class="token-usage-row"> <div class="token-label">Context:</div> <div class="token-value"> <span id="context-window">0</span> tokens </div> </div> <div class="token-usage-row"> <div class="token-label">Total Cache:</div> <div class="token-value"> <span id="total-cache-read">0</span> read / <span id="total-cache-write">0</span> write </div> </div> <div class="token-usage-row"> <div class="token-label">Total:</div> <div class="token-value"> <span id="total-request">0</span> req / <span id="total-response">0</span> resp </div> </div> </div> </div> </div> <div class="footer"> Powered by <a href="https://probeai.dev/" target="_blank">Probe</a> </div> </div> <script> // Token Usage Display Functionality // Function to update token usage display function updateTokenUsageDisplay(tokenUsage) { if (!tokenUsage) return; console.log('[TokenUsage] Updating display with:', tokenUsage); // Update current token usage if (tokenUsage.current) { document.getElementById('current-request').textContent = tokenUsage.current.request || 0; document.getElementById('current-response').textContent = tokenUsage.current.response || 0; // Update cache information in separate row const cacheRead = tokenUsage.current.cacheRead || 0; const cacheWrite = tokenUsage.current.cacheWrite || 0; document.getElementById('current-cache-read').textContent = cacheRead; document.getElementById('current-cache-write').textContent = cacheWrite; } // Update context window - force display even if 0 const contextWindow = tokenUsage.contextWindow || 0; document.getElementById('context-window').textContent = contextWindow; // Update total cache information if (tokenUsage.total && tokenUsage.total.cache) { const totalCacheRead = tokenUsage.total.cache.read || 0; const totalCacheWrite = tokenUsage.total.cache.write || 0; document.getElementById('total-cache-read').textContent = totalCacheRead; document.getElementById('total-cache-write').textContent = totalCacheWrite; } else if (tokenUsage.total) { // Fallback to direct properties if cache object is not available const totalCacheRead = tokenUsage.total.cacheRead || 0; const totalCacheWrite = tokenUsage.total.cacheWrite || 0; document.getElementById('total-cache-read').textContent = totalCacheRead; document.getElementById('total-cache-write').textContent = totalCacheWrite; } // Update total token usage if (tokenUsage.total) { document.getElementById('total-request').textContent = tokenUsage.total.request || 0; document.getElementById('total-response').textContent = tokenUsage.total.response || 0; } // Show token usage display const tokenUsageElement = document.getElementById('token-usage'); if (tokenUsageElement) { tokenUsageElement.style.display = 'block'; } } // Make token usage functions available globally window.tokenUsageDisplay = { update: updateTokenUsageDisplay, fetch: function (sessionId) { if (!sessionId) { console.log('[TokenUsage] No session ID provided for fetchTokenUsage'); // Try to get session ID from window object if (window.sessionId) { console.log(`[TokenUsage] Using session ID from window object: ${window.sessionId}`); sessionId = window.sessionId; } else { console.log('[TokenUsage] No session ID available, cannot fetch token usage'); return; } } // Check if this session is still the current one if (sessionId !== window.sessionId) { console.log(`[TokenUsage] Session ID mismatch: ${sessionId} vs current ${window.sessionId}, skipping fetch`); return; } console.log(`[TokenUsage] Fetching token usage for session: ${sessionId}`); // Use a more reliable fetch with keepalive for Firefox fetch(`/api/token-usage?sessionId=${sessionId}`, { method: 'GET', cache: 'no-store', keepalive: true, headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } }) .then(response => { console.log(`[TokenUsage] Response status: ${response.status}`); if (response.ok) { return response.json(); } throw new Error('Failed to fetch token usage'); }) .then(data => { console.log('[TokenUsage] Received token usage data:', data); updateTokenUsageDisplay(data); }) .catch(error => { console.error('[TokenUsage] Error fetching token usage:', error); // Display a small error indicator in the token usage display const tokenUsageElement = document.getElementById('token-usage'); if (tokenUsageElement && tokenUsageElement.style.display !== 'none') { const errorIndicator = document.createElement('div'); errorIndicator.style.color = '#f44336'; errorIndicator.style.fontSize = '10px'; errorIndicator.style.marginTop = '5px'; errorIndicator.textContent = 'Error updating token usage'; // Remove any existing error indicators const existingIndicators = tokenUsageElement.querySelectorAll('[data-error-indicator]'); existingIndicators.forEach(el => el.remove()); // Add the data attribute for future reference errorIndicator.setAttribute('data-error-indicator', 'true'); // Add to the token usage display tokenUsageElement.querySelector('.token-usage-content').appendChild(errorIndicator); // Remove after 5 seconds setTimeout(() => { if (errorIndicator.parentNode) { errorIndicator.parentNode.removeChild(errorIndicator); } }, 5000); } }); } }; function convertSvgToPng(svgElement, containerDiv, index) { if (!svgElement) { console.error('No SVG found!'); return; } try { // Get the actual rendered size of the SVG const rect = svgElement.getBoundingClientRect(); const svgWidth = rect.width; const svgHeight = rect.height; console.log(`SVG rendered size: ${svgWidth}x${svgHeight}`); // Define scale factor for higher resolution (increase for more clarity) const scale = 6; // Try 3 or 4 if still blurry // Create canvas with scaled dimensions const canvasWidth = svgWidth * scale; const canvasHeight = svgHeight * scale; const canvas = document.createElement('canvas'); canvas.width = canvasWidth; canvas.height = canvasHeight; const ctx = canvas.getContext('2d'); // Enable image smoothing for better quality ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // Serialize SVG to string const svgString = new XMLSerializer().serializeToString(svgElement); const svgDataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgString); const img = new Image(); img.onload = function () { // Draw the image onto the canvas at scaled size ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); // Convert canvas to PNG data URL const pngDataUrl = canvas.toDataURL('image/png'); // Create the PNG image element const pngImage = document.createElement('img'); pngImage.src = pngDataUrl; pngImage.width = svgWidth; // Display at original size pngImage.height = svgHeight; pngImage.alt = 'Diagram as PNG'; pngImage.className = 'mermaid-png'; pngImage.setAttribute('data-full-size', pngDataUrl); // Store full-size image URL for zoom // Log PNG natural size to verify resolution pngImage.onload = function () { console.log(`PNG natural size: ${this.naturalWidth}x${this.naturalHeight}`); }; // Create a container for the image with zoom functionality const container = document.createElement('div'); container.className = 'mermaid-container'; // Create zoom icon const zoomIcon = document.createElement('div'); zoomIcon.className = 'zoom-icon'; zoomIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>'; // Add click event to zoom icon zoomIcon.addEventListener('click', function (e) { e.stopPropagation(); showDiagramDialog(pngDataUrl); }); // Replace the SVG with the PNG image in the container svgElement.style.display = 'none'; container.appendChild(pngImage); container.appendChild(zoomIcon); svgElement.insertAdjacentElement('afterend', container); console.log(`Replaced SVG with PNG image (index: ${index || 0})`); }; img.onerror = function () { console.error('Error loading SVG image for conversion'); }; img.src = svgDataUrl; } catch (error) { console.error('Error in SVG to PNG conversion process:', error); } } // Function to show diagram in fullscreen dialog function showDiagramDialog(imageUrl) { // Create dialog if it doesn't exist let dialog = document.getElementById('diagram-dialog'); if (!dialog) { dialog = document.createElement('div'); dialog.id = 'diagram-dialog'; dialog.className = 'diagram-dialog'; const dialogContent = document.createElement('div'); dialogContent.className = 'diagram-dialog-content'; const closeButton = document.createElement('div'); closeButton.className = 'close-dialog'; closeButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'; closeButton.addEventListener('click', function () { dialog.classList.remove('active'); }); dialog.appendChild(dialogContent); dialog.appendChild(closeButton); document.body.appendChild(dialog); // Close dialog when clicking outside content dialog.addEventListener('click', function (e) { if (e.target === dialog) { dialog.classList.remove('active'); } }); // Close dialog with Escape key document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && dialog.classList.contains('active')) { dialog.classList.remove('active'); } }); } // Update dialog content with the image const dialogContent = dialog.querySelector('.diagram-dialog-content'); dialogContent.innerHTML = ''; const img = document.createElement('img'); img.src = imageUrl; img.alt = 'Diagram (Full Size)'; // Ensure image fits screen by checking dimensions after loading img.onload = function () { // Image is now loaded, ensure it fits within the dialog content const viewportWidth = window.innerWidth * 0.9 - 40; // 90% of viewport minus padding const viewportHeight = window.innerHeight * 0.9 - 40; // 90% of viewport minus padding console.log(`Image natural size: ${img.naturalWidth}x${img.naturalHeight}`); console.log(`Available viewport space: ${viewportWidth}x${viewportHeight}`); // Image will be constrained by CSS max-width/max-height and object-fit }; dialogContent.appendChild(img); // Show dialog dialog.classList.add('active'); } // Generate a new session ID on every page load, store only in memory let sessionId = crypto.randomUUID(); console.log(`Initialized new session with ID: ${sessionId}`); // Make session ID available to other scripts // We use both direct property assignment and event dispatch for compatibility // Direct property is used by internal functions like tokenUsageDisplay.fetch window.sessionId = sessionId; // Dispatch an event with the session ID for any external scripts that may be listening window.dispatchEvent(new MessageEvent('message', { data: { sessionId: sessionId } })); const messagesDiv = document.getElementById('messages'); const form = document.getElementById('input-form'); const searchSuggestionsDiv = document.querySelector('.search-suggestions'); const input = document.getElementById('message-input'); const folderListDiv = document.getElementById('folder-list'); // Position the input form in the center initially and handle UI elements visibility function positionInputForm() { const footer = document.querySelector('.footer'); const header = document.querySelector('.header'); const emptyStateLogo = document.getElementById('empty-state-logo'); if (messagesDiv.children.length === 0) { form.classList.add('centered'); form.classList.remove('bottom'); // Show footer when no messages if (footer) { footer.style.display = 'block'; } // Hide the top header and show the centered logo if (header) { header.style.display = 'none'; } if (emptyStateLogo) { emptyStateLogo.style.display = 'block'; } } else { form.classList.remove('centered'); form.classList.add('bottom'); // Hide footer when chat is started if (footer) { footer.style.display = 'none'; } // Show the top header and hide the centered logo if (header) { header.style.display = 'block'; } if (emptyStateLogo) { emptyStateLogo.style.display = 'none'; } } } // Make search suggestions clickable function setupSearchSuggestions() { document.querySelectorAll('.search-suggestions li').forEach(item => { item.addEventListener('click', () => { input.value = item.textContent; input.focus(); }); }); } // Initialize on page load window.addEventListener('load', () => { setupSearchSuggestions(); positionInputForm(); positionSearchSuggestions(); // Focus the input field on page load setTimeout(() => { const inputField = document.getElementById('message-input'); if (inputField) { inputField.focus(); } }, 100); }); // Position search suggestions relative to input form function positionSearchSuggestions() { const formRect = form.getBoundingClientRect(); if (form.classList.contains('centered')) { // Position directly below the form searchSuggestionsDiv.style.top = formRect.bottom + 'px'; searchSuggestionsDiv.style.display = 'block'; } else { searchSuggestionsDiv.style.display = 'none'; } } // Update search suggestions position when window is resized window.addEventListener('resize', positionSearchSuggestions); // Check if Mermaid is properly loaded function checkMermaidLoaded() { if (typeof mermaid === 'undefined') { console.error('Mermaid is not loaded properly'); return false; } console.log('Mermaid version:', mermaid.version); return true; } // Initialize mermaid if (checkMermaidLoaded()) { mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose', flowchart: { htmlLabels: true }, logLevel: 3, // Add logging for debugging (1: error, 2: warn, 3: info, 4: debug, 5: trace) fontFamily: 'monospace' }); // Run mermaid on page load to render the test diagram window.addEventListener('DOMContentLoaded', () => { setTimeout(() => { try { console.log('Running mermaid on page load'); mermaid.run(); } catch (error) { console.error('Error initializing mermaid:', error); } }, 500); }); } // Configure marked.js // Configure Marked.js with logging marked.setOptions({ highlight: function (code, lang) { console.log(`Highlighting code with language: ${lang}`); if (lang === 'mermaid') { console.log('Returning mermaid div'); return `<div class="mermaid">${code}</div>`; } const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, langPrefix: 'hljs language-', gfm: true, breaks: true }); // Fetch API key status and check for no API keys mode on page load window.addEventListener('DOMContentLoaded', async () => { // First check if we have an API key in local storage const storedApiKey = localStorage.getItem('probeApiKey'); if (storedApiKey) { // Show the reset button in the header const headerResetButton = document.getElementById('header-reset-api-key'); if (headerResetButton) { headerResetButton.style.display = 'inline-block'; } } // Check if we're in API key setup mode const apiKeySetupDiv = document.getElementById('api-key-setup'); const inputForm = document.getElementById('input-form'); const searchSuggestions = document.querySelector('.search-suggestions'); // If API key setup is visible, we're in API setup mode if (apiKeySetupDiv && window.getComputedStyle(apiKeySetupDiv).display !== 'none') { // Add class to body for API setup mode styling document.body.classList.add('api-setup-mode'); // Hide search suggestions and input form if (inputForm) inputForm.style.display = 'none'; if (searchSuggestions) searchSuggestions.style.display = 'none'; } else { // Remove API setup mode class if not in setup mode document.body.classList.remove('api-setup-mode'); } try { const response = await fetch('/folders'); const data = await response.json(); // Check if we're in no API keys mode if (data.noApiKeysMode) { handleNoApiKeysMode(); } // Display folder information displayFolderInfo(data.folders); } catch (error) { console.error('Error fetching API status:', error); } }); // Function to display folder information function displayFolderInfo(folders) { const folderInfoDiv = document.getElementById('folder-info'); if (!folderInfoDiv) return; // Clear any existing content folderInfoDiv.innerHTML = ''; // Set a loading message folderInfoDiv.textContent = 'Determining search location...'; // Fetch the current directory from the server's /folders endpoint fetch('/folders') .then(response => response.json()) .then(data => { // Use the currentDir property which contains the absolute path if (data.currentDir) { // Display the absolute path from the server folderInfoDiv.textContent = `Searching in: ${data.currentDir}`; // If there are multiple folders, show that info if (data.folders && data.folders.length > 1) { folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`; } } // Fallback to folders if currentDir is not available else if (data.folders && data.folders.length > 0) { folderInfoDiv.textContent = `Searching in: ${data.folders[0]}`; if (data.folders.length > 1) { folderInfoDiv.textContent += ` (and ${data.folders.length - 1} other folder${data.folders.length > 2 ? 's' : ''})`; } } // Last resort fallback else { folderInfoDiv.textContent = `Searching in: . (current directory)`; } }) .catch(error => { console.error('Error fetching folder info:', error); folderInfoDiv.textContent = `Searching in: . (current directory)`; }); } // Handle no API keys mode function handleNoApiKeysMode() { // Check if body has the data-no-api-keys attribute const noApiKeys = document.body.getAttribute('data-no-api-keys') === 'true'; // Check if API key is already stored in local storage const storedApiKey = localStorage.getItem('probeApiKey'); // Add or remove api-setup-mode class based on whether we need to show the API key setup if (noApiKeys && !storedApiKey) { document.body.classList.add('api-setup-mode'); } else { document.body.classList.remove('api-setup-mode'); } // Get UI elements const apiKeySetupDiv = document.getElementById('api-key-setup'); const inputForm = document.getElementById('input-form'); const searchSuggestions = document.querySelector('.search-suggestions'); if (noApiKeys && !storedApiKey) { console.log('No API keys detected and no local storage key - showing setup instructions'); // Show the API key setup div if (apiKeySetupDiv) { apiKeySetupDiv.style.display = 'block'; } // Hide the chat interface elements if (inputForm) { inputForm.style.display = 'none'; } if (searchSuggestions) { searchSuggestions.style.display = 'none'; } } else if (noApiKeys && storedApiKey) { console.log('No server API keys but local storage key found - enabling chat interface'); // Hide the API key setup div if (apiKeySetupDiv) { apiKeySetupDiv.style.display = 'none'; } // Show the chat interface elements if (inputForm) { inputForm.style.display = 'flex'; } // Remove API setup mode class document.body.classList.remove('api-setup-mode'); } } // Render markdown content function renderMarkdown(text) { // Just parse the markdown and return the HTML return marked.parse(text); } // Test function to manually render a Mermaid diagram function testMermaidRendering() { console.log('Testing Mermaid rendering...'); try { // Create a simple test diagram directly const testDiv = document.createElement('div'); testDiv.className = 'mermaid'; testDiv.textContent = 'graph TD;\nA-->B;'; document.body.appendChild(testDiv); console.log('Created test diagram with content:', testDiv.textContent); // Render the direct mermaid div setTimeout(() => { try { console.log('Running mermaid on test div'); if (typeof mermaid.run === 'function') { console.log('Using mermaid.run() for test'); mermaid.run({ nodes: [testDiv] }); } else if (typeof mermaid.init === 'function') { console.log('Using mermaid.init() for test'); mermaid.init(undefined, [testDiv]); } // Verify if rendering worked setTimeout(() => { const svg = testDiv.querySelector('svg'); if (svg) { console.log('Test diagram rendered successfully!'); } else { console.error('Test diagram did not render to SVG'); } // Remove test div after verification document.body.removeChild(testDiv); }, 100); } catch (error) { console.error('Error rendering test mermaid diagram:', error); console.error('Error details:', error.message); // Remove test div on error document.body.removeChild(testDiv); } }, 200); } catch (error) { console.error('Unexpected error in test function:', error); } } // Run test on page load window.addEventListener('DOMContentLoaded', () => { setTimeout(testMermaidRendering, 1000); }); // Connect to SSE endpoint for tool calls let eventSource; let currentAiMessageDiv = null; function connectToToolEvents() { // Close existing connection if any if (eventSource) { console.log('Closing existing SSE connection'); eventSource.close(); } // Clear any existing displayed tool calls when connecting with a new session ID if (window.displayedToolCalls) { window.displayedToolCalls.clear(); console.log('Cleared displayed tool calls for new session'); } console.log(`%c Connecting to SSE endpoint with session ID: ${sessionId}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;'); // Connect to SSE endpoint with session ID const sseUrl = `/api/tool-events?sessionId=${sessionId}`; console.log('SSE URL:', sseUrl); // Add a timestamp to prevent caching in Firefox const nocacheUrl = `${sseUrl}&_nocache=${Date.now()}`; eventSource = new EventSource(nocacheUrl); // Handle connection event eventSource.addEventListener('connection', (event) => { console.log('%c Connected to tool events stream', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event.data); try { const connectionData = JSON.parse(event.data); console.log('Connection data:', connectionData); } catch (error) { console.error('Error parsing connection data:', error, event.data); } }); // Handle test events eventSource.addEventListener('test', (event) => { console.log('%c Received test event:', 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;', event.data); try { const testData = JSON.parse(event.data); console.log('%c Test data:', 'background: #673AB7; color: white; padding: 2px 5px; border-radius: 2px;', testData); // Log specific test data properties console.log('Test message:', testData.message); console.log('Test timestamp:', testData.timestamp); console.log('Test session ID:', testData.sessionId); if (testData.status) { console.log('Test status:', testData.status); } if (testData.connectionInfo) { console.log('Connection info:', testData.connectionInfo); } if (testData.sequence === 2) { console.log('%c SSE connection fully verified with follow-up test', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;'); } // Add a visual indicator that the SSE connection is working const connectionIndicator = document.createElement('div'); connectionIndicator.style.position = 'fixed'; connectionIndicator.style.bottom = '10px'; connectionIndicator.style.right = '10px'; connectionIndicator.style.backgroundColor = '#4CAF50'; connectionIndicator.style.color = 'white'; connectionIndicator.style.padding = '5px 10px'; connectionIndicator.style.borderRadius = '4px'; connectionIndicator.style.fontSize = '12px'; connectionIndicator.style.zIndex = '1000'; connectionIndicator.style.opacity = '0.8'; connectionIndicator.textContent = 'SSE Connected'; // Remove after 3 seconds setTimeout(() => { if (document.body.contains(connectionIndicator)) { document.body.removeChild(connectionIndicator); } }, 3000); document.body.appendChild(connectionIndicator); } catch (error) { console.error('Error parsing test event data:', error, event.data); } }); // Initialize a Set to track displayed tool calls if (!window.displayedToolCalls) { window.displayedToolCalls = new Set(); } // Handle tool call events eventSource.addEventListener('toolCall', (event) => { // If no request is in progress, ignore the tool call if (!isRequestInProgress) { console.log('Tool call received but no request in progress, ignoring'); return; } console.log('%c Received tool call event:', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 2px;', event); try { const toolCall = JSON.parse(event.data); console.log('%c Tool call data:', 'background: #2196F3; color: white; padding: 2px 5px; border-radius: 2px;', toolCall); // Skip events with status "started" - only process "completed" events if (toolCall.status === "started") { console.log('%c Skipping "started" event, waiting for "completed"', 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;'); return; } // Create a unique identifier for this tool call const query = toolCall.args.query || toolCall.args.keywords || toolCall.args.pattern || ''; const path = toolCall.args.path || toolCall.args.folder || '.'; // Create a simpler fingerprint that doesn't include timestamp // This helps catch duplicate events with different timestamps const toolCallFingerprint = `${toolCall.name}-${query}-${path}`; // Check if we've already displayed this exact tool call if (window.displayedToolCalls.has(toolCallFingerprint)) { console.log(`%c Skipping duplicate tool call: ${toolCallFingerprint}`, 'background: #FF9800; color: white; padding: 2px 5px; border-radius: 2px;'); return; } // Add this tool call to our set of displayed tool calls window.displayedToolCalls.add(toolCallFingerprint); console.log(`%c Added tool call to displayed set: ${toolCallFingerprint}`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;'); // Format the tool call description for display let toolDescription = ''; if (toolCall.name === 'searchCode' || toolCall.name === 'search') { const language = toolCall.args.language; const exact = toolCall.args.exact; let locationInfo = path !== '.' ? ` in ${path}` : ''; let languageInfo = language ? ` (language: ${language})` : ''; let exactInfo = exact === true ? ` (exact match)` : ''; toolDescription = `Searching code with "${query}"${locationInfo}${languageInfo}${exactInfo}`; } else if (toolCall.name === 'queryCode' || toolCall.name === 'query') { toolDescription = `Querying code with pattern "${query}"${path === '.' ? '' : ` in ${path}`}`; } else if (toolCall.name === 'extractCode' || toolCall.name === 'extract') { const filePath = toolCall.args.file_path || ''; const line = toolCall.args.line; const endLine = toolCall.args.end_line; let lineInfo = ''; if (line && endLine) { lineInfo = ` (lines ${line}-${endLine})`; } else if (line) { lineInfo = ` (from line ${line})`; } toolDescription = `Extracting code from ${filePath}${lineInfo}`; } else { toolDescription = `Using ${toolCall.name} tool`; } // Log the tool call being processed console.log(`%c Processing tool call: "${toolDescription}"`, 'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 2px;'); // Add tool call to the current AI message if it exists if (currentAiMessageDiv) { addToolCallToMessage(currentAiMessageDiv, toolCall); } else { console.warn('No current AI message div to add tool call to'); // Create a temporary div to display the tool call const tempDiv = document.createElement('div'); tempDiv.className = 'ai-message'; tempDiv.innerHTML = '<div class="tool-call-header">Tool call received but no message context</div>'; messagesDiv.appendChild(tempDiv); addToolCallToMessage(tempDiv, toolCall); } } catch (error) { console.error('Error parsing tool call data:', error, event.data); } }); // Handle errors eventSource.onerror = (error) => { console.error('%c SSE Error:', 'background: #F44336; color: white; padding: 2px 5px; border-radius: 2px;', error); // Log detailed readyState information const readyStateMap = { 0: 'CONNECTING', 1: 'OPEN', 2: 'CLOSED' }; const readyState = eventSource.readyState; console.log(`EventSource readyState: ${readyState} (${readyStateMap[readyState] || 'UNKNOWN'})`); // Check if the connecti