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,424 +1,400 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interacciones - 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; }
|
||||
.btn-secondary:hover { background-color: #4b5563; }
|
||||
.interaction-item { transition: all 0.2s; }
|
||||
.interaction-item:hover { background-color: #1e3a5f; }
|
||||
.interaction-item.selected { border-left: 3px solid #d4a574; }
|
||||
.platform-x { color: #1d9bf0; }
|
||||
.platform-threads { color: #fff; }
|
||||
.platform-instagram { color: #e1306c; }
|
||||
.platform-facebook { color: #1877f2; }
|
||||
.type-comment { background-color: #1e40af; }
|
||||
.type-mention { background-color: #7c3aed; }
|
||||
.type-dm { background-color: #059669; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Interacciones{% 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 hover:bg-gray-800">+ Crear</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="/dashboard/interactions" class="px-4 py-2 rounded bg-gray-800 accent">Interacciones</a>
|
||||
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</a>
|
||||
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
|
||||
</nav>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Interacciones</h1>
|
||||
<p class="text-gray-400 mt-1">Gestiona comentarios, menciones y mensajes</p>
|
||||
</div>
|
||||
</header>
|
||||
<button onclick="syncInteractions()" class="btn-secondary px-6 py-3 rounded-xl font-medium flex items-center gap-2">
|
||||
<span id="sync-icon">🔄</span>
|
||||
<span>Sincronizar</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<!-- Interactions List -->
|
||||
<div class="col-span-2">
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<select id="filter-platform" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="">Todas las plataformas</option>
|
||||
<option value="x">X (Twitter)</option>
|
||||
<option value="threads">Threads</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
</select>
|
||||
<select id="filter-type" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="">Todos los tipos</option>
|
||||
<option value="comment">Comentarios</option>
|
||||
<option value="mention">Menciones</option>
|
||||
<option value="dm">Mensajes</option>
|
||||
</select>
|
||||
<select id="filter-status" onchange="filterInteractions()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="pending">Sin responder</option>
|
||||
<option value="responded">Respondidos</option>
|
||||
<option value="all">Todos</option>
|
||||
</select>
|
||||
<div class="flex-1"></div>
|
||||
<button onclick="syncInteractions()" class="btn-secondary px-4 py-2 rounded flex items-center gap-2">
|
||||
<span id="sync-icon">🔄</span> Sincronizar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-4">
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-xl font-bold text-red-400" id="stat-pending">0</div>
|
||||
<div class="text-xs text-gray-400">Sin responder</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-xl font-bold text-green-400" id="stat-responded">0</div>
|
||||
<div class="text-xs text-gray-400">Respondidos</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-xl font-bold text-purple-400" id="stat-leads">0</div>
|
||||
<div class="text-xs text-gray-400">Leads</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-xl font-bold text-gray-400" id="stat-total">0</div>
|
||||
<div class="text-xs text-gray-400">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div class="card">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="font-semibold">Interacciones</h2>
|
||||
</div>
|
||||
<div id="interactions-list" class="divide-y divide-gray-700 max-h-screen overflow-y-auto">
|
||||
<div class="p-8 text-center text-gray-500">Cargando...</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Interactions List -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Filters -->
|
||||
<div class="card rounded-2xl p-4">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<select id="filter-platform" onchange="filterInteractions()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||||
<option value="">Todas las plataformas</option>
|
||||
<option value="x">X (Twitter)</option>
|
||||
<option value="threads">Threads</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
</select>
|
||||
<select id="filter-type" onchange="filterInteractions()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||||
<option value="">Todos los tipos</option>
|
||||
<option value="comment">Comentarios</option>
|
||||
<option value="mention">Menciones</option>
|
||||
<option value="dm">Mensajes</option>
|
||||
</select>
|
||||
<select id="filter-status" onchange="filterInteractions()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||||
<option value="pending">Sin responder</option>
|
||||
<option value="responded">Respondidos</option>
|
||||
<option value="all">Todos</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Panel -->
|
||||
<div class="col-span-1">
|
||||
<div class="card p-6 sticky top-6" id="detail-panel">
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<p>Selecciona una interacción</p>
|
||||
<p class="text-sm mt-2">para ver detalles y responder</p>
|
||||
</div>
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="card rounded-2xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-red-400" id="stat-pending">0</div>
|
||||
<div class="text-xs text-gray-400">Sin responder</div>
|
||||
</div>
|
||||
<div class="card rounded-2xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-green-400" id="stat-responded">0</div>
|
||||
<div class="text-xs text-gray-400">Respondidos</div>
|
||||
</div>
|
||||
<div class="card rounded-2xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-purple-400" id="stat-leads">0</div>
|
||||
<div class="text-xs text-gray-400">Leads</div>
|
||||
</div>
|
||||
<div class="card rounded-2xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-gray-400" id="stat-total">0</div>
|
||||
<div class="text-xs text-gray-400">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div class="card rounded-2xl overflow-hidden">
|
||||
<div class="p-4 border-b border-dark-600">
|
||||
<h2 class="font-semibold">Interacciones</h2>
|
||||
</div>
|
||||
<div id="interactions-list" class="divide-y divide-dark-600 max-h-[600px] overflow-y-auto">
|
||||
<div class="p-8 text-center text-gray-500">Cargando...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let interactions = [];
|
||||
let selectedId = null;
|
||||
<!-- Detail Panel -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="card rounded-2xl p-6 sticky top-6" id="detail-panel">
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<span class="text-4xl block mb-4">💬</span>
|
||||
<p>Selecciona una interacción</p>
|
||||
<p class="text-sm mt-2">para ver detalles y responder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
window.addEventListener('load', loadInteractions);
|
||||
{% block extra_scripts %}
|
||||
<style>
|
||||
.interaction-item { transition: all 0.2s; }
|
||||
.interaction-item:hover { background-color: rgba(99, 102, 241, 0.1); }
|
||||
.interaction-item.selected { border-left: 3px solid #6366f1; background-color: rgba(99, 102, 241, 0.1); }
|
||||
.platform-x { color: #1d9bf0; }
|
||||
.platform-threads { color: #fff; }
|
||||
.platform-instagram { color: #e1306c; }
|
||||
.platform-facebook { color: #1877f2; }
|
||||
.type-comment { background-color: rgba(59, 130, 246, 0.3); color: #60a5fa; }
|
||||
.type-mention { background-color: rgba(139, 92, 246, 0.3); color: #a78bfa; }
|
||||
.type-dm { background-color: rgba(16, 185, 129, 0.3); color: #34d399; }
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let interactions = [];
|
||||
let selectedId = null;
|
||||
|
||||
async function loadInteractions() {
|
||||
try {
|
||||
const response = await fetch('/api/interactions/');
|
||||
interactions = await response.json();
|
||||
updateStats();
|
||||
filterInteractions();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
document.getElementById('interactions-list').innerHTML =
|
||||
'<div class="p-8 text-center text-red-400">Error cargando interacciones</div>';
|
||||
}
|
||||
window.addEventListener('load', loadInteractions);
|
||||
|
||||
async function loadInteractions() {
|
||||
try {
|
||||
const response = await fetch('/api/interactions/');
|
||||
interactions = await response.json();
|
||||
updateStats();
|
||||
filterInteractions();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
document.getElementById('interactions-list').innerHTML =
|
||||
'<div class="p-8 text-center text-red-400">Error cargando interacciones</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const pending = interactions.filter(i => !i.responded && !i.is_archived).length;
|
||||
const responded = interactions.filter(i => i.responded).length;
|
||||
const leads = interactions.filter(i => i.is_potential_lead).length;
|
||||
|
||||
document.getElementById('stat-pending').textContent = pending;
|
||||
document.getElementById('stat-responded').textContent = responded;
|
||||
document.getElementById('stat-leads').textContent = leads;
|
||||
document.getElementById('stat-total').textContent = interactions.length;
|
||||
}
|
||||
|
||||
function filterInteractions() {
|
||||
const platform = document.getElementById('filter-platform').value;
|
||||
const type = document.getElementById('filter-type').value;
|
||||
const status = document.getElementById('filter-status').value;
|
||||
|
||||
let filtered = interactions;
|
||||
|
||||
if (platform) {
|
||||
filtered = filtered.filter(i => i.platform === platform);
|
||||
}
|
||||
if (type) {
|
||||
filtered = filtered.filter(i => i.interaction_type === type);
|
||||
}
|
||||
if (status === 'pending') {
|
||||
filtered = filtered.filter(i => !i.responded && !i.is_archived);
|
||||
} else if (status === 'responded') {
|
||||
filtered = filtered.filter(i => i.responded);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const pending = interactions.filter(i => !i.responded && !i.is_archived).length;
|
||||
const responded = interactions.filter(i => i.responded).length;
|
||||
const leads = interactions.filter(i => i.is_potential_lead).length;
|
||||
renderInteractions(filtered);
|
||||
}
|
||||
|
||||
document.getElementById('stat-pending').textContent = pending;
|
||||
document.getElementById('stat-responded').textContent = responded;
|
||||
document.getElementById('stat-leads').textContent = leads;
|
||||
document.getElementById('stat-total').textContent = interactions.length;
|
||||
function renderInteractions(items) {
|
||||
const container = document.getElementById('interactions-list');
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="p-8 text-center text-gray-500">No hay interacciones</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
function filterInteractions() {
|
||||
const platform = document.getElementById('filter-platform').value;
|
||||
const type = document.getElementById('filter-type').value;
|
||||
const status = document.getElementById('filter-status').value;
|
||||
|
||||
let filtered = interactions;
|
||||
|
||||
if (platform) {
|
||||
filtered = filtered.filter(i => i.platform === platform);
|
||||
}
|
||||
if (type) {
|
||||
filtered = filtered.filter(i => i.interaction_type === type);
|
||||
}
|
||||
if (status === 'pending') {
|
||||
filtered = filtered.filter(i => !i.responded && !i.is_archived);
|
||||
} else if (status === 'responded') {
|
||||
filtered = filtered.filter(i => i.responded);
|
||||
}
|
||||
|
||||
renderInteractions(filtered);
|
||||
}
|
||||
|
||||
function renderInteractions(items) {
|
||||
const container = document.getElementById('interactions-list');
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="p-8 text-center text-gray-500">No hay interacciones</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = items.map(item => `
|
||||
<div class="interaction-item p-4 cursor-pointer ${selectedId === item.id ? 'selected' : ''}"
|
||||
onclick="selectInteraction(${item.id})">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="platform-${item.platform} font-medium">@${item.author_username || 'usuario'}</span>
|
||||
<span class="type-${item.interaction_type} text-xs px-2 py-0.5 rounded">${item.interaction_type}</span>
|
||||
<span class="text-gray-500 text-xs">${item.platform}</span>
|
||||
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-0.5 rounded">Lead</span>' : ''}
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm line-clamp-2">${escapeHtml(item.content || '')}</p>
|
||||
<div class="text-gray-500 text-xs mt-1">
|
||||
${formatDate(item.interaction_at)}
|
||||
${item.responded ? '<span class="text-green-400 ml-2">✓ Respondido</span>' : ''}
|
||||
</div>
|
||||
container.innerHTML = items.map(item => `
|
||||
<div class="interaction-item p-4 cursor-pointer ${selectedId === item.id ? 'selected' : ''}"
|
||||
onclick="selectInteraction(${item.id}, this)">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="platform-${item.platform} font-medium">@${item.author_username || 'usuario'}</span>
|
||||
<span class="type-${item.interaction_type} text-xs px-2 py-0.5 rounded-full">${item.interaction_type}</span>
|
||||
<span class="text-gray-500 text-xs">${item.platform}</span>
|
||||
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-0.5 rounded-full">Lead</span>' : ''}
|
||||
</div>
|
||||
${!item.responded ? '<span class="w-2 h-2 bg-red-500 rounded-full"></span>' : ''}
|
||||
<p class="text-gray-300 text-sm line-clamp-2">${escapeHtml(item.content || '')}</p>
|
||||
<div class="text-gray-500 text-xs mt-1">
|
||||
${formatDate(item.interaction_at)}
|
||||
${item.responded ? '<span class="text-green-400 ml-2">✓ Respondido</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
${!item.responded ? '<span class="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"></span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectInteraction(id, element) {
|
||||
selectedId = id;
|
||||
const item = interactions.find(i => i.id === id);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
document.querySelectorAll('.interaction-item').forEach(el => el.classList.remove('selected'));
|
||||
element?.classList.add('selected');
|
||||
|
||||
const panel = document.getElementById('detail-panel');
|
||||
panel.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="platform-${item.platform} font-semibold text-lg">@${item.author_username || 'usuario'}</span>
|
||||
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-1 rounded-full">Lead</span>' : ''}
|
||||
</div>
|
||||
<span class="type-${item.interaction_type} text-xs px-2 py-1 rounded-full">${item.interaction_type}</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-800 rounded-xl p-4">
|
||||
<p class="text-gray-200">${escapeHtml(item.content || '')}</p>
|
||||
<div class="text-gray-500 text-xs mt-2">
|
||||
${formatDate(item.interaction_at)} • ${item.platform}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectInteraction(id) {
|
||||
selectedId = id;
|
||||
const item = interactions.find(i => i.id === id);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
// Update selection visual
|
||||
document.querySelectorAll('.interaction-item').forEach(el => el.classList.remove('selected'));
|
||||
event.currentTarget?.classList.add('selected');
|
||||
|
||||
// Render detail panel
|
||||
const panel = document.getElementById('detail-panel');
|
||||
panel.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="platform-${item.platform} font-semibold text-lg">@${item.author_username || 'usuario'}</span>
|
||||
${item.is_potential_lead ? '<span class="bg-purple-600 text-xs px-2 py-1 rounded">Lead</span>' : ''}
|
||||
</div>
|
||||
<span class="type-${item.interaction_type} text-xs px-2 py-1 rounded">${item.interaction_type}</span>
|
||||
${item.responded && item.response_content ? `
|
||||
<div class="bg-green-900/30 rounded-xl p-4">
|
||||
<div class="text-xs text-green-400 mb-1">Tu respuesta:</div>
|
||||
<p class="text-gray-200">${escapeHtml(item.response_content)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<p class="text-gray-200">${escapeHtml(item.content || '')}</p>
|
||||
<div class="text-gray-500 text-xs mt-2">
|
||||
${formatDate(item.interaction_at)} • ${item.platform}
|
||||
${!item.responded ? `
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Responder</label>
|
||||
<textarea id="response-text" rows="3"
|
||||
class="w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-sm focus:border-primary focus:outline-none"
|
||||
placeholder="Escribe tu respuesta..."></textarea>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button onclick="generateSuggestions(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
|
||||
🤖 Sugerir IA
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="suggestions-${item.id}" class="hidden mt-3 space-y-2">
|
||||
<!-- AI suggestions -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${item.responded && item.response_content ? `
|
||||
<div class="bg-green-900 bg-opacity-30 rounded-lg p-4">
|
||||
<div class="text-xs text-green-400 mb-1">Tu respuesta:</div>
|
||||
<p class="text-gray-200">${escapeHtml(item.response_content)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${!item.responded ? `
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Responder</label>
|
||||
<textarea id="response-text" rows="3"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm"
|
||||
placeholder="Escribe tu respuesta..."></textarea>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button onclick="generateSuggestions(${item.id})" class="btn-secondary px-3 py-1 rounded text-sm flex-1">
|
||||
🤖 Sugerir IA
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="suggestions-${item.id}" class="hidden mt-3 space-y-2">
|
||||
<!-- AI suggestions -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button onclick="sendResponse(${item.id})" class="btn-primary flex-1 py-2 rounded">
|
||||
Responder
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="flex gap-2 pt-4 border-t border-gray-700">
|
||||
${!item.is_potential_lead ? `
|
||||
<button onclick="markAsLead(${item.id})" class="btn-secondary px-3 py-2 rounded text-sm flex-1">
|
||||
⭐ Marcar Lead
|
||||
</button>
|
||||
` : `
|
||||
<button onclick="unmarkLead(${item.id})" class="btn-secondary px-3 py-2 rounded text-sm flex-1">
|
||||
Quitar Lead
|
||||
</button>
|
||||
`}
|
||||
<button onclick="archiveInteraction(${item.id})" class="text-gray-400 hover:text-red-400 px-3 py-2 text-sm">
|
||||
Archivar
|
||||
<div class="flex gap-2">
|
||||
<button onclick="sendResponse(${item.id})" class="btn-primary flex-1 py-3 rounded-xl">
|
||||
Responder
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="flex gap-2 pt-4 border-t border-dark-600">
|
||||
${!item.is_potential_lead ? `
|
||||
<button onclick="markAsLead(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
|
||||
⭐ Marcar Lead
|
||||
</button>
|
||||
` : `
|
||||
<button onclick="unmarkLead(${item.id})" class="btn-secondary px-3 py-2 rounded-xl text-sm flex-1">
|
||||
Quitar Lead
|
||||
</button>
|
||||
`}
|
||||
<button onclick="archiveInteraction(${item.id})" class="text-gray-400 hover:text-red-400 px-3 py-2 text-sm">
|
||||
Archivar
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSuggestions(id) {
|
||||
const item = interactions.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
async function generateSuggestions(id) {
|
||||
const item = interactions.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
|
||||
const suggestionsDiv = document.getElementById(`suggestions-${id}`);
|
||||
suggestionsDiv.classList.remove('hidden');
|
||||
suggestionsDiv.innerHTML = '<div class="text-gray-400 text-sm">Generando sugerencias...</div>';
|
||||
const suggestionsDiv = document.getElementById(`suggestions-${id}`);
|
||||
suggestionsDiv.classList.remove('hidden');
|
||||
suggestionsDiv.innerHTML = '<div class="text-gray-400 text-sm">Generando sugerencias...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate/response', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interaction_content: item.content,
|
||||
interaction_type: item.interaction_type
|
||||
})
|
||||
});
|
||||
try {
|
||||
const response = await fetch('/api/generate/response', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interaction_content: item.content,
|
||||
interaction_type: item.interaction_type
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.contents) {
|
||||
suggestionsDiv.innerHTML = data.contents.map((suggestion, i) => `
|
||||
<div class="bg-gray-800 rounded p-2 text-sm cursor-pointer hover:bg-gray-700"
|
||||
onclick="useSuggestion('${escapeHtml(suggestion).replace(/'/g, "\\'")}')">
|
||||
${i + 1}. ${escapeHtml(suggestion)}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
suggestionsDiv.innerHTML = '<div class="text-red-400 text-sm">Error: ' + (data.error || 'No se pudo generar') + '</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
suggestionsDiv.innerHTML = '<div class="text-red-400 text-sm">Error de conexión</div>';
|
||||
if (data.success && data.contents) {
|
||||
suggestionsDiv.innerHTML = data.contents.map((suggestion, i) => `
|
||||
<div class="bg-dark-800 rounded-xl p-3 text-sm cursor-pointer hover:bg-dark-700 transition-colors"
|
||||
onclick="useSuggestion('${escapeHtml(suggestion).replace(/'/g, "\\'")}')">
|
||||
${i + 1}. ${escapeHtml(suggestion)}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
suggestionsDiv.innerHTML = '<div class="text-red-400 text-sm">Error: ' + (data.error || 'No se pudo generar') + '</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
suggestionsDiv.innerHTML = '<div class="text-red-400 text-sm">Error de conexión</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function useSuggestion(text) {
|
||||
document.getElementById('response-text').value = text;
|
||||
}
|
||||
|
||||
async function sendResponse(id) {
|
||||
const responseText = document.getElementById('response-text').value.trim();
|
||||
if (!responseText) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">⚠️</span><p>Escribe una respuesta</p></div>');
|
||||
return;
|
||||
}
|
||||
|
||||
function useSuggestion(text) {
|
||||
document.getElementById('response-text').value = text;
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/respond`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ response: responseText })
|
||||
});
|
||||
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al enviar</p></div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function markAsLead(id) {
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/mark-lead`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function unmarkLead(id) {
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/unmark-lead`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveInteraction(id) {
|
||||
if (!confirm('¿Archivar esta interacción?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/archive`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
document.getElementById('detail-panel').innerHTML =
|
||||
'<div class="text-center text-gray-500 py-8"><span class="text-4xl block mb-4">📦</span><p>Interacción archivada</p></div>';
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error</p></div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncInteractions() {
|
||||
const icon = document.getElementById('sync-icon');
|
||||
icon.style.animation = 'spin 1s linear infinite';
|
||||
|
||||
try {
|
||||
await fetch('/api/interactions/sync', { method: 'POST' });
|
||||
await loadInteractions();
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error al sincronizar</p></div>');
|
||||
}
|
||||
|
||||
async function sendResponse(id) {
|
||||
const responseText = document.getElementById('response-text').value.trim();
|
||||
if (!responseText) {
|
||||
alert('Escribe una respuesta');
|
||||
return;
|
||||
}
|
||||
icon.style.animation = '';
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/respond`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ response: responseText })
|
||||
});
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
alert('Error al enviar: ' + error.message);
|
||||
}
|
||||
}
|
||||
if (diffMins < 60) return `hace ${diffMins}m`;
|
||||
if (diffHours < 24) return `hace ${diffHours}h`;
|
||||
if (diffDays < 7) return `hace ${diffDays}d`;
|
||||
|
||||
async function markAsLead(id) {
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/mark-lead`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
return date.toLocaleDateString('es-MX', { day: '2-digit', month: 'short' });
|
||||
}
|
||||
|
||||
async function unmarkLead(id) {
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/unmark-lead`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveInteraction(id) {
|
||||
if (!confirm('¿Archivar esta interacción?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/interactions/${id}/archive`, { method: 'POST' });
|
||||
loadInteractions();
|
||||
document.getElementById('detail-panel').innerHTML =
|
||||
'<div class="text-center text-gray-500 py-8"><p>Interacción archivada</p></div>';
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncInteractions() {
|
||||
const icon = document.getElementById('sync-icon');
|
||||
icon.style.animation = 'spin 1s linear infinite';
|
||||
|
||||
try {
|
||||
await fetch('/api/interactions/sync', { method: 'POST' });
|
||||
await loadInteractions();
|
||||
} catch (error) {
|
||||
alert('Error al sincronizar: ' + error.message);
|
||||
}
|
||||
|
||||
icon.style.animation = '';
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 60) return `hace ${diffMins}m`;
|
||||
if (diffHours < 24) return `hace ${diffHours}h`;
|
||||
if (diffDays < 7) return `hace ${diffDays}d`;
|
||||
|
||||
return date.toLocaleDateString('es-MX', { day: '2-digit', month: 'short' });
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user