Automation_Threads_EU / dashboard.html
FincH12's picture
Deploy Dynamic Dashboard, Analytics and Prompts Editor
fd40f81 verified
Raw History Blame Contribute Delete
25 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Threads Automation Agent</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-base: #050505;
--bg-surface: rgba(18, 18, 20, 0.6);
--bg-surface-hover: rgba(28, 28, 30, 0.8);
--glass-border: rgba(255, 255, 255, 0.08);
--accent-primary: #00f0ff;
--accent-secondary: #7000ff;
--text-main: #f4f4f5;
--text-muted: #a1a1aa;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Outfit', sans-serif;
background-color: var(--bg-base);
color: var(--text-main);
min-height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
/* Dynamic Background */
.ambient-bg {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: -1; overflow: hidden; background: var(--bg-base);
}
.orb {
position: absolute; border-radius: 50%; filter: blur(100px); opacity: 0.4;
animation: float 20s infinite alternate cubic-bezier(0.4, 0, 0.2, 1);
}
.orb-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: radial-gradient(circle, var(--accent-secondary) 0%, transparent 70%); animation-delay: -5s; }
.orb-2 { bottom: -20%; right: -10%; width: 60vw; height: 60vw; background: radial-gradient(circle, var(--accent-primary) 0%, transparent 70%); }
@keyframes float { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(10%, 15%) scale(1.2); } }
/* Layout */
.app-container {
max-width: 1500px; margin: 0 auto; padding: 2rem;
flex: 1; display: flex; flex-direction: column; gap: 2rem; width: 100%;
}
/* Header */
header {
display: flex; justify-content: space-between; align-items: center;
padding: 1.5rem 2rem; background: var(--bg-surface);
backdrop-filter: blur(20px); border: 1px solid var(--glass-border);
border-radius: 24px; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.logo-area { display: flex; align-items: center; gap: 1rem; }
.logo-icon {
width: 48px; height: 48px; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: 14px; display: flex; align-items: center; justify-content: center;
box-shadow: 0 0 20px rgba(112, 0, 255, 0.4);
}
.logo-icon svg { width: 28px; height: 28px; color: white; }
.title { font-size: 1.8rem; font-weight: 800; background: linear-gradient(to right, #fff, #a1a1aa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.status-pill {
display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem;
background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 99px; font-size: 0.9rem; font-weight: 600; color: var(--success);
}
.pulse-dot { width: 8px; height: 8px; background: var(--success); border-radius: 50%; animation: ping 1.5s infinite; }
@keyframes ping { 75%, 100% { transform: scale(2.5); opacity: 0; } }
/* Tabs Nav */
.tabs {
display: flex; gap: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem;
}
.tab-btn {
background: transparent; color: var(--text-muted); border: none; font-size: 1.1rem;
font-family: 'Outfit'; font-weight: 600; cursor: pointer; padding: 0.5rem 1rem;
border-radius: 8px; transition: 0.2s;
}
.tab-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
.tab-btn.active { color: var(--accent-primary); background: rgba(0, 240, 255, 0.1); }
/* Main Grid */
.dashboard-grid {
display: grid; grid-template-columns: 1fr 400px; gap: 2rem; height: calc(100vh - 250px); min-height: 600px;
}
.tab-content { display: none; flex-direction: column; gap: 1.5rem; overflow-y: auto; padding-right: 10px;}
.tab-content.active { display: flex; }
.tab-content::-webkit-scrollbar { width: 6px; }
.tab-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
/* Panels */
.panel {
background: var(--bg-surface); backdrop-filter: blur(20px);
border: 1px solid var(--glass-border); border-radius: 24px; padding: 1.5rem;
display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.panel-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--glass-border); justify-content: space-between;}
.panel-title { font-size: 1.1rem; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 1px; display: flex; align-items: center; gap: 0.5rem;}
/* Stats & Overview */
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.stat-box { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); border-radius: 16px; padding: 1.25rem; border-left: 4px solid var(--accent-primary); }
.stat-box:nth-child(2) { border-left-color: var(--accent-secondary); }
.stat-box:nth-child(3) { border-left-color: var(--success); }
.stat-label { font-size: 0.85rem; color: var(--text-muted); text-transform: uppercase; }
.stat-value { font-size: 2.5rem; font-weight: 800; color: #fff; line-height: 1.2; }
.chart-container { height: 250px; width: 100%; position: relative; }
.post-card { background: rgba(0,0,0,0.2); border: 1px solid var(--glass-border); border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem;}
.post-text { color: #e4e4e7; margin-bottom: 0.5rem; font-size: 0.9rem; }
.post-metrics { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--accent-primary); font-weight: 600; }
.metric-badge { background: rgba(0, 240, 255, 0.1); padding: 2px 8px; border-radius: 4px; }
/* Forms & Inputs */
.form-group { margin-bottom: 1rem; }
.form-label { display: block; margin-bottom: 0.5rem; color: #e4e4e7; font-size: 0.9rem; font-weight: 500; }
.form-input {
width: 100%; padding: 0.75rem 1rem; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border);
border-radius: 8px; color: #fff; font-family: 'Outfit'; font-size: 1rem; transition: border 0.2s;
}
.form-input:focus { outline: none; border-color: var(--accent-primary); }
textarea.form-input { min-height: 250px; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; resize: vertical; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.btn-save {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: #fff; border: none; padding: 0.75rem 2rem; border-radius: 8px;
font-family: 'Outfit'; font-weight: 700; font-size: 1rem; cursor: pointer;
box-shadow: 0 4px 15px rgba(112,0,255,0.3); transition: transform 0.2s;
}
.btn-save:hover { transform: translateY(-2px); }
.toast {
position: fixed; bottom: 20px; right: 20px; background: var(--success); color: #000;
padding: 1rem 2rem; border-radius: 8px; font-weight: 700; font-size: 1rem;
box-shadow: 0 4px 20px rgba(16,185,129,0.4); transform: translateY(150%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
}
.toast.show { transform: translateY(0); }
/* Terminal */
.terminal-panel { height: 100%; }
.terminal-window {
flex: 1; background: #09090b; border-radius: 12px; padding: 1rem;
font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: #d4d4d8;
overflow-y: auto; border: 1px solid #27272a; line-height: 1.6;
}
.log-time { color: #52525b; margin-right: 8px; }
.log-error { color: var(--error); font-weight: 700; }
.log-success { color: var(--success); font-weight: 700; }
.log-warning { color: var(--warning); }
.log-info { color: var(--accent-primary); }
</style>
</head>
<body>
<div class="ambient-bg">
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
</div>
<div class="app-container">
<header>
<div class="logo-area">
<div class="logo-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg></div>
<div class="title">Threads Automation Agent</div>
</div>
<div class="status-pill">
<div class="pulse-dot"></div> System Online
</div>
</header>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('overview')">📊 Overview & Analytics</button>
<button class="tab-btn" onclick="switchTab('settings')">⚙️ Settings & Limits</button>
<button class="tab-btn" onclick="switchTab('prompts')">🧠 Prompt Engineering</button>
</div>
<div class="dashboard-grid">
<!-- Left Column: Tabs Content -->
<div style="display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<!-- TAB: OVERVIEW -->
<div id="tab-overview" class="tab-content active">
<div class="stats-row">
<div class="stat-box"><div class="stat-label">Total Posts</div><div class="stat-value" id="val-posts">0</div></div>
<div class="stat-box"><div class="stat-label">Total Likes</div><div class="stat-value" id="val-likes">0</div></div>
<div class="stat-box"><div class="stat-label">Total Replies</div><div class="stat-value" id="val-replies">0</div></div>
</div>
<div class="panel">
<div class="panel-header"><div class="panel-title">📈 Engagement Analytics</div></div>
<div class="chart-container">
<canvas id="analyticsChart"></canvas>
</div>
</div>
<div class="panel">
<div class="panel-header"><div class="panel-title">⭐ Top Performing Posts</div></div>
<div id="top-posts-container">
<div style="color:var(--text-muted); text-align:center; padding: 1rem 0;">Awaiting data...</div>
</div>
</div>
</div>
<!-- TAB: SETTINGS -->
<div id="tab-settings" class="tab-content">
<div class="panel">
<div class="panel-header">
<div class="panel-title">⚙️ Bot Configuration</div>
<button class="btn-save" onclick="saveSettings()">Save Changes</button>
</div>
<h3 style="margin-bottom: 1rem; color: var(--accent-primary);">Schedule Slots (Kyiv Time)</h3>
<div class="form-row">
<div class="form-group">
<label class="form-label">Morning Slot Start</label>
<input type="time" class="form-input" id="set_morning_start">
</div>
<div class="form-group">
<label class="form-label">Morning Slot End</label>
<input type="time" class="form-input" id="set_morning_end">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Evening Slot Start</label>
<input type="time" class="form-input" id="set_evening_start">
</div>
<div class="form-group">
<label class="form-label">Evening Slot End</label>
<input type="time" class="form-input" id="set_evening_end">
</div>
</div>
<h3 style="margin: 1.5rem 0 1rem 0; color: var(--accent-primary);">Daily Limits & Content</h3>
<div class="form-row">
<div class="form-group">
<label class="form-label">Daily Single Posts Limit</label>
<input type="number" class="form-input" id="set_single_limit" min="0" max="10">
</div>
<div class="form-group">
<label class="form-label">Daily Carousels Limit</label>
<input type="number" class="form-input" id="set_carousel_limit" min="0" max="10">
</div>
</div>
<div class="form-group">
<label class="form-label">Slides per Carousel</label>
<input type="number" class="form-input" id="set_slide_count" min="2" max="10">
</div>
</div>
</div>
<!-- TAB: PROMPTS -->
<div id="tab-prompts" class="tab-content">
<div class="panel">
<div class="panel-header">
<div class="panel-title">🧠 AI Prompt Editor</div>
<button class="btn-save" onclick="saveSettings()">Save Prompts</button>
</div>
<div class="form-group">
<label class="form-label">Single Post Generation Prompt</label>
<textarea class="form-input" id="set_prompt_single"></textarea>
<small style="color:var(--text-muted); display:block; margin-top:5px;">Available vars: {news_context}, {exclude_str}, {tone}</small>
</div>
<div class="form-group">
<label class="form-label">Carousel Generation Prompt</label>
<textarea class="form-input" id="set_prompt_carousel"></textarea>
<small style="color:var(--text-muted); display:block; margin-top:5px;">Available vars: {news_context}, {exclude_str}, {tone}, {slide_count}</small>
</div>
<div class="form-group">
<label class="form-label">Viral Predictor AI Prompt</label>
<textarea class="form-input" id="set_prompt_predictor"></textarea>
</div>
</div>
</div>
</div>
<!-- Right Column: Terminal -->
<div class="panel terminal-panel">
<div class="panel-header">
<div class="panel-title">Terminal Logs</div>
</div>
<div class="terminal-window" id="terminal-window">
<div style="color: #a1a1aa;">> Initiating connection...</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast">Settings Saved Successfully!</div>
<script>
let myChart = null;
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
}
function showToast() {
const t = document.getElementById('toast');
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
async function loadSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
document.getElementById('set_morning_start').value = data.MORNING_SLOT_START;
document.getElementById('set_morning_end').value = data.MORNING_SLOT_END;
document.getElementById('set_evening_start').value = data.EVENING_SLOT_START;
document.getElementById('set_evening_end').value = data.EVENING_SLOT_END;
document.getElementById('set_single_limit').value = data.DAILY_SINGLE_POSTS_LIMIT;
document.getElementById('set_carousel_limit').value = data.DAILY_CAROUSELS_LIMIT;
document.getElementById('set_slide_count').value = data.CAROUSEL_SLIDE_COUNT;
document.getElementById('set_prompt_single').value = data.PROMPT_SINGLE;
document.getElementById('set_prompt_carousel').value = data.PROMPT_CAROUSEL;
document.getElementById('set_prompt_predictor').value = data.PROMPT_PREDICTOR;
} catch (err) { console.error("Error loading settings:", err); }
}
async function saveSettings() {
const data = {
MORNING_SLOT_START: document.getElementById('set_morning_start').value,
MORNING_SLOT_END: document.getElementById('set_morning_end').value,
EVENING_SLOT_START: document.getElementById('set_evening_start').value,
EVENING_SLOT_END: document.getElementById('set_evening_end').value,
DAILY_SINGLE_POSTS_LIMIT: parseInt(document.getElementById('set_single_limit').value),
DAILY_CAROUSELS_LIMIT: parseInt(document.getElementById('set_carousel_limit').value),
CAROUSEL_SLIDE_COUNT: parseInt(document.getElementById('set_slide_count').value),
PROMPT_SINGLE: document.getElementById('set_prompt_single').value,
PROMPT_CAROUSEL: document.getElementById('set_prompt_carousel').value,
PROMPT_PREDICTOR: document.getElementById('set_prompt_predictor').value,
};
try {
const res = await fetch('/api/save_settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if(res.ok) showToast();
} catch (err) { alert("Error saving settings."); }
}
// Analytics Chart Setup
function initChart(chartData) {
const ctx = document.getElementById('analyticsChart').getContext('2d');
// Dummy historical data simulation if not enough data
const labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const likesData = [0,0,0,0,0,0, chartData.total_likes];
const repliesData = [0,0,0,0,0,0, chartData.total_replies];
if(myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Likes',
data: likesData,
borderColor: '#00f0ff',
backgroundColor: 'rgba(0, 240, 255, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: 'Comments',
data: repliesData,
borderColor: '#7000ff',
backgroundColor: 'rgba(112, 0, 255, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#a1a1aa' } } },
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#a1a1aa' } },
x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#a1a1aa' } }
}
}
});
}
// Terminal & Stats Logic
function colorizeLog(line) {
if (line.includes('[ERROR]') || line.includes('Exception') || line.includes('Error')) return `<span class="log-error">${line}</span>`;
if (line.includes('SUCCESS') || line.includes('одобрен') || line.includes('успешно')) return `<span class="log-success">${line}</span>`;
if (line.includes('Predictor') || line.includes('[Auto-Improvement]')) return `<span class="log-warning">${line}</span>`;
if (line.includes('Scheduler') || line.includes('Keep-Alive') || line.includes('Content Generator')) return `<span class="log-info">${line}</span>`;
return line.replace(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\])(.*)/, '<span class="log-time">$1</span>$2');
}
let lastLogs = "";
async function fetchLogs() {
try {
const res = await fetch('/api/logs');
const data = await res.json();
const term = document.getElementById('terminal-window');
if (data.logs !== lastLogs) {
const isScrolledToBottom = term.scrollHeight - term.clientHeight <= term.scrollTop + 5;
term.innerHTML = data.logs.split('\n').filter(l => l.trim() !== '').map(l => colorizeLog(l)).join('<br>') || '<span style="color:#a1a1aa">> No logs...</span>';
lastLogs = data.logs;
if (isScrolledToBottom) term.scrollTop = term.scrollHeight;
}
} catch(e) {}
}
async function fetchStats() {
try {
const res = await fetch('/api/stats');
const data = await res.json();
document.getElementById('val-posts').innerText = data.total_posts;
document.getElementById('val-likes').innerText = data.total_likes;
document.getElementById('val-replies').innerText = data.total_replies;
const tc = document.getElementById('top-posts-container');
if (data.top_posts && data.top_posts.length > 0) {
tc.innerHTML = data.top_posts.map(post => `
<div class="post-card">
<div class="post-text">${post.text.length > 120 ? post.text.substring(0,120)+'...' : post.text}</div>
<div class="post-metrics">
<span class="metric-badge">❤️ ${post.likes || 0}</span>
<span class="metric-badge">💬 ${post.replies || 0}</span>
${post.predicted_score ? `<span class="metric-badge">Score: ${post.predicted_score}/10</span>` : ''}
</div>
</div>
`).join('');
}
if(!myChart) initChart(data);
} catch(e) {}
}
// Init
loadSettings();
fetchLogs();
fetchStats();
setInterval(fetchLogs, 2000);
setInterval(fetchStats, 10000);
</script>
</body>
</html>