refactor: Wave 2 - auto.html remove dataset selector + timeline merge + file_count fix

This commit is contained in:
PM-pinou
2026-07-22 20:46:10 +08:00
parent 2a64f554c4
commit 0264bade2e
4 changed files with 86 additions and 79 deletions
+6 -1
View File
@@ -155,7 +155,12 @@ class SessionStore:
continue
try:
lf, schema_csv, row_count, file_count, memory_mb = load_csv_directory(csv_glob)
# Merge schema from CSV (may be more accurate than persisted schema)
# Preserve original metadata counts from disk — CSV re-scan may
# detect different file/row counts if files changed on disk.
if 'file_count' not in meta and file_count is not None:
meta['file_count'] = file_count
if 'row_count' not in meta and row_count is not None:
meta['row_count'] = row_count
self.store_dataset(ds_id, lf, schema=schema_csv, metadata=meta)
restored += 1
except Exception:
+8 -2
View File
@@ -1301,11 +1301,17 @@ def run_llm_analysis_view(request):
def auto_page(request):
"""LLM-driven auto analysis page."""
runs = AnalysisRun.objects.filter(status='ready').order_by('-created_at')[:20]
cfg = get_config()
selected_run = None
run_id = request.GET.get('run_id')
if run_id:
try:
selected_run = AnalysisRun.objects.get(display_id=int(run_id))
except (ValueError, AnalysisRun.DoesNotExist):
pass
return render(request, 'tianxuan/auto.html', {
'runs': runs,
'llm_configured': bool(cfg.llm.base_url and cfg.llm.api_key),
'selected_run': selected_run,
})
+71 -75
View File
@@ -116,40 +116,23 @@
{% endif %}
</div>
<div class="card">
<h3>选择数据集</h3>
{% if runs %}
<table>
<thead><tr><th></th><th>ID</th><th>文件</th><th>状态</th></tr></thead>
<tbody>
{% for run in runs %}
<tr>
<td><input type="radio" name="auto_run_id" value="{{ run.display_id }}"></td>
<td>#{{ run.display_id }}</td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;">{% if run.sqlite_table %}SQLite表 {{ run.sqlite_table }}{% else %}{{ run.csv_glob }}{% endif %}</td>
<td><span class="badge badge-success">{{ run.status }}</span></td>
</tr>
{% empty %}
<tr><td colspan="4" style="text-align:center;">暂无数据。先去 <a href="/upload/">上传 CSV</a></td></tr>
{% endfor %}
</tbody>
</table>
<button class="btn btn-primary" style="margin-top:1rem;" onclick="startAuto()" {% if not llm_configured %}disabled{% endif %}>开始自动分析</button>
{% endif %}
<div id="autoProgress" style="display:none;margin-top:1rem;">
<p>LLM 编排中... <span id="autoStatus"></span></p>
<div style="background:#eee;height:8px;border-radius:4px;overflow:hidden;">
<div id="autoBar" style="background:#4361ee;height:100%;width:0%;"></div>
</div>
<button id="cancelBtn" class="btn" style="background:#f72585;color:white;display:none;" onclick="cancelAnalysis()">取消分析</button>
<!-- Unified timeline: interleaved thinking + tool calls -->
<div id="timelinePanel" style="display:none;margin-top:1rem;">
<h4 style="margin-bottom:0.6rem;color:#333;">📋 分析步骤</h4>
<div id="timeline"></div>
</div>
<div id="autoProgress" style="display:none;margin-top:1rem;">
<p>LLM 编排中... <span id="autoStatus"></span></p>
<div style="background:#eee;height:8px;border-radius:4px;overflow:hidden;">
<div id="autoBar" style="background:#4361ee;height:100%;width:0%;"></div>
</div>
<button id="cancelBtn" class="btn" style="background:#f72585;color:white;display:none;" onclick="cancelAnalysis()">取消分析</button>
<!-- Unified timeline: interleaved thinking + tool calls -->
<div id="timelinePanel" style="display:none;margin-top:1rem;">
<h4 style="margin-bottom:0.6rem;color:#333;">📋 分析步骤</h4>
<div id="timeline"></div>
</div>
</div>
<div id="noRunMessage" style="background:#fff3cd;padding:1rem;border-radius:6px;margin:1rem 0;display:none;">
请从上传页面启动LLM分析
</div>
<script>
@@ -176,20 +159,8 @@ function formatTimestamp() {
}
function buildTimeline(llmThinking, toolCalls) {
// Parse llm_thinking into [{step, text}] entries
const thoughtEntries = [];
if (llmThinking) {
const parts = llmThinking.split(/\n(?=\[\d+\])/);
for (const part of parts) {
const m = part.match(/^\[(\d+)\]\s*(.*)/s);
if (m) {
thoughtEntries.push({ step: parseInt(m[1]), type: 'thought', text: m[2].trim() });
}
}
}
// tool_calls already have step, type, name, input, output
const toolEntries = (toolCalls || []).map(tc => ({
// Build tool entries from tool_calls_json (no separate thought entries)
const entries = (toolCalls || []).map(tc => ({
step: tc.step,
type: 'tool',
name: tc.name,
@@ -197,9 +168,25 @@ function buildTimeline(llmThinking, toolCalls) {
output: tc.output,
}));
// Merge and sort by step
const merged = [...thoughtEntries, ...toolEntries].sort((a, b) => a.step - b.step);
return merged;
// Match llm_thinking text to tool entries by step number
if (llmThinking) {
const thinkingByStep = {};
const parts = llmThinking.split(/\n(?=\[\d+\])/);
for (const part of parts) {
const m = part.match(/^\[(\d+)\]\s*(.*)/s);
if (m) {
thinkingByStep[parseInt(m[1])] = m[2].trim();
}
}
for (const entry of entries) {
if (thinkingByStep[entry.step] !== undefined) {
entry.thinking = thinkingByStep[entry.step];
}
}
}
entries.sort((a, b) => a.step - b.step);
return entries;
}
function renderTimeline(entries) {
@@ -222,26 +209,26 @@ function renderTimeline(entries) {
lastStep = entry.step;
}
if (entry.type === 'thought') {
// LLM reasoning — collapsible
const card = document.createElement('div');
card.style.cssText = 'border:1px solid #d0d8f0;border-radius:6px;margin-bottom:0.3rem;overflow:hidden;background:#f8f9ff;';
card.innerHTML =
'<div class="tool-call-header" onclick="toggleStep(this)" style="background:#eef2ff;padding:0.4rem 0.8rem;">' +
'<span><span style="background:#4361ee;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;">LLM 推理</span></span>' +
'<span class="arrow">▶</span>' +
'</div>' +
'<div class="tool-call-body" style="padding:0.6rem 0.8rem;font-size:0.78rem;line-height:1.6;color:#333;background:#fff;">' +
escapeHtml(entry.text) +
'</div>';
container.appendChild(card);
} else if (entry.type === 'tool') {
// Tool call — collapsible with I/O
if (entry.type === 'tool') {
// Tool call — collapsible with I/O; thinking shown when output is null
const card = document.createElement('div');
card.style.cssText = 'border:1px solid #e0e0e0;border-radius:6px;margin-bottom:0.3rem;overflow:hidden;background:#fff;';
const inputStr = prettyJson(entry.input);
const hasThinking = entry.thinking && (entry.output == null || entry.output === '' || entry.output === 'null' || entry.output === 'None');
const outputStr = prettyJson(entry.output);
let bodyInner = '';
bodyInner +=
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输入 (INPUT):</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 0;"><code>' + escapeHtml(inputStr) + '</code></pre>';
if (hasThinking) {
bodyInner +=
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">思考 (THINKING):</div>' +
'<div style="background:#eef2ff;padding:0.6rem;border-radius:4px;font-size:0.78rem;line-height:1.6;color:#333;max-height:200px;overflow:auto;margin:0;white-space:pre-wrap;">' + escapeHtml(entry.thinking) + '</div>';
} else {
bodyInner +=
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输出 (OUTPUT):</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>';
}
card.innerHTML =
'<div class="tool-call-header" onclick="toggleStep(this)" style="padding:0.4rem 0.8rem;">' +
'<span><span style="background:#43aa8b;color:#fff;border-radius:3px;padding:0 5px;font-size:0.7rem;font-weight:600;margin-right:6px;">🔧</span>' +
@@ -249,10 +236,7 @@ function renderTimeline(entries) {
'<span class="arrow">▶</span>' +
'</div>' +
'<div class="tool-call-body" style="padding:0.5rem 0.8rem;background:#fafbfc;">' +
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输入 (INPUT):</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 0;"><code>' + escapeHtml(inputStr) + '</code></pre>' +
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输出 (OUTPUT):</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>' +
bodyInner +
'</div>';
container.appendChild(card);
}
@@ -260,13 +244,18 @@ function renderTimeline(entries) {
}
async function startAuto() {
const selected = document.querySelector('input[name="auto_run_id"]:checked');
if (!selected) { showError('请先选择一个数据集'); return; }
const params = new URLSearchParams(window.location.search);
const runId = params.get('run_id');
if (!runId) {
document.getElementById('noRunMessage').style.display = 'block';
return;
}
const timelineContainer = document.getElementById('timeline');
const timelinePanel = document.getElementById('timelinePanel');
// Reset UI
document.getElementById('noRunMessage').style.display = 'none';
document.getElementById('autoProgress').style.display = 'block';
document.getElementById('cancelBtn').style.display = 'inline-block';
timelinePanel.style.display = 'none';
@@ -277,11 +266,10 @@ async function startAuto() {
resp = await fetch('/analyze/llm/', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({ run_id: parseInt(selected.value) }),
body: JSON.stringify({ run_id: parseInt(runId) }),
});
} catch (e) { showError('启动 LLM 分析失败: ' + e.message); return; }
const data = await resp.json();
const runId = selected.value;
let lastTimelineHash = '';
@@ -296,7 +284,7 @@ async function startAuto() {
// Build and render unified timeline
const entries = buildTimeline(s.llm_thinking, s.tool_calls);
const hash = JSON.stringify(entries.map(e => ({step:e.step,type:e.type,name:e.name,text:(e.text||'').slice(0,40)})));
const hash = JSON.stringify(entries.map(e => ({step:e.step,name:e.name,thinking:(e.thinking||'').slice(0,40)})));
if (hash !== lastTimelineHash) {
renderTimeline(entries);
lastTimelineHash = hash;
@@ -324,6 +312,14 @@ function cancelAnalysis() {
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('timelinePanel').innerHTML += '\n--- 分析已取消 ---\n';
}
// Auto-start if run_id is provided via URL parameter
(function() {
const params = new URLSearchParams(window.location.search);
if (params.get('run_id')) {
startAuto();
}
})();
</script>
{% endblock %}
<!--VERSION2-->
+1 -1
View File
@@ -194,7 +194,7 @@ function buildDatasetTable(filter) {
return;
}
let html = '<table class="ds-table"><thead><tr><th>ID</th><th>状态</th><th>行数</th><th>文件</th><th></th></tr></thead><tbody>';
let html = '<table class="ds-table"><thead><tr><th>ID</th><th>状态</th><th>行数</th><th>文件</th><th></th></tr></thead><tbody>';
rows.forEach(ds => {
const label = ds.status_label || '待分析';
const badgeClass = (ds.run_status === 'completed') ? 'completed' :