Files
tianxuan/templates/analysis/tool_lab.html
T

181 lines
8.1 KiB
HTML

{% 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 %}