feat: LEXAI-15 writing style selector in modal, popup, and right-click menu
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 47s

This commit is contained in:
Forge
2026-03-06 15:43:10 +08:00
parent 2bc73d5fc9
commit b0f79566e6
3 changed files with 217 additions and 39 deletions

View File

@@ -18,6 +18,7 @@ async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 3
interface AnalyzePayload {
text: string;
action: string;
style?: string;
}
interface LexAIConfig {
@@ -35,7 +36,9 @@ interface LexAIResponse {
// ─── System prompts ───────────────────────────────────────────────────────────
function getSystemPrompt(action: string): string {
function getSystemPrompt(action: string, style?: string): string {
// Normalize 'fix' (used by context menu) to 'grammar'
const normalizedAction = action === 'fix' ? 'grammar' : action;
const prompts: Record<string, string> = {
grammar:
'You are a professional grammar editor. Fix all grammar, spelling, and punctuation errors in the provided text. ' +
@@ -58,7 +61,11 @@ function getSystemPrompt(action: string): string {
'Break down complex terms, jargon, or concepts so anyone can understand. ' +
'Be concise but clear. Return only the explanation, no extra commentary.',
};
return prompts[action] ?? prompts.grammar;
const base = prompts[normalizedAction] ?? prompts.grammar;
const styleModifier = style && style !== 'Default'
? ` Write in a ${style.toLowerCase()} style.`
: '';
return base + styleModifier;
}
// ─── Encryption helpers ───────────────────────────────────────────────────────
@@ -89,7 +96,7 @@ async function callOpenAI(payload: AnalyzePayload, config: LexAIConfig): Promise
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: getSystemPrompt(payload.action) },
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
{ role: 'user', content: payload.text },
],
max_tokens: 1024,
@@ -127,7 +134,7 @@ async function callAnthropic(payload: AnalyzePayload, config: LexAIConfig): Prom
body: JSON.stringify({
model,
max_tokens: 1024,
system: getSystemPrompt(payload.action),
system: getSystemPrompt(payload.action, payload.style),
messages: [{ role: 'user', content: payload.text }],
}),
});
@@ -161,7 +168,7 @@ async function callGroq(payload: AnalyzePayload, config: LexAIConfig): Promise<L
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: getSystemPrompt(payload.action) },
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
{ role: 'user', content: payload.text },
],
max_tokens: 1024,
@@ -200,7 +207,7 @@ async function callOpenRouter(payload: AnalyzePayload, config: LexAIConfig): Pro
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: getSystemPrompt(payload.action) },
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
{ role: 'user', content: payload.text },
],
max_tokens: 1024,
@@ -388,48 +395,61 @@ export default defineBackground(() => {
console.log('LexAI background service worker started');
// ─── Context menus ───────────────────────────────────────────────────────
const CONTEXT_ACTIONS = ['fix', 'rephrase', 'shorten', 'expand', 'explain'] as const;
const CONTEXT_ACTION_LABELS: Record<string, string> = {
fix: 'Fix Grammar',
rephrase: 'Rephrase',
shorten: 'Shorten',
expand: 'Expand',
explain: 'Explain',
};
const CONTEXT_STYLES = ['Formal', 'Casual', 'Academic', 'Creative', 'Concise'];
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'lexai-grammar',
title: '⚡ LexAI: Fix Grammar',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'lexai-rephrase',
title: '⚡ LexAI: Rephrase',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'lexai-shorten',
title: '⚡ LexAI: Shorten',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'lexai-expand',
title: '⚡ LexAI: Expand',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'lexai-explain',
title: '⚡ LexAI: Explain',
contexts: ['selection'],
chrome.contextMenus.removeAll(() => {
CONTEXT_ACTIONS.forEach(action => {
chrome.contextMenus.create({
id: `lexai-${action}`,
title: `⚡ LexAI: ${CONTEXT_ACTION_LABELS[action]}`,
contexts: ['selection'],
});
CONTEXT_STYLES.forEach(style => {
chrome.contextMenus.create({
id: `lexai-${action}-${style.toLowerCase()}`,
parentId: `lexai-${action}`,
title: style,
contexts: ['selection'],
});
});
});
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (!info.selectionText || !tab?.id) return;
const action = info.menuItemId.toString().replace('lexai-', '');
// Parse action and style from menuItemId e.g. "lexai-fix-formal"
const parts = info.menuItemId.toString().replace('lexai-', '').split('-');
const action = parts[0];
const style = parts[1] ? parts[1].charAt(0).toUpperCase() + parts[1].slice(1) : 'Default';
chrome.tabs.sendMessage(tab.id, {
type: 'lexai-context-menu',
action,
text: info.selectionText,
style,
});
});
// ─── Message handler ─────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'ANALYZE_TEXT') {
handleAnalyzeText(message.payload as AnalyzePayload)
// Support both { payload: { text, action, style } } (content.ts) and
// { text, action, style } (popup) formats
const payload: AnalyzePayload = message.payload ?? {
text: message.text as string,
action: message.action as string,
style: message.style as string | undefined,
};
handleAnalyzeText(payload)
.then(sendResponse)
.catch((err) => sendResponse({ error: String(err) }));
return true; // Keep channel open for async response