import React, { useState, useEffect, useRef } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { api } from '../api' import { useAuth } from '../AuthContext' import SocialAccounts from './SocialAccounts' import AffiliateList from './AffiliateList' import CommentsQueue from './CommentsQueue' // ─── Provider catalogs ──────────────────────────────────────────────────────── const TEXT_PROVIDERS = [ { value: 'claude', label: 'Claude (Anthropic)', defaultModel: 'claude-sonnet-4-20250514' }, // Sonnet 4.6 { value: 'openai', label: 'OpenAI', defaultModel: 'gpt-4o-mini' }, { value: 'gemini', label: 'Gemini (Google)', defaultModel: 'gemini-2.0-flash' }, { value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini' }, { value: 'custom', label: 'Personalizzato', defaultModel: '', needsBaseUrl: true }, ] // ─── Model catalogs per provider ───────────────────────────────────────────── const MODELS_BY_PROVIDER = { claude: [ { value: 'claude-opus-4-20250514', label: 'Claude Opus 4.6' }, { value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4.6' }, { value: 'claude-haiku-4-20250514', label: 'Claude Haiku 4.5' }, { value: 'claude-4-5-sonnet-20250514', label: 'Claude 4.5 Sonnet (legacy)' }, { value: 'claude-4-5-haiku-20250514', label: 'Claude 4.5 Haiku (legacy)' }, ], openai: [ { value: 'gpt-4o', label: 'GPT-4o' }, { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, { value: 'gpt-4.1', label: 'GPT-4.1' }, { value: 'gpt-4.1-mini', label: 'GPT-4.1 Mini' }, { value: 'gpt-4.1-nano', label: 'GPT-4.1 Nano' }, { value: 'o3-mini', label: 'o3 Mini' }, ], gemini: [ { value: 'gemini-2.5-flash-preview-05-20', label: 'Gemini 2.5 Flash' }, { value: 'gemini-2.5-pro-preview-05-06', label: 'Gemini 2.5 Pro' }, { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' }, ], openrouter: [ { value: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4.6' }, { value: 'anthropic/claude-haiku-4', label: 'Claude Haiku 4.5' }, { value: 'openai/gpt-4o-mini', label: 'GPT-4o Mini' }, { value: 'google/gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, { value: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick' }, { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1' }, ], } const IMAGE_PROVIDERS = [ { value: 'dalle', label: 'DALL-E (OpenAI)' }, { value: 'replicate', label: 'Replicate' }, { value: 'wavespeed', label: 'WaveSpeed' }, { value: 'custom', label: 'Personalizzato', needsBaseUrl: true }, ] const VIDEO_PROVIDERS = [ { value: 'wavespeed', label: 'WaveSpeed' }, { value: 'replicate', label: 'Replicate' }, { value: 'custom', label: 'Personalizzato', needsBaseUrl: true }, ] const VOICE_PROVIDERS = [ { value: 'elevenlabs', label: 'ElevenLabs' }, { value: 'openai_tts', label: 'OpenAI TTS' }, { value: 'wavespeed', label: 'WaveSpeed' }, { value: 'custom', label: 'Personalizzato', needsBaseUrl: true }, ] const PROVIDER_GUIDES = { claude: { steps: [ 'Vai su console.anthropic.com e crea un account (o accedi).', 'Dal menu laterale, clicca su "API Keys".', 'Clicca "+ Create Key", assegna un nome (es. "Leopost").', 'Copia la chiave generata — non sarà più visibile dopo.', ], link: 'https://console.anthropic.com/account/keys', }, openai: { steps: [ 'Vai su platform.openai.com e accedi.', 'In alto a destra clicca il tuo account → "API keys".', 'Clicca "+ Create new secret key".', 'Copia la chiave (inizia con "sk-").', ], link: 'https://platform.openai.com/api-keys', }, gemini: { steps: [ 'Vai su aistudio.google.com e accedi con Google.', 'Clicca "Get API Key" → "+ Create API key in new project".', 'Copia la chiave generata.', ], link: 'https://aistudio.google.com/app/apikey', }, openrouter: { steps: [ 'Vai su openrouter.ai e registrati.', 'In alto a destra: account → "API Keys" → "+ Create Key".', 'Copia la chiave (inizia con "sk-or-").', ], link: 'https://openrouter.ai/keys', }, dalle: { steps: [ 'DALL-E usa le stesse chiavi di OpenAI.', 'Vai su platform.openai.com e crea una API key.', 'Assicurati di avere crediti disponibili nel tuo account.', ], link: 'https://platform.openai.com/api-keys', }, replicate: { steps: [ 'Vai su replicate.com e crea un account.', 'Avatar in alto a destra → "API tokens" → "Create token".', 'Copia il token (inizia con "r8_").', ], link: 'https://replicate.com/account/api-tokens', }, wavespeed: { steps: [ 'Vai su wavespeed.ai e registrati.', 'Dal pannello account, vai alla sezione API Keys.', 'Genera una nuova chiave e copiala.', ], link: 'https://wavespeed.ai', }, elevenlabs: { steps: [ 'Vai su elevenlabs.io e accedi.', 'In basso a sinistra: profilo → "Profile + API key".', 'Copia la chiave API nella sezione "API Key".', "Per il Voice ID: vai su \"Voices\" → seleziona voce → copia l'ID dalla sidebar.", ], link: 'https://elevenlabs.io/app/settings/api-keys', }, openai_tts: { steps: [ 'OpenAI TTS usa le stesse chiavi di OpenAI (GPT).', 'Vai su platform.openai.com e crea una API key.', ], link: 'https://platform.openai.com/api-keys', }, custom: { steps: [ 'Inserisci la Base URL del provider personalizzato.', "Inserisci l'API key fornita dal provider.", 'Il provider deve essere compatibile con il formato API OpenAI.', ], link: null, }, } const SECTIONS = [ { id: 'profilo', label: 'Profilo' }, { id: 'piano', label: 'Piano & Account' }, { id: 'ai', label: 'Provider AI' }, { id: 'social', label: 'Account Social' }, { id: 'affiliati', label: 'Link Affiliati' }, { id: 'commenti', label: 'Commenti' }, { id: 'sicurezza', label: 'Sicurezza' }, { id: 'privacy', label: 'Privacy & Dati' }, ] export default function SettingsPage() { const { user, isPro } = useAuth() const navigate = useNavigate() const [searchParams] = useSearchParams() const [activeSection, setActiveSection] = useState(searchParams.get('tab') || 'profilo') const [aiValues, setAiValues] = useState({ llm_provider: 'claude', llm_api_key: '', llm_model: '', llm_base_url: '', image_provider: 'dalle', image_api_key: '', image_base_url: '', video_provider: 'wavespeed', video_api_key: '', video_model: '', video_base_url: '', voice_provider: 'elevenlabs', voice_api_key: '', voice_base_url: '', elevenlabs_voice_id: '', }) const [loadingAI, setLoadingAI] = useState(true) const [saving, setSaving] = useState({}) const [success, setSuccess] = useState({}) const [errors, setErrors] = useState({}) useEffect(() => { api.get('/settings/').then(data => { if (Array.isArray(data)) { const n = {} data.forEach(s => { n[s.key] = s.value }) setAiValues(prev => ({ ...prev, ...n })) } }).catch(() => {}).finally(() => setLoadingAI(false)) }, []) const setMsg = (key, type, msg) => { if (type === 'success') { setSuccess(p => ({ ...p, [key]: true })) setErrors(p => ({ ...p, [key]: '' })) setTimeout(() => setSuccess(p => ({ ...p, [key]: false })), 3000) } else { setErrors(p => ({ ...p, [key]: msg })) setSuccess(p => ({ ...p, [key]: false })) } } const saveAI = async (section, keys) => { setSaving(p => ({ ...p, [section]: true })) try { for (const key of keys) { if (aiValues[key] !== undefined) await api.put(`/settings/${key}`, { value: aiValues[key] }) } setMsg(section, 'success') } catch (e) { setMsg(section, 'error', e.message || 'Errore nel salvataggio') } finally { setSaving(p => ({ ...p, [section]: false })) } } return (
{/* Page header */}
Impostazioni

Impostazioni

{/* Sidebar nav */} {/* Content */}
{activeSection === 'profilo' && ( )} {activeSection === 'piano' && ( )} {activeSection === 'ai' && ( setAiValues(p => ({ ...p, [k]: v }))} saveAI={saveAI} loading={loadingAI} saving={saving} success={success} errors={errors} /> )} {activeSection === 'social' && } {activeSection === 'affiliati' && } {activeSection === 'commenti' && } {activeSection === 'sicurezza' && ( )} {activeSection === 'privacy' && ( )}
) } // ─── Profilo ───────────────────────────────────────────────────────────────── function ProfiloSection({ user, saving, success, errors, setSaving, setMsg }) { const [displayName, setDisplayName] = React.useState(user?.display_name || '') React.useEffect(() => { setDisplayName(user?.display_name || '') }, [user?.display_name]) const handleSave = async () => { const name = displayName.trim() if (!name) return setSaving(p => ({ ...p, profilo: true })) try { await api.put('/auth/me', { display_name: name }) setMsg('profilo', 'success') } catch (e) { setMsg('profilo', 'error', e.message || 'Errore nel salvataggio') } finally { setSaving(p => ({ ...p, profilo: false })) } } return (
Profilo

Il nome viene mostrato nella sidebar. L'email non è modificabile.

{errors.profilo && {errors.profilo}} {success.profilo && Profilo aggiornato con successo.}
setDisplayName(e.target.value)} placeholder="Il tuo nome" style={inputStyle} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} onKeyDown={e => e.key === 'Enter' && handleSave()} />

{user?.auth_provider === 'google' ? 'Account Google — email gestita da Google.' : "L'email non è modificabile per motivi di sicurezza."}

) } // ─── Piano & Account ───────────────────────────────────────────────────────── function PianoSection({ user, isPro, saving, success, errors, setSaving, setMsg }) { const [code, setCode] = useState('') const handleRedeem = async () => { if (!code.trim()) return setSaving(p => ({ ...p, redeem: true })) try { const data = await api.post('/auth/redeem', { code: code.trim() }) setMsg('redeem', 'success') setCode('') // Refresh the page to update plan badge setTimeout(() => window.location.reload(), 1500) } catch (e) { setMsg('redeem', 'error', e.message || 'Codice non valido') } finally { setSaving(p => ({ ...p, redeem: false })) } } const expiry = user?.subscription_expires_at const expiryDate = expiry ? new Date(expiry).toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric' }) : null return (
Piano & Account {/* Current plan */}
{isPro ? 'PRO' : 'FREEMIUM'} {isPro && expiryDate && ( Attivo fino al {expiryDate} )}
{!isPro && (

Vuoi passare a Pro?

Piano Pro Early Adopter: €14,95/mese · €39,95/3 mesi · €64,95/6 mesi · €119,95/anno

Scrivici per attivarlo →
)}
{/* Redeem code */}

Hai ricevuto un codice di accesso anticipato? Inseriscilo qui per attivare il piano Pro.

{errors.redeem && {errors.redeem}} {success.redeem && Codice riscattato! Piano Pro attivato.}
setCode(e.target.value.toUpperCase())} placeholder="LEOPOST-XXXXX" maxLength={20} style={{ ...inputStyle, flex: 1, fontFamily: 'monospace', letterSpacing: '0.08em' }} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} onKeyDown={e => e.key === 'Enter' && handleRedeem()} />
) } // ─── AI Providers ───────────────────────────────────────────────────────────── function AISection({ values, onChange, saveAI, loading, saving, success, errors }) { if (loading) return const currentText = TEXT_PROVIDERS.find(p => p.value === values.llm_provider) || TEXT_PROVIDERS[0] const currentImage = IMAGE_PROVIDERS.find(p => p.value === values.image_provider) || IMAGE_PROVIDERS[0] const currentVideo = VIDEO_PROVIDERS.find(p => p.value === values.video_provider) || VIDEO_PROVIDERS[0] const currentVoice = VOICE_PROVIDERS.find(p => p.value === values.voice_provider) || VOICE_PROVIDERS[0] return (
Provider AI {/* Step-by-step setup guide */}

Come configurare — Guida passo per passo

{[ { n: 1, title: 'Scegli un provider LLM (per i testi)', body: 'Il più semplice da iniziare: Claude (Anthropic) o OpenAI. Serve una API key gratuita o a pagamento dal sito del provider.' }, { n: 2, title: 'Ottieni la tua API key', body: 'Clicca su "▼ Mostra guida" sotto il campo API Key — trovi le istruzioni specifiche per ogni provider, con link diretto alla pagina giusta.' }, { n: 3, title: 'Incolla la chiave e salva', body: 'Copia la chiave dal sito del provider, incollala nel campo apposito e clicca "Salva". La chiave viene cifrata e non è più visibile dopo il salvataggio.' }, { n: 4, title: 'Configura immagini (opzionale)', body: 'Per generare immagini AI aggiungi DALL-E (OpenAI) o Replicate. Non obbligatorio per generare solo testi.' }, { n: 5, title: 'Torna alla Dashboard e verifica', body: 'Il pannello "Stato Provider" in Dashboard mostra ✓ o ✗ per ogni servizio configurato.' }, ].map(step => (
{step.n}

{step.title}

{step.body}

))}
saveAI('text', ['llm_provider','llm_api_key','llm_model','llm_base_url'])} saving={saving.text} success={success.text} error={errors.text} /> saveAI('image', ['image_provider','image_api_key','image_base_url'])} saving={saving.image} success={success.image} error={errors.image} /> saveAI('video', ['video_provider','video_api_key','video_model','video_base_url'])} saving={saving.video} success={success.video} error={errors.video} /> saveAI('voice', ['voice_provider','voice_api_key','voice_base_url','elevenlabs_voice_id'])} saving={saving.voice} success={success.voice} error={errors.voice} />
) } function ModelSelector({ providerValue, modelValue, defaultModel, onChange }) { const models = MODELS_BY_PROVIDER[providerValue] || [] const isKnownModel = models.some(m => m.value === modelValue) const isCustom = modelValue && !isKnownModel const [showCustom, setShowCustom] = React.useState(isCustom) // Reset to dropdown when provider changes and current value isn't custom React.useEffect(() => { const providerModels = MODELS_BY_PROVIDER[providerValue] || [] if (providerModels.length === 0) { setShowCustom(true) } else if (modelValue && !providerModels.some(m => m.value === modelValue)) { // Keep custom if user had typed something not in the list setShowCustom(true) } else { setShowCustom(false) } }, [providerValue]) if (models.length === 0) { // No catalog for this provider — show plain input return ( onChange(e.target.value)} placeholder={defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace' }} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> ) } return ( {!showCustom ? (
) : (
onChange(e.target.value)} placeholder={defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace', marginBottom: '0.35rem' }} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
)}
) } function ProviderCard({ title, description, providers, providerKey, apiKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder, currentProvider, values, onChange, onSave, saving, success, error }) { return (

{description}

{error && {error}} {success && Salvato con successo} {baseUrlKey && ( onChange(baseUrlKey, e.target.value)} placeholder="https://..." style={{ ...inputStyle, fontFamily: 'monospace' }} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> )} onChange(apiKey, e.target.value)} placeholder="Inserisci la tua API key…" style={{ ...inputStyle, fontFamily: 'monospace' }} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> {modelKey && ( onChange(modelKey, v)} /> )} {extraKey && ( onChange(extraKey, e.target.value)} placeholder={extraPlaceholder} style={inputStyle} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> )} {/* API key guide */} {(() => { const guide = PROVIDER_GUIDES[values[providerKey]] if (!guide) return null return ( ) })()}
) } function GuideAccordion({ guide, providerName }) { const [open, setOpen] = React.useState(false) if (!guide?.steps?.length) return null return (
{open && (
    {guide.steps.map((step, i) => (
  1. {step}
  2. ))}
{guide.link && ( Vai alla pagina → )}
)}
) } // ─── Sicurezza ──────────────────────────────────────────────────────────────── function SicurezzaSection({ user, saving, success, errors, setMsg, setSaving }) { const [form, setForm] = useState({ current_password: '', new_password: '', confirm_password: '' }) const isGoogle = user?.auth_provider === 'google' const handleChange = async () => { if (form.new_password !== form.confirm_password) { setMsg('pwd', 'error', 'Le password non coincidono.') return } if (form.new_password.length < 8) { setMsg('pwd', 'error', 'La nuova password deve essere di almeno 8 caratteri.') return } setSaving(p => ({ ...p, pwd: true })) try { await api.post('/auth/change-password', { current_password: form.current_password, new_password: form.new_password, }) setMsg('pwd', 'success') setForm({ current_password: '', new_password: '', confirm_password: '' }) } catch (e) { setMsg('pwd', 'error', e.message || 'Errore nel cambio password') } finally { setSaving(p => ({ ...p, pwd: false })) } } return (
Sicurezza {isGoogle ? (

Il tuo account è collegato tramite Google. La password è gestita da Google — puoi modificarla dal tuo account Google.

) : ( <> {errors.pwd && {errors.pwd}} {success.pwd && Password aggiornata con successo.}
setForm(p => ({ ...p, current_password: e.target.value }))} placeholder="Password attuale" style={inputStyle} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> setForm(p => ({ ...p, new_password: e.target.value }))} placeholder="Nuova password" style={inputStyle} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> setForm(p => ({ ...p, confirm_password: e.target.value }))} placeholder="Ripeti la nuova password" style={inputStyle} onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} onKeyDown={e => e.key === 'Enter' && handleChange()} />
)}
) } // ─── Privacy & Dati ─────────────────────────────────────────────────────────── function PrivacySection({ user, navigate, saving, success, errors, setMsg, setSaving }) { const [deleteConfirm, setDeleteConfirm] = useState('') const [showDeleteForm, setShowDeleteForm] = useState(false) const handleExport = () => { window.location.href = '/api/auth/export-data' } const handleDelete = async () => { if (deleteConfirm !== 'ELIMINA') { setMsg('delete', 'error', 'Digita ELIMINA (in maiuscolo) per confermare.') return } setSaving(p => ({ ...p, delete: true })) try { await api.delete('/auth/account', { confirmation: 'ELIMINA' }) localStorage.clear() navigate('/login') } catch (e) { setMsg('delete', 'error', e.message || 'Errore durante la cancellazione') } finally { setSaving(p => ({ ...p, delete: false })) } } return (
Privacy & Dati {/* Export */}

Scarica tutti i tuoi dati personali in formato JSON (richiesto dal GDPR, Art. 20 — Portabilità dei dati).

{/* Delete */}

L'eliminazione è permanente e irreversibile. Tutti i tuoi dati (personaggi, contenuti, impostazioni) verranno cancellati.

{!showDeleteForm ? ( ) : (
{errors.delete && {errors.delete}}

Digita ELIMINA per confermare:

setDeleteConfirm(e.target.value)} placeholder="ELIMINA" style={{ ...inputStyle, flex: 1 }} onFocus={e => e.target.style.borderColor = 'var(--error)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
)}
) } // ─── Shared components ──────────────────────────────────────────────────────── function SectionTitle({ children }) { return (

{children}

) } function Card({ children, style }) { return (
{children}
) } function Label({ children, style }) { return ( {children} ) } function Field({ label, children }) { return (
{children}
) } function Feedback({ type, children }) { const colors = type === 'error' ? { bg: 'var(--error-light)', border: '#FED7D7', color: 'var(--error)' } : { bg: 'var(--success-light)', border: '#86EFAC', color: 'var(--success)' } return (
{children}
) } function Spinner() { return (
) } function EmbeddedSection({ title, description, children }) { return (

{title}

{description}

{children}
) } function SocialAccountsEmbed() { return } function AffiliateListEmbed() { return } function CommentsQueueEmbed() { return } const inputStyle = { width: '100%', padding: '0.625rem 0.875rem', border: '1px solid var(--border)', borderRadius: 0, fontSize: '0.875rem', color: 'var(--ink)', backgroundColor: 'var(--surface)', outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.15s', fontFamily: "'DM Sans', sans-serif", } const selectStyle = { ...inputStyle, cursor: 'pointer', appearance: 'auto' } const btnPrimary = { display: 'inline-block', padding: '0.6rem 1.25rem', backgroundColor: 'var(--ink)', color: 'white', fontFamily: "'DM Sans', sans-serif", fontWeight: 600, fontSize: '0.875rem', textDecoration: 'none', border: 'none', cursor: 'pointer', whiteSpace: 'nowrap', }