From 9604bef3a429783a95ebe477bf1b4640d33c1e2c Mon Sep 17 00:00:00 2001 From: Forge Date: Fri, 6 Mar 2026 15:01:29 +0800 Subject: [PATCH] feat: LEXAI-21 popup text input with quick action buttons --- entrypoints/popup/Popup.tsx | 366 +++++++++++++++++++++++++++++++---- entrypoints/popup/index.html | 2 +- 2 files changed, 330 insertions(+), 38 deletions(-) diff --git a/entrypoints/popup/Popup.tsx b/entrypoints/popup/Popup.tsx index 5432938..6243b25 100644 --- a/entrypoints/popup/Popup.tsx +++ b/entrypoints/popup/Popup.tsx @@ -1,32 +1,224 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; // ─── Safe chrome storage wrapper ───────────────────────────────────────────── -function safeStorageGet(keys: string[], callback: (result: Record) => void) { +function safeStorageGet( + storage: chrome.storage.StorageArea, + keys: string[], + callback: (result: Record) => void, +) { try { if (typeof chrome === 'undefined' || !chrome.runtime?.id) return; - chrome.storage.local.get(keys, callback); + storage.get(keys, callback); } catch (err) { console.warn('LexAI: Extension context invalidated', err); } } +function safeStorageSet(storage: chrome.storage.StorageArea, data: Record) { + try { + if (typeof chrome === 'undefined' || !chrome.runtime?.id) return; + storage.set(data); + } catch (err) { + console.warn('LexAI: Extension context invalidated', err); + } +} + +async function safeSendMessage( + message: Record, +): Promise | null> { + try { + if (typeof chrome === 'undefined' || !chrome.runtime?.id) return null; + return await chrome.runtime.sendMessage(message); + } catch (err) { + console.warn('LexAI: sendMessage failed', err); + return null; + } +} + +// ─── Types ─────────────────────────────────────────────────────────────────── + +type Action = 'fix' | 'rephrase' | 'shorten' | 'expand'; + +// ─── Styles ────────────────────────────────────────────────────────────────── + +const S = { + root: { + fontFamily: 'system-ui, -apple-system, sans-serif', + background: '#1e1e2e', + color: '#cdd6f4', + padding: '0', + minWidth: '420px', + maxHeight: '600px', + overflowY: 'auto' as const, + }, + header: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '12px 16px', + borderBottom: '1px solid rgba(205,214,244,0.1)', + }, + headerLeft: { + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '16px', + fontWeight: 700, + }, + settingsBtn: { + background: 'none', + border: '1px solid rgba(205,214,244,0.2)', + borderRadius: '6px', + color: '#a6adc8', + cursor: 'pointer', + fontSize: '12px', + padding: '4px 10px', + }, + section: { + padding: '14px 16px', + }, + divider: { + borderTop: '1px solid rgba(205,214,244,0.1)', + }, + textarea: { + width: '100%', + minHeight: '100px', + background: 'rgba(49,50,68,0.95)', + border: '1px solid rgba(205,214,244,0.15)', + borderRadius: '8px', + color: '#cdd6f4', + fontSize: '13px', + lineHeight: '1.5', + padding: '10px 12px', + resize: 'vertical' as const, + outline: 'none', + boxSizing: 'border-box' as const, + fontFamily: 'inherit', + }, + actionsRow: { + display: 'flex', + flexWrap: 'wrap' as const, + gap: '8px', + marginTop: '10px', + }, + actionBtn: { + background: 'linear-gradient(135deg, #cba6f7, #89b4fa)', + border: 'none', + borderRadius: '6px', + color: '#1e1e2e', + cursor: 'pointer', + fontSize: '12px', + fontWeight: 600, + padding: '6px 12px', + }, + actionBtnDisabled: { + background: 'rgba(203,166,247,0.3)', + border: 'none', + borderRadius: '6px', + color: '#a6adc8', + cursor: 'not-allowed', + fontSize: '12px', + fontWeight: 600, + padding: '6px 12px', + }, + processingBadge: { + fontSize: '12px', + color: '#a6adc8', + marginTop: '8px', + }, + resultSection: { + padding: '0 16px 14px', + }, + resultLabel: { + fontSize: '12px', + color: '#a6adc8', + marginBottom: '6px', + fontWeight: 600, + }, + resultBox: { + background: 'rgba(49,50,68,0.8)', + border: '1px solid rgba(205,214,244,0.1)', + borderRadius: '8px', + color: '#cdd6f4', + fontSize: '13px', + lineHeight: '1.5', + padding: '10px 12px', + whiteSpace: 'pre-wrap' as const, + wordBreak: 'break-word' as const, + }, + copyBtn: { + marginTop: '8px', + background: 'rgba(137,180,250,0.15)', + border: '1px solid rgba(137,180,250,0.3)', + borderRadius: '6px', + color: '#89b4fa', + cursor: 'pointer', + fontSize: '12px', + fontWeight: 600, + padding: '6px 14px', + }, + warningBox: { + background: 'rgba(243,139,168,0.1)', + border: '1px solid rgba(243,139,168,0.3)', + borderRadius: '8px', + color: '#f38ba8', + fontSize: '12px', + padding: '10px 12px', + marginBottom: '12px', + }, + errorBox: { + background: 'rgba(243,139,168,0.1)', + border: '1px solid rgba(243,139,168,0.3)', + borderRadius: '8px', + color: '#f38ba8', + fontSize: '12px', + padding: '10px 12px', + marginTop: '8px', + }, +}; + +// ─── Component ──────────────────────────────────────────────────────────────── + function Popup() { const [configured, setConfigured] = useState(false); const [provider, setProvider] = useState(''); const [model, setModel] = useState(''); + const [inputText, setInputText] = useState(''); + const [result, setResult] = useState(''); + const [error, setError] = useState(''); + const [processing, setProcessing] = useState(false); + const [copied, setCopied] = useState(false); + const sessionRestored = useRef(false); + // Load config + restore session input useEffect(() => { - safeStorageGet(["provider", "apiKey", "model"], (result) => { + safeStorageGet(chrome.storage.local, ['provider', 'apiKey', 'model'], (result) => { if (result.apiKey) { setConfigured(true); setProvider(result.provider || 'openai'); setModel(result.model || ''); } }); + + if (!sessionRestored.current) { + sessionRestored.current = true; + safeStorageGet(chrome.storage.session, ['lexai_popup_input'], (res) => { + if (res.lexai_popup_input) { + setInputText(res.lexai_popup_input); + } + }); + } }, []); + // Persist input to session storage on change + const handleInputChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setInputText(val); + safeStorageSet(chrome.storage.session, { lexai_popup_input: val }); + }; + const openSettings = () => { try { if (typeof chrome !== 'undefined' && chrome.runtime?.id) { @@ -37,46 +229,146 @@ function Popup() { } }; + const runAction = async (action: Action) => { + const text = inputText.trim(); + if (!text || processing) return; + + setProcessing(true); + setResult(''); + setError(''); + + const response = await safeSendMessage({ + type: 'lexai-action', + action, + text, + }); + + setProcessing(false); + + if (!response) { + setError('No response from extension. Try reloading.'); + return; + } + if (response.error) { + setError(response.error); + return; + } + if (response.result) { + setResult(response.result); + } + }; + + const copyResult = async () => { + if (!result) return; + try { + await navigator.clipboard.writeText(result); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // fallback + } + }; + + const actions: { label: string; id: Action }[] = [ + { label: 'Fix Grammar', id: 'fix' }, + { label: 'Rephrase', id: 'rephrase' }, + { label: 'Shorten', id: 'shorten' }, + { label: 'Expand', id: 'expand' }, + ]; + return ( -
-
- - LexAI +
+ {/* Header */} +
+
+ + LexAI +
+
- {configured ? ( -
-
-
✓ Active
-
{provider} / {model}
+ {/* Not configured warning */} + {!configured && ( +
+
+ ⚠ Setup Required +
+ Add your API key in Settings to use LexAI.
-

- Select any text on a webpage to see LexAI options appear. -

-
- ) : ( -
-
⚠ Setup Required
-
Add your API key to get started
+
)} - + {/* Main input area — only when configured */} + {configured && ( + <> +
+ {/* Provider badge */} +
+ ✓ {provider} + {model ? ` / ${model}` : ''} +
+ + {/* Textarea */} +