- 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>
349 lines
16 KiB
HTML
349 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Crear Post{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="animate-fade-in">
|
||
<!-- 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>
|
||
|
||
<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 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('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')" 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>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<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"
|
||
rows="6"
|
||
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>
|
||
<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>
|
||
|
||
<!-- 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>
|
||
</div>
|
||
|
||
<!-- Schedule -->
|
||
<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>
|
||
</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="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>
|
||
|
||
<!-- 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 %}
|
||
|
||
{% block extra_scripts %}
|
||
<script>
|
||
let selectedPlatforms = [];
|
||
const charLimits = { x: 280, facebook: 63206, instagram: 2200, threads: 500 };
|
||
|
||
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();
|
||
}
|
||
|
||
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 {
|
||
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>`);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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();
|
||
|
||
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>`);
|
||
}
|
||
}
|
||
|
||
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 response = await fetch('/api/posts/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
const data = await response.json();
|
||
|
||
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>`);
|
||
}
|
||
}
|
||
|
||
// 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`;
|
||
}
|
||
}
|
||
|
||
checkAIStatus();
|
||
</script>
|
||
{% endblock %}
|