Showing
3 changed files
with
903 additions
and
0 deletions
app.py
0 → 100644
| 1 | +""" | ||
| 2 | +Flask主应用 - 统一管理三个Streamlit应用 | ||
| 3 | +""" | ||
| 4 | + | ||
| 5 | +import os | ||
| 6 | +import sys | ||
| 7 | +import subprocess | ||
| 8 | +import time | ||
| 9 | +import json | ||
| 10 | +import threading | ||
| 11 | +from datetime import datetime | ||
| 12 | +from queue import Queue, Empty | ||
| 13 | +from flask import Flask, render_template, request, jsonify, Response | ||
| 14 | +from flask_socketio import SocketIO, emit | ||
| 15 | +import signal | ||
| 16 | +import atexit | ||
| 17 | +import requests | ||
| 18 | + | ||
| 19 | +app = Flask(__name__) | ||
| 20 | +app.config['SECRET_KEY'] = 'weibo_analysis_system_2024' | ||
| 21 | +socketio = SocketIO(app, cors_allowed_origins="*") | ||
| 22 | + | ||
| 23 | +# 全局变量存储进程信息 | ||
| 24 | +processes = { | ||
| 25 | + 'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': []}, | ||
| 26 | + 'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': []}, | ||
| 27 | + 'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': []} | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +# 输出队列 | ||
| 31 | +output_queues = { | ||
| 32 | + 'insight': Queue(), | ||
| 33 | + 'media': Queue(), | ||
| 34 | + 'query': Queue() | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +def read_process_output(process, app_name): | ||
| 38 | + """读取进程输出并放入队列""" | ||
| 39 | + while True: | ||
| 40 | + try: | ||
| 41 | + if process.poll() is not None: | ||
| 42 | + break | ||
| 43 | + | ||
| 44 | + output = process.stdout.readline() | ||
| 45 | + if output: | ||
| 46 | + line = output.decode('utf-8', errors='ignore').strip() | ||
| 47 | + if line: | ||
| 48 | + timestamp = datetime.now().strftime('%H:%M:%S') | ||
| 49 | + formatted_line = f"[{timestamp}] {line}" | ||
| 50 | + | ||
| 51 | + # 添加到输出列表(保持最近100行) | ||
| 52 | + processes[app_name]['output'].append(formatted_line) | ||
| 53 | + if len(processes[app_name]['output']) > 100: | ||
| 54 | + processes[app_name]['output'].pop(0) | ||
| 55 | + | ||
| 56 | + # 发送到前端 | ||
| 57 | + socketio.emit('console_output', { | ||
| 58 | + 'app': app_name, | ||
| 59 | + 'line': formatted_line | ||
| 60 | + }) | ||
| 61 | + except Exception as e: | ||
| 62 | + print(f"Error reading output for {app_name}: {e}") | ||
| 63 | + break | ||
| 64 | + | ||
| 65 | +def start_streamlit_app(app_name, script_path, port): | ||
| 66 | + """启动Streamlit应用""" | ||
| 67 | + try: | ||
| 68 | + if processes[app_name]['process'] is not None: | ||
| 69 | + return False, "应用已经在运行" | ||
| 70 | + | ||
| 71 | + # 检查文件是否存在 | ||
| 72 | + if not os.path.exists(script_path): | ||
| 73 | + return False, f"文件不存在: {script_path}" | ||
| 74 | + | ||
| 75 | + cmd = [ | ||
| 76 | + sys.executable, '-m', 'streamlit', 'run', | ||
| 77 | + script_path, | ||
| 78 | + '--server.port', str(port), | ||
| 79 | + '--server.headless', 'true', | ||
| 80 | + '--browser.gatherUsageStats', 'false', | ||
| 81 | + '--logger.level', 'info' | ||
| 82 | + ] | ||
| 83 | + | ||
| 84 | + # 使用当前工作目录而不是脚本目录 | ||
| 85 | + process = subprocess.Popen( | ||
| 86 | + cmd, | ||
| 87 | + stdout=subprocess.PIPE, | ||
| 88 | + stderr=subprocess.STDOUT, | ||
| 89 | + bufsize=1, | ||
| 90 | + universal_newlines=False, | ||
| 91 | + cwd=os.getcwd() | ||
| 92 | + ) | ||
| 93 | + | ||
| 94 | + processes[app_name]['process'] = process | ||
| 95 | + processes[app_name]['status'] = 'starting' | ||
| 96 | + processes[app_name]['output'] = [] | ||
| 97 | + | ||
| 98 | + # 启动输出读取线程 | ||
| 99 | + output_thread = threading.Thread( | ||
| 100 | + target=read_process_output, | ||
| 101 | + args=(process, app_name), | ||
| 102 | + daemon=True | ||
| 103 | + ) | ||
| 104 | + output_thread.start() | ||
| 105 | + | ||
| 106 | + return True, f"{app_name} 应用启动中..." | ||
| 107 | + | ||
| 108 | + except Exception as e: | ||
| 109 | + return False, f"启动失败: {str(e)}" | ||
| 110 | + | ||
| 111 | +def stop_streamlit_app(app_name): | ||
| 112 | + """停止Streamlit应用""" | ||
| 113 | + try: | ||
| 114 | + if processes[app_name]['process'] is None: | ||
| 115 | + return False, "应用未运行" | ||
| 116 | + | ||
| 117 | + process = processes[app_name]['process'] | ||
| 118 | + process.terminate() | ||
| 119 | + | ||
| 120 | + # 等待进程结束 | ||
| 121 | + try: | ||
| 122 | + process.wait(timeout=5) | ||
| 123 | + except subprocess.TimeoutExpired: | ||
| 124 | + process.kill() | ||
| 125 | + process.wait() | ||
| 126 | + | ||
| 127 | + processes[app_name]['process'] = None | ||
| 128 | + processes[app_name]['status'] = 'stopped' | ||
| 129 | + | ||
| 130 | + return True, f"{app_name} 应用已停止" | ||
| 131 | + | ||
| 132 | + except Exception as e: | ||
| 133 | + return False, f"停止失败: {str(e)}" | ||
| 134 | + | ||
| 135 | +def check_app_status(): | ||
| 136 | + """检查应用状态""" | ||
| 137 | + for app_name, info in processes.items(): | ||
| 138 | + if info['process'] is not None: | ||
| 139 | + if info['process'].poll() is None: | ||
| 140 | + # 进程仍在运行,检查端口是否可访问 | ||
| 141 | + try: | ||
| 142 | + response = requests.get(f"http://localhost:{info['port']}", timeout=2) | ||
| 143 | + if response.status_code == 200: | ||
| 144 | + info['status'] = 'running' | ||
| 145 | + else: | ||
| 146 | + info['status'] = 'starting' | ||
| 147 | + except requests.exceptions.RequestException: | ||
| 148 | + info['status'] = 'starting' | ||
| 149 | + except Exception: | ||
| 150 | + info['status'] = 'starting' | ||
| 151 | + else: | ||
| 152 | + # 进程已结束 | ||
| 153 | + info['process'] = None | ||
| 154 | + info['status'] = 'stopped' | ||
| 155 | + | ||
| 156 | +def wait_for_app_startup(app_name, max_wait_time=30): | ||
| 157 | + """等待应用启动完成""" | ||
| 158 | + import time | ||
| 159 | + start_time = time.time() | ||
| 160 | + while time.time() - start_time < max_wait_time: | ||
| 161 | + info = processes[app_name] | ||
| 162 | + if info['process'] is None: | ||
| 163 | + return False, "进程已停止" | ||
| 164 | + | ||
| 165 | + if info['process'].poll() is not None: | ||
| 166 | + return False, "进程启动失败" | ||
| 167 | + | ||
| 168 | + try: | ||
| 169 | + response = requests.get(f"http://localhost:{info['port']}", timeout=2) | ||
| 170 | + if response.status_code == 200: | ||
| 171 | + info['status'] = 'running' | ||
| 172 | + return True, "启动成功" | ||
| 173 | + except: | ||
| 174 | + pass | ||
| 175 | + | ||
| 176 | + time.sleep(1) | ||
| 177 | + | ||
| 178 | + return False, "启动超时" | ||
| 179 | + | ||
| 180 | +def cleanup_processes(): | ||
| 181 | + """清理所有进程""" | ||
| 182 | + for app_name in processes: | ||
| 183 | + stop_streamlit_app(app_name) | ||
| 184 | + | ||
| 185 | +# 注册清理函数 | ||
| 186 | +atexit.register(cleanup_processes) | ||
| 187 | + | ||
| 188 | +@app.route('/') | ||
| 189 | +def index(): | ||
| 190 | + """主页""" | ||
| 191 | + return render_template('index.html') | ||
| 192 | + | ||
| 193 | +@app.route('/api/status') | ||
| 194 | +def get_status(): | ||
| 195 | + """获取所有应用状态""" | ||
| 196 | + check_app_status() | ||
| 197 | + return jsonify({ | ||
| 198 | + app_name: { | ||
| 199 | + 'status': info['status'], | ||
| 200 | + 'port': info['port'], | ||
| 201 | + 'output_lines': len(info['output']) | ||
| 202 | + } | ||
| 203 | + for app_name, info in processes.items() | ||
| 204 | + }) | ||
| 205 | + | ||
| 206 | +@app.route('/api/start/<app_name>') | ||
| 207 | +def start_app(app_name): | ||
| 208 | + """启动指定应用""" | ||
| 209 | + if app_name not in processes: | ||
| 210 | + return jsonify({'success': False, 'message': '未知应用'}) | ||
| 211 | + | ||
| 212 | + script_paths = { | ||
| 213 | + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py', | ||
| 214 | + 'media': 'SingleEngineApp/media_engine_streamlit_app.py', | ||
| 215 | + 'query': 'SingleEngineApp/query_engine_streamlit_app.py' | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + success, message = start_streamlit_app( | ||
| 219 | + app_name, | ||
| 220 | + script_paths[app_name], | ||
| 221 | + processes[app_name]['port'] | ||
| 222 | + ) | ||
| 223 | + | ||
| 224 | + if success: | ||
| 225 | + # 等待应用启动 | ||
| 226 | + startup_success, startup_message = wait_for_app_startup(app_name, 15) | ||
| 227 | + if not startup_success: | ||
| 228 | + message += f" 但启动检查失败: {startup_message}" | ||
| 229 | + | ||
| 230 | + return jsonify({'success': success, 'message': message}) | ||
| 231 | + | ||
| 232 | +@app.route('/api/stop/<app_name>') | ||
| 233 | +def stop_app(app_name): | ||
| 234 | + """停止指定应用""" | ||
| 235 | + if app_name not in processes: | ||
| 236 | + return jsonify({'success': False, 'message': '未知应用'}) | ||
| 237 | + | ||
| 238 | + success, message = stop_streamlit_app(app_name) | ||
| 239 | + return jsonify({'success': success, 'message': message}) | ||
| 240 | + | ||
| 241 | +@app.route('/api/output/<app_name>') | ||
| 242 | +def get_output(app_name): | ||
| 243 | + """获取应用输出""" | ||
| 244 | + if app_name not in processes: | ||
| 245 | + return jsonify({'success': False, 'message': '未知应用'}) | ||
| 246 | + | ||
| 247 | + return jsonify({ | ||
| 248 | + 'success': True, | ||
| 249 | + 'output': processes[app_name]['output'] | ||
| 250 | + }) | ||
| 251 | + | ||
| 252 | +@app.route('/api/search', methods=['POST']) | ||
| 253 | +def search(): | ||
| 254 | + """统一搜索接口""" | ||
| 255 | + data = request.get_json() | ||
| 256 | + query = data.get('query', '').strip() | ||
| 257 | + | ||
| 258 | + if not query: | ||
| 259 | + return jsonify({'success': False, 'message': '搜索查询不能为空'}) | ||
| 260 | + | ||
| 261 | + # 检查哪些应用正在运行 | ||
| 262 | + check_app_status() | ||
| 263 | + running_apps = [name for name, info in processes.items() if info['status'] == 'running'] | ||
| 264 | + | ||
| 265 | + if not running_apps: | ||
| 266 | + return jsonify({'success': False, 'message': '没有运行中的应用'}) | ||
| 267 | + | ||
| 268 | + # 向运行中的应用发送搜索请求 | ||
| 269 | + results = {} | ||
| 270 | + api_ports = {'insight': 8601, 'media': 8602, 'query': 8603} | ||
| 271 | + | ||
| 272 | + for app_name in running_apps: | ||
| 273 | + try: | ||
| 274 | + api_port = api_ports[app_name] | ||
| 275 | + # 调用Streamlit应用的API端点 | ||
| 276 | + response = requests.post( | ||
| 277 | + f"http://localhost:{api_port}/api/search", | ||
| 278 | + json={'query': query}, | ||
| 279 | + timeout=10 | ||
| 280 | + ) | ||
| 281 | + if response.status_code == 200: | ||
| 282 | + results[app_name] = response.json() | ||
| 283 | + else: | ||
| 284 | + results[app_name] = {'success': False, 'message': 'API调用失败'} | ||
| 285 | + except Exception as e: | ||
| 286 | + results[app_name] = {'success': False, 'message': str(e)} | ||
| 287 | + | ||
| 288 | + return jsonify({ | ||
| 289 | + 'success': True, | ||
| 290 | + 'query': query, | ||
| 291 | + 'results': results | ||
| 292 | + }) | ||
| 293 | + | ||
| 294 | +@socketio.on('connect') | ||
| 295 | +def handle_connect(): | ||
| 296 | + """客户端连接""" | ||
| 297 | + emit('status', 'Connected to Flask server') | ||
| 298 | + | ||
| 299 | +@socketio.on('request_status') | ||
| 300 | +def handle_status_request(): | ||
| 301 | + """请求状态更新""" | ||
| 302 | + check_app_status() | ||
| 303 | + emit('status_update', { | ||
| 304 | + app_name: { | ||
| 305 | + 'status': info['status'], | ||
| 306 | + 'port': info['port'] | ||
| 307 | + } | ||
| 308 | + for app_name, info in processes.items() | ||
| 309 | + }) | ||
| 310 | + | ||
| 311 | +if __name__ == '__main__': | ||
| 312 | + # 启动时自动启动所有Streamlit应用 | ||
| 313 | + print("正在启动Streamlit应用...") | ||
| 314 | + | ||
| 315 | + script_paths = { | ||
| 316 | + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py', | ||
| 317 | + 'media': 'SingleEngineApp/media_engine_streamlit_app.py', | ||
| 318 | + 'query': 'SingleEngineApp/query_engine_streamlit_app.py' | ||
| 319 | + } | ||
| 320 | + | ||
| 321 | + for app_name, script_path in script_paths.items(): | ||
| 322 | + print(f"检查文件: {script_path}") | ||
| 323 | + if os.path.exists(script_path): | ||
| 324 | + print(f"启动 {app_name}...") | ||
| 325 | + success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port']) | ||
| 326 | + print(f"{app_name}: {message}") | ||
| 327 | + | ||
| 328 | + if success: | ||
| 329 | + print(f"等待 {app_name} 启动完成...") | ||
| 330 | + startup_success, startup_message = wait_for_app_startup(app_name, 30) | ||
| 331 | + print(f"{app_name} 启动检查: {startup_message}") | ||
| 332 | + else: | ||
| 333 | + print(f"错误: {script_path} 不存在") | ||
| 334 | + | ||
| 335 | + print("所有应用启动完成,启动Flask服务器...") | ||
| 336 | + | ||
| 337 | + try: | ||
| 338 | + # 启动Flask应用 | ||
| 339 | + socketio.run(app, host='0.0.0.0', port=5000, debug=False) | ||
| 340 | + except KeyboardInterrupt: | ||
| 341 | + print("\n正在关闭应用...") | ||
| 342 | + cleanup_processes() |
| @@ -30,3 +30,11 @@ uuid>=1.30 | @@ -30,3 +30,11 @@ uuid>=1.30 | ||
| 30 | pytest>=7.4.0 | 30 | pytest>=7.4.0 |
| 31 | black>=23.0.0 | 31 | black>=23.0.0 |
| 32 | flake8>=6.0.0 | 32 | flake8>=6.0.0 |
| 33 | + | ||
| 34 | +# Flask Web应用 | ||
| 35 | +flask==2.3.3 | ||
| 36 | +flask-socketio==5.3.6 | ||
| 37 | +streamlit==1.28.1 | ||
| 38 | +requests==2.31.0 | ||
| 39 | +python-socketio==5.8.0 | ||
| 40 | +eventlet==0.33.3 |
templates/index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | +<head> | ||
| 4 | + <meta charset="UTF-8"> | ||
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | + <title>微博舆情预测系统</title> | ||
| 7 | + <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script> | ||
| 8 | + <style> | ||
| 9 | + * { | ||
| 10 | + margin: 0; | ||
| 11 | + padding: 0; | ||
| 12 | + box-sizing: border-box; | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + body { | ||
| 16 | + font-family: 'Arial', sans-serif; | ||
| 17 | + background-color: #ffffff; | ||
| 18 | + color: #000000; | ||
| 19 | + line-height: 1.6; | ||
| 20 | + overflow-x: hidden; | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + .container { | ||
| 24 | + max-width: 100vw; | ||
| 25 | + min-height: 100vh; | ||
| 26 | + display: flex; | ||
| 27 | + flex-direction: column; | ||
| 28 | + border: 2px solid #000000; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + /* 搜索框区域 */ | ||
| 32 | + .search-section { | ||
| 33 | + border-bottom: 2px solid #000000; | ||
| 34 | + padding: 20px; | ||
| 35 | + background-color: #ffffff; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + .search-title { | ||
| 39 | + font-size: 24px; | ||
| 40 | + font-weight: bold; | ||
| 41 | + text-align: center; | ||
| 42 | + margin-bottom: 20px; | ||
| 43 | + letter-spacing: 1px; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + .search-box { | ||
| 47 | + display: flex; | ||
| 48 | + max-width: 800px; | ||
| 49 | + margin: 0 auto; | ||
| 50 | + border: 2px solid #000000; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + .search-input { | ||
| 54 | + flex: 1; | ||
| 55 | + padding: 15px; | ||
| 56 | + border: none; | ||
| 57 | + outline: none; | ||
| 58 | + font-size: 16px; | ||
| 59 | + background-color: #ffffff; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + .search-button { | ||
| 63 | + padding: 15px 30px; | ||
| 64 | + border: none; | ||
| 65 | + border-left: 2px solid #000000; | ||
| 66 | + background-color: #000000; | ||
| 67 | + color: #ffffff; | ||
| 68 | + cursor: pointer; | ||
| 69 | + font-size: 16px; | ||
| 70 | + font-weight: bold; | ||
| 71 | + transition: all 0.3s ease; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + .search-button:hover { | ||
| 75 | + background-color: #333333; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + .search-button:disabled { | ||
| 79 | + background-color: #666666; | ||
| 80 | + cursor: not-allowed; | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + /* 主内容区域 */ | ||
| 84 | + .main-content { | ||
| 85 | + flex: 1; | ||
| 86 | + display: flex; | ||
| 87 | + height: calc(100vh - 140px); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + /* 嵌入页面区域 */ | ||
| 91 | + .embedded-section { | ||
| 92 | + flex: 2; | ||
| 93 | + border-right: 2px solid #000000; | ||
| 94 | + background-color: #ffffff; | ||
| 95 | + position: relative; | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + .embedded-header { | ||
| 99 | + padding: 15px; | ||
| 100 | + border-bottom: 2px solid #000000; | ||
| 101 | + background-color: #ffffff; | ||
| 102 | + font-weight: bold; | ||
| 103 | + text-align: center; | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + .embedded-content { | ||
| 107 | + height: calc(100% - 60px); | ||
| 108 | + position: relative; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /* 控制台输出区域 */ | ||
| 112 | + .console-section { | ||
| 113 | + flex: 1; | ||
| 114 | + display: flex; | ||
| 115 | + flex-direction: column; | ||
| 116 | + background-color: #ffffff; | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /* 应用切换按钮 */ | ||
| 120 | + .app-switcher { | ||
| 121 | + display: flex; | ||
| 122 | + border-bottom: 2px solid #000000; | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + .app-button { | ||
| 126 | + flex: 1; | ||
| 127 | + padding: 15px; | ||
| 128 | + border: none; | ||
| 129 | + border-right: 2px solid #000000; | ||
| 130 | + background-color: #ffffff; | ||
| 131 | + cursor: pointer; | ||
| 132 | + font-size: 14px; | ||
| 133 | + font-weight: bold; | ||
| 134 | + transition: all 0.3s ease; | ||
| 135 | + position: relative; | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + .app-button:last-child { | ||
| 139 | + border-right: none; | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + .app-button.active { | ||
| 143 | + background-color: #000000; | ||
| 144 | + color: #ffffff; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + .app-button:not(.active):hover { | ||
| 148 | + background-color: #f0f0f0; | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + .status-indicator { | ||
| 152 | + position: absolute; | ||
| 153 | + top: 5px; | ||
| 154 | + right: 5px; | ||
| 155 | + width: 8px; | ||
| 156 | + height: 8px; | ||
| 157 | + border-radius: 50%; | ||
| 158 | + background-color: #ff0000; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + .status-indicator.running { | ||
| 162 | + background-color: #00ff00; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + .status-indicator.starting { | ||
| 166 | + background-color: #ffff00; | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + /* 控制台输出 */ | ||
| 170 | + .console-output { | ||
| 171 | + flex: 1; | ||
| 172 | + padding: 15px; | ||
| 173 | + background-color: #000000; | ||
| 174 | + color: #00ff00; | ||
| 175 | + font-family: 'Courier New', monospace; | ||
| 176 | + font-size: 12px; | ||
| 177 | + overflow-y: auto; | ||
| 178 | + white-space: pre-wrap; | ||
| 179 | + word-break: break-all; | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + .console-line { | ||
| 183 | + margin-bottom: 2px; | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + /* 状态信息 */ | ||
| 187 | + .status-bar { | ||
| 188 | + padding: 10px 20px; | ||
| 189 | + border-top: 2px solid #000000; | ||
| 190 | + background-color: #ffffff; | ||
| 191 | + font-size: 12px; | ||
| 192 | + display: flex; | ||
| 193 | + justify-content: space-between; | ||
| 194 | + align-items: center; | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + .loading { | ||
| 198 | + display: inline-block; | ||
| 199 | + width: 12px; | ||
| 200 | + height: 12px; | ||
| 201 | + border: 2px solid #000000; | ||
| 202 | + border-radius: 50%; | ||
| 203 | + border-top-color: transparent; | ||
| 204 | + animation: spin 1s ease-in-out infinite; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + @keyframes spin { | ||
| 208 | + to { transform: rotate(360deg); } | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + /* 响应式设计 */ | ||
| 212 | + @media (max-width: 768px) { | ||
| 213 | + .main-content { | ||
| 214 | + flex-direction: column; | ||
| 215 | + height: auto; | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + .embedded-section { | ||
| 219 | + border-right: none; | ||
| 220 | + border-bottom: 2px solid #000000; | ||
| 221 | + height: 400px; | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + .console-section { | ||
| 225 | + height: 300px; | ||
| 226 | + } | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + /* 消息提示 */ | ||
| 230 | + .message { | ||
| 231 | + position: fixed; | ||
| 232 | + top: 20px; | ||
| 233 | + right: 20px; | ||
| 234 | + padding: 10px 20px; | ||
| 235 | + border: 2px solid #000000; | ||
| 236 | + background-color: #ffffff; | ||
| 237 | + z-index: 1000; | ||
| 238 | + opacity: 0; | ||
| 239 | + transform: translateX(100%); | ||
| 240 | + transition: all 0.3s ease; | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + .message.show { | ||
| 244 | + opacity: 1; | ||
| 245 | + transform: translateX(0); | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + .message.error { | ||
| 249 | + background-color: #ffeeee; | ||
| 250 | + border-color: #ff0000; | ||
| 251 | + } | ||
| 252 | + | ||
| 253 | + .message.success { | ||
| 254 | + background-color: #eeffee; | ||
| 255 | + border-color: #00ff00; | ||
| 256 | + } | ||
| 257 | + </style> | ||
| 258 | +</head> | ||
| 259 | +<body> | ||
| 260 | + <div class="container"> | ||
| 261 | + <!-- 搜索框区域 --> | ||
| 262 | + <div class="search-section"> | ||
| 263 | + <div class="search-title">搜索框</div> | ||
| 264 | + <div class="search-box"> | ||
| 265 | + <input type="text" class="search-input" id="searchInput" placeholder="请输入搜索内容..."> | ||
| 266 | + <button class="search-button" id="searchButton">搜索</button> | ||
| 267 | + </div> | ||
| 268 | + </div> | ||
| 269 | + | ||
| 270 | + <!-- 主内容区域 --> | ||
| 271 | + <div class="main-content"> | ||
| 272 | + <!-- 嵌入页面区域 --> | ||
| 273 | + <div class="embedded-section"> | ||
| 274 | + <div class="embedded-header" id="embeddedHeader">嵌入的页面</div> | ||
| 275 | + <div class="embedded-content" id="embeddedContent"> | ||
| 276 | + <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;"> | ||
| 277 | + <span>只显示一个页面 - 点击按钮切换页面</span> | ||
| 278 | + </div> | ||
| 279 | + </div> | ||
| 280 | + </div> | ||
| 281 | + | ||
| 282 | + <!-- 控制台输出区域 --> | ||
| 283 | + <div class="console-section"> | ||
| 284 | + <!-- 应用切换按钮 --> | ||
| 285 | + <div class="app-switcher"> | ||
| 286 | + <button class="app-button active" data-app="insight"> | ||
| 287 | + <span class="status-indicator" id="status-insight"></span> | ||
| 288 | + Insight Engine | ||
| 289 | + </button> | ||
| 290 | + <button class="app-button" data-app="media"> | ||
| 291 | + <span class="status-indicator" id="status-media"></span> | ||
| 292 | + Media Engine | ||
| 293 | + </button> | ||
| 294 | + <button class="app-button" data-app="query"> | ||
| 295 | + <span class="status-indicator" id="status-query"></span> | ||
| 296 | + Query Engine | ||
| 297 | + </button> | ||
| 298 | + </div> | ||
| 299 | + | ||
| 300 | + <!-- 控制台输出 --> | ||
| 301 | + <div class="console-output" id="consoleOutput"> | ||
| 302 | + <div class="console-line">[系统] 等待连接...</div> | ||
| 303 | + </div> | ||
| 304 | + </div> | ||
| 305 | + </div> | ||
| 306 | + | ||
| 307 | + <!-- 状态栏 --> | ||
| 308 | + <div class="status-bar"> | ||
| 309 | + <span id="connectionStatus">连接中...</span> | ||
| 310 | + <span id="systemTime"></span> | ||
| 311 | + </div> | ||
| 312 | + </div> | ||
| 313 | + | ||
| 314 | + <!-- 消息提示 --> | ||
| 315 | + <div class="message" id="message"></div> | ||
| 316 | + | ||
| 317 | + <script> | ||
| 318 | + // 全局变量 | ||
| 319 | + let socket; | ||
| 320 | + let currentApp = 'insight'; | ||
| 321 | + let appStatus = { | ||
| 322 | + insight: 'stopped', | ||
| 323 | + media: 'stopped', | ||
| 324 | + query: 'stopped' | ||
| 325 | + }; | ||
| 326 | + | ||
| 327 | + // 初始化 | ||
| 328 | + document.addEventListener('DOMContentLoaded', function() { | ||
| 329 | + initializeSocket(); | ||
| 330 | + initializeEventListeners(); | ||
| 331 | + updateTime(); | ||
| 332 | + setInterval(updateTime, 1000); | ||
| 333 | + checkStatus(); | ||
| 334 | + setInterval(checkStatus, 5000); | ||
| 335 | + }); | ||
| 336 | + | ||
| 337 | + // Socket.IO连接 | ||
| 338 | + function initializeSocket() { | ||
| 339 | + socket = io(); | ||
| 340 | + | ||
| 341 | + socket.on('connect', function() { | ||
| 342 | + updateConnectionStatus('已连接'); | ||
| 343 | + socket.emit('request_status'); | ||
| 344 | + }); | ||
| 345 | + | ||
| 346 | + socket.on('disconnect', function() { | ||
| 347 | + updateConnectionStatus('连接断开'); | ||
| 348 | + }); | ||
| 349 | + | ||
| 350 | + socket.on('console_output', function(data) { | ||
| 351 | + if (data.app === currentApp) { | ||
| 352 | + addConsoleOutput(data.line); | ||
| 353 | + } | ||
| 354 | + }); | ||
| 355 | + | ||
| 356 | + socket.on('status_update', function(data) { | ||
| 357 | + updateAppStatus(data); | ||
| 358 | + }); | ||
| 359 | + } | ||
| 360 | + | ||
| 361 | + // 事件监听器 | ||
| 362 | + function initializeEventListeners() { | ||
| 363 | + // 搜索按钮 | ||
| 364 | + document.getElementById('searchButton').addEventListener('click', performSearch); | ||
| 365 | + document.getElementById('searchInput').addEventListener('keypress', function(e) { | ||
| 366 | + if (e.key === 'Enter') { | ||
| 367 | + performSearch(); | ||
| 368 | + } | ||
| 369 | + }); | ||
| 370 | + | ||
| 371 | + // 应用切换按钮 | ||
| 372 | + document.querySelectorAll('.app-button').forEach(button => { | ||
| 373 | + button.addEventListener('click', function() { | ||
| 374 | + const app = this.dataset.app; | ||
| 375 | + switchToApp(app); | ||
| 376 | + }); | ||
| 377 | + }); | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + // 执行搜索 | ||
| 381 | + function performSearch() { | ||
| 382 | + const query = document.getElementById('searchInput').value.trim(); | ||
| 383 | + if (!query) { | ||
| 384 | + showMessage('请输入搜索内容', 'error'); | ||
| 385 | + return; | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + const button = document.getElementById('searchButton'); | ||
| 389 | + button.disabled = true; | ||
| 390 | + button.innerHTML = '<span class="loading"></span> 搜索中...'; | ||
| 391 | + | ||
| 392 | + fetch('/api/search', { | ||
| 393 | + method: 'POST', | ||
| 394 | + headers: { | ||
| 395 | + 'Content-Type': 'application/json', | ||
| 396 | + }, | ||
| 397 | + body: JSON.stringify({ query: query }) | ||
| 398 | + }) | ||
| 399 | + .then(response => response.json()) | ||
| 400 | + .then(data => { | ||
| 401 | + if (data.success) { | ||
| 402 | + showMessage('搜索请求已发送到所有运行中的应用', 'success'); | ||
| 403 | + } else { | ||
| 404 | + showMessage(data.message || '搜索失败', 'error'); | ||
| 405 | + } | ||
| 406 | + }) | ||
| 407 | + .catch(error => { | ||
| 408 | + console.error('搜索错误:', error); | ||
| 409 | + showMessage('搜索请求失败', 'error'); | ||
| 410 | + }) | ||
| 411 | + .finally(() => { | ||
| 412 | + button.disabled = false; | ||
| 413 | + button.innerHTML = '搜索'; | ||
| 414 | + }); | ||
| 415 | + } | ||
| 416 | + | ||
| 417 | + // 切换应用 | ||
| 418 | + function switchToApp(app) { | ||
| 419 | + if (app === currentApp) return; | ||
| 420 | + | ||
| 421 | + // 更新按钮状态 | ||
| 422 | + document.querySelectorAll('.app-button').forEach(btn => { | ||
| 423 | + btn.classList.remove('active'); | ||
| 424 | + }); | ||
| 425 | + document.querySelector(`[data-app="${app}"]`).classList.add('active'); | ||
| 426 | + | ||
| 427 | + currentApp = app; | ||
| 428 | + | ||
| 429 | + // 清空并加载新的控制台输出 | ||
| 430 | + document.getElementById('consoleOutput').innerHTML = '<div class="console-line">[系统] 切换到 ' + app + ' 应用</div>'; | ||
| 431 | + loadConsoleOutput(app); | ||
| 432 | + | ||
| 433 | + // 更新嵌入页面 | ||
| 434 | + updateEmbeddedPage(app); | ||
| 435 | + } | ||
| 436 | + | ||
| 437 | + // 加载控制台输出 | ||
| 438 | + function loadConsoleOutput(app) { | ||
| 439 | + fetch(`/api/output/${app}`) | ||
| 440 | + .then(response => response.json()) | ||
| 441 | + .then(data => { | ||
| 442 | + if (data.success && data.output.length > 0) { | ||
| 443 | + const consoleOutput = document.getElementById('consoleOutput'); | ||
| 444 | + data.output.forEach(line => { | ||
| 445 | + const div = document.createElement('div'); | ||
| 446 | + div.className = 'console-line'; | ||
| 447 | + div.textContent = line; | ||
| 448 | + consoleOutput.appendChild(div); | ||
| 449 | + }); | ||
| 450 | + consoleOutput.scrollTop = consoleOutput.scrollHeight; | ||
| 451 | + } | ||
| 452 | + }) | ||
| 453 | + .catch(error => { | ||
| 454 | + console.error('加载输出失败:', error); | ||
| 455 | + }); | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + // 添加控制台输出 | ||
| 459 | + function addConsoleOutput(line) { | ||
| 460 | + const consoleOutput = document.getElementById('consoleOutput'); | ||
| 461 | + const div = document.createElement('div'); | ||
| 462 | + div.className = 'console-line'; | ||
| 463 | + div.textContent = line; | ||
| 464 | + consoleOutput.appendChild(div); | ||
| 465 | + | ||
| 466 | + // 保持最近100行 | ||
| 467 | + const lines = consoleOutput.children; | ||
| 468 | + if (lines.length > 100) { | ||
| 469 | + consoleOutput.removeChild(lines[0]); | ||
| 470 | + } | ||
| 471 | + | ||
| 472 | + consoleOutput.scrollTop = consoleOutput.scrollHeight; | ||
| 473 | + } | ||
| 474 | + | ||
| 475 | + // 更新嵌入页面 | ||
| 476 | + function updateEmbeddedPage(app) { | ||
| 477 | + const header = document.getElementById('embeddedHeader'); | ||
| 478 | + const content = document.getElementById('embeddedContent'); | ||
| 479 | + | ||
| 480 | + const appNames = { | ||
| 481 | + insight: 'Insight Engine - 私有数据库分析', | ||
| 482 | + media: 'Media Engine - 多模态能力', | ||
| 483 | + query: 'Query Engine - 网页搜索' | ||
| 484 | + }; | ||
| 485 | + | ||
| 486 | + header.textContent = appNames[app] || app; | ||
| 487 | + | ||
| 488 | + // 如果应用正在运行,显示iframe | ||
| 489 | + if (appStatus[app] === 'running') { | ||
| 490 | + const ports = { insight: 8501, media: 8502, query: 8503 }; | ||
| 491 | + content.innerHTML = `<iframe src="http://localhost:${ports[app]}" style="width: 100%; height: 100%; border: none;"></iframe>`; | ||
| 492 | + } else { | ||
| 493 | + content.innerHTML = ` | ||
| 494 | + <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column;"> | ||
| 495 | + <div style="margin-bottom: 10px;">${appNames[app]} 未运行</div> | ||
| 496 | + <div style="font-size: 12px;">状态: ${appStatus[app]}</div> | ||
| 497 | + </div> | ||
| 498 | + `; | ||
| 499 | + } | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + // 检查应用状态 | ||
| 503 | + function checkStatus() { | ||
| 504 | + fetch('/api/status') | ||
| 505 | + .then(response => response.json()) | ||
| 506 | + .then(data => { | ||
| 507 | + updateAppStatus(data); | ||
| 508 | + }) | ||
| 509 | + .catch(error => { | ||
| 510 | + console.error('状态检查失败:', error); | ||
| 511 | + }); | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + // 更新应用状态 | ||
| 515 | + function updateAppStatus(data) { | ||
| 516 | + for (const [app, info] of Object.entries(data)) { | ||
| 517 | + appStatus[app] = info.status; | ||
| 518 | + const indicator = document.getElementById(`status-${app}`); | ||
| 519 | + if (indicator) { | ||
| 520 | + indicator.className = `status-indicator ${info.status}`; | ||
| 521 | + } | ||
| 522 | + } | ||
| 523 | + | ||
| 524 | + // 如果当前显示的应用状态发生变化,更新嵌入页面 | ||
| 525 | + updateEmbeddedPage(currentApp); | ||
| 526 | + } | ||
| 527 | + | ||
| 528 | + // 更新连接状态 | ||
| 529 | + function updateConnectionStatus(status) { | ||
| 530 | + document.getElementById('connectionStatus').textContent = status; | ||
| 531 | + } | ||
| 532 | + | ||
| 533 | + // 更新时间 | ||
| 534 | + function updateTime() { | ||
| 535 | + const now = new Date(); | ||
| 536 | + const timeString = now.toLocaleTimeString('zh-CN'); | ||
| 537 | + document.getElementById('systemTime').textContent = timeString; | ||
| 538 | + } | ||
| 539 | + | ||
| 540 | + // 显示消息 | ||
| 541 | + function showMessage(text, type = 'info') { | ||
| 542 | + const message = document.getElementById('message'); | ||
| 543 | + message.textContent = text; | ||
| 544 | + message.className = `message ${type}`; | ||
| 545 | + message.classList.add('show'); | ||
| 546 | + | ||
| 547 | + setTimeout(() => { | ||
| 548 | + message.classList.remove('show'); | ||
| 549 | + }, 3000); | ||
| 550 | + } | ||
| 551 | + </script> | ||
| 552 | +</body> | ||
| 553 | +</html> |
-
Please register or login to post a comment