diff --git a/backend/app/routers/content.py b/backend/app/routers/content.py
index f723a59..418e84a 100644
--- a/backend/app/routers/content.py
+++ b/backend/app/routers/content.py
@@ -45,14 +45,13 @@ def _get_setting(db: Session, key: str, user_id: int = None) -> str | None:
return setting.value
-@router.post("/generate", response_model=PostResponse)
+@router.post("/generate", response_model=list[PostResponse])
def generate_content(
request: GenerateContentRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
- """Generate content for a character using LLM."""
- # Validate character belongs to user
+ """Generate content for a character using LLM. One post per platform."""
character = (
db.query(Character)
.filter(Character.id == request.character_id, Character.user_id == current_user.id)
@@ -61,6 +60,9 @@ def generate_content(
if not character:
raise HTTPException(status_code=404, detail="Character not found")
+ # Determine platforms to generate for
+ platforms = request.platforms if request.platforms else [request.platform]
+
# Check monthly post limit
first_of_month = date.today().replace(day=1)
if current_user.posts_reset_date != first_of_month:
@@ -68,11 +70,20 @@ def generate_content(
current_user.posts_reset_date = first_of_month
db.commit()
- allowed, msg = check_limit(current_user, "posts_per_month", current_user.posts_generated_this_month or 0)
+ current_count = current_user.posts_generated_this_month or 0
+ allowed, msg = check_limit(current_user, "posts_per_month", current_count)
if not allowed:
raise HTTPException(status_code=403, detail={"message": msg, "upgrade_required": True})
- # Get LLM settings (user-specific first, then global)
+ # Also check if we have room for all platforms
+ allowed_after, msg_after = check_limit(current_user, "posts_per_month", current_count + len(platforms) - 1)
+ if not allowed_after:
+ raise HTTPException(status_code=403, detail={
+ "message": f"Non hai abbastanza post rimanenti per generare su {len(platforms)} piattaforme. {msg_after}",
+ "upgrade_required": True,
+ })
+
+ # Get LLM settings
provider_name = request.provider or _get_setting(db, "llm_provider", current_user.id)
api_key = _get_setting(db, "llm_api_key", current_user.id)
model = request.model or _get_setting(db, "llm_model", current_user.id)
@@ -94,7 +105,6 @@ def generate_content(
},
)
- # Build character dict for content service
char_dict = {
"name": character.name,
"niche": character.niche,
@@ -102,22 +112,11 @@ def generate_content(
"tone": character.tone or "professional",
}
- # Create LLM provider and generate text
base_url = _get_setting(db, "llm_base_url", current_user.id)
llm = get_llm_provider(provider_name, api_key, model, base_url=base_url)
- text = generate_post_text(
- character=char_dict,
- llm_provider=llm,
- platform=request.effective_platform,
- topic_hint=request.topic_hint,
- brief=request.brief,
- )
- # Generate hashtags
- hashtags = generate_hashtags(text, llm, request.effective_platform)
-
- # Handle affiliate links
- affiliate_links_used: list[dict] = []
+ # Preload affiliate links once
+ affiliate_link_dicts: list[dict] = []
if request.include_affiliates:
links = (
db.query(AffiliateLink)
@@ -128,39 +127,51 @@ def generate_content(
)
.all()
)
- if links:
- link_dicts = [
- {
- "url": link.url,
- "label": link.name,
- "keywords": link.topics or [],
- }
- for link in links
- ]
+ affiliate_link_dicts = [
+ {"url": link.url, "label": link.name, "keywords": link.topics or []}
+ for link in links
+ ]
+
+ # Generate one post per platform
+ posts_created: list[Post] = []
+ for platform in platforms:
+ text = generate_post_text(
+ character=char_dict,
+ llm_provider=llm,
+ platform=platform,
+ topic_hint=request.topic_hint,
+ brief=request.brief,
+ )
+
+ hashtags = generate_hashtags(text, llm, platform)
+
+ affiliate_links_used: list[dict] = []
+ if affiliate_link_dicts:
text, affiliate_links_used = inject_affiliate_links(
- text, link_dicts, character.topics or []
+ text, affiliate_link_dicts, character.topics or []
)
- # Create post record
- post = Post(
- character_id=character.id,
- user_id=current_user.id,
- content_type=request.content_type,
- text_content=text,
- hashtags=hashtags,
- affiliate_links_used=affiliate_links_used,
- llm_provider=provider_name,
- llm_model=model,
- platform_hint=request.platform,
- status="draft",
- )
- db.add(post)
+ post = Post(
+ character_id=character.id,
+ user_id=current_user.id,
+ content_type=request.content_type,
+ text_content=text,
+ hashtags=hashtags,
+ affiliate_links_used=affiliate_links_used,
+ llm_provider=provider_name,
+ llm_model=model,
+ platform_hint=platform,
+ status="draft",
+ )
+ db.add(post)
+ posts_created.append(post)
- # Increment monthly counter
- current_user.posts_generated_this_month = (current_user.posts_generated_this_month or 0) + 1
+ # Increment monthly counter by number of posts generated
+ current_user.posts_generated_this_month = current_count + len(platforms)
db.commit()
- db.refresh(post)
- return post
+ for post in posts_created:
+ db.refresh(post)
+ return posts_created
@router.post("/generate-image", response_model=PostResponse)
diff --git a/frontend/src/components/ConfirmModal.jsx b/frontend/src/components/ConfirmModal.jsx
new file mode 100644
index 0000000..25d41f1
--- /dev/null
+++ b/frontend/src/components/ConfirmModal.jsx
@@ -0,0 +1,51 @@
+import { useEffect } from 'react'
+
+export default function ConfirmModal({ open, title, message, confirmLabel = 'Conferma', cancelLabel = 'Annulla', confirmStyle = 'danger', onConfirm, onCancel }) {
+ useEffect(() => {
+ if (!open) return
+ const onKey = (e) => { if (e.key === 'Escape') onCancel() }
+ document.addEventListener('keydown', onKey)
+ return () => document.removeEventListener('keydown', onKey)
+ }, [open, onCancel])
+
+ if (!open) return null
+
+ const confirmColors = {
+ danger: { bg: 'var(--error)', color: 'white' },
+ primary: { bg: 'var(--ink)', color: 'white' },
+ success: { bg: 'var(--success)', color: 'white' },
+ }
+ const cc = confirmColors[confirmStyle] || confirmColors.danger
+
+ return (
+
+
+
+
+ {title}
+
+
+ {message}
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/ContentArchive.jsx b/frontend/src/components/ContentArchive.jsx
index b89eb79..219899e 100644
--- a/frontend/src/components/ContentArchive.jsx
+++ b/frontend/src/components/ContentArchive.jsx
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../api'
+import ConfirmModal from './ConfirmModal'
const statusLabels = { draft: 'Bozza', approved: 'Approvato', scheduled: 'Schedulato', published: 'Pubblicato' }
const statusColors = {
@@ -19,6 +20,7 @@ export default function ContentArchive() {
const [editText, setEditText] = useState('')
const [filterCharacter, setFilterCharacter] = useState('')
const [filterStatus, setFilterStatus] = useState('')
+ const [deleteTarget, setDeleteTarget] = useState(null)
useEffect(() => { loadData() }, [])
@@ -40,8 +42,7 @@ export default function ContentArchive() {
try { await api.post(`/content/posts/${postId}/approve`); loadData() } catch {}
}
const handleDelete = async (postId) => {
- if (!confirm('Eliminare questo contenuto?')) return
- try { await api.delete(`/content/posts/${postId}`); loadData() } catch {}
+ try { await api.delete(`/content/posts/${postId}`); setDeleteTarget(null); loadData() } catch {}
}
const handleSaveEdit = async (postId) => {
try {
@@ -171,7 +172,7 @@ export default function ContentArchive() {
)}
-