- core.py: 注入ws_push回调, 17个推理节点实时推送事件(上下文/规划/工具调用/执行) - wwwroot/reasoning_console.wss: 新建websocket端点, 支持connect/start_reasoning/ping - wwwroot/reasoning_console.ui: 重写HTML前端, 时间线式可视化展示推理过程
6 lines
12 KiB
XML
6 lines
12 KiB
XML
{
|
||
"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>"
|
||
}
|
||
} |