@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
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;
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> - 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 {