refactor: integrate 27 MCP tools into manual analysis workflow builder (remove separate tool_lab)
This commit is contained in:
@@ -1,181 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}MCP 工具实验室 - 天璇{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.tool-group { margin-bottom: 2rem; }
|
||||
.tool-card {
|
||||
border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; margin: 0.5rem 0;
|
||||
cursor: pointer; transition: box-shadow 0.2s;
|
||||
}
|
||||
.tool-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.tool-card.selected { border-color: #4361ee; box-shadow: 0 0 0 2px #4361ee33; }
|
||||
.tool-name { font-weight: 700; font-size: 1.05rem; color: #4361ee; }
|
||||
.tool-desc { color: #666; font-size: 0.9rem; margin: 0.3rem 0; }
|
||||
.tool-badge {
|
||||
display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px;
|
||||
font-size: 0.75rem; font-weight: 600; margin-right: 0.5rem;
|
||||
}
|
||||
.badge-core { background: #4361ee; color: #fff; }
|
||||
.badge-diag { background: #f72585; color: #fff; }
|
||||
.badge-analysis { background: #43aa8b; color: #fff; }
|
||||
.param-row { margin: 0.5rem 0; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.param-row label { min-width: 140px; font-size: 0.9rem; color: #555; }
|
||||
.param-row input, .param-row select { flex: 1; padding: 0.4rem; border: 1px solid #ccc; border-radius: 4px; }
|
||||
#resultArea {
|
||||
background: #1a1a2e; color: #e0e0e0; padding: 1rem; border-radius: 6px;
|
||||
margin-top: 1rem; max-height: 500px; overflow: auto; font-size: 0.8rem;
|
||||
display: none; white-space: pre-wrap;
|
||||
}
|
||||
.executing { opacity: 0.6; pointer-events: none; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔧 MCP 工具实验室</h2>
|
||||
<p style="color:#666;">手动选择并执行分析工具。工具按类别分组,只读工具可安全任意调用。</p>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
|
||||
<!-- Tool list panel -->
|
||||
<div style="flex:1;min-width:350px;">
|
||||
<div class="tool-group">
|
||||
<h3>⚡ 核心工具</h3>
|
||||
{% for t in core_tools %}
|
||||
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
||||
<div><span class="tool-badge badge-core">CORE</span><span class="tool-name">{{ t.name }}</span></div>
|
||||
<div class="tool-desc">{{ t.description }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<h3>🔍 分析工具(只读)</h3>
|
||||
{% for t in analysis_tools %}
|
||||
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
||||
<div><span class="tool-badge badge-analysis">ANALYSIS</span><span class="tool-name">{{ t.name }}</span></div>
|
||||
<div class="tool-desc">{{ t.description }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<h3>🩺 诊断工具</h3>
|
||||
{% for t in diag_tools %}
|
||||
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
||||
<div><span class="tool-badge badge-diag">DIAG</span><span class="tool-name">{{ t.name }}</span></div>
|
||||
<div class="tool-desc">{{ t.description }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execution panel -->
|
||||
<div style="flex:1;min-width:350px;">
|
||||
<div class="card" id="execPanel">
|
||||
<h3 id="selectedToolName">选择工具查看参数</h3>
|
||||
<div id="toolParams" style="margin:1rem 0;"></div>
|
||||
<button class="btn btn-primary" id="execBtn" onclick="executeTool()" disabled style="display:none;">执行</button>
|
||||
<div id="executing" style="display:none;color:#666;margin-top:0.5rem;">执行中...</div>
|
||||
</div>
|
||||
<div id="resultArea"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const toolsMeta = {{ tools_meta|safe }};
|
||||
let currentTool = null;
|
||||
|
||||
function selectTool(name, el) {
|
||||
document.querySelectorAll('.tool-card').forEach(c => c.classList.remove('selected'));
|
||||
if (el) el.classList.add('selected');
|
||||
currentTool = name;
|
||||
document.getElementById('selectedToolName').textContent = '📌 ' + name;
|
||||
document.getElementById('execBtn').style.display = 'inline-block';
|
||||
document.getElementById('execBtn').disabled = false;
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
|
||||
// Build parameter form from tool schema
|
||||
const tool = toolsMeta.find(t => t.name === name);
|
||||
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
||||
document.getElementById('toolParams').innerHTML = '<p style="color:#888;">无需参数</p>';
|
||||
return;
|
||||
}
|
||||
const props = tool.inputSchema.properties;
|
||||
const required = tool.inputSchema.required || [];
|
||||
let html = '';
|
||||
for (const [key, prop] of Object.entries(props)) {
|
||||
const isReq = required.includes(key);
|
||||
const label = prop.description || key;
|
||||
let input;
|
||||
if (prop.type === 'boolean') {
|
||||
input = `<select id="param_${key}"><option value="true">true</option><option value="false">false</option></select>`;
|
||||
} else if (prop.type === 'array') {
|
||||
input = `<input type="text" id="param_${key}" placeholder='["val1","val2"]' value='${prop.default || ""}'>`;
|
||||
} else if (prop.type === 'number' || prop.type === 'integer') {
|
||||
input = `<input type="number" id="param_${key}" value="${prop.default || ""}" step="${prop.type === 'integer' ? 1 : 0.01}">`;
|
||||
} else {
|
||||
{% if datasets %}
|
||||
if (key === 'dataset_id' || key === 'dataset_id_a' || key === 'dataset_id_b') {
|
||||
let opts = '<option value="">-- select --</option>';
|
||||
{% for ds in datasets %}
|
||||
opts += `<option value="${'{{ ds.id }}'}">${'{{ ds.id }}'}</option>`;
|
||||
{% endfor %}
|
||||
input = `<select id="param_${key}">${opts}</select>`;
|
||||
} else {
|
||||
input = `<input type="text" id="param_${key}" placeholder="${label}" value='${prop.default || ""}'>`;
|
||||
}
|
||||
{% else %}
|
||||
input = `<input type="text" id="param_${key}" placeholder="${label}" value='${prop.default || ""}'>`;
|
||||
{% endif %}
|
||||
}
|
||||
html += `<div class="param-row"><label>${key}${isReq ? ' *' : ''}</label>${input}</div>`;
|
||||
}
|
||||
document.getElementById('toolParams').innerHTML = html;
|
||||
}
|
||||
|
||||
async function executeTool() {
|
||||
if (!currentTool) return;
|
||||
const tool = toolsMeta.find(t => t.name === currentTool);
|
||||
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
||||
// No-param tool
|
||||
await doExecute(currentTool, {});
|
||||
return;
|
||||
}
|
||||
const params = {};
|
||||
for (const key of Object.keys(tool.inputSchema.properties)) {
|
||||
const el = document.getElementById('param_' + key);
|
||||
if (!el) continue;
|
||||
let val = el.value;
|
||||
const prop = tool.inputSchema.properties[key];
|
||||
if (prop.type === 'boolean') val = val === 'true';
|
||||
else if (prop.type === 'number') val = parseFloat(val);
|
||||
else if (prop.type === 'integer') val = parseInt(val);
|
||||
else if (prop.type === 'array') { try { val = JSON.parse(val); } catch(e) { val = []; } }
|
||||
if (val !== '' && val !== null && val !== undefined) params[key] = val;
|
||||
}
|
||||
await doExecute(currentTool, params);
|
||||
}
|
||||
|
||||
async function doExecute(name, params) {
|
||||
const btn = document.getElementById('execBtn');
|
||||
const status = document.getElementById('executing');
|
||||
const resultArea = document.getElementById('resultArea');
|
||||
btn.disabled = true; status.style.display = 'block';
|
||||
resultArea.style.display = 'none';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/tools/run/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: JSON.stringify({ tool: name, params: params }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultArea.textContent = JSON.stringify(data, null, 2);
|
||||
resultArea.style.display = 'block';
|
||||
} catch (err) {
|
||||
resultArea.textContent = 'Error: ' + err.message;
|
||||
resultArea.style.display = 'block';
|
||||
}
|
||||
btn.disabled = false; status.style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
+1
-1
@@ -47,7 +47,7 @@
|
||||
<a href="{% url 'analysis:auto' %}">LLM分析</a>
|
||||
<a href="{% url 'analysis:run_list' %}">运行记录</a>
|
||||
<a href="{% url 'analysis:globe' %}">地球</a>
|
||||
<a href="{% url 'analysis:tool_lab' %}">工具</a>
|
||||
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
+227
-128
@@ -1,148 +1,247 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load query_helpers %}
|
||||
{% block title %}手动分析 - 天璇{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.step-card {
|
||||
border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; margin: 0.75rem 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
.step-card.executing { opacity: 0.6; pointer-events: none; }
|
||||
.step-result {
|
||||
background: #1a1a2e; color: #e0e0e0; padding: 0.75rem; border-radius: 6px;
|
||||
margin-top: 0.5rem; font-size: 0.8rem; max-height: 300px; overflow: auto; white-space: pre-wrap;
|
||||
display: none;
|
||||
}
|
||||
.step-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
|
||||
.step-num {
|
||||
background: #4361ee; color: #fff; border-radius: 50%; width: 24px; height: 24px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: 700;
|
||||
}
|
||||
.step-name { font-weight: 600; font-size: 1rem; }
|
||||
.param-row { margin: 0.4rem 0; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.param-row label { min-width: 120px; font-size: 0.85rem; color: #555; }
|
||||
.param-row input, .param-row select { flex: 1; padding: 0.35rem; border: 1px solid #ccc; border-radius: 4px; font-size: 0.85rem; }
|
||||
.remove-step { color: #dc3545; cursor: pointer; font-size: 1.2rem; margin-left: auto; }
|
||||
.badge-tool {
|
||||
display: inline-block; padding: 0.1rem 0.4rem; border-radius: 3px;
|
||||
font-size: 0.7rem; font-weight: 600; margin-right: 0.3rem;
|
||||
}
|
||||
.badge-core { background: #4361ee; color: #fff; }
|
||||
.badge-analysis { background: #43aa8b; color: #fff; }
|
||||
.badge-diag { background: #f72585; color: #fff; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h2>手动分析 - 第 {{ step }} 步 / 共 3 步</h2>
|
||||
<h2>🔧 手动分析工作台</h2>
|
||||
<p style="color:#666;">构建你的分析流程:选择数据集 → 添加分析步骤 → 逐条执行</p>
|
||||
</div>
|
||||
|
||||
{% if step == 1 %}
|
||||
<!-- Step 0: Dataset selector -->
|
||||
<div class="card">
|
||||
<h3>Step 1: 选择数据集</h3>
|
||||
<p>选择已上传且预处理完成的数据集:</p>
|
||||
{% if runs %}
|
||||
<form method="get" action="/analyze/manual/" style="margin-top:1rem;">
|
||||
<input type="hidden" name="step" value="2">
|
||||
<table>
|
||||
<thead><tr><th></th><th>ID</th><th>文件</th><th>行数</th><th>实体列</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr>
|
||||
<td><input type="radio" name="run_id" value="{{ run.id }}" required></td>
|
||||
<td>#{{ run.id }}</td>
|
||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;">{{ run.csv_glob }}</td>
|
||||
<td>{{ run.total_flows|default:"-" }}</td>
|
||||
<td>{{ run.entity_column|default:"-" }}</td>
|
||||
<td><span class="badge badge-success">{{ run.status }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" style="text-align:center;">暂无已预处理的数据集。先去 <a href="/upload/">上传页面</a> 上传 CSV。</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-primary" style="margin-top:1rem;">下一步 →</button>
|
||||
</form>
|
||||
<h3>📁 选择数据集</h3>
|
||||
{% if datasets %}
|
||||
<select id="datasetSelect" style="width:100%;padding:0.5rem;border:1px solid #ccc;border-radius:4px;">
|
||||
<option value="">-- 选择数据集 --</option>
|
||||
{% for ds in datasets %}
|
||||
<option value="{{ ds.id }}">{{ ds.id }} ({{ ds.metadata.row_count|default:"?" }} rows, {{ ds.metadata.file_count|default:"?" }} files)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<p style="color:#999;">暂无数据集。先去 <a href="/upload/">上传页面</a> 上传 CSV。</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if step == 2 %}
|
||||
<!-- Workflow builder -->
|
||||
<div class="card">
|
||||
<h3>Step 2: 配置聚类参数</h3>
|
||||
<form method="get" action="/analyze/manual/">
|
||||
<input type="hidden" name="step" value="3">
|
||||
<input type="hidden" name="run_id" value="{{ request.GET.run_id }}">
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>实体列(entity_columns):</label>
|
||||
<select name="entity_columns" multiple style="width:100%;min-height:100px;">
|
||||
{% if entity_candidates %}
|
||||
{% for c in entity_candidates %}
|
||||
<option value="{{ c.column }}" {% if c.column in selected_entity_cols %}selected{% endif %}>{{ c.column }} (score: {{ c.score }})</option>
|
||||
{% endfor %}
|
||||
{% elif schema_keys %}
|
||||
{% for col in schema_keys %}
|
||||
<option value="{{ col }}" {% if col in selected_entity_cols %}selected{% endif %}>{{ col }}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="">暂无可选列</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<small style="color:#666;">按住 Ctrl 多选,留空则使用自动检测结果</small>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>特征列(feature_columns):</label>
|
||||
<select name="feature_columns" multiple style="width:100%;min-height:100px;">
|
||||
{% if feature_options %}
|
||||
{% for opt in feature_options %}
|
||||
<option value="{{ opt.name }}">{{ opt.name }} ({{ opt.dtype }})</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="">暂无可选数值列</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<small style="color:#666;">按住 Ctrl 多选,留空则自动选择前10个数值列。仅显示数值类型列。</small>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>聚类算法:</label>
|
||||
<select name="algorithm">
|
||||
<option value="hdbscan" {% if default_algo == 'hdbscan' %}selected{% endif %}>HDBSCAN(自动确定簇数)</option>
|
||||
<option value="kmeans" {% if default_algo == 'kmeans' %}selected{% endif %}>KMeans(需预设簇数)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>最小簇大小(min_cluster_size):</label>
|
||||
<input type="number" name="min_cluster_size" value="{{ default_min_size }}" min="2" max="100">
|
||||
</div>
|
||||
<a href="/analyze/manual/?step=1" class="btn" style="background:#ddd;margin-right:0.5rem;">← 上一步</a>
|
||||
<button type="submit" class="btn btn-primary">下一步 →</button>
|
||||
</form>
|
||||
<h3>📋 分析流程 <span style="font-weight:400;font-size:0.85rem;color:#888;">依次添加分析步骤</span></h3>
|
||||
<div id="workflowSteps"></div>
|
||||
<button class="btn" onclick="showToolPicker()" style="margin-top:0.5rem;background:#e8f0fe;color:#4361ee;">+ 添加步骤</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if step == 3 %}
|
||||
<div class="card">
|
||||
<h3>Step 3: 确认并运行</h3>
|
||||
<p>请确认以下配置,确认无误后点击"开始分析":</p>
|
||||
<table>
|
||||
<tr><td>数据集 ID</td><td>#{{ request.GET.run_id }}</td></tr>
|
||||
<tr><td>实体列</td><td>{{ request.GET|getlist:"entity_columns"|default:"自动检测" }}</td></tr>
|
||||
<tr><td>特征列</td><td>{{ request.GET.feature_columns|default:"自动选择" }}</td></tr>
|
||||
<tr><td>聚类算法</td><td>{{ request.GET.algorithm }}</td></tr>
|
||||
<tr><td>最小簇大小</td><td>{{ request.GET.min_cluster_size }}</td></tr>
|
||||
</table>
|
||||
<div style="margin-top:1rem;">
|
||||
<a href="/analyze/manual/?step=2&run_id={{ request.GET.run_id }}" class="btn" style="background:#ddd;margin-right:0.5rem;">← 上一步</a>
|
||||
<button class="btn btn-primary" onclick="runAnalysis()">开始分析</button>
|
||||
</div>
|
||||
<div id="runProgress" style="display:none;margin-top:1rem;">
|
||||
<p>分析进行中... <span id="runStatus"></span></p>
|
||||
<div style="background:#eee;height:8px;border-radius:4px;overflow:hidden;">
|
||||
<div id="runBar" style="background:#4361ee;height:100%;width:0%;"></div>
|
||||
<!-- Tool picker modal -->
|
||||
<div id="toolPicker" style="display:none;margin-top:1rem;">
|
||||
<div class="card">
|
||||
<h4>选择工具</h4>
|
||||
<div style="display:flex;gap:0.5rem;margin:0.5rem 0;flex-wrap:wrap;">
|
||||
<button class="btn" onclick="filterTools('core')" style="background:#4361ee;color:#fff;font-size:0.8rem;">核心</button>
|
||||
<button class="btn" onclick="filterTools('analysis')" style="background:#43aa8b;color:#fff;font-size:0.8rem;">分析</button>
|
||||
<button class="btn" onclick="filterTools('diag')" style="background:#f72585;color:#fff;font-size:0.8rem;">诊断</button>
|
||||
<button class="btn" onclick="filterTools('all')" style="font-size:0.8rem;">全部</button>
|
||||
</div>
|
||||
<div id="toolList" style="max-height:300px;overflow-y:auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function runAnalysis() {
|
||||
document.getElementById('runProgress').style.display = 'block';
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const entityCols = params.getAll('entity_columns');
|
||||
const featureCols = params.getAll('feature_columns');
|
||||
const resp = await fetch('/analyze/run/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: JSON.stringify({
|
||||
run_id: parseInt('{{ request.GET.run_id }}'),
|
||||
entity_column: '',
|
||||
entity_columns: entityCols.length ? entityCols : undefined,
|
||||
feature_columns: featureCols.length ? featureCols : undefined,
|
||||
algorithm: '{{ request.GET.algorithm }}',
|
||||
min_cluster_size: parseInt('{{ request.GET.min_cluster_size }}'),
|
||||
}),
|
||||
// ── All 27 tools metadata (embedded from server) ──
|
||||
const TOOLS = {{ tools_meta|safe }};
|
||||
const CATEGORIES = {
|
||||
{% for t in core_tools %}'{{ t.name }}': 'core', {% endfor %}
|
||||
{% for t in analysis_tools %}'{{ t.name }}': 'analysis', {% endfor %}
|
||||
{% for t in diag_tools %}'{{ t.name }}': 'diag', {% endfor %}
|
||||
};
|
||||
let stepCounter = 0;
|
||||
const workflow = [];
|
||||
|
||||
// ── Tool picker ──
|
||||
function showToolPicker() {
|
||||
document.getElementById('toolPicker').style.display = 'block';
|
||||
filterTools('all');
|
||||
}
|
||||
function filterTools(cat) {
|
||||
const list = document.getElementById('toolList');
|
||||
list.innerHTML = '';
|
||||
TOOLS.forEach(t => {
|
||||
const c = CATEGORIES[t.name] || 'other';
|
||||
if (cat !== 'all' && c !== cat) return;
|
||||
const badge = c === 'core' ? 'CORE' : c === 'analysis' ? 'ANALYSIS' : 'DIAG';
|
||||
const color = c === 'core' ? '#4361ee' : c === 'analysis' ? '#43aa8b' : '#f72585';
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'padding:0.5rem;cursor:pointer;border-bottom:1px solid #eee;display:flex;align-items:center;gap:0.5rem;';
|
||||
div.innerHTML = `<span style="background:${color};color:#fff;padding:0.1rem 0.4rem;border-radius:3px;font-size:0.7rem;font-weight:600;">${badge}</span>
|
||||
<span style="font-weight:600;">${t.name}</span>
|
||||
<span style="color:#888;font-size:0.8rem;flex:1;">${t.description.substring(0, 80)}...</span>`;
|
||||
div.onclick = () => addStep(t);
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Add a step to the workflow ──
|
||||
function addStep(tool) {
|
||||
document.getElementById('toolPicker').style.display = 'none';
|
||||
const idx = stepCounter++;
|
||||
const step = { idx, tool, params: {} };
|
||||
workflow.push(step);
|
||||
renderSteps();
|
||||
}
|
||||
|
||||
function removeStep(idx) {
|
||||
const pos = workflow.findIndex(s => s.idx === idx);
|
||||
if (pos >= 0) workflow.splice(pos, 1);
|
||||
renderSteps();
|
||||
}
|
||||
|
||||
// ── Build parameter form for a tool ──
|
||||
function buildParamForm(tool, container, stepIdx) {
|
||||
const props = tool.inputSchema && tool.inputSchema.properties;
|
||||
if (!props) { container.innerHTML = '<p style="color:#888;font-size:0.85rem;">无需参数</p>'; return; }
|
||||
const required = tool.inputSchema.required || [];
|
||||
let html = '';
|
||||
for (const [key, prop] of Object.entries(props)) {
|
||||
const isReq = required.includes(key);
|
||||
const label = prop.description || key;
|
||||
let input;
|
||||
if (prop.type === 'boolean') {
|
||||
input = `<select class="param-${stepIdx}" data-key="${key}"><option value="true">true</option><option value="false" selected>false</option></select>`;
|
||||
} else if (prop.type === 'array') {
|
||||
input = `<input class="param-${stepIdx}" data-key="${key}" placeholder='["val1","val2"]' style="width:100%;">`;
|
||||
} else if (prop.type === 'number' || prop.type === 'integer') {
|
||||
input = `<input class="param-${stepIdx}" data-key="${key}" type="number" value="${prop.default || ''}" step="${prop.type==='integer'?1:0.01}" style="width:100%;">`;
|
||||
} else {
|
||||
// Check if this is a dataset_id field — offer dataset list
|
||||
if (key.includes('dataset_id')) {
|
||||
let opts = '<option value="">-- select --</option>';
|
||||
document.querySelectorAll('#datasetSelect option').forEach(o => {
|
||||
if (o.value) opts += `<option value="${o.value}">${o.value}</option>`;
|
||||
});
|
||||
input = `<select class="param-${stepIdx}" data-key="${key}" style="width:100%;">${opts}</select>`;
|
||||
} else if (key === 'algorithm') {
|
||||
input = `<select class="param-${stepIdx}" data-key="${key}" style="width:100%;">
|
||||
<option value="hdbscan">HDBSCAN</option><option value="kmeans">KMeans</option></select>`;
|
||||
} else if (key === 'method') {
|
||||
input = `<select class="param-${stepIdx}" data-key="${key}" style="width:100%;">
|
||||
<option value="zscore">zscore</option><option value="anova">anova</option></select>`;
|
||||
} else {
|
||||
input = `<input class="param-${stepIdx}" data-key="${key}" placeholder="${label}" style="width:100%;">`;
|
||||
}
|
||||
}
|
||||
html += `<div class="param-row"><label>${key}${isReq ? ' <span style="color:#dc3545;">*</span>' : ''}</label>${input}</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Collect params from form ──
|
||||
function collectParams(stepIdx) {
|
||||
const params = {};
|
||||
document.querySelectorAll(`.param-${stepIdx}`).forEach(el => {
|
||||
let val = el.value;
|
||||
const key = el.dataset.key;
|
||||
if (!key || val === '' || val === null) return;
|
||||
// Determine type from input type
|
||||
if (el.type === 'number') val = parseFloat(val);
|
||||
else if (el.tagName === 'SELECT' && el.dataset.key === 'algorithm') { /* keep string */ }
|
||||
else if (val.startsWith('[') || val.startsWith('{')) { try { val = JSON.parse(val); } catch(e) {} }
|
||||
params[key] = val;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
// ── Execute a single step ──
|
||||
async function executeStep(idx) {
|
||||
const step = workflow.find(s => s.idx === idx);
|
||||
if (!step) return;
|
||||
const card = document.getElementById(`step-${idx}`);
|
||||
const resultDiv = document.getElementById(`result-${idx}`);
|
||||
const btn = document.getElementById(`exec-${idx}`);
|
||||
btn.disabled = true; card.classList.add('executing');
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
const params = collectParams(idx);
|
||||
try {
|
||||
const resp = await fetch('/tools/run/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: JSON.stringify({ tool: step.tool.name, params }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultDiv.textContent = JSON.stringify(data, null, 2);
|
||||
resultDiv.style.display = 'block';
|
||||
} catch (err) {
|
||||
resultDiv.textContent = 'Error: ' + err.message;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
btn.disabled = false; card.classList.remove('executing');
|
||||
|
||||
// Update dataset_id suggestions from results
|
||||
if (data && data.result && data.result.dataset_id) {
|
||||
const sel = document.getElementById('datasetSelect');
|
||||
const exists = Array.from(sel.options).some(o => o.value === data.result.dataset_id);
|
||||
if (!exists) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = data.result.dataset_id;
|
||||
opt.text = data.result.dataset_id + ' (from step ' + (stepCounter) + ')';
|
||||
sel.add(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render all workflow steps ──
|
||||
function renderSteps() {
|
||||
const container = document.getElementById('workflowSteps');
|
||||
container.innerHTML = '';
|
||||
workflow.forEach((step, i) => {
|
||||
const c = CATEGORIES[step.tool.name] || 'other';
|
||||
const color = c === 'core' ? '#4361ee' : c === 'analysis' ? '#43aa8b' : '#f72585';
|
||||
const badge = c === 'core' ? 'CORE' : c === 'analysis' ? 'ANALYSIS' : 'DIAG';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'step-card';
|
||||
div.id = `step-${step.idx}`;
|
||||
div.innerHTML = `
|
||||
<div class="step-header">
|
||||
<span class="step-num">${i+1}</span>
|
||||
<span class="step-name">${step.tool.name}</span>
|
||||
<span class="badge-tool" style="background:${color};">${badge}</span>
|
||||
<span style="font-size:0.8rem;color:#888;flex:1;">${step.tool.description.substring(0, 60)}</span>
|
||||
<span class="remove-step" onclick="removeStep(${step.idx})" title="移除此步骤">✕</span>
|
||||
</div>
|
||||
<div id="params-${step.idx}"></div>
|
||||
<button class="btn btn-primary" id="exec-${step.idx}" onclick="executeStep(${step.idx})" style="font-size:0.85rem;padding:0.35rem 0.75rem;margin-top:0.3rem;">▶ 执行</button>
|
||||
<div class="step-result" id="result-${step.idx}"></div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
buildParamForm(step.tool, document.getElementById(`params-${step.idx}`), step.idx);
|
||||
});
|
||||
const data = await resp.json();
|
||||
const runId = '{{ request.GET.run_id }}';
|
||||
const poll = setInterval(async () => {
|
||||
const r = await fetch(`/runs/${runId}/status/`);
|
||||
const s = await r.json();
|
||||
const pct = s.progress_pct || 0;
|
||||
const msg = s.progress_msg || s.status;
|
||||
document.getElementById('runStatus').textContent = msg + ' (' + pct + '%)';
|
||||
document.getElementById('runBar').style.width = pct + '%';
|
||||
if (s.status === 'completed') { clearInterval(poll); window.location.href = `/runs/${runId}/`; }
|
||||
if (s.status === 'failed') { clearInterval(poll); document.getElementById('runStatus').textContent = '失败: ' + (s.error_message || ''); }
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user