import React, { useEffect, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; // ─── Provider config ────────────────────────────────────────────────────────── const PROVIDERS = [ { id: 'openai', name: 'OpenAI', placeholder: 'sk-...', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'], docsUrl: 'https://platform.openai.com/api-keys', }, { id: 'anthropic', name: 'Anthropic (Claude)', placeholder: 'sk-ant-...', models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'], docsUrl: 'https://console.anthropic.com/keys', }, { id: 'groq', name: 'Groq (Free tier)', placeholder: 'gsk_...', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'], docsUrl: 'https://console.groq.com/keys', }, { id: 'openrouter', name: 'OpenRouter (100+ models)', placeholder: 'sk-or-...', models: ['openai/gpt-4o-mini', 'anthropic/claude-3-5-haiku', 'meta-llama/llama-3.3-70b-instruct:free'], docsUrl: 'https://openrouter.ai/keys', }, ]; // ─── Styles ─────────────────────────────────────────────────────────────────── const styles = { page: { minHeight: '100vh', background: 'linear-gradient(135deg, #1e1e2e 0%, #181825 100%)', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: '40px 20px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', } as React.CSSProperties, card: { background: 'rgba(30,30,46,0.95)', border: '1px solid rgba(205,214,244,0.12)', borderRadius: '16px', padding: '32px', width: '100%', maxWidth: '480px', boxShadow: '0 8px 48px rgba(0,0,0,0.4)', color: '#cdd6f4', } as React.CSSProperties, logoRow: { display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '4px', } as React.CSSProperties, logo: { fontSize: '28px', } as React.CSSProperties, title: { fontSize: '22px', fontWeight: '800', color: '#89b4fa', margin: 0, } as React.CSSProperties, subtitle: { fontSize: '13px', color: '#6c7086', marginBottom: '28px', marginTop: '4px', } as React.CSSProperties, label: { display: 'block', fontSize: '13px', fontWeight: '600', color: '#a6adc8', marginBottom: '6px', textTransform: 'uppercase' as const, letterSpacing: '0.04em', } as React.CSSProperties, select: { width: '100%', padding: '10px 14px', borderRadius: '9px', border: '1px solid rgba(205,214,244,0.15)', background: 'rgba(49,50,68,0.7)', color: '#cdd6f4', fontSize: '14px', outline: 'none', cursor: 'pointer', marginBottom: '0', } as React.CSSProperties, input: { width: '100%', padding: '10px 14px', borderRadius: '9px', border: '1px solid rgba(205,214,244,0.15)', background: 'rgba(49,50,68,0.7)', color: '#cdd6f4', fontSize: '14px', outline: 'none', boxSizing: 'border-box' as const, } as React.CSSProperties, formGroup: { marginBottom: '20px', } as React.CSSProperties, hint: { fontSize: '12px', color: '#6c7086', marginTop: '6px', display: 'flex', alignItems: 'center', gap: '4px', } as React.CSSProperties, docsLink: { color: '#89b4fa', textDecoration: 'none', fontSize: '12px', } as React.CSSProperties, saveBtn: { width: '100%', padding: '12px', borderRadius: '10px', border: 'none', fontSize: '15px', fontWeight: '700', cursor: 'pointer', transition: 'all 0.2s ease', marginTop: '8px', } as React.CSSProperties, divider: { borderTop: '1px solid rgba(205,214,244,0.08)', margin: '24px 0', } as React.CSSProperties, statusBadge: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12px', padding: '4px 10px', borderRadius: '6px', marginTop: '12px', } as React.CSSProperties, }; // ─── Component ──────────────────────────────────────────────────────────────── function OptionsPage() { const [provider, setProvider] = useState('openai'); const [apiKey, setApiKey] = useState(''); const [model, setModel] = useState('gpt-4o-mini'); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const [showKey, setShowKey] = useState(false); const apiKeyRef = useRef(null); useEffect(() => { chrome.storage.local.get(['provider', 'apiKey', 'model'], (result) => { if (result.provider) setProvider(result.provider); if (result.apiKey) setApiKey(result.apiKey); if (result.model) setModel(result.model); }); }, []); const currentProvider = PROVIDERS.find((p) => p.id === provider) ?? PROVIDERS[0]; const handleProviderChange = (newProvider: string) => { setProvider(newProvider); const p = PROVIDERS.find((p) => p.id === newProvider); if (p) setModel(p.models[0]); }; const handleSave = () => { if (!apiKey.trim()) { apiKeyRef.current?.focus(); return; } setSaveStatus('saving'); chrome.storage.local.set({ provider, apiKey: apiKey.trim(), model }, () => { if (chrome.runtime.lastError) { setSaveStatus('error'); setTimeout(() => setSaveStatus('idle'), 3000); } else { setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2500); } }); }; const saveBtnStyle: React.CSSProperties = { ...styles.saveBtn, background: saveStatus === 'saved' ? '#a6e3a1' : saveStatus === 'error' ? '#f38ba8' : 'linear-gradient(135deg, #89b4fa 0%, #b4befe 100%)', color: '#1e1e2e', }; return (
{/* Header */}

LexAI Settings

Configure your LLM provider and API key. Your key is stored locally and never shared.

{/* Provider */}
{/* Model */}
{/* API Key */}
setApiKey(e.target.value)} placeholder={currentProvider.placeholder} style={{ ...styles.input, paddingRight: '42px' }} onKeyDown={(e) => e.key === 'Enter' && handleSave()} />
🔒 Stored only on your device. · Get API key →
{/* Save */}
{/* Info */}
How to use LexAI:
  1. Select any text on a webpage
  2. Click Fix, Rephrase, Shorten, or Expand
  3. Accept or replace the suggestion
); } createRoot(document.getElementById('app')!).render();