/** * 计划卡片组件 * * 功能说明: * - 显示执行计划的卡片界面 * - 包含计划标题、摘要和步骤列表 * - 摘要支持 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; } .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: 10px; padding: 14px 16px; border-top: 1px solid var(--vscode-input-border); background: var(--vscode-sideBar-background); } .plan-actions .question-options { display: flex; flex-wrap: wrap; gap: 8px; } .plan-btn { padding: 8px 18px; border-radius: 4px; border: none; cursor: pointer; font-size: 12px; font-weight: 500; } .plan-btn-confirm { background: var(--vscode-button-background); color: var(--vscode-button-foreground); } .plan-btn-confirm:hover { background: var(--vscode-button-hoverBackground); } .plan-btn-modify { background: var(--vscode-input-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-input-border); } .plan-btn-cancel { background: transparent; color: var(--vscode-descriptionForeground); } .plan-actions .custom-input-container { display: flex; gap: 8px; width: 100%; } .plan-actions .custom-input { flex: 1; padding: 8px 12px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; font-size: 13px; } .plan-actions .custom-submit { padding: 8px 18px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } .plan-actions .custom-submit:hover { background: var(--vscode-button-hoverBackground); } /* 阶段进度条样式 */ .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 || []) : ''; // 选项按钮 const options = ['确认执行', '修改计划', '取消']; const optionsHtml = options.map(opt => { const isSelected = isAnswered && opt === selectedAnswer; return \`\`; }).join(''); // 渲染 Markdown 格式的摘要 const summaryHtml = renderPlanMarkdown(segment.planSummary || ''); segmentDiv.innerHTML = \`
    ${plannerIconSvg} \${segment.planTitle || '执行计划'}
    \${progressHtml}
    \${summaryHtml}
    \${hasPhases ? \`
    \${phasesHtml}
    \` : \`
    \${stepsHtml}
    \`}
    \${optionsHtml}
    \`; // 只在未回答时添加事件监听 if (!isAnswered) { setTimeout(() => { const optionButtons = segmentDiv.querySelectorAll('.question-option'); optionButtons.forEach(btn => { btn.addEventListener('click', function() { const option = this.getAttribute('data-option'); // 发送答案到后端 handleQuestionAnswerInSegment(segment.askId, option, segmentDiv); // 同时发送 planAction 用于模式切换 const actionMap = { '确认执行': 'confirm', '修改计划': 'modify', '取消': 'cancel' }; vscode.postMessage({ command: 'planAction', action: actionMap[option] || option, planTitle: segment.planTitle, model: getCurrentModel() }); }); }); const submitBtn = segmentDiv.querySelector('.custom-submit'); const customInput = segmentDiv.querySelector('.custom-input'); if (submitBtn && customInput) { submitBtn.addEventListener('click', function() { const customValue = customInput.value.trim(); if (customValue) { handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); } }); customInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { const customValue = customInput.value.trim(); if (customValue) { handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); } } }); } }, 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 planCard = segmentDiv.querySelector('.plan-card'); if (planCard) { planCard.querySelectorAll('.plan-btn').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.currentTarget?.dataset?.action; vscode.postMessage({ command: 'planAction', action: action, planTitle: segment.planTitle, model: getCurrentModel() }); }); }); } }, 0); } `; }