Initial setup of web app.
This commit is contained in:
@@ -0,0 +1,342 @@
|
|||||||
|
"""
|
||||||
|
Flask主应用 - 统一管理三个Streamlit应用
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from flask import Flask, render_template, request, jsonify, Response
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
import signal
|
||||||
|
import atexit
|
||||||
|
import requests
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'weibo_analysis_system_2024'
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||||
|
|
||||||
|
# 全局变量存储进程信息
|
||||||
|
processes = {
|
||||||
|
'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': []},
|
||||||
|
'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': []},
|
||||||
|
'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': []}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 输出队列
|
||||||
|
output_queues = {
|
||||||
|
'insight': Queue(),
|
||||||
|
'media': Queue(),
|
||||||
|
'query': Queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_process_output(process, app_name):
|
||||||
|
"""读取进程输出并放入队列"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if process.poll() is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
output = process.stdout.readline()
|
||||||
|
if output:
|
||||||
|
line = output.decode('utf-8', errors='ignore').strip()
|
||||||
|
if line:
|
||||||
|
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||||
|
formatted_line = f"[{timestamp}] {line}"
|
||||||
|
|
||||||
|
# 添加到输出列表(保持最近100行)
|
||||||
|
processes[app_name]['output'].append(formatted_line)
|
||||||
|
if len(processes[app_name]['output']) > 100:
|
||||||
|
processes[app_name]['output'].pop(0)
|
||||||
|
|
||||||
|
# 发送到前端
|
||||||
|
socketio.emit('console_output', {
|
||||||
|
'app': app_name,
|
||||||
|
'line': formatted_line
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading output for {app_name}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
def start_streamlit_app(app_name, script_path, port):
|
||||||
|
"""启动Streamlit应用"""
|
||||||
|
try:
|
||||||
|
if processes[app_name]['process'] is not None:
|
||||||
|
return False, "应用已经在运行"
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
return False, f"文件不存在: {script_path}"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, '-m', 'streamlit', 'run',
|
||||||
|
script_path,
|
||||||
|
'--server.port', str(port),
|
||||||
|
'--server.headless', 'true',
|
||||||
|
'--browser.gatherUsageStats', 'false',
|
||||||
|
'--logger.level', 'info'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 使用当前工作目录而不是脚本目录
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=False,
|
||||||
|
cwd=os.getcwd()
|
||||||
|
)
|
||||||
|
|
||||||
|
processes[app_name]['process'] = process
|
||||||
|
processes[app_name]['status'] = 'starting'
|
||||||
|
processes[app_name]['output'] = []
|
||||||
|
|
||||||
|
# 启动输出读取线程
|
||||||
|
output_thread = threading.Thread(
|
||||||
|
target=read_process_output,
|
||||||
|
args=(process, app_name),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
output_thread.start()
|
||||||
|
|
||||||
|
return True, f"{app_name} 应用启动中..."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"启动失败: {str(e)}"
|
||||||
|
|
||||||
|
def stop_streamlit_app(app_name):
|
||||||
|
"""停止Streamlit应用"""
|
||||||
|
try:
|
||||||
|
if processes[app_name]['process'] is None:
|
||||||
|
return False, "应用未运行"
|
||||||
|
|
||||||
|
process = processes[app_name]['process']
|
||||||
|
process.terminate()
|
||||||
|
|
||||||
|
# 等待进程结束
|
||||||
|
try:
|
||||||
|
process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
processes[app_name]['process'] = None
|
||||||
|
processes[app_name]['status'] = 'stopped'
|
||||||
|
|
||||||
|
return True, f"{app_name} 应用已停止"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"停止失败: {str(e)}"
|
||||||
|
|
||||||
|
def check_app_status():
|
||||||
|
"""检查应用状态"""
|
||||||
|
for app_name, info in processes.items():
|
||||||
|
if info['process'] is not None:
|
||||||
|
if info['process'].poll() is None:
|
||||||
|
# 进程仍在运行,检查端口是否可访问
|
||||||
|
try:
|
||||||
|
response = requests.get(f"http://localhost:{info['port']}", timeout=2)
|
||||||
|
if response.status_code == 200:
|
||||||
|
info['status'] = 'running'
|
||||||
|
else:
|
||||||
|
info['status'] = 'starting'
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
info['status'] = 'starting'
|
||||||
|
except Exception:
|
||||||
|
info['status'] = 'starting'
|
||||||
|
else:
|
||||||
|
# 进程已结束
|
||||||
|
info['process'] = None
|
||||||
|
info['status'] = 'stopped'
|
||||||
|
|
||||||
|
def wait_for_app_startup(app_name, max_wait_time=30):
|
||||||
|
"""等待应用启动完成"""
|
||||||
|
import time
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < max_wait_time:
|
||||||
|
info = processes[app_name]
|
||||||
|
if info['process'] is None:
|
||||||
|
return False, "进程已停止"
|
||||||
|
|
||||||
|
if info['process'].poll() is not None:
|
||||||
|
return False, "进程启动失败"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"http://localhost:{info['port']}", timeout=2)
|
||||||
|
if response.status_code == 200:
|
||||||
|
info['status'] = 'running'
|
||||||
|
return True, "启动成功"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return False, "启动超时"
|
||||||
|
|
||||||
|
def cleanup_processes():
|
||||||
|
"""清理所有进程"""
|
||||||
|
for app_name in processes:
|
||||||
|
stop_streamlit_app(app_name)
|
||||||
|
|
||||||
|
# 注册清理函数
|
||||||
|
atexit.register(cleanup_processes)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""主页"""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/api/status')
|
||||||
|
def get_status():
|
||||||
|
"""获取所有应用状态"""
|
||||||
|
check_app_status()
|
||||||
|
return jsonify({
|
||||||
|
app_name: {
|
||||||
|
'status': info['status'],
|
||||||
|
'port': info['port'],
|
||||||
|
'output_lines': len(info['output'])
|
||||||
|
}
|
||||||
|
for app_name, info in processes.items()
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/start/<app_name>')
|
||||||
|
def start_app(app_name):
|
||||||
|
"""启动指定应用"""
|
||||||
|
if app_name not in processes:
|
||||||
|
return jsonify({'success': False, 'message': '未知应用'})
|
||||||
|
|
||||||
|
script_paths = {
|
||||||
|
'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',
|
||||||
|
'media': 'SingleEngineApp/media_engine_streamlit_app.py',
|
||||||
|
'query': 'SingleEngineApp/query_engine_streamlit_app.py'
|
||||||
|
}
|
||||||
|
|
||||||
|
success, message = start_streamlit_app(
|
||||||
|
app_name,
|
||||||
|
script_paths[app_name],
|
||||||
|
processes[app_name]['port']
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# 等待应用启动
|
||||||
|
startup_success, startup_message = wait_for_app_startup(app_name, 15)
|
||||||
|
if not startup_success:
|
||||||
|
message += f" 但启动检查失败: {startup_message}"
|
||||||
|
|
||||||
|
return jsonify({'success': success, 'message': message})
|
||||||
|
|
||||||
|
@app.route('/api/stop/<app_name>')
|
||||||
|
def stop_app(app_name):
|
||||||
|
"""停止指定应用"""
|
||||||
|
if app_name not in processes:
|
||||||
|
return jsonify({'success': False, 'message': '未知应用'})
|
||||||
|
|
||||||
|
success, message = stop_streamlit_app(app_name)
|
||||||
|
return jsonify({'success': success, 'message': message})
|
||||||
|
|
||||||
|
@app.route('/api/output/<app_name>')
|
||||||
|
def get_output(app_name):
|
||||||
|
"""获取应用输出"""
|
||||||
|
if app_name not in processes:
|
||||||
|
return jsonify({'success': False, 'message': '未知应用'})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'output': processes[app_name]['output']
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/search', methods=['POST'])
|
||||||
|
def search():
|
||||||
|
"""统一搜索接口"""
|
||||||
|
data = request.get_json()
|
||||||
|
query = data.get('query', '').strip()
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
return jsonify({'success': False, 'message': '搜索查询不能为空'})
|
||||||
|
|
||||||
|
# 检查哪些应用正在运行
|
||||||
|
check_app_status()
|
||||||
|
running_apps = [name for name, info in processes.items() if info['status'] == 'running']
|
||||||
|
|
||||||
|
if not running_apps:
|
||||||
|
return jsonify({'success': False, 'message': '没有运行中的应用'})
|
||||||
|
|
||||||
|
# 向运行中的应用发送搜索请求
|
||||||
|
results = {}
|
||||||
|
api_ports = {'insight': 8601, 'media': 8602, 'query': 8603}
|
||||||
|
|
||||||
|
for app_name in running_apps:
|
||||||
|
try:
|
||||||
|
api_port = api_ports[app_name]
|
||||||
|
# 调用Streamlit应用的API端点
|
||||||
|
response = requests.post(
|
||||||
|
f"http://localhost:{api_port}/api/search",
|
||||||
|
json={'query': query},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
results[app_name] = response.json()
|
||||||
|
else:
|
||||||
|
results[app_name] = {'success': False, 'message': 'API调用失败'}
|
||||||
|
except Exception as e:
|
||||||
|
results[app_name] = {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'query': query,
|
||||||
|
'results': results
|
||||||
|
})
|
||||||
|
|
||||||
|
@socketio.on('connect')
|
||||||
|
def handle_connect():
|
||||||
|
"""客户端连接"""
|
||||||
|
emit('status', 'Connected to Flask server')
|
||||||
|
|
||||||
|
@socketio.on('request_status')
|
||||||
|
def handle_status_request():
|
||||||
|
"""请求状态更新"""
|
||||||
|
check_app_status()
|
||||||
|
emit('status_update', {
|
||||||
|
app_name: {
|
||||||
|
'status': info['status'],
|
||||||
|
'port': info['port']
|
||||||
|
}
|
||||||
|
for app_name, info in processes.items()
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 启动时自动启动所有Streamlit应用
|
||||||
|
print("正在启动Streamlit应用...")
|
||||||
|
|
||||||
|
script_paths = {
|
||||||
|
'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',
|
||||||
|
'media': 'SingleEngineApp/media_engine_streamlit_app.py',
|
||||||
|
'query': 'SingleEngineApp/query_engine_streamlit_app.py'
|
||||||
|
}
|
||||||
|
|
||||||
|
for app_name, script_path in script_paths.items():
|
||||||
|
print(f"检查文件: {script_path}")
|
||||||
|
if os.path.exists(script_path):
|
||||||
|
print(f"启动 {app_name}...")
|
||||||
|
success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port'])
|
||||||
|
print(f"{app_name}: {message}")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"等待 {app_name} 启动完成...")
|
||||||
|
startup_success, startup_message = wait_for_app_startup(app_name, 30)
|
||||||
|
print(f"{app_name} 启动检查: {startup_message}")
|
||||||
|
else:
|
||||||
|
print(f"错误: {script_path} 不存在")
|
||||||
|
|
||||||
|
print("所有应用启动完成,启动Flask服务器...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动Flask应用
|
||||||
|
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n正在关闭应用...")
|
||||||
|
cleanup_processes()
|
||||||
@@ -30,3 +30,11 @@ uuid>=1.30
|
|||||||
pytest>=7.4.0
|
pytest>=7.4.0
|
||||||
black>=23.0.0
|
black>=23.0.0
|
||||||
flake8>=6.0.0
|
flake8>=6.0.0
|
||||||
|
|
||||||
|
# Flask Web应用
|
||||||
|
flask==2.3.3
|
||||||
|
flask-socketio==5.3.6
|
||||||
|
streamlit==1.28.1
|
||||||
|
requests==2.31.0
|
||||||
|
python-socketio==5.8.0
|
||||||
|
eventlet==0.33.3
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>微博舆情预测系统</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框区域 */
|
||||||
|
.search-section {
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
padding: 15px 30px;
|
||||||
|
border: none;
|
||||||
|
border-left: 2px solid #000000;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:disabled {
|
||||||
|
background-color: #666666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 嵌入页面区域 */
|
||||||
|
.embedded-section {
|
||||||
|
flex: 2;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-header {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-content {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制台输出区域 */
|
||||||
|
.console-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 应用切换按钮 */
|
||||||
|
.app-switcher {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-button:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-button.active {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-button:not(.active):hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.running {
|
||||||
|
background-color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.starting {
|
||||||
|
background-color: #ffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制台输出 */
|
||||||
|
.console-output {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #00ff00;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态信息 */
|
||||||
|
.status-bar {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-top: 2px solid #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: transparent;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-content {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-section {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-section {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息提示 */
|
||||||
|
.message {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: #ffeeee;
|
||||||
|
border-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background-color: #eeffee;
|
||||||
|
border-color: #00ff00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 搜索框区域 -->
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-title">搜索框</div>
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" class="search-input" id="searchInput" placeholder="请输入搜索内容...">
|
||||||
|
<button class="search-button" id="searchButton">搜索</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 嵌入页面区域 -->
|
||||||
|
<div class="embedded-section">
|
||||||
|
<div class="embedded-header" id="embeddedHeader">嵌入的页面</div>
|
||||||
|
<div class="embedded-content" id="embeddedContent">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">
|
||||||
|
<span>只显示一个页面 - 点击按钮切换页面</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制台输出区域 -->
|
||||||
|
<div class="console-section">
|
||||||
|
<!-- 应用切换按钮 -->
|
||||||
|
<div class="app-switcher">
|
||||||
|
<button class="app-button active" data-app="insight">
|
||||||
|
<span class="status-indicator" id="status-insight"></span>
|
||||||
|
Insight Engine
|
||||||
|
</button>
|
||||||
|
<button class="app-button" data-app="media">
|
||||||
|
<span class="status-indicator" id="status-media"></span>
|
||||||
|
Media Engine
|
||||||
|
</button>
|
||||||
|
<button class="app-button" data-app="query">
|
||||||
|
<span class="status-indicator" id="status-query"></span>
|
||||||
|
Query Engine
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<div class="console-output" id="consoleOutput">
|
||||||
|
<div class="console-line">[系统] 等待连接...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<span id="connectionStatus">连接中...</span>
|
||||||
|
<span id="systemTime"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息提示 -->
|
||||||
|
<div class="message" id="message"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 全局变量
|
||||||
|
let socket;
|
||||||
|
let currentApp = 'insight';
|
||||||
|
let appStatus = {
|
||||||
|
insight: 'stopped',
|
||||||
|
media: 'stopped',
|
||||||
|
query: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeSocket();
|
||||||
|
initializeEventListeners();
|
||||||
|
updateTime();
|
||||||
|
setInterval(updateTime, 1000);
|
||||||
|
checkStatus();
|
||||||
|
setInterval(checkStatus, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Socket.IO连接
|
||||||
|
function initializeSocket() {
|
||||||
|
socket = io();
|
||||||
|
|
||||||
|
socket.on('connect', function() {
|
||||||
|
updateConnectionStatus('已连接');
|
||||||
|
socket.emit('request_status');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
updateConnectionStatus('连接断开');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('console_output', function(data) {
|
||||||
|
if (data.app === currentApp) {
|
||||||
|
addConsoleOutput(data.line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('status_update', function(data) {
|
||||||
|
updateAppStatus(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件监听器
|
||||||
|
function initializeEventListeners() {
|
||||||
|
// 搜索按钮
|
||||||
|
document.getElementById('searchButton').addEventListener('click', performSearch);
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用切换按钮
|
||||||
|
document.querySelectorAll('.app-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const app = this.dataset.app;
|
||||||
|
switchToApp(app);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行搜索
|
||||||
|
function performSearch() {
|
||||||
|
const query = document.getElementById('searchInput').value.trim();
|
||||||
|
if (!query) {
|
||||||
|
showMessage('请输入搜索内容', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = document.getElementById('searchButton');
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="loading"></span> 搜索中...';
|
||||||
|
|
||||||
|
fetch('/api/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: query })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('搜索请求已发送到所有运行中的应用', 'success');
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '搜索失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('搜索错误:', error);
|
||||||
|
showMessage('搜索请求失败', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = '搜索';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换应用
|
||||||
|
function switchToApp(app) {
|
||||||
|
if (app === currentApp) return;
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
document.querySelectorAll('.app-button').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.querySelector(`[data-app="${app}"]`).classList.add('active');
|
||||||
|
|
||||||
|
currentApp = app;
|
||||||
|
|
||||||
|
// 清空并加载新的控制台输出
|
||||||
|
document.getElementById('consoleOutput').innerHTML = '<div class="console-line">[系统] 切换到 ' + app + ' 应用</div>';
|
||||||
|
loadConsoleOutput(app);
|
||||||
|
|
||||||
|
// 更新嵌入页面
|
||||||
|
updateEmbeddedPage(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载控制台输出
|
||||||
|
function loadConsoleOutput(app) {
|
||||||
|
fetch(`/api/output/${app}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.output.length > 0) {
|
||||||
|
const consoleOutput = document.getElementById('consoleOutput');
|
||||||
|
data.output.forEach(line => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'console-line';
|
||||||
|
div.textContent = line;
|
||||||
|
consoleOutput.appendChild(div);
|
||||||
|
});
|
||||||
|
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('加载输出失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加控制台输出
|
||||||
|
function addConsoleOutput(line) {
|
||||||
|
const consoleOutput = document.getElementById('consoleOutput');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'console-line';
|
||||||
|
div.textContent = line;
|
||||||
|
consoleOutput.appendChild(div);
|
||||||
|
|
||||||
|
// 保持最近100行
|
||||||
|
const lines = consoleOutput.children;
|
||||||
|
if (lines.length > 100) {
|
||||||
|
consoleOutput.removeChild(lines[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新嵌入页面
|
||||||
|
function updateEmbeddedPage(app) {
|
||||||
|
const header = document.getElementById('embeddedHeader');
|
||||||
|
const content = document.getElementById('embeddedContent');
|
||||||
|
|
||||||
|
const appNames = {
|
||||||
|
insight: 'Insight Engine - 私有数据库分析',
|
||||||
|
media: 'Media Engine - 多模态能力',
|
||||||
|
query: 'Query Engine - 网页搜索'
|
||||||
|
};
|
||||||
|
|
||||||
|
header.textContent = appNames[app] || app;
|
||||||
|
|
||||||
|
// 如果应用正在运行,显示iframe
|
||||||
|
if (appStatus[app] === 'running') {
|
||||||
|
const ports = { insight: 8501, media: 8502, query: 8503 };
|
||||||
|
content.innerHTML = `<iframe src="http://localhost:${ports[app]}" style="width: 100%; height: 100%; border: none;"></iframe>`;
|
||||||
|
} else {
|
||||||
|
content.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column;">
|
||||||
|
<div style="margin-bottom: 10px;">${appNames[app]} 未运行</div>
|
||||||
|
<div style="font-size: 12px;">状态: ${appStatus[app]}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查应用状态
|
||||||
|
function checkStatus() {
|
||||||
|
fetch('/api/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateAppStatus(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('状态检查失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新应用状态
|
||||||
|
function updateAppStatus(data) {
|
||||||
|
for (const [app, info] of Object.entries(data)) {
|
||||||
|
appStatus[app] = info.status;
|
||||||
|
const indicator = document.getElementById(`status-${app}`);
|
||||||
|
if (indicator) {
|
||||||
|
indicator.className = `status-indicator ${info.status}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前显示的应用状态发生变化,更新嵌入页面
|
||||||
|
updateEmbeddedPage(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新连接状态
|
||||||
|
function updateConnectionStatus(status) {
|
||||||
|
document.getElementById('connectionStatus').textContent = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
function updateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString('zh-CN');
|
||||||
|
document.getElementById('systemTime').textContent = timeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
function showMessage(text, type = 'info') {
|
||||||
|
const message = document.getElementById('message');
|
||||||
|
message.textContent = text;
|
||||||
|
message.className = `message ${type}`;
|
||||||
|
message.classList.add('show');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
message.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user