diff --git a/BettaFish.bat b/BettaFish.bat new file mode 100644 index 0000000..e851f51 --- /dev/null +++ b/BettaFish.bat @@ -0,0 +1,13 @@ +@echo off + +REM 切换到当前 bat 文件所在目录(不怕有中文路径) +cd /d "%~dp0" + +REM 激活虚拟环境(注意:是 activate.bat,不是 Activate.ps1) +call .\myenv\Scripts\activate.bat + +REM 运行你的程序 +python app.py + +REM 防止双击后窗口一闪而过(如果不需要可以删掉这一行) +pause diff --git a/ReportEngine/agent.py b/ReportEngine/agent.py index 82c67eb..b1a0005 100644 --- a/ReportEngine/agent.py +++ b/ReportEngine/agent.py @@ -190,10 +190,16 @@ class ReportAgent: save_report: 鏄惁淇濆瓨鎶ュ憡鍒版枃浠 Returns: - 鏈缁圚TML鎶ュ憡鍐呭 + dict: 鍖呭惈HTML鍐呭涓庝繚瀛樻枃浠朵俊鎭 """ start_time = datetime.now() + # 涓烘柊鐨勬煡璇㈤噸缃姸鎬侊紝纭繚鏂囦欢鍛藉悕淇℃伅瀹屾暣 + self.state = ReportState(query=query) + self.state.metadata.query = query + self.state.query = query + self.state.mark_processing() + logger.info(f"寮濮嬬敓鎴愭姤鍛: {query}") logger.info(f"杈撳叆鏁版嵁 - 鎶ュ憡鏁伴噺: {len(reports)}, 璁哄潧鏃ュ織闀垮害: {len(forum_logs)}") @@ -205,8 +211,9 @@ class ReportAgent: html_report = self._generate_html_report(query, reports, forum_logs, template_result) # Step 3: 淇濆瓨鎶ュ憡 + saved_files = {} if save_report: - self._save_report(html_report) + saved_files = self._save_report(html_report) # 鏇存柊鐢熸垚鏃堕棿 end_time = datetime.now() @@ -215,7 +222,10 @@ class ReportAgent: logger.info(f"鎶ュ憡鐢熸垚瀹屾垚锛岃楁椂: {generation_time:.2f} 绉") - return html_report + return { + 'html_content': html_report, + **saved_files + } except Exception as e: logger.exception(f"鎶ュ憡鐢熸垚杩囩▼涓彂鐢熼敊璇: {str(e)}") @@ -357,13 +367,26 @@ class ReportAgent: with open(filepath, 'w', encoding='utf-8') as f: f.write(html_content) - logger.info(f"鎶ュ憡宸蹭繚瀛樺埌: {filepath}") + abs_report_path = os.path.abspath(filepath) + rel_report_path = os.path.relpath(abs_report_path, os.getcwd()) + logger.info(f"鎶ュ憡宸蹭繚瀛樺埌: {abs_report_path}") # 淇濆瓨鐘舵 state_filename = f"report_state_{query_safe}_{timestamp}.json" state_filepath = os.path.join(self.config.OUTPUT_DIR, state_filename) self.state.save_to_file(state_filepath) - logger.info(f"鐘舵佸凡淇濆瓨鍒: {state_filepath}") + abs_state_path = os.path.abspath(state_filepath) + rel_state_path = os.path.relpath(abs_state_path, os.getcwd()) + logger.info(f"鐘舵佸凡淇濆瓨鍒: {abs_state_path}") + + return { + 'report_filename': filename, + 'report_filepath': abs_report_path, + 'report_relative_path': rel_report_path, + 'state_filename': state_filename, + 'state_filepath': abs_state_path, + 'state_relative_path': rel_state_path + } def get_progress_summary(self) -> Dict[str, Any]: """鑾峰彇杩涘害鎽樿""" @@ -492,4 +515,4 @@ def create_agent(config_file: Optional[str] = None) -> ReportAgent: """ config = Settings() # 浠ョ┖閰嶇疆鍒濆鍖栵紝鑰屼粠浠庣幆澧冨彉閲忓垵濮嬪寲 - return ReportAgent(config) + return ReportAgent(config) \ No newline at end of file diff --git a/ReportEngine/flask_interface.py b/ReportEngine/flask_interface.py index 75f71d4..73b9375 100644 --- a/ReportEngine/flask_interface.py +++ b/ReportEngine/flask_interface.py @@ -8,7 +8,7 @@ import json import threading import time from datetime import datetime -from flask import Blueprint, request, jsonify, Response +from flask import Blueprint, request, jsonify, Response, send_file from typing import Dict, Any from loguru import logger from .agent import ReportAgent, create_agent @@ -50,6 +50,11 @@ class ReportTask: self.created_at = datetime.now() self.updated_at = datetime.now() self.html_content = "" + self.report_file_path = "" + self.report_file_relative_path = "" + self.report_file_name = "" + self.state_file_path = "" + self.state_file_relative_path = "" def update_status(self, status: str, progress: int = None, error_message: str = ""): """鏇存柊浠诲姟鐘舵""" @@ -70,7 +75,10 @@ class ReportTask: 'error_message': self.error_message, 'created_at': self.created_at.isoformat(), 'updated_at': self.updated_at.isoformat(), - 'has_result': bool(self.html_content) + 'has_result': bool(self.html_content), + 'report_file_ready': bool(self.report_file_path), + 'report_file_name': self.report_file_name, + 'report_file_path': self.report_file_relative_path } @@ -119,7 +127,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " task.update_status("running", 50) # 鐢熸垚鎶ュ憡 - html_report = report_agent.generate_report( + generation_result = report_agent.generate_report( query=query, reports=content['reports'], forum_logs=content['forum_logs'], @@ -127,10 +135,17 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " save_report=True ) + html_report = generation_result.get('html_content', '') + task.update_status("running", 90) # 淇濆瓨缁撴灉 task.html_content = html_report + task.report_file_path = generation_result.get('report_filepath', '') + task.report_file_relative_path = generation_result.get('report_relative_path', '') + task.report_file_name = generation_result.get('report_filename', '') + task.state_file_path = generation_result.get('state_filepath', '') + task.state_file_relative_path = generation_result.get('state_relative_path', '') task.update_status("completed", 100) except Exception as e: @@ -251,7 +266,10 @@ def get_progress(task_id: str): 'status': 'completed', 'progress': 100, 'error_message': '', - 'has_result': True + 'has_result': True, + 'report_file_ready': False, + 'report_file_name': '', + 'report_file_path': '' } }) @@ -329,6 +347,44 @@ def get_result_json(task_id: str): }), 500 +@report_bp.route('/download/', methods=['GET']) +def download_report(task_id: str): + """涓嬭浇宸茬敓鎴愮殑鎶ュ憡HTML鏂囦欢""" + try: + if not current_task or current_task.task_id != task_id: + return jsonify({ + 'success': False, + 'error': '浠诲姟涓嶅瓨鍦' + }), 404 + + if current_task.status != "completed" or not current_task.report_file_path: + return jsonify({ + 'success': False, + 'error': '鎶ュ憡灏氭湭瀹屾垚鎴栧皻鏈繚瀛' + }), 400 + + if not os.path.exists(current_task.report_file_path): + return jsonify({ + 'success': False, + 'error': '鎶ュ憡鏂囦欢涓嶅瓨鍦ㄦ垨宸茶鍒犻櫎' + }), 404 + + download_name = current_task.report_file_name or os.path.basename(current_task.report_file_path) + return send_file( + current_task.report_file_path, + mimetype='text/html', + as_attachment=True, + download_name=download_name + ) + + except Exception as e: + logger.exception(f"涓嬭浇鎶ュ憡澶辫触: {str(e)}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @report_bp.route('/cancel/', methods=['POST']) def cancel_task(task_id: str): """鍙栨秷鎶ュ憡鐢熸垚浠诲姟""" @@ -478,4 +534,4 @@ def clear_log(): return jsonify({ 'success': False, 'error': f'娓呯┖鏃ュ織澶辫触: {str(e)}' - }), 500 + }), 500 \ No newline at end of file diff --git a/app.py b/app.py index 3134e5f..5b2d979 100644 --- a/app.py +++ b/app.py @@ -656,6 +656,14 @@ def stop_streamlit_app(app_name): except Exception as e: return False, f"鍋滄澶辫触: {str(e)}" +HEALTHCHECK_PATH = "/_stcore/health" +HEALTHCHECK_PROXIES = {'http': None, 'https': None} + + +def _build_healthcheck_url(port): + return f"http://127.0.0.1:{port}{HEALTHCHECK_PATH}" + + def check_app_status(): """妫鏌ュ簲鐢ㄧ姸鎬""" for app_name, info in processes.items(): @@ -663,21 +671,24 @@ def check_app_status(): if info['process'].poll() is None: # 杩涚▼浠嶅湪杩愯锛屾鏌ョ鍙f槸鍚﹀彲璁块棶 try: - response = requests.get(f"http://localhost:{info['port']}", timeout=2) + response = requests.get( + _build_healthcheck_url(info['port']), + timeout=2, + proxies=HEALTHCHECK_PROXIES + ) if response.status_code == 200: info['status'] = 'running' else: info['status'] = 'starting' - except requests.exceptions.RequestException: - info['status'] = 'starting' - except Exception: + except Exception as exc: + logger.warning(f"{app_name} 鍋ュ悍妫鏌ュけ璐: {exc}") info['status'] = 'starting' else: # 杩涚▼宸茬粨鏉 info['process'] = None info['status'] = 'stopped' -def wait_for_app_startup(app_name, max_wait_time=30): +def wait_for_app_startup(app_name, max_wait_time=90): """绛夊緟搴旂敤鍚姩瀹屾垚""" import time start_time = time.time() @@ -690,15 +701,19 @@ def wait_for_app_startup(app_name, max_wait_time=30): return False, "杩涚▼鍚姩澶辫触" try: - response = requests.get(f"http://localhost:{info['port']}", timeout=2) + response = requests.get( + _build_healthcheck_url(info['port']), + timeout=2, + proxies=HEALTHCHECK_PROXIES + ) if response.status_code == 200: info['status'] = 'running' return True, "鍚姩鎴愬姛" - except: - pass - + except Exception as exc: + logger.warning(f"{app_name} 鍋ュ悍妫鏌ュけ璐: {exc}") + time.sleep(1) - + return False, "鍚姩瓒呮椂" def cleanup_processes(): @@ -1042,4 +1057,4 @@ if __name__ == '__main__': logger.info("\n姝e湪鍏抽棴搴旂敤...") cleanup_processes() - + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 2bd05b8..eb39fcb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -699,6 +699,13 @@ font-size: 13px; } + .task-actions { + margin-top: 15px; + display: flex; + gap: 12px; + flex-wrap: wrap; + } + @keyframes spin { to { transform: rotate(360deg); } } @@ -801,51 +808,51 @@ } /* 涓嶅悓Engine鐨勯鑹插尯鍒 */ -聽 聽 聽 聽 .forum-message.agent:has(.forum-message-header:contains("Query Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #eaf1f8; -聽 聽 聽 聽 聽 聽 border-color: #608ab1; -聽 聽 聽 聽 } + .forum-message.agent:has(.forum-message-header:contains("Query Engine")) { + background-color: #eaf1f8; + border-color: #608ab1; + } .forum-message.agent:has(.forum-message-header:contains("QUERY Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #eaf1f8; -聽 聽 聽 聽 聽 聽 border-color: #608ab1; - 聽 聽 } + background-color: #eaf1f8; + border-color: #608ab1; + } -聽 聽 聽 聽 .forum-message.agent:has(.forum-message-header:contains("Insight Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #f2ebf3; -聽 聽 聽 聽 聽 聽 border-color: #8e6a9f; -聽 聽 聽 聽 } + .forum-message.agent:has(.forum-message-header:contains("Insight Engine")) { + background-color: #f2ebf3; + border-color: #8e6a9f; + } .forum-message.agent:has(.forum-message-header:contains("INSIGHT Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #f2ebf3; -聽 聽 聽 聽 聽 聽 border-color: #8e6a9f; -聽 聽 聽 聽 } + background-color: #f2ebf3; + border-color: #8e6a9f; + } -聽 聽 聽 聽 .forum-message.agent:has(.forum-message-header:contains("Media Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #ebf2ea; -聽 聽 聽 聽 聽 聽 border-color: #6a9a6e; -聽 聽 聽 聽 } + .forum-message.agent:has(.forum-message-header:contains("Media Engine")) { + background-color: #ebf2ea; + border-color: #6a9a6e; + } .forum-message.agent:has(.forum-message-header:contains("MEDIA Engine")) { -聽 聽 聽 聽 聽 聽 background-color: #ebf2ea; -聽 聽 聽 聽 聽 聽 border-color: #6a9a6e; -聽 聽 聽 聽 } + background-color: #ebf2ea; + border-color: #6a9a6e; + } -聽 聽 聽 聽 /* 澶囩敤鏂规锛氶氳繃JavaScript娣诲姞鐨勭被 */ -聽 聽 聽 聽 .forum-message.query-engine { -聽 聽 聽 聽 聽 聽 background-color: #eaf1f8; -聽 聽 聽 聽 聽 聽 border-color: #608ab1; -聽 聽 聽 聽 } + /* 澶囩敤鏂规锛氶氳繃JavaScript娣诲姞鐨勭被 */ + .forum-message.query-engine { + background-color: #eaf1f8; + border-color: #608ab1; + } -聽 聽 聽 聽 .forum-message.insight-engine { -聽 聽 聽 聽 聽 聽 background-color: #f2ebf3; -聽 聽 聽 聽 聽 聽 border-color: #8e6a9f; -聽 聽 聽 聽 } + .forum-message.insight-engine { + background-color: #f2ebf3; + border-color: #8e6a9f; + } -聽 聽 聽 聽 .forum-message.media-engine { -聽 聽 聽 聽 聽 聽 background-color: #ebf2ea; -聽 聽 聽 聽 聽 聽 border-color: #6a9a6e; -聽 聽 聽 聽 } + .forum-message.media-engine { + background-color: #ebf2ea; + border-color: #6a9a6e; + } .forum-message.agent.QUERY { background-color: #eaf1f8; @@ -857,10 +864,10 @@ border-color: #608ab1; } -聽 聽 聽 聽 .forum-message.agent.MEDIA { -聽 聽 聽 聽 聽 聽 background-color: #ebf2ea; -聽 聽 聽 聽 聽 聽 border-color: #6a9a6e; -聽 聽 聽 聽 } + .forum-message.agent.MEDIA { + background-color: #ebf2ea; + border-color: #6a9a6e; + } .forum-message.agent.media-engine { background-color: #ebf2ea; @@ -2378,6 +2385,7 @@ // Report Engine 鐩稿叧鍑芥暟 let reportLogLineCount = 0; let reportLockCheckInterval = null; + let lastCompletedReportTask = null; // 瀹炴椂鍒锋柊璁哄潧娑堟伅锛堥傜敤浜庢墍鏈夐〉闈級 function refreshForumMessages() { @@ -2783,6 +2791,12 @@
姝e湪鍒濆鍖...
+ + +
+ + +
@@ -2796,19 +2810,146 @@ `; reportContent.innerHTML = interfaceHTML; + initializeReportControls(); // 绔嬪嵆鏇存柊鐘舵佷俊鎭 updateEngineStatusDisplay(statusData); // 濡傛灉鏈夊綋鍓嶄换鍔★紝鏄剧ず浠诲姟鐘舵 if (statusData.current_task) { - const taskArea = document.getElementById('taskProgressArea'); - if (taskArea) { - taskArea.innerHTML = renderTaskStatus(statusData.current_task); + updateTaskProgressStatus(statusData.current_task); + } else { + updateDownloadButtonState(null); + } + } + + function initializeReportControls() { + const generateButton = document.getElementById('generateReportButton'); + if (generateButton && !generateButton.dataset.bound) { + generateButton.dataset.bound = 'true'; + generateButton.addEventListener('click', () => { + if (reportTaskId) { + showMessage('宸叉湁鎶ュ憡鐢熸垚浠诲姟鍦ㄨ繍琛', 'info'); + return; + } + const reportButton = document.querySelector('[data-app="report"]'); + if (reportButton && reportButton.classList.contains('locked')) { + showMessage('闇绛夊緟涓変釜Agent瀹屾垚鏈鏂板垎鏋愬悗鎵嶈兘鐢熸垚鏈缁堟姤鍛', 'error'); + return; + } + generateReport(); + }); + } + + const downloadButton = document.getElementById('downloadReportButton'); + if (downloadButton && !downloadButton.dataset.bound) { + downloadButton.dataset.bound = 'true'; + downloadButton.addEventListener('click', () => downloadReport()); + } + + if (reportTaskId) { + setGenerateButtonState(true); + } else { + setGenerateButtonState(false); + } + + if (lastCompletedReportTask) { + updateDownloadButtonState(lastCompletedReportTask); + } + } + + function setGenerateButtonState(forceLoading = false) { + const generateButton = document.getElementById('generateReportButton'); + if (!generateButton) return; + + if (forceLoading || reportTaskId) { + if (!generateButton.dataset.originalText) { + generateButton.dataset.originalText = generateButton.textContent || '鐢熸垚鏈缁堟姤鍛'; + } + generateButton.disabled = true; + generateButton.textContent = '鐢熸垚涓...'; + } else { + const originalText = generateButton.dataset.originalText || '鐢熸垚鏈缁堟姤鍛'; + generateButton.disabled = false; + generateButton.textContent = originalText; + } + } + + function updateDownloadButtonState(task) { + const downloadButton = document.getElementById('downloadReportButton'); + if (!downloadButton) return; + + if (task && task.status === 'completed' && (task.report_file_ready || task.report_file_path)) { + downloadButton.disabled = false; + downloadButton.dataset.taskId = task.task_id; + downloadButton.dataset.filename = task.report_file_name || ''; + const label = task.report_file_name ? `涓嬭浇HTML (${task.report_file_name})` : '涓嬭浇HTML'; + downloadButton.textContent = label; + lastCompletedReportTask = task; + } else if (!lastCompletedReportTask || (task && task.status !== 'completed')) { + downloadButton.disabled = true; + downloadButton.dataset.taskId = ''; + downloadButton.dataset.filename = ''; + downloadButton.textContent = '涓嬭浇HTML'; + if (!reportTaskId) { + lastCompletedReportTask = null; } } } + function downloadReport(taskId = null) { + const downloadButton = document.getElementById('downloadReportButton'); + const targetTaskId = taskId || (downloadButton ? downloadButton.dataset.taskId : ''); + + if (!targetTaskId) { + showMessage('鏆傛棤鍙笅杞界殑鎶ュ憡锛岃鍏堢敓鎴愭渶缁堟姤鍛', 'error'); + return; + } + + let preferredFileName = ''; + if (downloadButton && downloadButton.dataset.filename) { + preferredFileName = downloadButton.dataset.filename; + } else if (lastCompletedReportTask && lastCompletedReportTask.task_id === targetTaskId) { + preferredFileName = lastCompletedReportTask.report_file_name || ''; + } + + fetch(`/api/report/download/${targetTaskId}`) + .then(response => { + if (!response.ok) { + const contentType = response.headers.get('Content-Type') || ''; + if (contentType.includes('application/json')) { + return response.json().then(err => { + throw new Error(err.error || '涓嬭浇澶辫触'); + }); + } + throw new Error('涓嬭浇澶辫触'); + } + const disposition = response.headers.get('Content-Disposition') || ''; + return response.blob().then(blob => ({ blob, disposition })); + }) + .then(({ blob, disposition }) => { + let filename = preferredFileName; + if (!filename) { + const match = disposition.match(/filename="?([^";]+)"?/i); + filename = match ? match[1] : `final_report_${targetTaskId}.html`; + } + + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename || 'final_report.html'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + showMessage('鎶ュ憡鏂囦欢宸插紑濮嬩笅杞', 'success'); + }) + .catch(error => { + console.error('涓嬭浇鎶ュ憡澶辫触:', error); + showMessage('涓嬭浇鎶ュ憡澶辫触: ' + error.message, 'error'); + }); + } + // 娓叉煋浠诲姟鐘舵侊紙浣跨敤鏂扮殑杩涘害鏉℃牱寮忥級 function renderTaskStatus(task) { // 鐘舵佹枃鏈殑涓枃鏄犲皠 @@ -2863,7 +3004,18 @@ `; - + + if (task.report_file_path) { + statusHTML += ` +
+
+ 淇濆瓨璺緞: + ${task.report_file_path} +
+
+ `; + } + if (task.error_message) { statusHTML += `
@@ -2871,13 +3023,33 @@
`; } - + + if (task.status === 'completed') { + statusHTML += ` +
+ + ${task.report_file_ready ? `` : ''} +
+ `; + } + statusHTML += ''; return statusHTML; } // 鐢熸垚鎶ュ憡 function generateReport() { + if (reportTaskId) { + showMessage('宸叉湁鎶ュ憡鐢熸垚浠诲姟鍦ㄨ繍琛', 'info'); + return; + } + + const reportButton = document.querySelector('[data-app="report"]'); + if (reportButton && reportButton.classList.contains('locked')) { + showMessage('闇绛夊緟涓変釜Agent瀹屾垚鏈鏂板垎鏋愬悗鎵嶈兘鐢熸垚鏈缁堟姤鍛', 'error'); + return; + } + const query = document.getElementById('searchInput').value.trim() || '鏅鸿兘鑸嗘儏鍒嗘瀽鎶ュ憡'; // 閲嶇疆鏃ュ織璁℃暟鍣紝鍥犱负鍚庡彴浼氭竻绌烘棩蹇楁枃浠 @@ -2887,8 +3059,8 @@ const consoleOutput = document.getElementById('consoleOutput'); consoleOutput.innerHTML = '
[绯荤粺] 寮濮嬬敓鎴愭姤鍛婏紝鏃ュ織宸查噸缃
'; - // 鎸夐挳宸茬Щ闄わ紝鏃犻渶鎿嶄綔鎸夐挳鐘舵 - + setGenerateButtonState(true); + // 鍦ㄧ幇鏈夌姸鎬佷俊鎭悗娣诲姞浠诲姟杩涘害鐘舵侊紝鑰屼笉鏄浛鎹 addTaskProgressStatus('姝e湪鍚姩鎶ュ憡鐢熸垚浠诲姟...', 'loading'); @@ -2934,6 +3106,7 @@ // 閲嶇疆鏍囧織鍏佽閲嶆柊灏濊瘯 autoGenerateTriggered = false; reportTaskId = null; + setGenerateButtonState(false); } }) .catch(error => { @@ -2942,6 +3115,7 @@ // 閲嶇疆鏍囧織鍏佽閲嶆柊灏濊瘯 autoGenerateTriggered = false; reportTaskId = null; + setGenerateButtonState(false); }); } @@ -2977,6 +3151,7 @@ // 閲嶇疆鑷姩鐢熸垚鏍囧織锛屽厑璁镐笅娆℃湁鏂板唴瀹规椂鑷姩鐢熸垚 autoGenerateTriggered = false; reportTaskId = null; + setGenerateButtonState(false); } else if (data.task.status === 'error') { clearInterval(reportPollingInterval); showMessage('鎶ュ憡鐢熸垚澶辫触: ' + data.task.error_message, 'error'); @@ -2984,6 +3159,7 @@ // 閲嶇疆鑷姩鐢熸垚鏍囧織锛屽厑璁搁噸鏂板皾璇 autoGenerateTriggered = false; reportTaskId = null; + setGenerateButtonState(false); } } }) @@ -3020,11 +3196,17 @@ if (task) { taskArea.innerHTML = renderTaskStatus(task); + if (task.status === 'completed') { + lastCompletedReportTask = task; + } else if (task.status === 'running') { + lastCompletedReportTask = null; + } + updateDownloadButtonState(task); } else if (status && errorMessage) { const loadingIndicator = status === 'loading' ? '' : ''; const statusBadgeClass = status === 'error' ? 'task-status-error' : 'task-status-running'; const statusText = status === 'error' ? '閿欒' : '澶勭悊涓'; - + taskArea.innerHTML = `
@@ -3253,4 +3435,4 @@ } - + \ No newline at end of file