Initial commit: Leopost Full — merge di Leopost, Post Generator e Autopilot OS

- Backend FastAPI con multi-LLM (Claude/OpenAI/Gemini)
- Publishing su Facebook, Instagram, YouTube, TikTok
- Calendario editoriale con awareness levels (PAS, AIDA, BAB...)
- Design system Editorial Fresh (Fraunces + DM Sans)
- Scheduler automatico, gestione commenti AI, affiliate links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-31 17:23:16 +02:00
commit 519a580679
58 changed files with 8348 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
import { useState, useEffect } from 'react'
import { api } from '../api'
const cardStyle = {
backgroundColor: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: '0.75rem',
padding: '1.5rem',
}
const inputStyle = {
width: '100%',
padding: '0.625rem 1rem',
border: '1px solid var(--border)',
borderRadius: '0.5rem',
fontSize: '0.875rem',
color: 'var(--ink)',
backgroundColor: 'var(--cream)',
outline: 'none',
}
export default function ContentPage() {
const [characters, setCharacters] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [generated, setGenerated] = useState(null)
const [editing, setEditing] = useState(false)
const [editText, setEditText] = useState('')
const [form, setForm] = useState({
character_id: '',
platform: 'instagram',
content_type: 'text',
topic_hint: '',
include_affiliates: false,
})
useEffect(() => {
api.get('/characters/').then(setCharacters).catch(() => {})
}, [])
const handleChange = (field, value) => {
setForm((prev) => ({ ...prev, [field]: value }))
}
const handleGenerate = async (e) => {
e.preventDefault()
if (!form.character_id) {
setError('Seleziona un personaggio')
return
}
setError('')
setLoading(true)
setGenerated(null)
try {
const data = await api.post('/content/generate', {
character_id: parseInt(form.character_id),
platform: form.platform,
content_type: form.content_type,
topic_hint: form.topic_hint || null,
include_affiliates: form.include_affiliates,
})
setGenerated(data)
setEditText(data.text_content || '')
} catch (err) {
setError(err.message || 'Errore nella generazione')
} finally {
setLoading(false)
}
}
const handleApprove = async () => {
if (!generated) return
try {
await api.post(`/content/posts/${generated.id}/approve`)
setGenerated((prev) => ({ ...prev, 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 })
setGenerated((prev) => ({ ...prev, text_content: editText }))
setEditing(false)
} catch (err) {
setError(err.message || 'Errore salvataggio')
}
}
const handleDelete = async () => {
if (!generated) return
if (!confirm('Eliminare questo contenuto?')) return
try {
await api.delete(`/content/posts/${generated.id}`)
setGenerated(null)
} catch (err) {
setError(err.message || 'Errore eliminazione')
}
}
const platformLabels = {
instagram: 'Instagram',
facebook: 'Facebook',
youtube: 'YouTube',
tiktok: 'TikTok',
}
const contentTypeLabels = {
text: 'Testo',
image: 'Immagine',
video: 'Video',
}
return (
<div>
<div className="mb-6">
<h2 className="text-2xl font-bold" style={{ color: 'var(--ink)' }}>Contenuti</h2>
<p className="mt-1 text-sm" style={{ color: 'var(--muted)' }}>
Genera e gestisci contenuti per i tuoi personaggi
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Generation form */}
<div style={cardStyle}>
<h3 className="font-semibold text-sm uppercase tracking-wider mb-4" style={{ color: 'var(--muted)' }}>
Genera Contenuto
</h3>
<form onSubmit={handleGenerate} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Personaggio</label>
<select
value={form.character_id}
onChange={(e) => handleChange('character_id', e.target.value)}
style={inputStyle}
required
>
<option value="">Seleziona personaggio...</option>
{characters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Piattaforma</label>
<select
value={form.platform}
onChange={(e) => handleChange('platform', e.target.value)}
style={inputStyle}
>
{Object.entries(platformLabels).map(([val, label]) => (
<option key={val} value={val}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Tipo contenuto</label>
<select
value={form.content_type}
onChange={(e) => handleChange('content_type', e.target.value)}
style={inputStyle}
>
{Object.entries(contentTypeLabels).map(([val, label]) => (
<option key={val} value={val}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>
Suggerimento tema <span className="font-normal" style={{ color: 'var(--muted)' }}>(opzionale)</span>
</label>
<input
type="text"
value={form.topic_hint}
onChange={(e) => handleChange('topic_hint', e.target.value)}
placeholder="Es. ultimi trend, tutorial..."
style={inputStyle}
/>
</div>
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={form.include_affiliates}
onChange={(e) => handleChange('include_affiliates', e.target.checked)}
className="sr-only peer"
/>
<div className="w-9 h-5 bg-slate-200 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-coral"></div>
</label>
<span className="text-sm" style={{ color: 'var(--ink)' }}>Includi link affiliati</span>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 text-white font-medium rounded-lg transition-opacity text-sm"
style={{ backgroundColor: 'var(--coral)', opacity: loading ? 0.7 : 1 }}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
Generazione in corso...
</span>
) : 'Genera'}
</button>
</form>
</div>
{/* Preview */}
<div style={cardStyle}>
<h3 className="font-semibold text-sm uppercase tracking-wider mb-4" style={{ color: 'var(--muted)' }}>
Ultimo Contenuto Generato
</h3>
{loading ? (
<div className="flex flex-col items-center justify-center py-16">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-t-transparent" style={{ borderColor: 'var(--coral)' }} />
<p className="text-sm mt-3" style={{ color: 'var(--muted)' }}>Generazione in corso...</p>
</div>
) : generated ? (
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${
generated.status === 'approved' ? 'bg-emerald-50 text-emerald-600' :
generated.status === 'published' ? 'bg-blue-50 text-blue-600' :
'bg-amber-50 text-amber-600'
}`}>
{generated.status === 'approved' ? 'Approvato' :
generated.status === 'published' ? 'Pubblicato' : 'Bozza'}
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600">
{platformLabels[generated.platform_hint] || generated.platform_hint}
</span>
</div>
{editing ? (
<div className="space-y-2">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={8}
className="w-full px-4 py-2.5 rounded-lg text-sm resize-none focus:outline-none"
style={{ border: '1px solid var(--border)', color: 'var(--ink)' }}
/>
<div className="flex gap-2">
<button
onClick={handleSaveEdit}
className="px-3 py-1.5 text-white text-xs rounded-lg"
style={{ backgroundColor: 'var(--coral)' }}
>
Salva
</button>
<button
onClick={() => { setEditing(false); setEditText(generated.text_content || '') }}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg"
>
Annulla
</button>
</div>
</div>
) : (
<div className="p-4 rounded-lg" style={{ backgroundColor: 'var(--cream)' }}>
<p className="text-sm whitespace-pre-wrap leading-relaxed" style={{ color: 'var(--ink)' }}>
{generated.text_content}
</p>
</div>
)}
{generated.hashtags && generated.hashtags.length > 0 && (
<div>
<p className="text-xs font-medium mb-1.5" style={{ color: 'var(--muted)' }}>Hashtag</p>
<div className="flex flex-wrap gap-1.5">
{generated.hashtags.map((tag, i) => (
<span key={i} className="text-xs px-2 py-0.5 rounded" style={{ backgroundColor: '#FFF0EC', color: 'var(--coral)' }}>
{tag}
</span>
))}
</div>
</div>
)}
<div className="flex items-center gap-2 pt-3 border-t" style={{ borderColor: 'var(--border)' }}>
{generated.status !== 'approved' && (
<button
onClick={handleApprove}
className="text-xs px-3 py-1.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-600 rounded-lg transition-colors font-medium"
>
Approva
</button>
)}
{!editing && (
<button
onClick={() => setEditing(true)}
className="text-xs px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg transition-colors"
>
Modifica
</button>
)}
<button
onClick={handleDelete}
className="text-xs px-3 py-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors ml-auto"
>
Elimina
</button>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-16 text-center">
<p className="text-4xl mb-3"></p>
<p className="font-medium" style={{ color: 'var(--ink)' }}>Nessun contenuto generato</p>
<p className="text-sm mt-1" style={{ color: 'var(--muted)' }}>
Compila il form e clicca "Genera"
</p>
</div>
)}
</div>
</div>
</div>
)
}