增加实时在线人数显示功能

This commit is contained in:
2025-10-19 14:32:09 +08:00
parent cfa13d5e4e
commit b80391415b
3 changed files with 190 additions and 211 deletions
-171
View File
@@ -1,171 +0,0 @@
# 贡献指南
感谢您对本项目的关注!我们欢迎任何形式的贡献。
## 🤝 如何贡献
### 报告 Bug
如果您发现了 Bug,请:
1. 在 [Issues](https://github.com/your-repo/issues) 中搜索是否已有相同问题
2. 如果没有,创建新 Issue,包含:
- 清晰的标题
- Bug 复现步骤
- 预期行为 vs 实际行为
- 系统环境(操作系统、Python 版本等)
- 错误截图或日志(如果有)
### 提出新功能
如果您有新功能建议:
1. 创建 Feature Request Issue
2. 描述功能的用途和价值
3. 提供可能的实现思路(可选)
### 提交代码
#### 准备工作
1. Fork 本仓库
2. 克隆到本地:
```bash
git clone https://github.com/your-username/AutoControl.git
cd AutoControl
```
3. 创建新分支:
```bash
git checkout -b feature/your-feature-name
```
#### 开发规范
**代码风格**
- 遵循 PEP 8 Python 代码规范
- 使用有意义的变量名和函数名
- 添加必要的注释和文档字符串
**提交信息**
```
<type>(<scope>): <subject>
<body>
<footer>
```
类型(type):
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档更新
- `style`: 代码格式调整
- `refactor`: 重构
- `test`: 测试相关
- `chore`: 构建/工具链相关
示例:
```
feat(analysis): 添加状态空间分析模块
- 实现状态空间表示转换
- 添加可控性和可观测性判断
- 集成到主界面标签页
Closes #123
```
#### 测试
在提交前确保:
- 代码能正常运行
- 没有破坏现有功能
- 新功能经过测试
#### 提交 Pull Request
1. 推送到您的 Fork
```bash
git push origin feature/your-feature-name
```
2. 在 GitHub 上创建 Pull Request
3. 填写 PR 描述:
- 做了什么改动
- 为什么需要这个改动
- 如何测试
4. 等待代码审查
## 📋 开发任务
### 当前优先级
#### 高优先级
- [ ] 修复已知 Bug
- [ ] 改进性能(大系统分析优化)
- [ ] 移动端适配
#### 中优先级
- [ ] 状态空间分析模块
- [ ] PID 控制器调优工具
- [ ] 系统对比功能
#### 低优先级
- [ ] 导出分析报告
- [ ] 多语言支持
- [ ] 主题切换(暗色模式)
### 适合新手的任务
标记为 `good first issue` 的 Issue 适合新手参与:
- 文档改进
- UI 微调
- 小功能添加
## 🎨 UI/UX 贡献
我们欢迎设计相关的贡献:
- 界面美化
- 交互优化
- 图标设计
- 配色方案
## 📚 文档贡献
文档同样重要:
- 修正错别字
- 改进说明
- 添加示例
- 翻译文档
## 💬 社区
- GitHub Issues: 技术讨论
- [讨论区](https://github.com/your-repo/discussions): 一般交流
## ⚖️ 行为准则
### 我们的承诺
为建设开放友好的环境,我们贡献者和维护者承诺:
- 使用友好和包容的语言
- 尊重不同的观点和经验
- 优雅地接受建设性批评
- 关注对社区最有利的事情
### 不可接受的行为
- 使用性暗示语言或图像
- 挑衅、侮辱性评论,人身攻击
- 公开或私下骚扰
- 未经许可发布他人隐私信息
## 📄 许可证
贡献代码即表示您同意将您的贡献以 MIT 许可证授权。
---
**感谢您的贡献!** 🎉
如有任何问题,请随时联系维护者。
+159 -40
View File
@@ -65,6 +65,48 @@ API_TYPE = "deepseek" # API 类型: "deepseek" 或 "gemini"
# ==================================================
import json
import os
import time
from datetime import datetime
from threading import Lock
# ==================== 在线人数统计 ====================
# 全局变量:追踪活跃用户
active_users = {} # 存储用户最后活跃时间 {session_id: timestamp}
users_lock = Lock() # 线程锁,保证并发安全
TIMEOUT_SECONDS = 300 # 5分钟无活动视为离线
def get_active_users_count():
"""获取当前活跃用户数量"""
current_time = time.time()
with users_lock:
# 移除超时用户
expired_users = [uid for uid, last_time in active_users.items()
if current_time - last_time > TIMEOUT_SECONDS]
for uid in expired_users:
del active_users[uid]
return len(active_users)
def update_user_activity(session_id):
"""更新用户活跃时间"""
with users_lock:
active_users[session_id] = time.time()
def get_online_status_html():
"""生成在线人数显示的HTML"""
count = get_active_users_count()
return f"""
<div style='display: inline-flex; align-items: center; gap: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 8px 16px; border-radius: 20px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);'>
<span style='font-size: 1.2em;'>🌐</span>
<span style='color: white; font-weight: 600; font-size: 1em;'>在线人数:</span>
<span style='color: #fff; font-weight: 700; font-size: 1.2em;
background: rgba(255,255,255,0.2); padding: 2px 12px;
border-radius: 12px; min-width: 30px; text-align: center;'>{count}</span>
</div>
"""
# ==================================================
# --- [FIXED] 辅助函数:将系数数组转换为LaTeX数学公式字符串 ---
def coeffs_to_latex(coeffs, var='s'):
@@ -116,30 +158,32 @@ def coeffs_to_latex(coeffs, var='s'):
# --- 功能函数1:显示传递函数 ---
def display_transfer_function(num_str, den_str):
def display_transfer_function(num_str, den_str, session_id):
update_user_activity(session_id)
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num_coeffs.size == 0 or den_coeffs.size == 0:
return "分子或分母不能为空"
return "分子或分母不能为空", get_online_status_html()
num_latex = coeffs_to_latex(num_coeffs)
den_latex = coeffs_to_latex(den_coeffs)
tf_latex = f"$$ G(s) = \\frac{{{num_latex}}}{{{den_latex}}} $$"
return tf_latex
return tf_latex, get_online_status_html()
except Exception as e:
return f"输入格式错误: {e}"
return f"输入格式错误: {e}", get_online_status_html()
# --- 功能函数2:执行时域分析和绘图 ---
def time_domain_analysis(num_str, den_str):
def time_domain_analysis(num_str, den_str, session_id):
update_user_activity(session_id)
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num.size == 0 or den.size == 0:
return None, "错误:分子或分母系数不能为空。"
return None, "错误:分子或分母系数不能为空。", get_online_status_html()
system = ct.TransferFunction(num, den)
t = np.linspace(0, 15, 1000)
T_step, yout_step = ct.step_response(system, T=t)
@@ -170,12 +214,13 @@ def time_domain_analysis(num_str, den_str):
plt.tight_layout()
return fig, metrics_text
return fig, metrics_text, get_online_status_html()
except Exception as e:
return None, f"错误: {e}\n请检查您的输入。"
return None, f"错误: {e}\n请检查您的输入。", get_online_status_html()
# --- 功能函数3:执行频域分析和绘图 ---
def frequency_domain_analysis(num_str, den_str, k):
def frequency_domain_analysis(num_str, den_str, k, session_id):
update_user_activity(session_id)
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
@@ -224,7 +269,8 @@ def frequency_domain_analysis(num_str, den_str, k):
return None, f"错误: {e}", "", ""
# --- 功能函数4:执行根轨迹分析 ---
def root_locus_analysis(num_str, den_str, log_k):
def root_locus_analysis(num_str, den_str, log_k, session_id):
update_user_activity(session_id)
try:
k = 10**log_k
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
@@ -265,11 +311,12 @@ def root_locus_analysis(num_str, den_str, log_k):
# --- [新增] 功能函数5: AI 智能问答 (支持 DeepSeek 和 Gemini) ---
# [已修复] 兼容新版 Gradio 的 Chatbot 格式
async def chat_with_ai(message, history):
async def chat_with_ai(message, history, session_id):
"""
与 AI 模型进行流式对话。支持 DeepSeek 和 Gemini API。
使用新版 Gradio 的 'messages' 格式。
"""
update_user_activity(session_id)
# 系统指令
system_prompt = "你是一位精通自动控制原理的专家教授。请用清晰、准确、专业的中文来回答有关自动控制课程内容的问题。在适当的时候,可以使用公式和示例来辅助解释。"
@@ -948,10 +995,19 @@ button[variant="secondary"]:hover {
"""
with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=custom_css) as demo:
# 用户会话ID(隐藏组件)
session_id = gr.State(value=lambda: str(time.time()) + "_" + str(hash(time.time())))
# 主标题 - 带动画效果
gr.HTML("<h1 class='main-title'> 自动控制理论AI+数智平台</h1>")
gr.HTML("<p class='subtitle'>✨ 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 ✨</p>")
# 在线人数显示
online_counter = gr.HTML(value=get_online_status_html(), elem_id="online-counter")
# 定时器:每10秒触发一次更新(隐藏组件)
timer = gr.Timer(value=10, active=True)
# 项目信息横幅 - 优化对比度和可读性
gr.HTML("""
<div class='project-info-banner'>
@@ -1966,7 +2022,6 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
chatbot = gr.Chatbot(
label="🎓 自控原理AI助教",
type="messages",
bubble_full_width=False,
avatar_images=(
"https://img.icons8.com/fluency/96/user-male-circle.png",
"https://img.icons8.com/fluency/96/chatbot.png"
@@ -2032,54 +2087,103 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
# --- 事件绑定部分 ---
confirm_button.click(fn=display_transfer_function, inputs=[num_input, den_input], outputs=tf_display)
analyze_button.click(fn=time_domain_analysis, inputs=[num_input, den_input], outputs=[output_plot, output_metrics])
# 包装函数:更新在线人数
def update_online_counter_wrapper(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
# 如果返回的是元组,添加在线人数HTML
if isinstance(result, tuple):
return result + (get_online_status_html(),)
return result, get_online_status_html()
return wrapper
def update_frequency_analysis(num, den, log_k):
confirm_button.click(
fn=display_transfer_function,
inputs=[num_input, den_input, session_id],
outputs=[tf_display, online_counter]
)
analyze_button.click(
fn=time_domain_analysis,
inputs=[num_input, den_input, session_id],
outputs=[output_plot, output_metrics, online_counter]
)
def update_frequency_analysis(num, den, log_k, sid):
k = 10**log_k
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k)
return fig, metrics, tf_latex, stability, k
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k, sid)
return fig, metrics, tf_latex, stability, k, get_online_status_html()
log_k_slider_freq.release(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
num_input.change(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
den_input.change(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
log_k_slider_freq.release(
fn=update_frequency_analysis,
inputs=[num_input, den_input, log_k_slider_freq, session_id],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, online_counter]
)
def update_rl_view(log_k, num, den):
fig, poles, k_val = root_locus_analysis(num, den, log_k)
return fig, poles, k_val
num_input.change(
fn=update_frequency_analysis,
inputs=[num_input, den_input, log_k_slider_freq, session_id],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, online_counter]
)
log_k_slider_rl.release(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
num_input.change(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
den_input.change(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
den_input.change(
fn=update_frequency_analysis,
inputs=[num_input, den_input, log_k_slider_freq, session_id],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, online_counter]
)
def update_rl_view(log_k, num, den, sid):
fig, poles, k_val = root_locus_analysis(num, den, log_k, sid)
return fig, poles, k_val, get_online_status_html()
log_k_slider_rl.release(
fn=update_rl_view,
inputs=[log_k_slider_rl, num_input, den_input, session_id],
outputs=[rl_plot_output, rl_poles_display, k_number_display, online_counter]
)
num_input.change(
fn=update_rl_view,
inputs=[log_k_slider_rl, num_input, den_input, session_id],
outputs=[rl_plot_output, rl_poles_display, k_number_display, online_counter]
)
den_input.change(
fn=update_rl_view,
inputs=[log_k_slider_rl, num_input, den_input, session_id],
outputs=[rl_plot_output, rl_poles_display, k_number_display, online_counter]
)
# 聊天机器人事件处理 - 支持 DeepSeek 和 Gemini
# 按钮点击事件
send_button.click(
fn=chat_with_ai,
inputs=[chat_input, chatbot],
inputs=[chat_input, chatbot, session_id],
outputs=chatbot,
).then(
lambda: "", # 清空输入框
outputs=chat_input
lambda: ("", get_online_status_html()), # 清空输入框并更新在线人数
outputs=[chat_input, online_counter]
)
# 输入框回车事件
chat_input.submit(
fn=chat_with_ai,
inputs=[chat_input, chatbot],
inputs=[chat_input, chatbot, session_id],
outputs=chatbot,
).then(
lambda: "", # 清空输入框
outputs=chat_input
lambda: ("", get_online_status_html()), # 清空输入框并更新在线人数
outputs=[chat_input, online_counter]
)
# 清空聊天记录
clear_button.click(
lambda: [],
outputs=chatbot
lambda sid: ([], get_online_status_html()),
inputs=[session_id],
outputs=[chatbot, online_counter]
)
def on_tab_select(evt: gr.SelectData, num, den, log_k_freq, log_k_rl):
def on_tab_select(evt: gr.SelectData, num, den, log_k_freq, log_k_rl, sid):
update_user_activity(sid)
outputs = {
freq_plot_output: gr.update(),
freq_metrics_display: gr.update(),
@@ -2088,21 +2192,36 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
k_number_display_freq: gr.update(),
rl_plot_output: gr.update(),
rl_poles_display: gr.update(),
k_number_display: gr.update()
k_number_display: gr.update(),
online_counter: get_online_status_html()
}
if evt.index == 1:
k_freq = 10**log_k_freq
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k_freq)
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k_freq, sid)
outputs[freq_plot_output], outputs[freq_metrics_display], outputs[freq_tf_display], outputs[freq_stability_display], outputs[k_number_display_freq] = fig, metrics, tf_latex, stability, k_freq
elif evt.index == 2:
fig, poles, k_val = root_locus_analysis(num, den, log_k_rl)
fig, poles, k_val = root_locus_analysis(num, den, log_k_rl, sid)
outputs[rl_plot_output], outputs[rl_poles_display], outputs[k_number_display] = fig, poles, k_val
return outputs
tabs.select(
on_tab_select,
inputs=[num_input, den_input, log_k_slider_freq, log_k_slider_rl],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, rl_plot_output, rl_poles_display, k_number_display]
inputs=[num_input, den_input, log_k_slider_freq, log_k_slider_rl, session_id],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, rl_plot_output, rl_poles_display, k_number_display, online_counter]
)
# 页面加载时初始化在线人数
demo.load(
fn=lambda sid: (get_online_status_html(),),
inputs=[session_id],
outputs=[online_counter]
)
# 定时器触发:每10秒更新一次在线人数
timer.tick(
fn=lambda sid: get_online_status_html(),
inputs=[session_id],
outputs=[online_counter]
)
if __name__ == "__main__":
+31
View File
@@ -0,0 +1,31 @@
version: '3.8'
services:
autocontrol:
build: .
container_name: autocontrol-app
ports:
- "7860:7860"
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
- PYTHONUNBUFFERED=1
# 可选:资源限制
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# 可选:日志持久化
volumes:
- ./logs:/app/logs
# 健康检查
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7860"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s