/** * 计划卡片组件 * * 功能说明: * - 显示执行计划的卡片界面 * - 包含计划标题、摘要和步骤列表 * - 摘要支持 Markdown 格式渲染 * - 提供确认执行、修改计划、取消等操作按钮 */ import { plannerIconSvg } from "../constants/toolIcons"; /** * 获取计划卡片的样式 */ export function getPlanCardStyles(): string { return ` /* 计划卡片样式 */ .segment-plan { margin: 12px 0; } .plan-card { border: 1px solid var(--vscode-input-border); border-radius: 8px; overflow: hidden; background: var(--vscode-editor-background); } .plan-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: var(--vscode-sideBar-background); border-bottom: 1px solid var(--vscode-input-border); } .plan-icon { font-size: 18px; } .plan-title { font-weight: 600; font-size: 14px; } .plan-body { padding: 16px; } .plan-summary { color: var(--vscode-foreground); margin-bottom: 12px; font-size: 13px; line-height: 1.6; } /* Markdown 渲染样式 */ .plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 { margin: 16px 0 8px 0; font-weight: 600; color: var(--vscode-foreground); } .plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; } .plan-summary h2 { font-size: 16px; } .plan-summary h3 { font-size: 14px; } .plan-summary h4 { font-size: 13px; } .plan-summary p { margin: 8px 0; letter-spacing: 0.5px; } .plan-summary ul, .plan-summary ol { margin: 8px 0; padding-left: 0; } .plan-summary li { margin: 4px 0 4px 27px; } .plan-summary code { background: var(--vscode-textCodeBlock-background); padding: 2px 6px; border-radius: 3px; font-family: var(--vscode-editor-font-family); font-size: 12px; } .plan-summary pre { background: var(--vscode-textCodeBlock-background); padding: 12px; border-radius: 4px; overflow-x: auto; margin: 8px 0; } .plan-summary pre code { background: none; padding: 0; } .plan-summary table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 12px; } .plan-summary th, .plan-summary td { border: 1px solid var(--vscode-input-border); padding: 6px 10px; text-align: left; } .plan-summary th { background: var(--vscode-sideBar-background); font-weight: 600; } .plan-summary strong { font-weight: 600; } .plan-summary em { font-style: italic; } .plan-steps { font-size: 13px; } .plan-step { padding: 8px 12px; margin-bottom: 6px; background: var(--vscode-list-hoverBackground); border-radius: 4px; line-height: 1.5; } .plan-step strong { color: var(--vscode-textLink-foreground); } .step-details { margin-top: 4px; font-size: 12px; color: var(--vscode-descriptionForeground); line-height: 1.4; } .plan-step:last-child { margin-bottom: 0; } .step-checkbox { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin-right: 8px; border: 2px solid var(--vscode-textLink-foreground); border-radius: 4px; background: transparent; flex-shrink: 0; opacity: 0.6; transition: all 0.2s ease; } .step-checkbox.completed { background: var(--vscode-textLink-foreground); border-color: var(--vscode-textLink-foreground); opacity: 1; } .step-checkbox.completed::after { content: '✓'; color: var(--vscode-editor-background); font-size: 11px; font-weight: bold; } .plan-actions { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; border-top: 1px solid var(--vscode-input-border); background: var(--vscode-sideBar-background); } .plan-input-row { display: flex; gap: 8px; width: 100%; } .plan-input { flex: 1; padding: 10px 12px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; font-size: 13px; box-sizing: border-box; } .plan-input:focus { outline: none; border-color: var(--vscode-focusBorder); } .plan-btn-row { display: flex; gap: 10px; } .plan-btn { padding: 8px 20px; border-radius: 4px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; } .plan-btn-submit { background: var(--vscode-input-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-input-border); } .plan-btn-submit:hover { background: var(--vscode-list-hoverBackground); } .plan-btn-confirm { background: #007ACC; color: #ffffff; } .plan-btn-confirm:hover { background: #005a9e; } .plan-btn-cancel { background: transparent; color: var(--vscode-descriptionForeground); border: 1px solid var(--vscode-input-border); } .plan-btn-cancel:hover { background: var(--vscode-list-hoverBackground); } .plan-answered { padding: 12px 16px; border-top: 1px solid var(--vscode-input-border); background: var(--vscode-sideBar-background); font-size: 13px; } .answered-label { color: var(--vscode-descriptionForeground); } .answered-value { color: var(--vscode-textLink-foreground); font-weight: 500; } /* 阶段进度条样式 */ .phase-progress { display: flex; align-items: center; padding: 12px 16px; background: var(--vscode-sideBar-background); border-bottom: 1px solid var(--vscode-input-border); } .phase-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vscode-descriptionForeground); } .phase-item.current { color: var(--vscode-textLink-foreground); font-weight: 600; } .phase-item.completed { color: #4caf50; } .phase-item.skipped { color: var(--vscode-descriptionForeground); opacity: 0.6; } .phase-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--vscode-input-border); flex-shrink: 0; } .phase-dot.current { background: var(--vscode-textLink-foreground); box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2); } .phase-dot.completed { background: #4caf50; } .phase-dot.skipped { background: var(--vscode-descriptionForeground); opacity: 0.5; } .phase-line { flex: 1; height: 2px; background: var(--vscode-input-border); margin: 0 8px; } .phase-line.completed { background: #4caf50; } /* 阶段列表样式 */ .plan-phases { font-size: 13px; } .plan-phase { margin-bottom: 12px; border: 1px solid var(--vscode-input-border); border-radius: 6px; overflow: hidden; } .plan-phase:last-child { margin-bottom: 0; } .phase-header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--vscode-list-hoverBackground); cursor: pointer; user-select: none; } .phase-header:hover { background: var(--vscode-list-activeSelectionBackground); } .phase-toggle { font-size: 10px; color: var(--vscode-descriptionForeground); transition: transform 0.2s; } .phase-toggle.expanded { transform: rotate(90deg); } .phase-name { flex: 1; font-weight: 500; } .phase-status { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); } .phase-status.current { background: var(--vscode-textLink-foreground); color: white; } .phase-status.skipped { background: var(--vscode-descriptionForeground); opacity: 0.6; } .phase-status.completed { background: #4caf50; color: white; } .phase-content { padding: 0 12px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; } .phase-content.expanded { padding: 12px; max-height: 500px; } .phase-reason { font-size: 12px; color: var(--vscode-descriptionForeground); font-style: italic; margin-bottom: 8px; } .phase-steps { margin: 0; padding: 0; list-style: none; } .phase-step-item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--vscode-input-border); } .phase-step-item:last-child { border-bottom: none; } .phase-step-checkbox { width: 14px; height: 14px; border: 2px solid var(--vscode-textLink-foreground); border-radius: 3px; flex-shrink: 0; margin-top: 2px; } .phase-step-text { flex: 1; } .phase-step-name { font-weight: 500; color: var(--vscode-foreground); } .phase-step-desc { font-size: 12px; color: var(--vscode-descriptionForeground); margin-top: 2px; } `; } /** * 获取计划卡片的脚本 */ export function getPlanCardScript(): string { return ` // 简单的 Markdown 渲染函数 function renderPlanMarkdown(text) { if (!text) return ''; let html = text; // 转义 HTML 特殊字符(保留换行) html = html.replace(/&/g, '&') .replace(//g, '>'); // 标题(必须在转义之后、其他处理之前) html = html.replace(/^#### (.+)$/gm, '

$1

'); html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // 代码块 (\`\`\`code\`\`\`) html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '
$1
'); // 行内代码 (\`code\`) html = html.replace(/\\x60([^\\x60]+)\\x60/g, '$1'); // 表格处理 html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) { const headers = header.split('|').map(h => h.trim()).filter(h => h); const rows = body.trim().split('\\n').map(row => row.split('|').map(cell => cell.trim()).filter(cell => cell) ); let table = ''; headers.forEach(h => table += ''); table += ''; rows.forEach(row => { table += ''; row.forEach(cell => table += ''); table += ''; }); table += '
' + h + '
' + cell + '
'; return table; }); // 粗体和斜体 html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1'); html = html.replace(/\\*(.+?)\\*/g, '$1'); // 无序列表 html = html.replace(/^[\\s]*[-*] (.+)$/gm, '
  • $1
  • '); html = html.replace(/(
  • .*<\\/li>\\n?)+/g, ''); // 有序列表 html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '
  • $1
  • '); // 段落(连续的非空行) html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '

    $1

    '); // 清理多余的空行 html = html.replace(/

    <\\/p>/g, ''); html = html.replace(/\\n{2,}/g, '\\n'); return html; } // 解析并渲染步骤列表 function renderPlanSteps(steps) { if (!steps || steps.length === 0) return ''; // 尝试解析 JSON 格式的步骤 let parsedSteps = steps; // 如果是单个字符串且看起来像 JSON 数组,尝试解析 if (steps.length === 1 && typeof steps[0] === 'string') { const str = steps[0].trim(); if (str.startsWith('[') && str.endsWith(']')) { try { parsedSteps = JSON.parse(str); } catch (e) { // 解析失败,保持原样 } } } return parsedSteps.map((step, i) => { // 如果是对象,格式化显示 if (typeof step === 'object' && step !== null) { const name = step.name || step.id || ('步骤 ' + (i + 1)); const desc = step.description || ''; const inputs = step.inputs || ''; const outputs = step.outputs || ''; const logic = step.logic || ''; let content = '' + name + ''; if (desc) content += ':' + desc; let details = []; if (inputs) details.push('输入: ' + inputs); if (outputs) details.push('输出: ' + outputs); if (logic) details.push('逻辑: ' + logic); if (details.length > 0) { content += '

    ' + details.join(' | ') + '
    '; } return '
    ' + content + '
    '; } // 普通字符串 return '
    ' + step + '
    '; }).join(''); } // 渲染阶段进度条 function renderPhaseProgress(phases) { if (!phases || phases.length === 0) return ''; const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' }; let html = '
    '; phases.forEach((phase, i) => { const name = phaseNames[phase.id] || phase.name || phase.id; const status = phase.status || 'pending'; html += \`
    \${name}
    \`; // 添加连接线(最后一个不加) if (i < phases.length - 1) { const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : ''; html += \`
    \`; } }); html += '
    '; return html; } // 渲染阶段列表(两级结构) function renderPlanPhases(phases) { if (!phases || phases.length === 0) return ''; const statusLabels = { skipped: '跳过', completed: '已完成', current: '当前', pending: '待执行' }; return phases.map((phase, i) => { const status = phase.status || 'pending'; const statusLabel = statusLabels[status] || status; const isExpanded = status === 'current'; const hasSteps = phase.steps && phase.steps.length > 0; const hasReason = phase.reason && status === 'skipped'; let stepsHtml = ''; if (phase.steps && phase.steps.length > 0) { stepsHtml = phase.steps.map(step => \`
  • \${step.name || ''}
    \${step.description ? \`
    \${step.description}
    \` : ''}
  • \`).join(''); } return \`
    \${phase.name || phase.id} \${statusLabel}
    \${hasReason ? \`
    \${phase.reason}
    \` : ''} \${hasSteps ? \`\` : ''} \${!hasSteps && !hasReason ? '
    暂无步骤
    ' : ''}
    \`; }).join(''); } // 切换阶段展开/折叠 function togglePhase(header) { const toggle = header.querySelector('.phase-toggle'); const content = header.nextElementSibling; toggle.classList.toggle('expanded'); content.classList.toggle('expanded'); } // 渲染计划卡片(在 updateSegmentsRealtime 中使用) function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) { segmentDiv.className += ' segment-plan'; // 检查是否已回答 const isAnswered = answeredQuestions.has(segment.askId); const selectedAnswer = answeredQuestions.get(segment.askId); if (isAnswered) { segmentDiv.classList.add('answered'); } // 判断是否有 phases(新格式)还是 steps(旧格式) const hasPhases = segment.planPhases && segment.planPhases.length > 0; // 渲染阶段进度条和阶段列表(新格式) const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : ''; const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : ''; // 兼容旧格式:渲染步骤列表 const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : ''; // 渲染 Markdown 格式的摘要 const summaryHtml = renderPlanMarkdown(segment.planSummary || ''); // 已回答时显示用户的选择 const answeredHtml = isAnswered ? \`
    已回复: \${selectedAnswer}
    \` : ''; segmentDiv.innerHTML = \`
    ${plannerIconSvg} \${segment.planTitle || '执行计划'}
    \${progressHtml}
    \${summaryHtml}
    \${hasPhases ? \`
    \${phasesHtml}
    \` : \`
    \${stepsHtml}
    \`}
    \${answeredHtml}
    \`; // 只在未回答时添加事件监听 if (!isAnswered) { setTimeout(() => { const submitBtn = segmentDiv.querySelector('.plan-btn-submit'); const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm'); const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel'); const planInput = segmentDiv.querySelector('.plan-input'); // 提交修改按钮 if (submitBtn && planInput) { submitBtn.addEventListener('click', function() { const inputValue = planInput.value.trim(); if (inputValue) { handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv); } }); // 回车键提交修改 planInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { const inputValue = planInput.value.trim(); if (inputValue) { handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv); } } }); } // 确认执行按钮 if (confirmBtn) { confirmBtn.addEventListener('click', function() { handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv); }); } // 取消按钮 - 直接中止对话,不发送给智能体 if (cancelBtn) { cancelBtn.addEventListener('click', function() { // 标记问题已回答 answeredQuestions.set(segment.askId, '取消'); segmentDiv.classList.add('answered'); // 隐藏操作按钮 const actionsDiv = segmentDiv.querySelector('.plan-actions'); if (actionsDiv) { actionsDiv.style.display = 'none'; } // 发送中止对话命令 vscode.postMessage({ command: 'abortDialog' }); }); } }, 0); } } // 渲染计划卡片(在 renderSegments 中使用) function renderPlanCardStatic(segment, segmentDiv) { segmentDiv.className += ' segment-plan'; // 判断是否有 phases(新格式)还是 steps(旧格式) const hasPhases = segment.planPhases && segment.planPhases.length > 0; // 渲染阶段进度条和阶段列表(新格式) const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : ''; const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : ''; // 兼容旧格式:渲染步骤列表 const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : ''; // 渲染 Markdown 格式的摘要 const summaryHtml = renderPlanMarkdown(segment.planSummary || ''); segmentDiv.innerHTML = \`
    \${segment.planTitle || '执行计划'}
    \${progressHtml}
    \${summaryHtml}
    \${hasPhases ? \`
    \${phasesHtml}
    \` : \`
    \${stepsHtml}
    \`}
    \`; // 绑定按钮事件(静态渲染时也需要能响应) setTimeout(() => { const submitBtn = segmentDiv.querySelector('.plan-btn-submit'); const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm'); const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel'); const planInput = segmentDiv.querySelector('.plan-input'); // 提交修改按钮 if (submitBtn && planInput) { submitBtn.addEventListener('click', function() { const inputValue = planInput.value.trim(); if (inputValue) { vscode.postMessage({ command: 'submitAnswer', askId: segment.askId, selected: [inputValue], customInput: inputValue }); } }); } // 确认执行按钮 if (confirmBtn) { confirmBtn.addEventListener('click', function() { vscode.postMessage({ command: 'submitAnswer', askId: segment.askId, selected: ['确认执行'], customInput: '确认执行' }); }); } // 取消按钮 - 直接中止对话 if (cancelBtn) { cancelBtn.addEventListener('click', function() { // 隐藏操作按钮 const actionsDiv = segmentDiv.querySelector('.plan-actions'); if (actionsDiv) { actionsDiv.style.display = 'none'; } // 发送中止对话命令 vscode.postMessage({ command: 'abortDialog' }); }); } }, 0); } `; }