harnessed_reasoning/wwwroot/reasoning_console.ui
yumoqing ea4a9e3bd9 feat: 推理过程可视化 - websocket实时推送推理步骤到前端
- core.py: 注入ws_push回调, 17个推理节点实时推送事件(上下文/规划/工具调用/执行)
- wwwroot/reasoning_console.wss: 新建websocket端点, 支持connect/start_reasoning/ping
- wwwroot/reasoning_console.ui: 重写HTML前端, 时间线式可视化展示推理过程
2026-05-09 15:48:12 +08:00

6 lines
12 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"widgettype": "HTML",
"options": {
"html": "<!DOCTYPE html>\n<html>\n<head>\n<style>\n* { margin: 0; padding: 0; box-sizing: border-box; }\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f1923; color: #e0e0e0; min-height: 100vh; }\n.container { max-width: 900px; margin: 0 auto; padding: 20px; }\n.header { display: flex; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid #1e3a5f; margin-bottom: 20px; }\n.header h1 { font-size: 22px; color: #4fc3f7; }\n.ws-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }\n.ws-badge.disconnected { background: #b71c1c; color: white; }\n.ws-badge.connecting { background: #f57f17; color: black; }\n.ws-badge.connected { background: #2e7d32; color: white; }\n.input-area { background: #1a2733; border: 1px solid #2d4a5e; border-radius: 12px; padding: 16px; margin-bottom: 16px; }\n.input-area textarea { width: 100%; min-height: 80px; background: transparent; border: none; color: #e0e0e0; font-size: 14px; resize: vertical; font-family: inherit; outline: none; }\n.input-area textarea::placeholder { color: #546e7a; }\n.btn-row { display: flex; gap: 10px; margin-top: 12px; }\n.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }\n.btn-primary { background: #2196F3; color: white; }\n.btn-primary:hover { background: #1976D2; }\n.btn-primary:disabled { background: #546e7a; cursor: not-allowed; }\n.btn-secondary { background: #37474f; color: #b0bec5; }\n.btn-secondary:hover { background: #455a64; }\n.status-bar { background: #1a2733; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 12px; }\n.status-icon { width: 8px; height: 8px; border-radius: 50%; background: #546e7a; }\n.status-icon.active { background: #4caf50; animation: pulse 1.5s infinite; }\n@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }\n.status-text { font-size: 13px; color: #90caf9; }\n.current-action { background: #1a2733; border: 1px solid #2d4a5e; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; display: none; }\n.current-action.visible { display: block; }\n.current-action-label { font-size: 13px; color: #4caf50; font-weight: 600; }\n.progress-bar { width: 100%; height: 4px; background: #263238; border-radius: 2px; margin-top: 8px; overflow: hidden; }\n.progress-fill { height: 100%; background: linear-gradient(90deg, #2196F3, #4fc3f7); transition: width 0.3s; width: 0%; }\n.timeline { position: relative; padding-left: 24px; }\n.timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: #263238; }\n.step { position: relative; margin-bottom: 12px; animation: fadeIn 0.3s ease; }\n@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }\n.step-dot { position: absolute; left: -20px; top: 12px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #546e7a; background: #0f1923; }\n.step-dot.success { border-color: #4caf50; background: #4caf50; }\n.step-dot.error { border-color: #f44336; background: #f44336; }\n.step-dot.info { border-color: #2196F3; background: #2196F3; }\n.step-dot.warning { border-color: #ff9800; background: #ff9800; }\n.step-dot.thinking { border-color: #ab47bc; background: #ab47bc; animation: pulse 1s infinite; }\n.step-card { background: #1a2733; border: 1px solid #2d4a5e; border-radius: 8px; padding: 12px 16px; }\n.step-card:hover { border-color: #3d6a8e; }\n.step-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n.step-type { font-size: 11px; padding: 2px 8px; border-radius: 4px; font-weight: 600; text-transform: uppercase; }\n.step-type.context { background: #1b5e20; color: #a5d6a7; }\n.step-type.plan { background: #0d47a1; color: #90caf9; }\n.step-type.tool { background: #4a148c; color: #ce93d8; }\n.step-type.execute { background: #e65100; color: #ffcc80; }\n.step-type.result { background: #1b5e20; color: #a5d6a7; }\n.step-type.error { background: #b71c1c; color: #ef9a9a; }\n.step-type.thinking { background: #4a148c; color: #ce93d8; }\n.step-time { font-size: 11px; color: #546e7a; }\n.step-message { font-size: 13px; color: #b0bec5; line-height: 1.5; }\n.step-details { margin-top: 8px; padding: 8px 12px; background: #0f1923; border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; color: #78909c; max-height: 120px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }\n.step-details::-webkit-scrollbar { width: 4px; }\n.step-details::-webkit-scrollbar-thumb { background: #37474f; border-radius: 2px; }\n.empty-state { text-align: center; padding: 60px 20px; color: #546e7a; }\n.empty-state svg { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.3; }\n.empty-state p { font-size: 14px; }\n</style>\n</head>\n<body>\n<div class=\"container\">\n <div class=\"header\">\n <h1>AI 推理控制台</h1>\n <span class=\"ws-badge disconnected\" id=\"wsBadge\">未连接</span>\n </div>\n <div class=\"input-area\">\n <textarea id=\"reasoningInput\" placeholder=\"输入你的请求AI ...\"></textarea>\n <div class=\"btn-row\">\n <button class=\"btn btn-primary\" id=\"startBtn\" onclick=\"sendReasoning()\">开始推理</button>\n <button class=\"btn btn-secondary\" onclick=\"clearTimeline()\">清空日志</button>\n </div>\n </div>\n <div class=\"status-bar\">\n <div class=\"status-icon\" id=\"statusIcon\"></div>\n <span class=\"status-text\" id=\"statusText\">等待连接...</span>\n </div>\n <div class=\"current-action\" id=\"currentAction\">\n <div class=\"current-action-label\" id=\"currentActionLabel\"></div>\n <div class=\"progress-bar\"><div class=\"progress-fill\" id=\"progressFill\"></div></div>\n </div>\n <div class=\"timeline\" id=\"timeline\">\n <div class=\"empty-state\" id=\"emptyState\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"/></svg>\n <p>输入请求后点击\"开始推理\"查看 AI 推理过程</p>\n </div>\n </div>\n</div>\n<script>\nlet ws = null;\nlet stepCount = 0;\nlet progress = 0;\n\nfunction getWsUrl() {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return protocol + '//' + window.location.host + '/harnessed_reasoning/reasoning_console.wss';\n}\n\nfunction connect() {\n updateStatus('connecting', '正在连接...');\n ws = new WebSocket(getWsUrl());\n ws.onopen = function() {\n updateStatus('connected', '已连接');\n ws.send(JSON.stringify({ cmd: 'connect', user_id: 'current_user' }));\n };\n ws.onmessage = function(e) {\n try {\n const msg = JSON.parse(e.data);\n handleMessage(msg);\n } catch(err) {\n console.error('Parse error:', err);\n }\n };\n ws.onerror = function() {\n updateStatus('disconnected', '连接错误');\n };\n ws.onclose = function() {\n updateStatus('disconnected', '已断开3秒后重连...');\n setTimeout(connect, 3000);\n };\n}\n\nfunction updateStatus(state, text) {\n const badge = document.getElementById('wsBadge');\n const icon = document.getElementById('statusIcon');\n const statusText = document.getElementById('statusText');\n badge.className = 'ws-badge ' + state;\n badge.textContent = state === 'connected' ? '已连接' : state === 'connecting' ? '连接中' : '未连接';\n icon.className = 'status-icon' + (state === 'connected' ? ' active' : '');\n statusText.textContent = text;\n}\n\nfunction handleMessage(msg) {\n if (msg.type === 'connected') return;\n if (msg.type === 'pong') return;\n if (msg.type === 'error') {\n var errData = msg.data || {};\n addStep('error', errData.message || msg.message || '错误', errData);\n document.getElementById('startBtn').disabled = false;\n return;\n }\n var event = msg.event || msg.type;\n var data = msg.data || {};\n var message = data.message || event;\n addStep(event, message, data);\n if (event === 'reasoning_complete' || event === 'execution_complete') {\n document.getElementById('startBtn').disabled = false;\n }\n}\n\nfunction addStep(event, message, data) {\n document.getElementById('emptyState').style.display = 'none';\n stepCount++;\n const timeline = document.getElementById('timeline');\n const step = document.createElement('div');\n step.className = 'step';\n let dotClass = 'info';\n let typeClass = 'info';\n let typeLabel = event;\n if (event.includes('context')) { dotClass = 'info'; typeClass = 'context'; typeLabel = '上下文'; }\n else if (event.includes('plan') || event.includes('thinking')) { dotClass = 'thinking'; typeClass = 'plan'; typeLabel = '思考'; }\n else if (event.includes('tool_call')) { dotClass = 'info'; typeClass = 'tool'; typeLabel = data.success === false ? '工具失败' : '工具调用'; }\n else if (event.includes('step_')) { dotClass = 'success'; typeClass = 'execute'; typeLabel = '执行步骤'; }\n else if (event.includes('complete') || event.includes('result')) { dotClass = 'success'; typeClass = 'result'; typeLabel = '完成'; }\n else if (event.includes('error') || event.includes('violation')) { dotClass = 'error'; typeClass = 'error'; typeLabel = '错误'; }\n else if (event.includes('safety')) { dotClass = 'warning'; typeClass = 'plan'; typeLabel = '安全检查'; }\n const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });\n let detailsHtml = '';\n if (data.request) detailsHtml += '<div class=\"step-details\">请求: ' + escapeHtml(data.request) + '</div>';\n if (data.analysis) detailsHtml += '<div class=\"step-details\">分析: ' + escapeHtml(data.analysis) + '</div>';\n if (data.tool) detailsHtml += '<div class=\"step-details\">工具: ' + data.tool + (data.parameters ? '\\n参数: ' + JSON.stringify(data.parameters) : '') + '</div>';\n if (data.result) detailsHtml += '<div class=\"step-details\">结果: ' + escapeHtml(String(data.result).substring(0, 500)) + '</div>';\n if (data.violations) detailsHtml += '<div class=\"step-details\">违规: ' + data.violations.join(', ') + '</div>';\n step.innerHTML = '<div class=\"step-dot ' + dotClass + '\"></div>' +\n '<div class=\"step-card\">' +\n '<div class=\"step-header\">' +\n '<span class=\"step-type ' + typeClass + '\">' + typeLabel + '</span>' +\n '<span class=\"step-time\">' + time + '</span>' +\n '</div>' +\n '<div class=\"step-message\">' + escapeHtml(message) + '</div>' +\n detailsHtml +\n '</div>';\n timeline.appendChild(step);\n if (event.includes('tool_call') || event.includes('step_')) {\n updateProgress(10);\n }\n if (event.includes('complete') && !event.includes('execution')) {\n updateProgress(100);\n }\n updateCurrentAction(message);\n step.scrollIntoView({ behavior: 'smooth', block: 'end' });\n}\n\nfunction updateProgress(delta) {\n progress = Math.min(100, progress + delta);\n document.getElementById('progressFill').style.width = progress + '%';\n}\n\nfunction updateCurrentAction(text) {\n const el = document.getElementById('currentAction');\n const label = document.getElementById('currentActionLabel');\n el.classList.add('visible');\n label.textContent = text;\n}\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\nfunction sendReasoning() {\n const input = document.getElementById('reasoningInput');\n const text = input.value.trim();\n if (!text) return;\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n alert('WebSocket 未连接');\n return;\n }\n document.getElementById('startBtn').disabled = true;\n progress = 0;\n document.getElementById('progressFill').style.width = '0%';\n ws.send(JSON.stringify({ cmd: 'start_reasoning', request: text, user_id: 'current_user' }));\n}\n\nfunction clearTimeline() {\n document.getElementById('timeline').innerHTML = '<div class=\"empty-state\" id=\"emptyState\"><svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"/></svg><p>输入请求后点击\"开始推理\"查看 AI 推理过程</p></div>';\n stepCount = 0;\n progress = 0;\n document.getElementById('progressFill').style.width = '0%';\n document.getElementById('currentAction').classList.remove('visible');\n document.getElementById('startBtn').disabled = false;\n}\n\nconnect();\n</script>\n</body>\n</html>"
}
}