feat(phase-3): Complete AI content generation system
- Add /api/generate endpoints for AI content generation - Integrate DeepSeek AI into dashboard compose page - Add content templates for tips, products, services, engagement - Implement batch generation for weekly/monthly calendars - Add cost estimation endpoint New endpoints: - POST /api/generate/tip - Generate tech tips - POST /api/generate/product - Generate product posts - POST /api/generate/service - Generate service posts - POST /api/generate/thread - Generate educational threads - POST /api/generate/response - Generate response suggestions - POST /api/generate/adapt - Adapt content to platform - POST /api/generate/improve - Improve existing content - POST /api/generate/batch - Batch generate for calendar - GET /api/generate/batch/estimate - Estimate batch costs - GET /api/generate/status - Check AI connection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
551
app/api/routes/generate.py
Normal file
551
app/api/routes/generate.py
Normal file
@@ -0,0 +1,551 @@
|
||||
"""
|
||||
API endpoints para generación de contenido con IA.
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.content_generator import content_generator
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Schemas
|
||||
# ============================================================
|
||||
|
||||
class GenerateTipRequest(BaseModel):
|
||||
"""Solicitud para generar tip tech."""
|
||||
category: str = "general"
|
||||
platform: str = "x"
|
||||
template: Optional[str] = None
|
||||
|
||||
|
||||
class GenerateProductPostRequest(BaseModel):
|
||||
"""Solicitud para generar post de producto."""
|
||||
product: dict # name, description, price, category, specs, highlights
|
||||
platform: str = "x"
|
||||
|
||||
|
||||
class GenerateServicePostRequest(BaseModel):
|
||||
"""Solicitud para generar post de servicio."""
|
||||
service: dict # name, description, category, target_sectors, benefits, call_to_action
|
||||
platform: str = "x"
|
||||
|
||||
|
||||
class GenerateThreadRequest(BaseModel):
|
||||
"""Solicitud para generar hilo educativo."""
|
||||
topic: str
|
||||
num_posts: int = 5
|
||||
|
||||
|
||||
class GenerateResponseRequest(BaseModel):
|
||||
"""Solicitud para generar sugerencias de respuesta."""
|
||||
interaction_content: str
|
||||
interaction_type: str = "comment" # comment, mention, dm
|
||||
context: Optional[str] = None
|
||||
|
||||
|
||||
class AdaptContentRequest(BaseModel):
|
||||
"""Solicitud para adaptar contenido a plataforma."""
|
||||
content: str
|
||||
target_platform: str
|
||||
|
||||
|
||||
class ImproveContentRequest(BaseModel):
|
||||
"""Solicitud para mejorar contenido existente."""
|
||||
content: str
|
||||
platform: str = "x"
|
||||
style: str = "engaging" # engaging, professional, casual, educational
|
||||
|
||||
|
||||
class GenerateResponse(BaseModel):
|
||||
"""Respuesta de generación."""
|
||||
success: bool
|
||||
content: Optional[str] = None
|
||||
contents: Optional[List[str]] = None
|
||||
error: Optional[str] = None
|
||||
tokens_used: Optional[int] = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helper
|
||||
# ============================================================
|
||||
|
||||
def check_api_configured():
|
||||
"""Verificar que la API de DeepSeek esté configurada."""
|
||||
if not settings.DEEPSEEK_API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="DeepSeek API no configurada. Agrega DEEPSEEK_API_KEY en .env"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Endpoints
|
||||
# ============================================================
|
||||
|
||||
@router.post("/tip", response_model=GenerateResponse)
|
||||
async def generate_tip(request: GenerateTipRequest):
|
||||
"""
|
||||
Generar un tip de tecnología.
|
||||
|
||||
Categorías sugeridas:
|
||||
- productividad, seguridad, ia, programacion, hardware,
|
||||
- redes, cloud, automatizacion, impresion3d, general
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
try:
|
||||
content = await content_generator.generate_tip_tech(
|
||||
category=request.category,
|
||||
platform=request.platform,
|
||||
template=request.template
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
content=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/product", response_model=GenerateResponse)
|
||||
async def generate_product_post(request: GenerateProductPostRequest):
|
||||
"""
|
||||
Generar post promocional de producto.
|
||||
|
||||
Campos del producto:
|
||||
- name (requerido): Nombre del producto
|
||||
- description: Descripción
|
||||
- price (requerido): Precio en MXN
|
||||
- category: Categoría (computadoras, impresoras3d, accesorios)
|
||||
- specs: Dict con especificaciones técnicas
|
||||
- highlights: Lista de puntos destacados
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
# Validar campos mínimos
|
||||
if "name" not in request.product or "price" not in request.product:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="El producto debe tener 'name' y 'price'"
|
||||
)
|
||||
|
||||
try:
|
||||
content = await content_generator.generate_product_post(
|
||||
product=request.product,
|
||||
platform=request.platform
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
content=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/service", response_model=GenerateResponse)
|
||||
async def generate_service_post(request: GenerateServicePostRequest):
|
||||
"""
|
||||
Generar post promocional de servicio.
|
||||
|
||||
Campos del servicio:
|
||||
- name (requerido): Nombre del servicio
|
||||
- description: Descripción
|
||||
- category: Categoría (consultoria, desarrollo, soporte, capacitacion)
|
||||
- target_sectors: Lista de sectores objetivo
|
||||
- benefits: Lista de beneficios
|
||||
- call_to_action: CTA personalizado
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
if "name" not in request.service:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="El servicio debe tener 'name'"
|
||||
)
|
||||
|
||||
try:
|
||||
content = await content_generator.generate_service_post(
|
||||
service=request.service,
|
||||
platform=request.platform
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
content=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/thread", response_model=GenerateResponse)
|
||||
async def generate_thread(request: GenerateThreadRequest):
|
||||
"""
|
||||
Generar hilo educativo.
|
||||
|
||||
- topic: Tema del hilo (ej: "5 tips para proteger tu PC")
|
||||
- num_posts: Cantidad de posts en el hilo (3-10)
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
if request.num_posts < 3 or request.num_posts > 10:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="num_posts debe estar entre 3 y 10"
|
||||
)
|
||||
|
||||
try:
|
||||
posts = await content_generator.generate_thread(
|
||||
topic=request.topic,
|
||||
num_posts=request.num_posts
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
contents=posts
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/response", response_model=GenerateResponse)
|
||||
async def generate_response_suggestions(request: GenerateResponseRequest):
|
||||
"""
|
||||
Generar sugerencias de respuesta para una interacción.
|
||||
|
||||
Devuelve 3 opciones de respuesta diferentes.
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
try:
|
||||
suggestions = await content_generator.generate_response_suggestion(
|
||||
interaction_content=request.interaction_content,
|
||||
interaction_type=request.interaction_type,
|
||||
context=request.context
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
contents=suggestions
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/adapt", response_model=GenerateResponse)
|
||||
async def adapt_content(request: AdaptContentRequest):
|
||||
"""
|
||||
Adaptar contenido existente a una plataforma específica.
|
||||
|
||||
Útil para tomar contenido de una plataforma y adaptarlo
|
||||
a los límites y estilo de otra.
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
valid_platforms = ["x", "threads", "instagram", "facebook"]
|
||||
if request.target_platform.lower() not in valid_platforms:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Plataforma no válida. Opciones: {valid_platforms}"
|
||||
)
|
||||
|
||||
try:
|
||||
content = await content_generator.adapt_content_for_platform(
|
||||
content=request.content,
|
||||
target_platform=request.target_platform.lower()
|
||||
)
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
content=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/improve", response_model=GenerateResponse)
|
||||
async def improve_content(request: ImproveContentRequest):
|
||||
"""
|
||||
Mejorar contenido existente.
|
||||
|
||||
Estilos disponibles:
|
||||
- engaging: Más atractivo y con gancho
|
||||
- professional: Más formal y corporativo
|
||||
- casual: Más cercano y amigable
|
||||
- educational: Más informativo y didáctico
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
try:
|
||||
# Usar el generador para mejorar
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=settings.DEEPSEEK_API_KEY,
|
||||
base_url=settings.DEEPSEEK_BASE_URL
|
||||
)
|
||||
|
||||
style_prompts = {
|
||||
"engaging": "Hazlo más atractivo, con un gancho inicial que capture atención",
|
||||
"professional": "Hazlo más profesional y corporativo, manteniendo credibilidad",
|
||||
"casual": "Hazlo más cercano y amigable, como si hablaras con un amigo",
|
||||
"educational": "Hazlo más informativo y didáctico, que enseñe algo útil"
|
||||
}
|
||||
|
||||
char_limits = {
|
||||
"x": 280,
|
||||
"threads": 500,
|
||||
"instagram": 2200,
|
||||
"facebook": 1000
|
||||
}
|
||||
|
||||
prompt = f"""Mejora este contenido para {request.platform}:
|
||||
|
||||
CONTENIDO ORIGINAL:
|
||||
{request.content}
|
||||
|
||||
ESTILO DESEADO: {style_prompts.get(request.style, style_prompts['engaging'])}
|
||||
|
||||
LÍMITE DE CARACTERES: {char_limits.get(request.platform, 500)}
|
||||
|
||||
REQUISITOS:
|
||||
- Mantén el mensaje principal
|
||||
- Mejora la redacción y el impacto
|
||||
- Incluye emojis relevantes
|
||||
- Termina con hashtags apropiados
|
||||
|
||||
Responde SOLO con el contenido mejorado."""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=[
|
||||
{"role": "system", "content": content_generator._get_system_prompt()},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
max_tokens=400,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
improved = response.choices[0].message.content.strip()
|
||||
|
||||
return GenerateResponse(
|
||||
success=True,
|
||||
content=improved,
|
||||
tokens_used=response.usage.total_tokens
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return GenerateResponse(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_tip_categories():
|
||||
"""Obtener categorías disponibles para tips."""
|
||||
return {
|
||||
"categories": [
|
||||
{"id": "productividad", "name": "Productividad", "description": "Tips para ser más eficiente"},
|
||||
{"id": "seguridad", "name": "Seguridad", "description": "Ciberseguridad y protección"},
|
||||
{"id": "ia", "name": "Inteligencia Artificial", "description": "IA y machine learning"},
|
||||
{"id": "programacion", "name": "Programación", "description": "Desarrollo de software"},
|
||||
{"id": "hardware", "name": "Hardware", "description": "Equipos y componentes"},
|
||||
{"id": "redes", "name": "Redes", "description": "Networking y conectividad"},
|
||||
{"id": "cloud", "name": "Cloud", "description": "Servicios en la nube"},
|
||||
{"id": "automatizacion", "name": "Automatización", "description": "Automatizar procesos"},
|
||||
{"id": "impresion3d", "name": "Impresión 3D", "description": "Impresoras y diseño 3D"},
|
||||
{"id": "general", "name": "General", "description": "Tips variados de tecnología"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class BatchGenerateRequest(BaseModel):
|
||||
"""Solicitud de generación por lotes."""
|
||||
platforms: List[str]
|
||||
days: int = 7 # 7 para semana, 30 para mes
|
||||
tip_categories: Optional[List[str]] = None
|
||||
|
||||
|
||||
@router.post("/batch")
|
||||
async def generate_batch(request: BatchGenerateRequest):
|
||||
"""
|
||||
Generar contenido por lotes para el calendario.
|
||||
|
||||
- **platforms**: Lista de plataformas ["x", "threads", "instagram"]
|
||||
- **days**: Cantidad de días (7 para semana, 30 para mes)
|
||||
- **tip_categories**: Categorías de tips a usar
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
if request.days < 1 or request.days > 30:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="days debe estar entre 1 y 30"
|
||||
)
|
||||
|
||||
from app.services.batch_generator import batch_generator
|
||||
|
||||
try:
|
||||
result = await batch_generator._generate_batch(
|
||||
platforms=request.platforms,
|
||||
start_date=None, # Usa mañana por defecto
|
||||
days=request.days,
|
||||
tip_categories=request.tip_categories
|
||||
)
|
||||
|
||||
return {
|
||||
"success": result.success,
|
||||
"total_requested": result.total_requested,
|
||||
"total_generated": result.total_generated,
|
||||
"posts": [
|
||||
{
|
||||
"content": p.content,
|
||||
"content_type": p.content_type.value,
|
||||
"platform": p.platform,
|
||||
"scheduled_at": p.scheduled_at.isoformat(),
|
||||
"metadata": p.metadata
|
||||
}
|
||||
for p in result.posts
|
||||
],
|
||||
"errors": result.errors
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/batch/estimate")
|
||||
async def estimate_batch_cost(
|
||||
platforms: str, # comma-separated
|
||||
days: int = 7
|
||||
):
|
||||
"""
|
||||
Estimar costo de generación por lotes.
|
||||
|
||||
Calcula tokens y costo estimado sin generar contenido.
|
||||
"""
|
||||
from app.services.batch_generator import batch_generator
|
||||
|
||||
platform_list = [p.strip() for p in platforms.split(",")]
|
||||
estimate = batch_generator.calculate_costs(platform_list, days)
|
||||
|
||||
return estimate
|
||||
|
||||
|
||||
@router.post("/tips/batch")
|
||||
async def generate_tips_batch(
|
||||
count: int = 10,
|
||||
platform: str = "x",
|
||||
categories: Optional[str] = None # comma-separated
|
||||
):
|
||||
"""
|
||||
Generar múltiples tips de una vez.
|
||||
|
||||
- **count**: Cantidad de tips (1-20)
|
||||
- **platform**: Plataforma destino
|
||||
- **categories**: Categorías separadas por coma
|
||||
"""
|
||||
check_api_configured()
|
||||
|
||||
if count < 1 or count > 20:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="count debe estar entre 1 y 20"
|
||||
)
|
||||
|
||||
from app.services.batch_generator import batch_generator
|
||||
|
||||
category_list = None
|
||||
if categories:
|
||||
category_list = [c.strip() for c in categories.split(",")]
|
||||
|
||||
try:
|
||||
tips = await batch_generator.generate_tips_batch(
|
||||
count=count,
|
||||
platform=platform,
|
||||
categories=category_list
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(tips),
|
||||
"tips": tips
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"tips": []
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_ai_status():
|
||||
"""Verificar estado de la API de IA."""
|
||||
configured = bool(settings.DEEPSEEK_API_KEY)
|
||||
|
||||
result = {
|
||||
"configured": configured,
|
||||
"provider": "DeepSeek",
|
||||
"model": "deepseek-chat",
|
||||
"base_url": settings.DEEPSEEK_BASE_URL
|
||||
}
|
||||
|
||||
if configured:
|
||||
try:
|
||||
# Test rápido
|
||||
from openai import OpenAI
|
||||
client = OpenAI(
|
||||
api_key=settings.DEEPSEEK_API_KEY,
|
||||
base_url=settings.DEEPSEEK_BASE_URL
|
||||
)
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "test"}],
|
||||
max_tokens=5
|
||||
)
|
||||
result["status"] = "connected"
|
||||
result["test"] = "OK"
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["error"] = str(e)
|
||||
else:
|
||||
result["status"] = "not_configured"
|
||||
|
||||
return result
|
||||
31
app/data/__init__.py
Normal file
31
app/data/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Data module - templates and static data for content generation.
|
||||
"""
|
||||
|
||||
from app.data.content_templates import (
|
||||
TIP_TEMPLATES,
|
||||
PRODUCT_TEMPLATES,
|
||||
SERVICE_TEMPLATES,
|
||||
ENGAGEMENT_TEMPLATES,
|
||||
THREAD_TEMPLATES,
|
||||
OPTIMAL_POSTING_TIMES,
|
||||
get_tip_template,
|
||||
get_product_template,
|
||||
get_service_template,
|
||||
get_engagement_template,
|
||||
get_optimal_times,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TIP_TEMPLATES",
|
||||
"PRODUCT_TEMPLATES",
|
||||
"SERVICE_TEMPLATES",
|
||||
"ENGAGEMENT_TEMPLATES",
|
||||
"THREAD_TEMPLATES",
|
||||
"OPTIMAL_POSTING_TIMES",
|
||||
"get_tip_template",
|
||||
"get_product_template",
|
||||
"get_service_template",
|
||||
"get_engagement_template",
|
||||
"get_optimal_times",
|
||||
]
|
||||
269
app/data/content_templates.py
Normal file
269
app/data/content_templates.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Plantillas predefinidas para generación de contenido.
|
||||
Estas plantillas guían a la IA para generar contenido consistente.
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# TIPS TECH
|
||||
# ============================================================
|
||||
|
||||
TIP_TEMPLATES = {
|
||||
"productividad": [
|
||||
"Tip de productividad: [herramienta/técnica] puede ahorrarte [tiempo/esfuerzo] al [tarea]. #Productividad",
|
||||
"¿Sabías que [atajo/función] en [app/sistema] puede [beneficio]? #TipTech",
|
||||
"3 segundos que te ahorran 30 minutos: [tip rápido]. #Eficiencia",
|
||||
],
|
||||
"seguridad": [
|
||||
"Tip de seguridad: [acción] para proteger [recurso]. Nunca [error común]. #Ciberseguridad",
|
||||
"¿Tu [dispositivo/cuenta] está protegido? Verifica que tengas [medida de seguridad]. #SeguridadDigital",
|
||||
"Error común de seguridad: [error]. Solución: [solución]. #InfoSec",
|
||||
],
|
||||
"ia": [
|
||||
"Prompt tip: Para mejores resultados con IA, [técnica]. #IAparaNegocios",
|
||||
"Herramienta IA del día: [herramienta] te ayuda a [beneficio]. #InteligenciaArtificial",
|
||||
"Cómo uso IA para [tarea]: [paso breve]. #ProductividadIA",
|
||||
],
|
||||
"programacion": [
|
||||
"Code tip: [técnica/patrón] mejora [aspecto] en tu código. #DevTips",
|
||||
"Atajo en [IDE/lenguaje]: [atajo] → [resultado]. #ProgrammingTips",
|
||||
"Evita este error común en [tecnología]: [error] → [solución]. #CodeQuality",
|
||||
],
|
||||
"hardware": [
|
||||
"Tip de hardware: [acción] para [beneficio] en tu [equipo]. #TechTips",
|
||||
"¿Tu PC está lenta? Prueba [solución]. #Mantenimiento",
|
||||
"Antes de comprar [componente], verifica [aspecto]. #CompraTech",
|
||||
],
|
||||
"impresion3d": [
|
||||
"Tip 3D: [configuración/técnica] mejora [aspecto] de tus impresiones. #Impresion3D",
|
||||
"Material del día: [filamento] es ideal para [uso]. #3DPrinting",
|
||||
"Error común en 3D: [problema] → Solución: [solución]. #Makers",
|
||||
],
|
||||
"general": [
|
||||
"Tip tech del día: [consejo general]. #Tecnologia",
|
||||
"¿Conocías este truco? [tip]. #TechTips",
|
||||
"Tecnología que facilita tu día: [herramienta/técnica]. #Tech",
|
||||
]
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PRODUCTOS
|
||||
# ============================================================
|
||||
|
||||
PRODUCT_TEMPLATES = {
|
||||
"computadoras": """
|
||||
🖥️ {name}
|
||||
|
||||
{highlight_1}
|
||||
{highlight_2}
|
||||
{highlight_3}
|
||||
|
||||
💰 ${price:,.0f} MXN
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#Computadoras #Tech #Tijuana
|
||||
""",
|
||||
"laptops": """
|
||||
💻 {name}
|
||||
|
||||
✅ {highlight_1}
|
||||
✅ {highlight_2}
|
||||
✅ {highlight_3}
|
||||
|
||||
Precio: ${price:,.0f} MXN
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#Laptops #Portátiles #Tech
|
||||
""",
|
||||
"impresoras3d": """
|
||||
🔧 {name}
|
||||
|
||||
Lo que puedes crear:
|
||||
• Prototipos
|
||||
• Piezas personalizadas
|
||||
• Figuras y modelos
|
||||
|
||||
{specs_summary}
|
||||
|
||||
💰 ${price:,.0f} MXN
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#Impresion3D #3DPrinting #Makers
|
||||
""",
|
||||
"accesorios": """
|
||||
{emoji} {name}
|
||||
|
||||
{description}
|
||||
|
||||
${price:,.0f} MXN
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#Accesorios #Tech
|
||||
""",
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# SERVICIOS
|
||||
# ============================================================
|
||||
|
||||
SERVICE_TEMPLATES = {
|
||||
"consultoria": """
|
||||
🎯 {name}
|
||||
|
||||
¿{pain_point}?
|
||||
|
||||
Te ayudamos a:
|
||||
✅ {benefit_1}
|
||||
✅ {benefit_2}
|
||||
✅ {benefit_3}
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#ConsultoríaTech #TransformaciónDigital
|
||||
""",
|
||||
"desarrollo": """
|
||||
💻 {name}
|
||||
|
||||
Desarrollamos soluciones a medida:
|
||||
• {feature_1}
|
||||
• {feature_2}
|
||||
• {feature_3}
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#DesarrolloSoftware #Automatización
|
||||
""",
|
||||
"soporte": """
|
||||
🛠️ {name}
|
||||
|
||||
Problemas técnicos resueltos:
|
||||
✓ {issue_1}
|
||||
✓ {issue_2}
|
||||
✓ {issue_3}
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#SoporteTécnico #IT #Tijuana
|
||||
""",
|
||||
"capacitacion": """
|
||||
📚 {name}
|
||||
|
||||
Aprende:
|
||||
🎓 {topic_1}
|
||||
🎓 {topic_2}
|
||||
🎓 {topic_3}
|
||||
|
||||
{call_to_action}
|
||||
|
||||
#Capacitación #TechTraining
|
||||
""",
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# ENGAGEMENT
|
||||
# ============================================================
|
||||
|
||||
ENGAGEMENT_TEMPLATES = {
|
||||
"pregunta": [
|
||||
"🤔 ¿Cuál es tu mayor reto con [tema]? Cuéntanos en los comentarios.",
|
||||
"Pregunta del día: ¿[pregunta relevante]? 👇",
|
||||
"Si pudieras automatizar UNA tarea de tu trabajo, ¿cuál sería? 🤖",
|
||||
],
|
||||
"encuesta": [
|
||||
"¿Qué prefieres?\n\n🅰️ [Opción A]\n🅱️ [Opción B]\n\nResponde con el emoji.",
|
||||
"Tu herramienta favorita para [tarea]:\n\n1️⃣ [Op1]\n2️⃣ [Op2]\n3️⃣ [Op3]\n4️⃣ Otra (comenta)",
|
||||
],
|
||||
"behind_scenes": [
|
||||
"Un día normal en @ConsultoriaAS: [actividad]. ¿Qué les gustaría ver? 👀",
|
||||
"Trabajando en [proyecto]. Pronto más detalles... 🔜",
|
||||
],
|
||||
"celebracion": [
|
||||
"🎉 [Logro/Milestone]. ¡Gracias a todos los que confían en nosotros!",
|
||||
"Otro [proyecto/cliente] satisfecho. Esto es lo que nos motiva 💪",
|
||||
],
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HILOS EDUCATIVOS
|
||||
# ============================================================
|
||||
|
||||
THREAD_TEMPLATES = {
|
||||
"tutorial": {
|
||||
"hook": "🧵 [Título llamativo en forma de promesa]\n\nEn este hilo te explico paso a paso 👇",
|
||||
"content_format": "{number}/ {step_title}\n\n{explanation}",
|
||||
"closing": "📌 Guarda este hilo para cuando lo necesites.\n\n¿Te fue útil? RT para que llegue a más personas.\n\n#[hashtag1] #[hashtag2]"
|
||||
},
|
||||
"tips_list": {
|
||||
"hook": "🧵 {count} tips de {topic} que ojalá hubiera sabido antes:\n\n👇",
|
||||
"content_format": "{number}/ {tip}",
|
||||
"closing": "¿Cuál agregarías tú?\n\nSíguenos para más tips como estos.\n\n#[hashtag1] #[hashtag2]"
|
||||
},
|
||||
"comparison": {
|
||||
"hook": "🧵 {option_a} vs {option_b}: ¿Cuál elegir?\n\nTe lo explico 👇",
|
||||
"content_format": "{number}/ {aspect}:\n\n{option_a}: {value_a}\n{option_b}: {value_b}",
|
||||
"closing": "Mi recomendación: {recommendation}\n\n¿Con cuál te quedas?\n\n#[hashtag1] #[hashtag2]"
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HORARIOS ÓPTIMOS
|
||||
# ============================================================
|
||||
|
||||
OPTIMAL_POSTING_TIMES = {
|
||||
"x": {
|
||||
"weekday": ["09:00", "12:00", "17:00", "20:00"],
|
||||
"weekend": ["10:00", "14:00", "19:00"],
|
||||
"best": "12:00"
|
||||
},
|
||||
"threads": {
|
||||
"weekday": ["08:00", "12:00", "18:00"],
|
||||
"weekend": ["10:00", "15:00"],
|
||||
"best": "18:00"
|
||||
},
|
||||
"instagram": {
|
||||
"weekday": ["11:00", "14:00", "19:00"],
|
||||
"weekend": ["10:00", "14:00", "20:00"],
|
||||
"best": "19:00"
|
||||
},
|
||||
"facebook": {
|
||||
"weekday": ["09:00", "13:00", "16:00"],
|
||||
"weekend": ["12:00", "15:00"],
|
||||
"best": "13:00"
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HELPERS
|
||||
# ============================================================
|
||||
|
||||
def get_tip_template(category: str) -> str:
|
||||
"""Obtener plantilla aleatoria para un tip."""
|
||||
import random
|
||||
templates = TIP_TEMPLATES.get(category, TIP_TEMPLATES["general"])
|
||||
return random.choice(templates)
|
||||
|
||||
|
||||
def get_product_template(category: str) -> str:
|
||||
"""Obtener plantilla para producto."""
|
||||
return PRODUCT_TEMPLATES.get(category, PRODUCT_TEMPLATES["accesorios"])
|
||||
|
||||
|
||||
def get_service_template(category: str) -> str:
|
||||
"""Obtener plantilla para servicio."""
|
||||
return SERVICE_TEMPLATES.get(category, SERVICE_TEMPLATES["consultoria"])
|
||||
|
||||
|
||||
def get_engagement_template(template_type: str) -> str:
|
||||
"""Obtener plantilla de engagement."""
|
||||
import random
|
||||
templates = ENGAGEMENT_TEMPLATES.get(template_type, ENGAGEMENT_TEMPLATES["pregunta"])
|
||||
return random.choice(templates)
|
||||
|
||||
|
||||
def get_optimal_times(platform: str, is_weekend: bool = False) -> list:
|
||||
"""Obtener horarios óptimos para publicar."""
|
||||
times = OPTIMAL_POSTING_TIMES.get(platform, OPTIMAL_POSTING_TIMES["x"])
|
||||
return times["weekend"] if is_weekend else times["weekday"]
|
||||
@@ -11,7 +11,7 @@ from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish
|
||||
from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish, generate
|
||||
from app.core.config import settings
|
||||
from app.core.database import engine
|
||||
from app.models import Base
|
||||
@@ -63,6 +63,7 @@ app.include_router(services.router, prefix="/api/services", tags=["Services"])
|
||||
app.include_router(calendar.router, prefix="/api/calendar", tags=["Calendar"])
|
||||
app.include_router(interactions.router, prefix="/api/interactions", tags=["Interactions"])
|
||||
app.include_router(publish.router, prefix="/api/publish", tags=["Publish"])
|
||||
app.include_router(generate.router, prefix="/api/generate", tags=["AI Generation"])
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
|
||||
326
app/services/batch_generator.py
Normal file
326
app/services/batch_generator.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
Servicio de generación de contenido por lotes.
|
||||
Genera múltiples posts para el calendario de contenido.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from app.services.content_generator import content_generator
|
||||
from app.data.content_templates import (
|
||||
get_tip_template,
|
||||
get_optimal_times,
|
||||
OPTIMAL_POSTING_TIMES
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class ContentType(str, Enum):
|
||||
TIP = "tip"
|
||||
PRODUCT = "product"
|
||||
SERVICE = "service"
|
||||
ENGAGEMENT = "engagement"
|
||||
THREAD = "thread"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratedPost:
|
||||
"""Post generado para el calendario."""
|
||||
content: str
|
||||
content_type: ContentType
|
||||
platform: str
|
||||
scheduled_at: datetime
|
||||
metadata: Dict = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BatchResult:
|
||||
"""Resultado de generación por lotes."""
|
||||
success: bool
|
||||
posts: List[GeneratedPost]
|
||||
errors: List[str]
|
||||
total_requested: int
|
||||
total_generated: int
|
||||
|
||||
|
||||
class BatchGenerator:
|
||||
"""Generador de contenido por lotes."""
|
||||
|
||||
# Distribución de contenido por defecto (porcentaje)
|
||||
DEFAULT_DISTRIBUTION = {
|
||||
ContentType.TIP: 60,
|
||||
ContentType.PRODUCT: 20,
|
||||
ContentType.SERVICE: 15,
|
||||
ContentType.ENGAGEMENT: 5,
|
||||
}
|
||||
|
||||
# Frecuencia por plataforma (posts por día)
|
||||
DEFAULT_FREQUENCY = {
|
||||
"x": 4,
|
||||
"threads": 3,
|
||||
"instagram": 2,
|
||||
"facebook": 1,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.generator = content_generator
|
||||
|
||||
async def generate_week(
|
||||
self,
|
||||
platforms: List[str],
|
||||
start_date: Optional[datetime] = None,
|
||||
distribution: Optional[Dict[ContentType, int]] = None,
|
||||
frequency: Optional[Dict[str, int]] = None,
|
||||
tip_categories: Optional[List[str]] = None,
|
||||
) -> BatchResult:
|
||||
"""
|
||||
Generar contenido para una semana.
|
||||
|
||||
Args:
|
||||
platforms: Lista de plataformas
|
||||
start_date: Fecha de inicio (default: mañana)
|
||||
distribution: Distribución de tipos de contenido
|
||||
frequency: Posts por día por plataforma
|
||||
tip_categories: Categorías para tips
|
||||
|
||||
Returns:
|
||||
BatchResult con posts generados
|
||||
"""
|
||||
if start_date is None:
|
||||
start_date = datetime.now().replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
) + timedelta(days=1)
|
||||
|
||||
return await self._generate_batch(
|
||||
platforms=platforms,
|
||||
start_date=start_date,
|
||||
days=7,
|
||||
distribution=distribution,
|
||||
frequency=frequency,
|
||||
tip_categories=tip_categories
|
||||
)
|
||||
|
||||
async def generate_month(
|
||||
self,
|
||||
platforms: List[str],
|
||||
start_date: Optional[datetime] = None,
|
||||
distribution: Optional[Dict[ContentType, int]] = None,
|
||||
frequency: Optional[Dict[str, int]] = None,
|
||||
tip_categories: Optional[List[str]] = None,
|
||||
) -> BatchResult:
|
||||
"""Generar contenido para un mes (30 días)."""
|
||||
if start_date is None:
|
||||
start_date = datetime.now().replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
) + timedelta(days=1)
|
||||
|
||||
return await self._generate_batch(
|
||||
platforms=platforms,
|
||||
start_date=start_date,
|
||||
days=30,
|
||||
distribution=distribution,
|
||||
frequency=frequency,
|
||||
tip_categories=tip_categories
|
||||
)
|
||||
|
||||
async def _generate_batch(
|
||||
self,
|
||||
platforms: List[str],
|
||||
start_date: datetime,
|
||||
days: int,
|
||||
distribution: Optional[Dict[ContentType, int]] = None,
|
||||
frequency: Optional[Dict[str, int]] = None,
|
||||
tip_categories: Optional[List[str]] = None,
|
||||
) -> BatchResult:
|
||||
"""Generar lote de posts."""
|
||||
distribution = distribution or self.DEFAULT_DISTRIBUTION
|
||||
frequency = frequency or self.DEFAULT_FREQUENCY
|
||||
tip_categories = tip_categories or [
|
||||
"productividad", "seguridad", "ia", "programacion",
|
||||
"hardware", "impresion3d", "general"
|
||||
]
|
||||
|
||||
posts = []
|
||||
errors = []
|
||||
total_requested = 0
|
||||
|
||||
# Calcular posts necesarios por plataforma
|
||||
for platform in platforms:
|
||||
posts_per_day = frequency.get(platform, 2)
|
||||
total_posts = posts_per_day * days
|
||||
total_requested += total_posts
|
||||
|
||||
# Distribuir por tipo
|
||||
posts_by_type = {}
|
||||
for content_type, percentage in distribution.items():
|
||||
count = int(total_posts * percentage / 100)
|
||||
posts_by_type[content_type] = count
|
||||
|
||||
# Generar posts para cada tipo
|
||||
current_date = start_date
|
||||
day_counter = 0
|
||||
|
||||
for content_type, count in posts_by_type.items():
|
||||
for i in range(count):
|
||||
# Calcular fecha y hora
|
||||
day_offset = (i * days) // count
|
||||
post_date = start_date + timedelta(days=day_offset)
|
||||
is_weekend = post_date.weekday() >= 5
|
||||
|
||||
# Obtener hora óptima
|
||||
times = get_optimal_times(platform, is_weekend)
|
||||
time_index = i % len(times)
|
||||
hour, minute = map(int, times[time_index].split(":"))
|
||||
scheduled_at = post_date.replace(hour=hour, minute=minute)
|
||||
|
||||
try:
|
||||
# Generar contenido
|
||||
content = await self._generate_content(
|
||||
content_type=content_type,
|
||||
platform=platform,
|
||||
tip_categories=tip_categories
|
||||
)
|
||||
|
||||
if content:
|
||||
posts.append(GeneratedPost(
|
||||
content=content,
|
||||
content_type=content_type,
|
||||
platform=platform,
|
||||
scheduled_at=scheduled_at,
|
||||
metadata={
|
||||
"batch_generated": True,
|
||||
"category": tip_categories[i % len(tip_categories)]
|
||||
if content_type == ContentType.TIP else None
|
||||
}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"{platform}/{content_type}: {str(e)}")
|
||||
|
||||
return BatchResult(
|
||||
success=len(posts) > 0,
|
||||
posts=posts,
|
||||
errors=errors,
|
||||
total_requested=total_requested,
|
||||
total_generated=len(posts)
|
||||
)
|
||||
|
||||
async def _generate_content(
|
||||
self,
|
||||
content_type: ContentType,
|
||||
platform: str,
|
||||
tip_categories: List[str]
|
||||
) -> Optional[str]:
|
||||
"""Generar contenido individual."""
|
||||
import random
|
||||
|
||||
if content_type == ContentType.TIP:
|
||||
category = random.choice(tip_categories)
|
||||
template = get_tip_template(category)
|
||||
return await self.generator.generate_tip_tech(
|
||||
category=category,
|
||||
platform=platform,
|
||||
template=template
|
||||
)
|
||||
|
||||
elif content_type == ContentType.ENGAGEMENT:
|
||||
# Generar pregunta de engagement
|
||||
prompts = [
|
||||
"¿Cuál es tu mayor reto tecnológico esta semana?",
|
||||
"¿Qué herramienta de productividad no podrías dejar de usar?",
|
||||
"¿Prefieres trabajar en oficina o remoto? ¿Por qué?",
|
||||
"¿Qué tecnología te gustaría aprender este año?",
|
||||
"¿Cuál es tu consejo #1 para mantenerte productivo?",
|
||||
]
|
||||
return random.choice(prompts)
|
||||
|
||||
# Para productos y servicios, devolver None
|
||||
# (requieren datos específicos)
|
||||
return None
|
||||
|
||||
async def generate_tips_batch(
|
||||
self,
|
||||
count: int,
|
||||
platform: str,
|
||||
categories: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Generar múltiples tips de una vez.
|
||||
|
||||
Args:
|
||||
count: Cantidad de tips a generar
|
||||
platform: Plataforma destino
|
||||
categories: Categorías a usar (se rotarán)
|
||||
|
||||
Returns:
|
||||
Lista de tips generados
|
||||
"""
|
||||
categories = categories or [
|
||||
"productividad", "seguridad", "ia", "programacion",
|
||||
"hardware", "general"
|
||||
]
|
||||
|
||||
tips = []
|
||||
for i in range(count):
|
||||
category = categories[i % len(categories)]
|
||||
try:
|
||||
tip = await self.generator.generate_tip_tech(
|
||||
category=category,
|
||||
platform=platform
|
||||
)
|
||||
tips.append(tip)
|
||||
except Exception:
|
||||
pass # Ignorar errores individuales
|
||||
|
||||
# Pequeña pausa para no saturar la API
|
||||
if i < count - 1:
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return tips
|
||||
|
||||
def calculate_costs(
|
||||
self,
|
||||
platforms: List[str],
|
||||
days: int,
|
||||
frequency: Optional[Dict[str, int]] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Calcular costos estimados para un lote.
|
||||
|
||||
Returns:
|
||||
Dict con estimaciones de tokens y costos
|
||||
"""
|
||||
frequency = frequency or self.DEFAULT_FREQUENCY
|
||||
|
||||
total_posts = sum(
|
||||
frequency.get(p, 2) * days
|
||||
for p in platforms
|
||||
)
|
||||
|
||||
# Estimaciones basadas en análisis previo
|
||||
avg_input_tokens = 650
|
||||
avg_output_tokens = 250
|
||||
|
||||
total_input = total_posts * avg_input_tokens
|
||||
total_output = total_posts * avg_output_tokens
|
||||
|
||||
# Precios DeepSeek
|
||||
input_cost = total_input * 0.14 / 1_000_000
|
||||
output_cost = total_output * 0.28 / 1_000_000
|
||||
|
||||
return {
|
||||
"total_posts": total_posts,
|
||||
"estimated_input_tokens": total_input,
|
||||
"estimated_output_tokens": total_output,
|
||||
"estimated_cost_usd": round(input_cost + output_cost, 4),
|
||||
"platforms": platforms,
|
||||
"days": days,
|
||||
"frequency": frequency
|
||||
}
|
||||
|
||||
|
||||
# Instancia global
|
||||
batch_generator = BatchGenerator()
|
||||
@@ -107,23 +107,23 @@
|
||||
|
||||
<!-- AI Assist -->
|
||||
<div class="card p-6">
|
||||
<h3 class="font-semibold mb-4">Asistente IA</h3>
|
||||
<h3 class="font-semibold mb-4">Asistente IA (DeepSeek)</h3>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button type="button" onclick="generateTip()"
|
||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
||||
Generar Tip Tech
|
||||
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<span>Generar Tip Tech</span>
|
||||
</button>
|
||||
<button type="button" onclick="improveContent()"
|
||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
||||
Mejorar Texto
|
||||
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<span>Mejorar Texto</span>
|
||||
</button>
|
||||
<button type="button" onclick="adaptContent()"
|
||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
||||
Adaptar por Plataforma
|
||||
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<span>Adaptar por Plataforma</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mt-2">
|
||||
Usa IA para generar o mejorar el contenido
|
||||
<p id="ai-status" class="text-gray-500 text-sm mt-2">
|
||||
Verificando estado de IA...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -402,17 +402,183 @@
|
||||
}
|
||||
});
|
||||
|
||||
// AI functions (placeholder - will connect to backend)
|
||||
// AI functions
|
||||
let aiLoading = false;
|
||||
|
||||
function setAiLoading(loading) {
|
||||
aiLoading = loading;
|
||||
document.querySelectorAll('.ai-btn').forEach(btn => {
|
||||
btn.disabled = loading;
|
||||
if (loading) {
|
||||
btn.classList.add('opacity-50', 'cursor-wait');
|
||||
} else {
|
||||
btn.classList.remove('opacity-50', 'cursor-wait');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkAiStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/generate/status');
|
||||
const data = await response.json();
|
||||
return data.configured && data.status === 'connected';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateTip() {
|
||||
alert('Función de generación con IA - Requiere configurar DEEPSEEK_API_KEY');
|
||||
if (aiLoading) return;
|
||||
|
||||
// Show category selector
|
||||
const categories = [
|
||||
'productividad', 'seguridad', 'ia', 'programacion',
|
||||
'hardware', 'redes', 'cloud', 'automatizacion', 'impresion3d', 'general'
|
||||
];
|
||||
|
||||
const category = prompt(
|
||||
'Categoría del tip:\n\n' + categories.join(', ') + '\n\n(Enter para "general")'
|
||||
) || 'general';
|
||||
|
||||
const platform = selectedPlatforms[0] || 'x';
|
||||
|
||||
setAiLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate/tip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ category, platform })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('content').value = data.content;
|
||||
updateCharCount();
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'No se pudo generar'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error de conexión: ' + error.message);
|
||||
} finally {
|
||||
setAiLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function improveContent() {
|
||||
alert('Función de mejora con IA - Requiere configurar DEEPSEEK_API_KEY');
|
||||
if (aiLoading) return;
|
||||
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content.trim()) {
|
||||
alert('Primero escribe algo de contenido para mejorar');
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = ['engaging', 'professional', 'casual', 'educational'];
|
||||
const style = prompt(
|
||||
'Estilo de mejora:\n\n' +
|
||||
'- engaging: Más atractivo\n' +
|
||||
'- professional: Más formal\n' +
|
||||
'- casual: Más cercano\n' +
|
||||
'- educational: Más didáctico\n\n' +
|
||||
'(Enter para "engaging")'
|
||||
) || 'engaging';
|
||||
|
||||
const platform = selectedPlatforms[0] || 'x';
|
||||
|
||||
setAiLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate/improve', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, platform, style })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('content').value = data.content;
|
||||
updateCharCount();
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'No se pudo mejorar'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error de conexión: ' + error.message);
|
||||
} finally {
|
||||
setAiLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function adaptContent() {
|
||||
alert('Función de adaptación con IA - Requiere configurar DEEPSEEK_API_KEY');
|
||||
if (aiLoading) return;
|
||||
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content.trim()) {
|
||||
alert('Primero escribe algo de contenido para adaptar');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPlatforms.length < 2) {
|
||||
alert('Selecciona al menos 2 plataformas para adaptar el contenido');
|
||||
return;
|
||||
}
|
||||
|
||||
setAiLoading(true);
|
||||
|
||||
try {
|
||||
// Adaptar a cada plataforma excepto la primera (que es el contenido original)
|
||||
const adaptedContent = { [selectedPlatforms[0]]: content };
|
||||
|
||||
for (let i = 1; i < selectedPlatforms.length; i++) {
|
||||
const platform = selectedPlatforms[i];
|
||||
const response = await fetch('/api/generate/adapt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, target_platform: platform })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
adaptedContent[platform] = data.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar resultados en preview
|
||||
const previewDiv = document.getElementById('preview-content');
|
||||
let html = '<p class="text-gray-400 mb-4">Contenido adaptado por plataforma:</p>';
|
||||
|
||||
for (const [platform, text] of Object.entries(adaptedContent)) {
|
||||
html += `
|
||||
<div class="bg-gray-800 rounded-lg p-4 mb-2">
|
||||
<div class="font-semibold mb-2">${platform.toUpperCase()}</div>
|
||||
<p class="text-gray-300 whitespace-pre-wrap text-sm">${text}</p>
|
||||
<button onclick="useAdaptedContent('${platform}', this)"
|
||||
class="mt-2 text-amber-500 text-sm hover:underline"
|
||||
data-content="${encodeURIComponent(text)}">
|
||||
Usar este
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
previewDiv.innerHTML = html;
|
||||
document.getElementById('preview-modal').classList.remove('hidden');
|
||||
document.getElementById('preview-modal').classList.add('flex');
|
||||
|
||||
} catch (error) {
|
||||
alert('Error de conexión: ' + error.message);
|
||||
} finally {
|
||||
setAiLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function useAdaptedContent(platform, btn) {
|
||||
const content = decodeURIComponent(btn.dataset.content);
|
||||
document.getElementById('content').value = content;
|
||||
updateCharCount();
|
||||
closePreview();
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
@@ -423,7 +589,7 @@
|
||||
}
|
||||
|
||||
// Load draft on page load
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener('load', async () => {
|
||||
const draftContent = localStorage.getItem('draft_content');
|
||||
const draftPlatforms = localStorage.getItem('draft_platforms');
|
||||
|
||||
@@ -437,6 +603,23 @@
|
||||
}
|
||||
|
||||
updateCharCount();
|
||||
|
||||
// Check AI status
|
||||
const aiStatus = document.getElementById('ai-status');
|
||||
try {
|
||||
const response = await fetch('/api/generate/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.configured && data.status === 'connected') {
|
||||
aiStatus.innerHTML = '<span class="text-green-400">IA conectada y lista</span>';
|
||||
} else if (data.configured) {
|
||||
aiStatus.innerHTML = '<span class="text-yellow-400">IA configurada pero con error: ' + (data.error || 'desconocido') + '</span>';
|
||||
} else {
|
||||
aiStatus.innerHTML = '<span class="text-red-400">IA no configurada. Agrega DEEPSEEK_API_KEY en .env</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
aiStatus.innerHTML = '<span class="text-red-400">No se pudo verificar estado de IA</span>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user