mirror of
https://github.com/aaronjmars/opendia.git
synced 2025-12-29 16:16:00 +00:00
better tools
This commit is contained in:
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal 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.
|
||||
65
README.md
65
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user