pipeline/wwwroot/pipeline.js
yumoqing e75e6f77e1 feat: pipeline Sage前端桥接模块
- 通过HTTP调用Hermes Pipeline API
- 产线列表/详情/提交/节点查看
- CSS+JS前端样式
2026-06-11 14:49:20 +08:00

493 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Pipeline Module - Main JavaScript */
(function() {
'use strict';
var API_BASE = ''; // Will be set on init
var currentView = 'list';
var currentPipelineId = null;
// === Initialization ===
function init() {
// Detect API base from current URL
var base = window.location.pathname.replace(/\/pipeline\/index\.ui.*$/, '/pipeline');
API_BASE = base;
showListView();
}
// === API Helpers ===
function apiGet(path, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', API_BASE + path, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
try {
var data = JSON.parse(xhr.responseText);
callback(null, data);
} catch(e) {
callback('解析响应失败', null);
}
}
};
xhr.onerror = function() { callback('网络请求失败', null); };
xhr.send();
}
function apiPost(path, body, callback) {
var xhr = new XMLHttpRequest();
xhr.open('POST', API_BASE + path, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
try {
var data = JSON.parse(xhr.responseText);
callback(null, data);
} catch(e) {
callback('解析响应失败', null);
}
}
};
xhr.onerror = function() { callback('网络请求失败', null); };
xhr.send(JSON.stringify(body));
}
// === View Rendering ===
function getContainer() {
return document.getElementById('pipeline_content');
}
function showListView() {
currentView = 'list';
currentPipelineId = null;
var c = getContainer();
if (!c) return;
c.innerHTML = '<div class="pipeline-container">' +
'<div class="pipeline-header">' +
'<h2>产线任务</h2>' +
'<button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.showCreateView()">+ 新建任务</button>' +
'</div>' +
'<div id="pipeline_list_body"><div class="pipeline-loading">加载中...</div></div>' +
'</div>';
loadTaskList();
}
function loadTaskList() {
apiGet('/api/pipeline_list.dspy', function(err, data) {
var el = document.getElementById('pipeline_list_body');
if (!el) return;
if (err) { el.innerHTML = '<div class="pipeline-error">' + err + '</div>'; return; }
if (data.status === 'error') { el.innerHTML = '<div class="pipeline-error">' + (data.message || '加载失败') + '</div>'; return; }
var tasks = data.tasks || data.data || [];
if (!tasks.length) {
el.innerHTML = '<div class="pipeline-empty">暂无产线任务<br><br><button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.showCreateView()">创建第一个任务</button></div>';
return;
}
var html = '<ul class="pipeline-task-list">';
for (var i = 0; i < tasks.length; i++) {
var t = tasks[i];
var st = t.state || 'unknown';
var stClass = 'status-' + st;
var stText = {completed:'已完成', running:'运行中', failed:'失败', submitted:'已提交', processing:'处理中'}[st] || st;
html += '<li class="pipeline-task-item" onclick="PipelineUI.showDetailView(\'' + t.pipeline_id + '\')">' +
'<span class="pipeline-task-id">' + esc(t.pipeline_id) + '</span>' +
'<span class="pipeline-task-title">' + esc(t.title || t.mode || '-') + '</span>' +
'<span class="pipeline-task-meta">' +
'<span>' + esc(t.mode || '') + '</span>' +
'<span class="pipeline-status ' + stClass + '">' + stText + '</span>' +
'<span>v' + (t.current_version || 1) + '</span>' +
'</span></li>';
}
html += '</ul>';
el.innerHTML = html;
});
}
function showCreateView() {
currentView = 'create';
var c = getContainer();
if (!c) return;
c.innerHTML = '<div class="pipeline-container">' +
'<div class="pipeline-detail-header">' +
'<button class="pipeline-back-btn" onclick="PipelineUI.showListView()">← 返回</button>' +
'<span class="pipeline-detail-title">新建产线任务</span></div>' +
'<div class="pipeline-form" id="pipeline_create_form">' +
'<div class="pipeline-form-group">' +
'<label class="pipeline-form-label">产线模式</label>' +
'<select class="pipeline-form-select" id="pf_mode" onchange="PipelineUI.onModeChange()">' +
'<option value="audio_lyrics">音频+歌词 (Mode A)</option>' +
'<option value="video_lyrics">视频+歌词 (Mode B)</option>' +
'<option value="lyrics_only" selected>仅歌词/大纲 (Mode C)</option>' +
'</select></div>' +
'<div class="pipeline-form-group" id="pf_audio_group" style="display:none">' +
'<label class="pipeline-form-label">音频文件路径</label>' +
'<input class="pipeline-form-input" id="pf_audio" placeholder="/path/to/audio.mp3" /></div>' +
'<div class="pipeline-form-group" id="pf_video_group" style="display:none">' +
'<label class="pipeline-form-label">视频文件路径</label>' +
'<input class="pipeline-form-input" id="pf_video" placeholder="/path/to/video.mp4" /></div>' +
'<div class="pipeline-form-group">' +
'<label class="pipeline-form-label">歌曲名称</label>' +
'<input class="pipeline-form-input" id="pf_title" placeholder="输入歌曲名称" /></div>' +
'<div class="pipeline-form-group" id="pf_lyrics_group">' +
'<label class="pipeline-form-label">歌词</label>' +
'<textarea class="pipeline-form-textarea" id="pf_lyrics" placeholder="输入歌词文本或留空由AI生成"></textarea></div>' +
'<div class="pipeline-form-group">' +
'<label class="pipeline-form-label">词作者</label>' +
'<input class="pipeline-form-input" id="pf_lyricist" placeholder="可选" /></div>' +
'<div class="pipeline-form-group">' +
'<label class="pipeline-form-label">曲作者</label>' +
'<input class="pipeline-form-input" id="pf_composer" placeholder="可选" /></div>' +
'<div style="margin-top:24px">' +
'<button class="pipeline-btn pipeline-btn-primary" onclick="PipelineUI.submitTask()" id="pf_submit_btn">提交任务</button>' +
'<button class="pipeline-btn pipeline-btn-secondary" onclick="PipelineUI.showListView()" style="margin-left:8px">取消</button>' +
'</div></div>' +
'<div id="pipeline_create_msg"></div></div>';
}
function onModeChange() {
var mode = document.getElementById('pf_mode').value;
var audioG = document.getElementById('pf_audio_group');
var videoG = document.getElementById('pf_video_group');
var lyricsG = document.getElementById('pf_lyrics_group');
if (audioG) audioG.style.display = (mode === 'audio_lyrics') ? 'block' : 'none';
if (videoG) videoG.style.display = (mode === 'video_lyrics') ? 'block' : 'none';
if (lyricsG) lyricsG.style.display = (mode === 'video_lyrics') ? 'none' : 'block';
}
function submitTask() {
var mode = document.getElementById('pf_mode').value;
var title = document.getElementById('pf_title').value;
var lyrics = document.getElementById('pf_lyrics') ? document.getElementById('pf_lyrics').value : '';
var lyricist = document.getElementById('pf_lyricist').value;
var composer = document.getElementById('pf_composer').value;
if (!title) { alert('请输入歌曲名称'); return; }
var body = { mode: mode, title: title, lyrics: lyrics, lyricist: lyricist, composer: composer };
if (mode === 'audio_lyrics') {
body.input_audio = document.getElementById('pf_audio').value;
if (!body.input_audio) { alert('请输入音频文件路径'); return; }
} else if (mode === 'video_lyrics') {
body.input_video = document.getElementById('pf_video').value;
if (!body.input_video) { alert('请输入视频文件路径'); return; }
}
var btn = document.getElementById('pf_submit_btn');
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
apiPost('/api/pipeline_submit.dspy', body, function(err, data) {
if (btn) { btn.disabled = false; btn.textContent = '提交任务'; }
if (err) { alert('提交失败: ' + err); return; }
if (data.status === 'error') { alert('提交失败: ' + (data.message || '未知错误')); return; }
var pid = data.pipeline_id;
if (pid) {
showDetailView(pid);
} else {
showListView();
}
});
}
// === Task Detail View ===
function showDetailView(pipelineId) {
currentView = 'detail';
currentPipelineId = pipelineId;
var c = getContainer();
if (!c) return;
c.innerHTML = '<div class="pipeline-container">' +
'<div class="pipeline-detail-header">' +
'<button class="pipeline-back-btn" onclick="PipelineUI.showListView()">← 返回</button>' +
'<div><div class="pipeline-detail-title" id="pd_title">加载中...</div>' +
'<div class="pipeline-detail-meta" id="pd_meta"></div></div></div>' +
'<div id="pd_nodes"><div class="pipeline-loading">加载任务节点...</div></div>' +
'</div>';
loadTaskDetail(pipelineId);
}
function loadTaskDetail(pid) {
apiGet('/api/pipeline_detail.dspy?pipeline_id=' + encodeURIComponent(pid), function(err, data) {
if (err) {
document.getElementById('pd_nodes').innerHTML = '<div class="pipeline-error">' + err + '</div>';
return;
}
if (data.status === 'error') {
document.getElementById('pd_nodes').innerHTML = '<div class="pipeline-error">' + (data.message || '加载失败') + '</div>';
return;
}
var titleEl = document.getElementById('pd_title');
var metaEl = document.getElementById('pd_meta');
var nodesEl = document.getElementById('pd_nodes');
if (titleEl) titleEl.textContent = data.title || pid;
if (metaEl) {
var st = data.state || '';
var stText = {completed:'已完成', running:'运行中', failed:'失败', submitted:'已提交'}[st] || st;
metaEl.innerHTML = 'ID: ' + esc(pid) + ' | ' +
esc(data.mode || '') + ' | ' +
'<span class="pipeline-status status-' + st + '">' + stText + '</span>' +
' | 版本 v' + (data.current_version || 1);
}
renderNodeTree(nodesEl, data);
});
}
function renderNodeTree(container, data) {
if (!container) return;
var steps = data.steps || {};
var artifacts = data.artifacts || {};
var versions = data.versions || {};
// Sort steps by order
var stepList = [];
for (var name in steps) {
if (steps.hasOwnProperty(name)) {
var s = steps[name];
stepList.push({name: name, order: s.order || 0, version: s.version || 1, deps: s.deps || [], state: s.state || ''});
}
}
stepList.sort(function(a, b) { return a.order - b.order; });
if (!stepList.length) {
container.innerHTML = '<div class="pipeline-empty">无步骤数据</div>';
return;
}
var html = '<div class="pipeline-node-tree">';
for (var i = 0; i < stepList.length; i++) {
var step = stepList[i];
var st = step.state || 'pending';
var stText = {completed:'✓', running:'⟳', pending:'○', failed:'✗'}[st] || '?';
var iconClass = 'node-icon-' + st;
var nameCN = STEP_NAMES[step.name] || step.name;
// Find artifact for this step
var artKey = '';
for (var ak in artifacts) {
if (artifacts.hasOwnProperty(ak) && artifacts[ak].step === step.name) {
artKey = ak;
break;
}
}
html += '<div class="pipeline-node-card" id="node_' + step.name + '">' +
'<div class="pipeline-node-header" onclick="PipelineUI.toggleNode(\'' + step.name + '\')">' +
'<div class="pipeline-node-icon ' + iconClass + '">' + stText + '</div>' +
'<span class="pipeline-node-name">' + esc(nameCN) + '</span>' +
'<span class="pipeline-node-version">v' + step.version + '</span>' +
'</div>' +
'<div class="pipeline-node-body" id="node_body_' + step.name + '">';
if (st === 'completed') {
html += '<div class="pipeline-io-section">' +
'<div class="pipeline-io-label">输入 / 输出</div>' +
'<div id="node_io_' + step.name + '" class="pipeline-io-content">点击加载...</div>' +
'<div class="pipeline-io-actions">' +
'<button class="pipeline-btn pipeline-btn-secondary pipeline-btn-sm" onclick="PipelineUI.loadNodeIO(\'' + step.name + '\')">刷新</button>' +
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.showNodeEdit(\'' + step.name + '\', \'input\')">修改输入</button>' +
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.showNodeEdit(\'' + step.name + '\', \'output\')">修改输出</button>' +
'</div></div>';
} else if (st === 'running') {
html += '<div class="pipeline-loading">该节点正在运行中...</div>';
} else if (st === 'failed') {
html += '<div class="pipeline-error">该节点执行失败</div>';
} else {
html += '<div style="color:#aaa;font-size:13px">等待执行</div>';
}
html += '</div></div>';
}
html += '</div>';
container.innerHTML = html;
// Auto-expand failed or running node
for (var j = 0; j < stepList.length; j++) {
if (stepList[j].state === 'failed' || stepList[j].state === 'running') {
toggleNode(stepList[j].name);
}
}
}
// Step name Chinese mapping
var STEP_NAMES = {
'audio_preparing': '音频准备',
'video_preparing': '视频准备',
'demucs_separating': '人声分离',
'lyric_generating': '歌词生成',
'lyric_evaluating': '歌词评估',
'music_generating': '音乐生成',
'music_polling': '音乐轮询',
'lyric_calibrating': '歌词校准',
'subtitle_rendering': '字幕渲染',
'subtitle_exporting': '字幕导出',
'character_designing': '角色设计',
'character_image_generating': '角色图生成',
'storyboard_generating': '分镜剧本',
'scene_video_generating': '分镜视频生成',
'scene_video_evaluating': '分镜视频评估',
'scene_video_concatenating': '分镜视频拼接',
'ktv_synthesizing': 'KTV合成'
};
function toggleNode(stepName) {
var body = document.getElementById('node_body_' + stepName);
if (!body) return;
var isExpanded = body.classList.contains('expanded');
body.classList.toggle('expanded');
// Load IO data on first expand
if (!isExpanded) {
var ioEl = document.getElementById('node_io_' + stepName);
if (ioEl && ioEl.textContent === '点击加载...') {
loadNodeIO(stepName);
}
}
}
function loadNodeIO(stepName) {
var ioEl = document.getElementById('node_io_' + stepName);
if (!ioEl) return;
ioEl.textContent = '加载中...';
apiGet('/api/pipeline_node.dspy?pipeline_id=' + encodeURIComponent(currentPipelineId) + '&step=' + encodeURIComponent(stepName), function(err, data) {
if (err) { ioEl.textContent = '加载失败: ' + err; return; }
if (data.status === 'error') { ioEl.textContent = '错误: ' + (data.message || ''); return; }
var text = '';
if (data.input) {
text += '【输入】\n' + formatData(data.input) + '\n\n';
}
if (data.output) {
text += '【输出】\n' + formatData(data.output);
}
if (!text) text = '(无数据)';
ioEl.textContent = text;
});
}
// === Node Edit ===
function showNodeEdit(stepName, modifyType) {
var nodeBody = document.getElementById('node_body_' + stepName);
if (!nodeBody) return;
// Check if edit section already exists
var existing = document.getElementById('node_edit_' + stepName);
if (existing) { existing.remove(); return; }
var typeLabel = modifyType === 'input' ? '修改输入(从该节点重跑)' : '修改输出(从下一节点重跑)';
// Get current content
var ioEl = document.getElementById('node_io_' + stepName);
var currentContent = '';
if (ioEl) {
var text = ioEl.textContent;
if (modifyType === 'input' && text.indexOf('【输入】') >= 0) {
currentContent = text.substring(text.indexOf('【输入】') + 4, text.indexOf('【输出】') >= 0 ? text.indexOf('【输出】') : text.length).trim();
} else if (modifyType === 'output' && text.indexOf('【输出】') >= 0) {
currentContent = text.substring(text.indexOf('【输出】') + 4).trim();
}
}
var html = '<div class="pipeline-edit-section" id="node_edit_' + stepName + '">' +
'<div class="pipeline-io-label">' + esc(typeLabel) + '</div>' +
'<textarea class="pipeline-edit-textarea" id="node_edit_content_' + stepName + '">' + esc(currentContent) + '</textarea>' +
'<input type="hidden" id="node_edit_type_' + stepName + '" value="' + modifyType + '" />' +
'<div class="pipeline-edit-actions">' +
'<button class="pipeline-btn pipeline-btn-primary pipeline-btn-sm" onclick="PipelineUI.submitNodeEdit(\'' + stepName + '\')" id="node_edit_btn_' + stepName + '">确认修改并重跑</button>' +
'<button class="pipeline-btn pipeline-btn-secondary pipeline-btn-sm" onclick="document.getElementById(\'node_edit_' + stepName + '\').remove()">取消</button>' +
'</div>' +
'<div id="node_edit_msg_' + stepName + '"></div></div>';
nodeBody.insertAdjacentHTML('beforeend', html);
}
function submitNodeEdit(stepName) {
var content = document.getElementById('node_edit_content_' + stepName).value;
var modifyType = document.getElementById('node_edit_type_' + stepName).value;
var msgEl = document.getElementById('node_edit_msg_' + stepName);
var btn = document.getElementById('node_edit_btn_' + stepName);
if (!content.trim()) { alert('内容不能为空'); return; }
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
if (msgEl) msgEl.innerHTML = '<div class="pipeline-loading">提交修改...</div>';
apiPost('/api/pipeline_modify.dspy', {
pipeline_id: currentPipelineId,
step: stepName,
modify_type: modifyType,
content: content
}, function(err, data) {
if (btn) { btn.disabled = false; btn.textContent = '确认修改并重跑'; }
if (err) {
if (msgEl) msgEl.innerHTML = '<div class="pipeline-error">' + err + '</div>';
return;
}
if (data.status === 'error') {
if (msgEl) msgEl.innerHTML = '<div class="pipeline-error">' + (data.message || '修改失败') + '</div>';
return;
}
if (msgEl) msgEl.innerHTML = '<div style="color:green;font-size:13px;margin-top:8px">修改成功!' +
(data.new_version ? ' 新版本: v' + data.new_version : '') +
(data.rerun_steps ? ' 重跑步骤: ' + data.rerun_steps.join(', ') : '') +
'</div>';
// Reload task detail after 2 seconds
setTimeout(function() { loadTaskDetail(currentPipelineId); }, 2000);
});
}
// === Utilities ===
function formatData(obj) {
if (typeof obj === 'string') return obj;
try { return JSON.stringify(obj, null, 2); } catch(e) { return String(obj); }
}
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.appendChild(document.createTextNode(s));
return d.innerHTML;
}
// === Expose Public API ===
window.PipelineUI = {
init: init,
showListView: showListView,
showCreateView: showCreateView,
showDetailView: showDetailView,
submitTask: submitTask,
onModeChange: onModeChange,
toggleNode: toggleNode,
loadNodeIO: loadNodeIO,
showNodeEdit: showNodeEdit,
submitNodeEdit: submitNodeEdit
};
// Auto-init on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();