Spaces:
Sleeping
Sleeping
Commit
·
3efffd7
1
Parent(s):
9305ec9
feat: Deploy to Hugging Face Space
Browse files- Dockerfile +1 -0
- docs/修改记录.md +1 -1
- src/app.py +70 -188
- src/debate_controller.py +7 -35
- static/css/style.css +51 -0
- static/js/script.js +87 -0
- templates/index.html +2 -142
Dockerfile
CHANGED
@@ -19,6 +19,7 @@ RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
|
19 |
# 将src目录下的所有代码复制到工作目录的src子目录中
|
20 |
COPY --chown=user ./src /app/src
|
21 |
COPY --chown=user ./templates /app/templates
|
|
|
22 |
|
23 |
# 暴露端口,让Hugging Face平台可以访问我们的应用
|
24 |
EXPOSE 7860
|
|
|
19 |
# 将src目录下的所有代码复制到工作目录的src子目录中
|
20 |
COPY --chown=user ./src /app/src
|
21 |
COPY --chown=user ./templates /app/templates
|
22 |
+
COPY --chown=user ./static /app/static
|
23 |
|
24 |
# 暴露端口,让Hugging Face平台可以访问我们的应用
|
25 |
EXPOSE 7860
|
docs/修改记录.md
CHANGED
@@ -26,7 +26,7 @@
|
|
26 |
- 创建项目规划文档 (docs/项目规划.md)
|
27 |
- 创建requirements.txt文件
|
28 |
- 创建README.md文档
|
29 |
-
- 创建修改记录文档 (docs/修改记录.md)
|
30 |
|
31 |
## 第2次修改
|
32 |
- 创建更新计划文档 (docs/更新计划.md)
|
|
|
26 |
- 创建项目规划文档 (docs/项目规划.md)
|
27 |
- 创建requirements.txt文件
|
28 |
- 创建README.md文档
|
29 |
+
- 创建修改记录文档 (docs/修改记录.md)
|
30 |
|
31 |
## 第2次修改
|
32 |
- 创建更新计划文档 (docs/更新计划.md)
|
src/app.py
CHANGED
@@ -13,6 +13,7 @@ from datetime import datetime
|
|
13 |
from typing import Optional
|
14 |
import importlib.util
|
15 |
import asyncio
|
|
|
16 |
|
17 |
# 在代码开头强制设置终端编码为UTF-8
|
18 |
os.system('chcp 6001 > nul')
|
@@ -100,6 +101,8 @@ api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
|
100 |
model_manager = ModelManager(api_key)
|
101 |
active_debate_session = None
|
102 |
active_websocket = None
|
|
|
|
|
103 |
|
104 |
@app.get("/", response_class=HTMLResponse)
|
105 |
async def read_root(request: Request):
|
@@ -139,18 +142,20 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
139 |
|
140 |
async def handle_websocket_message(websocket: WebSocket, message: str):
|
141 |
"""处理WebSocket消息"""
|
|
|
142 |
try:
|
143 |
data = json.loads(message)
|
144 |
action = data.get("action")
|
145 |
|
146 |
if action == "start_debate":
|
147 |
-
|
|
|
|
|
|
|
|
|
|
|
148 |
elif action == "stop_debate":
|
149 |
await stop_debate(websocket)
|
150 |
-
elif action == "pause_debate":
|
151 |
-
await pause_debate(websocket)
|
152 |
-
elif action == "resume_debate":
|
153 |
-
await resume_debate(websocket)
|
154 |
else:
|
155 |
await websocket.send_text(json.dumps({
|
156 |
"type": "error",
|
@@ -170,23 +175,19 @@ async def handle_websocket_message(websocket: WebSocket, message: str):
|
|
170 |
|
171 |
async def start_debate(websocket: WebSocket, data: dict):
|
172 |
"""开始辩论"""
|
173 |
-
global active_debate_session
|
174 |
loop = asyncio.get_event_loop()
|
175 |
|
176 |
try:
|
177 |
topic = data.get("topic", "人工智能是否会取代人类的工作")
|
178 |
rounds = int(data.get("rounds", 5))
|
179 |
first_model = data.get("first_model", "glm45")
|
180 |
-
initial_prompt = data.get("initial_prompt", "").strip()
|
181 |
|
182 |
await websocket.send_text(json.dumps({ "type": "debate_started", "message": "辩论已开始", "topic": topic, "rounds": rounds, "first_model": first_model }))
|
183 |
|
184 |
-
active_debate_session = DebateSession(topic, rounds, first_model)
|
185 |
|
186 |
-
# 新增:如果存在自定义初始提示,则添加到会话历史中
|
187 |
-
if initial_prompt:
|
188 |
-
active_debate_session.add_message(DebateMessage("user", initial_prompt, "system"))
|
189 |
-
|
190 |
model_a_name = first_model
|
191 |
model_b_name = 'deepseek_v31' if model_a_name == 'glm45' else 'glm45'
|
192 |
model_a = model_manager.get_model(model_a_name)
|
@@ -198,9 +199,6 @@ async def start_debate(websocket: WebSocket, data: dict):
|
|
198 |
|
199 |
# Main debate loop
|
200 |
for i in range(rounds * 2):
|
201 |
-
if not active_debate_session.is_active:
|
202 |
-
break
|
203 |
-
|
204 |
round_num = (i // 2) + 1
|
205 |
if i % 2 == 0:
|
206 |
active_debate_session.current_round = round_num
|
@@ -228,43 +226,43 @@ async def start_debate(websocket: WebSocket, data: dict):
|
|
228 |
active_debate_session.add_message(DebateMessage("assistant", response_content, speaker_name))
|
229 |
save_debate_record() # Incremental save
|
230 |
|
231 |
-
if active_debate_session.is_active:
|
232 |
active_debate_session.end_time = datetime.now()
|
233 |
active_debate_session.is_active = False
|
234 |
await websocket.send_text(json.dumps({ "type": "debate_ended", "message": "=== 辩论结束 ===" }))
|
235 |
save_debate_record()
|
236 |
logger.info("辩论结束")
|
237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
except Exception as e:
|
239 |
-
logger.error(f"辩论过程中出错: {str(e)}")
|
240 |
await websocket.send_text(json.dumps({ "type": "error", "message": f"辩论过程中出错: {str(e)}" }))
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
async def stop_debate(websocket: WebSocket):
|
243 |
"""停止辩论"""
|
244 |
-
global
|
245 |
-
if
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
}))
|
252 |
-
save_debate_record()
|
253 |
-
active_debate_session = None
|
254 |
-
|
255 |
-
async def pause_debate(websocket: WebSocket):
|
256 |
-
"""暂停辩论"""
|
257 |
-
await websocket.send_text(json.dumps({
|
258 |
-
"type": "debate_paused",
|
259 |
-
"message": "辩论已暂停"
|
260 |
-
}))
|
261 |
|
262 |
-
async def resume_debate(websocket: WebSocket):
|
263 |
-
"""继续辩论"""
|
264 |
-
await websocket.send_text(json.dumps({
|
265 |
-
"type": "debate_resumed",
|
266 |
-
"message": "辩论已继续"
|
267 |
-
}))
|
268 |
|
269 |
def save_debate_record():
|
270 |
"""保存辩论记录"""
|
@@ -280,67 +278,17 @@ def save_debate_record():
|
|
280 |
logger.error(f"保存辩论记录时出错: {str(e)}")
|
281 |
|
282 |
def create_templates():
|
283 |
-
"""创建HTML
|
284 |
-
|
|
|
|
|
285 |
<!DOCTYPE html>
|
286 |
<html lang="zh-CN">
|
287 |
<head>
|
288 |
<meta charset="UTF-8">
|
289 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
290 |
<title>AI大模型辩论系统</title>
|
291 |
-
<style>
|
292 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #333; height: 100vh; display: flex; flex-direction: column; }
|
293 |
-
.container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 95%; max-width: 1400px; margin: 20px auto; flex-grow: 1; display: flex; flex-direction: column; }
|
294 |
-
.main-layout { display: flex; gap: 20px; flex-grow: 1; min-height: 0; }
|
295 |
-
.sidebar { flex: 0 0 320px; display: flex; flex-direction: column; gap: 15px; }
|
296 |
-
.chat-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
297 |
-
.header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
|
298 |
-
.header h1 { color: #1a73e8; }
|
299 |
-
.controls, .control-group { margin-bottom: 20px; }
|
300 |
-
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
301 |
-
.control-group { flex: 1; min-width: 200px; }
|
302 |
-
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
|
303 |
-
input, select, button, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px; }
|
304 |
-
textarea { resize: vertical; }
|
305 |
-
button { background-color: #1a73e8; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
|
306 |
-
button:hover { background-color: #1558b8; }
|
307 |
-
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
308 |
-
.output-container { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; height: 500px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
|
309 |
-
.status { padding: 10px; border-radius: 6px; margin-bottom: 15px; border: 1px solid; }
|
310 |
-
.status.connected { background-color: #e6f4ea; color: #155724; border-color: #c3e6cb; }
|
311 |
-
.status.disconnected { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
312 |
-
|
313 |
-
/* 聊天气泡样式 */
|
314 |
-
.message { display: flex; align-items: flex-start; gap: 10px; max-width: 80%; }
|
315 |
-
.message .avatar { width: 40px; height: 40px; border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; font-size: 18px; }
|
316 |
-
.message .content { background-color: #ffffff; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
317 |
-
.message .sender { font-weight: bold; margin-bottom: 5px; color: #333; }
|
318 |
-
.message.glm45 { align-self: flex-start; }
|
319 |
-
.message.glm45 .avatar { background-color: #34a853; } /* Google Green */
|
320 |
-
.message.glm45 .content { border-top-left-radius: 4px; }
|
321 |
-
.message.deepseek_v31 { align-self: flex-end; flex-direction: row-reverse; }
|
322 |
-
.message.deepseek_v31 .avatar { background-color: #4285f4; } /* Google Blue */
|
323 |
-
.message.deepseek_v31 .content { background-color: #e7f3ff; border-top-right-radius: 4px; }
|
324 |
-
.message .text { white-space: pre-wrap; word-wrap: break-word; }
|
325 |
-
.message .text p { margin: 0 0 10px; }
|
326 |
-
.message .text h1, .message .text h2, .message .text h3 { margin: 15px 0 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
327 |
-
.message .text ul, .message .text ol { padding-left: 20px; }
|
328 |
-
.message .text code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
|
329 |
-
.message .text pre { background-color: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; }
|
330 |
-
.message .text pre code { background-color: transparent; padding: 0; }
|
331 |
-
.round-separator { text-align: center; color: #888; font-size: 0.9em; margin: 20px 0; font-weight: 600; }
|
332 |
-
|
333 |
-
/* 响应式设计 - 针对手机等小屏幕设备 */
|
334 |
-
@media (max-width: 768px) {
|
335 |
-
body { padding: 0; }
|
336 |
-
.container { width: 100%; margin: 0; border-radius: 0; padding: 10px; height: 100%; }
|
337 |
-
.main-layout { flex-direction: column; }
|
338 |
-
.sidebar { flex: 0 0 auto; }
|
339 |
-
.header h1 { font-size: 1.5em; }
|
340 |
-
.controls { flex-direction: column; }
|
341 |
-
.control-group { min-width: unset; }
|
342 |
-
}
|
343 |
-
</style>
|
344 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
345 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
346 |
</head>
|
@@ -368,106 +316,40 @@ def create_templates():
|
|
368 |
</div>
|
369 |
</div>
|
370 |
</div>
|
371 |
-
<script>
|
372 |
-
let websocket = null, isConnected = false, currentMessageElement = null, currentMessageContent = '';
|
373 |
-
const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn');
|
374 |
-
const outputDiv = document.getElementById('output');
|
375 |
-
|
376 |
-
function handleWebSocketMessage(data) {
|
377 |
-
let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10;
|
378 |
-
|
379 |
-
switch (data.type) {
|
380 |
-
case 'debate_started':
|
381 |
-
outputDiv.innerHTML = '';
|
382 |
-
const topicDiv = document.createElement('div');
|
383 |
-
topicDiv.className = 'round-separator';
|
384 |
-
topicDiv.innerHTML = `<strong>话题:</strong> ${data.topic}`;
|
385 |
-
outputDiv.appendChild(topicDiv);
|
386 |
-
break;
|
387 |
-
case 'round_info':
|
388 |
-
const separator = document.createElement('div');
|
389 |
-
separator.className = 'round-separator';
|
390 |
-
separator.textContent = data.message.trim();
|
391 |
-
outputDiv.appendChild(separator);
|
392 |
-
break;
|
393 |
-
case 'model_speaking':
|
394 |
-
currentMessageContent = ''; // 重置当前消息内容
|
395 |
-
const messageDiv = document.createElement('div');
|
396 |
-
messageDiv.className = `message ${data.model}`;
|
397 |
-
messageDiv.innerHTML = `
|
398 |
-
<div class="avatar">${data.model.substring(0, 1).toUpperCase()}</div>
|
399 |
-
<div class="content">
|
400 |
-
<div class="sender">${data.model} (${data.role})</div>
|
401 |
-
<div class="text"><i>正在思考...</i></div>
|
402 |
-
</div>`;
|
403 |
-
outputDiv.appendChild(messageDiv);
|
404 |
-
currentMessageElement = messageDiv.querySelector('.text');
|
405 |
-
break;
|
406 |
-
case 'stream_content':
|
407 |
-
if (currentMessageElement) {
|
408 |
-
currentMessageContent += data.content;
|
409 |
-
currentMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(currentMessageContent));
|
410 |
-
}
|
411 |
-
break;
|
412 |
-
case 'stream_end':
|
413 |
-
currentMessageElement = null;
|
414 |
-
break;
|
415 |
-
case 'debate_ended':
|
416 |
-
case 'debate_stopped':
|
417 |
-
const endMsg = document.createElement('div');
|
418 |
-
endMsg.className = 'round-separator';
|
419 |
-
endMsg.textContent = data.message;
|
420 |
-
outputDiv.appendChild(endMsg);
|
421 |
-
startBtn.disabled = false; stopBtn.disabled = true;
|
422 |
-
break;
|
423 |
-
case 'error':
|
424 |
-
outputDiv.innerHTML += `<div class="round-separator" style="color: red;">错误: ${data.message}</div>`;
|
425 |
-
break;
|
426 |
-
}
|
427 |
-
if(shouldScroll) {
|
428 |
-
outputDiv.scrollTop = outputDiv.scrollHeight;
|
429 |
-
}
|
430 |
-
}
|
431 |
-
|
432 |
-
function connect() {
|
433 |
-
const wsUrl = `ws://${window.location.host}/ws`;
|
434 |
-
websocket = new WebSocket(wsUrl);
|
435 |
-
websocket.onopen = () => { isConnected = true; startBtn.disabled = false; };
|
436 |
-
websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
|
437 |
-
websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; };
|
438 |
-
websocket.onerror = (error) => { console.error('WebSocket Error:', error); };
|
439 |
-
}
|
440 |
-
|
441 |
-
window.addEventListener('load', connect);
|
442 |
-
|
443 |
-
startBtn.addEventListener('click', () => {
|
444 |
-
if (!websocket) return;
|
445 |
-
const message = {
|
446 |
-
action: "start_debate",
|
447 |
-
topic: document.getElementById('topic').value,
|
448 |
-
rounds: parseInt(document.getElementById('rounds').value),
|
449 |
-
first_model: document.getElementById('firstModel').value,
|
450 |
-
initial_prompt: document.getElementById('initialPrompt').value
|
451 |
-
};
|
452 |
-
websocket.send(JSON.stringify(message));
|
453 |
-
startBtn.disabled = true; stopBtn.disabled = false;
|
454 |
-
});
|
455 |
-
stopBtn.addEventListener('click', () => {
|
456 |
-
if (!websocket) return;
|
457 |
-
websocket.send(JSON.stringify({ action: "stop_debate" }));
|
458 |
-
});
|
459 |
-
</script>
|
460 |
</body>
|
461 |
</html>"""
|
462 |
-
|
463 |
-
|
464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
|
466 |
if __name__ == "__main__":
|
467 |
os.makedirs(static_dir, exist_ok=True)
|
468 |
os.makedirs(templates_dir, exist_ok=True)
|
469 |
create_templates()
|
470 |
-
|
471 |
# 智能端口切换:优先使用环境变量PORT,否则默认为8000
|
472 |
port = int(os.environ.get("PORT", 8000))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
473 |
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
|
13 |
from typing import Optional
|
14 |
import importlib.util
|
15 |
import asyncio
|
16 |
+
import socket
|
17 |
|
18 |
# 在代码开头强制设置终端编码为UTF-8
|
19 |
os.system('chcp 6001 > nul')
|
|
|
101 |
model_manager = ModelManager(api_key)
|
102 |
active_debate_session = None
|
103 |
active_websocket = None
|
104 |
+
active_debate_task = None
|
105 |
+
|
106 |
|
107 |
@app.get("/", response_class=HTMLResponse)
|
108 |
async def read_root(request: Request):
|
|
|
142 |
|
143 |
async def handle_websocket_message(websocket: WebSocket, message: str):
|
144 |
"""处理WebSocket消息"""
|
145 |
+
global active_debate_task
|
146 |
try:
|
147 |
data = json.loads(message)
|
148 |
action = data.get("action")
|
149 |
|
150 |
if action == "start_debate":
|
151 |
+
if active_debate_task and not active_debate_task.done():
|
152 |
+
await websocket.send_text(json.dumps({
|
153 |
+
"type": "error", "message": "另一场辩论正在进行中,请等待其结束或停止它。"
|
154 |
+
}))
|
155 |
+
return
|
156 |
+
active_debate_task = asyncio.create_task(start_debate(websocket, data))
|
157 |
elif action == "stop_debate":
|
158 |
await stop_debate(websocket)
|
|
|
|
|
|
|
|
|
159 |
else:
|
160 |
await websocket.send_text(json.dumps({
|
161 |
"type": "error",
|
|
|
175 |
|
176 |
async def start_debate(websocket: WebSocket, data: dict):
|
177 |
"""开始辩论"""
|
178 |
+
global active_debate_session, active_debate_task
|
179 |
loop = asyncio.get_event_loop()
|
180 |
|
181 |
try:
|
182 |
topic = data.get("topic", "人工智能是否会取代人类的工作")
|
183 |
rounds = int(data.get("rounds", 5))
|
184 |
first_model = data.get("first_model", "glm45")
|
185 |
+
initial_prompt = data.get("initial_prompt", "").strip()
|
186 |
|
187 |
await websocket.send_text(json.dumps({ "type": "debate_started", "message": "辩论已开始", "topic": topic, "rounds": rounds, "first_model": first_model }))
|
188 |
|
189 |
+
active_debate_session = DebateSession(topic, rounds, first_model, initial_prompt)
|
190 |
|
|
|
|
|
|
|
|
|
191 |
model_a_name = first_model
|
192 |
model_b_name = 'deepseek_v31' if model_a_name == 'glm45' else 'glm45'
|
193 |
model_a = model_manager.get_model(model_a_name)
|
|
|
199 |
|
200 |
# Main debate loop
|
201 |
for i in range(rounds * 2):
|
|
|
|
|
|
|
202 |
round_num = (i // 2) + 1
|
203 |
if i % 2 == 0:
|
204 |
active_debate_session.current_round = round_num
|
|
|
226 |
active_debate_session.add_message(DebateMessage("assistant", response_content, speaker_name))
|
227 |
save_debate_record() # Incremental save
|
228 |
|
229 |
+
if active_debate_session and active_debate_session.is_active:
|
230 |
active_debate_session.end_time = datetime.now()
|
231 |
active_debate_session.is_active = False
|
232 |
await websocket.send_text(json.dumps({ "type": "debate_ended", "message": "=== 辩论结束 ===" }))
|
233 |
save_debate_record()
|
234 |
logger.info("辩论结束")
|
235 |
|
236 |
+
except asyncio.CancelledError:
|
237 |
+
logger.info("辩论任务被取消。")
|
238 |
+
if active_debate_session:
|
239 |
+
active_debate_session.is_active = False
|
240 |
+
active_debate_session.end_time = datetime.now()
|
241 |
+
save_debate_record() # 保存部分辩论记录
|
242 |
+
await websocket.send_text(json.dumps({
|
243 |
+
"type": "debate_stopped",
|
244 |
+
"message": "辩论已由用户停止"
|
245 |
+
}))
|
246 |
+
raise # 重新引发CancelledError以确保任务状态正确
|
247 |
except Exception as e:
|
248 |
+
logger.error(f"辩论过程中出错: {str(e)}", exc_info=True)
|
249 |
await websocket.send_text(json.dumps({ "type": "error", "message": f"辩论过程中出错: {str(e)}" }))
|
250 |
+
finally:
|
251 |
+
active_debate_session = None
|
252 |
+
active_debate_task = None
|
253 |
+
logger.info("辩论会话清理完毕。")
|
254 |
+
|
255 |
|
256 |
async def stop_debate(websocket: WebSocket):
|
257 |
"""停止辩论"""
|
258 |
+
global active_debate_task
|
259 |
+
if active_debate_task and not active_debate_task.done():
|
260 |
+
active_debate_task.cancel()
|
261 |
+
logger.info("发送取消请求到辩论任务。")
|
262 |
+
else:
|
263 |
+
logger.warning("请求停止辩论,但没有活动的任务。")
|
264 |
+
await websocket.send_text(json.dumps({"type": "info", "message": "没有正在进行的辩论可供停止。"}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
266 |
|
267 |
def save_debate_record():
|
268 |
"""保存辩论记录"""
|
|
|
278 |
logger.error(f"保存辩论记录时出错: {str(e)}")
|
279 |
|
280 |
def create_templates():
|
281 |
+
"""创建HTML模板文件,如果不存在的话"""
|
282 |
+
template_path = os.path.join(templates_dir, "index.html")
|
283 |
+
if not os.path.exists(template_path):
|
284 |
+
index_html = """
|
285 |
<!DOCTYPE html>
|
286 |
<html lang="zh-CN">
|
287 |
<head>
|
288 |
<meta charset="UTF-8">
|
289 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
290 |
<title>AI大模型辩论系统</title>
|
291 |
+
<link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
293 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
294 |
</head>
|
|
|
316 |
</div>
|
317 |
</div>
|
318 |
</div>
|
319 |
+
<script src="{{ url_for('static', path='js/script.js') }}"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
320 |
</body>
|
321 |
</html>"""
|
322 |
+
with open(template_path, "w", encoding="utf-8") as f:
|
323 |
+
f.write(index_html)
|
324 |
+
logger.info("Web应用模板文件 'index.html' 不存在,已创建。")
|
325 |
+
else:
|
326 |
+
logger.info("Web应用模板文件 'index.html' 已存在,跳过创建。")
|
327 |
+
|
328 |
+
|
329 |
+
def get_local_ip():
|
330 |
+
"""获取本机局域网IP地址"""
|
331 |
+
try:
|
332 |
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
333 |
+
# 连接到一个公共DNS服务器的IP(不会真的发送数据)
|
334 |
+
s.connect(("8.8.8.8", 80))
|
335 |
+
return s.getsockname()[0]
|
336 |
+
except Exception:
|
337 |
+
return "127.0.0.1" # 如果获取失败,返回本地回环地址
|
338 |
+
|
339 |
|
340 |
if __name__ == "__main__":
|
341 |
os.makedirs(static_dir, exist_ok=True)
|
342 |
os.makedirs(templates_dir, exist_ok=True)
|
343 |
create_templates()
|
344 |
+
|
345 |
# 智能端口切换:优先使用环境变量PORT,否则默认为8000
|
346 |
port = int(os.environ.get("PORT", 8000))
|
347 |
+
local_ip = get_local_ip()
|
348 |
+
|
349 |
+
logger.info("="*50)
|
350 |
+
logger.info("AI大模型辩论系统已启动")
|
351 |
+
logger.info(f" - 本机访问: http://localhost:{port}")
|
352 |
+
logger.info(f" - 局域网访问: http://{local_ip}:{port}")
|
353 |
+
logger.info("="*50)
|
354 |
+
|
355 |
uvicorn.run(app, host="0.0.0.0", port=port)
|
src/debate_controller.py
CHANGED
@@ -86,7 +86,7 @@ class DebateMessage:
|
|
86 |
class DebateSession:
|
87 |
"""辩论会话类"""
|
88 |
|
89 |
-
def __init__(self, topic: str, max_rounds: int = 5, first_model: str = 'glm45'):
|
90 |
"""
|
91 |
初始化辩论会话
|
92 |
|
@@ -94,14 +94,15 @@ class DebateSession:
|
|
94 |
topic: 辩论话题
|
95 |
max_rounds: 最大轮数
|
96 |
first_model: 首发模型 ('glm45' 或 'kimi_k2')
|
|
|
97 |
"""
|
98 |
self.topic = topic
|
99 |
self.max_rounds = max_rounds
|
100 |
self.first_model = first_model
|
|
|
101 |
self.messages: List[DebateMessage] = []
|
102 |
self.current_round = 0
|
103 |
self.is_active = True
|
104 |
-
self.is_paused = False
|
105 |
self.start_time = None
|
106 |
self.end_time = None
|
107 |
self.debate_id = f"debate_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
@@ -141,7 +142,6 @@ class DebateSession:
|
|
141 |
'current_round': self.current_round,
|
142 |
'total_messages': len(self.messages),
|
143 |
'is_active': self.is_active,
|
144 |
-
'is_paused': self.is_paused,
|
145 |
'start_time': self.start_time.isoformat() if self.start_time else None,
|
146 |
'end_time': self.end_time.isoformat() if self.end_time else None,
|
147 |
'duration': (self.end_time - self.start_time).total_seconds() if self.start_time and self.end_time else None
|
@@ -175,7 +175,6 @@ class DebateSession:
|
|
175 |
session.debate_id = debate_info['debate_id']
|
176 |
session.current_round = debate_info['current_round']
|
177 |
session.is_active = debate_info['is_active']
|
178 |
-
session.is_paused = debate_info['is_paused']
|
179 |
session.start_time = datetime.fromisoformat(debate_info['start_time']) if debate_info['start_time'] else None
|
180 |
session.end_time = datetime.fromisoformat(debate_info['end_time']) if debate_info['end_time'] else None
|
181 |
|
@@ -200,7 +199,10 @@ class DebateSession:
|
|
200 |
|
201 |
if not self.messages:
|
202 |
# 辩论开始,第一个发言者
|
203 |
-
|
|
|
|
|
|
|
204 |
else:
|
205 |
last_message = self.messages[-1]
|
206 |
opponent_statement = last_message.content
|
@@ -228,7 +230,6 @@ class DebateController:
|
|
228 |
self.current_session: Optional[DebateSession] = None
|
229 |
self.debate_thread: Optional[threading.Thread] = None
|
230 |
self.stop_event = threading.Event()
|
231 |
-
self.pause_event = threading.Event()
|
232 |
|
233 |
logger.info("辩论控制器初始化完成")
|
234 |
|
@@ -259,10 +260,8 @@ class DebateController:
|
|
259 |
return
|
260 |
|
261 |
self.current_session.is_active = True
|
262 |
-
self.current_session.is_paused = False
|
263 |
self.current_session.start_time = datetime.now()
|
264 |
self.stop_event.clear()
|
265 |
-
self.pause_event.clear()
|
266 |
|
267 |
# 启动辩论线程
|
268 |
self.debate_thread = threading.Thread(target=self._debate_loop)
|
@@ -271,26 +270,6 @@ class DebateController:
|
|
271 |
|
272 |
logger.info("辩论开始")
|
273 |
|
274 |
-
def pause_debate(self):
|
275 |
-
"""暂停辩论"""
|
276 |
-
if not self.current_session or not self.current_session.is_active:
|
277 |
-
logger.warning("没有活动的辩论会话")
|
278 |
-
return
|
279 |
-
|
280 |
-
self.current_session.is_paused = True
|
281 |
-
self.pause_event.set()
|
282 |
-
logger.info("辩论已暂停")
|
283 |
-
|
284 |
-
def resume_debate(self):
|
285 |
-
"""恢复辩论"""
|
286 |
-
if not self.current_session or not self.current_session.is_active:
|
287 |
-
logger.warning("没有活动的辩论会话")
|
288 |
-
return
|
289 |
-
|
290 |
-
self.current_session.is_paused = False
|
291 |
-
self.pause_event.clear()
|
292 |
-
logger.info("辩论已恢复")
|
293 |
-
|
294 |
def stop_debate(self):
|
295 |
"""停止辩论"""
|
296 |
if not self.current_session or not self.current_session.is_active:
|
@@ -299,7 +278,6 @@ class DebateController:
|
|
299 |
|
300 |
self.stop_event.set()
|
301 |
self.current_session.is_active = False
|
302 |
-
self.current_session.is_paused = False
|
303 |
self.current_session.end_time = datetime.now()
|
304 |
|
305 |
if self.debate_thread and self.debate_thread.is_alive():
|
@@ -348,12 +326,6 @@ class DebateController:
|
|
348 |
|
349 |
# 后续轮次
|
350 |
while self._should_continue_debate(session):
|
351 |
-
# 检查暂停状态
|
352 |
-
if self.pause_event.is_set():
|
353 |
-
self._output_message("辩论已暂停,等待恢复...\n")
|
354 |
-
self.pause_event.wait()
|
355 |
-
self._output_message("辩论已恢复\n")
|
356 |
-
|
357 |
session.current_round += 1
|
358 |
self._output_message(f"--- 第{session.current_round}轮 ---\n")
|
359 |
|
|
|
86 |
class DebateSession:
|
87 |
"""辩论会话类"""
|
88 |
|
89 |
+
def __init__(self, topic: str, max_rounds: int = 5, first_model: str = 'glm45', initial_prompt: str = ""):
|
90 |
"""
|
91 |
初始化辩论会话
|
92 |
|
|
|
94 |
topic: 辩论话题
|
95 |
max_rounds: 最大轮数
|
96 |
first_model: 首发模型 ('glm45' 或 'kimi_k2')
|
97 |
+
initial_prompt: 自定义初始提示
|
98 |
"""
|
99 |
self.topic = topic
|
100 |
self.max_rounds = max_rounds
|
101 |
self.first_model = first_model
|
102 |
+
self.initial_prompt = initial_prompt
|
103 |
self.messages: List[DebateMessage] = []
|
104 |
self.current_round = 0
|
105 |
self.is_active = True
|
|
|
106 |
self.start_time = None
|
107 |
self.end_time = None
|
108 |
self.debate_id = f"debate_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
|
142 |
'current_round': self.current_round,
|
143 |
'total_messages': len(self.messages),
|
144 |
'is_active': self.is_active,
|
|
|
145 |
'start_time': self.start_time.isoformat() if self.start_time else None,
|
146 |
'end_time': self.end_time.isoformat() if self.end_time else None,
|
147 |
'duration': (self.end_time - self.start_time).total_seconds() if self.start_time and self.end_time else None
|
|
|
175 |
session.debate_id = debate_info['debate_id']
|
176 |
session.current_round = debate_info['current_round']
|
177 |
session.is_active = debate_info['is_active']
|
|
|
178 |
session.start_time = datetime.fromisoformat(debate_info['start_time']) if debate_info['start_time'] else None
|
179 |
session.end_time = datetime.fromisoformat(debate_info['end_time']) if debate_info['end_time'] else None
|
180 |
|
|
|
199 |
|
200 |
if not self.messages:
|
201 |
# 辩论开始,第一个发言者
|
202 |
+
base_prompt = f"你将作为{role},就以下话题进行辩论:{self.topic}。请提出你的主要论点,陈述你的核心立场和关键论据。"
|
203 |
+
if self.initial_prompt:
|
204 |
+
return f"{base_prompt}\n\n另外,请特别注意以下指示:{self.initial_prompt}"
|
205 |
+
return base_prompt
|
206 |
else:
|
207 |
last_message = self.messages[-1]
|
208 |
opponent_statement = last_message.content
|
|
|
230 |
self.current_session: Optional[DebateSession] = None
|
231 |
self.debate_thread: Optional[threading.Thread] = None
|
232 |
self.stop_event = threading.Event()
|
|
|
233 |
|
234 |
logger.info("辩论控制器初始化完成")
|
235 |
|
|
|
260 |
return
|
261 |
|
262 |
self.current_session.is_active = True
|
|
|
263 |
self.current_session.start_time = datetime.now()
|
264 |
self.stop_event.clear()
|
|
|
265 |
|
266 |
# 启动辩论线程
|
267 |
self.debate_thread = threading.Thread(target=self._debate_loop)
|
|
|
270 |
|
271 |
logger.info("辩论开始")
|
272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
def stop_debate(self):
|
274 |
"""停止辩论"""
|
275 |
if not self.current_session or not self.current_session.is_active:
|
|
|
278 |
|
279 |
self.stop_event.set()
|
280 |
self.current_session.is_active = False
|
|
|
281 |
self.current_session.end_time = datetime.now()
|
282 |
|
283 |
if self.debate_thread and self.debate_thread.is_alive():
|
|
|
326 |
|
327 |
# 后续轮次
|
328 |
while self._should_continue_debate(session):
|
|
|
|
|
|
|
|
|
|
|
|
|
329 |
session.current_round += 1
|
330 |
self._output_message(f"--- 第{session.current_round}轮 ---\n")
|
331 |
|
static/css/style.css
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #333; height: 100vh; display: flex; flex-direction: column; }
|
2 |
+
.container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 95%; max-width: 1400px; margin: 20px auto; flex-grow: 1; display: flex; flex-direction: column; }
|
3 |
+
.main-layout { display: flex; gap: 20px; flex-grow: 1; min-height: 0; }
|
4 |
+
.sidebar { flex: 0 0 320px; display: flex; flex-direction: column; gap: 15px; }
|
5 |
+
.chat-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
6 |
+
.header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
|
7 |
+
.header h1 { color: #1a73e8; }
|
8 |
+
.controls, .control-group { margin-bottom: 20px; }
|
9 |
+
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
10 |
+
.control-group { flex: 1; min-width: 200px; }
|
11 |
+
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
|
12 |
+
input, select, button, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px; }
|
13 |
+
textarea { resize: vertical; }
|
14 |
+
button { background-color: #1a73e8; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
|
15 |
+
button:hover { background-color: #1558b8; }
|
16 |
+
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
17 |
+
.output-container { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; height: 500px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
|
18 |
+
.status { padding: 10px; border-radius: 6px; margin-bottom: 15px; border: 1px solid; }
|
19 |
+
.status.connected { background-color: #e6f4ea; color: #155724; border-color: #c3e6cb; }
|
20 |
+
.status.disconnected { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
21 |
+
|
22 |
+
/* 聊天气泡样式 */
|
23 |
+
.message { display: flex; align-items: flex-start; gap: 10px; max-width: 80%; }
|
24 |
+
.message .avatar { width: 40px; height: 40px; border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; font-size: 18px; }
|
25 |
+
.message .content { background-color: #ffffff; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
26 |
+
.message .sender { font-weight: bold; margin-bottom: 5px; color: #333; }
|
27 |
+
.message.glm45 { align-self: flex-start; }
|
28 |
+
.message.glm45 .avatar { background-color: #34a853; } /* Google Green */
|
29 |
+
.message.glm45 .content { border-top-left-radius: 4px; }
|
30 |
+
.message.deepseek_v31 { align-self: flex-end; flex-direction: row-reverse; }
|
31 |
+
.message.deepseek_v31 .avatar { background-color: #4285f4; } /* Google Blue */
|
32 |
+
.message.deepseek_v31 .content { background-color: #e7f3ff; border-top-right-radius: 4px; }
|
33 |
+
.message .text { white-space: pre-wrap; word-wrap: break-word; }
|
34 |
+
.message .text p { margin: 0 0 10px; }
|
35 |
+
.message .text h1, .message .text h2, .message .text h3 { margin: 15px 0 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
36 |
+
.message .text ul, .message .text ol { padding-left: 20px; }
|
37 |
+
.message .text code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
|
38 |
+
.message .text pre { background-color: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; }
|
39 |
+
.message .text pre code { background-color: transparent; padding: 0; }
|
40 |
+
.round-separator { text-align: center; color: #888; font-size: 0.9em; margin: 20px 0; font-weight: 600; }
|
41 |
+
|
42 |
+
/* 响应式设计 - 针对手机等小屏幕设备 */
|
43 |
+
@media (max-width: 768px) {
|
44 |
+
body { padding: 0; }
|
45 |
+
.container { width: 100%; margin: 0; border-radius: 0; padding: 10px; height: 100%; }
|
46 |
+
.main-layout { flex-direction: column; }
|
47 |
+
.sidebar { flex: 0 0 auto; }
|
48 |
+
.header h1 { font-size: 1.5em; }
|
49 |
+
.controls { flex-direction: column; }
|
50 |
+
.control-group { min-width: unset; }
|
51 |
+
}
|
static/js/script.js
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let websocket = null, isConnected = false, currentMessageElement = null, currentMessageContent = '';
|
2 |
+
const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn');
|
3 |
+
const outputDiv = document.getElementById('output');
|
4 |
+
|
5 |
+
function handleWebSocketMessage(data) {
|
6 |
+
let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10;
|
7 |
+
|
8 |
+
switch (data.type) {
|
9 |
+
case 'debate_started':
|
10 |
+
outputDiv.innerHTML = '';
|
11 |
+
const topicDiv = document.createElement('div');
|
12 |
+
topicDiv.className = 'round-separator';
|
13 |
+
topicDiv.innerHTML = `<strong>话题:</strong> ${data.topic}`;
|
14 |
+
outputDiv.appendChild(topicDiv);
|
15 |
+
break;
|
16 |
+
case 'round_info':
|
17 |
+
const separator = document.createElement('div');
|
18 |
+
separator.className = 'round-separator';
|
19 |
+
separator.textContent = data.message.trim();
|
20 |
+
outputDiv.appendChild(separator);
|
21 |
+
break;
|
22 |
+
case 'model_speaking':
|
23 |
+
currentMessageContent = ''; // 重置当前消息内容
|
24 |
+
const messageDiv = document.createElement('div');
|
25 |
+
messageDiv.className = `message ${data.model}`;
|
26 |
+
messageDiv.innerHTML = `
|
27 |
+
<div class="avatar">${data.model.substring(0, 1).toUpperCase()}</div>
|
28 |
+
<div class="content">
|
29 |
+
<div class="sender">${data.model} (${data.role})</div>
|
30 |
+
<div class="text"><i>正在思考...</i></div>
|
31 |
+
</div>`;
|
32 |
+
outputDiv.appendChild(messageDiv);
|
33 |
+
currentMessageElement = messageDiv.querySelector('.text');
|
34 |
+
break;
|
35 |
+
case 'stream_content':
|
36 |
+
if (currentMessageElement) {
|
37 |
+
currentMessageContent += data.content;
|
38 |
+
currentMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(currentMessageContent));
|
39 |
+
}
|
40 |
+
break;
|
41 |
+
case 'stream_end':
|
42 |
+
currentMessageElement = null;
|
43 |
+
break;
|
44 |
+
case 'debate_ended':
|
45 |
+
case 'debate_stopped':
|
46 |
+
const endMsg = document.createElement('div');
|
47 |
+
endMsg.className = 'round-separator';
|
48 |
+
endMsg.textContent = data.message;
|
49 |
+
outputDiv.appendChild(endMsg);
|
50 |
+
startBtn.disabled = false; stopBtn.disabled = true;
|
51 |
+
break;
|
52 |
+
case 'error':
|
53 |
+
outputDiv.innerHTML += `<div class="round-separator" style="color: red;">错误: ${data.message}</div>`;
|
54 |
+
break;
|
55 |
+
}
|
56 |
+
if(shouldScroll) {
|
57 |
+
outputDiv.scrollTop = outputDiv.scrollHeight;
|
58 |
+
}
|
59 |
+
}
|
60 |
+
|
61 |
+
function connect() {
|
62 |
+
const wsUrl = `ws://${window.location.host}/ws`;
|
63 |
+
websocket = new WebSocket(wsUrl);
|
64 |
+
websocket.onopen = () => { isConnected = true; startBtn.disabled = false; };
|
65 |
+
websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
|
66 |
+
websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; };
|
67 |
+
websocket.onerror = (error) => { console.error('WebSocket Error:', error); };
|
68 |
+
}
|
69 |
+
|
70 |
+
window.addEventListener('load', connect);
|
71 |
+
|
72 |
+
startBtn.addEventListener('click', () => {
|
73 |
+
if (!websocket) return;
|
74 |
+
const message = {
|
75 |
+
action: "start_debate",
|
76 |
+
topic: document.getElementById('topic').value,
|
77 |
+
rounds: parseInt(document.getElementById('rounds').value),
|
78 |
+
first_model: document.getElementById('firstModel').value,
|
79 |
+
initial_prompt: document.getElementById('initialPrompt').value
|
80 |
+
};
|
81 |
+
websocket.send(JSON.stringify(message));
|
82 |
+
startBtn.disabled = true; stopBtn.disabled = false;
|
83 |
+
});
|
84 |
+
stopBtn.addEventListener('click', () => {
|
85 |
+
if (!websocket) return;
|
86 |
+
websocket.send(JSON.stringify({ action: "stop_debate" }));
|
87 |
+
});
|
templates/index.html
CHANGED
@@ -5,59 +5,7 @@
|
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
<title>AI大模型辩论系统</title>
|
8 |
-
<style>
|
9 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #333; height: 100vh; display: flex; flex-direction: column; }
|
10 |
-
.container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 95%; max-width: 1400px; margin: 20px auto; flex-grow: 1; display: flex; flex-direction: column; }
|
11 |
-
.main-layout { display: flex; gap: 20px; flex-grow: 1; min-height: 0; }
|
12 |
-
.sidebar { flex: 0 0 320px; display: flex; flex-direction: column; gap: 15px; }
|
13 |
-
.chat-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
14 |
-
.header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
|
15 |
-
.header h1 { color: #1a73e8; }
|
16 |
-
.controls, .control-group { margin-bottom: 20px; }
|
17 |
-
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
18 |
-
.control-group { flex: 1; min-width: 200px; }
|
19 |
-
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
|
20 |
-
input, select, button, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px; }
|
21 |
-
textarea { resize: vertical; }
|
22 |
-
button { background-color: #1a73e8; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
|
23 |
-
button:hover { background-color: #1558b8; }
|
24 |
-
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
25 |
-
.output-container { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; height: 500px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
|
26 |
-
.status { padding: 10px; border-radius: 6px; margin-bottom: 15px; border: 1px solid; }
|
27 |
-
.status.connected { background-color: #e6f4ea; color: #155724; border-color: #c3e6cb; }
|
28 |
-
.status.disconnected { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
29 |
-
|
30 |
-
/* 聊天气泡样式 */
|
31 |
-
.message { display: flex; align-items: flex-start; gap: 10px; max-width: 80%; }
|
32 |
-
.message .avatar { width: 40px; height: 40px; border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; font-size: 18px; }
|
33 |
-
.message .content { background-color: #ffffff; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
34 |
-
.message .sender { font-weight: bold; margin-bottom: 5px; color: #333; }
|
35 |
-
.message.glm45 { align-self: flex-start; }
|
36 |
-
.message.glm45 .avatar { background-color: #34a853; } /* Google Green */
|
37 |
-
.message.glm45 .content { border-top-left-radius: 4px; }
|
38 |
-
.message.deepseek_v31 { align-self: flex-end; flex-direction: row-reverse; }
|
39 |
-
.message.deepseek_v31 .avatar { background-color: #4285f4; } /* Google Blue */
|
40 |
-
.message.deepseek_v31 .content { background-color: #e7f3ff; border-top-right-radius: 4px; }
|
41 |
-
.message .text { white-space: pre-wrap; word-wrap: break-word; }
|
42 |
-
.message .text p { margin: 0 0 10px; }
|
43 |
-
.message .text h1, .message .text h2, .message .text h3 { margin: 15px 0 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
44 |
-
.message .text ul, .message .text ol { padding-left: 20px; }
|
45 |
-
.message .text code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
|
46 |
-
.message .text pre { background-color: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; }
|
47 |
-
.message .text pre code { background-color: transparent; padding: 0; }
|
48 |
-
.round-separator { text-align: center; color: #888; font-size: 0.9em; margin: 20px 0; font-weight: 600; }
|
49 |
-
|
50 |
-
/* 响应式设计 - 针对手机等小屏幕设备 */
|
51 |
-
@media (max-width: 768px) {
|
52 |
-
body { padding: 0; }
|
53 |
-
.container { width: 100%; margin: 0; border-radius: 0; padding: 10px; height: 100%; }
|
54 |
-
.main-layout { flex-direction: column; }
|
55 |
-
.sidebar { flex: 0 0 auto; }
|
56 |
-
.header h1 { font-size: 1.5em; }
|
57 |
-
.controls { flex-direction: column; }
|
58 |
-
.control-group { min-width: unset; }
|
59 |
-
}
|
60 |
-
</style>
|
61 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
62 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
63 |
</head>
|
@@ -85,94 +33,6 @@
|
|
85 |
</div>
|
86 |
</div>
|
87 |
</div>
|
88 |
-
<script>
|
89 |
-
let websocket = null, isConnected = false, currentMessageElement = null, currentMessageContent = '';
|
90 |
-
const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn');
|
91 |
-
const outputDiv = document.getElementById('output');
|
92 |
-
|
93 |
-
function handleWebSocketMessage(data) {
|
94 |
-
let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10;
|
95 |
-
|
96 |
-
switch (data.type) {
|
97 |
-
case 'debate_started':
|
98 |
-
outputDiv.innerHTML = '';
|
99 |
-
const topicDiv = document.createElement('div');
|
100 |
-
topicDiv.className = 'round-separator';
|
101 |
-
topicDiv.innerHTML = `<strong>话题:</strong> ${data.topic}`;
|
102 |
-
outputDiv.appendChild(topicDiv);
|
103 |
-
break;
|
104 |
-
case 'round_info':
|
105 |
-
const separator = document.createElement('div');
|
106 |
-
separator.className = 'round-separator';
|
107 |
-
separator.textContent = data.message.trim();
|
108 |
-
outputDiv.appendChild(separator);
|
109 |
-
break;
|
110 |
-
case 'model_speaking':
|
111 |
-
currentMessageContent = ''; // 重置当前消息内容
|
112 |
-
const messageDiv = document.createElement('div');
|
113 |
-
messageDiv.className = `message ${data.model}`;
|
114 |
-
messageDiv.innerHTML = `
|
115 |
-
<div class="avatar">${data.model.substring(0, 1).toUpperCase()}</div>
|
116 |
-
<div class="content">
|
117 |
-
<div class="sender">${data.model} (${data.role})</div>
|
118 |
-
<div class="text"><i>正在思考...</i></div>
|
119 |
-
</div>`;
|
120 |
-
outputDiv.appendChild(messageDiv);
|
121 |
-
currentMessageElement = messageDiv.querySelector('.text');
|
122 |
-
break;
|
123 |
-
case 'stream_content':
|
124 |
-
if (currentMessageElement) {
|
125 |
-
currentMessageContent += data.content;
|
126 |
-
currentMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(currentMessageContent));
|
127 |
-
}
|
128 |
-
break;
|
129 |
-
case 'stream_end':
|
130 |
-
currentMessageElement = null;
|
131 |
-
break;
|
132 |
-
case 'debate_ended':
|
133 |
-
case 'debate_stopped':
|
134 |
-
const endMsg = document.createElement('div');
|
135 |
-
endMsg.className = 'round-separator';
|
136 |
-
endMsg.textContent = data.message;
|
137 |
-
outputDiv.appendChild(endMsg);
|
138 |
-
startBtn.disabled = false; stopBtn.disabled = true;
|
139 |
-
break;
|
140 |
-
case 'error':
|
141 |
-
outputDiv.innerHTML += `<div class="round-separator" style="color: red;">错误: ${data.message}</div>`;
|
142 |
-
break;
|
143 |
-
}
|
144 |
-
if(shouldScroll) {
|
145 |
-
outputDiv.scrollTop = outputDiv.scrollHeight;
|
146 |
-
}
|
147 |
-
}
|
148 |
-
|
149 |
-
function connect() {
|
150 |
-
const wsUrl = `ws://${window.location.host}/ws`;
|
151 |
-
websocket = new WebSocket(wsUrl);
|
152 |
-
websocket.onopen = () => { isConnected = true; startBtn.disabled = false; };
|
153 |
-
websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
|
154 |
-
websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; };
|
155 |
-
websocket.onerror = (error) => { console.error('WebSocket Error:', error); };
|
156 |
-
}
|
157 |
-
|
158 |
-
window.addEventListener('load', connect);
|
159 |
-
|
160 |
-
startBtn.addEventListener('click', () => {
|
161 |
-
if (!websocket) return;
|
162 |
-
const message = {
|
163 |
-
action: "start_debate",
|
164 |
-
topic: document.getElementById('topic').value,
|
165 |
-
rounds: parseInt(document.getElementById('rounds').value),
|
166 |
-
first_model: document.getElementById('firstModel').value,
|
167 |
-
initial_prompt: document.getElementById('initialPrompt').value
|
168 |
-
};
|
169 |
-
websocket.send(JSON.stringify(message));
|
170 |
-
startBtn.disabled = true; stopBtn.disabled = false;
|
171 |
-
});
|
172 |
-
stopBtn.addEventListener('click', () => {
|
173 |
-
if (!websocket) return;
|
174 |
-
websocket.send(JSON.stringify({ action: "stop_debate" }));
|
175 |
-
});
|
176 |
-
</script>
|
177 |
</body>
|
178 |
</html>
|
|
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
<title>AI大模型辩论系统</title>
|
8 |
+
<link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
10 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
11 |
</head>
|
|
|
33 |
</div>
|
34 |
</div>
|
35 |
</div>
|
36 |
+
<script src="{{ url_for('static', path='js/script.js') }}"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
</body>
|
38 |
</html>
|