fix: Fix YAML syntax errors and validator prompt formatting
- Fix YAML files with unquoted strings containing quotes - Export singleton instances in ai/__init__.py - Fix validator scoring prompt to use replace() instead of format() to avoid conflicts with JSON curly braces Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,626 +1,348 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crear Post - Social Media Automation</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #1a1a2e; color: #eee; }
|
||||
.card { background-color: #16213e; border-radius: 12px; }
|
||||
.accent { color: #d4a574; }
|
||||
.btn-primary { background-color: #d4a574; color: #1a1a2e; }
|
||||
.btn-primary:hover { background-color: #c49564; }
|
||||
.btn-secondary { background-color: #374151; color: #fff; }
|
||||
.btn-secondary:hover { background-color: #4b5563; }
|
||||
textarea { background-color: #0f172a; border-color: #334155; }
|
||||
textarea:focus { border-color: #d4a574; outline: none; }
|
||||
.platform-btn { transition: all 0.2s; }
|
||||
.platform-btn.selected { ring: 2px; ring-color: #d4a574; }
|
||||
.char-warning { color: #fbbf24; }
|
||||
.char-error { color: #ef4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Crear Post{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="animate-fade-in">
|
||||
<!-- Header -->
|
||||
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">
|
||||
<span class="accent">Consultoría AS</span> - Social Media
|
||||
</h1>
|
||||
<nav class="flex gap-4">
|
||||
<a href="/dashboard" class="px-4 py-2 rounded hover:bg-gray-800">Home</a>
|
||||
<a href="/dashboard/compose" class="px-4 py-2 rounded bg-gray-800 accent">Crear Post</a>
|
||||
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
|
||||
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
|
||||
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold">Crear Publicación</h1>
|
||||
<p class="text-gray-400 mt-1">Genera y programa contenido para redes sociales</p>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-6 py-8 max-w-4xl">
|
||||
<h2 class="text-2xl font-bold mb-6">Crear Nueva Publicación</h2>
|
||||
|
||||
<form id="compose-form" class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Form -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Platform Selection -->
|
||||
<div class="card p-6">
|
||||
<h3 class="font-semibold mb-4">Plataformas</h3>
|
||||
<div class="flex gap-4">
|
||||
<button type="button" onclick="togglePlatform('x')"
|
||||
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2"
|
||||
id="btn-x">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
<span>X</span>
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>📱</span>
|
||||
<span>Plataformas</span>
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button type="button" onclick="togglePlatform('x')" id="btn-x"
|
||||
class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
|
||||
<span class="text-xl">𝕏</span>
|
||||
<span>Twitter/X</span>
|
||||
</button>
|
||||
<button type="button" onclick="togglePlatform('threads')"
|
||||
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2"
|
||||
id="btn-threads">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.96-.065-1.17.408-2.168 1.332-2.81.88-.612 2.104-.9 3.64-.857 1.016.028 1.96.132 2.828.31-.055-.792-.283-1.392-.683-1.786-.468-.46-1.166-.682-2.135-.682h-.07c-.834.019-1.548.26-2.063.7-.472.4-.768.945-.859 1.576l-2.028-.283c.142-.981.58-1.838 1.265-2.477.891-.829 2.092-1.27 3.474-1.274h.096c1.492.013 2.706.46 3.607 1.328.857.825 1.348 2.007 1.461 3.517.636.174 1.227.398 1.768.67 1.327.666 2.358 1.634 2.982 2.8.818 1.524.876 3.916-.935 5.686-1.818 1.779-4.16 2.606-7.378 2.606zm-.39-5.086c1.075-.055 1.834-.467 2.254-1.22.396-.71.583-1.745.558-3.078-.842-.156-1.73-.242-2.66-.258-1.927-.048-3.085.484-3.181 1.461-.064.648.222 1.27.806 1.753.618.512 1.42.785 2.223.785z"/>
|
||||
</svg>
|
||||
<span>Threads</span>
|
||||
</button>
|
||||
<button type="button" onclick="togglePlatform('facebook')"
|
||||
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2"
|
||||
id="btn-facebook">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
<button type="button" onclick="togglePlatform('facebook')" id="btn-facebook"
|
||||
class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
|
||||
<span class="text-xl">📘</span>
|
||||
<span>Facebook</span>
|
||||
</button>
|
||||
<button type="button" onclick="togglePlatform('instagram')"
|
||||
class="platform-btn px-6 py-3 rounded-lg bg-gray-800 hover:bg-gray-700 flex items-center gap-2"
|
||||
id="btn-instagram">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/>
|
||||
</svg>
|
||||
<button type="button" onclick="togglePlatform('instagram')" id="btn-instagram"
|
||||
class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
|
||||
<span class="text-xl">📸</span>
|
||||
<span>Instagram</span>
|
||||
</button>
|
||||
<button type="button" onclick="togglePlatform('threads')" id="btn-threads"
|
||||
class="platform-btn px-5 py-3 rounded-xl bg-dark-800 hover:bg-dark-700 flex items-center gap-2 transition-all border-2 border-transparent">
|
||||
<span class="text-xl">🧵</span>
|
||||
<span>Threads</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mt-2">Selecciona una o más plataformas</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="card p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold">Contenido</h3>
|
||||
<div id="char-counter" class="text-sm">
|
||||
<span id="char-count">0</span> caracteres
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>✍️</span>
|
||||
<span>Contenido</span>
|
||||
</h3>
|
||||
<textarea
|
||||
id="content"
|
||||
name="content"
|
||||
rows="6"
|
||||
class="w-full px-4 py-3 rounded-lg border resize-none"
|
||||
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-primary focus:outline-none resize-none transition-colors"
|
||||
placeholder="Escribe tu publicación aquí..."
|
||||
oninput="updateCharCount()"
|
||||
></textarea>
|
||||
|
||||
<!-- Platform limits -->
|
||||
<div id="platform-limits" class="mt-3 space-y-1 text-sm">
|
||||
<!-- Se llena dinámicamente -->
|
||||
<div class="flex justify-between items-center mt-3">
|
||||
<div id="char-count" class="text-sm text-gray-500">0 / 280</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" onclick="generateWithAI('tip')" class="btn-secondary px-3 py-1 rounded-lg text-sm flex items-center gap-1">
|
||||
<span>💡</span> Generar Tip
|
||||
</button>
|
||||
<button type="button" onclick="generateWithAI('promo')" class="btn-secondary px-3 py-1 rounded-lg text-sm flex items-center gap-1">
|
||||
<span>📢</span> Generar Promo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Assist -->
|
||||
<div class="card p-6">
|
||||
<h3 class="font-semibold mb-4">Asistente IA (DeepSeek)</h3>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button type="button" onclick="generateTip()"
|
||||
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="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="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<span>Adaptar por Plataforma</span>
|
||||
</button>
|
||||
<!-- Hashtags -->
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>#️⃣</span>
|
||||
<span>Hashtags</span>
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
id="hashtags"
|
||||
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-primary focus:outline-none transition-colors"
|
||||
placeholder="#Tecnología #Automatización #Tijuana"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<button type="button" onclick="addHashtag('#ConsultoríaAS')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#ConsultoríaAS</button>
|
||||
<button type="button" onclick="addHashtag('#Tijuana')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Tijuana</button>
|
||||
<button type="button" onclick="addHashtag('#Tecnología')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Tecnología</button>
|
||||
<button type="button" onclick="addHashtag('#Automatización')" class="text-xs bg-dark-700 px-2 py-1 rounded-full hover:bg-dark-600 transition-colors">#Automatización</button>
|
||||
</div>
|
||||
<p id="ai-status" class="text-gray-500 text-sm mt-2">
|
||||
Verificando estado de IA...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Schedule -->
|
||||
<div class="card p-6">
|
||||
<h3 class="font-semibold mb-4">Programación</h3>
|
||||
<div class="flex gap-4 items-center">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="schedule" value="now" checked
|
||||
class="text-amber-500" onchange="toggleSchedule()">
|
||||
<span>Publicar ahora</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="schedule" value="later"
|
||||
class="text-amber-500" onchange="toggleSchedule()">
|
||||
<span>Programar</span>
|
||||
</label>
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>📅</span>
|
||||
<span>Programación</span>
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Fecha</label>
|
||||
<input
|
||||
type="date"
|
||||
id="schedule-date"
|
||||
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white focus:border-primary focus:outline-none transition-colors"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Hora</label>
|
||||
<input
|
||||
type="time"
|
||||
id="schedule-time"
|
||||
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-white focus:border-primary focus:outline-none transition-colors"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div id="schedule-picker" class="mt-4 hidden">
|
||||
<input type="datetime-local" id="scheduled_at" name="scheduled_at"
|
||||
class="bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Preview -->
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>👁️</span>
|
||||
<span>Vista Previa</span>
|
||||
</h3>
|
||||
<div id="preview" class="bg-dark-800 rounded-xl p-4 min-h-32">
|
||||
<p class="text-gray-500 text-sm italic">El contenido aparecerá aquí...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<button type="button" onclick="saveDraft()"
|
||||
class="btn-secondary px-6 py-3 rounded-lg">
|
||||
Guardar Borrador
|
||||
</button>
|
||||
<button type="button" onclick="previewPost()"
|
||||
class="btn-secondary px-6 py-3 rounded-lg">
|
||||
Vista Previa
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-8 py-3 rounded-lg font-semibold">
|
||||
Publicar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div id="preview-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="card p-6 max-w-2xl w-full mx-4 max-h-screen overflow-y-auto">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Vista Previa</h3>
|
||||
<button onclick="closePreview()" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>⚡</span>
|
||||
<span>Acciones</span>
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<button onclick="saveAsDraft()" class="w-full btn-secondary px-4 py-3 rounded-xl flex items-center justify-center gap-2">
|
||||
<span>💾</span>
|
||||
<span>Guardar Borrador</span>
|
||||
</button>
|
||||
<button onclick="schedulePost()" class="w-full btn-secondary px-4 py-3 rounded-xl flex items-center justify-center gap-2">
|
||||
<span>📅</span>
|
||||
<span>Programar</span>
|
||||
</button>
|
||||
<button onclick="publishNow()" class="w-full btn-primary px-4 py-3 rounded-xl flex items-center justify-center gap-2 font-medium">
|
||||
<span>🚀</span>
|
||||
<span>Publicar Ahora</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="preview-content" class="space-y-4">
|
||||
<!-- Se llena dinámicamente -->
|
||||
</div>
|
||||
|
||||
<!-- AI Status -->
|
||||
<div class="card rounded-2xl p-6">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2">
|
||||
<span>🤖</span>
|
||||
<span>IA</span>
|
||||
</h3>
|
||||
<div id="ai-status" class="text-sm text-gray-400">
|
||||
Verificando...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Result Modal -->
|
||||
<div id="result-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="card p-6 max-w-md w-full mx-4">
|
||||
<div id="result-content">
|
||||
<!-- Se llena dinámicamente -->
|
||||
</div>
|
||||
<button onclick="closeResult()" class="btn-primary w-full mt-4 py-2 rounded-lg">
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
let selectedPlatforms = [];
|
||||
const charLimits = { x: 280, facebook: 63206, instagram: 2200, threads: 500 };
|
||||
|
||||
<script>
|
||||
// State
|
||||
let selectedPlatforms = [];
|
||||
const charLimits = {
|
||||
x: 280,
|
||||
threads: 500,
|
||||
instagram: 2200,
|
||||
facebook: 63206
|
||||
};
|
||||
function togglePlatform(platform) {
|
||||
const btn = document.getElementById(`btn-${platform}`);
|
||||
const index = selectedPlatforms.indexOf(platform);
|
||||
|
||||
// Platform selection
|
||||
function togglePlatform(platform) {
|
||||
const btn = document.getElementById(`btn-${platform}`);
|
||||
const index = selectedPlatforms.indexOf(platform);
|
||||
if (index > -1) {
|
||||
selectedPlatforms.splice(index, 1);
|
||||
btn.classList.remove('border-primary', 'bg-primary/20');
|
||||
btn.classList.add('border-transparent');
|
||||
} else {
|
||||
selectedPlatforms.push(platform);
|
||||
btn.classList.add('border-primary', 'bg-primary/20');
|
||||
btn.classList.remove('border-transparent');
|
||||
}
|
||||
updateCharCount();
|
||||
}
|
||||
|
||||
if (index > -1) {
|
||||
selectedPlatforms.splice(index, 1);
|
||||
btn.classList.remove('ring-2', 'ring-amber-500');
|
||||
function updateCharCount() {
|
||||
const content = document.getElementById('content').value;
|
||||
const countEl = document.getElementById('char-count');
|
||||
const previewEl = document.getElementById('preview');
|
||||
|
||||
const limit = selectedPlatforms.includes('x') ? 280 :
|
||||
selectedPlatforms.includes('threads') ? 500 : 2200;
|
||||
|
||||
countEl.textContent = `${content.length} / ${limit}`;
|
||||
|
||||
if (content.length > limit) {
|
||||
countEl.classList.add('text-red-400');
|
||||
countEl.classList.remove('text-yellow-400', 'text-gray-500');
|
||||
} else if (content.length > limit * 0.9) {
|
||||
countEl.classList.add('text-yellow-400');
|
||||
countEl.classList.remove('text-red-400', 'text-gray-500');
|
||||
} else {
|
||||
countEl.classList.add('text-gray-500');
|
||||
countEl.classList.remove('text-red-400', 'text-yellow-400');
|
||||
}
|
||||
|
||||
// Update preview
|
||||
if (content) {
|
||||
previewEl.innerHTML = `<p class="text-white whitespace-pre-wrap">${content}</p>`;
|
||||
} else {
|
||||
previewEl.innerHTML = `<p class="text-gray-500 text-sm italic">El contenido aparecerá aquí...</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function addHashtag(tag) {
|
||||
const input = document.getElementById('hashtags');
|
||||
if (!input.value.includes(tag)) {
|
||||
input.value = input.value ? `${input.value} ${tag}` : tag;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateWithAI(type) {
|
||||
showModal('Generando contenido con IA...', true);
|
||||
try {
|
||||
const response = await fetch(`/api/generate/${type}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ platforms: selectedPlatforms.length ? selectedPlatforms : ['x'] })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.content) {
|
||||
document.getElementById('content').value = data.content;
|
||||
updateCharCount();
|
||||
closeModal();
|
||||
} else {
|
||||
selectedPlatforms.push(platform);
|
||||
btn.classList.add('ring-2', 'ring-amber-500');
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error generando contenido</p></div>`);
|
||||
}
|
||||
} catch (error) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>`);
|
||||
}
|
||||
}
|
||||
|
||||
updateCharCount();
|
||||
async function saveAsDraft() {
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
|
||||
return;
|
||||
}
|
||||
await savePost('draft');
|
||||
}
|
||||
|
||||
async function schedulePost() {
|
||||
const content = document.getElementById('content').value;
|
||||
const date = document.getElementById('schedule-date').value;
|
||||
const time = document.getElementById('schedule-time').value;
|
||||
|
||||
if (!content) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
|
||||
return;
|
||||
}
|
||||
if (!date || !time) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Selecciona fecha y hora</p></div>`);
|
||||
return;
|
||||
}
|
||||
await savePost('scheduled', `${date}T${time}:00`);
|
||||
}
|
||||
|
||||
async function publishNow() {
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe algo primero</p></div>`);
|
||||
return;
|
||||
}
|
||||
if (!selectedPlatforms.length) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Selecciona al menos una plataforma</p></div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Character counter
|
||||
function updateCharCount() {
|
||||
const content = document.getElementById('content').value;
|
||||
const count = content.length;
|
||||
const counter = document.getElementById('char-count');
|
||||
const limitsDiv = document.getElementById('platform-limits');
|
||||
|
||||
counter.textContent = count;
|
||||
|
||||
// Update limits display
|
||||
let limitsHtml = '';
|
||||
selectedPlatforms.forEach(platform => {
|
||||
const limit = charLimits[platform];
|
||||
const remaining = limit - count;
|
||||
let statusClass = '';
|
||||
let icon = '✓';
|
||||
|
||||
if (remaining < 0) {
|
||||
statusClass = 'char-error';
|
||||
icon = '✗';
|
||||
} else if (remaining < 50) {
|
||||
statusClass = 'char-warning';
|
||||
icon = '⚠';
|
||||
}
|
||||
|
||||
limitsHtml += `
|
||||
<div class="${statusClass}">
|
||||
${icon} ${platform.charAt(0).toUpperCase() + platform.slice(1)}:
|
||||
${remaining >= 0 ? remaining + ' restantes' : Math.abs(remaining) + ' excedidos'}
|
||||
(máx ${limit})
|
||||
</div>
|
||||
`;
|
||||
showModal('Publicando...', true);
|
||||
try {
|
||||
const response = await fetch('/api/publish/single', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
platforms: selectedPlatforms,
|
||||
hashtags: document.getElementById('hashtags').value.split(' ').filter(h => h)
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
limitsDiv.innerHTML = limitsHtml;
|
||||
}
|
||||
|
||||
// Schedule toggle
|
||||
function toggleSchedule() {
|
||||
const scheduleValue = document.querySelector('input[name="schedule"]:checked').value;
|
||||
const picker = document.getElementById('schedule-picker');
|
||||
picker.classList.toggle('hidden', scheduleValue === 'now');
|
||||
}
|
||||
|
||||
// Preview
|
||||
function previewPost() {
|
||||
if (selectedPlatforms.length === 0) {
|
||||
alert('Selecciona al menos una plataforma');
|
||||
return;
|
||||
if (data.success) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p class="text-lg">¡Publicado!</p><p class="text-gray-400 mt-2">${data.message || 'Post publicado correctamente'}</p></div>`);
|
||||
document.getElementById('content').value = '';
|
||||
updateCharCount();
|
||||
} else {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error: ${data.detail || data.error || 'Error al publicar'}</p></div>`);
|
||||
}
|
||||
} catch (error) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content.trim()) {
|
||||
alert('Escribe el contenido del post');
|
||||
return;
|
||||
}
|
||||
async function savePost(status, scheduledAt = null) {
|
||||
showModal('Guardando...', true);
|
||||
try {
|
||||
const body = {
|
||||
content: document.getElementById('content').value,
|
||||
platforms: selectedPlatforms.length ? selectedPlatforms : ['x'],
|
||||
hashtags: document.getElementById('hashtags').value.split(' ').filter(h => h),
|
||||
status: status
|
||||
};
|
||||
if (scheduledAt) body.scheduled_at = scheduledAt;
|
||||
|
||||
const previewDiv = document.getElementById('preview-content');
|
||||
let html = '';
|
||||
|
||||
selectedPlatforms.forEach(platform => {
|
||||
const limit = charLimits[platform];
|
||||
const truncated = content.length > limit;
|
||||
const displayContent = truncated ? content.substring(0, limit) + '...' : content;
|
||||
|
||||
html += `
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="font-semibold">${platform.charAt(0).toUpperCase() + platform.slice(1)}</span>
|
||||
${truncated ? '<span class="text-red-400 text-xs">(truncado)</span>' : ''}
|
||||
</div>
|
||||
<p class="text-gray-300 whitespace-pre-wrap">${displayContent}</p>
|
||||
<div class="text-gray-500 text-xs mt-2">
|
||||
${content.length}/${limit} caracteres
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const response = await fetch('/api/posts/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
previewDiv.innerHTML = html;
|
||||
document.getElementById('preview-modal').classList.remove('hidden');
|
||||
document.getElementById('preview-modal').classList.add('flex');
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p class="text-lg">Guardado</p><p class="text-gray-400 mt-2">Post ${status === 'scheduled' ? 'programado' : 'guardado como borrador'}</p></div>`);
|
||||
} catch (error) {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error guardando</p></div>`);
|
||||
}
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
document.getElementById('preview-modal').classList.add('hidden');
|
||||
document.getElementById('preview-modal').classList.remove('flex');
|
||||
// Check AI status
|
||||
async function checkAIStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/generate/status');
|
||||
const data = await response.json();
|
||||
document.getElementById('ai-status').innerHTML = data.configured
|
||||
? `<span class="text-green-400">✓</span> ${data.provider || 'DeepSeek'} configurado`
|
||||
: `<span class="text-red-400">✗</span> No configurado`;
|
||||
} catch (error) {
|
||||
document.getElementById('ai-status').innerHTML = `<span class="text-red-400">✗</span> Error`;
|
||||
}
|
||||
}
|
||||
|
||||
// Result modal
|
||||
function showResult(success, message, details = null) {
|
||||
const resultDiv = document.getElementById('result-content');
|
||||
const icon = success ? '✅' : '❌';
|
||||
|
||||
let html = `
|
||||
<div class="text-center">
|
||||
<div class="text-4xl mb-4">${icon}</div>
|
||||
<h3 class="text-xl font-semibold mb-2">${success ? 'Publicado' : 'Error'}</h3>
|
||||
<p class="text-gray-400">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (details) {
|
||||
html += `
|
||||
<div class="mt-4 space-y-2">
|
||||
${Object.entries(details).map(([platform, result]) => `
|
||||
<div class="flex justify-between items-center bg-gray-800 rounded p-2">
|
||||
<span>${platform}</span>
|
||||
<span class="${result.success ? 'text-green-400' : 'text-red-400'}">
|
||||
${result.success ? '✓ OK' : '✗ ' + (result.error || 'Error')}
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
document.getElementById('result-modal').classList.remove('hidden');
|
||||
document.getElementById('result-modal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeResult() {
|
||||
document.getElementById('result-modal').classList.add('hidden');
|
||||
document.getElementById('result-modal').classList.remove('flex');
|
||||
}
|
||||
|
||||
// Form submission
|
||||
document.getElementById('compose-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (selectedPlatforms.length === 0) {
|
||||
alert('Selecciona al menos una plataforma');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = document.getElementById('content').value;
|
||||
if (!content.trim()) {
|
||||
alert('Escribe el contenido del post');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare content per platform
|
||||
const platformContent = {};
|
||||
selectedPlatforms.forEach(p => {
|
||||
platformContent[p] = content;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/publish/multiple', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
platforms: selectedPlatforms,
|
||||
content: platformContent
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showResult(true,
|
||||
`Publicado en ${data.successful_platforms.length} plataforma(s)`,
|
||||
data.results
|
||||
);
|
||||
// Clear form
|
||||
document.getElementById('content').value = '';
|
||||
selectedPlatforms.forEach(p => togglePlatform(p));
|
||||
selectedPlatforms = [];
|
||||
} else {
|
||||
showResult(false,
|
||||
'Algunas publicaciones fallaron',
|
||||
data.results
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(false, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
const content = document.getElementById('content').value;
|
||||
localStorage.setItem('draft_content', content);
|
||||
localStorage.setItem('draft_platforms', JSON.stringify(selectedPlatforms));
|
||||
alert('Borrador guardado localmente');
|
||||
}
|
||||
|
||||
// Load draft on page load
|
||||
window.addEventListener('load', async () => {
|
||||
const draftContent = localStorage.getItem('draft_content');
|
||||
const draftPlatforms = localStorage.getItem('draft_platforms');
|
||||
|
||||
if (draftContent) {
|
||||
document.getElementById('content').value = draftContent;
|
||||
}
|
||||
|
||||
if (draftPlatforms) {
|
||||
const platforms = JSON.parse(draftPlatforms);
|
||||
platforms.forEach(p => togglePlatform(p));
|
||||
}
|
||||
|
||||
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>
|
||||
</html>
|
||||
checkAIStatus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user