feat: Plan卡片支持Markdown渲染和智能步骤解析
- 添加renderPlanMarkdown函数,支持标题、列表、表格、代码块等 - 添加renderPlanSteps函数,智能解析JSON格式步骤对象 - 步骤显示模块名、描述、输入输出、逻辑等详细信息 - 添加plan-summary和step-details样式
This commit is contained in:
@ -4,6 +4,7 @@
|
|||||||
* 功能说明:
|
* 功能说明:
|
||||||
* - 显示执行计划的卡片界面
|
* - 显示执行计划的卡片界面
|
||||||
* - 包含计划标题、摘要和步骤列表
|
* - 包含计划标题、摘要和步骤列表
|
||||||
|
* - 摘要支持 Markdown 格式渲染
|
||||||
* - 提供确认执行、修改计划、取消等操作按钮
|
* - 提供确认执行、修改计划、取消等操作按钮
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -43,11 +44,62 @@ export function getPlanCardStyles(): string {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.plan-summary {
|
.plan-summary {
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-foreground);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
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: 24px;
|
||||||
|
}
|
||||||
|
.plan-summary li { margin: 4px 0; }
|
||||||
|
.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 {
|
.plan-steps {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@ -58,6 +110,15 @@ export function getPlanCardStyles(): string {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 1.5;
|
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 {
|
.plan-step:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@ -158,6 +219,117 @@ export function getPlanCardStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getPlanCardScript(): string {
|
export function getPlanCardScript(): string {
|
||||||
return `
|
return `
|
||||||
|
// 简单的 Markdown 渲染函数
|
||||||
|
function renderPlanMarkdown(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
let html = text;
|
||||||
|
|
||||||
|
// 转义 HTML 特殊字符(保留换行)
|
||||||
|
html = html.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// 代码块 (\`\`\`code\`\`\`)
|
||||||
|
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
|
||||||
|
|
||||||
|
// 行内代码 (\`code\`)
|
||||||
|
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// 表格处理
|
||||||
|
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 = '<table><thead><tr>';
|
||||||
|
headers.forEach(h => table += '<th>' + h + '</th>');
|
||||||
|
table += '</tr></thead><tbody>';
|
||||||
|
rows.forEach(row => {
|
||||||
|
table += '<tr>';
|
||||||
|
row.forEach(cell => table += '<td>' + cell + '</td>');
|
||||||
|
table += '</tr>';
|
||||||
|
});
|
||||||
|
table += '</tbody></table>';
|
||||||
|
return table;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
||||||
|
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// 粗体和斜体
|
||||||
|
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
||||||
|
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// 无序列表
|
||||||
|
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
|
||||||
|
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
||||||
|
|
||||||
|
// 有序列表
|
||||||
|
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
|
||||||
|
|
||||||
|
// 段落(连续的非空行)
|
||||||
|
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
|
||||||
|
|
||||||
|
// 清理多余的空行
|
||||||
|
html = html.replace(/<p><\\/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 = '<strong>' + name + '</strong>';
|
||||||
|
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 += '<div class="step-details">' + details.join(' | ') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通字符串
|
||||||
|
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
||||||
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||||
segmentDiv.className += ' segment-plan';
|
segmentDiv.className += ' segment-plan';
|
||||||
@ -170,9 +342,8 @@ export function getPlanCardScript(): string {
|
|||||||
segmentDiv.classList.add('answered');
|
segmentDiv.classList.add('answered');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
// 解析并渲染步骤
|
||||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
const stepsHtml = renderPlanSteps(segment.planSteps || []);
|
||||||
).join('');
|
|
||||||
|
|
||||||
// 选项按钮
|
// 选项按钮
|
||||||
const options = ['确认执行', '修改计划', '取消'];
|
const options = ['确认执行', '修改计划', '取消'];
|
||||||
@ -181,6 +352,9 @@ export function getPlanCardScript(): string {
|
|||||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// 渲染 Markdown 格式的摘要
|
||||||
|
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="plan-card">
|
<div class="plan-card">
|
||||||
<div class="plan-header">
|
<div class="plan-header">
|
||||||
@ -188,7 +362,7 @@ export function getPlanCardScript(): string {
|
|||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-body">
|
<div class="plan-body">
|
||||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
<div class="plan-summary">\${summaryHtml}</div>
|
||||||
<div class="plan-steps">\${stepsHtml}</div>
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-actions">
|
<div class="plan-actions">
|
||||||
@ -250,9 +424,10 @@ export function getPlanCardScript(): string {
|
|||||||
// 渲染计划卡片(在 renderSegments 中使用)
|
// 渲染计划卡片(在 renderSegments 中使用)
|
||||||
function renderPlanCardStatic(segment, segmentDiv) {
|
function renderPlanCardStatic(segment, segmentDiv) {
|
||||||
segmentDiv.className += ' segment-plan';
|
segmentDiv.className += ' segment-plan';
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
const stepsHtml = renderPlanSteps(segment.planSteps || []);
|
||||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
|
||||||
).join('');
|
// 渲染 Markdown 格式的摘要
|
||||||
|
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="plan-card">
|
<div class="plan-card">
|
||||||
@ -261,7 +436,7 @@ export function getPlanCardScript(): string {
|
|||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-body">
|
<div class="plan-body">
|
||||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
<div class="plan-summary">\${summaryHtml}</div>
|
||||||
<div class="plan-steps">\${stepsHtml}</div>
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-actions">
|
<div class="plan-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user