Add exploration field rendering helpers for dynamic content display

- Implemented `renderExpField` to handle various data types for exploration fields.
- Created `renderExpArray` to format arrays, including support for objects with specific properties.
- Developed `renderExpObject` for recursive rendering of object values, filtering out private keys.
- Introduced HTML escaping for safe rendering of user-generated content.
This commit is contained in:
catlog22
2025-12-07 18:07:28 +08:00
parent 26a325efff
commit a6f9701679
6 changed files with 201 additions and 4549 deletions

View File

@@ -58,8 +58,9 @@ const MODULE_FILES = [
'components/notifications.js',
'components/mcp-manager.js',
'components/hook-manager.js',
'components/tabs-context.js',
'components/_exp_helpers.js',
'components/tabs-other.js',
'components/tabs-context.js',
'components/task-drawer-core.js',
'components/task-drawer-renderers.js',
'components/flowchart.js',

View File

@@ -0,0 +1,52 @@
// Helper: Render exploration field with smart type detection
function renderExpField(label, value) {
if (value === null || value === undefined) return '';
let rendered;
if (typeof value === 'string') {
rendered = `<p>${escapeHtml(value)}</p>`;
} else if (Array.isArray(value)) {
rendered = renderExpArray(value);
} else if (typeof value === 'object') {
rendered = renderExpObject(value);
} else {
rendered = `<p>${escapeHtml(String(value))}</p>`;
}
return `<div class="exp-field"><label>${escapeHtml(label)}</label>${rendered}</div>`;
}
// Helper: Render array values
function renderExpArray(arr) {
if (!arr.length) return '<p>-</p>';
if (typeof arr[0] === 'object' && arr[0] !== null) {
return `<div class="exp-array-objects">${arr.map(item => {
if (item.question) {
return `<div class="clarification-item">
<div class="clarification-question">${escapeHtml(item.question)}</div>
${item.impact ? `<div class="clarification-impact">Impact: ${escapeHtml(item.impact)}</div>` : ''}
${item.priority ? `<span class="priority-badge priority-${item.priority}">${item.priority}</span>` : ''}
</div>`;
}
return `<div class="exp-object-item">${renderExpObject(item)}</div>`;
}).join('')}</div>`;
}
return `<ul class="exp-list">${arr.map(item => `<li>${escapeHtml(String(item))}</li>`).join('')}</ul>`;
}
// Helper: Render object values recursively
function renderExpObject(obj) {
if (!obj || typeof obj !== 'object') return '';
const entries = Object.entries(obj).filter(([k]) => !k.startsWith('_'));
if (!entries.length) return '<p>-</p>';
return `<div class="exp-object">${entries.map(([key, val]) => {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
if (val === null || val === undefined) return '';
if (typeof val === 'string') {
return `<div class="exp-obj-field"><span class="exp-obj-key">${escapeHtml(label)}:</span> <span class="exp-obj-val">${escapeHtml(val)}</span></div>`;
} else if (Array.isArray(val)) {
return `<div class="exp-obj-field"><span class="exp-obj-key">${escapeHtml(label)}:</span>${renderExpArray(val)}</div>`;
} else if (typeof val === 'object') {
return `<div class="exp-obj-nested"><span class="exp-obj-key">${escapeHtml(label)}</span>${renderExpObject(val)}</div>`;
}
return `<div class="exp-obj-field"><span class="exp-obj-key">${escapeHtml(label)}:</span> <span class="exp-obj-val">${escapeHtml(String(val))}</span></div>`;
}).join('')}</div>`;
}

View File

@@ -986,25 +986,25 @@ function renderSessionContextContent(context, explorations, conflictResolution)
window._currentContextJson = contextJson;
// Use existing renderContextContent for detailed rendering
sections.push(\`
sections.push(`
<div class="session-context-section">
\${renderContextContent(context)}
${renderContextContent(context)}
</div>
\`);
`);
}
// If we have any sections, wrap them
if (sections.length > 0) {
return \`<div class="context-tab-content session-context-combined">\${sections.join('')}</div>\`;
return `<div class="context-tab-content session-context-combined">${sections.join('')}</div>`;
}
return \`
return `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json, exploration files, or conflict resolution data found for this session.</div>
</div>
\`;
`;
}
// ==========================================
@@ -1019,79 +1019,79 @@ function renderConflictResolutionContext(conflictResolution) {
let sections = [];
// Header
sections.push(\`
sections.push(`
<div class="conflict-resolution-header">
<h4>⚖️ Conflict Resolution Decisions</h4>
<div class="conflict-meta">
<span class="meta-item">Session: <strong>\${escapeHtml(conflictResolution.session_id || 'N/A')}</strong></span>
\${conflictResolution.resolved_at ? \`<span class="meta-item">Resolved: <strong>\${formatDate(conflictResolution.resolved_at)}</strong></span>\` : ''}
<span class="meta-item">Session: <strong>${escapeHtml(conflictResolution.session_id || 'N/A')}</strong></span>
${conflictResolution.resolved_at ? `<span class="meta-item">Resolved: <strong>${formatDate(conflictResolution.resolved_at)}</strong></span>` : ''}
</div>
</div>
\`);
`);
// User decisions
if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) {
const decisions = Object.entries(conflictResolution.user_decisions);
sections.push(\`
sections.push(`
<div class="conflict-decisions-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">🎯 User Decisions (\${decisions.length})</span>
<span class="section-label">🎯 User Decisions (${decisions.length})</span>
</div>
<div class="collapsible-content collapsed">
<div class="decisions-list">
\${decisions.map(([key, decision]) => \`
${decisions.map(([key, decision]) => `
<div class="decision-item">
<div class="decision-header">
<span class="decision-key">\${escapeHtml(key.replace(/_/g, ' '))}</span>
<span class="decision-choice">\${escapeHtml(decision.choice || 'N/A')}</span>
<span class="decision-key">${escapeHtml(key.replace(/_/g, ' '))}</span>
<span class="decision-choice">${escapeHtml(decision.choice || 'N/A')}</span>
</div>
\${decision.description ? \`<p class="decision-description">\${escapeHtml(decision.description)}</p>\` : ''}
\${decision.implications && decision.implications.length > 0 ? \`
${decision.description ? `<p class="decision-description">${escapeHtml(decision.description)}</p>` : ''}
${decision.implications && decision.implications.length > 0 ? `
<div class="decision-implications">
<span class="implications-label">Implications:</span>
<ul class="implications-list">
\${decision.implications.map(impl => \`<li>\${escapeHtml(impl)}</li>\`).join('')}
${decision.implications.map(impl => `<li>${escapeHtml(impl)}</li>`).join('')}
</ul>
</div>
\` : ''}
` : ''}
</div>
\`).join('')}
`).join('')}
</div>
</div>
</div>
\`);
`);
}
// Resolved conflicts
if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) {
sections.push(\`
sections.push(`
<div class="resolved-conflicts-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">✅ Resolved Conflicts (\${conflictResolution.resolved_conflicts.length})</span>
<span class="section-label">✅ Resolved Conflicts (${conflictResolution.resolved_conflicts.length})</span>
</div>
<div class="collapsible-content collapsed">
<div class="conflicts-list">
\${conflictResolution.resolved_conflicts.map(conflict => \`
${conflictResolution.resolved_conflicts.map(conflict => `
<div class="resolved-conflict-item">
<div class="conflict-row">
<span class="conflict-id">\${escapeHtml(conflict.id || 'N/A')}</span>
<span class="conflict-category-badge">\${escapeHtml(conflict.category || 'General')}</span>
<span class="conflict-id">${escapeHtml(conflict.id || 'N/A')}</span>
<span class="conflict-category-badge">${escapeHtml(conflict.category || 'General')}</span>
</div>
<div class="conflict-brief">\${escapeHtml(conflict.brief || '')}</div>
<div class="conflict-brief">${escapeHtml(conflict.brief || '')}</div>
<div class="conflict-strategy">
<span class="strategy-label">Strategy:</span>
<span class="strategy-value">\${escapeHtml(conflict.strategy || 'N/A')}</span>
<span class="strategy-value">${escapeHtml(conflict.strategy || 'N/A')}</span>
</div>
</div>
\`).join('')}
`).join('')}
</div>
</div>
</div>
\`);
`);
}
return \`<div class="conflict-resolution-context">\${sections.join('')}</div>\`;
return `<div class="conflict-resolution-context">${sections.join('')}</div>`;
}

View File

@@ -281,14 +281,9 @@ function renderExplorationContext(explorations) {
function renderExplorationAngle(angle, data) {
let content = [];
// Project structure (architecture)
// Project structure - handle string or object
if (data.project_structure) {
content.push(`
<div class="exp-field">
<label>Project Structure</label>
<p>${escapeHtml(data.project_structure)}</p>
</div>
`);
content.push(renderExpField('Project Structure', data.project_structure));
}
// Relevant files
@@ -310,69 +305,30 @@ function renderExplorationAngle(angle, data) {
`);
}
// Patterns
// Patterns - handle string or object
if (data.patterns) {
content.push(`
<div class="exp-field">
<label>Patterns</label>
<p class="patterns-text">${escapeHtml(data.patterns)}</p>
</div>
`);
content.push(renderExpField('Patterns', data.patterns));
}
// Dependencies
// Dependencies - handle string or object
if (data.dependencies) {
content.push(`
<div class="exp-field">
<label>Dependencies</label>
<p>${escapeHtml(data.dependencies)}</p>
</div>
`);
content.push(renderExpField('Dependencies', data.dependencies));
}
// Integration points
// Integration points - handle string or object
if (data.integration_points) {
content.push(`
<div class="exp-field">
<label>Integration Points</label>
<p>${escapeHtml(data.integration_points)}</p>
</div>
`);
content.push(renderExpField('Integration Points', data.integration_points));
}
// Constraints
// Constraints - handle string or object
if (data.constraints) {
content.push(`
<div class="exp-field">
<label>Constraints</label>
<p>${escapeHtml(data.constraints)}</p>
</div>
`);
content.push(renderExpField('Constraints', data.constraints));
}
// Clarification needs
if (data.clarification_needs && data.clarification_needs.length) {
content.push(`
<div class="exp-field">
<label>Clarification Needs</label>
<div class="clarification-list">
${data.clarification_needs.map(c => `
<div class="clarification-item">
<div class="clarification-question">${escapeHtml(c.question)}</div>
${c.options && c.options.length ? `
<div class="clarification-options">
${c.options.map((opt, i) => `
<span class="option-badge ${i === c.recommended ? 'recommended' : ''}">${escapeHtml(opt)}</span>
`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
// Clarification needs - handle array or object
if (data.clarification_needs) {
content.push(renderExpField('Clarification Needs', data.clarification_needs));
}
const result = content.join('') || '<p>No data available</p>';
return result;
return content.join('') || '<p>No data available</p>';
}

View File

@@ -3704,7 +3704,109 @@ ol.step-commands code {
white-space: pre-wrap;
}
/* Relevant Files Grid */
/* Exploration Object Rendering */
.exp-object {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--bg-secondary, #f9fafb);
border-radius: 8px;
border: 1px solid var(--border-color, #e5e7eb);
}
.exp-obj-field {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: flex-start;
font-size: 13px;
line-height: 1.5;
}
.exp-obj-key {
font-weight: 600;
color: var(--text-secondary, #6b7280);
min-width: 120px;
}
.exp-obj-val {
color: var(--text-primary, #374151);
flex: 1;
}
.exp-obj-nested {
margin-left: 16px;
padding: 8px;
border-left: 2px solid var(--border-color, #e5e7eb);
}
.exp-obj-nested > .exp-obj-key {
display: block;
margin-bottom: 8px;
color: var(--primary, #3b82f6);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.exp-list {
margin: 4px 0 0 0;
padding-left: 20px;
list-style: disc;
}
.exp-list li {
font-size: 13px;
color: var(--text-primary, #374151);
line-height: 1.6;
margin-bottom: 4px;
}
.exp-array-objects {
display: flex;
flex-direction: column;
gap: 10px;
}
.exp-object-item {
padding: 10px;
background: var(--bg-primary, #fff);
border-radius: 6px;
border: 1px solid var(--border-color, #e5e7eb);
}
.clarification-impact {
font-size: 12px;
color: var(--text-muted, #9ca3af);
margin-top: 6px;
}
.priority-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
}
.priority-badge.priority-high {
background: var(--bg-danger, #fee2e2);
color: var(--text-danger, #dc2626);
}
.priority-badge.priority-medium {
background: var(--bg-warning, #fef3c7);
color: var(--text-warning, #d97706);
}
.priority-badge.priority-low {
background: var(--bg-success, #d1fae5);
color: var(--text-success, #059669);
}
.relevant-files-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));

File diff suppressed because it is too large Load Diff