296 lines
12 KiB
HTML
296 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI 分析 - AI Agent 项目追踪</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
</head>
|
|
<body class="d-flex flex-column min-vh-100">
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="/">AI Agent 追踪</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="mainNav">
|
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
<li class="nav-item"><a class="nav-link" href="/">首页</a></li>
|
|
<li class="nav-item"><a class="nav-link" href="/projects">项目列表</a></li>
|
|
<li class="nav-item"><a class="nav-link" href="/search">全局搜索</a></li>
|
|
</ul>
|
|
<form class="d-flex" action="/search" method="get" id="navSearchForm">
|
|
<input class="form-control me-2 nav-search-input" type="search"
|
|
name="q" placeholder="搜索项目 / release / issue" aria-label="Search">
|
|
<button class="btn btn-outline-light" type="submit">搜索</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="container py-4 flex-grow-1">
|
|
|
|
<h1 class="mb-4">AI 分析</h1>
|
|
|
|
<!-- Control panel -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<!-- Scope selector -->
|
|
<div class="col-md-3">
|
|
<label class="form-label">分析范围</label>
|
|
<select class="form-select" id="scopeSelect">
|
|
<option value="home">全站概览</option>
|
|
<option value="projects">所有项目</option>
|
|
<option value="project/1">项目: hermes-agent</option>
|
|
<option value="project/2">项目: opencode</option>
|
|
<option value="project/3">项目: wxhook</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Preset buttons -->
|
|
<div class="col-md-6">
|
|
<label class="form-label">预设分析</label>
|
|
<div class="btn-group w-100" role="group">
|
|
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="activity">
|
|
📊 活跃度评估
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="evolution">
|
|
🚀 版本演进
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="issues_hot">
|
|
🔥 Issue 热点
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start button -->
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<button type="button" class="btn btn-success w-100" id="startBtn">
|
|
开始分析
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom question input -->
|
|
<div class="row g-3 mt-2">
|
|
<div class="col-12">
|
|
<label class="form-label">自定义问题 (可选)</label>
|
|
<input type="text" class="form-control" id="questionInput"
|
|
placeholder="例如: 这个项目的核心功能是什么? / 最近有什么重要更新?">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Output area with tabs -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs" id="outputTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="observation-tab" data-bs-toggle="tab"
|
|
data-bs-target="#observation" type="button" role="tab">
|
|
数据观察
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab"
|
|
data-bs-target="#prompt" type="button" role="tab">
|
|
Prompt
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="reasoning-tab" data-bs-toggle="tab"
|
|
data-bs-target="#reasoning" type="button" role="tab">
|
|
LLM 推理
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="conclusion-tab" data-bs-toggle="tab"
|
|
data-bs-target="#conclusion" type="button" role="tab">
|
|
结论
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tab-content" id="outputTabContent">
|
|
<div class="tab-pane fade show active" id="observation" role="tabpanel">
|
|
<div class="output-content" id="observationContent">
|
|
<div class="text-muted">等待分析开始...</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="prompt" role="tabpanel">
|
|
<div class="output-content" id="promptContent">
|
|
<div class="text-muted">等待分析开始...</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="reasoning" role="tabpanel">
|
|
<div class="output-content" id="reasoningContent">
|
|
<div class="text-muted">等待分析开始...</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="conclusion" role="tabpanel">
|
|
<div class="output-content" id="conclusionContent">
|
|
<div class="text-muted">等待分析开始...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer class="bg-dark text-light text-center py-3 mt-auto">
|
|
<div class="container">
|
|
AI Agent 项目追踪 - Powered by Flask
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="/static/js/app.js"></script>
|
|
|
|
<script>
|
|
// State
|
|
let currentEventSource = null;
|
|
let currentTab = 'observation'; // Track which tab we're writing to
|
|
let reasoningStarted = false;
|
|
let conclusionStarted = false;
|
|
|
|
// DOM elements
|
|
const scopeSelect = document.getElementById('scopeSelect');
|
|
const questionInput = document.getElementById('questionInput');
|
|
const startBtn = document.getElementById('startBtn');
|
|
const presetBtns = document.querySelectorAll('.preset-btn');
|
|
|
|
const observationContent = document.getElementById('observationContent');
|
|
const promptContent = document.getElementById('promptContent');
|
|
const reasoningContent = document.getElementById('reasoningContent');
|
|
const conclusionContent = document.getElementById('conclusionContent');
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('#outputTabs button').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const tabId = e.target.id.replace('-tab', '');
|
|
currentTab = tabId;
|
|
});
|
|
});
|
|
|
|
// Preset button click
|
|
presetBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const preset = btn.dataset.preset;
|
|
startAnalysis(null, preset);
|
|
});
|
|
});
|
|
|
|
// Start button click
|
|
startBtn.addEventListener('click', () => {
|
|
const question = questionInput.value.trim();
|
|
if (!question) {
|
|
alert('请输入自定义问题或选择预设分析');
|
|
return;
|
|
}
|
|
startAnalysis(question, null);
|
|
});
|
|
|
|
function startAnalysis(question, preset) {
|
|
// Close existing connection
|
|
if (currentEventSource) {
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
}
|
|
|
|
// Reset content
|
|
observationContent.innerHTML = '<div class="text-muted">正在收集上下文...</div>';
|
|
promptContent.innerHTML = '<div class="text-muted">等待...</div>';
|
|
reasoningContent.innerHTML = '<div class="text-muted">等待...</div>';
|
|
conclusionContent.innerHTML = '<div class="text-muted">等待...</div>';
|
|
reasoningStarted = false;
|
|
conclusionStarted = false;
|
|
|
|
// Build URL
|
|
const scope = scopeSelect.value;
|
|
const params = new URLSearchParams({scope});
|
|
if (question) params.append('question', question);
|
|
if (preset) params.append('preset', preset);
|
|
|
|
const url = `/api/analyze/stream?${params.toString()}`;
|
|
|
|
// Start SSE
|
|
currentEventSource = new EventSource(url);
|
|
|
|
// Handle step events
|
|
currentEventSource.addEventListener('step', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const step = data.step;
|
|
const content = data.content;
|
|
|
|
if (step === 'context') {
|
|
observationContent.innerHTML = `<div class="alert alert-info">${escapeHtml(content)}</div>`;
|
|
} else if (step === 'prompt') {
|
|
promptContent.innerHTML = `<pre class="bg-light p-3 rounded" style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
|
|
} else if (step === 'complete') {
|
|
conclusionContent.innerHTML = `<div class="alert alert-success">${escapeHtml(content)}</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
}
|
|
});
|
|
|
|
// Handle token events (streaming LLM output)
|
|
currentEventSource.addEventListener('token', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const text = data.text;
|
|
|
|
// Determine which tab to write to based on content markers
|
|
if (!reasoningStarted && (text.includes('1.') || text.includes('数据观察') || text.includes('初步判断'))) {
|
|
reasoningStarted = true;
|
|
reasoningContent.innerHTML = '';
|
|
}
|
|
if (!conclusionStarted && (text.includes('4.') || text.includes('结论'))) {
|
|
conclusionStarted = true;
|
|
conclusionContent.innerHTML = '';
|
|
}
|
|
|
|
// Write to appropriate tab
|
|
if (reasoningStarted && !conclusionStarted) {
|
|
reasoningContent.innerHTML += escapeHtml(text);
|
|
} else if (conclusionStarted) {
|
|
conclusionContent.innerHTML += escapeHtml(text);
|
|
} else {
|
|
// Default to reasoning if no markers yet
|
|
reasoningContent.innerHTML += escapeHtml(text);
|
|
}
|
|
});
|
|
|
|
// Handle errors
|
|
currentEventSource.addEventListener('error', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const errorMsg = data.error || '未知错误';
|
|
observationContent.innerHTML = `<div class="alert alert-danger">错误: ${escapeHtml(errorMsg)}</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
});
|
|
|
|
currentEventSource.onerror = (e) => {
|
|
observationContent.innerHTML = `<div class="alert alert-danger">连接错误, 请检查后端日志</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
};
|
|
}
|
|
|
|
// Helper: escape HTML
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
|
|
|
|
<!-- 全站浮动 AI 分析面板 -->
|
|
<button id="ai-fab" type="button" title="AI 分析">🤖</button>
|
|
<script src="/static/js/ai-panel.js"></script>
|
|
</body>
|
|
</html> |