refactor: Wave 2 - auto.html remove dataset selector + timeline merge + file_count fix
This commit is contained in:
@@ -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
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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-->
|
||||
@@ -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' :
|
||||
|
||||
Reference in New Issue
Block a user