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
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 47s
This commit is contained in:
@@ -18,6 +18,7 @@ async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 3
|
|||||||
interface AnalyzePayload {
|
interface AnalyzePayload {
|
||||||
text: string;
|
text: string;
|
||||||
action: string;
|
action: string;
|
||||||
|
style?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LexAIConfig {
|
interface LexAIConfig {
|
||||||
@@ -35,7 +36,9 @@ interface LexAIResponse {
|
|||||||
|
|
||||||
// ─── System prompts ───────────────────────────────────────────────────────────
|
// ─── 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> = {
|
const prompts: Record<string, string> = {
|
||||||
grammar:
|
grammar:
|
||||||
'You are a professional grammar editor. Fix all grammar, spelling, and punctuation errors in the provided text. ' +
|
'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. ' +
|
'Break down complex terms, jargon, or concepts so anyone can understand. ' +
|
||||||
'Be concise but clear. Return only the explanation, no extra commentary.',
|
'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 ───────────────────────────────────────────────────────
|
// ─── Encryption helpers ───────────────────────────────────────────────────────
|
||||||
@@ -89,7 +96,7 @@ async function callOpenAI(payload: AnalyzePayload, config: LexAIConfig): Promise
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
|
||||||
{ role: 'user', content: payload.text },
|
{ role: 'user', content: payload.text },
|
||||||
],
|
],
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
@@ -127,7 +134,7 @@ async function callAnthropic(payload: AnalyzePayload, config: LexAIConfig): Prom
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
system: getSystemPrompt(payload.action),
|
system: getSystemPrompt(payload.action, payload.style),
|
||||||
messages: [{ role: 'user', content: payload.text }],
|
messages: [{ role: 'user', content: payload.text }],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -161,7 +168,7 @@ async function callGroq(payload: AnalyzePayload, config: LexAIConfig): Promise<L
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
|
||||||
{ role: 'user', content: payload.text },
|
{ role: 'user', content: payload.text },
|
||||||
],
|
],
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
@@ -200,7 +207,7 @@ async function callOpenRouter(payload: AnalyzePayload, config: LexAIConfig): Pro
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: getSystemPrompt(payload.action) },
|
{ role: 'system', content: getSystemPrompt(payload.action, payload.style) },
|
||||||
{ role: 'user', content: payload.text },
|
{ role: 'user', content: payload.text },
|
||||||
],
|
],
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
@@ -388,48 +395,61 @@ export default defineBackground(() => {
|
|||||||
console.log('LexAI background service worker started');
|
console.log('LexAI background service worker started');
|
||||||
|
|
||||||
// ─── Context menus ───────────────────────────────────────────────────────
|
// ─── 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.runtime.onInstalled.addListener(() => {
|
||||||
chrome.contextMenus.create({
|
chrome.contextMenus.removeAll(() => {
|
||||||
id: 'lexai-grammar',
|
CONTEXT_ACTIONS.forEach(action => {
|
||||||
title: '⚡ LexAI: Fix Grammar',
|
chrome.contextMenus.create({
|
||||||
contexts: ['selection'],
|
id: `lexai-${action}`,
|
||||||
});
|
title: `⚡ LexAI: ${CONTEXT_ACTION_LABELS[action]}`,
|
||||||
chrome.contextMenus.create({
|
contexts: ['selection'],
|
||||||
id: 'lexai-rephrase',
|
});
|
||||||
title: '⚡ LexAI: Rephrase',
|
CONTEXT_STYLES.forEach(style => {
|
||||||
contexts: ['selection'],
|
chrome.contextMenus.create({
|
||||||
});
|
id: `lexai-${action}-${style.toLowerCase()}`,
|
||||||
chrome.contextMenus.create({
|
parentId: `lexai-${action}`,
|
||||||
id: 'lexai-shorten',
|
title: style,
|
||||||
title: '⚡ LexAI: Shorten',
|
contexts: ['selection'],
|
||||||
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.onClicked.addListener((info, tab) => {
|
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
if (!info.selectionText || !tab?.id) return;
|
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, {
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
type: 'lexai-context-menu',
|
type: 'lexai-context-menu',
|
||||||
action,
|
action,
|
||||||
text: info.selectionText,
|
text: info.selectionText,
|
||||||
|
style,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Message handler ─────────────────────────────────────────────────────
|
// ─── Message handler ─────────────────────────────────────────────────────
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
if (message.type === 'ANALYZE_TEXT') {
|
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)
|
.then(sendResponse)
|
||||||
.catch((err) => sendResponse({ error: String(err) }));
|
.catch((err) => sendResponse({ error: String(err) }));
|
||||||
return true; // Keep channel open for async response
|
return true; // Keep channel open for async response
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ export default defineContentScript({
|
|||||||
// Lock flag — prevents mouseup from resetting stored selection while modal is open
|
// Lock flag — prevents mouseup from resetting stored selection while modal is open
|
||||||
let selectionLocked = false;
|
let selectionLocked = false;
|
||||||
|
|
||||||
|
// Writing style — persisted in chrome.storage.local
|
||||||
|
let currentStyle = 'Default';
|
||||||
|
chrome.storage.local.get(['writingStyle'], (res) => {
|
||||||
|
currentStyle = (res.writingStyle as string) || 'Default';
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Selection capture (called immediately on mouseup) ────────────────────
|
// ─── Selection capture (called immediately on mouseup) ────────────────────
|
||||||
|
|
||||||
function captureSelectionNow(): boolean {
|
function captureSelectionNow(): boolean {
|
||||||
@@ -615,7 +621,7 @@ export default defineContentScript({
|
|||||||
try {
|
try {
|
||||||
const response = await safeSendMessage({
|
const response = await safeSendMessage({
|
||||||
type: 'ANALYZE_TEXT',
|
type: 'ANALYZE_TEXT',
|
||||||
payload: { text: textToProcess, action },
|
payload: { text: textToProcess, action, style: currentStyle },
|
||||||
}) as { error?: string; result?: string } | null;
|
}) as { error?: string; result?: string } | null;
|
||||||
|
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
@@ -623,7 +629,7 @@ export default defineContentScript({
|
|||||||
if (response === null) return; // safeSendMessage already handled the error
|
if (response === null) return; // safeSendMessage already handled the error
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
showModal(`❌ ${response.error}`, null, snapStart, snapEnd, snapElement, snapRange);
|
showModal(`❌ ${response.error}`, null, snapStart, snapEnd, snapElement, snapRange, action, textToProcess);
|
||||||
} else {
|
} else {
|
||||||
// Tone analysis is display-only — no Replace button (pass null for originalText)
|
// Tone analysis is display-only — no Replace button (pass null for originalText)
|
||||||
const isExplainAction = action === 'explain';
|
const isExplainAction = action === 'explain';
|
||||||
@@ -631,6 +637,7 @@ export default defineContentScript({
|
|||||||
response?.result ?? '(no result)',
|
response?.result ?? '(no result)',
|
||||||
isExplainAction ? null : textToProcess,
|
isExplainAction ? null : textToProcess,
|
||||||
snapStart, snapEnd, snapElement, snapRange,
|
snapStart, snapEnd, snapElement, snapRange,
|
||||||
|
action, textToProcess,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -639,7 +646,7 @@ export default defineContentScript({
|
|||||||
String(err).includes('message channel closed')) {
|
String(err).includes('message channel closed')) {
|
||||||
showErrorToast('LexAI was updated — please refresh this page.');
|
showErrorToast('LexAI was updated — please refresh this page.');
|
||||||
} else {
|
} else {
|
||||||
showModal(`❌ Error: ${String(err)}`, null, snapStart, snapEnd, snapElement, snapRange);
|
showModal(`❌ Error: ${String(err)}`, null, snapStart, snapEnd, snapElement, snapRange, action, textToProcess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,6 +662,8 @@ export default defineContentScript({
|
|||||||
snapEnd: number,
|
snapEnd: number,
|
||||||
snapElement: HTMLTextAreaElement | HTMLInputElement | null,
|
snapElement: HTMLTextAreaElement | HTMLInputElement | null,
|
||||||
snapRange: Range | null,
|
snapRange: Range | null,
|
||||||
|
action?: string,
|
||||||
|
textForRegenerate?: string,
|
||||||
) {
|
) {
|
||||||
if (modal) modal.remove();
|
if (modal) modal.remove();
|
||||||
|
|
||||||
@@ -741,6 +750,102 @@ export default defineContentScript({
|
|||||||
});
|
});
|
||||||
modal.appendChild(body);
|
modal.appendChild(body);
|
||||||
|
|
||||||
|
// ─── Style selector row ───────────────────────────────────────────────
|
||||||
|
const STYLES = ['Default', 'Formal', 'Casual', 'Academic', 'Creative', 'Concise'];
|
||||||
|
|
||||||
|
const styleRow = document.createElement('div');
|
||||||
|
Object.assign(styleRow.style, {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
marginBottom:'12px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleLabel = document.createElement('span');
|
||||||
|
styleLabel.textContent = 'Style:';
|
||||||
|
Object.assign(styleLabel.style, {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#a6adc8',
|
||||||
|
flexShrink: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleSelect = document.createElement('select');
|
||||||
|
Object.assign(styleSelect.style, {
|
||||||
|
background: 'rgba(49,50,68,0.9)',
|
||||||
|
color: '#cdd6f4',
|
||||||
|
border: '1px solid rgba(205,214,244,0.2)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
flex: '1',
|
||||||
|
outline: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
STYLES.forEach(s => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
styleSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load saved style and pre-select
|
||||||
|
chrome.storage.local.get(['writingStyle'], (res) => {
|
||||||
|
const saved = (res.writingStyle as string) || 'Default';
|
||||||
|
styleSelect.value = saved;
|
||||||
|
currentStyle = saved;
|
||||||
|
});
|
||||||
|
|
||||||
|
const regenBtn = document.createElement('button');
|
||||||
|
regenBtn.textContent = '↺ Regenerate';
|
||||||
|
regenBtn.setAttribute('data-lexai', 'true');
|
||||||
|
Object.assign(regenBtn.style, {
|
||||||
|
background: 'rgba(137,180,250,0.15)',
|
||||||
|
color: '#89b4fa',
|
||||||
|
border: '1px solid rgba(137,180,250,0.3)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '5px 10px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
flexShrink: '0',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doRegenerate() {
|
||||||
|
if (!action || !textForRegenerate) return;
|
||||||
|
const chosenStyle = styleSelect.value;
|
||||||
|
// Save style
|
||||||
|
currentStyle = chosenStyle;
|
||||||
|
await chrome.storage.local.set({ writingStyle: chosenStyle });
|
||||||
|
// Show spinner
|
||||||
|
body.innerHTML = '<span style="font-size:18px;animation:lexai-spin 0.8s linear infinite;display:inline-block">⟳</span> Regenerating…';
|
||||||
|
regenBtn.disabled = true;
|
||||||
|
styleSelect.disabled = true;
|
||||||
|
|
||||||
|
const response = await safeSendMessage({
|
||||||
|
type: 'ANALYZE_TEXT',
|
||||||
|
payload: { text: textForRegenerate, action, style: chosenStyle },
|
||||||
|
}) as { error?: string; result?: string } | null;
|
||||||
|
|
||||||
|
regenBtn.disabled = false;
|
||||||
|
styleSelect.disabled = false;
|
||||||
|
|
||||||
|
if (!response || response.error) {
|
||||||
|
body.textContent = `❌ ${response?.error ?? 'Unknown error'}`;
|
||||||
|
} else {
|
||||||
|
body.textContent = response.result ?? '(no result)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
styleSelect.addEventListener('change', () => doRegenerate());
|
||||||
|
regenBtn.addEventListener('click', () => doRegenerate());
|
||||||
|
|
||||||
|
styleRow.appendChild(styleLabel);
|
||||||
|
styleRow.appendChild(styleSelect);
|
||||||
|
styleRow.appendChild(regenBtn);
|
||||||
|
modal.appendChild(styleRow);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
const btnRow = document.createElement('div');
|
const btnRow = document.createElement('div');
|
||||||
Object.assign(btnRow.style, { display: 'flex', gap: '8px' });
|
Object.assign(btnRow.style, { display: 'flex', gap: '8px' });
|
||||||
@@ -949,6 +1054,7 @@ export default defineContentScript({
|
|||||||
chrome.runtime.onMessage.addListener((message) => {
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
if (message.type === 'lexai-context-menu') {
|
if (message.type === 'lexai-context-menu') {
|
||||||
selectedText = message.text as string;
|
selectedText = message.text as string;
|
||||||
|
currentStyle = (message.style as string) || 'Default';
|
||||||
// For context menu, we have no DOM selection positions — zero them out
|
// For context menu, we have no DOM selection positions — zero them out
|
||||||
storedStart = -1;
|
storedStart = -1;
|
||||||
storedEnd = -1;
|
storedEnd = -1;
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ async function safeSendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const WRITING_STYLES = ['Default', 'Formal', 'Casual', 'Academic', 'Creative', 'Concise'];
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Action = 'fix' | 'rephrase' | 'shorten' | 'expand';
|
type Action = 'fix' | 'rephrase' | 'shorten' | 'expand';
|
||||||
@@ -97,6 +101,29 @@ const S = {
|
|||||||
boxSizing: 'border-box' as const,
|
boxSizing: 'border-box' as const,
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
},
|
},
|
||||||
|
styleRow: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '2px',
|
||||||
|
},
|
||||||
|
styleLabel: {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#a6adc8',
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
styleSelect: {
|
||||||
|
background: 'rgba(49,50,68,0.95)',
|
||||||
|
border: '1px solid rgba(205,214,244,0.2)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
color: '#cdd6f4',
|
||||||
|
fontSize: '12px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
flex: 1,
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
actionsRow: {
|
actionsRow: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap' as const,
|
flexWrap: 'wrap' as const,
|
||||||
@@ -190,16 +217,20 @@ function Popup() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [writingStyle, setWritingStyle] = useState('Default');
|
||||||
const sessionRestored = useRef(false);
|
const sessionRestored = useRef(false);
|
||||||
|
|
||||||
// Load config + restore session input
|
// Load config + restore session input + load writing style
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
safeStorageGet(chrome.storage.local, ['provider', 'apiKey', 'apiKeyEnc', 'model'], (result) => {
|
safeStorageGet(chrome.storage.local, ['provider', 'apiKey', 'apiKeyEnc', 'model', 'writingStyle'], (result) => {
|
||||||
if (result.apiKey || result.apiKeyEnc) {
|
if (result.apiKey || result.apiKeyEnc) {
|
||||||
setConfigured(true);
|
setConfigured(true);
|
||||||
setProvider(result.provider || 'openai');
|
setProvider(result.provider || 'openai');
|
||||||
setModel(result.model || '');
|
setModel(result.model || '');
|
||||||
}
|
}
|
||||||
|
if (result.writingStyle) {
|
||||||
|
setWritingStyle(result.writingStyle);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sessionRestored.current) {
|
if (!sessionRestored.current) {
|
||||||
@@ -219,6 +250,13 @@ function Popup() {
|
|||||||
safeStorageSet(chrome.storage.session, { lexai_popup_input: val });
|
safeStorageSet(chrome.storage.session, { lexai_popup_input: val });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save writing style on change
|
||||||
|
const handleStyleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setWritingStyle(val);
|
||||||
|
safeStorageSet(chrome.storage.local, { writingStyle: val });
|
||||||
|
};
|
||||||
|
|
||||||
const openSettings = () => {
|
const openSettings = () => {
|
||||||
try {
|
try {
|
||||||
if (typeof chrome !== 'undefined' && chrome.runtime?.id) {
|
if (typeof chrome !== 'undefined' && chrome.runtime?.id) {
|
||||||
@@ -239,8 +277,7 @@ function Popup() {
|
|||||||
|
|
||||||
const response = await safeSendMessage({
|
const response = await safeSendMessage({
|
||||||
type: 'ANALYZE_TEXT',
|
type: 'ANALYZE_TEXT',
|
||||||
action,
|
payload: { text, action, style: writingStyle },
|
||||||
text,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -331,6 +368,21 @@ function Popup() {
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Style selector */}
|
||||||
|
<div style={S.styleRow}>
|
||||||
|
<span style={S.styleLabel}>Style:</span>
|
||||||
|
<select
|
||||||
|
style={S.styleSelect}
|
||||||
|
value={writingStyle}
|
||||||
|
onChange={handleStyleChange}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
|
{WRITING_STYLES.map(s => (
|
||||||
|
<option key={s} value={s}>{s}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div style={S.actionsRow}>
|
<div style={S.actionsRow}>
|
||||||
{actions.map(({ label, id }) => (
|
{actions.map(({ label, id }) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user