UNPKG

@probelabs/probe-chat

Version:

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

1,822 lines (1,584 loc) 119 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; display: block; /* Ensure header is visible by default */ } .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; } .header-left a { text-decoration: none; display: inline-block; } .header-left a:hover .header-logo { opacity: 0.8; transition: opacity 0.2s ease; } .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; } /* History dropdown styles */ .history-dropdown { position: relative; display: inline-block; margin-right: 5px; } .history-button { font-size: 15px; color: #555; background: none; border: 1px solid #ddd; border-radius: 4px; padding: 5px 10px; cursor: pointer; display: flex; align-items: center; gap: 5px; font-weight: 500; transition: all 0.2s ease; } .history-button:hover { background-color: #f5f5f5; border-color: #ccc; } .history-button svg { width: 16px; height: 16px; } .history-dropdown-menu { position: absolute; top: 100%; left: 0; background: white; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 1000; min-width: 320px; max-width: 400px; max-height: 400px; overflow-y: auto; display: none; margin-top: 2px; } .history-dropdown-menu.show { display: block; } .history-dropdown-header { padding: 12px 16px 8px; font-size: 14px; font-weight: 600; color: #333; border-bottom: 1px solid #eee; } .history-loading { padding: 16px; text-align: center; color: #666; font-size: 14px; } .history-empty { padding: 16px; text-align: center; color: #666; font-size: 14px; } .history-list { padding: 8px 0; } .history-item { padding: 12px 16px; cursor: pointer; border-bottom: 1px solid #f5f5f5; transition: background-color 0.2s ease; } .history-item:last-child { border-bottom: none; } .history-item:hover { background-color: #f8f9fa; } .history-item-preview { font-size: 14px; color: #333; margin-bottom: 4px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .history-item-meta { font-size: 12px; color: #666; display: flex; justify-content: space-between; align-items: center; } .history-item-time { font-size: 11px; } .history-item-count { font-size: 11px; background: #e9ecef; padding: 2px 6px; border-radius: 10px; } #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 40px 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; width: 100%; box-sizing: border-box; /* 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; } #search-button { padding: 12px 20px; background-color: #44CDF3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; white-space: nowrap; flex-shrink: 0; align-self: flex-start; margin-left: 0; height: auto; } #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: 6px; 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; } /* New minimal image upload styles */ .input-wrapper { display: flex; align-items: flex-start; gap: 8px; width: 100%; } .textarea-container { position: relative; flex: 1; display: flex; align-items: flex-start; } #message-input { flex: 1; min-height: 40px; padding-right: 40px; /* Make space for upload icon */ } .image-upload-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 24px; height: 24px; background: none !important; border: none !important; cursor: pointer; color: #999 !important; padding: 4px !important; border-radius: 4px !important; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; z-index: 10; margin: 0 !important; font-size: inherit !important; font-weight: normal !important; box-shadow: none !important; } .image-upload-icon:hover { color: #666 !important; background-color: rgba(0, 0, 0, 0.05) !important; } .image-upload-icon svg { width: 16px; height: 16px; } /* Floating thumbnails */ .floating-thumbnails { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; padding: 0; position: absolute; bottom: 100%; left: 0; right: 0; z-index: 101; justify-content: flex-start; } .floating-thumbnail { position: relative; width: 60px; height: 60px; } .floating-thumbnail img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; border: 1px solid #e0e0e0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .floating-thumbnail-remove { position: absolute; top: 0px; right: -16px; width: 16px; height: 16px; background: none !important; color: #000; border: none; cursor: pointer; font-size: 12px; font-weight: bold; display: flex; align-items: center; justify-content: center; line-height: 1; transition: opacity 0.2s ease; opacity: 0.6; text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); } .floating-thumbnail-remove:hover { opacity: 1; } /* Drag and drop styles */ .textarea-container.drag-over textarea { background-color: #e3f2fd; border-color: #2196f3; } /* Image display in messages */ .user-message img, .ai-message img { max-width: 100%; max-height: 300px; border-radius: 8px; margin: 8px 0; border: 1px solid #e0e0e0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s ease; } .user-message img:hover, .ai-message img:hover { transform: scale(1.02); } /* Mobile responsive adjustments */ @media (max-width: 768px) { .floating-thumbnails { gap: 6px; } .floating-thumbnail { width: 50px; height: 50px; } .user-message img, .ai-message img { max-height: 200px; } .image-upload-icon { right: 10px; } #message-input { padding: 12px 36px 12px 16px; } } </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"> <a href="/" title="Go to Home Page"> <img src="/logo.png" alt="Probe Logo" class="header-logo"> </a> <div class="history-dropdown"> <button id="history-button" class="history-button" title="Chat History"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 1v6l4-4"></path> <path d="M21 12c0 5-4 9-9 9s-9-4-9-9 4-9 9-9c2.5 0 4.8 1 6.5 2.8"></path> </svg> History </button> <div id="history-dropdown-menu" class="history-dropdown-menu"> <div class="history-dropdown-header">Recent Chats</div> <div id="history-loading" class="history-loading">Loading...</div> <div id="history-list" class="history-list"></div> <div id="history-empty" class="history-empty" style="display: none;">No recent chats</div> </div> </div> <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;"> <div class="input-wrapper"> <div class="textarea-container"> <!-- Floating image thumbnails - positioned above textarea --> <div id="floating-thumbnails" class="floating-thumbnails" style="display: none;"></div> <textarea id="message-input" placeholder="Ask about code..." required rows="1"></textarea> <input type="file" id="image-upload" accept="image/*" multiple style="display: none;"> <button type="button" id="image-upload-button" class="image-upload-icon" title="Upload images"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.64 16.2a2 2 0 0 1-2.83-2.83l8.49-8.48"/> </svg> </button> </div> <button type="button" id="search-button">Search</button> </div> </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'); } // Initialize session ID - either from URL or generate new one let sessionId; // Check if session ID is provided via URL (from server-side injection) const sessionIdFromUrl = document.body.getAttribute('data-session-id'); if (sessionIdFromUrl) { sessionId = sessionIdFromUrl; console.log(`Using session ID from URL: ${sessionId}`); // Restore session history for existing sessions restoreSessionHistory(sessionId); } else { // Generate new session ID for root path visits sessionId = crypto.randomUUID(); console.log(`Generated new session 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 } })); // Helper function to extract content from XML-wrapped messages function extractContentFromXML(content) { // Handle different XML patterns used by the assistant const patterns = [ /<task>([\s\S]*?)<\/task>/, /<attempt_completion>\s*<result>([\s\S]*?)<\/result>\s*<\/attempt_completion>/, /<result>([\s\S]*?)<\/result>/ ]; for (const pattern of patterns) { const match = content.match(pattern); if (match) { return match[1].trim(); } } // Remove all internal reasoning and tool call XML tags let cleanContent = content; // Remove thinking tags (internal reasoning) - these are never shown to users cleanContent = cleanContent.replace(/<thinking>[\s\S]*?<\/thinking>/gs, ''); // Remove all tool calls - these are rendered separately as tool call boxes cleanContent = cleanContent.replace(/<search>\s*<query>.*?<\/query>\s*(?:<path>.*?<\/path>)?\s*(?:<allow_tests>.*?<\/allow_tests>)?\s*<\/search>/gs, ''); cleanContent = cleanContent.replace(/<extract>\s*<file_path>.*?<\/file_path>\s*(?:<line>.*?<\/line>)?\s*(?:<end_line>.*?<\/end_line>)?\s*<\/extract>/gs, ''); cleanContent = cleanContent.replace(/<query>\s*<pattern>.*?<\/pattern>\s*(?:<path>.*?<\/path>)?\s*(?:<language>.*?<\/language>)?\s*<\/query>/gs, ''); cleanContent = cleanContent.replace(/<listFiles>\s*<directory>.*?<\/directory>\s*(?:<pattern>.*?<\/pattern>)?\s*<\/listFiles>/gs, ''); cleanContent = cleanContent.replace(/<searchFiles>\s*<pattern>.*?<\/pattern>\s*(?:<directory>.*?<\/directory>)?\s*<\/searchFiles>/gs, ''); // Clean up extra whitespace and empty lines cleanContent = cleanContent.replace(/\n\s*\n\s*\n/g, '\n\n').replace(/^\s+|\s+$/g, ''); // If after cleaning there's no meaningful content, return empty string if (!cleanContent || cleanContent.trim().length < 3) { return ''; } return cleanContent; } // Function to add a user message to the chat display function addUserMessage(content, images = []) { const userMsgDiv = document.createElement('div'); userMsgDiv.className = 'user-message markdown-content'; // Extract content from XML if wrapped const cleanContent = extractContentFromXML(content); // Render the text message userMsgDiv.innerHTML = renderMarkdown(cleanContent); // Handle images if present if (images && images.length > 0) { images.forEach(imageData => { const img = document.createElement('img'); img.src = imageData.url || imageData; img.style.maxWidth = '100%'; img.style.marginTop = '10px'; userMsgDiv.appendChild(img); }); } messagesDiv.appendChild(userMsgDiv); // Apply syntax highlighting to code blocks userMsgDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); } // Function to parse and extract tool calls from assistant message content function parseToolCallsFromContent(content) { const toolCalls = []; // Pattern to match actual tool call formats that are sent via SSE // These are the tool calls that users see in real-time, not internal reasoning const toolPatterns = [ { name: 'search', pattern: /<search>\s*<query>(.*?)<\/query>\s*(?:<path>(.*?)<\/path>)?\s*(?:<allow_tests>(.*?)<\/allow_tests>)?\s*<\/search>/s }, { name: 'extract', pattern: /<extract>\s*<file_path>(.*?)<\/file_path>\s*(?:<line>(.*?)<\/line>)?\s*(?:<end_line>(.*?)<\/end_line>)?\s*<\/extract>/s }, { name: 'query', pattern: /<query>\s*<pattern>(.*?)<\/pattern>\s*(?:<path>(.*?)<\/path>)?\s*(?:<language>(.*?)<\/language>)?\s*<\/query>/s }, { name: 'listFiles', pattern: /<listFiles>\s*<directory>(.*?)<\/directory>\s*(?:<pattern>(.*?)<\/pattern>)?\s*<\/listFiles>/s }, { name: 'searchFiles', pattern: /<searchFiles>\s*<pattern>(.*?)<\/pattern>\s*(?:<directory>(.*?)<\/directory>)?\s*<\/searchFiles>/s }, ]; toolPatterns.forEach(({ name, pattern }) => { const matches = content.matchAll(new RegExp(pattern.source, pattern.flags + 'g')); for (const match of matches) { const toolCall = { name: name, timestamp: new Date().toISOString(), status: 'completed', args: {} }; // Map captured groups to appropriate argument names if (name === 'search') { toolCall.args.query = match[1]?.trim() || ''; toolCall.args.path = match[2]?.trim() || '.'; toolCall.args.allow_tests = match[3]?.trim() === 'true'; } else if (name === 'extract') { toolCall.args.file_path = match[1]?.trim() || ''; toolCall.args.line = match[2] ? parseInt(match[2].trim()) : undefined; toolCall.args.end_line = match[3] ? parseInt(match[3].trim()) : undefined; } else if (name === 'query') { toolCall.args.pattern = match[1]?.trim() || ''; toolCall.args.path = match[2]?.trim() || '.'; toolCall.args.language = match[3]?.trim() || ''; } else if (name === 'listFiles') { toolCall.args.directory = match[1]?.trim() || '.'; toolCall.args.pattern = match[2]?.trim() || ''; } else if (name === 'searchFiles') { toolCall.args.pattern = match[1]?.trim() || ''; toolCall.args.directory = match[2]?.trim() || '.'; } toolCalls.push(toolCall); } }); return toolCalls; } // Function to add an assistant message to the chat display function addAssistantMessage(content) { const aiMsgDiv = document.createElement('div'); aiMsgDiv.className = 'ai-message markdown-content'; // Parse tool calls from the content first const toolCalls = parseToolCallsFromContent(content); // Add tool calls to the message if any exist toolCalls.forEach(toolCall => { addToolCallToMessage(aiMsgDiv, toolCall); }); // Extract final result content (skip tool call XML tags) const cleanContent = extractContentFromXML(content); // Only add text content if there's actual result content if (cleanContent && cleanContent.trim()) { // Store the original message for copying aiMsgDiv.setAttribute('data-original-markdown', cleanContent); // Process and render the content const processedContent = processMessageForDisplay(cleanContent); const contentDiv = document.createElement('div'); contentDiv.innerHTML = renderMarkdown(processedContent); aiMsgDiv.appendChild(contentDiv); // Apply syntax highlighting to code blocks contentDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); // Apply Mermaid rendering to diagrams const mermaidElements = contentDiv.querySelectorAll('.mermaid, .language-mermaid'); if (mermaidElements.length > 0) { console.log(`Found ${mermaidElements.length} mermaid diagrams in restored message`); try { if (typeof mermaid.run === 'function') { mermaid.run({ nodes: mermaidElements }); } else if (typeof mermaid.init === 'function') { mermaid.init(undefined, mermaidElements); } } catch (error) { console.error('Error rendering mermaid in restored message:', error); } } } // Add to messages container messagesDiv.appendChild(aiMsgDiv); } // Function to restore session history from server async function restoreSessionHistory(sessionId) { try { console.log(`Restoring session history for: ${sessionId}`); const response = await fetch(`/api/session/${sessionId}/history`); const data = await response.json(); console.log(`[DEBUG] Session restoration data:`, { exists: data.exists, historyLength: data.history ? data.history.length : 'null/undefined', condition: data.exists && data.history && data.history.length > 0 }); if (data.exists && data.history && data.history.length > 0) { console.log(`Restored ${data.history.length} messages for session: ${sessionId}`); // Count message types for debugging const messageTypes = {}; data.history.forEach(msg => { messageTypes[msg.role] = (messageTypes[msg.role] || 0) + 1; }); console.log(`[DEBUG] Message types to restore:`, messageTypes); // Render restored messages let currentAiMessage = null; data.history.forEach((message, index) => { console.log(`[DEBUG] Processing message ${index + 1}/${data.history.length}: role=${message.role}`); if (message.role === 'user') { // Ensure images is always an array const images = Array.isArray(message.images) ? message.images : []; addUserMessage(message.content, images); currentAiMessage = null; // Reset for new conversation turn } else if (message.role === 'assistant') { addAssistantMessage(message.content); // Get the most recent AI message for tool call rendering const messagesDiv = document.getElementById('messages'); const aiMessages = messagesDiv.querySelectorAll('.ai-message'); currentAiMessage = aiMessages[aiMessages.length - 1]; } else if (message.role === 'toolCall') { console.log(`[DEBUG] Rendering toolCall message`); // Handle tool call messages during restoration try { // Parse the tool call from the stored message const toolCall = { name: message.metadata?.name || 'unknown', args: message.metadata?.args || {}, timestamp: message.timestamp, status: 'completed' }; // If we have a current AI message, add the tool call to it if (currentAiMessage) { addToolCallToMessage(currentAiMessage, toolCall); } else { console.log(`[DEBUG] No current AI message for tool call, creating temporary message`); // Create a temporary AI message for the tool call const tempDiv = document.createElement('div'); tempDiv.className = 'ai-message'; const messagesDiv = document.getElementById('messages'); messagesDiv.appendChild(tempDiv); addToolCallToMessage(tempDiv, toolCall); currentAiMessage = tempDiv; } } catch (error) { console.error('[DEBUG] Error rendering tool call:', error, message); } } else { console.log(`[DEBUG] Skipping message with role: ${message.role}`); } }); // Update token usage if available if (data.tokenUsage && window.tokenUsageDisplay) { window.tokenUsageDisplay.update(data.tokenUsage); } // Update UI to reflect restored chat state positionInputForm(); // Hide search suggestions when loading from history const searchSuggestions = document.querySelector('.search-suggestions'); if (searchSuggestions) { searchSuggestions.style.display = 'none'; } // Ensure all Mermaid diagrams are rendered after restoration setTimeout(() => { const allMermaidElements = document.querySelectorAll('.mermaid:not([data-processed]), .language-mermaid:not([data-processed])'); if (allMermaidElements.length > 0) { console.log(`Rendering ${allMermaidElements.length} unprocessed mermaid diagrams after session restoration`); try {