You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cbmc/specgen/web_interface/index.html

844 lines
37 KiB

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CBMC SpecGen - 智能代码验证平台</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
<style>
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.code-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
tab-size: 4;
}
.status-running {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.result-card {
max-height: 400px;
overflow-y: auto;
}
pre[class*="language-"] {
background: #2d2d2d;
margin: 0;
padding: 1em;
border-radius: 0.5em;
}
.file-upload-area {
border: 2px dashed #cbd5e0;
border-radius: 0.5rem;
transition: all 0.3s ease;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.file-upload-area:hover {
border-color: #4299e1;
background: linear-gradient(135deg, #ebf8ff 0%, #dbeafe 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.file-upload-area.dragover {
border-color: #3182ce;
background: linear-gradient(135deg, #bee3f8 0%, #90cdf4 100%);
transform: scale(1.02);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
}
.file-input-hidden {
display: none;
}
.upload-icon {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.file-info-card {
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
border-left: 4px solid #0ea5e9;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Header -->
<header class="gradient-bg text-white shadow-lg">
<div class="container mx-auto px-4 py-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">CBMC SpecGen</h1>
<p class="text-blue-100 mt-2">基于大语言模型的C/C++智能代码验证平台</p>
</div>
<div class="flex items-center space-x-4">
<button onclick="loadSystemInfo()" class="bg-white text-purple-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition">
🔄 系统状态
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<!-- System Info Card -->
<div id="systemInfo" class="bg-white rounded-lg shadow-md p-6 mb-8 hidden">
<h2 class="text-xl font-bold mb-4">系统信息</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center">
<div class="text-3xl font-bold text-blue-600" id="cbmcVersion">-</div>
<div class="text-gray-600">CBMC版本</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-green-600" id="activeTasks">0</div>
<div class="text-gray-600">活跃任务</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-purple-600" id="completedTasks">0</div>
<div class="text-gray-600">完成任务</div>
</div>
</div>
</div>
<!-- Input Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold">📝 代码输入</h2>
<div class="flex space-x-2">
<button onclick="loadDemo()" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition">
📋 加载示例
</button>
<button onclick="clearInput()" class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition">
🗑️ 清空
</button>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
C/C++ 代码:
</label>
<!-- 文件上传区域 -->
<div id="fileUploadArea" class="file-upload-area p-6 mb-4 text-center cursor-pointer">
<div class="flex flex-col items-center">
<svg class="upload-icon w-12 h-12 text-blue-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-gray-700 font-medium mb-2">点击或拖拽C/C++文件到此处上传</p>
<p class="text-sm text-gray-500">支持 .c, .cpp, .h, .hpp, .cc, .cxx 文件 (最大5MB)</p>
</div>
<input type="file" id="fileInput" class="file-input-hidden" accept=".c,.cpp,.h,.hpp,.cc,.cxx" multiple>
</div>
<!-- 上传的文件信息 -->
<div id="uploadedFiles" class="hidden mb-4">
<div class="file-info-card border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-blue-600 mr-3 text-lg">📄</span>
<div>
<span id="fileName" class="text-sm font-medium text-blue-900 block"></span>
<span id="fileSize" class="text-xs text-blue-700"></span>
</div>
</div>
<button onclick="removeUploadedFile()" class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md text-sm transition-colors duration-200">
✕ 移除
</button>
</div>
</div>
</div>
<textarea
id="inputCode"
class="code-editor w-full h-64 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="在此输入您的C/C++代码,或通过上方文件上传..."
spellcheck="false"
></textarea>
</div>
<div class="flex items-center justify-between">
<div class="text-sm text-gray-600">
<span id="charCount">0</span> 字符
</div>
<button
onclick="submitTask()"
id="submitBtn"
class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition font-semibold disabled:bg-gray-400 disabled:cursor-not-allowed"
>
🚀 开始验证
</button>
</div>
</div>
<!-- Task Status -->
<div id="taskStatus" class="bg-white rounded-lg shadow-md p-6 mb-8 hidden">
<h2 class="text-xl font-bold mb-4">📊 任务状态</h2>
<div class="mb-4">
<div class="flex justify-between text-sm mb-2">
<span id="taskStatusText">准备中...</span>
<span id="taskProgress">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div id="progressBar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<div class="text-sm text-gray-600">
任务ID: <code id="taskId" class="bg-gray-100 px-2 py-1 rounded">-</code>
</div>
</div>
<!-- Results Section -->
<div id="results" class="hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Generated Code -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-bold mb-4">✨ 生成的验证代码</h3>
<div class="result-card">
<pre><code id="generatedCode" class="language-c"></code></pre>
</div>
<button onclick="copyToClipboard('generatedCode')" class="mt-4 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition">
📋 复制代码
</button>
</div>
<!-- Verification Results -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-bold mb-4">✅ CBMC验证结果</h3>
<div id="verificationSummary" class="mb-4"></div>
<div class="result-card">
<pre><code id="verificationOutput" class="language-text"></code></pre>
</div>
</div>
<!-- AI Analysis Report -->
<div id="analysisReportSection" class="bg-white rounded-lg shadow-md p-6 hidden">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold">🤖 AI智能分析报告</h3>
<div class="flex items-center space-x-2">
<span id="analysisStatus" class="px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
生成中...
</span>
<button onclick="refreshAnalysisReport()" class="text-blue-600 hover:text-blue-900" title="刷新分析报告">
🔄
</button>
</div>
</div>
<div id="analysisReportContent" class="prose prose-sm max-w-none">
<!-- AI分析报告内容将在这里显示 -->
</div>
</div>
</div>
<!-- Task History -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-bold mb-4">📜 任务历史</h3>
<div id="taskHistory" class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">任务ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">断言数</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">通过率</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">完成时间</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody id="taskHistoryBody" class="bg-white divide-y divide-gray-200">
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-6 mt-12">
<div class="container mx-auto px-4 text-center">
<p>© 2024 CBMC SpecGen - 智能代码验证平台</p>
<p class="text-gray-400 mt-2">基于DeepSeek API和CBMC的形式化验证工具</p>
</div>
</footer>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-c.min.js"></script>
<script>
const API_BASE = 'http://localhost:8080/api';
let currentTaskId = null;
let statusCheckInterval = null;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
const inputCode = document.getElementById('inputCode');
inputCode.addEventListener('input', updateCharCount);
// 初始化文件上传功能
initFileUpload();
loadTaskHistory();
loadSystemInfo();
});
// 初始化文件上传功能
function initFileUpload() {
const fileUploadArea = document.getElementById('fileUploadArea');
const fileInput = document.getElementById('fileInput');
// 点击上传区域触发文件选择
fileUploadArea.addEventListener('click', () => {
fileInput.click();
});
// 文件选择事件
fileInput.addEventListener('change', handleFileSelect);
// 拖拽事件
fileUploadArea.addEventListener('dragover', handleDragOver);
fileUploadArea.addEventListener('dragleave', handleDragLeave);
fileUploadArea.addEventListener('drop', handleFileDrop);
}
// 处理文件选择
function handleFileSelect(event) {
const files = event.target.files;
if (files.length > 0) {
processFile(files[0]);
}
}
// 处理拖拽悬停
function handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
document.getElementById('fileUploadArea').classList.add('dragover');
}
// 处理拖拽离开
function handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
document.getElementById('fileUploadArea').classList.remove('dragover');
}
// 处理文件拖拽放下
function handleFileDrop(event) {
event.preventDefault();
event.stopPropagation();
document.getElementById('fileUploadArea').classList.remove('dragover');
const files = event.dataTransfer.files;
if (files.length > 0) {
processFile(files[0]);
}
}
// 处理文件读取
async function processFile(file) {
// 验证文件类型
const validExtensions = ['.c', '.cpp', '.h', '.hpp', '.cc', '.cxx'];
const fileName = file.name.toLowerCase();
const isValidFile = validExtensions.some(ext => fileName.endsWith(ext));
if (!isValidFile) {
showToast('请选择有效的C/C++文件 (.c, .cpp, .h, .hpp)', 'error');
return;
}
// 验证文件大小 (限制为5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
showToast('文件大小不能超过5MB', 'error');
return;
}
try {
const content = await readFileContent(file);
// 将文件内容填入代码编辑器
document.getElementById('inputCode').value = content;
updateCharCount();
// 显示文件信息
showUploadedFileInfo(file);
showToast(`文件 "${file.name}" 上传成功`, 'success');
} catch (error) {
showToast('文件读取失败: ' + error.message, 'error');
}
}
// 读取文件内容
function readFileContent(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsText(file, 'UTF-8');
});
}
// 显示上传的文件信息
function showUploadedFileInfo(file) {
const uploadedFilesDiv = document.getElementById('uploadedFiles');
const fileNameSpan = document.getElementById('fileName');
const fileSizeSpan = document.getElementById('fileSize');
fileNameSpan.textContent = file.name;
fileSizeSpan.textContent = `(${formatFileSize(file.size)})`;
uploadedFilesDiv.classList.remove('hidden');
}
// 移除上传的文件
function removeUploadedFile() {
document.getElementById('uploadedFiles').classList.add('hidden');
document.getElementById('fileInput').value = '';
document.getElementById('inputCode').value = '';
updateCharCount();
showToast('文件已移除', 'info');
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 更新字符计数
function updateCharCount() {
const code = document.getElementById('inputCode').value;
document.getElementById('charCount').textContent = code.length;
}
// 加载示例代码
async function loadDemo() {
try {
const response = await fetch(`${API_BASE}/demo/sample`);
const data = await response.json();
document.getElementById('inputCode').value = data.sample_code;
updateCharCount();
showToast('示例代码已加载', 'success');
} catch (error) {
showToast('加载示例失败', 'error');
}
}
// 清空输入
function clearInput() {
document.getElementById('inputCode').value = '';
document.getElementById('uploadedFiles').classList.add('hidden');
document.getElementById('fileInput').value = '';
updateCharCount();
}
// 提交任务
async function submitTask() {
const inputCode = document.getElementById('inputCode').value.trim();
if (!inputCode) {
showToast('请输入代码', 'error');
return;
}
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '⏳ 提交中...';
try {
const response = await fetch(`${API_BASE}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
input_code: inputCode,
options: {}
})
});
const data = await response.json();
if (response.ok) {
currentTaskId = data.task_id;
showTaskStatus();
startStatusCheck();
showToast('任务提交成功', 'success');
} else {
showToast(data.error || '提交失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 开始验证';
}
}
// 显示任务状态
function showTaskStatus() {
document.getElementById('taskStatus').classList.remove('hidden');
document.getElementById('taskId').textContent = currentTaskId;
}
// 开始状态检查
function startStatusCheck() {
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
statusCheckInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/tasks/${currentTaskId}`);
const task = await response.json();
updateTaskStatus(task);
if (task.status === 'completed') {
clearInterval(statusCheckInterval);
loadTaskResult(task.task_id);
showToast('验证完成', 'success');
} else if (task.status === 'failed') {
clearInterval(statusCheckInterval);
showToast('验证失败: ' + (task.error || '未知错误'), 'error');
}
} catch (error) {
console.error('检查状态失败:', error);
}
}, 2000);
}
// 更新任务状态显示
function updateTaskStatus(task) {
const statusText = document.getElementById('taskStatusText');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('taskProgress');
const statusMap = {
'pending': '⏳ 等待中...',
'running': '🔄 处理中...',
'completed': '✅ 已完成',
'failed': '❌ 失败'
};
statusText.textContent = statusMap[task.status] || task.status;
progressBar.style.width = `${task.progress}%`;
progressText.textContent = `${task.progress}%`;
if (task.status === 'running') {
progressBar.classList.add('status-running');
} else {
progressBar.classList.remove('status-running');
}
}
// 加载任务结果
async function loadTaskResult(taskId) {
try {
const response = await fetch(`${API_BASE}/tasks/${taskId}/result`);
const result = await response.json();
displayResults(result);
loadTaskHistory();
} catch (error) {
showToast('加载结果失败', 'error');
}
}
// 显示结果
function displayResults(result) {
document.getElementById('results').classList.remove('hidden');
// 显示生成的代码
document.getElementById('generatedCode').textContent = result.generated_code;
Prism.highlightElement(document.getElementById('generatedCode'));
// 显示验证摘要
const summary = result.summary;
const summaryHtml = `
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold ${summary.verification_success ? 'text-green-600' : 'text-red-600'}">
${summary.verification_success ? '✅ 通过' : '❌ 失败'}
</div>
<div class="text-sm text-gray-600">验证状态</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">${summary.total_assertions}</div>
<div class="text-sm text-gray-600">总断言数</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600">${summary.passed_assertions}</div>
<div class="text-sm text-gray-600">通过断言</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-red-600">${summary.failed_assertions}</div>
<div class="text-sm text-gray-600">失败断言</div>
</div>
</div>
`;
document.getElementById('verificationSummary').innerHTML = summaryHtml;
// 显示验证输出
document.getElementById('verificationOutput').textContent = result.verification_result.output;
// 显示AI分析报告
displayAnalysisReport(result);
}
// 加载任务历史
async function loadTaskHistory() {
try {
const response = await fetch(`${API_BASE}/tasks`);
const tasks = await response.json();
const tbody = document.getElementById('taskHistoryBody');
tbody.innerHTML = '';
tasks.slice(0, 10).forEach(task => {
const row = document.createElement('tr');
const statusClass = task.status === 'completed' ? 'text-green-600' :
task.status === 'failed' ? 'text-red-600' : 'text-yellow-600';
const statusText = task.status === 'completed' ? '✅ 完成' :
task.status === 'failed' ? '❌ 失败' : '⏳ 运行中';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<code class="bg-gray-100 px-2 py-1 rounded">${task.task_id.substring(0, 8)}</code>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm ${statusClass} font-medium">
${statusText}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${task.result?.summary?.total_assertions || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${task.result?.summary?.total_assertions ?
Math.round((task.result.summary.passed_assertions / task.result.summary.total_assertions) * 100) + '%' : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${task.completed_at ? new Date(task.completed_at).toLocaleString() : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
${task.status === 'completed' ?
`<button onclick="viewResult('${task.task_id}')" class="text-blue-600 hover:text-blue-900">查看</button>` : '-'}
</td>
`;
tbody.appendChild(row);
});
if (tasks.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">暂无任务记录</td></tr>';
}
} catch (error) {
console.error('加载任务历史失败:', error);
}
}
// 查看结果
async function viewResult(taskId) {
try {
const response = await fetch(`${API_BASE}/tasks/${taskId}/result`);
const result = await response.json();
displayResults(result);
document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
} catch (error) {
showToast('加载结果失败', 'error');
}
}
// 加载系统信息
async function loadSystemInfo() {
try {
const response = await fetch(`${API_BASE}/system/info`);
const info = await response.json();
document.getElementById('cbmcVersion').textContent = info.cbmc_version.split('\n')[0];
document.getElementById('activeTasks').textContent = info.active_tasks;
document.getElementById('completedTasks').textContent = info.completed_tasks;
document.getElementById('systemInfo').classList.remove('hidden');
} catch (error) {
console.error('加载系统信息失败:', error);
}
}
// 复制到剪贴板
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
const textArea = document.createElement('textarea');
textArea.value = element.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showToast('已复制到剪贴板', 'success');
}
// 显示AI分析报告
function displayAnalysisReport(result) {
const reportSection = document.getElementById('analysisReportSection');
const reportContent = document.getElementById('analysisReportContent');
const analysisStatus = document.getElementById('analysisStatus');
if (result.analysis_generated && result.analysis_report) {
// 显示生成的分析报告
reportSection.classList.remove('hidden');
analysisStatus.textContent = 'AI分析完成';
analysisStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800';
// 将Markdown报告转换为HTML并显示
const htmlContent = markdownToHtml(result.analysis_report);
reportContent.innerHTML = htmlContent;
// 添加一些样式增强
enhanceReportStyles();
} else if (result.analysis_error) {
// 显示分析错误
reportSection.classList.remove('hidden');
analysisStatus.textContent = '分析失败';
analysisStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800';
reportContent.innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<div class="flex items-center">
<span class="text-red-600 mr-2">❌</span>
<div>
<h4 class="text-red-800 font-medium">分析报告生成失败</h4>
<p class="text-red-600 text-sm mt-1">${result.analysis_error}</p>
</div>
</div>
</div>
`;
} else {
// 分析报告不可用
reportSection.classList.add('hidden');
}
}
// 刷新分析报告
async function refreshAnalysisReport() {
if (!currentTaskId) return;
const analysisStatus = document.getElementById('analysisStatus');
const reportContent = document.getElementById('analysisReportContent');
try {
analysisStatus.textContent = '刷新中...';
analysisStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800';
const response = await fetch(`${API_BASE}/tasks/${currentTaskId}/analysis`);
const data = await response.json();
if (data.analysis_report) {
const htmlContent = markdownToHtml(data.analysis_report);
reportContent.innerHTML = htmlContent;
enhanceReportStyles();
analysisStatus.textContent = 'AI分析完成';
analysisStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800';
showToast('分析报告已刷新', 'success');
} else {
throw new Error(data.analysis_error || '分析报告不可用');
}
} catch (error) {
analysisStatus.textContent = '刷新失败';
analysisStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800';
showToast('刷新分析报告失败: ' + error.message, 'error');
}
}
// 简单的Markdown转HTML转换器
function markdownToHtml(markdown) {
if (!markdown) return '';
return markdown
// 标题
.replace(/^# (.*$)/gim, '<h1 class="text-2xl font-bold text-gray-900 mb-4 mt-6">$1</h1>')
.replace(/^## (.*$)/gim, '<h2 class="text-xl font-bold text-gray-800 mb-3 mt-5">$1</h2>')
.replace(/^### (.*$)/gim, '<h3 class="text-lg font-bold text-gray-700 mb-2 mt-4">$1</h3>')
// 粗体和斜体
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// 列表
.replace(/^- (.*$)/gim, '<li class="ml-4">• $1</li>')
.replace(/(<li.*<\/li>)/s, '<ul class="list-disc ml-6 mb-3">$1</ul>')
// 链接
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-600 hover:text-blue-800 underline" target="_blank">$1</a>')
// 代码块
.replace(/```([^`]+)```/g, '<pre class="bg-gray-100 p-3 rounded-md overflow-x-auto my-3"><code>$1</code></pre>')
// 行内代码
.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-2 py-1 rounded text-sm">$1</code>')
// 段落
.replace(/\n\n/g, '</p><p class="mb-4">')
.replace(/^/, '<p class="mb-4">')
.replace(/$/, '</p>');
}
// 增强报告样式
function enhanceReportStyles() {
// 为表格添加样式
const tables = document.querySelectorAll('#analysisReportContent table');
tables.forEach(table => {
table.className = 'min-w-full divide-y divide-gray-200 border border-gray-300 rounded-lg overflow-hidden my-4';
const headers = table.querySelectorAll('th');
headers.forEach(header => {
header.className = 'px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200';
});
const cells = table.querySelectorAll('td');
cells.forEach(cell => {
cell.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900 border-b border-gray-100';
});
});
// 为状态指示器添加样式
const statusIndicators = document.querySelectorAll('#analysisReportContent .text-green-600, #analysisReportContent .text-red-600, #analysisReportContent .text-yellow-600');
statusIndicators.forEach(indicator => {
if (indicator.textContent.includes('✅') || indicator.textContent.includes('🟢')) {
indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800';
} else if (indicator.textContent.includes('❌') || indicator.textContent.includes('🔴')) {
indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800';
} else if (indicator.textContent.includes('⚠️') || indicator.textContent.includes('🟡')) {
indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800';
}
});
}
// Toast 提示
function showToast(message, type = 'info') {
const toast = document.createElement('div');
const bgColor = type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500';
toast.className = `fixed top-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg z-50 transition-opacity duration-300`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => document.body.removeChild(toast), 300);
}, 3000);
}
</script>
</body>
</html>