feat: fix WXT setup — install @wxt-dev/module-react, fix sandbox imports, clean build (closes #1)
This commit is contained in:
@@ -1,105 +1,236 @@
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { defineBackground } from 'wxt/utils/define-background';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface AnalyzePayload {
|
||||
text: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface LexAIConfig {
|
||||
provider?: string;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
interface LexAIResponse {
|
||||
result?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ─── System prompts ───────────────────────────────────────────────────────────
|
||||
|
||||
function getSystemPrompt(action: string): string {
|
||||
const prompts: Record<string, string> = {
|
||||
grammar:
|
||||
'You are a professional grammar editor. Fix all grammar, spelling, and punctuation errors in the provided text. ' +
|
||||
'Preserve the original meaning and tone as closely as possible. ' +
|
||||
'Return ONLY the corrected text — no explanations, no preamble.',
|
||||
rephrase:
|
||||
'You are a skilled writing assistant. Rephrase the provided text to make it clearer, more engaging, and more professional. ' +
|
||||
'Keep the same meaning and approximate length. ' +
|
||||
'Return ONLY the rephrased text — no explanations.',
|
||||
shorten:
|
||||
'You are a concise editor. Shorten the provided text by at least 30% while preserving the core message. ' +
|
||||
'Remove filler words, redundant phrases, and unnecessary detail. ' +
|
||||
'Return ONLY the shortened text.',
|
||||
expand:
|
||||
'You are an experienced writer. Expand the provided text with more detail, context, and supporting points. ' +
|
||||
'Make it richer and more informative while staying on topic. ' +
|
||||
'Return ONLY the expanded text.',
|
||||
tone:
|
||||
'You are a writing coach. Analyze the tone of the provided text (e.g. formal, casual, aggressive, passive) ' +
|
||||
'and rewrite it to be professional and clear. ' +
|
||||
'Return ONLY the improved text.',
|
||||
};
|
||||
return prompts[action] ?? prompts.grammar;
|
||||
}
|
||||
|
||||
// ─── Provider implementations ─────────────────────────────────────────────────
|
||||
|
||||
async function callOpenAI(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
const model = config.model || 'gpt-4o-mini';
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
||||
{ role: 'user', content: payload.text },
|
||||
],
|
||||
max_tokens: 1024,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching OpenAI: ${String(err)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? `HTTP ${res.status}`;
|
||||
return { error: `OpenAI error: ${msg}` };
|
||||
}
|
||||
|
||||
const result = data?.choices?.[0]?.message?.content as string | undefined;
|
||||
if (!result) return { error: 'OpenAI returned an empty response.' };
|
||||
return { result: result.trim() };
|
||||
}
|
||||
|
||||
async function callAnthropic(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
const model = config.model || 'claude-3-5-haiku-20241022';
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey!,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
max_tokens: 1024,
|
||||
system: getSystemPrompt(payload.action),
|
||||
messages: [{ role: 'user', content: payload.text }],
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching Anthropic: ${String(err)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? `HTTP ${res.status}`;
|
||||
return { error: `Anthropic error: ${msg}` };
|
||||
}
|
||||
|
||||
const result = data?.content?.[0]?.text as string | undefined;
|
||||
if (!result) return { error: 'Anthropic returned an empty response.' };
|
||||
return { result: result.trim() };
|
||||
}
|
||||
|
||||
async function callGroq(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
const model = config.model || 'llama-3.3-70b-versatile';
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
||||
{ role: 'user', content: payload.text },
|
||||
],
|
||||
max_tokens: 1024,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching Groq: ${String(err)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? `HTTP ${res.status}`;
|
||||
return { error: `Groq error: ${msg}` };
|
||||
}
|
||||
|
||||
const result = data?.choices?.[0]?.message?.content as string | undefined;
|
||||
if (!result) return { error: 'Groq returned an empty response.' };
|
||||
return { result: result.trim() };
|
||||
}
|
||||
|
||||
async function callOpenRouter(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
const model = config.model || 'openai/gpt-4o-mini';
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
'HTTP-Referer': 'https://lexai.dev',
|
||||
'X-Title': 'LexAI',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
||||
{ role: 'user', content: payload.text },
|
||||
],
|
||||
max_tokens: 1024,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching OpenRouter: ${String(err)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? `HTTP ${res.status}`;
|
||||
return { error: `OpenRouter error: ${msg}` };
|
||||
}
|
||||
|
||||
const result = data?.choices?.[0]?.message?.content as string | undefined;
|
||||
if (!result) return { error: 'OpenRouter returned an empty response.' };
|
||||
return { result: result.trim() };
|
||||
}
|
||||
|
||||
// ─── Main handler ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse> {
|
||||
const config = await chrome.storage.local.get(['provider', 'apiKey', 'model']) as LexAIConfig;
|
||||
|
||||
if (!config.apiKey || config.apiKey.trim() === '') {
|
||||
return { error: 'No API key configured. Please open LexAI settings (click the extension icon).' };
|
||||
}
|
||||
|
||||
const provider = config.provider || 'openai';
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return callOpenAI(payload, config);
|
||||
case 'anthropic':
|
||||
return callAnthropic(payload, config);
|
||||
case 'groq':
|
||||
return callGroq(payload, config);
|
||||
case 'openrouter':
|
||||
return callOpenRouter(payload, config);
|
||||
default:
|
||||
return { error: `Unknown provider: "${provider}". Please check LexAI settings.` };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Background entry ─────────────────────────────────────────────────────────
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log('LexAI background service worker started');
|
||||
|
||||
// Handle messages from content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.type === 'ANALYZE_TEXT') {
|
||||
handleAnalyzeText(message.payload).then(sendResponse);
|
||||
handleAnalyzeText(message.payload as AnalyzePayload)
|
||||
.then(sendResponse)
|
||||
.catch((err) => sendResponse({ error: String(err) }));
|
||||
return true; // Keep channel open for async response
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function handleAnalyzeText(payload: { text: string; action: string }) {
|
||||
try {
|
||||
const config = await chrome.storage.local.get(['provider', 'apiKey', 'model']);
|
||||
|
||||
if (!config.apiKey) {
|
||||
return { error: 'No API key configured. Please open LexAI settings.' };
|
||||
}
|
||||
|
||||
// Route to correct provider
|
||||
switch (config.provider) {
|
||||
case 'openai':
|
||||
return await callOpenAI(payload, config);
|
||||
case 'anthropic':
|
||||
return await callAnthropic(payload, config);
|
||||
case 'groq':
|
||||
return await callGroq(payload, config);
|
||||
default:
|
||||
return { error: 'Unknown provider. Please check settings.' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
async function callOpenAI(payload: { text: string; action: string }, config: any) {
|
||||
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model || 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
||||
{ role: 'user', content: payload.text },
|
||||
],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { result: data.choices?.[0]?.message?.content };
|
||||
}
|
||||
|
||||
async function callAnthropic(payload: { text: string; action: string }, config: any) {
|
||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model || 'claude-3-5-haiku-20241022',
|
||||
max_tokens: 1024,
|
||||
system: getSystemPrompt(payload.action),
|
||||
messages: [{ role: 'user', content: payload.text }],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { result: data.content?.[0]?.text };
|
||||
}
|
||||
|
||||
async function callGroq(payload: { text: string; action: string }, config: any) {
|
||||
const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model || 'llama-3.3-70b-versatile',
|
||||
messages: [
|
||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
||||
{ role: 'user', content: payload.text },
|
||||
],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return { result: data.choices?.[0]?.message?.content };
|
||||
}
|
||||
|
||||
function getSystemPrompt(action: string): string {
|
||||
const prompts: Record<string, string> = {
|
||||
grammar: 'You are a grammar checker. Fix grammar, spelling, and punctuation errors in the text. Return only the corrected text without explanations.',
|
||||
rephrase: 'You are a writing assistant. Rephrase the given text to be clearer and more professional. Return only the rephrased text.',
|
||||
tone: 'You are a writing coach. Analyze the tone of the text and suggest improvements. Be concise.',
|
||||
shorten: 'You are an editor. Shorten the text while keeping the key message. Return only the shortened version.',
|
||||
expand: 'You are a writer. Expand the given text with more detail and context. Return only the expanded version.',
|
||||
};
|
||||
return prompts[action] || prompts.grammar;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user