wangxingjun778 commited on
Commit
03e6f63
·
1 Parent(s): bdb404e

init doc research

Browse files
Files changed (5) hide show
  1. .gitignore +139 -0
  2. README.md +91 -14
  3. app.py +2434 -0
  4. package.json +13 -0
  5. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ test.py
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ /package
27
+ /temp
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+
61
+ # Flask stuff:
62
+ instance/
63
+ .webassets-cache
64
+
65
+ # Scrapy stuff:
66
+ .scrapy
67
+
68
+ # Sphinx documentation
69
+ docs/_build/
70
+
71
+ # PyBuilder
72
+ target/
73
+
74
+ # Jupyter Notebook
75
+ .ipynb_checkpoints
76
+
77
+ # pyenv
78
+ .python-version
79
+
80
+ # celery beat schedule file
81
+ celerybeat-schedule
82
+
83
+ # SageMath parsed files
84
+ *.sage.py
85
+
86
+ # Environments
87
+ .env
88
+ .venv
89
+ env/
90
+ venv/
91
+ ENV/
92
+ env.bak/
93
+ venv.bak/
94
+
95
+ # Spyder project settings
96
+ .spyderproject
97
+ .spyproject
98
+
99
+ # Rope project settings
100
+ .ropeproject
101
+
102
+ # mkdocs documentation
103
+ /site
104
+
105
+ # mypy
106
+ .mypy_cache/
107
+
108
+ .vscode
109
+ .idea
110
+
111
+ # custom
112
+ *.pkl
113
+ *.pkl.json
114
+ *.log.json
115
+ *.whl
116
+ *.tar.gz
117
+ *.swp
118
+ *.log
119
+ *.tar.gz
120
+ source.sh
121
+ tensorboard.sh
122
+ .DS_Store
123
+ replace.sh
124
+ result.png
125
+ result.jpg
126
+ result.mp4
127
+ runs/
128
+ ckpt/
129
+
130
+ # Pytorch
131
+ *.pth
132
+ *.pt
133
+
134
+ # ast template
135
+ ast_index_file.py
136
+
137
+ .gradio/
138
+ temp_workspace/
139
+ *back*
README.md CHANGED
@@ -1,15 +1,92 @@
1
- ---
2
- title: DocResearch
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- short_description: 'Daily Paper Copilot: URLs or Files IN, Multimodal Report OUT'
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gradio研究工作流应用
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ 这是一个基于Gradio的研究工作流应用,支持文件上传和URL输入的智能研究分析。
4
+
5
+ ## 功能特性
6
+
7
+ - 📝 **用户提示输入**:支持中英文输入研究问题或任务描述
8
+ - 📁 **文件上传功能**:支持多文件上传,默认支持PDF格式
9
+ - 🔗 **URLs输入功能**:支持多个URL输入,每行一个
10
+ - 🚀 **一键运行**:点击按钮即可开始研究工作流
11
+ - 📊 **结果显示**:实时显示执行结果和工作目录
12
+ - 🗂️ **临时工作目录**:每次运行创建新的工作目录
13
+ - 🌐 **多语言支持**:支持中文和英文界面
14
+ - 👥 **多用户并发**:支持多用户同时使用,默认最大并发数为8
15
+ - 🔒 **用户隔离**:每个用户拥有独立的工作空间和会话数据
16
+ - ⏱️ **任务超时控制**:自动清理超时任务,默认超时时间15分钟
17
+ - 📈 **实时状态监控**:显示系统并发状态和用户任务状态
18
+
19
+ ## 安装和运行
20
+
21
+ 1. 安装依赖:
22
+ ```bash
23
+ pip install -r requirements.txt
24
+ ```
25
+
26
+ 2. 配置环境变量:
27
+ ```bash
28
+ cp .env.example .env
29
+ # 编辑 .env 文件,填入你的API配置
30
+ ```
31
+
32
+ 3. 运行应用:
33
+ ```bash
34
+ python app.py
35
+ ```
36
+
37
+ 4. 打开浏览器访问:http://localhost:7860
38
+
39
+ ## 环境变量配置
40
+
41
+ - `OPENAI_API_KEY`: OpenAI API密钥
42
+ - `OPENAI_BASE_URL`: OpenAI API基础URL
43
+ - `OPENAI_MODEL_ID`: 使用的模型ID
44
+ - `GRADIO_DEFAULT_CONCURRENCY_LIMIT`: Gradio默认并发限制(默认:8)
45
+ - `LOCAL_MODE`: 本地模式开关(默认:true)
46
+
47
+ ## 使用说明
48
+
49
+ 1. **用户提示**:在文本框中输入您的研究目标或问题
50
+ 2. **文件上传**:选择需要分析的文件(支持多选)
51
+ 3. **URLs输入**:输入相关的网页链接,每行一个URL
52
+ 4. **开始研究**:点击运行按钮开始执行工作流
53
+ 5. **查看结果**:在右侧区域查看执行结果和工作目录路径
54
+
55
+ ## 工作目录结构
56
+
57
+ 每次运行都会在 `temp_workspace` 目录下创建新的工作目录:
58
+ ```
59
+ temp_workspace/
60
+ ├── task_20231201_143022_a1b2c3d4/
61
+ ├── task_20231201_143156_e5f6g7h8/
62
+ └── ...
63
+ ```
64
+
65
+ ## 并发控制说明
66
+
67
+ ### 并发限制
68
+ - 系统默认支持最大8个用户同时执行研究任务
69
+ - 可通过环境变量 `GRADIO_DEFAULT_CONCURRENCY_LIMIT` 调整并发数
70
+ - 超出并发限制的用户会收到系统繁忙提示
71
+
72
+ ### 任务管理
73
+ - 每个用户同时只能执行一个研究任务
74
+ - 超时任务会被自动清理,释放系统资源
75
+
76
+ ### 状态监控
77
+ - 实时显示系统并发状态:活跃任务数/最大并发数
78
+ - 显示用户任务状态:运行中、已完成、失败等
79
+ - 提供系统状态刷新功能
80
+
81
+ ### 用户隔离
82
+ - 每个用户拥有独立的工作目录和会话数据
83
+ - 本地模式下使用时间戳区分不同会话
84
+ - 远程模式下基于用户ID进行隔离
85
+
86
+ ## 注意事项
87
+
88
+ - 确保有足够的磁盘空间用于临时文件存储
89
+ - 定期清理工作空间以释放存储空间
90
+ - 确保网络连接正常以访问外部URLs
91
+ - 在高并发场景下,建议适当增加服务器资源配置
92
+ - 长时间运行的任务可能会被超时机制清理
app.py ADDED
@@ -0,0 +1,2434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flake8: noqa
2
+ # isort: skip_file
3
+ # yapf: disable
4
+ import gradio as gr
5
+ import os
6
+ import shutil
7
+ from datetime import datetime
8
+ from typing import List, Tuple, Optional
9
+ import uuid
10
+ import base64
11
+ import re
12
+ import json
13
+ import socketserver
14
+ import markdown
15
+ import threading
16
+ import time
17
+
18
+ from ms_agent.llm.openai import OpenAIChat
19
+ from ms_agent.workflow.research_workflow import ResearchWorkflow
20
+ from ms_agent.utils.logger import get_logger
21
+
22
+ logger = get_logger()
23
+
24
+
25
+ """
26
+ This module provides a Gradio application for running a research workflow with file and URL inputs.
27
+
28
+ It includes functionalities for:
29
+ - Initializing the research workflow application
30
+ - Processing user inputs (files and URLs)
31
+ - Managing user status and task concurrency
32
+ - Converting markdown content with images to HTML or base64 format
33
+ - Reading and processing markdown reports
34
+ - Listing resource files in the working directory
35
+ """
36
+
37
+
38
+ class ResearchWorkflowApp:
39
+ """
40
+ Research Workflow Application, this class initializes the research workflow with the necessary model downloads.
41
+ """
42
+ def __init__(self, client, workdir: str):
43
+ from modelscope import snapshot_download
44
+ from modelscope.hub.utils.utils import get_cache_dir
45
+
46
+ self.client = client
47
+ self.workdir = workdir
48
+
49
+ target_dir: str = os.path.join(get_cache_dir(), 'models/EasyOCR')
50
+ if not os.path.exists(os.path.join(os.path.expanduser(target_dir), 'craft_mlt_25k.pth')):
51
+
52
+ os.makedirs(os.path.expanduser(target_dir), exist_ok=True)
53
+ # Download model to specified directory
54
+ snapshot_download(
55
+ model_id='ms-agent/craft_mlt_25k',
56
+ local_dir=os.path.expanduser(target_dir),
57
+ )
58
+ snapshot_download(
59
+ model_id='ms-agent/latin_g2',
60
+ local_dir=os.path.expanduser(target_dir),
61
+ )
62
+ logger.info(f"EasyOCR model downloaded to: {os.path.expanduser(target_dir)}")
63
+ # Unzip craft_mlt_25k.zip, latin_g2.zip
64
+ import zipfile
65
+ zip_path_craft = os.path.join(os.path.expanduser(target_dir), 'craft_mlt_25k.zip')
66
+ zip_path_latin = os.path.join(os.path.expanduser(target_dir), 'latin_g2.zip')
67
+ if os.path.exists(zip_path_craft):
68
+ with zipfile.ZipFile(zip_path_craft, 'r') as zip_ref_craft:
69
+ zip_ref_craft.extractall(os.path.expanduser(target_dir))
70
+ if os.path.exists(zip_path_latin):
71
+ with zipfile.ZipFile(zip_path_latin, 'r') as zip_ref_latin:
72
+ zip_ref_latin.extractall(os.path.expanduser(target_dir))
73
+
74
+ logger.info(f'EasyOCR model extracted to: {os.path.expanduser(target_dir)}')
75
+
76
+ self._workflow = ResearchWorkflow(
77
+ client=self.client,
78
+ workdir=self.workdir,
79
+ verbose=True,
80
+ )
81
+
82
+ def run(self, user_prompt: str, urls_or_files: List[str]) -> str:
83
+ # Check if input files/URLs are empty
84
+ if not urls_or_files:
85
+ return """
86
+ ❌ 输入错误:未提供任何文件或URLs
87
+
88
+ 请确保:
89
+ 1. 上传至少一个文件,或
90
+ 2. 在URLs输入框中输入至少一个有效的URL
91
+
92
+ 然后重新运行研究工作流。
93
+ """
94
+
95
+ self._workflow.run(
96
+ user_prompt=user_prompt,
97
+ urls_or_files=urls_or_files,
98
+ )
99
+
100
+ # Return execution statistics
101
+ result = f"""
102
+ 研究工作流执行完成!
103
+
104
+ 工作目录: {self.workdir}
105
+ 用户提示: {user_prompt}
106
+ 输入文件/URLs数量: {len(urls_or_files)}
107
+
108
+ 处理的内容:
109
+ """
110
+ for i, item in enumerate(urls_or_files, 1):
111
+ if item.startswith('http'):
112
+ result += f"{i}. URL: {item}\n"
113
+ else:
114
+ result += f"{i}. 文件: {os.path.basename(item)}\n"
115
+
116
+ result += "\n✅ 研究分析已完成,结果已保存到工作目录中。 请查看研究报告。"
117
+ return result
118
+
119
+
120
+ # Global variables
121
+ BASE_WORKDIR = "temp_workspace"
122
+
123
+ # Concurrency control configuration
124
+ GRADIO_DEFAULT_CONCURRENCY_LIMIT = int(os.environ.get('GRADIO_DEFAULT_CONCURRENCY_LIMIT', '10'))
125
+
126
+
127
+ # Simplified user status manager
128
+ class UserStatusManager:
129
+ def __init__(self):
130
+ self.active_users = {} # {user_id: {'start_time': time, 'status': status}}
131
+ self.lock = threading.Lock()
132
+
133
+ def get_user_status(self, user_id: str) -> dict:
134
+ """Get user task status"""
135
+ with self.lock:
136
+ if user_id in self.active_users:
137
+ user_info = self.active_users[user_id]
138
+ elapsed_time = time.time() - user_info['start_time']
139
+ return {
140
+ 'status': user_info['status'],
141
+ 'elapsed_time': elapsed_time,
142
+ 'is_active': True
143
+ }
144
+ return {'status': 'idle', 'elapsed_time': 0, 'is_active': False}
145
+
146
+ def start_user_task(self, user_id: str):
147
+ """Mark user task start"""
148
+ with self.lock:
149
+ self.active_users[user_id] = {
150
+ 'start_time': time.time(),
151
+ 'status': 'running'
152
+ }
153
+ logger.info(f"User task started - User: {user_id[:8]}***, Current active users: {len(self.active_users)}")
154
+
155
+ def finish_user_task(self, user_id: str):
156
+ """Mark user task completion"""
157
+ with self.lock:
158
+ if user_id in self.active_users:
159
+ del self.active_users[user_id]
160
+ logger.info(f"User task completed - User: {user_id[:8]}***, Remaining active users: {len(self.active_users)}")
161
+
162
+ def get_system_status(self) -> dict:
163
+ """Get system status"""
164
+ with self.lock:
165
+ active_count = len(self.active_users)
166
+ return {
167
+ 'active_tasks': active_count,
168
+ 'max_concurrent': GRADIO_DEFAULT_CONCURRENCY_LIMIT,
169
+ 'available_slots': GRADIO_DEFAULT_CONCURRENCY_LIMIT - active_count,
170
+ 'task_details': {
171
+ user_id: {
172
+ 'status': info['status'],
173
+ 'elapsed_time': time.time() - info['start_time']
174
+ }
175
+ for user_id, info in self.active_users.items()
176
+ }
177
+ }
178
+
179
+ def force_cleanup_user(self, user_id: str) -> bool:
180
+ """Force cleanup user task"""
181
+ with self.lock:
182
+ if user_id in self.active_users:
183
+ del self.active_users[user_id]
184
+ logger.info(f"Force cleanup user task - User: {user_id[:8]}***")
185
+ return True
186
+ return False
187
+
188
+
189
+ # Create global user status manager instance
190
+ user_status_manager = UserStatusManager()
191
+
192
+
193
+ def get_user_id_from_request(request: gr.Request) -> str:
194
+ """Get user ID from request headers"""
195
+ if request and hasattr(request, 'headers'):
196
+ user_id = request.headers.get('x-modelscope-router-id', '')
197
+ return user_id.strip() if user_id else ''
198
+ return ''
199
+
200
+
201
+ def check_user_auth(request: gr.Request) -> Tuple[bool, str]:
202
+ """Check user authentication status"""
203
+ user_id = get_user_id_from_request(request)
204
+ if not user_id:
205
+ return False, "请登录后使用 | Please log in to use this feature."
206
+ return True, user_id
207
+
208
+
209
+ def create_user_workdir(user_id: str) -> str:
210
+ """Create dedicated working directory for user"""
211
+ user_base_dir = os.path.join(BASE_WORKDIR, f"user_{user_id}")
212
+ if not os.path.exists(user_base_dir):
213
+ os.makedirs(user_base_dir)
214
+ return user_base_dir
215
+
216
+
217
+ def create_task_workdir(user_id: str) -> str:
218
+ """Create new task working directory"""
219
+ user_base_dir = create_user_workdir(user_id)
220
+
221
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
222
+ task_id = str(uuid.uuid4())[:8]
223
+ task_workdir = os.path.join(user_base_dir, f"task_{timestamp}_{task_id}")
224
+ os.makedirs(task_workdir, exist_ok=True)
225
+ return task_workdir
226
+
227
+
228
+ def process_urls_text(urls_text: str) -> List[str]:
229
+ """Process URL text input, split by newlines"""
230
+ if not urls_text.strip():
231
+ return []
232
+
233
+ urls = []
234
+ for line in urls_text.strip().split('\n'):
235
+ line = line.strip()
236
+ if line:
237
+ urls.append(line)
238
+ return urls
239
+
240
+
241
+ def process_files(files) -> List[str]:
242
+ """Process uploaded files"""
243
+ if not files:
244
+ return []
245
+
246
+ file_paths = []
247
+ # Ensure files is in list format
248
+ if not isinstance(files, list):
249
+ files = [files] if files else []
250
+
251
+ for file in files:
252
+ if file is not None:
253
+ if hasattr(file, 'name') and file.name:
254
+ file_paths.append(file.name)
255
+ elif isinstance(file, str) and file:
256
+ file_paths.append(file)
257
+
258
+ return file_paths
259
+
260
+
261
+ def check_port_available(port: int) -> bool:
262
+ """Check if port is available"""
263
+ import socket
264
+ try:
265
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
266
+ s.settimeout(1)
267
+ result = s.connect_ex(('localhost', port))
268
+ return result != 0 # 0 means connection successful, port is occupied
269
+ except Exception:
270
+ return True
271
+
272
+
273
+ class ReusableTCPServer(socketserver.TCPServer):
274
+ """TCP server that supports address reuse"""
275
+ allow_reuse_address = True
276
+
277
+
278
+ def convert_markdown_images_to_base64(markdown_content: str, workdir: str) -> str:
279
+ """Convert relative path images in markdown to base64 format (for online environments)"""
280
+
281
+ def replace_image(match):
282
+ alt_text = match.group(1)
283
+ image_path = match.group(2)
284
+
285
+ # Handle relative paths
286
+ if not os.path.isabs(image_path):
287
+ full_path = os.path.join(workdir, image_path)
288
+ else:
289
+ full_path = image_path
290
+
291
+ # Check if file exists
292
+ if os.path.exists(full_path):
293
+ try:
294
+ # Get file extension to determine MIME type
295
+ ext = os.path.splitext(full_path)[1].lower()
296
+ mime_types = {
297
+ '.png': 'image/png',
298
+ '.jpg': 'image/jpeg',
299
+ '.jpeg': 'image/jpeg',
300
+ '.gif': 'image/gif',
301
+ '.bmp': 'image/bmp',
302
+ '.webp': 'image/webp',
303
+ '.svg': 'image/svg+xml'
304
+ }
305
+ mime_type = mime_types.get(ext, 'image/png')
306
+
307
+ # Check file size to avoid oversized images
308
+ file_size = os.path.getsize(full_path)
309
+ max_size = 5 * 1024 * 1024 # 5MB limit
310
+
311
+ if file_size > max_size:
312
+ return f"""
313
+ **🖼️ 图片文件过大: {alt_text or os.path.basename(image_path)}**
314
+ - 📁 路径: `{image_path}`
315
+ - 📏 大小: {file_size / (1024 * 1024):.2f} MB (超过5MB限制)
316
+ - 💡 提示: 图片文件过大,无法在线显示,请通过文件管理器查看
317
+
318
+ ---
319
+ """
320
+
321
+ # Read image file and convert to base64
322
+ with open(full_path, 'rb') as img_file:
323
+ img_data = img_file.read()
324
+ base64_data = base64.b64encode(img_data).decode('utf-8')
325
+
326
+ # Create data URL
327
+ data_url = f"data:{mime_type};base64,{base64_data}"
328
+ return f'![{alt_text}]({data_url})'
329
+
330
+ except Exception as e:
331
+ logger.info(f"Unable to process image {full_path}: {e}")
332
+ return f"""
333
+ **❌ 图片处理失败: {alt_text or os.path.basename(image_path)}**
334
+ - 📁 路径: `{image_path}`
335
+ - ❌ 错误: {str(e)}
336
+
337
+ ---
338
+ """
339
+ else:
340
+ return f'**❌ 图片文件不存在: {alt_text or image_path}**\n\n'
341
+
342
+ # Match markdown image syntax: ![alt](path)
343
+ pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
344
+ return re.sub(pattern, replace_image, markdown_content)
345
+
346
+
347
+ def convert_markdown_images_to_file_info(markdown_content: str, workdir: str) -> str:
348
+ """Convert images in markdown to file info display (fallback solution)"""
349
+
350
+ def replace_image(match):
351
+ alt_text = match.group(1)
352
+ image_path = match.group(2)
353
+
354
+ # Handle relative paths
355
+ if not os.path.isabs(image_path):
356
+ full_path = os.path.join(workdir, image_path)
357
+ else:
358
+ full_path = image_path
359
+
360
+ # Check if file exists
361
+ if os.path.exists(full_path):
362
+ try:
363
+ # Get file information
364
+ file_size = os.path.getsize(full_path)
365
+ file_size_mb = file_size / (1024 * 1024)
366
+ ext = os.path.splitext(full_path)[1].lower()
367
+
368
+ return f"""
369
+ **🖼️ 图片文件: {alt_text or os.path.basename(image_path)}**
370
+ - 📁 路径: `{image_path}`
371
+ - 📏 大小: {file_size_mb:.2f} MB
372
+ - 🎨 格式: {ext.upper()}
373
+ - 💡 提示: 图片已保存到工作目录中,可通过文件管理器查看
374
+
375
+ ---
376
+ """
377
+ except Exception as e:
378
+ logger.info(f"Unable to read image info {full_path}: {e}")
379
+ return f'**❌ 图片加载失败: {alt_text or image_path}**\n\n'
380
+ else:
381
+ return f'**❌ 图片文件不存在: {alt_text or image_path}**\n\n'
382
+
383
+ # Match markdown image syntax: ![alt](path)
384
+ pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
385
+ return re.sub(pattern, replace_image, markdown_content)
386
+
387
+
388
+ def convert_markdown_to_html(markdown_content: str) -> str:
389
+ """Convert markdown to HTML, using KaTeX to process LaTeX formulas"""
390
+ try:
391
+ import re
392
+
393
+ # Protect LaTeX formulas to avoid misprocessing by markdown processor
394
+ latex_placeholders = {}
395
+ placeholder_counter = 0
396
+
397
+ def protect_latex(match):
398
+ nonlocal placeholder_counter
399
+ placeholder = f"LATEX_PLACEHOLDER_{placeholder_counter}"
400
+ latex_placeholders[placeholder] = match.group(0)
401
+ placeholder_counter += 1
402
+ return placeholder
403
+
404
+ # Protect various LaTeX formula formats
405
+ protected_content = markdown_content
406
+
407
+ # Protect $$...$$ (block-level formulas)
408
+ protected_content = re.sub(r'\$\$([^$]+?)\$\$', protect_latex, protected_content, flags=re.DOTALL)
409
+
410
+ # Protect $...$ (inline formulas)
411
+ protected_content = re.sub(r'(?<!\$)\$(?!\$)([^$\n]+?)\$(?!\$)', protect_latex, protected_content)
412
+
413
+ # Protect \[...\] (block-level formulas)
414
+ protected_content = re.sub(r'\\\[([^\\]+?)\\\]', protect_latex, protected_content, flags=re.DOTALL)
415
+
416
+ # Protect \(...\) (inline formulas)
417
+ protected_content = re.sub(r'\\\(([^\\]+?)\\\)', protect_latex, protected_content, flags=re.DOTALL)
418
+
419
+ # Configure markdown extensions
420
+ extensions = [
421
+ 'markdown.extensions.extra',
422
+ 'markdown.extensions.codehilite',
423
+ 'markdown.extensions.toc',
424
+ 'markdown.extensions.tables',
425
+ 'markdown.extensions.fenced_code',
426
+ 'markdown.extensions.nl2br'
427
+ ]
428
+
429
+ # Configure extension parameters
430
+ extension_configs = {
431
+ 'markdown.extensions.codehilite': {
432
+ 'css_class': 'highlight',
433
+ 'use_pygments': True
434
+ },
435
+ 'markdown.extensions.toc': {
436
+ 'permalink': True
437
+ }
438
+ }
439
+
440
+ # Create markdown instance
441
+ md = markdown.Markdown(
442
+ extensions=extensions,
443
+ extension_configs=extension_configs
444
+ )
445
+
446
+ # Convert to HTML
447
+ html_content = md.convert(protected_content)
448
+
449
+ # Restore LaTeX formulas
450
+ for placeholder, latex_formula in latex_placeholders.items():
451
+ html_content = html_content.replace(placeholder, latex_formula)
452
+
453
+ # Generate unique container ID to ensure independent KaTeX processing for each render
454
+ container_id = f"katex-content-{int(time.time() * 1000000)}"
455
+
456
+ # Use KaTeX to render LaTeX formulas
457
+ styled_html = f"""
458
+ <div class="markdown-html-content" id="{container_id}">
459
+ <!-- KaTeX CSS -->
460
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
461
+
462
+ <!-- Content area -->
463
+ <div class="content-area">
464
+ {html_content}
465
+ </div>
466
+
467
+ <!-- KaTeX JavaScript and auto-render extension -->
468
+ <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" integrity="sha384-XjKyOOlGwcjNTAIQHIpVOOVA+CuTF5UvLqGSXPM6njWx5iNxN7jyVjNOq8Ks4pxy" crossorigin="anonymous"></script>
469
+ <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"></script>
470
+
471
+ <!-- KaTeX rendering script -->
472
+ <script type="text/javascript">
473
+ (function() {{
474
+ const containerId = '{container_id}';
475
+ const container = document.getElementById(containerId);
476
+
477
+ if (!container) {{
478
+ console.warn('KaTeX container not found:', containerId);
479
+ return;
480
+ }}
481
+
482
+ // Wait for KaTeX to load before rendering
483
+ function renderKaTeX() {{
484
+ if (typeof renderMathInElement !== 'undefined') {{
485
+ console.log('Starting KaTeX rendering - Container:', containerId);
486
+
487
+ try {{
488
+ renderMathInElement(container, {{
489
+ // Configure delimiters
490
+ delimiters: [
491
+ {{left: '$$', right: '$$', display: true}},
492
+ {{left: '$', right: '$', display: false}},
493
+ {{left: '\\\\[', right: '\\\\]', display: true}},
494
+ {{left: '\\\\(', right: '\\\\)', display: false}}
495
+ ],
496
+ // Other configuration options
497
+ throwOnError: false,
498
+ errorColor: '#cc0000',
499
+ strict: false,
500
+ trust: false,
501
+ macros: {{
502
+ "\\\\RR": "\\\\mathbb{{R}}",
503
+ "\\\\NN": "\\\\mathbb{{N}}",
504
+ "\\\\ZZ": "\\\\mathbb{{Z}}",
505
+ "\\\\QQ": "\\\\mathbb{{Q}}",
506
+ "\\\\CC": "\\\\mathbb{{C}}"
507
+ }}
508
+ }});
509
+
510
+ console.log('KaTeX rendering completed - Container:', containerId);
511
+
512
+ // Count rendered formulas
513
+ const mathElements = container.querySelectorAll('.katex');
514
+ console.log('Found and processed', mathElements.length, 'mathematical formulas');
515
+
516
+ // Apply style corrections
517
+ mathElements.forEach(el => {{
518
+ const isDisplay = el.classList.contains('katex-display');
519
+ if (isDisplay) {{
520
+ el.style.margin = '1em 0';
521
+ el.style.textAlign = 'center';
522
+ }} else {{
523
+ el.style.margin = '0 0.2em';
524
+ el.style.verticalAlign = 'baseline';
525
+ }}
526
+ }});
527
+
528
+ }} catch (error) {{
529
+ console.error('KaTeX rendering error:', error);
530
+ }}
531
+ }} else {{
532
+ console.warn('KaTeX auto-render not loaded, waiting for retry...');
533
+ setTimeout(renderKaTeX, 200);
534
+ }}
535
+ }}
536
+
537
+ // Use delay to ensure Gradio is fully rendered
538
+ setTimeout(() => {{
539
+ console.log('Starting to load KaTeX...');
540
+ renderKaTeX();
541
+ }}, 300);
542
+ }})();
543
+ </script>
544
+
545
+ <style>
546
+ #{container_id} {{
547
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
548
+ line-height: 1.6;
549
+ color: #333;
550
+ max-width: 100%;
551
+ margin: 0 auto;
552
+ padding: 20px;
553
+ }}
554
+
555
+ /* KaTeX formula style optimization */
556
+ #{container_id} .katex {{
557
+ font-size: 1.1em !important;
558
+ color: inherit !important;
559
+ }}
560
+
561
+ #{container_id} .katex-display {{
562
+ margin: 1em 0 !important;
563
+ text-align: center !important;
564
+ overflow-x: auto !important;
565
+ display: block !important;
566
+ }}
567
+
568
+ /* Inline formula styles */
569
+ #{container_id} .katex:not(.katex-display) {{
570
+ display: inline-block !important;
571
+ margin: 0 0.1em !important;
572
+ vertical-align: baseline !important;
573
+ }}
574
+
575
+ /* Formula overflow handling */
576
+ #{container_id} .katex .katex-html {{
577
+ max-width: 100% !important;
578
+ overflow-x: auto !important;
579
+ }}
580
+
581
+ /* Ensure LaTeX formulas display correctly in Gradio */
582
+ #{container_id} .katex {{
583
+ line-height: normal !important;
584
+ }}
585
+
586
+ #{container_id} h1 {{
587
+ font-size: 2.2em;
588
+ margin-bottom: 1rem;
589
+ color: #2c3e50;
590
+ border-bottom: 2px solid #3498db;
591
+ padding-bottom: 0.5rem;
592
+ }}
593
+
594
+ #{container_id} h2 {{
595
+ font-size: 1.8em;
596
+ margin-bottom: 0.8rem;
597
+ color: #34495e;
598
+ border-bottom: 1px solid #bdc3c7;
599
+ padding-bottom: 0.3rem;
600
+ }}
601
+
602
+ #{container_id} h3 {{
603
+ font-size: 1.5em;
604
+ margin-bottom: 0.6rem;
605
+ color: #34495e;
606
+ }}
607
+
608
+ #{container_id} h4, #{container_id} h5, #{container_id} h6 {{
609
+ color: #34495e;
610
+ margin-bottom: 0.5rem;
611
+ }}
612
+
613
+ #{container_id} p {{
614
+ margin-bottom: 1rem;
615
+ text-align: justify;
616
+ }}
617
+
618
+ #{container_id} img {{
619
+ max-width: 100%;
620
+ height: auto;
621
+ border-radius: 8px;
622
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
623
+ margin: 1rem 0;
624
+ display: block;
625
+ margin-left: auto;
626
+ margin-right: auto;
627
+ }}
628
+
629
+ #{container_id} ul, #{container_id} ol {{
630
+ margin-bottom: 1rem;
631
+ padding-left: 2rem;
632
+ }}
633
+
634
+ #{container_id} li {{
635
+ margin-bottom: 0.3rem;
636
+ }}
637
+
638
+ #{container_id} blockquote {{
639
+ background: #f8f9fa;
640
+ border-left: 4px solid #3498db;
641
+ padding: 1rem;
642
+ margin: 1rem 0;
643
+ border-radius: 0 4px 4px 0;
644
+ font-style: italic;
645
+ }}
646
+
647
+ #{container_id} code {{
648
+ background: #f1f2f6;
649
+ padding: 0.2rem 0.4rem;
650
+ border-radius: 3px;
651
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
652
+ font-size: 0.9em;
653
+ color: #e74c3c;
654
+ }}
655
+
656
+ #{container_id} pre {{
657
+ background: #2c3e50;
658
+ color: #ecf0f1;
659
+ padding: 1rem;
660
+ border-radius: 6px;
661
+ overflow-x: auto;
662
+ margin: 1rem 0;
663
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
664
+ }}
665
+
666
+ #{container_id} pre code {{
667
+ background: transparent;
668
+ padding: 0;
669
+ color: inherit;
670
+ }}
671
+
672
+ #{container_id} table {{
673
+ width: 100%;
674
+ border-collapse: collapse;
675
+ margin: 1rem 0;
676
+ background: white;
677
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
678
+ border-radius: 6px;
679
+ overflow: hidden;
680
+ }}
681
+
682
+ #{container_id} th, #{container_id} td {{
683
+ padding: 0.75rem;
684
+ text-align: left;
685
+ border-bottom: 1px solid #ddd;
686
+ }}
687
+
688
+ #{container_id} th {{
689
+ background: #3498db;
690
+ color: white;
691
+ font-weight: 600;
692
+ }}
693
+
694
+ #{container_id} tr:nth-child(even) {{
695
+ background: #f8f9fa;
696
+ }}
697
+
698
+ #{container_id} a {{
699
+ color: #3498db;
700
+ text-decoration: none;
701
+ border-bottom: 1px solid transparent;
702
+ transition: border-bottom-color 0.2s ease;
703
+ }}
704
+
705
+ #{container_id} a:hover {{
706
+ border-bottom-color: #3498db;
707
+ }}
708
+
709
+ #{container_id} hr {{
710
+ border: none;
711
+ height: 2px;
712
+ background: linear-gradient(to right, #3498db, #2ecc71);
713
+ margin: 2rem 0;
714
+ border-radius: 1px;
715
+ }}
716
+
717
+ #{container_id} .highlight {{
718
+ background: #2c3e50;
719
+ color: #ecf0f1;
720
+ padding: 1rem;
721
+ border-radius: 6px;
722
+ overflow-x: auto;
723
+ margin: 1rem 0;
724
+ }}
725
+
726
+ /* Dark theme adaptation */
727
+ @media (prefers-color-scheme: dark) {{
728
+ #{container_id} {{
729
+ color: #ecf0f1;
730
+ background: #2c3e50;
731
+ }}
732
+
733
+ #{container_id} h1, #{container_id} h2, #{container_id} h3,
734
+ #{container_id} h4, #{container_id} h5, #{container_id} h6 {{
735
+ color: #ecf0f1;
736
+ }}
737
+
738
+ #{container_id} h1 {{
739
+ border-bottom-color: #3498db;
740
+ }}
741
+
742
+ #{container_id} h2 {{
743
+ border-bottom-color: #7f8c8d;
744
+ }}
745
+
746
+ #{container_id} blockquote {{
747
+ background: #34495e;
748
+ color: #ecf0f1;
749
+ }}
750
+
751
+ #{container_id} code {{
752
+ background: #34495e;
753
+ color: #e74c3c;
754
+ }}
755
+
756
+ #{container_id} table {{
757
+ background: #34495e;
758
+ }}
759
+
760
+ #{container_id} th {{
761
+ background: #2980b9;
762
+ }}
763
+
764
+ #{container_id} tr:nth-child(even) {{
765
+ background: #2c3e50;
766
+ }}
767
+
768
+ #{container_id} td {{
769
+ border-bottom-color: #7f8c8d;
770
+ }}
771
+
772
+ #{container_id} .katex {{
773
+ color: #ecf0f1 !important;
774
+ }}
775
+
776
+ #{container_id} .katex .katex-html {{
777
+ color: #ecf0f1 !important;
778
+ }}
779
+ }}
780
+
781
+ /* Responsive design */
782
+ @media (max-width: 768px) {{
783
+ #{container_id} {{
784
+ padding: 15px;
785
+ font-size: 14px;
786
+ }}
787
+
788
+ #{container_id} h1 {{
789
+ font-size: 1.8em;
790
+ }}
791
+
792
+ #{container_id} h2 {{
793
+ font-size: 1.5em;
794
+ }}
795
+
796
+ #{container_id} h3 {{
797
+ font-size: 1.3em;
798
+ }}
799
+
800
+ #{container_id} table {{
801
+ font-size: 12px;
802
+ }}
803
+
804
+ #{container_id} th, #{container_id} td {{
805
+ padding: 0.5rem;
806
+ }}
807
+
808
+ /* Mobile KaTeX optimization */
809
+ #{container_id} .katex {{
810
+ font-size: 1em !important;
811
+ }}
812
+ }}
813
+ </style>
814
+ </div>
815
+ """
816
+
817
+ return styled_html
818
+
819
+ except Exception as e:
820
+ logger.info(f"Markdown to HTML conversion failed: {e}")
821
+ # If conversion fails, return original markdown content wrapped in pre tags
822
+ return f"""
823
+ <div class="markdown-fallback">
824
+ <h3>⚠️ Markdown渲染失败,显示原始内容</h3>
825
+ <pre style="white-space: pre-wrap; word-wrap: break-word; background: #f8f9fa; padding: 1rem; border-radius: 6px; border: 1px solid #dee2e6;">{markdown_content}</pre>
826
+ </div>
827
+ """
828
+
829
+
830
+ def read_markdown_report(workdir: str) -> Tuple[str, str, str]:
831
+ """Read and process markdown report, return both markdown and html formats"""
832
+ report_path = os.path.join(workdir, 'report.md')
833
+
834
+ if not os.path.exists(report_path):
835
+ return "", "", "未找到报告文件 report.md"
836
+
837
+ try:
838
+ with open(report_path, 'r', encoding='utf-8') as f:
839
+ markdown_content = f.read()
840
+
841
+ # Uniformly use base64 method to process images
842
+ try:
843
+ processed_markdown = convert_markdown_images_to_base64(markdown_content, workdir)
844
+ except Exception as e:
845
+ logger.info(f"Base64 conversion failed, using file info display: {e}")
846
+ processed_markdown = convert_markdown_images_to_file_info(markdown_content, workdir)
847
+
848
+ # Check if non-local_mode, if so convert to HTML
849
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
850
+ if not local_mode:
851
+ try:
852
+ processed_html = convert_markdown_to_html(processed_markdown)
853
+ except Exception as e:
854
+ logger.info(f"HTML conversion failed, using markdown display: {e}")
855
+ processed_html = processed_markdown
856
+ else:
857
+ processed_html = processed_markdown
858
+
859
+ return processed_markdown, processed_html, ""
860
+ except Exception as e:
861
+ return "", "", f"读取报告文件失败: {str(e)}"
862
+
863
+
864
+ def list_resources_files(workdir: str) -> str:
865
+ """List files in resources folder"""
866
+ resources_path = os.path.join(workdir, 'resources')
867
+
868
+ if not os.path.exists(resources_path):
869
+ return "未找到 resources 文件夹"
870
+
871
+ try:
872
+ files = []
873
+ for root, dirs, filenames in os.walk(resources_path):
874
+ for filename in filenames:
875
+ rel_path = os.path.relpath(os.path.join(root, filename), workdir)
876
+ files.append(rel_path)
877
+
878
+ if files:
879
+ return "📁 资源文件列表:\n" + "\n".join(f"• {file}" for file in sorted(files))
880
+ else:
881
+ return "resources 文件夹为空"
882
+ except Exception as e:
883
+ return f"读取资源文件失败: {str(e)}"
884
+
885
+
886
+ def run_research_workflow_internal(
887
+ user_prompt: str,
888
+ uploaded_files,
889
+ urls_text: str,
890
+ user_id: str,
891
+ progress_callback=None
892
+ ) -> Tuple[str, str, str, str, str]:
893
+ """Internal research workflow execution function"""
894
+ try:
895
+ if progress_callback:
896
+ progress_callback(0.02, "验证输入参数...")
897
+
898
+ # Process files and URLs
899
+ file_paths = process_files(uploaded_files)
900
+ urls = process_urls_text(urls_text)
901
+
902
+ # Merge file paths and URLs
903
+ urls_or_files = file_paths + urls
904
+
905
+ if progress_callback:
906
+ progress_callback(0.05, "初始化工作环境...")
907
+
908
+ # Create new working directory
909
+ task_workdir = create_task_workdir(user_id)
910
+
911
+ user_prompt = user_prompt.strip() or "请深入分析和总结下列文档:"
912
+
913
+ if progress_callback:
914
+ progress_callback(0.10, "初始化AI客户端...")
915
+
916
+ # Initialize chat client
917
+ chat_client = OpenAIChat(
918
+ api_key=os.environ.get('OPENAI_API_KEY'),
919
+ base_url=os.environ.get('OPENAI_BASE_URL'),
920
+ model=os.environ.get('OPENAI_MODEL_ID'),
921
+ )
922
+
923
+ if progress_callback:
924
+ progress_callback(0.15, "创建研究工作流...")
925
+
926
+ # Create research workflow
927
+ research_workflow = ResearchWorkflowApp(
928
+ client=chat_client,
929
+ workdir=task_workdir,
930
+ )
931
+
932
+ if progress_callback:
933
+ progress_callback(0.20, "开始执行研究工作流...")
934
+
935
+ # Run workflow - this step takes most of the progress
936
+ result = research_workflow.run(
937
+ user_prompt=user_prompt,
938
+ urls_or_files=urls_or_files,
939
+ )
940
+
941
+ if progress_callback:
942
+ progress_callback(0.90, "处理研究报告...")
943
+
944
+ # Read markdown report
945
+ markdown_report, html_report, report_error = read_markdown_report(task_workdir)
946
+
947
+ if progress_callback:
948
+ progress_callback(0.95, "整理资源文件...")
949
+
950
+ # List resource files
951
+ resources_info = list_resources_files(task_workdir)
952
+
953
+ if progress_callback:
954
+ progress_callback(1.0, "任务完成!")
955
+
956
+ return result, task_workdir, markdown_report, html_report, resources_info
957
+
958
+ except Exception as e:
959
+ error_msg = f"❌ 执行过程中发生错误:{str(e)}"
960
+ return error_msg, "", "", "", ""
961
+
962
+
963
+ def run_research_workflow(
964
+ user_prompt: str,
965
+ uploaded_files,
966
+ urls_text: str,
967
+ request: gr.Request,
968
+ progress=gr.Progress()
969
+ ) -> Tuple[str, str, str, str, str]:
970
+ """Run research workflow (using Gradio built-in queue control)"""
971
+ try:
972
+ # Check LOCAL_MODE environment variable, default is true
973
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
974
+
975
+ if not local_mode:
976
+ # Check user authentication
977
+ is_authenticated, user_id_or_error = check_user_auth(request)
978
+ if not is_authenticated:
979
+ return f"❌ 认证失败:{user_id_or_error}", "", "", "", ""
980
+
981
+ user_id = user_id_or_error
982
+ else:
983
+ # Local mode, use default user ID with timestamp to avoid conflicts
984
+ user_id = f"local_user_{int(time.time() * 1000)}"
985
+
986
+ progress(0.01, desc="开始执行任务...")
987
+
988
+ # Mark user task start
989
+ user_status_manager.start_user_task(user_id)
990
+
991
+ # Create progress callback function
992
+ def progress_callback(value, desc):
993
+ # Map internal progress to 0.05-0.95 range
994
+ mapped_progress = 0.05 + (value * 0.9)
995
+ progress(mapped_progress, desc=desc)
996
+
997
+ try:
998
+ # Execute task directly, controlled by Gradio queue for concurrency
999
+ result = run_research_workflow_internal(
1000
+ user_prompt,
1001
+ uploaded_files,
1002
+ urls_text,
1003
+ user_id,
1004
+ progress_callback
1005
+ )
1006
+
1007
+ progress(1.0, desc="任务完成!")
1008
+ return result
1009
+
1010
+ except Exception as e:
1011
+ logger.info(f"Task execution exception - User: {user_id[:8]}***, Error: {str(e)}")
1012
+ error_msg = f"❌ 任务执行失败:{str(e)}"
1013
+ return error_msg, "", "", "", ""
1014
+ finally:
1015
+ # Ensure user status cleanup
1016
+ user_status_manager.finish_user_task(user_id)
1017
+
1018
+ except Exception as e:
1019
+ error_msg = f"❌ 系统错误:{str(e)}"
1020
+ return error_msg, "", "", "", ""
1021
+
1022
+
1023
+ def clear_workspace(request: gr.Request):
1024
+ """Clear workspace"""
1025
+ try:
1026
+ # Check LOCAL_MODE environment variable, default is true
1027
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
1028
+
1029
+ if not local_mode:
1030
+ # Check user authentication
1031
+ is_authenticated, user_id_or_error = check_user_auth(request)
1032
+ if not is_authenticated:
1033
+ return f"❌ 认证失败:{user_id_or_error}", "", ""
1034
+
1035
+ user_id = user_id_or_error
1036
+ else:
1037
+ # Local mode, use default user ID
1038
+ user_id = "local_user"
1039
+
1040
+ user_workdir = create_user_workdir(user_id)
1041
+
1042
+ if os.path.exists(user_workdir):
1043
+ shutil.rmtree(user_workdir)
1044
+ return "✅ 工作空间已清理", "", ""
1045
+ except Exception as e:
1046
+ return f"❌ 清理失败:{str(e)}", "", ""
1047
+
1048
+
1049
+ def get_session_file_path(user_id: str) -> str:
1050
+ """Get user-specific session file path"""
1051
+ user_workdir = create_user_workdir(user_id)
1052
+ return os.path.join(user_workdir, "session_data.json")
1053
+
1054
+
1055
+ def save_session_data(data, user_id: str):
1056
+ """Save session data to file"""
1057
+ try:
1058
+ session_file = get_session_file_path(user_id)
1059
+ os.makedirs(os.path.dirname(session_file), exist_ok=True)
1060
+ with open(session_file, 'w', encoding='utf-8') as f:
1061
+ json.dump(data, f, ensure_ascii=False, indent=2)
1062
+ except Exception as e:
1063
+ logger.info(f"Failed to save session data: {e}")
1064
+
1065
+
1066
+ def load_session_data(user_id: str):
1067
+ """Load session data from file"""
1068
+ try:
1069
+ session_file = get_session_file_path(user_id)
1070
+ if os.path.exists(session_file):
1071
+ with open(session_file, 'r', encoding='utf-8') as f:
1072
+ return json.load(f)
1073
+ except Exception as e:
1074
+ logger.info(f"Failed to load session data: {e}")
1075
+
1076
+ # Return default data
1077
+ return {
1078
+ "workdir": "",
1079
+ "result": "",
1080
+ "markdown": "",
1081
+ "resources": "",
1082
+ "user_prompt": "",
1083
+ "urls_text": "",
1084
+ "timestamp": ""
1085
+ }
1086
+
1087
+
1088
+ def get_user_status_html(request: gr.Request) -> str:
1089
+ # To be removed in future versions
1090
+ return ''
1091
+
1092
+
1093
+ def get_system_status_html() -> str:
1094
+ """Get system status HTML"""
1095
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
1096
+
1097
+ if local_mode:
1098
+ return "" # Local mode doesn't display system status info
1099
+
1100
+ system_status = user_status_manager.get_system_status()
1101
+
1102
+ status_html = f"""
1103
+ <div class="status-indicator status-info">
1104
+ 🖥️ 系统状态 | 活跃任务: {system_status['active_tasks']}/{system_status['max_concurrent']} | 可用槽位: {system_status['available_slots']}
1105
+ </div>
1106
+ """
1107
+
1108
+ if system_status['task_details']:
1109
+ status_html += "<div style='margin-top: 0.5rem; font-size: 0.9rem; color: #666;'>"
1110
+ status_html += "<strong>活跃任务详情:</strong><br>"
1111
+ for user_id, details in system_status['task_details'].items():
1112
+ masked_id = user_id[:8] + "***" if len(user_id) > 8 else user_id
1113
+ status_html += f"• {masked_id}: {details['status']} ({details['elapsed_time']:.1f}s)<br>"
1114
+ status_html += "</div>"
1115
+
1116
+ return status_html
1117
+
1118
+
1119
+ # Create Gradio interface
1120
+ def create_interface():
1121
+ with gr.Blocks(
1122
+ title="研究工作流应用 | Research Workflow App",
1123
+ theme=gr.themes.Soft(),
1124
+ css="""
1125
+ /* Responsive container settings */
1126
+ .gradio-container {
1127
+ max-width: none !important;
1128
+ width: 100% !important;
1129
+ padding: 0 1rem !important;
1130
+ }
1131
+
1132
+ /* Non-local_mode HTML report scrolling styles */
1133
+ .scrollable-html-report {
1134
+ height: 750px !important;
1135
+ overflow-y: auto !important;
1136
+ border: 1px solid var(--border-color-primary) !important;
1137
+ border-radius: 0.5rem !important;
1138
+ padding: 1rem !important;
1139
+ background: var(--background-fill-primary) !important;
1140
+ }
1141
+
1142
+ /* HTML report content area styles */
1143
+ #html-report {
1144
+ height: 750px !important;
1145
+ overflow-y: auto !important;
1146
+ }
1147
+
1148
+ /* HTML report scrolling in fullscreen mode */
1149
+ #fullscreen-html {
1150
+ height: calc(100vh - 1.2rem) !important;
1151
+ overflow-y: auto !important;
1152
+ }
1153
+
1154
+ /* HTML report scrollbar beautification */
1155
+ .scrollable-html-report::-webkit-scrollbar,
1156
+ #html-report::-webkit-scrollbar,
1157
+ #fullscreen-html::-webkit-scrollbar {
1158
+ width: 12px !important;
1159
+ }
1160
+
1161
+ .scrollable-html-report::-webkit-scrollbar-track,
1162
+ #html-report::-webkit-scrollbar-track,
1163
+ #fullscreen-html::-webkit-scrollbar-track {
1164
+ background: var(--background-fill-secondary) !important;
1165
+ border-radius: 6px !important;
1166
+ }
1167
+
1168
+ .scrollable-html-report::-webkit-scrollbar-thumb,
1169
+ #html-report::-webkit-scrollbar-thumb,
1170
+ #fullscreen-html::-webkit-scrollbar-thumb {
1171
+ background: var(--border-color-primary) !important;
1172
+ border-radius: 6px !important;
1173
+ }
1174
+
1175
+ .scrollable-html-report::-webkit-scrollbar-thumb:hover,
1176
+ #html-report::-webkit-scrollbar-thumb:hover,
1177
+ #fullscreen-html::-webkit-scrollbar-thumb:hover {
1178
+ background: var(--color-accent) !important;
1179
+ }
1180
+
1181
+ /* Ensure HTML content displays correctly within container */
1182
+ .scrollable-html-report .markdown-html-content {
1183
+ max-width: 100% !important;
1184
+ margin: 0 !important;
1185
+ padding: 0 !important;
1186
+ }
1187
+
1188
+ /* Responsive adaptation */
1189
+ @media (max-width: 768px) {
1190
+ .scrollable-html-report,
1191
+ #html-report {
1192
+ height: 600px !important;
1193
+ padding: 0.75rem !important;
1194
+ }
1195
+
1196
+ #fullscreen-html {
1197
+ height: calc(100vh - 1rem) !important;
1198
+ }
1199
+
1200
+ #fullscreen-modal {
1201
+ padding: 0.1rem !important;
1202
+ }
1203
+
1204
+ #fullscreen-modal .gr-column {
1205
+ padding: 0.1rem !important;
1206
+ height: calc(100vh - 0.2rem) !important;
1207
+ }
1208
+
1209
+ #fullscreen-markdown {
1210
+ height: calc(100vh - 1rem) !important;
1211
+ }
1212
+
1213
+ #fullscreen-btn {
1214
+ min-width: 20px !important;
1215
+ width: 20px !important;
1216
+ height: 20px !important;
1217
+ font-size: 0.8rem !important;
1218
+ }
1219
+
1220
+ #close-btn {
1221
+ min-width: 18px !important;
1222
+ width: 18px !important;
1223
+ height: 18px !important;
1224
+ font-size: 0.8rem !important;
1225
+ }
1226
+ }
1227
+
1228
+ @media (max-width: 480px) {
1229
+ .scrollable-html-report,
1230
+ #html-report {
1231
+ height: 500px !important;
1232
+ padding: 0.5rem !important;
1233
+ }
1234
+
1235
+ #fullscreen-html {
1236
+ height: calc(100vh - 0.8rem) !important;
1237
+ }
1238
+
1239
+ #fullscreen-modal {
1240
+ padding: 0.05rem !important;
1241
+ }
1242
+
1243
+ #fullscreen-modal .gr-column {
1244
+ padding: 0.05rem !important;
1245
+ height: calc(100vh - 0.1rem) !important;
1246
+ }
1247
+
1248
+ #fullscreen-markdown {
1249
+ height: calc(100vh - 0.8rem) !important;
1250
+ }
1251
+
1252
+ #fullscreen-btn {
1253
+ min-width: 18px !important;
1254
+ width: 18px !important;
1255
+ height: 18px !important;
1256
+ font-size: 0.75rem !important;
1257
+ }
1258
+
1259
+ #close-btn {
1260
+ min-width: 16px !important;
1261
+ width: 16px !important;
1262
+ height: 16px !important;
1263
+ font-size: 0.75rem !important;
1264
+ }
1265
+ }
1266
+
1267
+ /* Fullscreen modal styles */
1268
+ #fullscreen-modal {
1269
+ position: fixed !important;
1270
+ top: 0 !important;
1271
+ left: 0 !important;
1272
+ width: 100vw !important;
1273
+ height: 100vh !important;
1274
+ background: var(--background-fill-primary) !important;
1275
+ z-index: 9999 !important;
1276
+ padding: 0.15rem !important;
1277
+ box-sizing: border-box !important;
1278
+ }
1279
+
1280
+ #fullscreen-modal .gr-column {
1281
+ background: var(--background-fill-primary) !important;
1282
+ border: 1px solid var(--border-color-primary) !important;
1283
+ border-radius: 0.5rem !important;
1284
+ padding: 0.15rem !important;
1285
+ height: calc(100vh - 0.3rem) !important;
1286
+ overflow: hidden !important;
1287
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;
1288
+ }
1289
+
1290
+ #fullscreen-markdown {
1291
+ height: calc(100vh - 1.2rem) !important;
1292
+ overflow-y: auto !important;
1293
+ background: var(--background-fill-primary) !important;
1294
+ color: var(--body-text-color) !important;
1295
+ }
1296
+
1297
+ #fullscreen-html {
1298
+ height: calc(100vh - 1.2rem) !important;
1299
+ overflow-y: auto !important;
1300
+ }
1301
+
1302
+ #fullscreen-btn {
1303
+ min-width: 24px !important;
1304
+ width: 24px !important;
1305
+ height: 24px !important;
1306
+ padding: 0 !important;
1307
+ display: flex !important;
1308
+ align-items: center !important;
1309
+ justify-content: center !important;
1310
+ font-size: 0.9rem !important;
1311
+ margin-bottom: 0.25rem !important;
1312
+ border-radius: 4px !important;
1313
+ }
1314
+
1315
+ #close-btn {
1316
+ min-width: 22px !important;
1317
+ width: 22px !important;
1318
+ height: 22px !important;
1319
+ padding: 0 !important;
1320
+ display: flex !important;
1321
+ align-items: center !important;
1322
+ justify-content: center !important;
1323
+ font-size: 0.9rem !important;
1324
+ margin-left: auto !important;
1325
+ background: var(--button-secondary-background-fill) !important;
1326
+ color: var(--button-secondary-text-color) !important;
1327
+ border: 1px solid var(--border-color-primary) !important;
1328
+ border-radius: 4px !important;
1329
+ }
1330
+
1331
+ #close-btn:hover {
1332
+ background: var(--button-secondary-background-fill-hover) !important;
1333
+ }
1334
+
1335
+ /* Fullscreen mode title styles */
1336
+ #fullscreen-modal h3 {
1337
+ color: var(--body-text-color) !important;
1338
+ margin: 0 !important;
1339
+ flex: 1 !important;
1340
+ font-size: 1.1rem !important;
1341
+ line-height: 1.2 !important;
1342
+ padding: 0 !important;
1343
+ }
1344
+
1345
+ /* Fullscreen mode title row styles */
1346
+ #fullscreen-modal .gr-row {
1347
+ margin-bottom: 0.15rem !important;
1348
+ align-items: center !important;
1349
+ padding: 0 !important;
1350
+ min-height: auto !important;
1351
+ }
1352
+
1353
+ /* Fullscreen mode markdown style optimization */
1354
+ #fullscreen-markdown .gr-markdown {
1355
+ font-size: 1.1rem !important;
1356
+ line-height: 1.7 !important;
1357
+ color: var(--body-text-color) !important;
1358
+ background: var(--background-fill-primary) !important;
1359
+ }
1360
+
1361
+ #fullscreen-markdown .gr-markdown * {
1362
+ color: inherit !important;
1363
+ }
1364
+
1365
+ #fullscreen-markdown h1 {
1366
+ font-size: 2.2rem !important;
1367
+ margin-bottom: 1.5rem !important;
1368
+ color: var(--body-text-color) !important;
1369
+ border-bottom: 2px solid var(--border-color-primary) !important;
1370
+ padding-bottom: 0.5rem !important;
1371
+ }
1372
+
1373
+ #fullscreen-markdown h2 {
1374
+ font-size: 1.8rem !important;
1375
+ margin-bottom: 1.2rem !important;
1376
+ color: var(--body-text-color) !important;
1377
+ border-bottom: 1px solid var(--border-color-primary) !important;
1378
+ padding-bottom: 0.3rem !important;
1379
+ }
1380
+
1381
+ #fullscreen-markdown h3 {
1382
+ font-size: 1.5rem !important;
1383
+ margin-bottom: 1rem !important;
1384
+ color: var(--body-text-color) !important;
1385
+ }
1386
+
1387
+ #fullscreen-markdown h4,
1388
+ #fullscreen-markdown h5,
1389
+ #fullscreen-markdown h6 {
1390
+ color: var(--body-text-color) !important;
1391
+ margin-bottom: 0.8rem !important;
1392
+ }
1393
+
1394
+ #fullscreen-markdown p {
1395
+ color: var(--body-text-color) !important;
1396
+ margin-bottom: 1rem !important;
1397
+ }
1398
+
1399
+ #fullscreen-markdown ul,
1400
+ #fullscreen-markdown ol {
1401
+ color: var(--body-text-color) !important;
1402
+ margin-bottom: 1rem !important;
1403
+ padding-left: 1.5rem !important;
1404
+ }
1405
+
1406
+ #fullscreen-markdown li {
1407
+ color: var(--body-text-color) !important;
1408
+ margin-bottom: 0.3rem !important;
1409
+ }
1410
+
1411
+ #fullscreen-markdown strong,
1412
+ #fullscreen-markdown b {
1413
+ color: var(--body-text-color) !important;
1414
+ font-weight: 600 !important;
1415
+ }
1416
+
1417
+ #fullscreen-markdown em,
1418
+ #fullscreen-markdown i {
1419
+ color: var(--body-text-color) !important;
1420
+ }
1421
+
1422
+ #fullscreen-markdown a {
1423
+ color: var(--link-text-color) !important;
1424
+ text-decoration: none !important;
1425
+ }
1426
+
1427
+ #fullscreen-markdown a:hover {
1428
+ color: var(--link-text-color-hover) !important;
1429
+ text-decoration: underline !important;
1430
+ }
1431
+
1432
+ #fullscreen-markdown blockquote {
1433
+ background: var(--background-fill-secondary) !important;
1434
+ border-left: 4px solid var(--color-accent) !important;
1435
+ padding: 1rem !important;
1436
+ margin: 1rem 0 !important;
1437
+ color: var(--body-text-color) !important;
1438
+ border-radius: 0 0.5rem 0.5rem 0 !important;
1439
+ }
1440
+
1441
+ #fullscreen-markdown img {
1442
+ max-width: 100% !important;
1443
+ height: auto !important;
1444
+ border-radius: 0.5rem !important;
1445
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
1446
+ margin: 1rem 0 !important;
1447
+ }
1448
+
1449
+ #fullscreen-markdown pre {
1450
+ background: var(--background-fill-secondary) !important;
1451
+ color: var(--body-text-color) !important;
1452
+ padding: 1rem !important;
1453
+ border-radius: 0.5rem !important;
1454
+ overflow-x: auto !important;
1455
+ font-size: 0.95rem !important;
1456
+ border: 1px solid var(--border-color-primary) !important;
1457
+ margin: 1rem 0 !important;
1458
+ }
1459
+
1460
+ #fullscreen-markdown code {
1461
+ background: var(--background-fill-secondary) !important;
1462
+ color: var(--body-text-color) !important;
1463
+ padding: 0.2rem 0.4rem !important;
1464
+ border-radius: 0.25rem !important;
1465
+ font-size: 0.9rem !important;
1466
+ border: 1px solid var(--border-color-primary) !important;
1467
+ }
1468
+
1469
+ #fullscreen-markdown pre code {
1470
+ background: transparent !important;
1471
+ padding: 0 !important;
1472
+ border: none !important;
1473
+ }
1474
+
1475
+ #fullscreen-markdown table {
1476
+ width: 100% !important;
1477
+ border-collapse: collapse !important;
1478
+ margin: 1rem 0 !important;
1479
+ background: var(--background-fill-primary) !important;
1480
+ }
1481
+
1482
+ #fullscreen-markdown th,
1483
+ #fullscreen-markdown td {
1484
+ padding: 0.75rem !important;
1485
+ border: 1px solid var(--border-color-primary) !important;
1486
+ color: var(--body-text-color) !important;
1487
+ }
1488
+
1489
+ #fullscreen-markdown th {
1490
+ background: var(--background-fill-secondary) !important;
1491
+ font-weight: 600 !important;
1492
+ }
1493
+
1494
+ #fullscreen-markdown tr:nth-child(even) {
1495
+ background: var(--background-fill-secondary) !important;
1496
+ }
1497
+
1498
+ #fullscreen-markdown hr {
1499
+ border: none !important;
1500
+ height: 1px !important;
1501
+ background: var(--border-color-primary) !important;
1502
+ margin: 2rem 0 !important;
1503
+ }
1504
+
1505
+ /* Fullscreen mode scrollbar styles */
1506
+ #fullscreen-markdown::-webkit-scrollbar {
1507
+ width: 12px !important;
1508
+ }
1509
+
1510
+ #fullscreen-markdown::-webkit-scrollbar-track {
1511
+ background: var(--background-fill-secondary) !important;
1512
+ border-radius: 6px !important;
1513
+ }
1514
+
1515
+ #fullscreen-markdown::-webkit-scrollbar-thumb {
1516
+ background: var(--border-color-primary) !important;
1517
+ border-radius: 6px !important;
1518
+ }
1519
+
1520
+ #fullscreen-markdown::-webkit-scrollbar-thumb:hover {
1521
+ background: var(--color-accent) !important;
1522
+ }
1523
+
1524
+ /* Dark theme special adaptation */
1525
+ @media (prefers-color-scheme: dark) {
1526
+ #fullscreen-modal {
1527
+ background: var(--background-fill-primary) !important;
1528
+ }
1529
+
1530
+ #fullscreen-markdown img {
1531
+ box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1) !important;
1532
+ }
1533
+ }
1534
+
1535
+ .dark #fullscreen-modal {
1536
+ background: var(--background-fill-primary) !important;
1537
+ }
1538
+
1539
+ .dark #fullscreen-markdown img {
1540
+ box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1) !important;
1541
+ }
1542
+
1543
+ /* Large screen adaptation */
1544
+ @media (min-width: 1400px) {
1545
+ .gradio-container {
1546
+ max-width: 1600px !important;
1547
+ margin: 0 auto !important;
1548
+ padding: 0 2rem !important;
1549
+ }
1550
+ }
1551
+
1552
+ @media (min-width: 1800px) {
1553
+ .gradio-container {
1554
+ max-width: 1800px !important;
1555
+ padding: 0 3rem !important;
1556
+ }
1557
+ }
1558
+
1559
+ /* Main title styles */
1560
+ .main-header {
1561
+ text-align: center;
1562
+ margin-bottom: 2rem;
1563
+ padding: 1rem 0;
1564
+ }
1565
+
1566
+ .main-header h1 {
1567
+ font-size: clamp(1.8rem, 4vw, 3rem);
1568
+ margin-bottom: 0.5rem;
1569
+ }
1570
+
1571
+ .main-header h2 {
1572
+ font-size: clamp(1.2rem, 2.5vw, 2rem);
1573
+ margin-bottom: 0.5rem;
1574
+ color: #6b7280;
1575
+ }
1576
+
1577
+ /* Description text styles */
1578
+ .description {
1579
+ font-size: clamp(1rem, 1.8vw, 1.2rem);
1580
+ color: #6b7280;
1581
+ margin-bottom: 0.5rem;
1582
+ font-weight: 500;
1583
+ line-height: 1.5;
1584
+ }
1585
+
1586
+ /* Powered by styles */
1587
+ .powered-by {
1588
+ font-size: clamp(0.85rem, 1.2vw, 1rem);
1589
+ color: #9ca3af;
1590
+ margin-top: 0.25rem;
1591
+ font-weight: 400;
1592
+ }
1593
+
1594
+ .powered-by a {
1595
+ color: #06b6d4;
1596
+ text-decoration: none;
1597
+ font-weight: normal;
1598
+ transition: color 0.2s ease;
1599
+ }
1600
+
1601
+ .powered-by a:hover {
1602
+ color: #0891b2;
1603
+ text-decoration: underline;
1604
+ }
1605
+
1606
+ /* Dark theme adaptation */
1607
+ @media (prefers-color-scheme: dark) {
1608
+ .description {
1609
+ color: #9ca3af;
1610
+ }
1611
+
1612
+ .powered-by {
1613
+ color: #6b7280;
1614
+ }
1615
+
1616
+ .powered-by a {
1617
+ color: #22d3ee;
1618
+ font-weight: normal;
1619
+ }
1620
+
1621
+ .powered-by a:hover {
1622
+ color: #67e8f9;
1623
+ }
1624
+ }
1625
+
1626
+ .dark .description {
1627
+ color: #9ca3af;
1628
+ }
1629
+
1630
+ .dark .powered-by {
1631
+ color: #6b7280;
1632
+ }
1633
+
1634
+ .dark .powered-by a {
1635
+ color: #22d3ee;
1636
+ font-weight: normal;
1637
+ }
1638
+
1639
+ .dark .powered-by a:hover {
1640
+ color: #67e8f9;
1641
+ }
1642
+
1643
+ /* Section headers */
1644
+ .section-header {
1645
+ color: #2563eb;
1646
+ font-weight: 600;
1647
+ margin: 1rem 0 0.5rem 0;
1648
+ font-size: clamp(1rem, 1.8vw, 1.3rem);
1649
+ }
1650
+
1651
+ /* Status indicators */
1652
+ .status-indicator {
1653
+ padding: 0.75rem 1rem;
1654
+ border-radius: 0.5rem;
1655
+ margin: 0.5rem 0;
1656
+ font-size: clamp(0.85rem, 1.2vw, 1rem);
1657
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1658
+ }
1659
+
1660
+ .status-success {
1661
+ background-color: #dcfce7;
1662
+ color: #166534;
1663
+ border: 1px solid #bbf7d0;
1664
+ }
1665
+
1666
+ .status-info {
1667
+ background-color: #dbeafe;
1668
+ color: #1e40af;
1669
+ border: 1px solid #bfdbfe;
1670
+ }
1671
+
1672
+ /* Input component optimization */
1673
+ .gr-textbox, .gr-file {
1674
+ font-size: clamp(0.85rem, 1.1vw, 1rem) !important;
1675
+ }
1676
+
1677
+ /* Button style optimization */
1678
+ .gr-button {
1679
+ font-size: clamp(0.9rem, 1.2vw, 1.1rem) !important;
1680
+ padding: 0.75rem 1.5rem !important;
1681
+ border-radius: 0.5rem !important;
1682
+ font-weight: 500 !important;
1683
+ }
1684
+
1685
+ /* Tab label optimization */
1686
+ .gr-tab-nav {
1687
+ font-size: clamp(0.85rem, 1.1vw, 1rem) !important;
1688
+ }
1689
+
1690
+ /* Output area optimization */
1691
+ .gr-markdown {
1692
+ font-size: clamp(0.85rem, 1vw, 1rem) !important;
1693
+ line-height: 1.6 !important;
1694
+ }
1695
+
1696
+ /* Instructions area */
1697
+ .instructions {
1698
+ margin-top: 2rem;
1699
+ padding: 1.5rem;
1700
+ background-color: var(--background-fill-secondary);
1701
+ border-radius: 0.75rem;
1702
+ border: 1px solid var(--border-color-primary);
1703
+ }
1704
+
1705
+ .instructions h4 {
1706
+ font-size: clamp(1rem, 1.5vw, 1.2rem);
1707
+ margin-bottom: 1rem;
1708
+ color: var(--body-text-color);
1709
+ }
1710
+
1711
+ .instructions-subtitle {
1712
+ color: var(--body-text-color);
1713
+ margin-bottom: 0.75rem;
1714
+ font-size: 1.1rem;
1715
+ font-weight: 600;
1716
+ }
1717
+
1718
+ .instructions-list {
1719
+ font-size: clamp(0.85rem, 1.1vw, 1rem);
1720
+ line-height: 1.6;
1721
+ margin: 0;
1722
+ padding-left: 1.2rem;
1723
+ color: var(--body-text-color);
1724
+ }
1725
+
1726
+ .instructions-list li {
1727
+ margin-bottom: 0.5rem;
1728
+ color: var(--body-text-color);
1729
+ }
1730
+
1731
+ .instructions-list strong {
1732
+ color: var(--body-text-color);
1733
+ font-weight: 600;
1734
+ }
1735
+
1736
+ /* Dark theme adaptation */
1737
+ @media (prefers-color-scheme: dark) {
1738
+ .instructions {
1739
+ background-color: rgba(255, 255, 255, 0.05);
1740
+ border-color: rgba(255, 255, 255, 0.1);
1741
+ }
1742
+
1743
+ .instructions h4,
1744
+ .instructions-subtitle,
1745
+ .instructions-list,
1746
+ .instructions-list li,
1747
+ .instructions-list strong {
1748
+ color: rgba(255, 255, 255, 0.9) !important;
1749
+ }
1750
+ }
1751
+
1752
+ /* Gradio dark theme adaptation */
1753
+ .dark .instructions {
1754
+ background-color: rgba(255, 255, 255, 0.05);
1755
+ border-color: rgba(255, 255, 255, 0.1);
1756
+ }
1757
+
1758
+ .dark .instructions h4,
1759
+ .dark .instructions-subtitle,
1760
+ .dark .instructions-list,
1761
+ .dark .instructions-list li,
1762
+ .dark .instructions-list strong {
1763
+ color: rgba(255, 255, 255, 0.9) !important;
1764
+ }
1765
+
1766
+ /* Responsive column layout */
1767
+ @media (max-width: 768px) {
1768
+ .gr-row {
1769
+ flex-direction: column !important;
1770
+ }
1771
+
1772
+ .gr-column {
1773
+ width: 100% !important;
1774
+ margin-bottom: 1rem !important;
1775
+ }
1776
+ }
1777
+
1778
+ /* Column width optimization for large screens */
1779
+ @media (min-width: 1400px) {
1780
+ .input-column {
1781
+ min-width: 500px !important;
1782
+ }
1783
+
1784
+ .output-column {
1785
+ min-width: 700px !important;
1786
+ }
1787
+ }
1788
+
1789
+ /* Scrollbar beautification */
1790
+ .gr-textbox textarea::-webkit-scrollbar,
1791
+ .gr-markdown::-webkit-scrollbar {
1792
+ width: 8px;
1793
+ }
1794
+
1795
+ .gr-textbox textarea::-webkit-scrollbar-track,
1796
+ .gr-markdown::-webkit-scrollbar-track {
1797
+ background: #f1f5f9;
1798
+ border-radius: 4px;
1799
+ }
1800
+
1801
+ .gr-textbox textarea::-webkit-scrollbar-thumb,
1802
+ .gr-markdown::-webkit-scrollbar-thumb {
1803
+ background: #cbd5e1;
1804
+ border-radius: 4px;
1805
+ }
1806
+
1807
+ .gr-textbox textarea::-webkit-scrollbar-thumb:hover,
1808
+ .gr-markdown::-webkit-scrollbar-thumb:hover {
1809
+ background: #94a3b8;
1810
+ }
1811
+ """
1812
+ ) as demo:
1813
+
1814
+ # State management - for maintaining data persistence
1815
+ current_workdir = gr.State("")
1816
+ current_result = gr.State("")
1817
+ current_markdown = gr.State("")
1818
+ current_html = gr.State("")
1819
+ current_resources = gr.State("")
1820
+ current_user_prompt = gr.State("")
1821
+ current_urls_text = gr.State("")
1822
+
1823
+ gr.HTML("""
1824
+ <div class="main-header">
1825
+ <h1>🔬 文档深度研究</h1>
1826
+ <h2>Doc Research Workflow</h2>
1827
+ <p class="description">Your Daily Paper Copilot - URLs or Files IN, Multimodal Report OUT</p>
1828
+ <p class="powered-by">Powered by <a href="https://github.com/modelscope/ms-agent" target="_blank" rel="noopener noreferrer">MS-Agent</a> | <a href="https://github.com/modelscope/ms-agent/blob/main/projects/doc_research/README.md" target="_blank" rel="noopener noreferrer">Readme</a> </p>
1829
+ </div>
1830
+ """)
1831
+
1832
+ # 用户状态显示
1833
+ user_status = gr.HTML()
1834
+
1835
+ # 系统状态显示
1836
+ system_status = gr.HTML()
1837
+
1838
+ with gr.Row():
1839
+ with gr.Column(scale=2, elem_classes=["input-column"]):
1840
+ gr.HTML('<h3 class="section-header">📝 输入区域 | Input Area</h3>')
1841
+
1842
+ # 用户提示输入
1843
+ user_prompt = gr.Textbox(
1844
+ label="用户提示 | User Prompt",
1845
+ placeholder="请输入您的研究问题或任务描述(可为空)...\nPlease enter your research question or task description (Optional)...",
1846
+ lines=4,
1847
+ max_lines=8
1848
+ )
1849
+
1850
+ with gr.Row():
1851
+ with gr.Column():
1852
+ # 文件上传
1853
+ uploaded_files = gr.File(
1854
+ label="上传文件 | Upload Files",
1855
+ file_count="multiple",
1856
+ file_types=None,
1857
+ interactive=True,
1858
+ height=120
1859
+ )
1860
+
1861
+ with gr.Column():
1862
+ # URLs输入
1863
+ urls_text = gr.Textbox(
1864
+ label="URLs输入 | URLs Input",
1865
+ placeholder="请输入URLs,每行一个...\nEnter URLs, one per line...\n\nhttps://example1.com\nhttps://example2.com",
1866
+ lines=6,
1867
+ max_lines=10
1868
+ )
1869
+
1870
+ # 运行按钮
1871
+ run_btn = gr.Button(
1872
+ "🚀 开始研究 | Start Research",
1873
+ variant="primary",
1874
+ size="lg"
1875
+ )
1876
+
1877
+ # 清理按钮
1878
+ clear_btn = gr.Button(
1879
+ "🧹 清理工作空间 | Clear Workspace",
1880
+ variant="secondary"
1881
+ )
1882
+
1883
+ # 恢复按钮
1884
+ restore_btn = gr.Button(
1885
+ "🔄 重载最近结果 | Reload Latest Results",
1886
+ variant="secondary"
1887
+ )
1888
+
1889
+ # 会话状态指示器
1890
+ session_status = gr.HTML()
1891
+
1892
+ # 刷新系统状态按钮
1893
+ refresh_status_btn = gr.Button(
1894
+ "🔄 刷新系统状态 | Refresh System Status",
1895
+ variant="secondary",
1896
+ size="sm"
1897
+ )
1898
+
1899
+ with gr.Column(scale=3, elem_classes=["output-column"]):
1900
+ gr.HTML('<h3 class="section-header">📊 输出区域 | Output Area</h3>')
1901
+
1902
+ with gr.Tabs():
1903
+ with gr.TabItem("📋 执行结果 | Results"):
1904
+ # 结果显示
1905
+ result_output = gr.Textbox(
1906
+ label="执行结果 | Execution Results",
1907
+ lines=26,
1908
+ max_lines=30,
1909
+ interactive=False,
1910
+ show_copy_button=True
1911
+ )
1912
+
1913
+ # 工作目录显示
1914
+ workdir_output = gr.Textbox(
1915
+ label="工作目录 | Working Directory",
1916
+ lines=2,
1917
+ interactive=False,
1918
+ show_copy_button=True
1919
+ )
1920
+
1921
+ with gr.TabItem("📄 研究报告 | Research Report"):
1922
+ # 检查是否为非local_mode来决定显示格式
1923
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
1924
+
1925
+ if local_mode:
1926
+ # Local模式:显示Markdown
1927
+ with gr.Row():
1928
+ with gr.Column(scale=10):
1929
+ # Markdown报告显示
1930
+ markdown_output = gr.Markdown(
1931
+ label="Markdown报告 | Markdown Report",
1932
+ height=750
1933
+ )
1934
+ with gr.Column(scale=1, min_width=30):
1935
+ # 全屏按钮
1936
+ fullscreen_btn = gr.Button(
1937
+ "⛶",
1938
+ size="sm",
1939
+ variant="secondary",
1940
+ elem_id="fullscreen-btn"
1941
+ )
1942
+
1943
+ # 全屏模态框
1944
+ with gr.Row(visible=False, elem_id="fullscreen-modal") as fullscreen_modal:
1945
+ with gr.Column():
1946
+ with gr.Row():
1947
+ gr.HTML('<h3 style="margin: 0; flex: 1;">📄 研究报告 - 全屏模式</h3>')
1948
+ close_btn = gr.Button(
1949
+ "✕",
1950
+ size="sm",
1951
+ variant="secondary",
1952
+ elem_id="close-btn"
1953
+ )
1954
+ fullscreen_markdown = gr.Markdown(
1955
+ height=700,
1956
+ elem_id="fullscreen-markdown"
1957
+ )
1958
+
1959
+ # 为本地模式创建空的HTML组件(保持兼容性)
1960
+ html_output = gr.HTML(visible=False)
1961
+ fullscreen_html = gr.HTML(visible=False)
1962
+ else:
1963
+ # 非Local模式:显示HTML
1964
+ with gr.Row():
1965
+ with gr.Column(scale=10):
1966
+ # HTML报告显示
1967
+ html_output = gr.HTML(
1968
+ label="研究报告 | Research Report",
1969
+ value="",
1970
+ elem_id="html-report",
1971
+ elem_classes=["scrollable-html-report"]
1972
+ )
1973
+ with gr.Column(scale=1, min_width=30):
1974
+ # 全屏按钮
1975
+ fullscreen_btn = gr.Button(
1976
+ "⛶",
1977
+ size="sm",
1978
+ variant="secondary",
1979
+ elem_id="fullscreen-btn"
1980
+ )
1981
+
1982
+ # 全屏模态框
1983
+ with gr.Row(visible=False, elem_id="fullscreen-modal") as fullscreen_modal:
1984
+ with gr.Column():
1985
+ with gr.Row():
1986
+ gr.HTML('<h3 style="margin: 0; flex: 1;">📄 研究报告 - 全屏模式</h3>')
1987
+ close_btn = gr.Button(
1988
+ "✕",
1989
+ size="sm",
1990
+ variant="secondary",
1991
+ elem_id="close-btn"
1992
+ )
1993
+ fullscreen_html = gr.HTML(
1994
+ value="",
1995
+ elem_id="fullscreen-html",
1996
+ elem_classes=["scrollable-html-report"]
1997
+ )
1998
+
1999
+ # 为非local模式创建空的markdown组件(保持兼容性)
2000
+ markdown_output = gr.Markdown(visible=False)
2001
+ fullscreen_markdown = gr.Markdown(visible=False)
2002
+
2003
+ with gr.TabItem("📁 资源文件 | Resources"):
2004
+ # 资源文件列表
2005
+ resources_output = gr.Textbox(
2006
+ label="资源文件信息 | Resources Info",
2007
+ lines=30,
2008
+ max_lines=50,
2009
+ interactive=False,
2010
+ show_copy_button=True
2011
+ )
2012
+
2013
+ # 使用说明
2014
+ gr.HTML("""
2015
+ <div class="instructions">
2016
+ <h4>📖 使用说明 | Instructions</h4>
2017
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1rem;">
2018
+ <div>
2019
+ <h5 class="instructions-subtitle">🇨🇳 中文说明</h5>
2020
+ <ul class="instructions-list">
2021
+ <li><strong>用户提示:</strong>描述您的研究目标或问题,支持详细的任务描述</li>
2022
+ <li><strong>文件上传:</strong>支持多文件上传,默认支持PDF格式</li>
2023
+ <li><strong>URLs输入:</strong>每行输入一个URL,支持网页、文档、论文等链接</li>
2024
+ <li><strong>工作目录:</strong>每次运行都会创建新的临时工作目录,便于管理结果</li>
2025
+ <li><strong>会话保存:</strong>自动保存执行结果,支持页面刷新后重载数据</li>
2026
+ <li><strong>用户隔离:</strong>每个用户拥有独立的工作空间和会话数据</li>
2027
+ </ul>
2028
+ </div>
2029
+ <div>
2030
+ <h5 class="instructions-subtitle">🇺🇸 English Instructions</h5>
2031
+ <ul class="instructions-list">
2032
+ <li><strong>User Prompt:</strong> Describe your research goals or questions with detailed task descriptions</li>
2033
+ <li><strong>File Upload:</strong> Support multiple file uploads, default support for PDF format</li>
2034
+ <li><strong>URLs Input:</strong> Enter one URL per line, supports web pages, documents, papers, etc.</li>
2035
+ <li><strong>Working Directory:</strong> A new temporary working directory is created for each run for better result management</li>
2036
+ <li><strong>Session Save:</strong> Automatically save execution results, support data reload after page refresh</li>
2037
+ <li><strong>User Isolation:</strong> Each user has independent workspace and session data</li>
2038
+ </ul>
2039
+ </div>
2040
+ </div>
2041
+ </div>
2042
+ """)
2043
+
2044
+ # Page initialization function on load
2045
+ def initialize_page(request: gr.Request):
2046
+ """Initialize user status and session data when page loads"""
2047
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2048
+
2049
+ # Get user status HTML
2050
+ user_status_html = get_user_status_html(request)
2051
+
2052
+ # Determine user ID
2053
+ if local_mode:
2054
+ user_id = "local_user"
2055
+ else:
2056
+ is_authenticated, user_id_or_error = check_user_auth(request)
2057
+ if not is_authenticated:
2058
+ return (
2059
+ user_status_html,
2060
+ "", "", "", "", "", "", # Interface display (6 items)
2061
+ "", "", "", "", "", "", # State saving (6 items)
2062
+ "", "", # Input state saving (2 items)
2063
+ """<div class="status-indicator status-info">📊 会话状态: 游客模式(请登录后使用)</div>""", # Session status
2064
+ get_system_status_html() # System status
2065
+ )
2066
+ user_id = user_id_or_error
2067
+
2068
+ # Load session data
2069
+ session_data = load_session_data(user_id)
2070
+
2071
+ # Generate session status HTML
2072
+ if local_mode:
2073
+ session_status_html = "" # Local mode doesn't display session status
2074
+ else:
2075
+ session_status_html = f"""
2076
+ <div class="status-indicator status-info">
2077
+ 📊 会话状态: {'已加载历史数据' if any(session_data.values()) else '新会话'}
2078
+ {f'| 最后更新: {session_data.get("timestamp", "未知")}' if session_data.get("timestamp") else ''}
2079
+ </div>
2080
+ """ if any(session_data.values()) else """
2081
+ <div class="status-indicator status-info">
2082
+ 📊 会话状态: 新会话
2083
+ </div>
2084
+ """
2085
+
2086
+ return (
2087
+ user_status_html,
2088
+ session_data.get("user_prompt", ""),
2089
+ session_data.get("urls_text", ""),
2090
+ session_data.get("result", ""),
2091
+ session_data.get("workdir", ""),
2092
+ session_data.get("markdown", ""),
2093
+ session_data.get("html", ""),
2094
+ session_data.get("resources", ""),
2095
+ session_data.get("workdir", ""),
2096
+ session_data.get("result", ""),
2097
+ session_data.get("markdown", ""),
2098
+ session_data.get("html", ""),
2099
+ session_data.get("resources", ""),
2100
+ session_data.get("user_prompt", ""),
2101
+ session_data.get("urls_text", ""),
2102
+ session_status_html,
2103
+ get_system_status_html() # System status
2104
+ )
2105
+
2106
+ # Fullscreen functionality functions
2107
+ def toggle_fullscreen(markdown_content, html_content):
2108
+ """Toggle fullscreen display"""
2109
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2110
+ if local_mode:
2111
+ return gr.update(visible=True), markdown_content, ""
2112
+ else:
2113
+ return gr.update(visible=True), "", html_content
2114
+
2115
+ def close_fullscreen():
2116
+ """Close fullscreen display"""
2117
+ return gr.update(visible=False), "", ""
2118
+
2119
+ # State-saving wrapper function
2120
+ def run_research_workflow_with_state(
2121
+ user_prompt_val, uploaded_files_val, urls_text_val,
2122
+ current_workdir_val, current_result_val, current_markdown_val, current_html_val, current_resources_val,
2123
+ current_user_prompt_val, current_urls_text_val,
2124
+ request: gr.Request
2125
+ ):
2126
+ result, workdir, markdown, html, resources = run_research_workflow(
2127
+ user_prompt_val, uploaded_files_val, urls_text_val, request
2128
+ )
2129
+
2130
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2131
+
2132
+ # Determine user ID
2133
+ if local_mode:
2134
+ user_id = "local_user"
2135
+ else:
2136
+ is_authenticated, user_id_or_error = check_user_auth(request)
2137
+ if not is_authenticated:
2138
+ status_html = f"""
2139
+ <div class="status-indicator status-info">
2140
+ 🚫 游客模式 - {user_id_or_error}
2141
+ </div>
2142
+ """
2143
+ return (
2144
+ result, workdir, markdown, html, resources,
2145
+ workdir, result, markdown, html, resources,
2146
+ user_prompt_val, urls_text_val,
2147
+ status_html,
2148
+ get_system_status_html(),
2149
+ get_user_status_html(request)
2150
+ )
2151
+ user_id = user_id_or_error
2152
+
2153
+ # Save session data
2154
+ session_data = {
2155
+ "workdir": workdir,
2156
+ "result": result,
2157
+ "markdown": markdown,
2158
+ "html": html,
2159
+ "resources": resources,
2160
+ "user_prompt": user_prompt_val,
2161
+ "urls_text": urls_text_val,
2162
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2163
+ }
2164
+ save_session_data(session_data, user_id)
2165
+
2166
+ # Update session status indicator
2167
+ if local_mode:
2168
+ status_html = "" # Local mode doesn't display session status
2169
+ else:
2170
+ status_html = f"""
2171
+ <div class="status-indicator status-success">
2172
+ ✅ 会话已保存 | 最后更新: {session_data['timestamp']}
2173
+ </div>
2174
+ """
2175
+
2176
+ return (
2177
+ result, workdir, markdown, html, resources, # Output display
2178
+ workdir, result, markdown, html, resources, # State saving
2179
+ user_prompt_val, urls_text_val, # Input state saving
2180
+ status_html, # Status indicator
2181
+ get_system_status_html(), # System status
2182
+ get_user_status_html(request) # User status
2183
+ )
2184
+
2185
+ # Restore state function
2186
+ def restore_latest_results(workdir, result, markdown, html, resources, user_prompt_state, urls_text_state,
2187
+ request: gr.Request):
2188
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2189
+
2190
+ # Determine user ID
2191
+ if local_mode:
2192
+ user_id = "local_user"
2193
+ else:
2194
+ is_authenticated, user_id_or_error = check_user_auth(request)
2195
+ if not is_authenticated:
2196
+ status_html = f"""
2197
+ <div class="status-indicator status-info">
2198
+ 🚫 游客模式 - {user_id_or_error}
2199
+ </div>
2200
+ """
2201
+ return result, workdir, markdown, html, resources, user_prompt_state, urls_text_state, status_html, get_system_status_html()
2202
+ user_id = user_id_or_error
2203
+
2204
+ # Reload session data
2205
+ session_data = load_session_data(user_id)
2206
+
2207
+ # Update status indicator
2208
+ if local_mode:
2209
+ status_html = "" # Local mode doesn't display status
2210
+ else:
2211
+ status_html = f"""
2212
+ <div class="status-indicator status-success">
2213
+ 🔄 已恢复会话数据 | 最后更新: {session_data.get('timestamp', '未知')}
2214
+ </div>
2215
+ """ if any(session_data.values()) else """
2216
+ <div class="status-indicator status-info">
2217
+ ℹ️ 没有找到可恢复的会话数据
2218
+ </div>
2219
+ """
2220
+
2221
+ return (
2222
+ session_data.get("result", result),
2223
+ session_data.get("workdir", workdir),
2224
+ session_data.get("markdown", markdown),
2225
+ session_data.get("html", html),
2226
+ session_data.get("resources", resources),
2227
+ session_data.get("user_prompt", user_prompt_state),
2228
+ session_data.get("urls_text", urls_text_state),
2229
+ status_html,
2230
+ get_system_status_html()
2231
+ )
2232
+
2233
+ # Cleanup function
2234
+ def clear_all_inputs_and_state(request: gr.Request):
2235
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2236
+
2237
+ # Determine user ID
2238
+ if local_mode:
2239
+ user_id = "local_user"
2240
+ else:
2241
+ is_authenticated, user_id_or_error = check_user_auth(request)
2242
+ if not is_authenticated:
2243
+ status_html = f"""
2244
+ <div class="status-indicator status-info">
2245
+ 🚫 游客模式 - {user_id_or_error}
2246
+ </div>
2247
+ """
2248
+ return "", None, "", "", "", "", "", "", "", "", "", "", "", "", "", status_html, get_system_status_html(), get_user_status_html(
2249
+ request)
2250
+ user_id = user_id_or_error
2251
+
2252
+ # Force cleanup user task
2253
+ user_status_manager.force_cleanup_user(user_id)
2254
+
2255
+ # Cleanup session data file
2256
+ try:
2257
+ session_file = get_session_file_path(user_id)
2258
+ if os.path.exists(session_file):
2259
+ os.remove(session_file)
2260
+ except Exception as e:
2261
+ logger.info(f"Failed to cleanup session file: {e}")
2262
+
2263
+ if local_mode:
2264
+ status_html = "" # Local mode doesn't display status
2265
+ else:
2266
+ status_html = """
2267
+ <div class="status-indicator status-info">
2268
+ 🧹 会话数据已清理
2269
+ </div>
2270
+ """
2271
+
2272
+ return "", None, "", "", "", "", "", "", "", "", "", "", "", "", "", status_html, get_system_status_html(), get_user_status_html(
2273
+ request)
2274
+
2275
+ # Clear workspace and keep state
2276
+ def clear_workspace_keep_state(current_workdir_val, current_result_val, current_markdown_val, current_html_val,
2277
+ current_resources_val, request: gr.Request):
2278
+ clear_result, clear_markdown, clear_resources = clear_workspace(request)
2279
+
2280
+ local_mode = os.environ.get('LOCAL_MODE', 'true').lower() == 'true'
2281
+
2282
+ if local_mode:
2283
+ status_html = "" # Local mode doesn't display status
2284
+ else:
2285
+ status_html = """
2286
+ <div class="status-indicator status-success">
2287
+ 🧹 工作空间已清理,会话数据已保留
2288
+ </div>
2289
+ """
2290
+
2291
+ return clear_result, clear_markdown, clear_resources, current_workdir_val, current_result_val, current_markdown_val, current_html_val, current_resources_val, status_html, get_system_status_html()
2292
+
2293
+ # Refresh system status function
2294
+ def refresh_system_status():
2295
+ return get_system_status_html()
2296
+
2297
+ # Initialize on page load
2298
+ demo.load(
2299
+ fn=initialize_page,
2300
+ outputs=[
2301
+ user_status,
2302
+ user_prompt, urls_text,
2303
+ result_output, workdir_output, markdown_output, html_output if not local_mode else markdown_output,
2304
+ resources_output,
2305
+ current_workdir, current_result, current_markdown, current_html, current_resources,
2306
+ current_user_prompt, current_urls_text, session_status, system_status
2307
+ ]
2308
+ )
2309
+
2310
+ # Periodic status display refresh
2311
+ def periodic_status_update(request: gr.Request):
2312
+ """Periodically update status display"""
2313
+ return get_user_status_html(request), get_system_status_html()
2314
+
2315
+ # Use timer component to implement periodic status updates
2316
+ status_timer = gr.Timer(10) # Trigger every 10 seconds
2317
+ status_timer.tick(
2318
+ fn=periodic_status_update,
2319
+ outputs=[user_status, system_status]
2320
+ )
2321
+
2322
+ # Fullscreen functionality event binding
2323
+ fullscreen_btn.click(
2324
+ fn=toggle_fullscreen,
2325
+ inputs=[current_markdown, current_html],
2326
+ outputs=[fullscreen_modal, fullscreen_markdown, fullscreen_html]
2327
+ )
2328
+
2329
+ close_btn.click(
2330
+ fn=close_fullscreen,
2331
+ outputs=[fullscreen_modal, fullscreen_markdown, fullscreen_html]
2332
+ )
2333
+
2334
+ # Event binding
2335
+ run_btn.click(
2336
+ fn=run_research_workflow_with_state,
2337
+ inputs=[
2338
+ user_prompt, uploaded_files, urls_text,
2339
+ current_workdir, current_result, current_markdown, current_html, current_resources,
2340
+ current_user_prompt, current_urls_text
2341
+ ],
2342
+ outputs=[
2343
+ result_output, workdir_output, markdown_output, html_output if not local_mode else markdown_output,
2344
+ resources_output,
2345
+ current_workdir, current_result, current_markdown, current_html, current_resources,
2346
+ current_user_prompt, current_urls_text, session_status, system_status, user_status
2347
+ ],
2348
+ show_progress=True
2349
+ )
2350
+
2351
+ # Restore recent results
2352
+ restore_btn.click(
2353
+ fn=restore_latest_results,
2354
+ inputs=[current_workdir, current_result, current_markdown, current_html, current_resources,
2355
+ current_user_prompt, current_urls_text],
2356
+ outputs=[result_output, workdir_output, markdown_output, html_output if not local_mode else markdown_output,
2357
+ resources_output, user_prompt, urls_text, session_status, system_status]
2358
+ )
2359
+
2360
+ # Refresh system status
2361
+ refresh_status_btn.click(
2362
+ fn=refresh_system_status,
2363
+ outputs=[system_status]
2364
+ )
2365
+
2366
+ clear_btn.click(
2367
+ fn=clear_workspace_keep_state,
2368
+ inputs=[current_workdir, current_result, current_markdown, current_html, current_resources],
2369
+ outputs=[result_output, markdown_output, resources_output, current_workdir, current_result,
2370
+ current_markdown, current_html, current_resources, session_status, system_status]
2371
+ ).then(
2372
+ fn=clear_all_inputs_and_state,
2373
+ outputs=[
2374
+ user_prompt, uploaded_files, urls_text,
2375
+ result_output, workdir_output, markdown_output, html_output if not local_mode else markdown_output,
2376
+ resources_output,
2377
+ current_workdir, current_result, current_markdown, current_html, current_resources,
2378
+ current_user_prompt, current_urls_text, session_status, system_status, user_status
2379
+ ]
2380
+ )
2381
+
2382
+ # Example data
2383
+ gr.Examples(
2384
+ examples=[
2385
+ [
2386
+ "深入分析和总结下列文档",
2387
+ None,
2388
+ "https://modelscope.cn/models/ms-agent/ms_agent_resources/resolve/master/numina_dataset.pdf"
2389
+ ],
2390
+ [
2391
+ "Qwen3跟Qwen2.5对比,有哪些优化?",
2392
+ None,
2393
+ "https://arxiv.org/abs/2505.09388\nhttps://arxiv.org/abs/2412.15115"
2394
+ ],
2395
+ [
2396
+ "Analyze and summarize the following documents, you must use English to answer.",
2397
+ None,
2398
+ "https://arxiv.org/abs/1706.03762"
2399
+ ]
2400
+ ],
2401
+ inputs=[user_prompt, uploaded_files, urls_text],
2402
+ label="示例 | Examples"
2403
+ )
2404
+
2405
+ return demo
2406
+
2407
+
2408
+ def launch_server(
2409
+ server_name: Optional[str] = "0.0.0.0",
2410
+ server_port: Optional[int] = 7860,
2411
+ share: Optional[bool] = False,
2412
+ debug: Optional[bool] = False,
2413
+ show_error: Optional[bool] = False,
2414
+ gradio_default_concurrency_limit: Optional[int] = GRADIO_DEFAULT_CONCURRENCY_LIMIT,
2415
+ ) -> None:
2416
+
2417
+ # Create interface
2418
+ demo = create_interface()
2419
+
2420
+ # Configure Gradio queue concurrency control
2421
+ demo.queue(default_concurrency_limit=gradio_default_concurrency_limit)
2422
+
2423
+ # Launch application
2424
+ demo.launch(
2425
+ server_name=server_name,
2426
+ server_port=server_port,
2427
+ share=share,
2428
+ debug=debug,
2429
+ show_error=show_error,
2430
+ )
2431
+
2432
+
2433
+ if __name__ == "__main__":
2434
+ launch_server()
package.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gradio-research-app",
3
+ "version": "1.0.0",
4
+ "description": "Gradio研究工作流应用",
5
+ "main": "app.py",
6
+ "scripts": {
7
+ "dev": "python app.py",
8
+ "install": "pip install -r requirements.txt"
9
+ },
10
+ "keywords": ["gradio", "research", "workflow"],
11
+ "author": "",
12
+ "license": "MIT"
13
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=5.0.0
2
+ openai>=1.0.0
3
+ requests>=2.28.0
4
+ python-dotenv>=1.0.0
5
+ ms-agent>=1.1.2