@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
HTML
<!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> - 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