Files
tianxuan/templates/analysis/run_detail.html
T

178 lines
8.6 KiB
HTML

{% extends 'base.html' %}
{% block title %}运行 #{{ run.display_id }} - 天璇{% endblock %}
{% block content %}
<style>
.svd-card { background:#f8f9fa; border:1px solid #e0e0e0; border-radius:8px; padding:0.8rem; }
.svd-card h4 { margin:0 0 0.3rem; font-size:0.9rem; }
.var-bar { height:8px; border-radius:4px; background:#4361ee; margin:0.2rem 0; transition:width 0.3s; }
.feat-tag { display:inline-block; background:#e8f0fe; padding:0.1rem 0.4rem; border-radius:3px; font-size:0.75rem; margin:0.1rem; }
</style>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
<div>
<h2 style="margin:0;">运行 #{{ run.display_id }}</h2>
<p style="margin:0.25rem 0 0;color:#666;font-size:0.9rem;">
{{ run.created_at|date:"Y-m-d H:i" }}
— 状态: <span class="badge {% if run.status == 'completed' %}badge-success{% elif run.status == 'failed' %}badge-danger{% else %}badge-info{% endif %}">{{ run.get_status_display }}</span>
</p>
</div>
<a href="{% url 'analysis:cluster_overview' run.display_id %}" class="btn btn-primary" style="font-size:0.85rem;">聚类概览 →</a>
</div>
{% if run.error_message %}
<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.78rem;margin-top:0.5rem;max-height:200px;overflow:auto;"><code>{{ run.error_message }}</code></pre>
{% endif %}
</div>
<div class="grid-3">
<div class="card" style="text-align:center;">
<div class="stat">{{ run.total_flows|default:"0" }}</div>
<div class="stat-label">总流数</div>
</div>
<div class="card" style="text-align:center;">
<div class="stat">{{ run.entity_count|default:"0" }}</div>
<div class="stat-label">行数</div>
</div>
<div class="card" style="text-align:center;">
<div class="stat">{{ run.cluster_count|default:"0" }}</div>
<div class="stat-label">聚类数</div>
</div>
</div>
<!-- ── Global SVD Features ── -->
{% if svd_features %}
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3>🔬 全域 SVD 特征分析</h3>
<span style="font-size:0.8rem;color:#888;">基于 {{ run.total_flows }} 行数据</span>
</div>
<p style="font-size:0.82rem;color:#666;margin-top:0.3rem;">奇异值分解 (TruncatedSVD) 降维后的各主成分解释方差比例与贡献特征。</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;margin-top:0.75rem;">
{% for svd in svd_features %}
<div class="svd-card">
<h4>PC{{ svd.component }}</h4>
<div style="display:flex;justify-content:space-between;font-size:0.78rem;color:#555;">
<span>解释方差: {{ svd.explained_var }}%</span>
<span>累积: {{ svd.cumulative_var }}%</span>
</div>
<div class="var-bar" style="width:{{ svd.explained_var }}%;"></div>
<div style="margin-top:0.4rem;">
{% for f in svd.top_features %}
<span class="feat-tag">{{ f.feature }}: {{ f.strength }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- ── LLM 自动分析流程 ── -->
{% if run.run_type == 'auto' and llm_timeline_json %}
<div class="card">
<h3>🧠 LLM 自动分析流程</h3>
<div id="llmTimeline"></div>
</div>
{% endif %}
<!-- ── 聚类列表 ── -->
<div class="card">
<h3>📊 聚类结果</h3>
{% if clusters %}
<table>
<thead>
<tr>
<th></th>
<th>大小</th>
<th>比例</th>
<th>轮廓系数</th>
<th></th>
</tr>
</thead>
<tbody>
{% for c in clusters %}
<tr>
<td><span class="badge badge-info">#{{ c.cluster_label }}</span></td>
<td>{{ c.size }}</td>
<td>{{ c.proportion|floatformat:2 }}</td>
<td>{{ c.silhouette_score|floatformat:4|default:"-" }}</td>
<td><a href="{% url 'analysis:cluster_detail' run.display_id c.cluster_label %}" class="btn btn-primary">详情</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state"><p>无聚类结果</p></div>
{% endif %}
</div>
{% if run.run_type == 'auto' and llm_timeline_json %}
<script>
const llmData = {{ llm_timeline_json|safe }};
(function() {
const container = document.getElementById('llmTimeline');
if (!container) return;
function escapeHtml(s) { if (s==null) return ''; return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function prettyJson(o) { try { return JSON.stringify(o,null,2); } catch(e) { return String(o); } }
var thinkingByStep = {}, thinkOrder = [];
if (llmData.thinking) {
var parts = llmData.thinking.split(/\n(?=\[\d+\])/);
for (var pi = 0; pi < parts.length; pi++) {
var m = parts[pi].match(/^\[(\d+)\]\s*(.*)/s);
if (m) {
var step = parseInt(m[1]);
if (!(step in thinkingByStep)) thinkOrder.push(step);
thinkingByStep[step] = (thinkingByStep[step]||'')+(thinkingByStep[step]?'\n':'')+m[2].trim();
}
}
}
var toolCalls = llmData.tool_calls || [];
var toolMap = {};
for (var ti = 0; ti < toolCalls.length; ti++) {
var tc = toolCalls[ti];
if (!toolMap[tc.step]) toolMap[tc.step] = [];
toolMap[tc.step].push(tc);
}
var allSteps = new Set(thinkOrder.concat(Object.keys(toolMap).map(Number)));
var sortedSteps = Array.from(allSteps).sort(function(a,b){return a-b;});
for (var si = 0; si < sortedSteps.length; si++) {
var stp = sortedSteps[si];
var hdr = document.createElement('div');
hdr.style.cssText = 'font-weight:700;font-size:0.85rem;color:#4361ee;padding:0.6rem 0 0.3rem;border-top:1px solid #eee;';
hdr.textContent = '步骤 ' + stp;
container.appendChild(hdr);
if (thinkingByStep[stp] !== undefined) {
var tdiv = document.createElement('div');
tdiv.style.cssText = 'background:#eef2ff;padding:0.5rem;border-radius:6px;font-size:0.78rem;line-height:1.6;color:#333;max-height:300px;overflow:auto;margin-bottom:0.3rem;border:1px solid #d0d8f0;';
tdiv.innerHTML = '<span style="font-weight:600;color:#4361ee;font-size:0.75rem;display:block;margin-bottom:0.25rem;">💭 思考</span>' + escapeHtml(thinkingByStep[stp]);
container.appendChild(tdiv);
}
if (toolMap[stp]) {
for (var tj = 0; tj < toolMap[stp].length; tj++) {
var tc2 = toolMap[stp][tj];
var card = document.createElement('div');
card.style.cssText = 'border:1px solid #e0e0e0;border-radius:6px;margin-bottom:0.3rem;overflow:hidden;background:#fff;';
var inputStr = prettyJson(tc2.input);
var outputStr = prettyJson(tc2.output);
card.innerHTML =
'<div onclick="this.classList.toggle(\'open\');var b=this.nextElementSibling;if(b)b.classList.toggle(\'open\');" style="padding:0.4rem 0.8rem;cursor:pointer;display:flex;justify-content:space-between;align-items:center;background:#f8f9fa;user-select:none;">' +
'<span><span style="background:#43aa8b;color:#fff;border-radius:3px;padding:0 5px;font-size:0.7rem;font-weight:600;margin-right:6px;">🔧</span>' +
'<span style="font-weight:600;font-size:0.8rem;">' + escapeHtml(tc2.name||'?') + '</span></span>' +
'<span style="font-size:0.75rem;color:#888;">▶</span></div>' +
'<div style="display:none;padding:0.5rem 0.8rem;background:#fafbfc;">' +
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输入:</div>' +
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0 0 0.3rem;"><code>' + escapeHtml(inputStr) + '</code></pre>' +
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输出:</div>' +
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0;"><code>' + escapeHtml(outputStr) + '</code></pre>' +
'</div>';
container.appendChild(card);
}
}
}
})();
</script>
{% endif %}
{% endblock %}