diff --git a/backend/app/routers/content.py b/backend/app/routers/content.py index 476114c..dd6a5c3 100644 --- a/backend/app/routers/content.py +++ b/backend/app/routers/content.py @@ -340,3 +340,78 @@ def approve_post( db.commit() db.refresh(post) return post + + +@router.get("/suggestions") +def get_topic_suggestions( + character_id: int | None = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Suggest 3 content topics based on character profile and recent posts.""" + if character_id: + character = ( + db.query(Character) + .filter(Character.id == character_id, Character.user_id == current_user.id) + .first() + ) + else: + character = ( + db.query(Character) + .filter(Character.user_id == current_user.id, Character.is_active == True) + .first() + ) + if not character: + return {"suggestions": [], "character_id": None} + + provider_name = _get_setting(db, "llm_provider", current_user.id) + api_key = _get_setting(db, "llm_api_key", current_user.id) + model = _get_setting(db, "llm_model", current_user.id) + + if not provider_name or not api_key: + return {"suggestions": [], "character_id": character.id, "needs_setup": True} + + recent_posts = ( + db.query(Post) + .filter(Post.character_id == character.id) + .order_by(Post.created_at.desc()) + .limit(5) + .all() + ) + recent_topics = [p.text_content[:100] for p in recent_posts if p.text_content] + recent_str = "\n".join(f"- {t}" for t in recent_topics) if recent_topics else "Nessun post recente." + + base_url = _get_setting(db, "llm_base_url", current_user.id) + llm = get_llm_provider(provider_name, api_key, model, base_url=base_url) + + topics = character.topics or [] + niche = character.niche or "general" + target = character.target_audience or "" + + system_prompt = ( + "Sei un social media strategist esperto. " + "Suggerisci 3 idee per post social, ciascuna su una riga. " + "Ogni idea deve essere una frase breve (max 15 parole) che descrive il topic. " + "Non numerare, non aggiungere spiegazioni. Solo 3 righe, una per idea." + ) + + prompt = ( + f"Personaggio: {character.name}, nicchia: {niche}\n" + f"Topic abituali: {', '.join(topics) if topics else 'generici'}\n" + f"Target: {target}\n" + f"Post recenti (evita ripetizioni):\n{recent_str}\n\n" + f"Suggerisci 3 idee per post nuovi e diversi dai recenti:" + ) + + try: + result = llm.generate(prompt, system=system_prompt) + lines = [line.strip() for line in result.strip().splitlines() if line.strip()] + suggestions = lines[:3] + except Exception: + suggestions = [] + + return { + "suggestions": suggestions, + "character_id": character.id, + "character_name": character.name, + } diff --git a/frontend/src/components/CharacterForm.jsx b/frontend/src/components/CharacterForm.jsx index 2402628..18d0bb7 100644 --- a/frontend/src/components/CharacterForm.jsx +++ b/frontend/src/components/CharacterForm.jsx @@ -299,7 +299,7 @@ export default function CharacterForm() { {/* ── Stile visivo ──────────────────────────────────────── */}
-
+
handleStyleChange('primary_color', e.target.value)} @@ -396,7 +396,7 @@ function RulesEditor({ doRules, dontRules, onChange }) { const removeDont = (i) => onChange(doRules, dontRules.filter((_, idx) => idx !== i)) return ( -
+
diff --git a/frontend/src/components/ContentArchive.jsx b/frontend/src/components/ContentArchive.jsx index 5d501fb..5b218af 100644 --- a/frontend/src/components/ContentArchive.jsx +++ b/frontend/src/components/ContentArchive.jsx @@ -127,7 +127,7 @@ export default function ContentArchive() {

) : ( -
+
{groups.map(group => { const activeIdx = activePlatform[group.key] || 0 const activePost = group.posts[activeIdx] || group.posts[0] diff --git a/frontend/src/components/ContentPage.jsx b/frontend/src/components/ContentPage.jsx index a783175..427bf27 100644 --- a/frontend/src/components/ContentPage.jsx +++ b/frontend/src/components/ContentPage.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react' -import { Link } from 'react-router-dom' +import { Link, useSearchParams } from 'react-router-dom' import { api } from '../api' import { useAuth } from '../AuthContext' import ConfirmModal from './ConfirmModal' @@ -38,7 +38,9 @@ const STATUS_COLORS = { 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) @@ -60,9 +62,32 @@ export default function ContentPage() { }) useEffect(() => { - api.get('/characters/').then(d => { setCharacters(d); setCharsLoading(false) }).catch(() => setCharsLoading(false)) + api.get('/characters/').then(d => { + setCharacters(d) + setCharsLoading(false) + // 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 || String(d[0].id) + setForm(prev => ({ ...prev, character_id: charId, topic_hint: urlTopic })) + autoGenerateRef.current = true + setSearchParams({}, { replace: true }) // clean URL + } + }).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] @@ -196,7 +221,7 @@ export default function ContentPage() {
))} -
+
{/* Generation form */}
diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index f7cd6ba..2f08f67 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -13,6 +13,8 @@ export default function Dashboard() { const [recentPosts, setRecentPosts] = useState([]) const [providerStatus, setProviderStatus] = useState(null) const [loading, setLoading] = useState(true) + const [suggestions, setSuggestions] = useState(null) + const [suggestionsLoading, setSuggestionsLoading] = useState(false) useEffect(() => { Promise.all([ @@ -36,6 +38,14 @@ export default function Dashboard() { setRecentPosts(posts.slice(0, 5)) setProviderStatus(providers) setLoading(false) + // Load suggestions if LLM is configured + if (providers?.llm?.configured && chars.length > 0) { + setSuggestionsLoading(true) + api.get('/content/suggestions') + .then(data => setSuggestions(data)) + .catch(() => {}) + .finally(() => setSuggestionsLoading(false)) + } }) }, []) @@ -87,7 +97,7 @@ export default function Dashboard() { {/* ── Stats grid ──────────────────────────────────────────── */}
@@ -133,6 +143,43 @@ export default function Dashboard() {
+ {/* ── Topic Suggestions (Phase C) ─────────────────────────── */} + {(suggestionsLoading || (suggestions?.suggestions?.length > 0)) && ( +
+ Suggerimenti per oggi + {suggestionsLoading ? ( +
+
+ Genero idee per te... +
+ ) : ( +
+ {suggestions.suggestions.map((topic, i) => ( + { e.currentTarget.style.backgroundColor = 'var(--accent-light)'; e.currentTarget.style.borderColor = 'var(--accent)' }} + onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'var(--surface)'; e.currentTarget.style.borderColor = 'var(--border)' }} + > +

+ {topic} +

+ + Genera → + + + ))} +
+ )} +
+ )} + {/* ── Recent posts ────────────────────────────────────────── */} {recentPosts.length > 0 && (
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 2861843..912150e 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -77,7 +77,7 @@ export default function Layout() { onClick={() => isMobile && setSidebarOpen(false)} style={({ isActive }) => ({ display: 'block', - padding: '0.6rem 0.875rem', + padding: '0.7rem 0.875rem', fontSize: '0.84rem', fontWeight: isActive ? 600 : 400, color: isActive ? 'var(--accent)' : 'var(--ink-light)', @@ -221,7 +221,7 @@ export default function Layout() {

© {new Date().getFullYear()} Leopost

-
+
{[{ to: '/privacy', label: 'Privacy' }, { to: '/termini', label: 'Termini' }, { to: '/cookie', label: 'Cookie' }].map(({ to, label }) => (