feat: initial LexAI Chrome extension scaffold
- WXT + React 18 + TypeScript setup - Background service worker (LLM API proxy) - Content script (floating toolbar + text selection) - Options page (provider/model/API key config) - Popup UI - Gitea Actions workflows (CI, preview, release) - Vitest unit tests + Playwright E2E setup - Supports: OpenAI, Anthropic, Groq, OpenRouter
This commit is contained in:
143
entrypoints/content.ts
Normal file
143
entrypoints/content.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
main() {
|
||||
console.log('LexAI content script loaded');
|
||||
|
||||
let toolbar: HTMLElement | null = null;
|
||||
let selectedText = '';
|
||||
|
||||
// Listen for text selection
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 10) {
|
||||
selectedText = selection.toString().trim();
|
||||
showToolbar(e.clientX, e.clientY);
|
||||
} else {
|
||||
hideToolbar();
|
||||
}
|
||||
});
|
||||
|
||||
function showToolbar(x: number, y: number) {
|
||||
hideToolbar();
|
||||
|
||||
toolbar = document.createElement('div');
|
||||
toolbar.id = 'lexai-toolbar';
|
||||
toolbar.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${y - 50}px;
|
||||
left: ${x}px;
|
||||
z-index: 999999;
|
||||
background: #1e1e2e;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
`;
|
||||
|
||||
const actions = [
|
||||
{ label: '✓ Fix', action: 'grammar' },
|
||||
{ label: '↺ Rephrase', action: 'rephrase' },
|
||||
{ label: '↓ Shorten', action: 'shorten' },
|
||||
{ label: '↑ Expand', action: 'expand' },
|
||||
];
|
||||
|
||||
actions.forEach(({ label, action }) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = label;
|
||||
btn.style.cssText = `
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
`;
|
||||
btn.addEventListener('mouseenter', () => btn.style.background = '#45475a');
|
||||
btn.addEventListener('mouseleave', () => btn.style.background = '#313244');
|
||||
btn.addEventListener('click', () => runAction(action));
|
||||
toolbar!.appendChild(btn);
|
||||
});
|
||||
|
||||
document.body.appendChild(toolbar);
|
||||
}
|
||||
|
||||
function hideToolbar() {
|
||||
if (toolbar) {
|
||||
toolbar.remove();
|
||||
toolbar = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAction(action: string) {
|
||||
if (!selectedText) return;
|
||||
|
||||
// Show loading state
|
||||
if (toolbar) {
|
||||
toolbar.innerHTML = '<span style="color:#cdd6f4;font-size:12px;padding:4px 8px;">⏳ LexAI thinking...</span>';
|
||||
}
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'ANALYZE_TEXT',
|
||||
payload: { text: selectedText, action },
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
showResult(`❌ ${response.error}`);
|
||||
} else {
|
||||
showResult(response.result, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showResult(text: string, canReplace = false) {
|
||||
hideToolbar();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 999999;
|
||||
background: #1e1e2e;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: #cdd6f4;
|
||||
`;
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="font-size:11px;color:#a6adc8;margin-bottom:8px;">LexAI Suggestion</div>
|
||||
<div style="font-size:14px;line-height:1.6;margin-bottom:12px;">${text}</div>
|
||||
${canReplace ? '<button id="lexai-replace" style="background:#89b4fa;color:#1e1e2e;border:none;border-radius:6px;padding:6px 16px;font-size:13px;cursor:pointer;margin-right:8px;">Replace</button>' : ''}
|
||||
<button id="lexai-close" style="background:#313244;color:#cdd6f4;border:none;border-radius:6px;padding:6px 16px;font-size:13px;cursor:pointer;">Close</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById('lexai-close')?.addEventListener('click', () => modal.remove());
|
||||
document.getElementById('lexai-replace')?.addEventListener('click', () => {
|
||||
replaceSelectedText(text);
|
||||
modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function replaceSelectedText(newText: string) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(newText));
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user