|
@@ -2,6 +2,7 @@ let requirements = [];
|
|
|
let currentSelectedIndex = null;
|
|
let currentSelectedIndex = null;
|
|
|
let activeRuns = {};
|
|
let activeRuns = {};
|
|
|
let statusInterval = null;
|
|
let statusInterval = null;
|
|
|
|
|
+let currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
|
|
|
|
|
|
|
|
let currentPromptName = null;
|
|
let currentPromptName = null;
|
|
|
const modalPrompts = document.getElementById('prompts-modal');
|
|
const modalPrompts = document.getElementById('prompts-modal');
|
|
@@ -41,6 +42,7 @@ if (selectForcePhase && groupPlatforms) {
|
|
|
const jsonStrategy = document.getElementById('json-strategy');
|
|
const jsonStrategy = document.getElementById('json-strategy');
|
|
|
const jsonBlueprint = document.getElementById('json-blueprint');
|
|
const jsonBlueprint = document.getElementById('json-blueprint');
|
|
|
const jsonCaps = document.getElementById('json-caps');
|
|
const jsonCaps = document.getElementById('json-caps');
|
|
|
|
|
+const jsonSource = document.getElementById('json-source');
|
|
|
const jsonRaw = document.getElementById('json-raw');
|
|
const jsonRaw = document.getElementById('json-raw');
|
|
|
|
|
|
|
|
// Modals
|
|
// Modals
|
|
@@ -100,6 +102,18 @@ function renderJSON(obj) {
|
|
|
return String(obj);
|
|
return String(obj);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function renderDataOrRaw(dataObj, renderFunc) {
|
|
|
|
|
+ if (!dataObj) return '<p style="color:var(--text-muted)">No data available</p>';
|
|
|
|
|
+ if (dataObj.error) {
|
|
|
|
|
+ const safeRaw = dataObj.raw_content ? dataObj.raw_content.replace(/</g, "<").replace(/>/g, ">") : "Empty file.";
|
|
|
|
|
+ return `<div style="padding:1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:8px;">
|
|
|
|
|
+ <h3 style="color:var(--danger); margin-bottom:0.5rem">⚠️ JSON Parsing Failed</h3>
|
|
|
|
|
+ <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return renderFunc(dataObj);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function renderRawCases(rawCasesObj) {
|
|
function renderRawCases(rawCasesObj) {
|
|
|
if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data';
|
|
if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data';
|
|
|
const platforms = Object.keys(rawCasesObj);
|
|
const platforms = Object.keys(rawCasesObj);
|
|
@@ -111,9 +125,22 @@ function renderRawCases(rawCasesObj) {
|
|
|
html += `</div><div class="sub-tab-contents">`;
|
|
html += `</div><div class="sub-tab-contents">`;
|
|
|
platforms.forEach((p, i) => {
|
|
platforms.forEach((p, i) => {
|
|
|
html += `<div id="sub-tab-${p}" class="sub-tab-pane ${i === 0 ? '' : 'hidden'}">`;
|
|
html += `<div id="sub-tab-${p}" class="sub-tab-pane ${i === 0 ? '' : 'hidden'}">`;
|
|
|
|
|
+
|
|
|
|
|
+ if (rawCasesObj[p].error) {
|
|
|
|
|
+ const safeRaw = rawCasesObj[p].raw_content ? rawCasesObj[p].raw_content.replace(/</g, "<").replace(/>/g, ">") : "Empty file.";
|
|
|
|
|
+ html += `<div style="padding:1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:8px;">
|
|
|
|
|
+ <h3 style="color:var(--danger); margin-bottom:0.5rem">⚠️ JSON Parsing Failed</h3>
|
|
|
|
|
+ <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
|
|
|
|
|
+ </div></div>`;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const cases = rawCasesObj[p].cases || [];
|
|
const cases = rawCasesObj[p].cases || [];
|
|
|
if (cases.length === 0) {
|
|
if (cases.length === 0) {
|
|
|
- html += `<p style="color:var(--text-muted)">No cases found for this platform.</p>`;
|
|
|
|
|
|
|
+ html += `<div style="padding:1rem; background:rgba(255, 255, 255, 0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px;">
|
|
|
|
|
+ <h3 style="color:var(--text-main); margin-bottom:0.5rem">⚠️ Non-standard format (No cases array found)</h3>
|
|
|
|
|
+ <div style="font-family: monospace; font-size: 0.85rem; overflow-x: auto">${renderJSON(rawCasesObj[p])}</div>
|
|
|
|
|
+ </div>`;
|
|
|
} else {
|
|
} else {
|
|
|
const platCode = p.replace('case_', '');
|
|
const platCode = p.replace('case_', '');
|
|
|
cases.forEach(c => {
|
|
cases.forEach(c => {
|
|
@@ -169,6 +196,110 @@ function renderRawCases(rawCasesObj) {
|
|
|
return html;
|
|
return html;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function renderSourceCases(sourceObj) {
|
|
|
|
|
+ if (!sourceObj) return '<p style="color:var(--text-muted)">No source data available</p>';
|
|
|
|
|
+ if (sourceObj.error) {
|
|
|
|
|
+ const safeRaw = sourceObj.raw_content ? sourceObj.raw_content.replace(/</g, "<").replace(/>/g, ">") : "Empty file.";
|
|
|
|
|
+ return `<div style="padding:1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:8px;">
|
|
|
|
|
+ <h3 style="color:var(--danger); margin-bottom:0.5rem">⚠️ JSON Parsing Failed</h3>
|
|
|
|
|
+ <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!sourceObj.sources) {
|
|
|
|
|
+ return `<div style="padding:1rem; background:rgba(255, 255, 255, 0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px;">
|
|
|
|
|
+ <h3 style="color:var(--text-main); margin-bottom:0.5rem">⚠️ Non-standard source format</h3>
|
|
|
|
|
+ <div style="font-family: monospace; font-size: 0.85rem; overflow-x: auto">${renderJSON(sourceObj)}</div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const sources = sourceObj.sources;
|
|
|
|
|
+ let html = `<input type="text" placeholder="🔍 Search sources by ID, title, content, or platform..."
|
|
|
|
|
+ style="width: 100%; padding: 0.8rem 1rem; margin-bottom: 1.5rem; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.2); color: var(--text-main); font-size: 0.95rem; outline: none; transition: border-color 0.2s;"
|
|
|
|
|
+ onfocus="this.style.borderColor='var(--primary)'" onblur="this.style.borderColor='rgba(255,255,255,0.15)'"
|
|
|
|
|
+ oninput="filterSources(this.value)" />`;
|
|
|
|
|
+
|
|
|
|
|
+ html += `<div id="sub-tab-source">`; // Container for searching
|
|
|
|
|
+
|
|
|
|
|
+ if (sources.length === 0) {
|
|
|
|
|
+ html += `<p style="color:var(--text-muted)">Source file is empty.</p>`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sources.forEach((s, idx) => {
|
|
|
|
|
+ const post = s.post || {};
|
|
|
|
|
+
|
|
|
|
|
+ let mediaHtml = '';
|
|
|
|
|
+
|
|
|
|
|
+ // Handle images (XHS uses images string array, X uses image_url_list object array)
|
|
|
|
|
+ const images = post.images || [];
|
|
|
|
|
+ const xImages = post.image_url_list || [];
|
|
|
|
|
+ const allImages = [...images, ...xImages.map(img => img.image_url)].filter(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ if (allImages.length > 0) {
|
|
|
|
|
+ mediaHtml += `<div style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 8px;">`;
|
|
|
|
|
+ allImages.forEach(imgUrl => {
|
|
|
|
|
+ mediaHtml += `<a href="${imgUrl}" target="_blank"><img src="${imgUrl}" style="height: 120px; border-radius: 6px; object-fit: cover; border: 1px solid rgba(255,255,255,0.1);" /></a>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ mediaHtml += `</div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Handle videos
|
|
|
|
|
+ const videos = post.videos || [];
|
|
|
|
|
+ const xVideos = post.video_url_list || [];
|
|
|
|
|
+ const allVideos = [...videos, ...xVideos.map(vid => vid.video_url)].filter(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ if (allVideos.length > 0) {
|
|
|
|
|
+ mediaHtml += `<div style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 8px;">`;
|
|
|
|
|
+ allVideos.forEach(vidUrl => {
|
|
|
|
|
+ mediaHtml += `<video controls src="${vidUrl}" style="height: 200px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1);"></video>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ mediaHtml += `</div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ html += `<div class="data-card" id="source-card-${idx}">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="card-title">📝 [${s.platform || post.channel || 'Unknown'}] ${post.title || 'Untitled'}</div>
|
|
|
|
|
+ ${s.source_url ? `<a href="${s.source_url}" target="_blank" class="badge-emoji primary" style="text-decoration:none">🔗 View Source</a>` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <div class="tags-container" style="margin-bottom:0.8rem">
|
|
|
|
|
+ <span class="badge-emoji">📱 Platform: ${s.platform || post.channel || 'N/A'}</span>
|
|
|
|
|
+ <span class="badge-emoji">❤️ Likes: ${post.like_count || 0}</span>
|
|
|
|
|
+ ${s.channel_content_id ? `<span class="badge-emoji">🆔 ID: ${s.channel_content_id}</span>` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="background:rgba(255,255,255,0.03); padding:1rem; border-radius:6px; font-size:0.9rem; line-height:1.5; color:var(--text-main); white-space: pre-wrap;">${post.body_text || 'No content available.'}</div>
|
|
|
|
|
+ ${mediaHtml}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ html += `</div>`;
|
|
|
|
|
+ return html;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+window.selectSubTab = function(p) {
|
|
|
|
|
+ document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => {
|
|
|
|
|
+ btn.classList.remove('active');
|
|
|
|
|
+ if(btn.textContent === p.replace('case_', '').toUpperCase()) btn.classList.add('active');
|
|
|
|
|
+ });
|
|
|
|
|
+ document.querySelectorAll('#json-raw .sub-tab-pane').forEach(pane => {
|
|
|
|
|
+ pane.classList.add('hidden');
|
|
|
|
|
+ });
|
|
|
|
|
+ const target = document.getElementById(`sub-tab-${p}`);
|
|
|
|
|
+ if (target) target.classList.remove('hidden');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+window.filterSources = function(query) {
|
|
|
|
|
+ const q = query.toLowerCase();
|
|
|
|
|
+ const cards = document.querySelectorAll('#sub-tab-source .data-card');
|
|
|
|
|
+ cards.forEach(card => {
|
|
|
|
|
+ if (card.textContent.toLowerCase().includes(q)) {
|
|
|
|
|
+ card.style.display = 'block';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ card.style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
function renderCapabilities(capsObj) {
|
|
function renderCapabilities(capsObj) {
|
|
|
if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data';
|
|
if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data';
|
|
|
const caps = capsObj.extracted_capabilities;
|
|
const caps = capsObj.extracted_capabilities;
|
|
@@ -447,10 +578,35 @@ async function fetchRequirementData(index) {
|
|
|
const res = await fetch(`/api/requirements/${index}/data`);
|
|
const res = await fetch(`/api/requirements/${index}/data`);
|
|
|
const data = await res.json();
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
- jsonStrategy.innerHTML = data.strategy ? renderStrategy(data.strategy) : '<p style="color:var(--text-muted)">No strategy data</p>';
|
|
|
|
|
- jsonBlueprint.innerHTML = data.blueprint ? renderBlueprint(data.blueprint) : '<p style="color:var(--text-muted)">No blueprint data</p>';
|
|
|
|
|
- jsonCaps.innerHTML = data.capabilities ? renderCapabilities(data.capabilities) : '<p style="color:var(--text-muted)">No capabilities data</p>';
|
|
|
|
|
- jsonRaw.innerHTML = data.raw_cases ? renderRawCases(data.raw_cases) : '<p style="color:var(--text-muted)">No raw cases data</p>';
|
|
|
|
|
|
|
+ jsonStrategy.innerHTML = renderDataOrRaw(data.strategy, renderStrategy);
|
|
|
|
|
+ jsonBlueprint.innerHTML = renderDataOrRaw(data.blueprint, renderBlueprint);
|
|
|
|
|
+ jsonCaps.innerHTML = renderDataOrRaw(data.capabilities, renderCapabilities);
|
|
|
|
|
+
|
|
|
|
|
+ let rawCasesClone = null;
|
|
|
|
|
+ if (data.raw_cases) {
|
|
|
|
|
+ rawCasesClone = { ...data.raw_cases };
|
|
|
|
|
+ if (rawCasesClone['source']) {
|
|
|
|
|
+ jsonSource.innerHTML = renderSourceCases(rawCasesClone['source']);
|
|
|
|
|
+ delete rawCasesClone['source'];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ jsonSource.innerHTML = '<p style="color:var(--text-muted)">No source data available</p>';
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ jsonSource.innerHTML = '<p style="color:var(--text-muted)">No source data available</p>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases);
|
|
|
|
|
+
|
|
|
|
|
+ if (rawCasesClone && Object.keys(rawCasesClone).length > 0) {
|
|
|
|
|
+ currentAvailablePlatforms = Object.keys(rawCasesClone)
|
|
|
|
|
+ .filter(p => p.startsWith('case_'))
|
|
|
|
|
+ .map(p => p.replace('case_', ''));
|
|
|
|
|
+ if (currentAvailablePlatforms.length === 0) {
|
|
|
|
|
+ currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
|
|
|
|
|
+ }
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error("Failed to fetch data", e);
|
|
console.error("Failed to fetch data", e);
|
|
|
}
|
|
}
|
|
@@ -576,20 +732,24 @@ function selectRequirement(index) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function updateDetailBannerStatus(status) {
|
|
function updateDetailBannerStatus(status) {
|
|
|
|
|
+ const btnStop = document.getElementById('btn-stop-pipeline');
|
|
|
if (status === 'running') {
|
|
if (status === 'running') {
|
|
|
elStatusBanner.classList.remove('hidden');
|
|
elStatusBanner.classList.remove('hidden');
|
|
|
elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
|
|
elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
|
|
|
elStatusText.textContent = 'Pipeline is currently running...';
|
|
elStatusText.textContent = 'Pipeline is currently running...';
|
|
|
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
|
|
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
|
|
|
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
|
|
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
|
|
|
|
|
+ if (btnStop) btnStop.style.display = 'inline-block';
|
|
|
} else if (status === 'failed') {
|
|
} else if (status === 'failed') {
|
|
|
elStatusBanner.classList.remove('hidden');
|
|
elStatusBanner.classList.remove('hidden');
|
|
|
elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
|
|
elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
|
|
|
elStatusText.textContent = 'Pipeline run failed.';
|
|
elStatusText.textContent = 'Pipeline run failed.';
|
|
|
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
|
|
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
|
|
|
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
|
|
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
|
|
|
|
|
+ if (btnStop) btnStop.style.display = 'none';
|
|
|
} else {
|
|
} else {
|
|
|
elStatusBanner.classList.add('hidden');
|
|
elStatusBanner.classList.add('hidden');
|
|
|
|
|
+ if (btnStop) btnStop.style.display = 'none';
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -657,6 +817,13 @@ function setupEventListeners() {
|
|
|
document.getElementById('btn-open-run-modal').addEventListener('click', () => {
|
|
document.getElementById('btn-open-run-modal').addEventListener('click', () => {
|
|
|
if (currentSelectedIndex !== null) {
|
|
if (currentSelectedIndex !== null) {
|
|
|
modalRun.classList.remove('hidden');
|
|
modalRun.classList.remove('hidden');
|
|
|
|
|
+ const selectForcePhase = document.getElementById('select-force-phase');
|
|
|
|
|
+ if (selectForcePhase) selectForcePhase.dispatchEvent(new Event('change'));
|
|
|
|
|
+
|
|
|
|
|
+ const inputPlatforms = document.getElementById('input-platforms');
|
|
|
|
|
+ if (inputPlatforms && currentAvailablePlatforms && currentAvailablePlatforms.length > 0) {
|
|
|
|
|
+ inputPlatforms.value = currentAvailablePlatforms.join(',');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -668,6 +835,19 @@ function setupEventListeners() {
|
|
|
modalRun.classList.add('hidden');
|
|
modalRun.classList.add('hidden');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const selectForcePhase = document.getElementById('select-force-phase');
|
|
|
|
|
+ const groupPlatforms = document.getElementById('group-platforms');
|
|
|
|
|
+ if (selectForcePhase && groupPlatforms) {
|
|
|
|
|
+ selectForcePhase.addEventListener('change', (e) => {
|
|
|
|
|
+ const val = e.target.value;
|
|
|
|
|
+ if (['smart', 'phase1_platforms', 'single_platforms'].includes(val)) {
|
|
|
|
|
+ groupPlatforms.style.display = 'block';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ groupPlatforms.style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
|
|
document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
|
|
|
|
|
|
|
|
document.getElementById('btn-view-logs').addEventListener('click', () => {
|
|
document.getElementById('btn-view-logs').addEventListener('click', () => {
|
|
@@ -680,6 +860,24 @@ function setupEventListeners() {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const btnStop = document.getElementById('btn-stop-pipeline');
|
|
|
|
|
+ if (btnStop) {
|
|
|
|
|
+ btnStop.addEventListener('click', async () => {
|
|
|
|
|
+ if (currentSelectedIndex === null) return;
|
|
|
|
|
+ if (!confirm('Are you sure you want to stop the running pipeline?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`/api/pipeline/stop/${currentSelectedIndex}`, { method: 'POST' });
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ alert("Error stopping pipeline: " + err.detail);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("Failed to stop pipeline", e);
|
|
|
|
|
+ alert("Failed to stop pipeline");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
document.getElementById('btn-close-logs').addEventListener('click', () => {
|
|
document.getElementById('btn-close-logs').addEventListener('click', () => {
|
|
|
modalLogs.classList.add('hidden');
|
|
modalLogs.classList.add('hidden');
|
|
|
});
|
|
});
|