better tools

This commit is contained in:
Aaron Elijah Mars
2025-06-25 19:07:09 +02:00
parent 1d870520f0
commit bc1f1d4f3c
7 changed files with 2812 additions and 326 deletions

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
OpenDia is a browser automation tool that provides an open alternative to Dia, enabling AI models to interact with browsers through the Model Context Protocol (MCP). The project consists of two main components:
1. **Chrome Extension** (`opendia-extension/`) - Provides browser automation capabilities
2. **MCP Server** (`opendia-mcp/`) - Bridges the extension to AI models via WebSocket
## Architecture
The system uses a hybrid intelligence architecture:
- **Pattern Database**: Pre-built selectors for Twitter/X, GitHub, and common patterns (99% local operations)
- **Semantic Analysis**: Fallback using HTML semantics and ARIA labels when patterns fail
- **WebSocket Bridge**: Real-time communication between extension and MCP server on port 3000
### 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
- `server.js:14-143` - MCP protocol implementation with tool registration and WebSocket handling
## Development Commands
### MCP Server
```bash
cd opendia-mcp
npm install
npm start # Starts server on ws://localhost:3000
```
### Chrome Extension
1. Go to `chrome://extensions/`
2. Enable "Developer mode"
3. Click "Load unpacked" and select `opendia-extension` directory
4. Extension will auto-connect to MCP server
### Health Check
```bash
curl http://localhost:3001/health # Check server and extension status
```
## MCP Tool Categories
### Core Automation Tools (6 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
- Enhanced pattern database with auth, content, search, nav, and form categories
- `page_extract_content` - **Smart content extraction with summarization**
- Intelligent content detection for articles, search results, and social posts
- Token-efficient summaries with quality metrics and sample items
- Site-specific extraction patterns for Twitter/X, GitHub, Google
- `element_click` - Click elements with auto-scroll and wait conditions
- `element_fill` - **Enhanced form filling** with proper focus simulation
- 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
- `page_navigate` - Navigate with optional element wait conditions
- `page_wait_for` - Wait for elements or text to appear
### Element State Tools (1 tool)
- `element_get_state` - Get detailed element state (disabled, clickable, focusable, empty)
### 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
## Key Implementation Details
### Phase 1 & 2 Token Efficiency Improvements
- **Element Fingerprinting** (`content.js:771-778`): Compact representation using `tag.class@context.position` format
- **Two-Phase Analysis** (`content.js:203-323`): Quick discovery vs detailed analysis with separate registries
- **Enhanced Pattern Database** (`content.js:4-95`): Intent-based categorization (auth, content, search, nav, form)
- **Viewport-Aware Analysis** (`content.js:838-859`): Intersection observer for visibility detection
- **Intelligent Element Scoring** (`content.js:861-869`): Confidence-based filtering and ranking
### Phase 2 Content & Performance Optimization
- **Smart Content Summarization** (`content.js:662-717`): Token-efficient summaries instead of full content
- **Site-Specific Extractors** (`content.js:603-659`): Pattern-based extraction for Twitter/X, GitHub, Google
- **Token Usage Tracking** (`content.js:115-263`): Performance metrics with localStorage persistence
- **Adaptive Optimization** (`content.js:152-166`): Auto-adjustment of limits based on success rates
- **Method Performance Tracking** (`content.js:188-211`): Success rate optimization per page type/intent
### Focus & State Enhancement (Latest)
- **Enhanced Focus Simulation** (`content.js:1218-1254`): Mouse events + focus + React state update
- **Element State Detection** (`content.js:1311-1409`): Comprehensive disabled/clickable/focusable analysis
- **Event Sequence Simulation** (`content.js:1270-1299`): beforeinput, input, change, composition events
- **Modern Web App Support**: Handles React, Vue, Angular state management requirements
### Core Architecture
- Element IDs generated dynamically with dual registries for quick/detailed phases
- Pattern matching prioritizes enhanced patterns → legacy patterns → semantic analysis
- WebSocket connection includes ping/pong heartbeat every 30 seconds
- Tool responses include execution time, confidence metrics, and token estimates
- CSP-aware JavaScript execution with multiple fallback strategies
## Security Considerations
The extension requires broad permissions (`<all_urls>`, tabs, scripting) and establishes localhost WebSocket connections. This is intentional for automation capabilities but should only be used in trusted environments.
## Testing
Use the extension popup to test connection status and tool availability. The MCP server provides real-time status via WebSocket connection state and tool registration logs.

View File

@@ -48,23 +48,58 @@ OpenDia is an open alternative to Dia. Connect to your browser with MCP & do any
}
```
## Available MCP Tools
## Enhanced MCP Tools (8 Total)
Once connected, the following tools will be available through MCP:
### 🎯 Core Automation Tools (6 Tools)
- **browser_navigate**: Navigate to a URL
- **browser_get_tabs**: List all open tabs
- **browser_create_tab**: Create a new tab
- **browser_close_tab**: Close a specific tab
- **browser_execute_script**: Run JavaScript in the active tab
- **browser_get_page_content**: Extract text content from the page
- **browser_take_screenshot**: Capture a screenshot
- **browser_get_bookmarks**: Search bookmarks
- **browser_add_bookmark**: Create a new bookmark
- **browser_get_history**: Search browsing history
- **browser_get_cookies**: Get cookies for a domain
- **browser_fill_form**: Automatically fill form fields
- **browser_click_element**: Click elements on the page
- **page_analyze**: Intelligent page analysis using pattern database + semantic analysis
- Finds relevant elements based on user intent (e.g., "post_tweet", "search", "login")
- Returns confidence-scored elements with stable IDs
- Supports Twitter/X, GitHub, and universal patterns
- **page_extract_content**: Structured content extraction
- Extract articles, search results, or social media posts
- Smart content detection using semantic analysis
- Returns structured data with metadata
- **element_click**: Reliable element clicking
- Uses element IDs from page analysis
- Supports different click types (left, right, double)
- Auto-scrolls elements into view
- **element_fill**: Smart form filling
- Fill input fields and textareas
- Supports contenteditable elements
- Option to clear existing content first
- **page_navigate**: Enhanced navigation
- Navigate to URLs with optional wait conditions
- Wait for specific elements to appear after navigation
- Timeout handling and error reporting
- **page_wait_for**: Conditional waiting
- Wait for elements to become visible
- Wait for specific text to appear on page
- Configurable timeout periods
### 🔧 Essential Legacy Tools (2 Tools)
- **browser_navigate**: Legacy navigation (compatibility)
- **browser_execute_script**: CSP-aware JavaScript execution with fallbacks
## 🚀 Key Features
### Hybrid Intelligence Architecture
- **99% Local Operations**: Pattern database eliminates most LLM calls ($0 cost vs $20+/month)
- **Pattern Database**: Pre-built selectors for Twitter/X, GitHub, and common patterns
- **Semantic Analysis**: Fallback using HTML semantics and ARIA labels
- **Confidence Scoring**: Reliable element detection with quality metrics
### Visual Testing Interface
- **Real-time Testing**: Test content extraction and page analysis
- **Element Highlighting**: Visual feedback with confidence-based colors
- **Performance Metrics**: Execution time and data size monitoring
- **JSON Viewer**: Full result inspection and debugging
## Project Structure

View File

@@ -8,15 +8,20 @@ let reconnectAttempts = 0;
function connectToMCPServer() {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return;
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
mcpSocket = new WebSocket(MCP_SERVER_URL);
mcpSocket.onopen = () => {
console.log('✅ Connected to MCP server');
clearInterval(reconnectInterval);
const tools = getAvailableTools();
console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name));
// Register available browser functions
mcpSocket.send(JSON.stringify({
type: 'register',
tools: getAvailableTools()
tools: tools
}));
};
@@ -26,21 +31,193 @@ function connectToMCPServer() {
};
mcpSocket.onclose = () => {
console.log('❌ Disconnected from MCP server, will reconnect...');
// Attempt to reconnect every 5 seconds
reconnectInterval = setInterval(connectToMCPServer, 5000);
};
mcpSocket.onerror = (error) => {
// Handle error silently in production
console.log('⚠️ MCP WebSocket error:', error);
};
}
// Define available browser tools for MCP
// Define available browser automation tools for MCP
function getAvailableTools() {
return [
// Page Analysis Tools
{
name: "page_analyze",
description: "Two-phase intelligent page analysis with token efficiency optimization",
inputSchema: {
type: "object",
properties: {
intent_hint: {
type: "string",
description: "User intent: login, signup, search, post_create, comment, menu, submit, etc."
},
phase: {
type: "string",
enum: ["discover", "detailed"],
default: "discover",
description: "Analysis phase: 'discover' for quick scan (<100 tokens), 'detailed' for full analysis"
},
focus_areas: {
type: "array",
items: { type: "string" },
description: "Areas to analyze in detail: buttons, forms, navigation, search_elements"
},
max_results: {
type: "number",
default: 5,
maximum: 15,
description: "Maximum number of elements to return"
},
element_ids: {
type: "array",
items: { type: "string" },
description: "Expand specific quick match IDs from discover phase (e.g. ['q1', 'q2'])"
}
},
required: ["intent_hint"]
}
},
{
name: "page_extract_content",
description: "Extract and summarize structured content with token efficiency optimization",
inputSchema: {
type: "object",
properties: {
content_type: {
type: "string",
enum: ["article", "search_results", "posts"],
description: "Type of content to extract"
},
max_items: {
type: "number",
description: "Maximum number of items to extract (for lists/collections)",
default: 20
},
summarize: {
type: "boolean",
default: true,
description: "Return summary instead of full content to save tokens"
}
},
required: ["content_type"]
}
},
// Element Interaction Tools
{
name: "element_click",
description: "Click on a specific page element",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Unique element identifier from page_analyze"
},
click_type: {
type: "string",
enum: ["left", "right", "double"],
default: "left"
},
wait_after: {
type: "number",
description: "Milliseconds to wait after click",
default: 500
}
},
required: ["element_id"]
}
},
{
name: "element_fill",
description: "Fill input field with enhanced focus and event simulation for modern web apps",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Unique element identifier from page_analyze"
},
value: {
type: "string",
description: "Text value to input"
},
clear_first: {
type: "boolean",
description: "Clear existing content before filling",
default: true
},
force_focus: {
type: "boolean",
description: "Use enhanced focus sequence with click simulation for modern apps",
default: true
}
},
required: ["element_id", "value"]
}
},
// Navigation Tools
{
name: "page_navigate",
description: "Navigate to specified URL and wait for page load",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "URL to navigate to"
},
wait_for: {
type: "string",
description: "CSS selector to wait for after navigation (ensures page is ready)"
},
timeout: {
type: "number",
description: "Maximum wait time in milliseconds",
default: 10000
}
},
required: ["url"]
}
},
{
name: "page_wait_for",
description: "Wait for specific element or condition on current page",
inputSchema: {
type: "object",
properties: {
condition_type: {
type: "string",
enum: ["element_visible", "text_present"],
description: "Type of condition to wait for"
},
selector: {
type: "string",
description: "CSS selector for element-based conditions"
},
text: {
type: "string",
description: "Text to wait for (when condition_type is 'text_present')"
},
timeout: {
type: "number",
description: "Maximum wait time in milliseconds",
default: 5000
}
},
required: ["condition_type"]
}
},
// Essential legacy tools for compatibility
{
name: "browser_navigate",
description: "Navigate to a URL in the active tab",
description: "Navigate to a URL in the active tab (legacy)",
inputSchema: {
type: "object",
properties: {
@@ -49,39 +226,6 @@ function getAvailableTools() {
required: ["url"],
},
},
{
name: "browser_get_tabs",
description: "Get all open tabs",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "browser_create_tab",
description: "Create a new tab",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL for the new tab" },
active: {
type: "boolean",
description: "Whether to make the tab active",
},
},
},
},
{
name: "browser_close_tab",
description: "Close a tab by ID",
inputSchema: {
type: "object",
properties: {
tabId: { type: "number", description: "ID of the tab to close" },
},
required: ["tabId"],
},
},
{
name: "browser_execute_script",
description: "Execute JavaScript in the active tab",
@@ -93,111 +237,46 @@ function getAvailableTools() {
required: ["code"],
},
},
// Element State Tools
{
name: "browser_get_page_content",
description: "Get the content of the active page",
name: "element_get_state",
description: "Get detailed state information for a specific element (disabled, clickable, etc.)",
inputSchema: {
type: "object",
properties: {
selector: {
element_id: {
type: "string",
description: "CSS selector to get specific content",
},
description: "Element ID from page_analyze"
}
},
},
required: ["element_id"]
}
},
// Analytics and Performance Tools
{
name: "browser_take_screenshot",
description: "Take a screenshot of the active tab",
name: "get_analytics",
description: "Get token usage analytics and performance metrics",
inputSchema: {
type: "object",
properties: {
format: {
type: "string",
enum: ["png", "jpeg"],
description: "Image format",
},
},
},
properties: {},
additionalProperties: false
}
},
{
name: "browser_get_bookmarks",
description: "Get browser bookmarks",
name: "clear_analytics",
description: "Clear all analytics data and reset performance tracking",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query for bookmarks" },
},
},
},
{
name: "browser_add_bookmark",
description: "Add a bookmark",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Bookmark title" },
url: { type: "string", description: "Bookmark URL" },
},
required: ["title", "url"],
},
},
{
name: "browser_get_history",
description: "Search browser history",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
maxResults: {
type: "number",
description: "Maximum number of results",
},
},
},
},
{
name: "browser_get_cookies",
description: "Get cookies for a domain",
inputSchema: {
type: "object",
properties: {
domain: { type: "string", description: "Domain to get cookies for" },
},
},
},
{
name: "browser_fill_form",
description: "Fill a form on the current page",
inputSchema: {
type: "object",
properties: {
formData: {
type: "object",
description: "Key-value pairs of form field names/IDs and values",
},
},
required: ["formData"],
},
},
{
name: "browser_click_element",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector of element to click",
},
},
required: ["selector"],
},
properties: {},
additionalProperties: false
}
},
];
}
// Handle MCP requests
// Handle MCP requests with enhanced automation tools
async function handleMCPRequest(message) {
const { id, method, params } = message;
@@ -205,44 +284,45 @@ async function handleMCPRequest(message) {
let result;
switch (method) {
// New automation tools
case "page_analyze":
result = await sendToContentScript('analyze', params);
break;
case "page_extract_content":
result = await sendToContentScript('extract_content', params);
break;
case "element_click":
result = await sendToContentScript('element_click', params);
break;
case "element_fill":
result = await sendToContentScript('element_fill', params);
break;
case "page_navigate":
result = await navigateToUrl(params.url, params.wait_for, params.timeout);
break;
case "page_wait_for":
result = await sendToContentScript('wait_for', params);
break;
// Essential legacy tools for compatibility
case "browser_navigate":
result = await navigateToUrl(params.url);
break;
case "browser_get_tabs":
result = await getTabs();
break;
case "browser_create_tab":
result = await createTab(params);
break;
case "browser_close_tab":
result = await closeTab(params.tabId);
break;
case "browser_execute_script":
result = await executeScript(params.code);
break;
case "browser_get_page_content":
result = await getPageContent(params.selector);
// Element state tools
case "element_get_state":
result = await sendToContentScript('get_element_state', params);
break;
case "browser_take_screenshot":
result = await takeScreenshot(params.format);
// Analytics tools
case "get_analytics":
result = await sendToContentScript('get_analytics', {});
break;
case "browser_get_bookmarks":
result = await getBookmarks(params.query);
break;
case "browser_add_bookmark":
result = await addBookmark(params);
break;
case "browser_get_history":
result = await getHistory(params);
break;
case "browser_get_cookies":
result = await getCookies(params.domain);
break;
case "browser_fill_form":
result = await fillForm(params.formData);
break;
case "browser_click_element":
result = await clickElement(params.selector);
case "clear_analytics":
result = await sendToContentScript('clear_analytics', {});
break;
default:
throw new Error(`Unknown method: ${method}`);
@@ -269,38 +349,76 @@ async function handleMCPRequest(message) {
}
}
// Browser function implementations
async function navigateToUrl(url) {
// Enhanced content script communication
async function sendToContentScript(action, data) {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
await chrome.tabs.update(activeTab.id, { url });
return { success: true, tabId: activeTab.id };
}
async function getTabs() {
const tabs = await chrome.tabs.query({});
return tabs.map((tab) => ({
id: tab.id,
title: tab.title,
url: tab.url,
active: tab.active,
windowId: tab.windowId,
}));
}
async function createTab(params) {
const tab = await chrome.tabs.create({
url: params.url || "about:blank",
active: params.active !== false,
if (!activeTab) {
throw new Error('No active tab found');
}
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(activeTab.id, { action, data }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Unknown error'));
}
});
});
return { id: tab.id, windowId: tab.windowId };
}
async function closeTab(tabId) {
await chrome.tabs.remove(tabId);
return { success: true };
async function navigateToUrl(url, waitFor, timeout = 10000) {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
await chrome.tabs.update(activeTab.id, { url });
// If waitFor is specified, wait for the element to appear
if (waitFor) {
try {
await waitForElement(activeTab.id, waitFor, timeout);
} catch (error) {
return { success: true, tabId: activeTab.id, warning: `Navigation completed but wait condition failed: ${error.message}` };
}
}
return { success: true, tabId: activeTab.id, url: url };
}
async function waitForElement(tabId, selector, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const result = await chrome.tabs.sendMessage(tabId, {
action: 'wait_for',
data: {
condition_type: 'element_visible',
selector: selector,
timeout: 1000
}
});
if (result.success) {
return true;
}
} catch (error) {
// Content script might not be ready yet, continue waiting
}
// Wait 500ms before next check
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new Error(`Timeout waiting for element: ${selector}`);
}
async function executeScript(code) {
@@ -308,105 +426,55 @@ async function executeScript(code) {
active: true,
currentWindow: true,
});
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: new Function(code),
});
return results[0].result;
}
async function getPageContent(selector) {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: (sel) => {
if (sel) {
const element = document.querySelector(sel);
return element ? element.innerText : null;
}
return document.body.innerText;
},
args: [selector],
});
return results[0].result;
}
async function takeScreenshot(format = "png") {
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format });
return { dataUrl, format };
}
async function getBookmarks(query) {
if (query) {
return await chrome.bookmarks.search(query);
}
return await chrome.bookmarks.getTree();
}
async function addBookmark(params) {
const bookmark = await chrome.bookmarks.create({
title: params.title,
url: params.url,
});
return bookmark;
}
async function getHistory(params) {
const historyItems = await chrome.history.search({
text: params.query || "",
maxResults: params.maxResults || 100,
});
return historyItems;
}
async function getCookies(domain) {
const cookies = await chrome.cookies.getAll({ domain });
return cookies;
}
async function fillForm(formData) {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: (data) => {
for (const [key, value] of Object.entries(data)) {
const element = document.querySelector(`[name="${key}"], #${key}`);
if (element) {
element.value = value;
element.dispatchEvent(new Event("input", { bubbles: true }));
try {
// Use chrome.scripting.executeScript with a function instead of eval
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: (codeToExecute) => {
// Execute code safely without eval
try {
// Create a script element to bypass CSP
const script = document.createElement('script');
script.textContent = codeToExecute;
document.head.appendChild(script);
document.head.removeChild(script);
return { success: true, executed: true };
} catch (error) {
// Fallback: try to execute common operations directly
if (codeToExecute.includes('window.scrollTo')) {
const match = codeToExecute.match(/window\.scrollTo\((\d+),\s*(\d+|[^)]+)\)/);
if (match) {
const x = parseInt(match[1]);
const y = match[2].includes('document.body.scrollHeight') ?
document.body.scrollHeight / 2 : parseInt(match[2]);
window.scrollTo(x, y);
return { success: true, scrolled: true, x, y };
}
}
if (codeToExecute.includes('document.querySelector')) {
// Handle simple querySelector operations
const match = codeToExecute.match(/document\.querySelector\(['"]([^'"]+)['"]\)/);
if (match) {
const element = document.querySelector(match[1]);
return { success: true, element: element ? 'found' : 'not found', selector: match[1] };
}
}
throw error;
}
}
return { success: true, filled: Object.keys(data).length };
},
args: [formData],
});
return results[0].result;
}
async function clickElement(selector) {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: (sel) => {
const element = document.querySelector(sel);
if (element) {
element.click();
return { success: true, clicked: sel };
}
throw new Error(`Element not found: ${sel}`);
},
args: [selector],
});
return results[0].result;
},
args: [code],
});
return results[0].result;
} catch (error) {
return {
success: false,
error: error.message,
note: "CSP restrictions prevent arbitrary JavaScript execution. Try using specific automation tools instead."
};
}
}
// Initialize connection when extension loads
@@ -435,4 +503,4 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
sendResponse({ success: true });
}
return true; // Keep the message channel open
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,15 @@
{
"manifest_version": 3,
"name": "OpenDia Browser Bridge",
"version": "1.0.0",
"description": "Exposes browser functions through Model Context Protocol",
"name": "OpenDia Enhanced Browser Automation",
"version": "2.0.0",
"description": "Enhanced browser automation through Model Context Protocol with pattern database and semantic analysis",
"permissions": [
"tabs",
"activeTab",
"storage",
"bookmarks",
"history",
"downloads",
"cookies",
"webNavigation",
"scripting",
"nativeMessaging",
"contextMenus",
"notifications",
"alarms",
"clipboardRead",
"clipboardWrite"
"webNavigation",
"notifications"
],
"host_permissions": [
"<all_urls>"
@@ -28,7 +19,7 @@
},
"action": {
"default_popup": "popup.html",
"default_title": "OpenDia Browser Bridge"
"default_title": "OpenDia Enhanced Browser Automation"
},
"content_scripts": [
{
@@ -41,4 +32,4 @@
"ids": ["*"],
"matches": ["http://localhost/*"]
}
}
}

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OpenDia Browser Bridge</title>
<style>
:root {
@@ -125,6 +126,167 @@
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;
}
.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;
}
.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);
@@ -170,7 +332,11 @@
</div>
<div class="info-row">
<span class="info-label">Available Tools</span>
<span class="info-value" id="toolCount">0</span>
<span class="info-value" id="toolCount">8</span>
</div>
<div class="info-row">
<span class="info-label">Current Page</span>
<span class="info-value" id="currentPage">Loading...</span>
</div>
</div>
@@ -179,6 +345,40 @@
<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>
@@ -188,4 +388,4 @@
<script src="popup.js"></script>
</body>
</html>
</html>

View File

@@ -1,14 +1,21 @@
// Popup script for status display
// Enhanced Popup with Testing Interface
let logContainer = document.getElementById("log");
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 initial tool count
const tools = 13; // Number of tools we expose
// Get initial tool count and page info
const tools = 8; // 6 core automation + 2 essential legacy tools
toolCount.textContent = tools;
// Check connection status
// Check connection status and get page info
function checkStatus() {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ action: "getStatus" }, (response) => {
@@ -19,6 +26,14 @@ function checkStatus() {
updateStatus(response?.connected || false);
}
});
// Get current page info
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
if (tabs[0]) {
const url = new URL(tabs[0].url);
currentPage.textContent = url.hostname;
}
});
} else {
updateStatus(false);
addLog("Extension context invalid", "error");
@@ -67,6 +82,282 @@ document.getElementById("testBtn").addEventListener("click", () => {
}
});
// 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");
@@ -109,4 +400,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
} else if (message.type === "log") {
addLog(message.message, message.type || "info");
}
});
});