Files
leopost-full/backend/app/schemas.py
Michele 743a6c1324 feat: group posts by batch in archive + fullscreen confirm modal
Backend:
- Add batch_id column to Post model (UUID, groups posts from same generation)
- Set batch_id in /generate endpoint for all posts in same request

Frontend:
- ContentArchive: group posts by batch_id into single cards with platform tabs
- Character name at top, platform tabs below, status badge, text preview
- Click platform tab to switch between variants of same content
- ConfirmModal: render via React portal to document.body for true fullscreen overlay
- Add box-shadow and higher z-index for better visual separation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:52:01 +02:00

333 lines
8.2 KiB
Python

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
# === Auth ===
class LoginRequest(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
user: Optional[dict] = None
# === Characters ===
class CharacterBase(BaseModel):
name: str
niche: str
topics: list[str] = []
tone: Optional[str] = None
visual_style: dict = {}
social_accounts: dict = {}
affiliate_links: list[dict] = []
avatar_url: Optional[str] = None
is_active: bool = True
class CharacterCreate(CharacterBase):
pass
class CharacterUpdate(BaseModel):
name: Optional[str] = None
niche: Optional[str] = None
topics: Optional[list[str]] = None
tone: Optional[str] = None
visual_style: Optional[dict] = None
social_accounts: Optional[dict] = None
affiliate_links: Optional[list[dict]] = None
avatar_url: Optional[str] = None
is_active: Optional[bool] = None
class CharacterResponse(CharacterBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Posts / Content ===
class PostCreate(BaseModel):
character_id: int
content_type: str = "text"
platform_hint: str = "instagram"
text_content: Optional[str] = None
hashtags: list[str] = []
image_url: Optional[str] = None
video_url: Optional[str] = None
media_urls: list[str] = []
affiliate_links_used: list[dict] = []
status: str = "draft"
class PostUpdate(BaseModel):
text_content: Optional[str] = None
hashtags: Optional[list[str]] = None
image_url: Optional[str] = None
video_url: Optional[str] = None
status: Optional[str] = None
affiliate_links_used: Optional[list[dict]] = None
class PostResponse(BaseModel):
id: int
batch_id: Optional[str] = None
character_id: int
content_type: str
text_content: Optional[str] = None
hashtags: list[str] = []
image_url: Optional[str] = None
video_url: Optional[str] = None
media_urls: list[str] = []
affiliate_links_used: list[dict] = []
llm_provider: Optional[str] = None
llm_model: Optional[str] = None
platform_hint: Optional[str] = None
status: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class GenerateContentRequest(BaseModel):
character_id: int
platform: str = "instagram" # legacy single-platform (kept for compat)
content_type: str = "text" # legacy single type (kept for compat)
platforms: List[str] = [] # new: multi-platform (overrides platform if non-empty)
content_types: List[str] = [] # new: multi-type (overrides content_type if non-empty)
topic_hint: Optional[str] = None
brief: Optional[str] = None # editorial brief: technique + instructions for the LLM
include_affiliates: bool = True
provider: Optional[str] = None
model: Optional[str] = None
@property
def effective_platform(self) -> str:
return self.platforms[0] if self.platforms else self.platform
@property
def effective_content_type(self) -> str:
return self.content_types[0] if self.content_types else self.content_type
class GenerateImageRequest(BaseModel):
character_id: int
prompt: Optional[str] = None # auto-generated if not provided
style_hint: Optional[str] = None
size: str = "1024x1024"
provider: Optional[str] = None # dalle, replicate
# === Affiliate Links ===
class AffiliateLinkBase(BaseModel):
character_id: Optional[int] = None
network: str
name: str
url: str
tag: Optional[str] = None
topics: list[str] = []
is_active: bool = True
class AffiliateLinkCreate(AffiliateLinkBase):
pass
class AffiliateLinkUpdate(BaseModel):
network: Optional[str] = None
name: Optional[str] = None
url: Optional[str] = None
tag: Optional[str] = None
topics: Optional[list[str]] = None
is_active: Optional[bool] = None
class AffiliateLinkResponse(AffiliateLinkBase):
id: int
click_count: int = 0
created_at: datetime
class Config:
from_attributes = True
# === Editorial Plans ===
class EditorialPlanBase(BaseModel):
character_id: int
name: str
frequency: str = "daily"
posts_per_day: int = 1
platforms: list[str] = []
content_types: list[str] = ["text"]
posting_times: list[str] = ["09:00"]
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_active: bool = False
class EditorialPlanCreate(EditorialPlanBase):
pass
class EditorialPlanUpdate(BaseModel):
name: Optional[str] = None
frequency: Optional[str] = None
posts_per_day: Optional[int] = None
platforms: Optional[list[str]] = None
content_types: Optional[list[str]] = None
posting_times: Optional[list[str]] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_active: Optional[bool] = None
class EditorialPlanResponse(EditorialPlanBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Scheduled Posts ===
class ScheduledPostCreate(BaseModel):
plan_id: Optional[int] = None
post_id: int
platform: str
scheduled_at: datetime
class ScheduledPostResponse(BaseModel):
id: int
plan_id: Optional[int] = None
post_id: int
platform: str
scheduled_at: datetime
published_at: Optional[datetime] = None
status: str
error_message: Optional[str] = None
external_post_id: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# === Social Accounts ===
class SocialAccountCreate(BaseModel):
character_id: int
platform: str
account_name: Optional[str] = None
account_id: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
page_id: Optional[str] = None
extra_data: dict = {}
class SocialAccountUpdate(BaseModel):
account_name: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
page_id: Optional[str] = None
extra_data: Optional[dict] = None
is_active: Optional[bool] = None
class SocialAccountResponse(BaseModel):
id: int
character_id: int
platform: str
account_name: Optional[str] = None
account_id: Optional[str] = None
page_id: Optional[str] = None
is_active: bool
token_expires_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
# === Comments ===
class CommentResponse(BaseModel):
id: int
scheduled_post_id: Optional[int] = None
platform: str
external_comment_id: Optional[str] = None
author_name: Optional[str] = None
content: Optional[str] = None
ai_suggested_reply: Optional[str] = None
approved_reply: Optional[str] = None
reply_status: str
replied_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
class CommentAction(BaseModel):
action: str # approve, edit, ignore
reply_text: Optional[str] = None # for edit action
# === System Settings ===
class SettingUpdate(BaseModel):
value: dict | str | list | int | bool | None
class SettingResponse(BaseModel):
key: str
value: dict | str | list | int | bool | None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Editorial Calendar (from PostGenerator) ===
class CalendarGenerateRequest(BaseModel):
topics: list[str]
format_narrativo: Optional[str] = None # PAS, AIDA, BAB, Storytelling, Listicle, Dato_Implicazione
awareness_level: Optional[int] = None # 1-5 (Schwartz levels)
num_posts: int = 7
start_date: Optional[str] = None # YYYY-MM-DD
character_id: Optional[int] = None
class CalendarSlotResponse(BaseModel):
indice: int
topic: str
formato_narrativo: str
awareness_level: int
awareness_label: str
data_pubblicazione: str
note: Optional[str] = None
class CalendarResponse(BaseModel):
slots: list[CalendarSlotResponse]
totale_post: int