Kraft102's picture
feat(api): implement dynamic api configuration via options page
038a1a3
/**
* WidgeTDC Browser Extension - Content Script
* Captures page content and enables AI assistance
*/
class WidgeTDCAssistant {
constructor() {
this.apiUrl = 'http://localhost:3001/api';
this.sidebar = null;
// Try to load from storage
try {
chrome.storage.sync.get({ widgetdc_api_url: 'http://localhost:3001' }, (items) => {
this.apiUrl = `${items.widgetdc_api_url}/api`;
});
} catch (e) {
// Fallback if storage access fails
}
this.init();
}
async init() {
// Listen for messages from background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
this.handleMessage(message, sendResponse);
return true; // Keep channel open for async response
});
// Add context menu handler
document.addEventListener('mouseup', this.handleTextSelection.bind(this));
}
/**
* Handle messages from background script
*/
async handleMessage(message, sendResponse) {
switch (message.action) {
case 'captureContent':
const content = this.capturePageContent();
await this.sendToBackend('/memory/ingest', content);
sendResponse({ success: true, content });
break;
case 'searchSimilar':
const results = await this.searchSimilar(message.query);
sendResponse({ success: true, results });
break;
case 'toggleSidebar':
this.toggleSidebar();
sendResponse({ success: true });
break;
case 'askQuestion':
const answer = await this.askQuestion(message.question);
sendResponse({ success: true, answer });
break;
default:
sendResponse({ success: false, error: 'Unknown action' });
}
}
/**
* Capture page content
*/
capturePageContent() {
// Extract main content (remove scripts, styles, etc.)
const clone = document.cloneNode(true);
const scripts = clone.querySelectorAll('script, style, noscript');
scripts.forEach(el => el.remove());
// Get text content
const text = clone.body.innerText.trim();
// Extract metadata
const metadata = {
author: this.getMetaContent('author'),
publishedDate: this.getMetaContent('article:published_time'),
description: this.getMetaContent('description'),
};
return {
url: window.location.href,
title: document.title,
text: text.substring(0, 10000), // Limit to 10k chars
metadata,
};
}
/**
* Get meta tag content
*/
getMetaContent(name) {
const meta = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
return meta ? meta.getAttribute('content') : undefined;
}
/**
* Handle text selection
*/
handleTextSelection(event) {
const selection = window.getSelection();
const selectedText = selection ? selection.toString().trim() : '';
if (selectedText && selectedText.length > 10) {
// Show floating action button
this.showFloatingButton(event.clientX, event.clientY, selectedText);
}
}
/**
* Show floating action button
*/
showFloatingButton(x, y, text) {
// Remove existing button
const existing = document.getElementById('widgetdc-floating-btn');
if (existing) existing.remove();
// Create button
const button = document.createElement('div');
button.id = 'widgetdc-floating-btn';
button.className = 'widgetdc-floating-button';
button.innerHTML = `
<button class="widgetdc-btn" data-action="save">💾 Save</button>
<button class="widgetdc-btn" data-action="search">🔍 Search Similar</button>
<button class="widgetdc-btn" data-action="ask">❓ Ask AI</button>
`;
button.style.position = 'fixed';
button.style.left = `${x}px`;
button.style.top = `${y + 10}px`;
button.style.zIndex = '10000';
// Add event listeners
button.querySelectorAll('.widgetdc-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const action = e.target.getAttribute('data-action');
await this.handleAction(action, text);
button.remove();
});
});
document.body.appendChild(button);
// Remove on click outside
setTimeout(() => {
document.addEventListener('click', () => button.remove(), { once: true });
}, 100);
}
/**
* Handle floating button action
*/
async handleAction(action, text) {
switch (action) {
case 'save':
await this.saveText(text);
this.showNotification('Text saved to WidgeTDC');
break;
case 'search':
const results = await this.searchSimilar(text);
this.showSidebarWithResults(results);
break;
case 'ask':
const answer = await this.askQuestion(text);
this.showSidebarWithAnswer(answer);
break;
}
}
/**
* Save text to backend
*/
async saveText(text) {
await this.sendToBackend('/memory/ingest', {
content: text,
source: 'browser_selection',
url: window.location.href,
timestamp: new Date().toISOString(),
});
}
/**
* Search for similar content
*/
async searchSimilar(query) {
const response = await this.sendToBackend('/search', { query, limit: 5 });
return response.results || [];
}
/**
* Ask AI a question
*/
async askQuestion(question) {
const response = await this.sendToBackend('/query', { question });
return response.answer || 'No answer available';
}
/**
* Send data to backend
*/
async sendToBackend(endpoint, data) {
try {
const response = await fetch(`${this.apiUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('WidgeTDC API error:', error);
return { error: String(error) };
}
}
/**
* Toggle sidebar
*/
toggleSidebar() {
if (this.sidebar) {
this.sidebar.remove();
this.sidebar = null;
} else {
this.createSidebar();
}
}
/**
* Create sidebar
*/
createSidebar() {
const sidebar = document.createElement('div');
sidebar.id = 'widgetdc-sidebar';
sidebar.className = 'widgetdc-sidebar';
sidebar.innerHTML = `
<div class="widgetdc-sidebar-header">
<h3>WidgeTDC Assistant</h3>
<button class="widgetdc-close">×</button>
</div>
<div class="widgetdc-sidebar-content">
<p>Loading...</p>
</div>
`;
sidebar.querySelector('.widgetdc-close')?.addEventListener('click', () => {
this.toggleSidebar();
});
document.body.appendChild(sidebar);
this.sidebar = sidebar;
}
/**
* Show sidebar with search results
*/
showSidebarWithResults(results) {
this.createSidebar();
const content = this.sidebar?.querySelector('.widgetdc-sidebar-content');
if (content) {
content.innerHTML = `
<h4>Similar Content</h4>
${results.map(r => `
<div class="widgetdc-result">
<h5>${r.title || 'Untitled'}</h5>
<p>${r.content?.substring(0, 200)}...</p>
<small>${r.source || 'Unknown source'}</small>
</div>
`).join('')}
`;
}
}
/**
* Show sidebar with AI answer
*/
showSidebarWithAnswer(answer) {
this.createSidebar();
const content = this.sidebar?.querySelector('.widgetdc-sidebar-content');
if (content) {
content.innerHTML = `
<h4>AI Answer</h4>
<div class="widgetdc-answer">${answer}</div>
`;
}
}
/**
* Show notification
*/
showNotification(message) {
const notification = document.createElement('div');
notification.className = 'widgetdc-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
}
// Initialize assistant
new WidgeTDCAssistant();