import { useState, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { Link, useSearchParams } from 'react-router-dom' import { api } from '../api' import { useAuth } from '../AuthContext' import ConfirmModal from './ConfirmModal' const PLATFORMS = [ { value: 'instagram', label: 'Instagram' }, { value: 'facebook', label: 'Facebook' }, { value: 'youtube', label: 'YouTube' }, { value: 'tiktok', label: 'TikTok' }, ] const CONTENT_TYPES = [ { value: 'text', label: 'Testo' }, { value: 'image', label: 'Immagine' }, { value: 'video', label: 'Video' }, { value: 'reel', label: 'Reel/Short' }, { value: 'story', label: 'Story' }, ] const TECNICHE_NARRATIVE = [ { value: 'PAS', label: 'PAS', desc: 'Problema → Agitazione → Soluzione' }, { value: 'AIDA', label: 'AIDA', desc: 'Attenzione → Interesse → Desiderio → Azione' }, { value: 'Storytelling', label: 'Storytelling', desc: 'Narrazione con arco emotivo' }, { value: 'Tutorial', label: 'Tutorial', desc: 'Step-by-step educativo' }, { value: 'Listicle', label: 'Listicle', desc: 'Lista di consigli/esempi' }, { value: 'Social_proof', label: 'Social Proof', desc: 'Risultati, testimonianze, numeri' }, { value: 'Hook_domanda', label: 'Hook + Domanda', desc: 'Apertura con domanda provocatoria' }, ] const STATUS_LABELS = { approved: 'Approvato', published: 'Pubblicato', draft: 'Bozza' } const STATUS_COLORS = { approved: { bg: 'var(--success-light)', color: 'var(--success)' }, published: { bg: '#EFF6FF', color: '#1D4ED8' }, draft: { bg: '#FFFBEB', color: '#B45309' }, } export default function ContentPage() { const { isPro } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const availablePlatforms = isPro ? PLATFORMS : PLATFORMS.filter(p => ['instagram', 'facebook'].includes(p.value)) const autoGenerateRef = useRef(false) const [characters, setCharacters] = useState([]) const [loading, setLoading] = useState(false) const [charsLoading, setCharsLoading] = useState(true) const [error, setError] = useState('') const [generatedPosts, setGeneratedPosts] = useState([]) // array of posts, one per platform const [activePlatformIdx, setActivePlatformIdx] = useState(0) const [editing, setEditing] = useState(false) const [editText, setEditText] = useState('') const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showScheduleModal, setShowScheduleModal] = useState(false) const [scheduleDate, setScheduleDate] = useState('') const [scheduleTime, setScheduleTime] = useState('09:00') const [form, setForm] = useState({ character_id: '', brief: '', platforms: ['instagram'], content_types: ['text'], topic_hint: '', tecnica: '', include_affiliates: false, }) useEffect(() => { api.get('/characters/').then(d => { setCharacters(d) setCharsLoading(false) // Auto-select default character (first active one) const activeChars = d.filter(c => c.is_active) const defaultChar = activeChars.length > 0 ? String(activeChars[0].id) : '' // One-click flow: pre-fill from URL params const urlTopic = searchParams.get('topic') const urlCharacter = searchParams.get('character') if (urlTopic && d.length > 0) { const charId = urlCharacter || defaultChar setForm(prev => ({ ...prev, character_id: charId, topic_hint: urlTopic })) autoGenerateRef.current = true setSearchParams({}, { replace: true }) } else if (defaultChar) { // Pre-select default character setForm(prev => ({ ...prev, character_id: prev.character_id || defaultChar })) } }).catch(() => setCharsLoading(false)) }, []) // Auto-generate when arriving from one-click flow useEffect(() => { if (autoGenerateRef.current && form.character_id && !charsLoading) { autoGenerateRef.current = false // Small delay to let React render the form setTimeout(() => { document.querySelector('form')?.requestSubmit() }, 300) } }, [form.character_id, charsLoading]) const toggleChip = (field, value) => { setForm(prev => { const arr = prev[field] if (arr.includes(value)) { return { ...prev, [field]: arr.length > 1 ? arr.filter(v => v !== value) : arr } } // Maintain canonical order (same as PLATFORMS/CONTENT_TYPES arrays) const canonical = field === 'platforms' ? PLATFORMS.map(p => p.value) : CONTENT_TYPES.map(t => t.value) const newArr = [...arr, value].sort((a, b) => canonical.indexOf(a) - canonical.indexOf(b)) return { ...prev, [field]: newArr } }) } const handleGenerate = async (e) => { e.preventDefault() if (!form.character_id) { setError('Seleziona un personaggio'); return } setError('') setLoading(true) setGeneratedPosts([]) setActivePlatformIdx(0) try { const data = await api.post('/content/generate', { character_id: parseInt(form.character_id), platforms: form.platforms, content_types: form.content_types, topic_hint: form.topic_hint || null, brief: [ form.tecnica ? `Tecnica narrativa: ${form.tecnica}` : '', form.brief, ].filter(Boolean).join('. ') || null, include_affiliates: form.include_affiliates, }) // Backend returns array of posts (one per platform) const posts = Array.isArray(data) ? data : [data] setGeneratedPosts(posts) setActivePlatformIdx(0) setEditText(posts[0]?.text_content || '') } catch (err) { if (err.data?.missing_settings) { setError('__MISSING_SETTINGS__') } else if (err.data?.upgrade_required) { setError(err.data.message || 'Limite piano raggiunto. Passa a Pro.') } else { setError(err.message || 'Errore nella generazione') } } finally { setLoading(false) } } const generated = generatedPosts[activePlatformIdx] || null const updateActivePost = (updates) => { setGeneratedPosts(prev => prev.map((p, i) => i === activePlatformIdx ? { ...p, ...updates } : p)) } const handleApprove = async () => { if (!generated) return try { await api.post(`/content/posts/${generated.id}/approve`) updateActivePost({ status: 'approved' }) } catch (err) { setError(err.message || 'Errore approvazione') } } const handleSaveEdit = async () => { if (!generated) return try { await api.put(`/content/posts/${generated.id}`, { text_content: editText }) updateActivePost({ text_content: editText }) setEditing(false) } catch (err) { setError(err.message || 'Errore salvataggio') } } const handleDelete = async () => { if (!generated) return try { await api.delete(`/content/posts/${generated.id}`) const remaining = generatedPosts.filter((_, i) => i !== activePlatformIdx) setGeneratedPosts(remaining) setActivePlatformIdx(Math.min(activePlatformIdx, Math.max(0, remaining.length - 1))) setShowDeleteConfirm(false) } catch (err) { setError(err.message || 'Errore eliminazione') } } const handleSchedule = async () => { if (!generated || !scheduleDate) return try { const scheduledAt = new Date(`${scheduleDate}T${scheduleTime}:00`).toISOString() await api.post('/plans/schedule', { post_id: generated.id, platform: generated.platform_hint || 'instagram', scheduled_at: scheduledAt, }) updateActivePost({ status: 'scheduled' }) setShowScheduleModal(false) setError('') } catch (err) { setError(err.message || 'Errore nella schedulazione') } } return (
Definisci un brief editoriale, scegli piattaforma e tipo, poi genera. L'AI terrà conto del tono e dei topic del personaggio selezionato.
Libreria →Per generare contenuti serve almeno un personaggio che definisca la voce editoriale (tono, nicchia, pubblico).
Crea il tuo primo Personaggio →Per generare contenuti devi prima configurare un provider AI (Claude, OpenAI, Gemini...) e inserire la tua API key.
Vai alle Impostazioni →Generazione in corso…
{generated.text_content}
Per pubblicare automaticamente, connetti i tuoi account social in Impostazioni Social.
)}Nessun contenuto ancora
Scrivi un brief, seleziona personaggio e piattaforma, poi clicca "Genera"
Scegli data e ora per la pubblicazione{platform ? ` su ${platform}` : ''}. Il post verrà pubblicato automaticamente quando i social saranno connessi.