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

@@ -19,6 +19,12 @@ export default defineContentScript({
// Lock flag — prevents mouseup from resetting stored selection while modal is open
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) ────────────────────
function captureSelectionNow(): boolean {
@@ -615,7 +621,7 @@ export default defineContentScript({
try {
const response = await safeSendMessage({
type: 'ANALYZE_TEXT',
payload: { text: textToProcess, action },
payload: { text: textToProcess, action, style: currentStyle },
}) as { error?: string; result?: string } | null;
hideToolbar();
@@ -623,7 +629,7 @@ export default defineContentScript({
if (response === null) return; // safeSendMessage already handled the error
if (response?.error) {
showModal(`${response.error}`, null, snapStart, snapEnd, snapElement, snapRange);
showModal(`${response.error}`, null, snapStart, snapEnd, snapElement, snapRange, action, textToProcess);
} else {
// Tone analysis is display-only — no Replace button (pass null for originalText)
const isExplainAction = action === 'explain';
@@ -631,6 +637,7 @@ export default defineContentScript({
response?.result ?? '(no result)',
isExplainAction ? null : textToProcess,
snapStart, snapEnd, snapElement, snapRange,
action, textToProcess,
);
}
} catch (err) {
@@ -639,7 +646,7 @@ export default defineContentScript({
String(err).includes('message channel closed')) {
showErrorToast('LexAI was updated — please refresh this page.');
} 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,
snapElement: HTMLTextAreaElement | HTMLInputElement | null,
snapRange: Range | null,
action?: string,
textForRegenerate?: string,
) {
if (modal) modal.remove();
@@ -741,6 +750,102 @@ export default defineContentScript({
});
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
const btnRow = document.createElement('div');
Object.assign(btnRow.style, { display: 'flex', gap: '8px' });
@@ -949,6 +1054,7 @@ export default defineContentScript({
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'lexai-context-menu') {
selectedText = message.text as string;
currentStyle = (message.style as string) || 'Default';
// For context menu, we have no DOM selection positions — zero them out
storedStart = -1;
storedEnd = -1;