@@ -188,6 +191,7 @@ function switchTab(name) {
document.querySelector(`.tab-content#tab-${name}`).classList.add('active');
event.target.classList.add('active');
if (name === 'history') loadHistory();
+ if (name === 'briefs') loadSavedBriefs();
}
// ─── JSON upload ───
@@ -219,6 +223,118 @@ function loadJSON(input) {
reader.readAsText(file);
}
+// ─── Build brief from form ───
+function buildBriefFromForm() {
+ const splitVal = (id) => document.getElementById(id).value.split(',').map(s => s.trim()).filter(Boolean);
+ const platforms = [];
+ if (document.getElementById('p-tiktok').checked) platforms.push('tiktok');
+ if (document.getElementById('p-instagram').checked) platforms.push('instagram');
+ if (document.getElementById('p-youtube').checked) platforms.push('youtube');
+ return {
+ clientName: document.getElementById('clientName').value,
+ category: document.getElementById('category').value,
+ hashtags: splitVal('hashtags'),
+ keywords: splitVal('keywords'),
+ platforms,
+ influencers: {
+ tiktok: splitVal('inf-tiktok'),
+ instagram: splitVal('inf-instagram'),
+ youtube: splitVal('inf-youtube'),
+ },
+ dateRange: (loadedBrief && loadedBrief.dateRange) ? loadedBrief.dateRange : undefined,
+ };
+}
+
+function populateForm(brief) {
+ loadedBrief = brief;
+ if (brief.clientName) document.getElementById('clientName').value = brief.clientName;
+ if (brief.category) document.getElementById('category').value = brief.category;
+ if (brief.hashtags) document.getElementById('hashtags').value = brief.hashtags.join(', ');
+ if (brief.keywords) document.getElementById('keywords').value = brief.keywords.join(', ');
+ document.getElementById('p-tiktok').checked = (brief.platforms || []).includes('tiktok');
+ document.getElementById('p-instagram').checked = (brief.platforms || []).includes('instagram');
+ document.getElementById('p-youtube').checked = (brief.platforms || []).includes('youtube');
+ if (brief.influencers) {
+ if (brief.influencers.tiktok) document.getElementById('inf-tiktok').value = brief.influencers.tiktok.join(', ');
+ if (brief.influencers.instagram) document.getElementById('inf-instagram').value = brief.influencers.instagram.join(', ');
+ if (brief.influencers.youtube) document.getElementById('inf-youtube').value = brief.influencers.youtube.join(', ');
+ }
+}
+
+// ─── Save/load briefs to server ───
+async function saveBriefToServer() {
+ const brief = buildBriefFromForm();
+ if (!brief.clientName) { alert('Enter a client name first'); return; }
+ try {
+ const res = await fetch('/api/briefs', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(brief),
+ });
+ const data = await res.json();
+ if (data.ok) {
+ document.getElementById('jsonFileName').textContent = 'Saved to server!';
+ setTimeout(() => { document.getElementById('jsonFileName').textContent = ''; }, 2000);
+ } else { alert('Save failed: ' + (data.error || 'unknown')); }
+ } catch (err) { alert('Save failed: ' + err.message); }
+}
+
+async function loadSavedBriefs() {
+ const el = document.getElementById('briefsContent');
+ try {
+ const res = await fetch('/api/briefs');
+ const briefs = await res.json();
+ if (!briefs.length) {
+ el.innerHTML = '
No saved briefs yet. Fill in a brief on the Pipeline tab and click "Save Current Brief".
';
+ return;
+ }
+ el.innerHTML = `
${briefs.map(b => {
+ const d = b.data;
+ const platforms = (d.platforms || []).join(', ');
+ const hashtags = (d.hashtags || []).slice(0, 5).join(', ');
+ const infCount = Object.values(d.influencers || {}).flat().length;
+ return `
`;
+ }).join('')}
`;
+ } catch (err) {
+ el.innerHTML = `
Failed to load briefs: ${err.message}
`;
+ }
+}
+
+function loadBriefAndSwitch(jsonStr) {
+ const brief = JSON.parse(jsonStr);
+ populateForm(brief);
+ document.getElementById('jsonFileName').textContent = brief.clientName + ' (loaded)';
+ // Switch to pipeline tab
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
+ document.getElementById('tab-pipeline').classList.add('active');
+ document.querySelector('.tab').classList.add('active');
+}
+
+async function deleteServerBrief(name) {
+ if (!confirm(`Delete saved brief "${name}"?`)) return;
+ try {
+ await fetch(`/api/briefs/${encodeURIComponent(name)}`, { method: 'DELETE' });
+ loadSavedBriefs();
+ } catch {}
+}
+
// ─── Cost display ───
function updateCosts() {
const total = totalClaude + totalApify;
diff --git a/agents/social-listening/dashboard/server.ts b/agents/social-listening/dashboard/server.ts
index d65caa1..d6ec3ca 100644
--- a/agents/social-listening/dashboard/server.ts
+++ b/agents/social-listening/dashboard/server.ts
@@ -1,7 +1,7 @@
#!/usr/bin/env tsx
// ─── Dashboard Server (HTTP + SSE) ───
import { createServer, IncomingMessage, ServerResponse } from 'http';
-import { readFileSync } from 'fs';
+import { readFileSync, writeFileSync, readdirSync, unlinkSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { createHmac, randomBytes } from 'crypto';
import { runPipeline } from '../pipeline-v2.js';
@@ -11,6 +11,8 @@ import { getApifyCostLimit } from '../apify.js';
const PORT = parseInt(process.env.DASHBOARD_PORT || '3456', 10);
const __dir = new URL('.', import.meta.url).pathname;
+const BRIEFS_DIR = join(__dir, '..', 'briefs');
+if (!existsSync(BRIEFS_DIR)) mkdirSync(BRIEFS_DIR, { recursive: true });
// ─── Auth ───
const DASH_USER = process.env.DASH_USER || 'admin';
@@ -264,6 +266,51 @@ const server = createServer(async (req, res) => {
return;
}
+ // ─── Briefs API ───
+
+ if (url.pathname === '/api/briefs' && req.method === 'GET') {
+ try {
+ const files = readdirSync(BRIEFS_DIR).filter(f => f.endsWith('.json'));
+ const briefs = files.map(f => {
+ const data = JSON.parse(readFileSync(join(BRIEFS_DIR, f), 'utf-8'));
+ return { name: f.replace(/\.json$/, ''), data };
+ });
+ sendJSON(res, 200, briefs);
+ } catch (err) {
+ sendJSON(res, 500, { error: (err as Error).message });
+ }
+ return;
+ }
+
+ if (url.pathname === '/api/briefs' && req.method === 'POST') {
+ const body = await parseBody(req);
+ try {
+ const brief = JSON.parse(body);
+ const name = (brief.clientName || 'untitled').replace(/[^a-zA-Z0-9_&-]/g, '-').toLowerCase();
+ writeFileSync(join(BRIEFS_DIR, `${name}.json`), JSON.stringify(brief, null, 2));
+ sendJSON(res, 200, { ok: true, name });
+ } catch (err) {
+ sendJSON(res, 400, { error: (err as Error).message });
+ }
+ return;
+ }
+
+ if (url.pathname.startsWith('/api/briefs/') && req.method === 'DELETE') {
+ const name = decodeURIComponent(url.pathname.split('/')[3]);
+ const filePath = join(BRIEFS_DIR, `${name}.json`);
+ try {
+ if (existsSync(filePath)) {
+ unlinkSync(filePath);
+ sendJSON(res, 200, { ok: true });
+ } else {
+ sendJSON(res, 404, { error: 'Brief not found' });
+ }
+ } catch (err) {
+ sendJSON(res, 500, { error: (err as Error).message });
+ }
+ return;
+ }
+
// ─── Routes ───
if (url.pathname === '/' && req.method === 'GET') {
diff --git a/frontend/index.html b/frontend/index.html
index c5ca82e..10e6bd5 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -79,6 +79,7 @@ button.run:disabled { background: #333; color: #666; cursor: not-allowed; }
Pipeline
+
Saved Briefs
Run History
@@ -86,15 +87,12 @@ button.run:disabled { background: #333; color: #666; cursor: not-allowed; }
+
+
+
@@ -191,6 +194,7 @@ function switchTab(name) {
document.querySelector(`.tab-content#tab-${name}`).classList.add('active');
event.target.classList.add('active');
if (name === 'history') loadHistory();
+ if (name === 'briefs') loadSavedBriefs();
}
// ─── JSON upload ───
@@ -222,6 +226,121 @@ function loadJSON(input) {
reader.readAsText(file);
}
+// ─── Build brief from form ───
+function buildBriefFromForm() {
+ const splitVal = (id) => document.getElementById(id).value.split(',').map(s => s.trim()).filter(Boolean);
+ const platforms = [];
+ if (document.getElementById('p-tiktok').checked) platforms.push('tiktok');
+ if (document.getElementById('p-instagram').checked) platforms.push('instagram');
+ if (document.getElementById('p-youtube').checked) platforms.push('youtube');
+ return {
+ clientName: document.getElementById('clientName').value,
+ category: document.getElementById('category').value,
+ hashtags: splitVal('hashtags'),
+ keywords: splitVal('keywords'),
+ platforms,
+ influencers: {
+ tiktok: splitVal('inf-tiktok'),
+ instagram: splitVal('inf-instagram'),
+ youtube: splitVal('inf-youtube'),
+ },
+ dateRange: (loadedBrief && loadedBrief.dateRange) ? loadedBrief.dateRange : undefined,
+ };
+}
+
+function populateForm(brief) {
+ loadedBrief = brief;
+ if (brief.clientName) document.getElementById('clientName').value = brief.clientName;
+ if (brief.category) document.getElementById('category').value = brief.category;
+ if (brief.hashtags) document.getElementById('hashtags').value = brief.hashtags.join(', ');
+ if (brief.keywords) document.getElementById('keywords').value = brief.keywords.join(', ');
+ document.getElementById('p-tiktok').checked = (brief.platforms || []).includes('tiktok');
+ document.getElementById('p-instagram').checked = (brief.platforms || []).includes('instagram');
+ document.getElementById('p-youtube').checked = (brief.platforms || []).includes('youtube');
+ if (brief.influencers) {
+ if (brief.influencers.tiktok) document.getElementById('inf-tiktok').value = brief.influencers.tiktok.join(', ');
+ if (brief.influencers.instagram) document.getElementById('inf-instagram').value = brief.influencers.instagram.join(', ');
+ if (brief.influencers.youtube) document.getElementById('inf-youtube').value = brief.influencers.youtube.join(', ');
+ }
+ document.getElementById('jsonPreview').style.display = 'block';
+ document.getElementById('jsonPreviewText').textContent = JSON.stringify(brief, null, 2);
+}
+
+// ─── Save/load briefs to server ───
+async function saveBriefToServer() {
+ const brief = buildBriefFromForm();
+ if (!brief.clientName) { alert('Enter a client name first'); return; }
+ try {
+ const res = await fetch(API + '/api/briefs', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify(brief),
+ });
+ const data = await res.json();
+ if (data.ok) {
+ document.getElementById('jsonFileName').textContent = 'Saved to server!';
+ setTimeout(() => { document.getElementById('jsonFileName').textContent = ''; }, 2000);
+ } else { alert('Save failed: ' + (data.error || 'unknown')); }
+ } catch (err) { alert('Save failed: ' + err.message); }
+}
+
+async function loadSavedBriefs() {
+ const el = document.getElementById('briefsContent');
+ try {
+ const res = await fetch(API + '/api/briefs', { credentials: 'include' });
+ const briefs = await res.json();
+ if (!briefs.length) {
+ el.innerHTML = '
No saved briefs yet. Fill in a brief on the Pipeline tab and click "Save Current Brief".
';
+ return;
+ }
+ el.innerHTML = `
${briefs.map(b => {
+ const d = b.data;
+ const platforms = (d.platforms || []).join(', ');
+ const hashtags = (d.hashtags || []).slice(0, 5).join(', ');
+ const infCount = Object.values(d.influencers || {}).flat().length;
+ return `
`;
+ }).join('')}
`;
+ } catch (err) {
+ el.innerHTML = `
Failed to load briefs: ${err.message}
`;
+ }
+}
+
+function loadBriefAndSwitch(jsonStr) {
+ const brief = JSON.parse(jsonStr);
+ populateForm(brief);
+ document.getElementById('jsonFileName').textContent = brief.clientName + ' (loaded)';
+ // Switch to pipeline tab
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
+ document.getElementById('tab-pipeline').classList.add('active');
+ document.querySelector('.tab').classList.add('active'); // first tab = Pipeline
+}
+
+async function deleteServerBrief(name) {
+ if (!confirm(`Delete saved brief "${name}"?`)) return;
+ try {
+ await fetch(API + `/api/briefs/${encodeURIComponent(name)}`, { method: 'DELETE', credentials: 'include' });
+ loadSavedBriefs();
+ } catch {}
+}
+
// ─── Cost display ───
function updateCosts() {
const total = totalClaude + totalApify;