improved design

This commit is contained in:
Aaron Elijah Mars
2025-06-28 20:26:46 +02:00
parent 30daf29374
commit 3006bceac4
10 changed files with 224 additions and 605 deletions

View File

@@ -18,8 +18,8 @@ The system uses a hybrid intelligence architecture:
### Core Components
- `background.js:44-213` - Defines 8 MCP tools for page analysis, content extraction, and element interaction
- `content.js:4-50` - Pattern database with confidence-scored selectors for known sites
- `background.js:44-213` - Defines 17 MCP tools for page analysis, content extraction, element interaction, tab management, and data access
- `content.js:4-95` - Enhanced pattern database with confidence-scored selectors for known sites
- `server.js:14-143` - MCP protocol implementation with tool registration and WebSocket handling
## Development Commands
@@ -44,7 +44,7 @@ curl http://localhost:3001/health # Check server and extension status
## MCP Tool Categories
### Core Automation Tools (6 tools)
### Web Browser Automation Tools (8 tools)
- `page_analyze` - **Two-phase intelligent analysis** with element state detection
- Phase 1 (`discover`): Quick scan with element state (enabled/disabled, clickable)
- Phase 2 (`detailed`): Full analysis with element fingerprinting and interaction readiness
@@ -58,19 +58,23 @@ curl http://localhost:3001/health # Check server and extension status
- Natural focus sequence: click → focus → fill for modern web apps
- Comprehensive event simulation (beforeinput, input, change, composition)
- Validation of successful fill with actual value verification
- `element_get_state` - Get detailed element state (disabled, clickable, focusable, empty)
- `page_navigate` - Navigate with optional element wait conditions
- `page_wait_for` - Wait for elements or text to appear
- `page_scroll` - Scroll pages in various directions
### Element State Tools (1 tool)
- `element_get_state` - Get detailed element state (disabled, clickable, focusable, empty)
### Tab Management Tools (4 tools)
- `tab_create` - Create new tabs with advanced options
- `tab_close` - Close tabs with flexible targeting
- `tab_list` - Get comprehensive tab information
- `tab_switch` - Switch between tabs intelligently
### Analytics Tools (2 tools)
- `get_analytics` - Token usage analytics and performance metrics
- `clear_analytics` - Reset performance tracking data
### Legacy Tools (2 tools)
- `browser_navigate` - Basic navigation (compatibility)
- `browser_execute_script` - JavaScript execution with CSP fallbacks
### Browser Data Access Tools (5 tools)
- `get_bookmarks` - Get all bookmarks or search for specific ones
- `add_bookmark` - Add new bookmarks with folder support
- `get_history` - Search browser history with comprehensive filters
- `get_selected_text` - Get currently selected text with rich metadata
- `get_page_links` - Get all hyperlinks with filtering options
## Key Implementation Details

BIN
opendia-extension/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
opendia-extension/logo.mp4 Normal file

Binary file not shown.

View File

@@ -1,8 +1,14 @@
{
"manifest_version": 3,
"name": "OpenDia Enhanced Browser Automation",
"version": "2.0.0",
"description": "Enhanced browser automation through Model Context Protocol with pattern database and semantic analysis",
"name": "OpenDia",
"version": "1.0.0",
"description": "Browser automation through Model Context Protocol",
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
},
"permissions": [
"tabs",
"activeTab",
@@ -21,7 +27,7 @@
},
"action": {
"default_popup": "popup.html",
"default_title": "OpenDia Enhanced Browser Automation"
"default_title": "OpenDia"
},
"content_scripts": [
{

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>OpenDia Browser Bridge</title>
<title>OpenDia</title>
<style>
:root {
--primary-color: #2563eb;
@@ -19,8 +19,27 @@
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--text-color);
background: var(--bg-color);
background: linear-gradient(135deg, #0081F7 0%, #FF75CA 50%, #FFAE87 100%);
margin: 0;
min-height: 300px;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
z-index: 0;
}
body > * {
position: relative;
z-index: 1;
}
.header {
@@ -33,13 +52,31 @@
width: 32px;
height: 32px;
margin-right: 12px;
background: var(--primary-color);
border-radius: 8px;
background: linear-gradient(135deg, #0081F7, #FF75CA);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
box-shadow: 0 2px 8px rgba(0, 129, 247, 0.3);
overflow: hidden;
position: relative;
}
.logo video {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
}
.logo span {
position: relative;
z-index: 1;
}
h2 {
@@ -53,9 +90,11 @@
align-items: center;
margin-bottom: 20px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.6);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.status-indicator {
@@ -77,11 +116,13 @@
}
.info {
background-color: #f8fafc;
background: rgba(255, 255, 255, 0.6);
padding: 16px;
border-radius: 8px;
border-radius: 10px;
margin-bottom: 16px;
border: 1px solid var(--border-color);
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.info-row {
@@ -102,6 +143,51 @@
.info-value {
font-weight: 500;
}
.tooltip {
position: relative;
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 2px;
text-decoration-color: #9ca3af;
}
.tooltip-content {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
transform: translateY(-5px);
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 10px;
border-radius: 6px;
font-size: 0.65rem;
font-weight: normal;
white-space: pre-wrap;
width: 320px;
margin: 0 auto;
text-align: center;
line-height: 1.3;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
word-wrap: break-word;
}
.info-row .tooltip .tooltip-content {
left: -350px;
margin: 0;
}
.tooltip:hover .tooltip-content {
opacity: 1;
visibility: visible;
}
.button-group {
display: flex;
@@ -110,219 +196,70 @@
}
button {
background-color: var(--primary-color);
color: white;
border: none;
background: rgba(255, 255, 255, 0.3);
color: #0081F7;
border: 1px solid rgba(0, 129, 247, 0.2);
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
font-weight: 600;
font-size: 0.875rem;
transition: background-color 0.2s ease;
transition: all 0.3s ease;
flex: 1;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 117, 202, 0.3), transparent);
transition: left 0.6s ease;
}
button:hover {
background-color: var(--hover-color);
}
.testing-section {
margin-top: 20px;
border-top: 1px solid var(--border-color);
padding-top: 20px;
}
.section-title {
font-weight: 600;
margin-bottom: 12px;
color: var(--text-color);
font-size: 0.9rem;
}
.test-form {
margin-bottom: 16px;
}
.form-group {
margin-bottom: 12px;
}
.form-label {
display: block;
font-size: 0.8rem;
color: #6b7280;
margin-bottom: 4px;
font-weight: 500;
}
select, input[type="text"] {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
background: white;
color: var(--text-color);
box-sizing: border-box;
}
select:focus, input[type="text"]:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.test-results {
margin-top: 16px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid var(--border-color);
max-height: 300px;
overflow-y: auto;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
display: none;
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 117, 202, 0.4);
box-shadow: 0 6px 20px rgba(255, 117, 202, 0.2);
}
.test-results-expanded {
max-height: 500px;
}
.test-results.show {
display: block;
}
.result-header {
font-weight: 600;
margin-bottom: 8px;
color: var(--text-color);
}
.result-meta {
font-size: 0.7rem;
color: #6b7280;
margin-bottom: 8px;
}
.result-content {
white-space: pre-wrap;
font-size: 0.7rem;
background: white;
padding: 8px;
border-radius: 4px;
border: 1px solid #e5e7eb;
margin: 8px 0;
max-height: 120px;
overflow-y: auto;
button:hover::before {
left: 100%;
}
.expand-button {
background-color: #6b7280;
font-size: 0.7rem;
padding: 4px 8px;
margin-top: 8px;
border-radius: 4px;
color: white;
border: none;
cursor: pointer;
}
.expand-button:hover {
background-color: #4b5563;
}
.json-viewer {
background: #1f2937;
color: #f3f4f6;
padding: 12px;
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.65rem;
white-space: pre-wrap;
margin: 8px 0;
max-height: 250px;
overflow-y: auto;
border: 1px solid #374151;
line-height: 1.4;
}
.size-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.75rem;
margin-top: 8px;
}
.size-value {
font-weight: 600;
}
.size-value.large {
color: var(--error-color);
}
.size-value.normal {
color: var(--success-color);
}
.test-button {
background-color: #10b981;
margin-top: 8px;
}
.test-button:hover {
background-color: #059669;
}
.highlight-button {
background-color: #f59e0b;
font-size: 0.75rem;
padding: 6px 12px;
margin-left: 8px;
}
.highlight-button:hover {
background-color: #d97706;
}
.log {
background-color: #f8fafc;
border: 1px solid var(--border-color);
padding: 12px;
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.log-entry {
margin-bottom: 4px;
padding: 4px 0;
border-bottom: 1px solid var(--border-color);
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #6b7280;
margin-right: 8px;
button:active {
transform: translateY(0);
}
</style>
</head>
<body>
<div class="header">
<div class="logo">OD</div>
<h2>OpenDia Browser Bridge</h2>
<div class="logo">
<video autoplay loop muted playsinline>
<source src="logo.webm" type="video/webm">
<source src="logo.mp4" type="video/mp4">
<span>OD</span>
</video>
</div>
<h2>OpenDia</h2>
</div>
<div class="status">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">Checking connection...</span>
<span id="statusText" class="tooltip">
Checking connection...
<span class="tooltip-content">
Make sure your MCP server is connected.
If it's the case, click on Reconnect.
If it still don't work, kill your 3000 port & try again.
</span>
</span>
</div>
<div class="info">
@@ -342,48 +279,6 @@
<div class="button-group">
<button id="reconnectBtn">Reconnect</button>
<button id="testBtn">Test Connection</button>
</div>
<div class="testing-section">
<div class="section-title">🔬 Testing Tools</div>
<div class="test-form">
<div class="form-group">
<label class="form-label" for="content-type">Content Extraction</label>
<select id="content-type">
<option value="article">Article Content</option>
<option value="search_results">Search Results</option>
<option value="posts">Posts/Feed Items</option>
</select>
</div>
<button class="test-button" id="test-extract">Test Extract</button>
</div>
<div class="test-form">
<div class="form-group">
<label class="form-label" for="intent-hint">Element Analysis</label>
<input type="text" id="intent-hint" placeholder="e.g., search, post_tweet, login">
</div>
<button class="test-button" id="test-analyze">Test Analyze</button>
<button class="highlight-button" id="highlight-elements">🎯 Highlight</button>
</div>
<div class="test-results" id="results">
<div class="result-header">Test Results</div>
<div class="result-meta" id="result-meta"></div>
<div class="result-content" id="result-content"></div>
<div class="size-info" id="data-size"></div>
<button class="expand-button" id="expand-results">📄 View Full JSON</button>
<div class="json-viewer" id="json-viewer" style="display: none;"></div>
</div>
</div>
<div class="log" id="log">
<div class="log-entry">
<span class="log-time">[System]</span>
<span>Waiting for activity...</span>
</div>
</div>
<script src="popup.js"></script>

View File

@@ -1,26 +1,29 @@
// Enhanced Popup with Testing Interface
let logContainer = document.getElementById("log");
// OpenDia Popup
let statusIndicator = document.getElementById("statusIndicator");
let statusText = document.getElementById("statusText");
let toolCount = document.getElementById("toolCount");
let currentPage = document.getElementById("currentPage");
let resultArea = document.getElementById("results");
let resultMeta = document.getElementById("result-meta");
let resultContent = document.getElementById("result-content");
let dataSizeInfo = document.getElementById("data-size");
let expandButton = document.getElementById("expand-results");
let jsonViewer = document.getElementById("json-viewer");
// Get dynamic tool count from background script
function updateToolCount() {
const toolsList = [
"page_analyze", "page_extract_content", "element_click", "element_fill",
"element_get_state", "page_navigate", "page_wait_for", "page_scroll",
"tab_create", "tab_close", "tab_list", "tab_switch",
"get_bookmarks", "add_bookmark", "get_history", "get_selected_text", "get_page_links"
];
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ action: "getToolCount" }, (response) => {
if (!chrome.runtime.lastError && response?.toolCount) {
toolCount.textContent = response.toolCount;
addLog(`Updated tool count: ${response.toolCount}`, "info");
toolCount.innerHTML = `<span class="tooltip">${response.toolCount}
<span class="tooltip-content">Available MCP Tools:\npage_analyze • page_extract_content • element_click • element_fill • element_get_state • page_navigate • page_wait_for • page_scroll • tab_create • tab_close • tab_list • tab_switch • get_bookmarks • add_bookmark • get_history • get_selected_text • get_page_links</span>
</span>`;
} else {
// Fallback to calculating from background script
toolCount.textContent = "18"; // Expected total based on background script
toolCount.innerHTML = `<span class="tooltip">17
<span class="tooltip-content">Available MCP Tools:\npage_analyze • page_extract_content • element_click • element_fill • element_get_state • page_navigate • page_wait_for • page_scroll • tab_create • tab_close • tab_list • tab_switch • get_bookmarks • add_bookmark • get_history • get_selected_text • get_page_links</span>
</span>`;
}
});
}
@@ -32,7 +35,6 @@ function checkStatus() {
chrome.runtime.sendMessage({ action: "getStatus" }, (response) => {
if (chrome.runtime.lastError) {
updateStatus(false);
addLog("Extension background script not responding", "error");
} else {
updateStatus(response?.connected || false);
}
@@ -50,7 +52,6 @@ function checkStatus() {
});
} else {
updateStatus(false);
addLog("Extension context invalid", "error");
}
}
@@ -62,10 +63,12 @@ setInterval(checkStatus, 2000);
function updateStatus(connected) {
if (connected) {
statusIndicator.className = "status-indicator connected";
statusText.textContent = "Connected to MCP server";
statusText.innerHTML = `Connected to MCP server
<span class="tooltip-content">Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.</span>`;
} else {
statusIndicator.className = "status-indicator disconnected";
statusText.textContent = "Disconnected from MCP server";
statusText.innerHTML = `Disconnected from MCP server
<span class="tooltip-content">Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.</span>`;
}
}
@@ -73,345 +76,56 @@ function updateStatus(connected) {
document.getElementById("reconnectBtn").addEventListener("click", () => {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ action: "reconnect" }, (response) => {
if (chrome.runtime.lastError) {
addLog(chrome.runtime.lastError.message, "error");
} else {
addLog("Attempting to reconnect...", "info");
if (!chrome.runtime.lastError) {
setTimeout(checkStatus, 1000);
}
});
}
});
// Test button
document.getElementById("testBtn").addEventListener("click", () => {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ action: "test" }, (response) => {
if (chrome.runtime.lastError) {
addLog(chrome.runtime.lastError.message, "error");
} else {
addLog("Sending test message...", "info");
}
});
}
});
// Testing Interface Functions
class TestingInterface {
constructor() {
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('test-extract').addEventListener('click', () => this.testExtraction());
document.getElementById('test-analyze').addEventListener('click', () => this.testAnalysis());
document.getElementById('highlight-elements').addEventListener('click', () => this.highlightElements());
expandButton.addEventListener('click', () => this.toggleJsonViewer());
}
async testExtraction() {
const contentType = document.getElementById('content-type').value;
addLog(`🔍 Testing content extraction: ${contentType}`, "info");
try {
const result = await this.sendToContentScript({
action: 'extract_content',
data: { content_type: contentType }
});
this.displayResults(result, 'Content Extraction');
addLog(`Extraction completed in ${result.execution_time}ms`, "success");
} catch (error) {
addLog(`Extraction failed: ${error.message}`, "error");
}
}
async testAnalysis() {
const intentHint = document.getElementById('intent-hint').value;
if (!intentHint.trim()) {
addLog('Please enter an intent hint', "error");
return;
}
addLog(`🎯 Testing page analysis: ${intentHint}`, "info");
try {
const result = await this.sendToContentScript({
action: 'analyze',
data: { intent_hint: intentHint }
});
this.displayResults(result, 'Page Analysis');
addLog(`Analysis completed in ${result.execution_time}ms`, "success");
} catch (error) {
addLog(`Analysis failed: ${error.message}`, "error");
}
}
async highlightElements() {
const intentHint = document.getElementById('intent-hint').value;
if (!intentHint.trim()) {
addLog('Please enter an intent hint to highlight elements', "error");
return;
}
try {
const result = await this.sendToContentScript({
action: 'analyze',
data: { intent_hint: intentHint }
});
if (result.success && result.data.elements?.length > 0) {
// Inject highlighting script
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
func: this.highlightElementsOnPage,
args: [result.data.elements]
});
});
addLog(`🎯 Highlighted ${result.data.elements.length} elements`, "success");
} else {
addLog('No elements found to highlight', "error");
}
} catch (error) {
addLog(`Highlighting failed: ${error.message}`, "error");
}
}
highlightElementsOnPage(elements) {
// Remove existing highlights
document.querySelectorAll('.opendia-highlight').forEach(el => {
el.classList.remove('opendia-highlight');
el.style.removeProperty('outline');
});
// Add new highlights
elements.forEach((elementData, index) => {
try {
const element = document.querySelector(elementData.selector);
if (element) {
element.classList.add('opendia-highlight');
element.style.outline = `3px solid ${this.getHighlightColor(elementData.confidence)}`;
element.style.outlineOffset = '2px';
// Add tooltip
element.title = `OpenDia: ${elementData.name} (${Math.round(elementData.confidence * 100)}%)`;
}
} catch (error) {
console.warn('Failed to highlight element:', elementData.selector, error);
}
});
// Auto-remove highlights after 10 seconds
setTimeout(() => {
document.querySelectorAll('.opendia-highlight').forEach(el => {
el.classList.remove('opendia-highlight');
el.style.removeProperty('outline');
el.style.removeProperty('outline-offset');
el.removeAttribute('title');
});
}, 10000);
}
getHighlightColor(confidence) {
if (confidence > 0.8) return '#22c55e'; // Green for high confidence
if (confidence > 0.6) return '#f59e0b'; // Orange for medium confidence
return '#ef4444'; // Red for low confidence
}
async sendToContentScript(message) {
return new Promise((resolve, reject) => {
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (response.success) {
resolve(response);
} else {
reject(new Error(response.error || 'Unknown error'));
}
});
} else {
reject(new Error('No active tab found'));
}
});
});
}
displayResults(result, testType) {
resultArea.classList.add('show');
this.lastResult = result; // Store for JSON viewer
// Display metadata with better formatting
const method = result.data.method || 'N/A';
const confidence = result.data.confidence ? Math.round(result.data.confidence * 100) + '%' : 'N/A';
const elementsCount = result.data.elements ? result.data.elements.length : 0;
resultMeta.innerHTML = `
<strong>✅ ${testType}</strong><br>
<small>
📊 Method: <code>${method}</code> |
⏱️ Time: <code>${result.execution_time}ms</code> |
🎯 Confidence: <code>${confidence}</code>
${elementsCount > 0 ? ` | 🔍 Elements: <code>${elementsCount}</code>` : ''}
</small>
`;
// Display enhanced content preview
const preview = this.createEnhancedPreview(result.data, testType);
resultContent.innerHTML = preview;
// Display size info
this.displayDataSize(result);
// Store full JSON for viewer
this.updateJsonViewer(result);
}
createEnhancedPreview(data, testType) {
if (testType === 'Page Analysis' && data.elements?.length > 0) {
return this.createElementsPreview(data.elements);
} else if (testType === 'Content Extraction' && data.content) {
return this.createContentPreview(data.content, data.content_type);
} else {
return `<div style="color: #6b7280; font-style: italic;">No relevant data found</div>`;
}
}
createElementsPreview(elements) {
const maxElements = 5;
const preview = elements.slice(0, maxElements).map(e => {
const confidenceColor = e.confidence > 0.8 ? '#22c55e' : e.confidence > 0.6 ? '#f59e0b' : '#ef4444';
return `
<div style="margin: 4px 0; padding: 6px; background: #f9fafb; border-radius: 4px; border-left: 3px solid ${confidenceColor};">
<strong>${e.name}</strong>
<div style="font-size: 0.65rem; color: #6b7280; margin-top: 2px;">
Type: ${e.type} | Confidence: ${Math.round(e.confidence * 100)}% | ID: ${e.id}
</div>
<div style="font-size: 0.6rem; color: #9ca3af; font-family: monospace; margin-top: 2px;">
${e.selector}
</div>
</div>
`;
}).join('');
const remaining = elements.length - maxElements;
const remainingText = remaining > 0 ? `<div style="color: #6b7280; font-size: 0.7rem; margin-top: 8px;">+ ${remaining} more elements...</div>` : '';
return preview + remainingText;
}
createContentPreview(content, contentType) {
if (typeof content === 'object') {
if (content.title) {
return `
<div style="margin-bottom: 8px;">
<strong>📰 ${content.title}</strong>
</div>
<div style="font-size: 0.65rem; color: #6b7280;">
${content.word_count ? `📝 Words: ${content.word_count}` : ''}
${content.reading_time ? ` | ⏱️ Read time: ${content.reading_time}min` : ''}
</div>
<div style="margin-top: 8px; font-size: 0.7rem; max-height: 60px; overflow: hidden;">
${(content.content || '').substring(0, 200)}${content.content?.length > 200 ? '...' : ''}
</div>
`;
} else {
return `<pre style="font-size: 0.65rem; max-height: 80px; overflow: hidden;">${JSON.stringify(content, null, 2).substring(0, 300)}</pre>`;
}
} else {
return `<div style="max-height: 80px; overflow: hidden; font-size: 0.7rem;">${content.substring(0, 300)}${content.length > 300 ? '...' : ''}</div>`;
}
}
displayDataSize(result) {
const dataSize = result.data_size;
const readableSize = this.formatBytes(dataSize);
const tokenEstimate = Math.round(dataSize / 4); // Rough token estimate
dataSizeInfo.innerHTML = `
<span class="size-label">Data Size:</span>
<span class="size-value ${dataSize > 10000 ? 'large' : 'normal'}">${readableSize}</span>
<span class="compression-info">(~${tokenEstimate} tokens)</span>
`;
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
toggleJsonViewer() {
if (jsonViewer.style.display === 'none') {
jsonViewer.style.display = 'block';
expandButton.textContent = '📄 Hide JSON';
resultArea.classList.add('test-results-expanded');
} else {
jsonViewer.style.display = 'none';
expandButton.textContent = '📄 View Full JSON';
resultArea.classList.remove('test-results-expanded');
}
}
updateJsonViewer(result) {
const formattedJson = this.formatJson(result);
jsonViewer.textContent = formattedJson;
}
formatJson(obj) {
return JSON.stringify(obj, null, 2);
}
}
// Initialize testing interface
const testingInterface = new TestingInterface();
// Add log entry
function addLog(message, type = "info") {
const entry = document.createElement("div");
entry.className = "log-entry";
const time = document.createElement("span");
time.className = "log-time";
time.textContent = `[${new Date().toLocaleTimeString()}]`;
const content = document.createElement("span");
content.textContent = message;
if (type === "error") {
content.style.color = "var(--error-color)";
} else if (type === "success") {
content.style.color = "var(--success-color)";
}
entry.appendChild(time);
entry.appendChild(content);
logContainer.appendChild(entry);
// Keep only last 20 entries
while (logContainer.children.length > 20) {
logContainer.removeChild(logContainer.firstChild);
}
logContainer.scrollTop = logContainer.scrollHeight;
}
// Listen for updates from background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "statusUpdate") {
updateStatus(message.connected);
if (message.connected) {
addLog("Connected to MCP server", "success");
} else {
addLog("Disconnected from MCP server", "error");
}
} else if (message.type === "log") {
addLog(message.message, message.type || "info");
}
});
});
// Video speed control based on mouse movement
const logoVideo = document.querySelector('.logo video');
let mouseTimeout;
let lastMouseX = 0;
let lastMouseY = 0;
let mouseSpeed = 0;
if (logoVideo) {
// Track mouse movement and calculate speed
document.addEventListener('mousemove', (e) => {
const deltaX = e.clientX - lastMouseX;
const deltaY = e.clientY - lastMouseY;
// Calculate mouse speed (distance moved)
mouseSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Update video playback rate based on mouse speed
// Faster mouse = faster video (clamped between 0.2x and 10x)
// Very sensitive to mouse movement (divided by 15 for more responsiveness)
const playbackRate = Math.min(10, Math.max(0.2, 1 + (mouseSpeed / 15)));
logoVideo.playbackRate = playbackRate;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
// Clear existing timeout
clearTimeout(mouseTimeout);
// Reset to normal speed after mouse stops
mouseTimeout = setTimeout(() => {
logoVideo.playbackRate = 1;
}, 100);
});
// Set initial playback rate
logoVideo.playbackRate = 1;
}