Spaces:
Running
Running
| {% extends "layout.html" %} | |
| {% block head %} | |
| <style> | |
| /* 修复“开始分析”按钮中加载动画和文字的对齐问题 */ | |
| #start-analysis-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; /* 在图标/加载动画和文字之间添加一些间距 */ | |
| } | |
| /* 为市场下拉菜单应用自定义样式 */ | |
| #market-type.form-select { | |
| -webkit-appearance: none; | |
| -moz-appearance: none; | |
| appearance: none; | |
| background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); | |
| background-repeat: no-repeat; | |
| background-position: right 0.75rem center; | |
| background-size: 16px 12px; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block title %}智能体分析 - 智能分析系统{% endblock %} | |
| {% block content %} | |
| <div class="container-fluid py-3"> | |
| <div id="alerts-container"></div> | |
| <!-- Page Title --> | |
| <div class="row mb-3"> | |
| <div class="col-12"> | |
| <h2 class="mb-0">智能体分析</h2> | |
| <p class="text-muted">利用多智能体框架进行深度股票评估</p> | |
| </div> | |
| </div> | |
| <!-- Input Form Card --> | |
| <div class="row"> | |
| <div class="col-12"> | |
| <div class="card"> | |
| <div class="card-header py-2"> | |
| <h5 class="mb-0">分析设置</h5> | |
| </div> | |
| <div class="card-body py-2"> | |
| <form id="agent-analysis-form" class="row g-3 align-items-end"> | |
| <div class="col-md-3"> | |
| <label for="stock-code" class="form-label">股票代码</label> | |
| <div class="input-group"> | |
| <span class="input-group-text"><i class="fas fa-barcode"></i></span> | |
| <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519, AAPL" required> | |
| </div> | |
| </div> | |
| <div class="col-md-2"> | |
| <label for="market-type" class="form-label">市场</label> | |
| <div class="input-group"> | |
| <select class="form-select" id="market-type"> | |
| <option value="A" selected>A股</option> | |
| <option value="US">美股</option> | |
| <option value="HK">港股</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="col-md-2"> | |
| <label for="analysis-date" class="form-label">分析日期</label> | |
| <div class="input-group"> | |
| <input type="date" class="form-control" id="analysis-date"> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <label for="research-depth" class="form-label">研究深度</label> | |
| <select class="form-select" id="research-depth"> | |
| <option value="1">1级 - 快速</option> | |
| <option value="2">2级 - 基础</option> | |
| <option value="3" selected>3级 - 标准</option> | |
| <option value="4">4级 - 深度</option> | |
| <option value="5">5级 - 全面</option> | |
| </select> | |
| </div> | |
| <div class="col-md-2"> | |
| <button type="submit" id="start-analysis-btn" class="btn btn-primary w-100"> | |
| <i class="fas fa-rocket"></i> 开始分析 | |
| </button> | |
| </div> | |
| <div class="col-12 mt-2"> | |
| <label class="form-label mb-1">选择分析师团队:</label> | |
| <div id="analyst-selection" class="d-flex flex-wrap gap-3"> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="analyst-market" value="market" checked> | |
| <label class="form-check-label" for="analyst-market">市场</label> | |
| </div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="analyst-news" value="news" checked> | |
| <label class="form-check-label" for="analyst-news">新闻</label> | |
| </div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="analyst-social" value="social" checked> | |
| <label class="form-check-label" for="analyst-social">社交</label> | |
| </div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="analyst-fundamentals" value="fundamentals" checked> | |
| <label class="form-check-label" for="analyst-fundamentals">基本面</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-9 mt-2"> | |
| <label for="max-output-length" class="form-label">最大输出长度: <span id="max-output-length-value" class="fw-bold text-primary">2048</span> tokens</label> | |
| <input type="range" class="form-range" id="max-output-length" min="512" max="8192" step="256" value="2048"> | |
| </div> | |
| <div class="col-md-3 mt-2 d-flex align-items-end justify-content-end"> | |
| <div class="form-check form-switch pb-2"> | |
| <input class="form-check-input" type="checkbox" id="enable-memory" checked> | |
| <label class="form-check-label" for="enable-memory">启用记忆功能</label> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Area --> | |
| <div id="results-area" class="mt-4" style="display: none;"> | |
| <!-- Progress Bar --> | |
| <div id="progress-container" class="mb-3"> | |
| <h5 id="progress-step" class="text-center mb-2">正在初始化...</h5> | |
| <div class="progress" style="height: 20px;"> | |
| <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> | |
| </div> | |
| </div> | |
| <!-- Analysis Results Card --> | |
| <div id="results-card" class="card" style="display: none;"> | |
| <div class="card-header d-flex justify-content-between align-items-center"> | |
| <h5 class="mb-0">分析报告</h5> | |
| <div id="export-buttons"> | |
| <!-- Export buttons will be added here --> | |
| </div> | |
| </div> | |
| <div class="card-body"> | |
| <div id="results-content"> | |
| <!-- Dynamic results will be rendered here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- History Card --> | |
| <div class="row mt-4"> | |
| <div class="col-12"> | |
| <div class="card"> | |
| <div class="card-header d-flex justify-content-between align-items-center py-2"> | |
| <h5 class="mb-0"><i class="fas fa-history me-2"></i>分析历史</h5> | |
| <button id="refresh-history-btn" class="btn btn-sm btn-outline-primary"> | |
| <i class="fas fa-sync-alt"></i> 刷新 | |
| </button> | |
| </div> | |
| <div class="card-body"> | |
| <div id="history-table-container"> | |
| <!-- History table will be rendered here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| $(document).ready(function() { | |
| let pollingInterval; | |
| $('#agent-analysis-form').submit(function(e) { | |
| e.preventDefault(); | |
| const stockCode = $('#stock-code').val().trim(); | |
| const researchDepth = $('#research-depth').val(); | |
| const marketType = $('#market-type').val(); | |
| const analysisDate = $('#analysis-date').val(); | |
| const enableMemory = $('#enable-memory').is(':checked'); | |
| const maxOutputLength = $('#max-output-length').val(); | |
| const selectedAnalysts = $('#analyst-selection input:checked').map(function() { | |
| return $(this).val(); | |
| }).get(); | |
| if (!stockCode) { | |
| showError('请输入股票代码!'); | |
| return; | |
| } | |
| if (selectedAnalysts.length === 0) { | |
| showError('请至少选择一个分析师!'); | |
| return; | |
| } | |
| // Disable button and show progress | |
| $('#start-analysis-btn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 分析中...'); | |
| $('#results-area').show(); | |
| $('#progress-container').show(); | |
| $('#results-card').hide(); | |
| updateProgress(0, '任务已提交...'); | |
| // Start analysis | |
| $.ajax({ | |
| url: '/api/start_agent_analysis', | |
| type: 'POST', | |
| contentType: 'application/json', | |
| data: JSON.stringify({ | |
| stock_code: stockCode, | |
| market_type: marketType, | |
| selected_analysts: selectedAnalysts, | |
| research_depth: parseInt(researchDepth), | |
| analysis_date: analysisDate, | |
| enable_memory: enableMemory, | |
| max_output_length: parseInt(maxOutputLength) | |
| }), | |
| success: function(response) { | |
| if (response.task_id) { | |
| startPolling(response.task_id); | |
| } else { | |
| showError(response.error || '无法启动分析任务'); | |
| resetForm(); | |
| } | |
| }, | |
| error: function(xhr) { | |
| const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '请求失败'; | |
| showError('启动分析失败: ' + errorMsg); | |
| resetForm(); | |
| } | |
| }); | |
| }); | |
| function startPolling(taskId) { | |
| pollingInterval = setInterval(function() { | |
| $.ajax({ | |
| url: `/api/agent_analysis_status/${taskId}`, | |
| type: 'GET', | |
| cache: false, // 禁用缓存,确保每次都获取最新状态 | |
| success: function(response) { | |
| if (!response) return; | |
| // 优先使用 result.current_step,如果没有则使用默认文本 | |
| const currentStep = response.result ? response.result.current_step : '处理中...'; | |
| updateProgress(response.progress, currentStep); | |
| if (response.status === 'completed' || response.status === 'failed') { | |
| clearInterval(pollingInterval); | |
| if (response.status === 'completed') { | |
| displayResults(response.result); | |
| } else { | |
| showError(response.error || '分析任务失败'); | |
| } | |
| resetForm(); | |
| } | |
| }, | |
| error: function(xhr) { | |
| clearInterval(pollingInterval); | |
| const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '请求失败'; | |
| showError('获取状态失败: ' + errorMsg); | |
| resetForm(); | |
| } | |
| }); | |
| }, 3000); // Poll every 3 seconds | |
| } | |
| function updateProgress(percent, step) { | |
| $('#alerts-container').empty(); // 清除“正在恢复”等提示信息 | |
| percent = Math.min(percent, 100); | |
| $('#progress-bar').css('width', percent + '%').attr('aria-valuenow', percent).text(percent + '%'); | |
| $('#progress-step').text(step); | |
| } | |
| function displayResults(result) { | |
| $('#progress-container').hide(); | |
| $('#results-card').show(); | |
| let contentHtml = '<h4><i class="fas fa-exclamation-triangle text-danger me-2"></i>分析结果加载失败</h4>'; | |
| if (result && result.decision) { | |
| const decision = result.decision; | |
| const state = result.final_state || {}; | |
| const stockInfo = { | |
| code: state.company_of_interest || 'N/A', | |
| name: state.company_name || '未知公司', | |
| market: state.market_type || 'N/A' | |
| }; | |
| // --- Helper functions --- | |
| const parseFundamentals = (report) => { | |
| if (typeof report !== 'string') { | |
| return {}; | |
| } | |
| const metrics = {}; | |
| const patterns = { | |
| debt_ratio: /\*\*?资产负债率\*\*?\s*[::\s是为]*\s*([\d\.]+)/i, | |
| liquidity_ratio: /\*\*?流动比率\*\*?\s*[::\s是为]*\s*([\d\.]+)/i, | |
| quick_ratio: /\*\*?速动比率\*\*?\s*[::\s是为]*\s*([\d\.]+)/i, | |
| gross_margin: /\*\*?毛利率\*\*?\s*[::\s是为]*\s*([\d\.]+)/i, | |
| net_margin: /\*\*?净利率\*\*?\s*[::\s是为]*\s*([\d\.]+)/i, | |
| pb_ratio: /\*\*?市净率(?:\(PB\))?\*\*?\s*[::\s是为]*\s*([\d\.]+)/i | |
| }; | |
| for (const key in patterns) { | |
| const match = report.match(patterns[key]); | |
| if (match && match[1]) { | |
| metrics[key] = parseFloat(match[1]); | |
| } else { | |
| metrics[key] = null; | |
| } | |
| } | |
| return metrics; | |
| }; | |
| const fundamentals = parseFundamentals(state.fundamentals_report); | |
| const getRatingClass = (value, thresholds, ascending = true) => { | |
| if (value === null || value === undefined || isNaN(value)) return 'text-muted'; | |
| const [low, mid, high] = thresholds; | |
| if (ascending) { | |
| if (value >= high) return 'text-success'; | |
| if (value >= mid) return 'text-primary'; | |
| if (value >= low) return 'text-warning'; | |
| return 'text-danger'; | |
| } else { | |
| if (value <= low) return 'text-success'; | |
| if (value <= mid) return 'text-primary'; | |
| if (value <= high) return 'text-warning'; | |
| return 'text-danger'; | |
| } | |
| }; | |
| const getRatingText = (value, thresholds, labels, ascending = true) => { | |
| if (value === null || value === undefined || isNaN(value)) return 'N/A'; | |
| const [low, mid, high] = thresholds; | |
| const [labelLow, labelMid, labelHigh, labelVeryHigh] = labels; | |
| if (ascending) { | |
| if (value >= high) return labelVeryHigh; | |
| if (value >= mid) return labelHigh; | |
| if (value >= low) return labelMid; | |
| return labelLow; | |
| } else { | |
| if (value <= low) return labelVeryHigh; | |
| if (value <= mid) return labelHigh; | |
| if (value <= high) return labelMid; | |
| return labelLow; | |
| } | |
| }; | |
| const renderTable = (title, headers, rows) => { | |
| let tableHtml = `<div class="card h-100 shadow-sm"> | |
| <div class="card-header bg-light"> | |
| <h6 class="mb-0">${title}</h6> | |
| </div> | |
| <div class="card-body p-0"> | |
| <table class="table table-sm table-hover mb-0"> | |
| <thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead> | |
| <tbody>`; | |
| rows.forEach(row => { | |
| tableHtml += `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`; | |
| }); | |
| tableHtml += `</tbody></table></div></div>`; | |
| return tableHtml; | |
| }; | |
| // --- Main Content --- | |
| contentHtml = ` | |
| <!-- Header --> | |
| <div class="text-center mb-4"> | |
| <h2>${stockInfo.name} (${stockInfo.code}) - 智能体深度分析报告</h2> | |
| <p class="text-muted">报告生成日期: ${new Date().toLocaleDateString()}</p> | |
| </div> | |
| <!-- Investment Suggestion --> | |
| <div class="alert ${getDecisionClass(decision.action).replace('text-', 'alert-')} border-0 shadow-lg mb-4"> | |
| <h4 class="alert-heading"><i class="fas fa-lightbulb me-2"></i>投资建议: <span class="fw-bold">${decision.action}</span></h4> | |
| <p class="mb-0">${decision.reasoning}</p> | |
| </div> | |
| <!-- Key Metrics --> | |
| <div class="row mb-4"> | |
| <div class="col-md-4"> | |
| <div class="card text-center h-100 shadow-sm"> | |
| <div class="card-body"> | |
| <h6 class="text-muted">置信度</h6> | |
| <p class="display-5 fw-bold mb-0">${(decision.confidence * 100).toFixed(1)}%</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="card text-center h-100 shadow-sm"> | |
| <div class="card-body"> | |
| <h6 class="text-muted">风险评分</h6> | |
| <p class="display-5 fw-bold mb-0 ${getRatingClass(decision.risk_score * 100, [30, 50, 70], false)}">${(decision.risk_score * 100).toFixed(1)}%</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="card text-center h-100 shadow-sm"> | |
| <div class="card-body"> | |
| <h6 class="text-muted">市场情绪</h6> | |
| <p class="display-5 fw-bold mb-0">${state.sentiment_score || 'N/A'}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Financial, Profitability & Valuation Tables --> | |
| <div class="row g-4 mb-4"> | |
| <div class="col-lg-6"> | |
| ${renderTable( | |
| '<i class="fas fa-balance-scale me-2"></i>财务状况评估', | |
| ['指标', '数值', '评估'], | |
| [ | |
| ['资产负债率', fundamentals.debt_ratio !== null ? `${fundamentals.debt_ratio}%` : 'N/A', `<span class="${getRatingClass(fundamentals.debt_ratio, [40, 60, 80], false)}">${getRatingText(fundamentals.debt_ratio, [40, 60, 80], ['高风险', '较高风险', '中等', '良好'], false)}</span>`], | |
| ['流动比率', fundamentals.liquidity_ratio !== null ? fundamentals.liquidity_ratio : 'N/A', `<span class="${getRatingClass(fundamentals.liquidity_ratio, [1, 1.5, 2], true)}">${getRatingText(fundamentals.liquidity_ratio, [1, 1.5, 2], ['风险', '吃紧', '良好', '优秀'], true)}</span>`], | |
| ['速动比率', fundamentals.quick_ratio !== null ? fundamentals.quick_ratio : 'N/A', `<span class="${getRatingClass(fundamentals.quick_ratio, [0.8, 1, 1.2], true)}">${getRatingText(fundamentals.quick_ratio, [0.8, 1, 1.2], ['危险', '风险', '尚可', '良好'], true)}</span>`] | |
| ] | |
| )} | |
| </div> | |
| <div class="col-lg-6"> | |
| ${renderTable( | |
| '<i class="fas fa-chart-line me-2"></i>盈利与估值', | |
| ['指标', '数值', '评估'], | |
| [ | |
| ['毛利率', fundamentals.gross_margin !== null ? `${fundamentals.gross_margin}%` : 'N/A', `<span class="${getRatingClass(fundamentals.gross_margin, [10, 20, 30], true)}">${getRatingText(fundamentals.gross_margin, [10, 20, 30], ['极低', '偏低', '中等', '优秀'], true)}</span>`], | |
| ['净利率', fundamentals.net_margin !== null ? `${fundamentals.net_margin}%` : 'N/A', `<span class="${getRatingClass(fundamentals.net_margin, [0, 5, 10], true)}">${getRatingText(fundamentals.net_margin, [0, 5, 10], ['亏损', '微利', '良好', '优秀'], true)}</span>`], | |
| ['市净率(PB)', fundamentals.pb_ratio !== null ? fundamentals.pb_ratio : 'N/A', `<span class="${getRatingClass(fundamentals.pb_ratio, [1, 2, 4], false)}">${getRatingText(fundamentals.pb_ratio, [1, 2, 4], ['高估', '偏高', '合理', '低估'], false)}</span>`] | |
| ] | |
| )} | |
| </div> | |
| </div> | |
| <!-- Detailed Reports Accordion --> | |
| <h3 class="mt-5 mb-3">详细分析报告</h3> | |
| <div class="accordion" id="reports-accordion"> | |
| `; | |
| const reports = [ | |
| { title: '<i class="fas fa-chart-pie me-2"></i>市场分析报告', content: state.market_report, id: 'market' }, | |
| { title: '<i class="fas fa-comments me-2"></i>社交媒体情绪报告', content: state.sentiment_report, id: 'social' }, | |
| { title: '<i class="far fa-newspaper me-2"></i>新闻分析报告', content: state.news_report, id: 'news' }, | |
| { title: '<i class="fas fa-file-invoice-dollar me-2"></i>基本面分析报告', content: state.fundamentals_report, id: 'fundamentals' }, | |
| { title: '<i class="fas fa-gavel me-2"></i>投资辩论总结', content: state.investment_debate_state?.judge_decision, id: 'invest_debate' }, | |
| { title: '<i class="fas fa-exclamation-triangle me-2"></i>风险分析总结', content: state.risk_debate_state?.judge_decision, id: 'risk_debate' } | |
| ]; | |
| reports.forEach((report, index) => { | |
| if (report.content && (typeof report.content === 'string' ? report.content.trim() !== '' : true)) { | |
| let reportContent; | |
| if (typeof report.content === 'object') { | |
| reportContent = `<pre>${JSON.stringify(report.content, null, 2)}</pre>`; | |
| } else if (typeof report.content === 'string') { | |
| // 使用Marked.js渲染Markdown | |
| reportContent = marked.parse(report.content); | |
| } else { | |
| reportContent = '<p>内容格式无法解析。</p>'; | |
| } | |
| contentHtml += ` | |
| <div class="accordion-item"> | |
| <h2 class="accordion-header" id="heading-${report.id}"> | |
| <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${report.id}" aria-expanded="false" aria-controls="collapse-${report.id}"> | |
| <strong>${report.title}</strong> | |
| </button> | |
| </h2> | |
| <div id="collapse-${report.id}" class="accordion-collapse collapse" aria-labelledby="heading-${report.id}" data-bs-parent="#reports-accordion"> | |
| <div class="accordion-body bg-light"> | |
| ${reportContent} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| }); | |
| contentHtml += '</div>'; | |
| } | |
| $('#results-content').html(contentHtml); | |
| } | |
| function getDecisionClass(action) { | |
| if (action.toLowerCase().includes('buy') || action.toLowerCase().includes('买入')) { | |
| return 'text-success'; | |
| } else if (action.toLowerCase().includes('sell') || action.toLowerCase().includes('卖出')) { | |
| return 'text-danger'; | |
| } | |
| return 'text-warning'; | |
| } | |
| function resetForm() { | |
| $('#start-analysis-btn').prop('disabled', false).html('<i class="fas fa-rocket"></i> 开始分析'); | |
| } | |
| // You can reuse the showError function from layout.html if it's globally available | |
| function showError(message) { | |
| const alertHtml = ` | |
| <div class="alert alert-danger alert-dismissible fade show" role="alert"> | |
| ${message} | |
| <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | |
| </div> | |
| `; | |
| $('#alerts-container').html(alertHtml); | |
| } | |
| // Check for task_id in URL on page load to resume polling | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const taskIdFromUrl = urlParams.get('task_id'); | |
| if (taskIdFromUrl) { | |
| showInfo('正在恢复分析任务...'); | |
| $('#results-area').show(); | |
| $('#progress-container').show(); | |
| $('#results-card').hide(); | |
| // Fetch initial task state to get stock code | |
| $.ajax({ | |
| url: `/api/agent_analysis_status/${taskIdFromUrl}`, | |
| type: 'GET', | |
| success: function(task) { | |
| if (task && task.params && task.params.stock_code) { | |
| $('#stock-code').val(task.params.stock_code); | |
| } | |
| startPolling(taskIdFromUrl); | |
| }, | |
| error: function() { | |
| showError('无法恢复分析任务,任务ID可能已失效。'); | |
| } | |
| }); | |
| } | |
| // --- History Section Logic --- | |
| function loadHistory() { | |
| $('#history-table-container').html('<div class="text-center p-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>'); | |
| $.ajax({ | |
| url: '/api/agent_analysis_history', | |
| type: 'GET', | |
| cache: false, | |
| success: function(response) { | |
| if (response && response.history) { | |
| renderHistoryTable(response.history); | |
| } else { | |
| showError('无法加载分析历史。'); | |
| } | |
| }, | |
| error: function(xhr) { | |
| const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '请求失败'; | |
| showError('加载历史失败: ' + errorMsg); | |
| $('#history-table-container').html('<div class="alert alert-danger">加载历史记录失败。</div>'); | |
| } | |
| }); | |
| } | |
| function renderHistoryTable(history) { | |
| if (history.length === 0) { | |
| $('#history-table-container').html('<div class="alert alert-info">没有找到任何分析历史记录。</div>'); | |
| return; | |
| } | |
| let tableHtml = ` | |
| <div class="mb-3"> | |
| <button id="delete-selected-btn" class="btn btn-danger" disabled> | |
| <i class="fas fa-trash-alt me-2"></i>删除选中项 | |
| </button> | |
| </div> | |
| <div class="table-responsive"> | |
| <table class="table table-hover table-sm align-middle"> | |
| <thead> | |
| <tr> | |
| <th><input class="form-check-input" type="checkbox" id="select-all-checkbox"></th> | |
| <th>#</th> | |
| <th>股票代码</th> | |
| <th>状态</th> | |
| <th>研究深度</th> | |
| <th>分析师</th> | |
| <th>创建时间</th> | |
| <th>更新时间</th> | |
| <th class="text-end">操作</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| history.forEach((task, index) => { | |
| const params = task.params || {}; | |
| const analysts = params.selected_analysts || []; | |
| const statusBadge = getStatusBadge(task.status); | |
| tableHtml += ` | |
| <tr data-task-id="${task.id}"> | |
| <td><input class="form-check-input row-checkbox" type="checkbox" value="${task.id}"></td> | |
| <td>${index + 1}</td> | |
| <td><strong>${params.stock_code || 'N/A'}</strong> <span class="badge bg-secondary">${params.market_type || ''}</span></td> | |
| <td>${statusBadge}</td> | |
| <td><span class="badge bg-info">${params.research_depth || 'N/A'}级</span></td> | |
| <td><small>${analysts.join(', ')}</small></td> | |
| <td><small>${task.created_at}</small></td> | |
| <td><small>${task.updated_at}</small></td> | |
| <td class="text-end"> | |
| <a href="/agent_analysis?task_id=${task.id}" class="btn btn-sm btn-primary"> | |
| <i class="fas fa-eye"></i> 查看 | |
| </a> | |
| <button class="btn btn-sm btn-outline-danger delete-single-btn" data-task-id="${task.id}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| tableHtml += '</tbody></table></div>'; | |
| $('#history-table-container').html(tableHtml); | |
| } | |
| function getStatusBadge(status) { | |
| if (status === 'completed') { | |
| return '<span class="badge bg-success">已完成</span>'; | |
| } else if (status === 'failed') { | |
| return '<span class="badge bg-danger">失败</span>'; | |
| } | |
| return `<span class="badge bg-secondary">${status}</span>`; | |
| } | |
| $('#refresh-history-btn').click(function() { | |
| loadHistory(); | |
| }); | |
| // Initial load of history | |
| loadHistory(); | |
| // --- Deletion and Selection Logic --- | |
| function performDeletion(taskIds) { | |
| if (!confirm(`您确定要删除 ${taskIds.length} 个分析记录吗?此操作无法撤销。`)) { | |
| return; | |
| } | |
| $.ajax({ | |
| url: '/api/delete_agent_analysis', | |
| type: 'POST', | |
| contentType: 'application/json', | |
| data: JSON.stringify({ task_ids: taskIds }), | |
| success: function(response) { | |
| if (response.success) { | |
| showSuccess(response.message || '成功删除记录'); | |
| loadHistory(); // Refresh the history table | |
| } else { | |
| showError(response.error || '删除失败'); | |
| } | |
| }, | |
| error: function(xhr) { | |
| const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '请求失败'; | |
| showError('删除操作失败: ' + errorMsg); | |
| } | |
| }); | |
| } | |
| // Event delegation for dynamically added elements | |
| $('#history-table-container').on('click', '.delete-single-btn', function() { | |
| const taskId = $(this).data('task-id'); | |
| performDeletion([taskId]); | |
| }); | |
| $('#history-table-container').on('click', '#delete-selected-btn', function() { | |
| const selectedIds = $('.row-checkbox:checked').map(function() { | |
| return $(this).val(); | |
| }).get(); | |
| if (selectedIds.length > 0) { | |
| performDeletion(selectedIds); | |
| } | |
| }); | |
| // Checkbox selection logic | |
| $('#history-table-container').on('change', '#select-all-checkbox', function() { | |
| $('.row-checkbox').prop('checked', $(this).prop('checked')); | |
| updateDeleteButtonState(); | |
| }); | |
| $('#history-table-container').on('change', '.row-checkbox', function() { | |
| if ($('.row-checkbox:checked').length === $('.row-checkbox').length) { | |
| $('#select-all-checkbox').prop('checked', true); | |
| } else { | |
| $('#select-all-checkbox').prop('checked', false); | |
| } | |
| updateDeleteButtonState(); | |
| }); | |
| // Set analysis date to today by default | |
| document.getElementById('analysis-date').valueAsDate = new Date(); | |
| // Handle max output length slider | |
| const maxLengthSlider = document.getElementById('max-output-length'); | |
| const maxLengthValue = document.getElementById('max-output-length-value'); | |
| if (maxLengthSlider) { | |
| maxLengthSlider.addEventListener('input', function() { | |
| maxLengthValue.textContent = this.value; | |
| }); | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |