feat: LEXAI-16 request timeout (fetchWithTimeout), LEXAI-19 Copy As, LEXAI-20 Download
Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled

This commit is contained in:
Forge
2026-03-06 15:25:45 +08:00
parent 209a26c09c
commit e3b4925ef5
2 changed files with 439 additions and 4 deletions

View File

@@ -156,7 +156,20 @@ export default defineContentScript({
return { top, left };
}
// Inject shared keyframe CSS once
function ensureLexAIStyles() {
if (document.getElementById('lexai-styles')) return;
const style = document.createElement('style');
style.id = 'lexai-styles';
style.textContent = `
@keyframes lexai-spin { to { transform: rotate(360deg); } }
@keyframes lexai-fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
`;
document.head.appendChild(style);
}
function showToolbar(rect: DOMRect) {
ensureLexAIStyles();
hideToolbar();
const { top, left } = getToolbarPosition(rect);
@@ -230,6 +243,92 @@ export default defineContentScript({
toolbar!.appendChild(btn);
});
// ── Separator ────────────────────────────────────────────────────────────
const sep = document.createElement('div');
Object.assign(sep.style, {
width: '1px',
height: '20px',
background: 'rgba(205,214,244,0.15)',
margin: '0 2px',
flexShrink: '0',
});
toolbar!.appendChild(sep);
// ── LEXAI-19: Copy As button ─────────────────────────────────────────────
const copyAsBtn = document.createElement('button');
copyAsBtn.textContent = '⎘ Copy As';
copyAsBtn.setAttribute('data-lexai', 'true');
Object.assign(copyAsBtn.style, {
background: 'rgba(49,50,68,0.8)',
color: '#f9e2af',
border: '1px solid rgba(205,214,244,0.1)',
borderRadius: '7px',
padding: '4px 10px',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.15s ease',
letterSpacing: '0.01em',
whiteSpace: 'nowrap',
});
copyAsBtn.addEventListener('mouseenter', () => {
copyAsBtn.style.background = 'rgba(69,71,90,0.9)';
copyAsBtn.style.borderColor = '#f9e2af50';
copyAsBtn.style.transform = 'translateY(-1px)';
});
copyAsBtn.addEventListener('mouseleave', () => {
copyAsBtn.style.background = 'rgba(49,50,68,0.8)';
copyAsBtn.style.borderColor = 'rgba(205,214,244,0.1)';
copyAsBtn.style.transform = 'translateY(0)';
});
copyAsBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
captureForButton();
});
copyAsBtn.addEventListener('click', (e) => {
e.stopPropagation();
showCopyAsModal(selectedText);
});
toolbar!.appendChild(copyAsBtn);
// ── LEXAI-20: Download button ────────────────────────────────────────────
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '⬇ Save';
downloadBtn.setAttribute('data-lexai', 'true');
Object.assign(downloadBtn.style, {
background: 'rgba(49,50,68,0.8)',
color: '#9399b2',
border: '1px solid rgba(205,214,244,0.1)',
borderRadius: '7px',
padding: '4px 10px',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.15s ease',
letterSpacing: '0.01em',
whiteSpace: 'nowrap',
});
downloadBtn.addEventListener('mouseenter', () => {
downloadBtn.style.background = 'rgba(69,71,90,0.9)';
downloadBtn.style.borderColor = '#9399b250';
downloadBtn.style.transform = 'translateY(-1px)';
});
downloadBtn.addEventListener('mouseleave', () => {
downloadBtn.style.background = 'rgba(49,50,68,0.8)';
downloadBtn.style.borderColor = 'rgba(205,214,244,0.1)';
downloadBtn.style.transform = 'translateY(0)';
});
downloadBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
captureForButton();
});
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
hideToolbar();
downloadAsText(selectedText);
});
toolbar!.appendChild(downloadBtn);
document.body.appendChild(toolbar);
}
@@ -263,6 +362,183 @@ export default defineContentScript({
setTimeout(() => toast.remove(), 4000);
}
function showToast(msg: string, isError = false) {
const toast = document.createElement('div');
toast.setAttribute('data-lexai', 'true');
toast.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 999999;
background: ${isError ? '#f38ba8' : '#a6e3a1'}; color: #1e1e2e;
padding: 10px 16px; border-radius: 8px;
font-family: system-ui, sans-serif; font-size: 13px; font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: lexai-fadein 0.15s ease;
`;
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
// ─── LEXAI-20: Download as .txt ───────────────────────────────────────────
function downloadAsText(text: string) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const filename = `lexai-note-${timestamp}.txt`;
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`Downloaded: ${filename}`);
}
// ─── LEXAI-19: Copy As modal ──────────────────────────────────────────────
function showCopyAsModal(text: string) {
// Remove any existing copy-as modal
document.getElementById('lexai-copyAs-overlay')?.remove();
const formats = ['JSON', 'Markdown', 'Bullet List', 'Numbered List', 'CSV', 'HTML'];
const overlay = document.createElement('div');
overlay.id = 'lexai-copyAs-overlay';
overlay.setAttribute('data-lexai', 'true');
Object.assign(overlay.style, {
position: 'fixed',
inset: '0',
zIndex: '2147483646',
background: 'rgba(0,0,0,0.25)',
});
const picker = document.createElement('div');
picker.setAttribute('data-lexai', 'true');
Object.assign(picker.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%,-50%)',
zIndex: '2147483647',
background: 'linear-gradient(135deg, #1e1e2e 0%, #181825 100%)',
borderRadius:'12px',
padding: '16px',
width: '280px',
boxShadow: '0 12px 40px rgba(0,0,0,0.55)',
border: '1px solid rgba(205,214,244,0.15)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
});
// Header row
const header = document.createElement('div');
Object.assign(header.style, {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '12px',
});
const title = document.createElement('div');
title.innerHTML = '⚡ <strong style="color:#cdd6f4">Copy As</strong>';
Object.assign(title.style, { fontSize: '12px', color: '#89b4fa' });
const closeX = document.createElement('button');
closeX.textContent = '✕';
Object.assign(closeX.style, {
background: 'none', border: 'none', color: '#6c7086',
cursor: 'pointer', fontSize: '14px', padding: '0 2px', lineHeight: '1',
});
closeX.addEventListener('click', () => overlay.remove());
header.appendChild(title);
header.appendChild(closeX);
picker.appendChild(header);
// Spinner slot (shown while loading)
const spinnerSlot = document.createElement('div');
spinnerSlot.setAttribute('data-lexai', 'true');
Object.assign(spinnerSlot.style, {
display: 'none',
justifyContent: 'center',
alignItems: 'center',
padding: '12px 0',
color: '#a6adc8',
fontSize: '13px',
gap: '8px',
});
spinnerSlot.innerHTML = '<span style="font-size:18px;animation:lexai-spin 0.8s linear infinite;display:inline-block">⟳</span> Formatting…';
picker.appendChild(spinnerSlot);
// 2-column grid of format buttons
const grid = document.createElement('div');
Object.assign(grid.style, {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '6px',
});
formats.forEach((fmt) => {
const btn = document.createElement('button');
btn.textContent = fmt;
btn.setAttribute('data-lexai', 'true');
Object.assign(btn.style, {
background: 'rgba(49,50,68,0.8)',
color: '#cdd6f4',
border: '1px solid rgba(205,214,244,0.12)',
borderRadius: '8px',
padding: '8px 10px',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.12s ease',
textAlign: 'center',
});
btn.addEventListener('mouseenter', () => {
btn.style.background = 'rgba(69,71,90,0.9)';
btn.style.borderColor = '#89b4fa50';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = 'rgba(49,50,68,0.8)';
btn.style.borderColor = 'rgba(205,214,244,0.12)';
});
btn.addEventListener('click', async () => {
// Show spinner, hide grid
grid.style.display = 'none';
spinnerSlot.style.display = 'flex';
const response = await safeSendMessage({
type: 'COPY_AS',
text,
format: fmt,
}) as { error?: string; result?: string } | null;
overlay.remove();
if (!response || response.error) {
showToast(`${response?.error ?? 'Unknown error'}`, true);
return;
}
try {
await navigator.clipboard.writeText(response.result ?? '');
showToast(`✓ Copied as ${fmt}!`);
} catch {
showToast('❌ Clipboard write failed', true);
}
});
grid.appendChild(btn);
});
picker.appendChild(grid);
overlay.appendChild(picker);
// Close on overlay click (outside picker)
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
document.body.appendChild(overlay);
}
// ─── Safe chrome.runtime.sendMessage wrapper ──────────────────────────────
async function safeSendMessage(payload: Record<string, unknown>): Promise<unknown> {