mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
Remove backup HTML template for workflow dashboard
This commit is contained in:
493
ccw/src/templates/dashboard-js/components/flowchart.js
Normal file
493
ccw/src/templates/dashboard-js/components/flowchart.js
Normal file
@@ -0,0 +1,493 @@
|
||||
// ==========================================
|
||||
// FLOWCHART RENDERING (D3.js)
|
||||
// ==========================================
|
||||
|
||||
function renderFlowchartForTask(sessionId, task) {
|
||||
// Will render on section expand
|
||||
}
|
||||
|
||||
function renderFlowchart(containerId, steps) {
|
||||
if (!steps || steps.length === 0) return;
|
||||
if (typeof d3 === 'undefined') {
|
||||
document.getElementById(containerId).innerHTML = '<div class="flowchart-fallback">D3.js not loaded</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
const width = container.clientWidth || 500;
|
||||
const nodeHeight = 50;
|
||||
const nodeWidth = Math.min(width - 40, 300);
|
||||
const padding = 15;
|
||||
const height = steps.length * (nodeHeight + padding) + padding * 2;
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
const svg = d3.select('#' + containerId)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('class', 'flowchart-svg');
|
||||
|
||||
// Arrow marker
|
||||
svg.append('defs').append('marker')
|
||||
.attr('id', 'arrow-' + containerId)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', 'hsl(var(--border))');
|
||||
|
||||
// Draw arrows
|
||||
for (let i = 0; i < steps.length - 1; i++) {
|
||||
const y1 = padding + i * (nodeHeight + padding) + nodeHeight;
|
||||
const y2 = padding + (i + 1) * (nodeHeight + padding);
|
||||
|
||||
svg.append('line')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', y1)
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', y2)
|
||||
.attr('stroke', 'hsl(var(--border))')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrow-' + containerId + ')');
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
const nodes = svg.selectAll('.node')
|
||||
.data(steps)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'flowchart-node')
|
||||
.attr('transform', (d, i) => `translate(${(width - nodeWidth) / 2}, ${padding + i * (nodeHeight + padding)})`);
|
||||
|
||||
// Node rectangles
|
||||
nodes.append('rect')
|
||||
.attr('width', nodeWidth)
|
||||
.attr('height', nodeHeight)
|
||||
.attr('rx', 6)
|
||||
.attr('fill', (d, i) => i === 0 ? 'hsl(var(--primary))' : 'hsl(var(--card))')
|
||||
.attr('stroke', 'hsl(var(--border))')
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
// Step number circle
|
||||
nodes.append('circle')
|
||||
.attr('cx', 20)
|
||||
.attr('cy', nodeHeight / 2)
|
||||
.attr('r', 12)
|
||||
.attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'hsl(var(--muted))');
|
||||
|
||||
nodes.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', nodeHeight / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('font-size', '11px')
|
||||
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--muted-foreground))')
|
||||
.text((d, i) => i + 1);
|
||||
|
||||
// Node text (step name)
|
||||
nodes.append('text')
|
||||
.attr('x', 45)
|
||||
.attr('y', nodeHeight / 2)
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--foreground))')
|
||||
.attr('font-size', '12px')
|
||||
.text(d => {
|
||||
const text = d.step || d.action || 'Step';
|
||||
return text.length > 35 ? text.substring(0, 32) + '...' : text;
|
||||
});
|
||||
}
|
||||
|
||||
function renderFullFlowchart(flowControl) {
|
||||
if (!flowControl) return;
|
||||
|
||||
const container = document.getElementById('flowchartContainer');
|
||||
if (!container) return;
|
||||
|
||||
const preAnalysis = Array.isArray(flowControl.pre_analysis) ? flowControl.pre_analysis : [];
|
||||
const implSteps = Array.isArray(flowControl.implementation_approach) ? flowControl.implementation_approach : [];
|
||||
|
||||
if (preAnalysis.length === 0 && implSteps.length === 0) {
|
||||
container.innerHTML = '<div class="empty-section">No flowchart data available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const width = container.clientWidth || 500;
|
||||
const nodeHeight = 90;
|
||||
const nodeWidth = Math.min(width - 40, 420);
|
||||
const nodeGap = 45;
|
||||
const sectionGap = 30;
|
||||
|
||||
// Calculate total nodes and height
|
||||
const totalPreNodes = preAnalysis.length;
|
||||
const totalImplNodes = implSteps.length;
|
||||
const hasBothSections = totalPreNodes > 0 && totalImplNodes > 0;
|
||||
const height = (totalPreNodes + totalImplNodes) * (nodeHeight + nodeGap) +
|
||||
(hasBothSections ? sectionGap + 60 : 0) + 60;
|
||||
|
||||
// Clear existing
|
||||
d3.select('#flowchartContainer').selectAll('*').remove();
|
||||
|
||||
const svg = d3.select('#flowchartContainer')
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', height)
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
// Add arrow markers
|
||||
const defs = svg.append('defs');
|
||||
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead-pre')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', '#f59e0b');
|
||||
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead-impl')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', 'hsl(var(--primary))');
|
||||
|
||||
let currentY = 20;
|
||||
|
||||
// Render Pre-Analysis section
|
||||
if (totalPreNodes > 0) {
|
||||
// Section label
|
||||
svg.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', currentY)
|
||||
.attr('fill', '#f59e0b')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '13px')
|
||||
.text('📋 Pre-Analysis Steps');
|
||||
|
||||
currentY += 25;
|
||||
|
||||
preAnalysis.forEach((step, idx) => {
|
||||
const x = (width - nodeWidth) / 2;
|
||||
|
||||
// Connection line to next node
|
||||
if (idx < preAnalysis.length - 1) {
|
||||
svg.append('line')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', currentY + nodeHeight)
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', currentY + nodeHeight + nodeGap - 10)
|
||||
.attr('stroke', '#f59e0b')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead-pre)');
|
||||
}
|
||||
|
||||
// Node group
|
||||
const nodeG = svg.append('g')
|
||||
.attr('class', 'flowchart-node')
|
||||
.attr('transform', `translate(${x}, ${currentY})`);
|
||||
|
||||
// Node rectangle (pre-analysis style - amber/orange)
|
||||
nodeG.append('rect')
|
||||
.attr('width', nodeWidth)
|
||||
.attr('height', nodeHeight)
|
||||
.attr('rx', 10)
|
||||
.attr('fill', 'hsl(var(--card))')
|
||||
.attr('stroke', '#f59e0b')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,3');
|
||||
|
||||
// Step badge
|
||||
nodeG.append('circle')
|
||||
.attr('cx', 25)
|
||||
.attr('cy', 25)
|
||||
.attr('r', 15)
|
||||
.attr('fill', '#f59e0b');
|
||||
|
||||
nodeG.append('text')
|
||||
.attr('x', 25)
|
||||
.attr('y', 30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', 'white')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '11px')
|
||||
.text('P' + (idx + 1));
|
||||
|
||||
// Step name
|
||||
const stepName = step.step || step.action || 'Pre-step ' + (idx + 1);
|
||||
nodeG.append('text')
|
||||
.attr('x', 50)
|
||||
.attr('y', 28)
|
||||
.attr('fill', 'hsl(var(--foreground))')
|
||||
.attr('font-weight', '600')
|
||||
.attr('font-size', '13px')
|
||||
.text(truncateText(stepName, 40));
|
||||
|
||||
// Action description
|
||||
if (step.action && step.action !== stepName) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 52)
|
||||
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||
.attr('font-size', '11px')
|
||||
.text(truncateText(step.action, 50));
|
||||
}
|
||||
|
||||
// Output indicator
|
||||
if (step.output_to) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 75)
|
||||
.attr('fill', '#f59e0b')
|
||||
.attr('font-size', '10px')
|
||||
.text('→ ' + truncateText(step.output_to, 45));
|
||||
}
|
||||
|
||||
currentY += nodeHeight + nodeGap;
|
||||
});
|
||||
}
|
||||
|
||||
// Section divider if both sections exist
|
||||
if (hasBothSections) {
|
||||
currentY += 10;
|
||||
svg.append('line')
|
||||
.attr('x1', 40)
|
||||
.attr('y1', currentY)
|
||||
.attr('x2', width - 40)
|
||||
.attr('y2', currentY)
|
||||
.attr('stroke', 'hsl(var(--border))')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke-dasharray', '4,4');
|
||||
|
||||
// Connecting arrow from pre-analysis to implementation
|
||||
svg.append('line')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', currentY - nodeGap + 5)
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', currentY + sectionGap - 5)
|
||||
.attr('stroke', 'hsl(var(--primary))')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead-impl)');
|
||||
|
||||
currentY += sectionGap;
|
||||
}
|
||||
|
||||
// Render Implementation section
|
||||
if (totalImplNodes > 0) {
|
||||
// Section label
|
||||
svg.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', currentY)
|
||||
.attr('fill', 'hsl(var(--primary))')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '13px')
|
||||
.text('🔧 Implementation Steps');
|
||||
|
||||
currentY += 25;
|
||||
|
||||
implSteps.forEach((step, idx) => {
|
||||
const x = (width - nodeWidth) / 2;
|
||||
|
||||
// Connection line to next node
|
||||
if (idx < implSteps.length - 1) {
|
||||
svg.append('line')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', currentY + nodeHeight)
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', currentY + nodeHeight + nodeGap - 10)
|
||||
.attr('stroke', 'hsl(var(--primary))')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead-impl)');
|
||||
}
|
||||
|
||||
// Node group
|
||||
const nodeG = svg.append('g')
|
||||
.attr('class', 'flowchart-node')
|
||||
.attr('transform', `translate(${x}, ${currentY})`);
|
||||
|
||||
// Node rectangle (implementation style - blue)
|
||||
nodeG.append('rect')
|
||||
.attr('width', nodeWidth)
|
||||
.attr('height', nodeHeight)
|
||||
.attr('rx', 10)
|
||||
.attr('fill', 'hsl(var(--card))')
|
||||
.attr('stroke', 'hsl(var(--primary))')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Step badge
|
||||
nodeG.append('circle')
|
||||
.attr('cx', 25)
|
||||
.attr('cy', 25)
|
||||
.attr('r', 15)
|
||||
.attr('fill', 'hsl(var(--primary))');
|
||||
|
||||
nodeG.append('text')
|
||||
.attr('x', 25)
|
||||
.attr('y', 30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', 'white')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '12px')
|
||||
.text(step.step || idx + 1);
|
||||
|
||||
// Step title
|
||||
nodeG.append('text')
|
||||
.attr('x', 50)
|
||||
.attr('y', 28)
|
||||
.attr('fill', 'hsl(var(--foreground))')
|
||||
.attr('font-weight', '600')
|
||||
.attr('font-size', '13px')
|
||||
.text(truncateText(step.title || 'Step ' + (idx + 1), 40));
|
||||
|
||||
// Description
|
||||
if (step.description) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 52)
|
||||
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||
.attr('font-size', '11px')
|
||||
.text(truncateText(step.description, 50));
|
||||
}
|
||||
|
||||
// Output/depends indicator
|
||||
if (step.depends_on?.length) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 75)
|
||||
.attr('fill', 'var(--warning-color)')
|
||||
.attr('font-size', '10px')
|
||||
.text('← Depends: ' + step.depends_on.join(', '));
|
||||
}
|
||||
|
||||
currentY += nodeHeight + nodeGap;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// D3.js Vertical Flowchart for Implementation Approach (legacy)
|
||||
function renderImplementationFlowchart(steps) {
|
||||
if (!Array.isArray(steps) || steps.length === 0) return;
|
||||
|
||||
const container = document.getElementById('flowchartContainer');
|
||||
if (!container) return;
|
||||
|
||||
const width = container.clientWidth || 500;
|
||||
const nodeHeight = 100;
|
||||
const nodeWidth = Math.min(width - 40, 400);
|
||||
const nodeGap = 50;
|
||||
const height = steps.length * (nodeHeight + nodeGap) + 40;
|
||||
|
||||
// Clear existing
|
||||
d3.select('#flowchartContainer').selectAll('*').remove();
|
||||
|
||||
const svg = d3.select('#flowchartContainer')
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', height)
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
// Add arrow marker
|
||||
svg.append('defs').append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', 'hsl(var(--primary))');
|
||||
|
||||
// Draw nodes and connections
|
||||
steps.forEach((step, idx) => {
|
||||
const y = idx * (nodeHeight + nodeGap) + 20;
|
||||
const x = (width - nodeWidth) / 2;
|
||||
|
||||
// Connection line to next node
|
||||
if (idx < steps.length - 1) {
|
||||
svg.append('line')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', y + nodeHeight)
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', y + nodeHeight + nodeGap - 10)
|
||||
.attr('stroke', 'hsl(var(--primary))')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
}
|
||||
|
||||
// Node group
|
||||
const nodeG = svg.append('g')
|
||||
.attr('class', 'flowchart-node')
|
||||
.attr('transform', `translate(${x}, ${y})`);
|
||||
|
||||
// Node rectangle with gradient
|
||||
nodeG.append('rect')
|
||||
.attr('width', nodeWidth)
|
||||
.attr('height', nodeHeight)
|
||||
.attr('rx', 10)
|
||||
.attr('fill', 'hsl(var(--card))')
|
||||
.attr('stroke', 'hsl(var(--primary))')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
|
||||
|
||||
// Step number badge
|
||||
nodeG.append('circle')
|
||||
.attr('cx', 25)
|
||||
.attr('cy', 25)
|
||||
.attr('r', 15)
|
||||
.attr('fill', 'hsl(var(--primary))');
|
||||
|
||||
nodeG.append('text')
|
||||
.attr('x', 25)
|
||||
.attr('y', 30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', 'white')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '12px')
|
||||
.text(step.step || idx + 1);
|
||||
|
||||
// Step title
|
||||
nodeG.append('text')
|
||||
.attr('x', 50)
|
||||
.attr('y', 30)
|
||||
.attr('fill', 'hsl(var(--foreground))')
|
||||
.attr('font-weight', '600')
|
||||
.attr('font-size', '14px')
|
||||
.text(truncateText(step.title || 'Step ' + (idx + 1), 35));
|
||||
|
||||
// Step description (if available)
|
||||
if (step.description) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 55)
|
||||
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||
.attr('font-size', '12px')
|
||||
.text(truncateText(step.description, 45));
|
||||
}
|
||||
|
||||
// Output indicator
|
||||
if (step.output) {
|
||||
nodeG.append('text')
|
||||
.attr('x', 15)
|
||||
.attr('y', 80)
|
||||
.attr('fill', 'var(--success-color)')
|
||||
.attr('font-size', '11px')
|
||||
.text('→ ' + truncateText(step.output, 40));
|
||||
}
|
||||
});
|
||||
}
|
||||
260
ccw/src/templates/dashboard-js/components/modals.js
Normal file
260
ccw/src/templates/dashboard-js/components/modals.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// ==========================================
|
||||
// MODAL DIALOGS
|
||||
// ==========================================
|
||||
|
||||
// SVG Icons
|
||||
const icons = {
|
||||
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
|
||||
check: '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
copy: '<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||||
terminal: '<svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>'
|
||||
};
|
||||
|
||||
function showPathSelectedModal(dirName, dirHandle) {
|
||||
// Try to guess full path based on current project path
|
||||
const currentPath = projectPath || '';
|
||||
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/')) || 'D:/projects';
|
||||
const suggestedPath = basePath + '/' + dirName;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'path-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="path-modal">
|
||||
<div class="path-modal-header">
|
||||
<span class="path-modal-icon">${icons.folder}</span>
|
||||
<h3>Folder Selected</h3>
|
||||
</div>
|
||||
<div class="path-modal-body">
|
||||
<div class="selected-folder">
|
||||
<strong>${dirName}</strong>
|
||||
</div>
|
||||
<p class="path-modal-note">
|
||||
Confirm or edit the full path:
|
||||
</p>
|
||||
<div class="path-input-group" style="margin-top: 12px;">
|
||||
<label>Full path:</label>
|
||||
<input type="text" id="fullPathInput" value="${suggestedPath}" />
|
||||
<button class="path-go-btn" id="pathGoBtn">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-modal-footer">
|
||||
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners (use arrow functions to ensure proper scope)
|
||||
document.getElementById('pathGoBtn').addEventListener('click', () => {
|
||||
console.log('Open button clicked');
|
||||
goToPath();
|
||||
});
|
||||
document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
|
||||
|
||||
// Focus input, select all text, and add enter key listener
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('fullPathInput');
|
||||
input?.focus();
|
||||
input?.select();
|
||||
input?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') goToPath();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function showPathInputModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'path-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="path-modal">
|
||||
<div class="path-modal-header">
|
||||
<span class="path-modal-icon">${icons.folder}</span>
|
||||
<h3>Open Project</h3>
|
||||
</div>
|
||||
<div class="path-modal-body">
|
||||
<div class="path-input-group" style="margin-top: 0;">
|
||||
<label>Project path:</label>
|
||||
<input type="text" id="fullPathInput" placeholder="D:/projects/my-project" />
|
||||
<button class="path-go-btn" id="pathGoBtn">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-modal-footer">
|
||||
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners (use arrow functions to ensure proper scope)
|
||||
document.getElementById('pathGoBtn').addEventListener('click', () => {
|
||||
console.log('Open button clicked');
|
||||
goToPath();
|
||||
});
|
||||
document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
|
||||
|
||||
// Focus input and add enter key listener
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('fullPathInput');
|
||||
input?.focus();
|
||||
input?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') goToPath();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function goToPath() {
|
||||
const input = document.getElementById('fullPathInput');
|
||||
const path = input?.value?.trim();
|
||||
if (path) {
|
||||
closePathModal();
|
||||
selectPath(path);
|
||||
} else {
|
||||
// Show error - input is empty
|
||||
input.style.borderColor = 'var(--danger-color)';
|
||||
input.placeholder = 'Please enter a path';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closePathModal() {
|
||||
const modal = document.querySelector('.path-modal-overlay');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function copyCommand(btn, dirName) {
|
||||
const input = document.getElementById('fullPathInput');
|
||||
const path = input?.value?.trim() || `[full-path-to-${dirName}]`;
|
||||
const command = `ccw view -p "${path}"`;
|
||||
navigator.clipboard.writeText(command).then(() => {
|
||||
btn.innerHTML = icons.check + ' <span>Copied!</span>';
|
||||
setTimeout(() => { btn.innerHTML = icons.copy + ' <span>Copy</span>'; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function showJsonModal(jsonId, taskId) {
|
||||
// Get JSON from memory store instead of DOM
|
||||
const rawTask = taskJsonStore[jsonId];
|
||||
if (!rawTask) return;
|
||||
|
||||
const jsonContent = JSON.stringify(rawTask, null, 2);
|
||||
|
||||
// Create modal
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'json-modal-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="json-modal">
|
||||
<div class="json-modal-header">
|
||||
<div class="json-modal-title">
|
||||
<span class="task-id-badge">${escapeHtml(taskId)}</span>
|
||||
<span>Task JSON</span>
|
||||
</div>
|
||||
<button class="json-modal-close" onclick="closeJsonModal(this)">×</button>
|
||||
</div>
|
||||
<div class="json-modal-body">
|
||||
<pre class="json-modal-content">${escapeHtml(jsonContent)}</pre>
|
||||
</div>
|
||||
<div class="json-modal-footer">
|
||||
<button class="btn-copy-json" onclick="copyJsonToClipboard(this)">Copy JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeJsonModal(overlay.querySelector('.json-modal-close'));
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeJsonModal(overlay.querySelector('.json-modal-close'));
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
function closeJsonModal(btn) {
|
||||
const overlay = btn.closest('.json-modal-overlay');
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => overlay.remove(), 200);
|
||||
}
|
||||
|
||||
function copyJsonToClipboard(btn) {
|
||||
const content = btn.closest('.json-modal').querySelector('.json-modal-content').textContent;
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
const original = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => btn.textContent = original, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function openMarkdownModal(title, content, type = 'markdown') {
|
||||
const modal = document.getElementById('markdownModal');
|
||||
const titleEl = document.getElementById('markdownModalTitle');
|
||||
const rawEl = document.getElementById('markdownRaw');
|
||||
const previewEl = document.getElementById('markdownPreview');
|
||||
|
||||
// Normalize line endings
|
||||
const normalizedContent = normalizeLineEndings(content);
|
||||
|
||||
titleEl.textContent = title;
|
||||
rawEl.textContent = normalizedContent;
|
||||
|
||||
// Render preview based on type
|
||||
if (typeof marked !== 'undefined' && type === 'markdown') {
|
||||
previewEl.innerHTML = marked.parse(normalizedContent);
|
||||
} else if (type === 'json') {
|
||||
// For JSON, try to parse and re-stringify with formatting
|
||||
try {
|
||||
const parsed = typeof normalizedContent === 'string' ? JSON.parse(normalizedContent) : normalizedContent;
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
previewEl.innerHTML = '<pre class="whitespace-pre-wrap language-json">' + escapeHtml(formatted) + '</pre>';
|
||||
} catch (e) {
|
||||
// If not valid JSON, show as-is
|
||||
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
|
||||
}
|
||||
} else {
|
||||
// Fallback: simple text with line breaks
|
||||
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
|
||||
}
|
||||
|
||||
// Show modal and default to preview tab
|
||||
modal.classList.remove('hidden');
|
||||
switchMarkdownTab('preview');
|
||||
}
|
||||
|
||||
function closeMarkdownModal() {
|
||||
const modal = document.getElementById('markdownModal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function switchMarkdownTab(tab) {
|
||||
const rawEl = document.getElementById('markdownRaw');
|
||||
const previewEl = document.getElementById('markdownPreview');
|
||||
const rawTabBtn = document.getElementById('mdTabRaw');
|
||||
const previewTabBtn = document.getElementById('mdTabPreview');
|
||||
|
||||
if (tab === 'raw') {
|
||||
rawEl.classList.remove('hidden');
|
||||
previewEl.classList.add('hidden');
|
||||
rawTabBtn.classList.add('active', 'bg-background', 'text-foreground');
|
||||
rawTabBtn.classList.remove('text-muted-foreground');
|
||||
previewTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
|
||||
previewTabBtn.classList.add('text-muted-foreground');
|
||||
} else {
|
||||
rawEl.classList.add('hidden');
|
||||
previewEl.classList.remove('hidden');
|
||||
previewTabBtn.classList.add('active', 'bg-background', 'text-foreground');
|
||||
previewTabBtn.classList.remove('text-muted-foreground');
|
||||
rawTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
|
||||
rawTabBtn.classList.add('text-muted-foreground');
|
||||
}
|
||||
}
|
||||
210
ccw/src/templates/dashboard-js/components/navigation.js
Normal file
210
ccw/src/templates/dashboard-js/components/navigation.js
Normal file
@@ -0,0 +1,210 @@
|
||||
// Navigation and Routing
|
||||
// Manages navigation events, active state, content title updates, search, and path selector
|
||||
|
||||
// Path Selector
|
||||
function initPathSelector() {
|
||||
const btn = document.getElementById('pathButton');
|
||||
const menu = document.getElementById('pathMenu');
|
||||
const recentContainer = document.getElementById('recentPaths');
|
||||
|
||||
// Render recent paths
|
||||
if (recentPaths && recentPaths.length > 0) {
|
||||
recentPaths.forEach(path => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'path-item' + (path === projectPath ? ' active' : '');
|
||||
item.textContent = path;
|
||||
item.dataset.path = path;
|
||||
item.addEventListener('click', () => selectPath(path));
|
||||
recentContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
menu.classList.add('hidden');
|
||||
});
|
||||
|
||||
document.getElementById('browsePath').addEventListener('click', async () => {
|
||||
await browseForFolder();
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function initNavigation() {
|
||||
document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
setActiveNavItem(item);
|
||||
currentFilter = item.dataset.filter;
|
||||
currentLiteType = null;
|
||||
currentView = 'sessions';
|
||||
currentSessionDetailKey = null;
|
||||
updateContentTitle();
|
||||
renderSessions();
|
||||
});
|
||||
});
|
||||
|
||||
// Lite Tasks Navigation
|
||||
document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
setActiveNavItem(item);
|
||||
currentLiteType = item.dataset.lite;
|
||||
currentFilter = null;
|
||||
currentView = 'liteTasks';
|
||||
currentSessionDetailKey = null;
|
||||
updateContentTitle();
|
||||
renderLiteTasks();
|
||||
});
|
||||
});
|
||||
|
||||
// Project Overview Navigation
|
||||
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
setActiveNavItem(item);
|
||||
currentView = item.dataset.view;
|
||||
currentFilter = null;
|
||||
currentLiteType = null;
|
||||
currentSessionDetailKey = null;
|
||||
updateContentTitle();
|
||||
renderProjectOverview();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveNavItem(item) {
|
||||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
}
|
||||
|
||||
function updateContentTitle() {
|
||||
const titleEl = document.getElementById('contentTitle');
|
||||
if (currentView === 'project-overview') {
|
||||
titleEl.textContent = 'Project Overview';
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||
} else if (currentView === 'sessionDetail') {
|
||||
titleEl.textContent = 'Session Detail';
|
||||
} else if (currentView === 'liteTaskDetail') {
|
||||
titleEl.textContent = 'Lite Task Detail';
|
||||
} else {
|
||||
const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
|
||||
titleEl.textContent = names[currentFilter] || 'Sessions';
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
function initSearch() {
|
||||
const input = document.getElementById('searchInput');
|
||||
input.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('.session-card').forEach(card => {
|
||||
const text = card.textContent.toLowerCase();
|
||||
card.style.display = text.includes(query) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh Workspace
|
||||
function initRefreshButton() {
|
||||
const btn = document.getElementById('refreshWorkspace');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', refreshWorkspace);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWorkspace() {
|
||||
const btn = document.getElementById('refreshWorkspace');
|
||||
|
||||
// Add spinning animation
|
||||
btn.classList.add('refreshing');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
if (window.SERVER_MODE) {
|
||||
// Reload data from server
|
||||
const data = await loadDashboardData(projectPath);
|
||||
if (data) {
|
||||
// Update stores
|
||||
sessionDataStore = {};
|
||||
liteTaskDataStore = {};
|
||||
|
||||
// Populate stores
|
||||
[...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => {
|
||||
sessionDataStore[s.session_id] = s;
|
||||
});
|
||||
|
||||
[...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => {
|
||||
liteTaskDataStore[s.session_id] = s;
|
||||
});
|
||||
|
||||
// Update global data
|
||||
window.workflowData = data;
|
||||
|
||||
// Update sidebar counts
|
||||
updateSidebarCounts(data);
|
||||
|
||||
// Re-render current view
|
||||
if (currentView === 'sessions') {
|
||||
renderSessions();
|
||||
} else if (currentView === 'liteTasks') {
|
||||
renderLiteTasks();
|
||||
} else if (currentView === 'sessionDetail' && currentSessionDetailKey) {
|
||||
showSessionDetailPage(currentSessionDetailKey);
|
||||
} else if (currentView === 'liteTaskDetail' && currentSessionDetailKey) {
|
||||
showLiteTaskDetailPage(currentSessionDetailKey);
|
||||
} else if (currentView === 'project-overview') {
|
||||
renderProjectOverview();
|
||||
}
|
||||
|
||||
showRefreshToast('Workspace refreshed', 'success');
|
||||
}
|
||||
} else {
|
||||
// Non-server mode: just reload page
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
showRefreshToast('Refresh failed: ' + error.message, 'error');
|
||||
} finally {
|
||||
btn.classList.remove('refreshing');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSidebarCounts(data) {
|
||||
// Update session counts
|
||||
const activeCount = document.querySelector('.nav-item[data-filter="active"] .nav-count');
|
||||
const archivedCount = document.querySelector('.nav-item[data-filter="archived"] .nav-count');
|
||||
const allCount = document.querySelector('.nav-item[data-filter="all"] .nav-count');
|
||||
|
||||
if (activeCount) activeCount.textContent = data.activeSessions?.length || 0;
|
||||
if (archivedCount) archivedCount.textContent = data.archivedSessions?.length || 0;
|
||||
if (allCount) allCount.textContent = (data.activeSessions?.length || 0) + (data.archivedSessions?.length || 0);
|
||||
|
||||
// Update lite task counts
|
||||
const litePlanCount = document.querySelector('.nav-item[data-lite="lite-plan"] .nav-count');
|
||||
const liteFixCount = document.querySelector('.nav-item[data-lite="lite-fix"] .nav-count');
|
||||
|
||||
if (litePlanCount) litePlanCount.textContent = data.liteTasks?.litePlan?.length || 0;
|
||||
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
|
||||
}
|
||||
|
||||
function showRefreshToast(message, type) {
|
||||
// Remove existing toast
|
||||
const existing = document.querySelector('.status-toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `status-toast ${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('fade-out');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 2000);
|
||||
}
|
||||
31
ccw/src/templates/dashboard-js/components/sidebar.js
Normal file
31
ccw/src/templates/dashboard-js/components/sidebar.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// ==========================================
|
||||
// SIDEBAR MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
function initSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
const menuToggle = document.getElementById('menuToggle');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
// Restore collapsed state
|
||||
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
|
||||
});
|
||||
|
||||
// Mobile menu
|
||||
menuToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('open');
|
||||
overlay.classList.toggle('open');
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', () => {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('open');
|
||||
});
|
||||
}
|
||||
963
ccw/src/templates/dashboard-js/components/tabs-context.js
Normal file
963
ccw/src/templates/dashboard-js/components/tabs-context.js
Normal file
@@ -0,0 +1,963 @@
|
||||
// ==========================================
|
||||
// Tab Content Renderers - Context Tab
|
||||
// ==========================================
|
||||
// Functions for rendering Context tab content in the dashboard
|
||||
// Note: getRelevanceColor and getRoleBadgeClass are defined in utils.js
|
||||
|
||||
// ==========================================
|
||||
// Context Tab Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderContextContent(context) {
|
||||
if (!context) {
|
||||
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 found for this session.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const contextJson = JSON.stringify(context, null, 2);
|
||||
// Store in global variable for modal access
|
||||
window._currentContextJson = contextJson;
|
||||
|
||||
// Parse context structure
|
||||
const metadata = context.metadata || {};
|
||||
const projectContext = context.project_context || {};
|
||||
const techStack = projectContext.tech_stack || metadata.tech_stack || {};
|
||||
const codingConventions = projectContext.coding_conventions || {};
|
||||
const architecturePatterns = projectContext.architecture_patterns || [];
|
||||
const assets = context.assets || {};
|
||||
const dependencies = context.dependencies || {};
|
||||
const testContext = context.test_context || {};
|
||||
const conflictDetection = context.conflict_detection || {};
|
||||
|
||||
return `
|
||||
<div class="context-tab-content">
|
||||
<!-- Header Card -->
|
||||
<div class="ctx-header-card">
|
||||
<div class="ctx-header-content">
|
||||
<h3 class="ctx-main-title">📦 Context Package</h3>
|
||||
<button class="btn-view-modal" onclick="openMarkdownModal('context-package.json', window._currentContextJson, 'json')">
|
||||
👁️ View JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Card -->
|
||||
${metadata.task_description || metadata.session_id ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">📋</span>
|
||||
<h4 class="ctx-card-title">Task Metadata</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${metadata.task_description ? `
|
||||
<p class="ctx-description">${escapeHtml(metadata.task_description)}</p>
|
||||
` : ''}
|
||||
<div class="ctx-meta-row">
|
||||
${metadata.session_id ? `
|
||||
<div class="ctx-meta-chip">
|
||||
<span class="ctx-meta-chip-label">SESSION</span>
|
||||
<code class="ctx-meta-chip-value">${escapeHtml(metadata.session_id)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
${metadata.complexity ? `
|
||||
<div class="ctx-meta-chip">
|
||||
<span class="ctx-meta-chip-label">COMPLEXITY</span>
|
||||
<span class="ctx-complexity-badge ctx-complexity-${metadata.complexity}">${escapeHtml(metadata.complexity.toUpperCase())}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${metadata.timestamp ? `
|
||||
<div class="ctx-meta-chip">
|
||||
<span class="ctx-meta-chip-label">CREATED</span>
|
||||
<span class="ctx-meta-chip-value">${formatDate(metadata.timestamp)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${metadata.keywords && metadata.keywords.length > 0 ? `
|
||||
<div class="ctx-keywords-row">
|
||||
${metadata.keywords.map(kw => `<span class="ctx-keyword-tag">${escapeHtml(kw)}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Architecture Patterns Card -->
|
||||
${architecturePatterns.length > 0 ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">🏛️</span>
|
||||
<h4 class="ctx-card-title">Architecture Patterns</h4>
|
||||
<span class="ctx-count-badge">${architecturePatterns.length}</span>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
<div class="ctx-pattern-tags">
|
||||
${architecturePatterns.map(p => `<span class="ctx-pattern-tag">${escapeHtml(p)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Tech Stack Card -->
|
||||
${Object.keys(techStack).length > 0 ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">💻</span>
|
||||
<h4 class="ctx-card-title">Technology Stack</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderTechStackCards(techStack)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Coding Conventions Card -->
|
||||
${Object.keys(codingConventions).length > 0 ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">📝</span>
|
||||
<h4 class="ctx-card-title">Coding Conventions</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderCodingConventionsCards(codingConventions)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Assets Section -->
|
||||
${Object.keys(assets).length > 0 ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">📚</span>
|
||||
<h4 class="ctx-card-title">Assets & Resources</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderAssetsCards(assets)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Dependencies Section -->
|
||||
${(dependencies.internal && dependencies.internal.length > 0) || (dependencies.external && dependencies.external.length > 0) ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">🔗</span>
|
||||
<h4 class="ctx-card-title">Dependencies</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderDependenciesCards(dependencies)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Test Context Section -->
|
||||
${Object.keys(testContext).length > 0 ? `
|
||||
<div class="ctx-card">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">🧪</span>
|
||||
<h4 class="ctx-card-title">Test Context</h4>
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderTestContextCards(testContext)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Conflict Detection Section -->
|
||||
${Object.keys(conflictDetection).length > 0 ? `
|
||||
<div class="ctx-card ctx-card-warning">
|
||||
<div class="ctx-card-header">
|
||||
<span class="ctx-card-icon">⚠️</span>
|
||||
<h4 class="ctx-card-title">Risk Analysis</h4>
|
||||
${conflictDetection.risk_level ? `
|
||||
<span class="ctx-risk-badge ctx-risk-${conflictDetection.risk_level}">${escapeHtml(conflictDetection.risk_level.toUpperCase())}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="ctx-card-body">
|
||||
${renderConflictCards(conflictDetection)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// New card-based renderers
|
||||
function renderTechStackCards(techStack) {
|
||||
const sections = [];
|
||||
|
||||
if (techStack.languages) {
|
||||
const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
|
||||
sections.push(`
|
||||
<div class="ctx-stack-section">
|
||||
<span class="ctx-stack-label">Languages</span>
|
||||
<div class="ctx-stack-tags">
|
||||
${langs.map(l => `<span class="ctx-lang-tag">${escapeHtml(String(l))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.frameworks) {
|
||||
const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
|
||||
sections.push(`
|
||||
<div class="ctx-stack-section">
|
||||
<span class="ctx-stack-label">Frameworks</span>
|
||||
<div class="ctx-stack-tags">
|
||||
${frameworks.map(f => `<span class="ctx-framework-tag">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.frontend_frameworks) {
|
||||
const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
|
||||
sections.push(`
|
||||
<div class="ctx-stack-section">
|
||||
<span class="ctx-stack-label">Frontend</span>
|
||||
<div class="ctx-stack-tags">
|
||||
${ff.map(f => `<span class="ctx-frontend-tag">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.backend_frameworks) {
|
||||
const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
|
||||
sections.push(`
|
||||
<div class="ctx-stack-section">
|
||||
<span class="ctx-stack-label">Backend</span>
|
||||
<div class="ctx-stack-tags">
|
||||
${bf.map(f => `<span class="ctx-backend-tag">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.libraries && typeof techStack.libraries === 'object') {
|
||||
Object.entries(techStack.libraries).forEach(([category, libList]) => {
|
||||
if (Array.isArray(libList) && libList.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-stack-section">
|
||||
<span class="ctx-stack-label">${escapeHtml(category)}</span>
|
||||
<div class="ctx-stack-tags">
|
||||
${libList.map(lib => `<span class="ctx-lib-tag">${escapeHtml(String(lib))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderCodingConventionsCards(conventions) {
|
||||
const sections = [];
|
||||
|
||||
// Helper to format leaf values
|
||||
const formatLeafValue = (val) => {
|
||||
if (val === null || val === undefined) return '-';
|
||||
if (Array.isArray(val)) return val.map(v => escapeHtml(String(v))).join(', ');
|
||||
return escapeHtml(String(val));
|
||||
};
|
||||
|
||||
// Helper to render items (handles nested objects like {backend: ..., frontend: ...})
|
||||
const renderItems = (data) => {
|
||||
return Object.entries(data).map(([key, val]) => {
|
||||
// Check if val is a nested object (like {backend: {...}, frontend: {...}})
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
// Render sub-items for nested structure
|
||||
return Object.entries(val).map(([subKey, subVal]) => `
|
||||
<div class="ctx-conv-item">
|
||||
<span class="ctx-conv-key">${escapeHtml(key)}</span>
|
||||
<span class="ctx-conv-value">${formatLeafValue(subVal)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
return `
|
||||
<div class="ctx-conv-item">
|
||||
<span class="ctx-conv-key">${escapeHtml(key)}</span>
|
||||
<span class="ctx-conv-value">${formatLeafValue(val)}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
// Render all convention sections
|
||||
Object.entries(conventions).forEach(([key, val]) => {
|
||||
if (val && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length > 0) {
|
||||
const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
sections.push(`
|
||||
<div class="ctx-conv-section">
|
||||
<span class="ctx-conv-label">${escapeHtml(label)}</span>
|
||||
<div class="ctx-conv-items">
|
||||
${renderItems(val)}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
return sections.length > 0 ? sections.join('') : '';
|
||||
}
|
||||
|
||||
function renderAssetsCards(assets) {
|
||||
const sections = [];
|
||||
|
||||
// Documentation section - card grid layout
|
||||
if (assets.documentation && assets.documentation.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-assets-category">
|
||||
<div class="ctx-assets-cat-header">
|
||||
<span class="ctx-assets-cat-icon">📄</span>
|
||||
<span class="ctx-assets-cat-title">Documentation</span>
|
||||
<span class="ctx-assets-cat-count">${assets.documentation.length}</span>
|
||||
</div>
|
||||
<div class="ctx-assets-card-grid">
|
||||
${assets.documentation.map(doc => `
|
||||
<div class="ctx-asset-card ctx-asset-doc">
|
||||
<span class="ctx-asset-card-path">${escapeHtml(doc.path)}</span>
|
||||
<span class="ctx-asset-card-badge ctx-relevance-badge">${(doc.relevance_score * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Source Code section - card grid layout
|
||||
if (assets.source_code && assets.source_code.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-assets-category">
|
||||
<div class="ctx-assets-cat-header">
|
||||
<span class="ctx-assets-cat-icon">💻</span>
|
||||
<span class="ctx-assets-cat-title">Source Code</span>
|
||||
<span class="ctx-assets-cat-count">${assets.source_code.length}</span>
|
||||
</div>
|
||||
<div class="ctx-assets-card-grid">
|
||||
${assets.source_code.map(src => `
|
||||
<div class="ctx-asset-card ctx-asset-src">
|
||||
<span class="ctx-asset-card-path">${escapeHtml(src.path)}</span>
|
||||
${src.role ? `<span class="ctx-asset-card-badge ctx-role-badge">${escapeHtml(src.role.replace(/-/g, ' '))}</span>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Tests section - card grid layout
|
||||
if (assets.tests && assets.tests.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-assets-category">
|
||||
<div class="ctx-assets-cat-header">
|
||||
<span class="ctx-assets-cat-icon">🧪</span>
|
||||
<span class="ctx-assets-cat-title">Tests</span>
|
||||
<span class="ctx-assets-cat-count">${assets.tests.length}</span>
|
||||
</div>
|
||||
<div class="ctx-assets-card-grid">
|
||||
${assets.tests.map(test => `
|
||||
<div class="ctx-asset-card ctx-asset-test">
|
||||
<span class="ctx-asset-card-path">${escapeHtml(test.path)}</span>
|
||||
${test.test_count ? `<span class="ctx-asset-card-badge ctx-test-badge">${test.test_count} tests</span>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderDependenciesCards(dependencies) {
|
||||
const sections = [];
|
||||
|
||||
if (dependencies.internal && dependencies.internal.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-deps-section">
|
||||
<div class="ctx-deps-header">
|
||||
<span class="ctx-deps-label">Internal Dependencies</span>
|
||||
<span class="ctx-deps-count">${dependencies.internal.length}</span>
|
||||
</div>
|
||||
<div class="ctx-deps-table">
|
||||
<div class="ctx-deps-table-header">
|
||||
<span class="ctx-deps-col-from">From</span>
|
||||
<span class="ctx-deps-col-type">Type</span>
|
||||
<span class="ctx-deps-col-to">To</span>
|
||||
</div>
|
||||
<div class="ctx-deps-table-body">
|
||||
${dependencies.internal.map(dep => `
|
||||
<div class="ctx-deps-row">
|
||||
<span class="ctx-deps-col-from">${escapeHtml(dep.from)}</span>
|
||||
<span class="ctx-deps-col-type">
|
||||
<span class="ctx-deps-type-badge ctx-deps-type-${dep.type}">${escapeHtml(dep.type)}</span>
|
||||
</span>
|
||||
<span class="ctx-deps-col-to">${escapeHtml(dep.to)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (dependencies.external && dependencies.external.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-deps-section">
|
||||
<div class="ctx-deps-header">
|
||||
<span class="ctx-deps-label">External Packages</span>
|
||||
<span class="ctx-deps-count">${dependencies.external.length}</span>
|
||||
</div>
|
||||
<div class="ctx-deps-packages">
|
||||
${dependencies.external.map(dep => `
|
||||
<span class="ctx-pkg-tag">${escapeHtml(dep.package)}${dep.version ? `@${escapeHtml(dep.version)}` : ''}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderTestContextCards(testContext) {
|
||||
const sections = [];
|
||||
|
||||
// Stats row
|
||||
const tests = testContext.existing_tests || {};
|
||||
let totalTests = 0;
|
||||
if (tests.backend) {
|
||||
if (tests.backend.integration) totalTests += tests.backend.integration.tests || 0;
|
||||
if (tests.backend.api_endpoints) totalTests += tests.backend.api_endpoints.tests || 0;
|
||||
}
|
||||
|
||||
sections.push(`
|
||||
<div class="ctx-test-stats">
|
||||
<div class="ctx-stat-box">
|
||||
<span class="ctx-stat-value">${totalTests}</span>
|
||||
<span class="ctx-stat-label">Total Tests</span>
|
||||
</div>
|
||||
${testContext.coverage_config?.target ? `
|
||||
<div class="ctx-stat-box">
|
||||
<span class="ctx-stat-value">${escapeHtml(testContext.coverage_config.target)}</span>
|
||||
<span class="ctx-stat-label">Coverage Target</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`);
|
||||
|
||||
if (testContext.frameworks) {
|
||||
const fw = testContext.frameworks;
|
||||
sections.push(`
|
||||
<div class="ctx-test-frameworks">
|
||||
${fw.backend ? `
|
||||
<div class="ctx-fw-card ctx-fw-installed">
|
||||
<span class="ctx-fw-type">Backend</span>
|
||||
<span class="ctx-fw-name">${escapeHtml(fw.backend.name || 'N/A')}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${fw.frontend ? `
|
||||
<div class="ctx-fw-card ${fw.frontend.name?.includes('NONE') ? 'ctx-fw-missing' : 'ctx-fw-installed'}">
|
||||
<span class="ctx-fw-type">Frontend</span>
|
||||
<span class="ctx-fw-name">${escapeHtml(fw.frontend.name || 'N/A')}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderConflictCards(conflictDetection) {
|
||||
const sections = [];
|
||||
|
||||
if (conflictDetection.mitigation_strategy) {
|
||||
// Parse numbered items like "(1) ... (2) ..." into list
|
||||
const strategy = conflictDetection.mitigation_strategy;
|
||||
const items = strategy.split(/\(\d+\)/).filter(s => s.trim());
|
||||
|
||||
if (items.length > 1) {
|
||||
sections.push(`
|
||||
<div class="ctx-mitigation">
|
||||
<span class="ctx-mitigation-label">Mitigation Strategy</span>
|
||||
<ol class="ctx-mitigation-list">
|
||||
${items.map(item => `<li>${escapeHtml(item.trim())}</li>`).join('')}
|
||||
</ol>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
sections.push(`
|
||||
<div class="ctx-mitigation">
|
||||
<span class="ctx-mitigation-label">Mitigation Strategy</span>
|
||||
<p class="ctx-mitigation-text">${escapeHtml(strategy)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictDetection.risk_factors) {
|
||||
const factors = conflictDetection.risk_factors;
|
||||
if (factors.test_gaps?.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-risk-section">
|
||||
<span class="ctx-risk-label">Test Gaps</span>
|
||||
<ul class="ctx-risk-list">
|
||||
${factors.test_gaps.map(gap => `<li>${escapeHtml(gap)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictDetection.affected_modules?.length > 0) {
|
||||
sections.push(`
|
||||
<div class="ctx-affected">
|
||||
<span class="ctx-affected-label">Affected Modules</span>
|
||||
<div class="ctx-affected-tags">
|
||||
${conflictDetection.affected_modules.map(mod => `<span class="ctx-affected-tag">${escapeHtml(mod)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderTechStackSection(techStack) {
|
||||
const sections = [];
|
||||
|
||||
if (techStack.languages) {
|
||||
const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Languages:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${langs.map(l => `<span class="badge badge-primary text-xs">${escapeHtml(String(l))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.frameworks) {
|
||||
const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Frameworks:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${frameworks.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.frontend_frameworks) {
|
||||
const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Frontend:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${ff.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.backend_frameworks) {
|
||||
const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Backend:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${bf.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (techStack.libraries) {
|
||||
const libs = techStack.libraries;
|
||||
if (typeof libs === 'object' && !Array.isArray(libs)) {
|
||||
Object.entries(libs).forEach(([category, libList]) => {
|
||||
if (Array.isArray(libList) && libList.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">${escapeHtml(category)}:</span>
|
||||
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
|
||||
${libList.map(lib => `<li>${escapeHtml(String(lib))}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderCodingConventions(conventions) {
|
||||
const sections = [];
|
||||
|
||||
if (conventions.naming) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Naming:</span>
|
||||
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
|
||||
${Object.entries(conventions.naming).map(([key, val]) =>
|
||||
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (conventions.error_handling) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Error Handling:</span>
|
||||
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
|
||||
${Object.entries(conventions.error_handling).map(([key, val]) =>
|
||||
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (conventions.testing) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<span class="context-label">Testing:</span>
|
||||
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
|
||||
${Object.entries(conventions.testing).map(([key, val]) => {
|
||||
if (Array.isArray(val)) {
|
||||
return `<li><strong>${escapeHtml(key)}:</strong> ${val.map(v => escapeHtml(String(v))).join(', ')}</li>`;
|
||||
}
|
||||
return `<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderAssetsSection(assets) {
|
||||
const sections = [];
|
||||
|
||||
// Documentation
|
||||
if (assets.documentation && assets.documentation.length > 0) {
|
||||
sections.push(`
|
||||
<div class="asset-category">
|
||||
<h5 class="asset-category-title">📄 Documentation</h5>
|
||||
<div class="asset-grid">
|
||||
${assets.documentation.map(doc => `
|
||||
<div class="asset-card">
|
||||
<div class="asset-card-header">
|
||||
<span class="asset-path">${escapeHtml(doc.path)}</span>
|
||||
<span class="relevance-score" style="background: ${getRelevanceColor(doc.relevance_score)}">${(doc.relevance_score * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div class="asset-card-body">
|
||||
<div class="asset-scope">${escapeHtml(doc.scope || '')}</div>
|
||||
${doc.contains && doc.contains.length > 0 ? `
|
||||
<div class="asset-tags">
|
||||
${doc.contains.map(tag => `<span class="asset-tag">${escapeHtml(tag)}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Source Code
|
||||
if (assets.source_code && assets.source_code.length > 0) {
|
||||
sections.push(`
|
||||
<div class="asset-category">
|
||||
<h5 class="asset-category-title">💻 Source Code</h5>
|
||||
<div class="asset-grid">
|
||||
${assets.source_code.map(src => `
|
||||
<div class="asset-card">
|
||||
<div class="asset-card-header">
|
||||
<span class="asset-path">${escapeHtml(src.path)}</span>
|
||||
<span class="relevance-score" style="background: ${getRelevanceColor(src.relevance_score)}">${(src.relevance_score * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div class="asset-card-body">
|
||||
<div class="asset-role-badge badge-${getRoleBadgeClass(src.role)}">${escapeHtml(src.role || '')}</div>
|
||||
${src.exports && src.exports.length > 0 ? `
|
||||
<div class="asset-meta"><strong>Exports:</strong> ${src.exports.map(e => `<code class="inline-code">${escapeHtml(e)}</code>`).join(', ')}</div>
|
||||
` : ''}
|
||||
${src.features && src.features.length > 0 ? `
|
||||
<div class="asset-features">${src.features.map(f => `<span class="feature-tag">${escapeHtml(f)}</span>`).join('')}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Tests
|
||||
if (assets.tests && assets.tests.length > 0) {
|
||||
sections.push(`
|
||||
<div class="asset-category">
|
||||
<h5 class="asset-category-title">🧪 Tests</h5>
|
||||
<div class="asset-grid">
|
||||
${assets.tests.map(test => `
|
||||
<div class="asset-card">
|
||||
<div class="asset-card-header">
|
||||
<span class="asset-path">${escapeHtml(test.path)}</span>
|
||||
${test.test_count ? `<span class="test-count-badge">${test.test_count} tests</span>` : ''}
|
||||
</div>
|
||||
<div class="asset-card-body">
|
||||
<div class="asset-type">${escapeHtml(test.type || '')}</div>
|
||||
${test.test_classes ? `<div class="asset-meta"><strong>Classes:</strong> ${escapeHtml(test.test_classes.join(', '))}</div>` : ''}
|
||||
${test.coverage ? `<div class="asset-meta"><strong>Coverage:</strong> ${escapeHtml(test.coverage)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderDependenciesSection(dependencies) {
|
||||
const sections = [];
|
||||
|
||||
// Internal Dependencies
|
||||
if (dependencies.internal && dependencies.internal.length > 0) {
|
||||
sections.push(`
|
||||
<div class="dep-category">
|
||||
<h5 class="dep-category-title">🔄 Internal Dependencies</h5>
|
||||
<div class="dep-graph">
|
||||
${dependencies.internal.slice(0, 10).map(dep => `
|
||||
<div class="dep-item">
|
||||
<div class="dep-from">${escapeHtml(dep.from)}</div>
|
||||
<div class="dep-arrow">
|
||||
<span class="dep-type-badge badge-${dep.type}">${escapeHtml(dep.type)}</span>
|
||||
→
|
||||
</div>
|
||||
<div class="dep-to">${escapeHtml(dep.to)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
${dependencies.internal.length > 10 ? `<div class="dep-more">... and ${dependencies.internal.length - 10} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// External Dependencies
|
||||
if (dependencies.external && dependencies.external.length > 0) {
|
||||
sections.push(`
|
||||
<div class="dep-category">
|
||||
<h5 class="dep-category-title">📦 External Dependencies</h5>
|
||||
<div class="dep-grid">
|
||||
${dependencies.external.map(dep => `
|
||||
<div class="dep-external-card">
|
||||
<div class="dep-package-name">${escapeHtml(dep.package)}</div>
|
||||
<div class="dep-version">${escapeHtml(dep.version || '')}</div>
|
||||
<div class="dep-usage">${escapeHtml(dep.usage || '')}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderTestContextSection(testContext) {
|
||||
const sections = [];
|
||||
|
||||
// Test Frameworks
|
||||
if (testContext.frameworks) {
|
||||
const frameworks = testContext.frameworks;
|
||||
sections.push(`
|
||||
<div class="test-category">
|
||||
<h5 class="test-category-title">🛠️ Test Frameworks</h5>
|
||||
<div class="test-frameworks-grid">
|
||||
${frameworks.backend ? `
|
||||
<div class="framework-card framework-installed">
|
||||
<div class="framework-header">
|
||||
<span class="framework-label">Backend</span>
|
||||
<span class="framework-name">${escapeHtml(frameworks.backend.name || 'N/A')}</span>
|
||||
</div>
|
||||
${frameworks.backend.plugins ? `
|
||||
<div class="framework-plugins">${frameworks.backend.plugins.map(p => `<span class="plugin-tag">${escapeHtml(p)}</span>`).join('')}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
${frameworks.frontend ? `
|
||||
<div class="framework-card ${frameworks.frontend.name && frameworks.frontend.name.includes('NONE') ? 'framework-missing' : 'framework-installed'}">
|
||||
<div class="framework-header">
|
||||
<span class="framework-label">Frontend</span>
|
||||
<span class="framework-name">${escapeHtml(frameworks.frontend.name || 'N/A')}</span>
|
||||
</div>
|
||||
${frameworks.frontend.recommended ? `<div class="framework-recommended">Recommended: ${escapeHtml(frameworks.frontend.recommended)}</div>` : ''}
|
||||
${frameworks.frontend.gap ? `<div class="framework-gap">⚠️ ${escapeHtml(frameworks.frontend.gap)}</div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Existing Tests Statistics
|
||||
if (testContext.existing_tests) {
|
||||
const tests = testContext.existing_tests;
|
||||
let totalTests = 0;
|
||||
let totalClasses = 0;
|
||||
|
||||
if (tests.backend) {
|
||||
if (tests.backend.integration) {
|
||||
totalTests += tests.backend.integration.tests || 0;
|
||||
totalClasses += tests.backend.integration.classes || 0;
|
||||
}
|
||||
if (tests.backend.api_endpoints) {
|
||||
totalTests += tests.backend.api_endpoints.tests || 0;
|
||||
totalClasses += tests.backend.api_endpoints.classes || 0;
|
||||
}
|
||||
}
|
||||
|
||||
sections.push(`
|
||||
<div class="test-category">
|
||||
<h5 class="test-category-title">📊 Test Statistics</h5>
|
||||
<div class="test-stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${totalTests}</div>
|
||||
<div class="stat-label">Total Tests</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${totalClasses}</div>
|
||||
<div class="stat-label">Test Classes</div>
|
||||
</div>
|
||||
${testContext.coverage_config && testContext.coverage_config.target ? `
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${escapeHtml(testContext.coverage_config.target)}</div>
|
||||
<div class="stat-label">Coverage Target</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Test Markers
|
||||
if (testContext.test_markers) {
|
||||
sections.push(`
|
||||
<div class="test-category">
|
||||
<h5 class="test-category-title">🏷️ Test Markers</h5>
|
||||
<div class="test-markers-grid">
|
||||
${Object.entries(testContext.test_markers).map(([marker, desc]) => `
|
||||
<div class="marker-card">
|
||||
<span class="marker-name">@${escapeHtml(marker)}</span>
|
||||
<span class="marker-desc">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
function renderConflictDetectionSection(conflictDetection) {
|
||||
const sections = [];
|
||||
|
||||
// Risk Level Indicator
|
||||
if (conflictDetection.risk_level) {
|
||||
const riskLevel = conflictDetection.risk_level;
|
||||
const riskColor = riskLevel === 'high' ? '#ef4444' : riskLevel === 'medium' ? '#f59e0b' : '#10b981';
|
||||
sections.push(`
|
||||
<div class="risk-indicator" style="border-color: ${riskColor}">
|
||||
<div class="risk-level" style="background: ${riskColor}">
|
||||
${escapeHtml(riskLevel.toUpperCase())} RISK
|
||||
</div>
|
||||
${conflictDetection.mitigation_strategy ? `
|
||||
<div class="risk-mitigation">
|
||||
<strong>Mitigation Strategy:</strong> ${escapeHtml(conflictDetection.mitigation_strategy)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Risk Factors
|
||||
if (conflictDetection.risk_factors) {
|
||||
const factors = conflictDetection.risk_factors;
|
||||
sections.push(`
|
||||
<div class="conflict-category">
|
||||
<h5 class="conflict-category-title">⚠️ Risk Factors</h5>
|
||||
<div class="risk-factors-list">
|
||||
${factors.test_gaps && factors.test_gaps.length > 0 ? `
|
||||
<div class="risk-factor">
|
||||
<strong class="risk-factor-title">Test Gaps:</strong>
|
||||
<ul class="risk-factor-items">
|
||||
${factors.test_gaps.map(gap => `<li>${escapeHtml(gap)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${factors.existing_implementations && factors.existing_implementations.length > 0 ? `
|
||||
<div class="risk-factor">
|
||||
<strong class="risk-factor-title">Existing Implementations:</strong>
|
||||
<ul class="risk-factor-items">
|
||||
${factors.existing_implementations.map(impl => `<li>${escapeHtml(impl)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Affected Modules
|
||||
if (conflictDetection.affected_modules && conflictDetection.affected_modules.length > 0) {
|
||||
sections.push(`
|
||||
<div class="conflict-category">
|
||||
<h5 class="conflict-category-title">📦 Affected Modules</h5>
|
||||
<div class="affected-modules-grid">
|
||||
${conflictDetection.affected_modules.map(mod => `
|
||||
<span class="affected-module-tag">${escapeHtml(mod)}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Historical Conflicts
|
||||
if (conflictDetection.historical_conflicts && conflictDetection.historical_conflicts.length > 0) {
|
||||
sections.push(`
|
||||
<div class="conflict-category">
|
||||
<h5 class="conflict-category-title">📜 Historical Lessons</h5>
|
||||
<div class="historical-conflicts-list">
|
||||
${conflictDetection.historical_conflicts.map(conflict => `
|
||||
<div class="historical-conflict-card">
|
||||
<div class="conflict-source">Source: ${escapeHtml(conflict.source || 'Unknown')}</div>
|
||||
${conflict.lesson ? `<div class="conflict-lesson"><strong>Lesson:</strong> ${escapeHtml(conflict.lesson)}</div>` : ''}
|
||||
${conflict.recommendation ? `<div class="conflict-recommendation"><strong>Recommendation:</strong> ${escapeHtml(conflict.recommendation)}</div>` : ''}
|
||||
${conflict.challenge ? `<div class="conflict-challenge"><strong>Challenge:</strong> ${escapeHtml(conflict.challenge)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
353
ccw/src/templates/dashboard-js/components/tabs-other.js
Normal file
353
ccw/src/templates/dashboard-js/components/tabs-other.js
Normal file
@@ -0,0 +1,353 @@
|
||||
// ==========================================
|
||||
// Tab Content Renderers - Other Tabs
|
||||
// ==========================================
|
||||
// Functions for rendering Summary, IMPL Plan, Review, and Lite Context tabs
|
||||
|
||||
// ==========================================
|
||||
// Summary Tab Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderSummaryContent(summaries) {
|
||||
if (!summaries || summaries.length === 0) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon">📝</div>
|
||||
<div class="empty-title">No Summaries</div>
|
||||
<div class="empty-text">No summaries found in .summaries/</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Store summaries in global variable for modal access
|
||||
window._currentSummaries = summaries;
|
||||
|
||||
return `
|
||||
<div class="summary-tab-content space-y-4">
|
||||
${summaries.map((s, idx) => {
|
||||
const normalizedContent = normalizeLineEndings(s.content || '');
|
||||
// Extract first 3 lines for preview
|
||||
const previewLines = normalizedContent.split('\n').slice(0, 3).join('\n');
|
||||
const hasMore = normalizedContent.split('\n').length > 3;
|
||||
return `
|
||||
<div class="summary-item-card">
|
||||
<div class="summary-item-header">
|
||||
<h4 class="summary-item-title">📄 ${escapeHtml(s.name || 'Summary')}</h4>
|
||||
<button class="btn-view-modal" onclick="openMarkdownModal('${escapeHtml(s.name || 'Summary')}', window._currentSummaries[${idx}].content, 'markdown');">
|
||||
👁️ View
|
||||
</button>
|
||||
</div>
|
||||
<div class="summary-item-preview">
|
||||
<pre class="summary-preview-text">${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// IMPL Plan Tab Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderImplPlanContent(implPlan) {
|
||||
if (!implPlan) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon">📐</div>
|
||||
<div class="empty-title">No IMPL Plan</div>
|
||||
<div class="empty-text">No IMPL_PLAN.md found for this session.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Normalize and store in global variable for modal access
|
||||
const normalizedContent = normalizeLineEndings(implPlan);
|
||||
window._currentImplPlan = normalizedContent;
|
||||
|
||||
// Extract first 5 lines for preview
|
||||
const previewLines = normalizedContent.split('\n').slice(0, 5).join('\n');
|
||||
const hasMore = normalizedContent.split('\n').length > 5;
|
||||
|
||||
return `
|
||||
<div class="impl-plan-tab-content">
|
||||
<div class="impl-plan-card">
|
||||
<div class="impl-plan-header">
|
||||
<h3 class="impl-plan-title">📐 Implementation Plan</h3>
|
||||
<button class="btn-view-modal" onclick="openMarkdownModal('IMPL_PLAN.md', window._currentImplPlan, 'markdown')">
|
||||
👁️ View
|
||||
</button>
|
||||
</div>
|
||||
<div class="impl-plan-preview">
|
||||
<pre class="impl-plan-preview-text">${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Review Tab Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderReviewContent(review) {
|
||||
if (!review || !review.dimensions) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<div class="empty-title">No Review Data</div>
|
||||
<div class="empty-text">No review findings in .review/</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const dimensions = Object.entries(review.dimensions);
|
||||
if (dimensions.length === 0) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<div class="empty-title">No Findings</div>
|
||||
<div class="empty-text">No review findings found.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="review-tab-content">
|
||||
${dimensions.map(([dim, rawFindings]) => {
|
||||
// Normalize findings to always be an array
|
||||
let findings = [];
|
||||
if (Array.isArray(rawFindings)) {
|
||||
findings = rawFindings;
|
||||
} else if (rawFindings && typeof rawFindings === 'object') {
|
||||
// If it's an object with a findings array, use that
|
||||
if (Array.isArray(rawFindings.findings)) {
|
||||
findings = rawFindings.findings;
|
||||
} else {
|
||||
// Wrap single object in array or show raw JSON
|
||||
findings = [{ title: dim, description: JSON.stringify(rawFindings, null, 2), severity: 'info' }];
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="review-dimension-section">
|
||||
<div class="dimension-header">
|
||||
<span class="dimension-name">${escapeHtml(dim)}</span>
|
||||
<span class="dimension-count">${findings.length} finding${findings.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="dimension-findings">
|
||||
${findings.map(f => `
|
||||
<div class="finding-item ${f.severity || 'medium'}">
|
||||
<div class="finding-header">
|
||||
<span class="finding-severity ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
|
||||
<span class="finding-title">${escapeHtml(f.title || 'Finding')}</span>
|
||||
</div>
|
||||
<p class="finding-description">${escapeHtml(f.description || '')}</p>
|
||||
${f.file ? `<div class="finding-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Lite Context Tab Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderLiteContextContent(context, session) {
|
||||
const plan = session.plan || {};
|
||||
|
||||
// If we have context from context-package.json
|
||||
if (context) {
|
||||
return `
|
||||
<div class="context-tab-content">
|
||||
<pre class="json-content">${escapeHtml(JSON.stringify(context, null, 2))}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Fallback: show context from plan
|
||||
if (plan.focus_paths?.length || plan.summary) {
|
||||
return `
|
||||
<div class="context-tab-content">
|
||||
${plan.summary ? `
|
||||
<div class="context-section">
|
||||
<h4>Summary</h4>
|
||||
<p>${escapeHtml(plan.summary)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
${plan.focus_paths?.length ? `
|
||||
<div class="context-section">
|
||||
<h4>Focus Paths</h4>
|
||||
<div class="path-tags">
|
||||
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 found for this session.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Exploration Context Rendering
|
||||
// ==========================================
|
||||
|
||||
function renderExplorationContext(explorations) {
|
||||
if (!explorations || !explorations.manifest) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const manifest = explorations.manifest;
|
||||
const data = explorations.data || {};
|
||||
|
||||
let sections = [];
|
||||
|
||||
// Header with manifest info
|
||||
sections.push(`
|
||||
<div class="exploration-header">
|
||||
<h4>${escapeHtml(manifest.task_description || 'Exploration Context')}</h4>
|
||||
<div class="exploration-meta">
|
||||
<span class="meta-item">Complexity: <strong>${escapeHtml(manifest.complexity || 'N/A')}</strong></span>
|
||||
<span class="meta-item">Explorations: <strong>${manifest.exploration_count || 0}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Render each exploration angle as collapsible section
|
||||
const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points'];
|
||||
const explorationTitles = {
|
||||
'architecture': 'Architecture',
|
||||
'dependencies': 'Dependencies',
|
||||
'patterns': 'Patterns',
|
||||
'integration-points': 'Integration Points'
|
||||
};
|
||||
|
||||
for (const angle of explorationOrder) {
|
||||
const expData = data[angle];
|
||||
if (!expData) continue;
|
||||
|
||||
sections.push(`
|
||||
<div class="exploration-section collapsible-section">
|
||||
<div class="collapsible-header" onclick="toggleSection(this)">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="section-label">${explorationTitles[angle] || angle}</span>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
${renderExplorationAngle(angle, expData)}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return `<div class="exploration-context">${sections.join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderExplorationAngle(angle, data) {
|
||||
let content = [];
|
||||
|
||||
// Project structure (architecture)
|
||||
if (data.project_structure) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Project Structure</label>
|
||||
<p>${escapeHtml(data.project_structure)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Relevant files
|
||||
if (data.relevant_files && data.relevant_files.length) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Relevant Files (${data.relevant_files.length})</label>
|
||||
<div class="relevant-files-list">
|
||||
${data.relevant_files.slice(0, 10).map(f => `
|
||||
<div class="file-item-exp">
|
||||
<div class="file-path"><code>${escapeHtml(f.path || '')}</code></div>
|
||||
<div class="file-relevance">Relevance: ${(f.relevance * 100).toFixed(0)}%</div>
|
||||
${f.rationale ? `<div class="file-rationale">${escapeHtml(f.rationale.substring(0, 200))}...</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
${data.relevant_files.length > 10 ? `<div class="more-files">... and ${data.relevant_files.length - 10} more files</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Patterns
|
||||
if (data.patterns) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Patterns</label>
|
||||
<p class="patterns-text">${escapeHtml(data.patterns)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (data.dependencies) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Dependencies</label>
|
||||
<p>${escapeHtml(data.dependencies)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Integration points
|
||||
if (data.integration_points) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Integration Points</label>
|
||||
<p>${escapeHtml(data.integration_points)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Constraints
|
||||
if (data.constraints) {
|
||||
content.push(`
|
||||
<div class="exp-field">
|
||||
<label>Constraints</label>
|
||||
<p>${escapeHtml(data.constraints)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// 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>
|
||||
`);
|
||||
}
|
||||
|
||||
return content.join('') || '<p>No data available</p>';
|
||||
}
|
||||
477
ccw/src/templates/dashboard-js/components/task-drawer-core.js
Normal file
477
ccw/src/templates/dashboard-js/components/task-drawer-core.js
Normal file
@@ -0,0 +1,477 @@
|
||||
// ==========================================
|
||||
// TASK DRAWER CORE
|
||||
// ==========================================
|
||||
// Core drawer functionality and main rendering functions
|
||||
|
||||
let currentDrawerTasks = [];
|
||||
|
||||
function openTaskDrawer(taskId) {
|
||||
const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId);
|
||||
if (!task) {
|
||||
console.error('Task not found:', taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
|
||||
document.getElementById('drawerContent').innerHTML = renderTaskDrawerContent(task);
|
||||
document.getElementById('taskDetailDrawer').classList.add('open');
|
||||
document.getElementById('drawerOverlay').classList.add('active');
|
||||
|
||||
// Initialize flowchart after DOM is updated
|
||||
setTimeout(() => {
|
||||
renderFullFlowchart(task.flow_control);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function openTaskDrawerForLite(sessionId, taskId) {
|
||||
const session = liteTaskDataStore[currentSessionDetailKey];
|
||||
if (!session) return;
|
||||
|
||||
const task = session.tasks?.find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
// Set current drawer tasks and session context
|
||||
currentDrawerTasks = session.tasks || [];
|
||||
window._currentDrawerSession = session;
|
||||
|
||||
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
|
||||
// Use dedicated lite task drawer renderer
|
||||
document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session);
|
||||
document.getElementById('taskDetailDrawer').classList.add('open');
|
||||
document.getElementById('drawerOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function closeTaskDrawer() {
|
||||
document.getElementById('taskDetailDrawer').classList.remove('open');
|
||||
document.getElementById('drawerOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
function switchDrawerTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.drawer-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Update tab panels
|
||||
document.querySelectorAll('.drawer-panel').forEach(panel => {
|
||||
panel.classList.toggle('active', panel.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Render flowchart if switching to flowchart tab
|
||||
if (tabName === 'flowchart') {
|
||||
const taskId = document.getElementById('drawerTaskTitle').textContent;
|
||||
const task = currentDrawerTasks.find(t => t.title === taskId || t.task_id === taskId);
|
||||
if (task?.flow_control) {
|
||||
setTimeout(() => renderFullFlowchart(task.flow_control), 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderTaskDrawerContent(task) {
|
||||
const fc = task.flow_control || {};
|
||||
|
||||
return `
|
||||
<!-- Task Header -->
|
||||
<div class="drawer-task-header">
|
||||
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
|
||||
<span class="task-status-badge ${task.status || 'pending'}">${task.status || 'pending'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="drawer-tabs">
|
||||
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
|
||||
<button class="drawer-tab" data-tab="flowchart" onclick="switchDrawerTab('flowchart')">Flowchart</button>
|
||||
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
|
||||
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="drawer-tab-content">
|
||||
<!-- Overview Tab (default) -->
|
||||
<div class="drawer-panel active" data-tab="overview">
|
||||
${renderPreAnalysisSteps(fc.pre_analysis)}
|
||||
${renderImplementationStepsList(fc.implementation_approach)}
|
||||
</div>
|
||||
|
||||
<!-- Flowchart Tab -->
|
||||
<div class="drawer-panel" data-tab="flowchart">
|
||||
<div id="flowchartContainer" class="flowchart-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Files Tab -->
|
||||
<div class="drawer-panel" data-tab="files">
|
||||
${renderTargetFiles(fc.target_files)}
|
||||
${fc.test_commands ? renderTestCommands(fc.test_commands) : ''}
|
||||
</div>
|
||||
|
||||
<!-- Raw JSON Tab -->
|
||||
<div class="drawer-panel" data-tab="raw">
|
||||
<pre class="json-view">${escapeHtml(JSON.stringify(task, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLiteTaskDrawerContent(task, session) {
|
||||
const rawTask = task._raw || task;
|
||||
|
||||
return `
|
||||
<!-- Task Header -->
|
||||
<div class="drawer-task-header">
|
||||
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
|
||||
${rawTask.action ? `<span class="action-badge">${escapeHtml(rawTask.action)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="drawer-tabs">
|
||||
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
|
||||
<button class="drawer-tab" data-tab="implementation" onclick="switchDrawerTab('implementation')">Implementation</button>
|
||||
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
|
||||
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="drawer-tab-content">
|
||||
<!-- Overview Tab (default) -->
|
||||
<div class="drawer-panel active" data-tab="overview">
|
||||
${renderLiteTaskOverview(rawTask)}
|
||||
</div>
|
||||
|
||||
<!-- Implementation Tab -->
|
||||
<div class="drawer-panel" data-tab="implementation">
|
||||
${renderLiteTaskImplementation(rawTask)}
|
||||
</div>
|
||||
|
||||
<!-- Files Tab -->
|
||||
<div class="drawer-panel" data-tab="files">
|
||||
${renderLiteTaskFiles(rawTask)}
|
||||
</div>
|
||||
|
||||
<!-- Raw JSON Tab -->
|
||||
<div class="drawer-panel" data-tab="raw">
|
||||
<pre class="json-view">${escapeHtml(JSON.stringify(rawTask, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render plan.json task details in drawer (for lite tasks)
|
||||
function renderPlanTaskDetails(task, session) {
|
||||
if (!task) return '';
|
||||
|
||||
// Get corresponding plan task if available
|
||||
const planTask = session?.plan?.tasks?.find(pt => pt.id === task.id);
|
||||
if (!planTask) {
|
||||
// Fallback: task itself might have plan-like structure
|
||||
return renderTaskImplementationDetails(task);
|
||||
}
|
||||
|
||||
return renderTaskImplementationDetails(planTask);
|
||||
}
|
||||
|
||||
function renderTaskImplementationDetails(task) {
|
||||
const sections = [];
|
||||
|
||||
// Description
|
||||
if (task.description) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Description</h4>
|
||||
<p class="task-description">${escapeHtml(task.description)}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Modification Points
|
||||
if (task.modification_points?.length) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Modification Points</h4>
|
||||
<div class="modification-points-list">
|
||||
${task.modification_points.map(mp => `
|
||||
<div class="mod-point-item">
|
||||
<div class="mod-point-file">
|
||||
<span class="file-icon">📄</span>
|
||||
<code>${escapeHtml(mp.file || mp.path || '')}</code>
|
||||
</div>
|
||||
${mp.target ? `<div class="mod-point-target">Target: <code>${escapeHtml(mp.target)}</code></div>` : ''}
|
||||
${mp.change ? `<div class="mod-point-change">${escapeHtml(mp.change)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Implementation Steps
|
||||
if (task.implementation?.length) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Implementation Steps</h4>
|
||||
<ol class="implementation-steps-list">
|
||||
${task.implementation.map(step => `
|
||||
<li class="impl-step-item">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</li>
|
||||
`).join('')}
|
||||
</ol>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Reference
|
||||
if (task.reference) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Reference</h4>
|
||||
${task.reference.pattern ? `<div class="ref-pattern"><strong>Pattern:</strong> ${escapeHtml(task.reference.pattern)}</div>` : ''}
|
||||
${task.reference.files?.length ? `
|
||||
<div class="ref-files">
|
||||
<strong>Files:</strong>
|
||||
<ul>
|
||||
${task.reference.files.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${task.reference.examples ? `<div class="ref-examples"><strong>Examples:</strong> ${escapeHtml(task.reference.examples)}</div>` : ''}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Acceptance Criteria
|
||||
if (task.acceptance?.length) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Acceptance Criteria</h4>
|
||||
<ul class="acceptance-list">
|
||||
${task.acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (task.depends_on?.length) {
|
||||
sections.push(`
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Dependencies</h4>
|
||||
<div class="dependencies-list">
|
||||
${task.depends_on.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
|
||||
// Render lite task overview
|
||||
function renderLiteTaskOverview(task) {
|
||||
let sections = [];
|
||||
|
||||
// Description Card
|
||||
if (task.description) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📝</span>
|
||||
<h4 class="lite-card-title">Description</h4>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<p class="lite-description">${escapeHtml(task.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Scope Card
|
||||
if (task.scope) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📂</span>
|
||||
<h4 class="lite-card-title">Scope</h4>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="lite-scope-box">
|
||||
<code>${escapeHtml(task.scope)}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Acceptance Criteria Card
|
||||
if (task.acceptance && task.acceptance.length > 0) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">✅</span>
|
||||
<h4 class="lite-card-title">Acceptance Criteria</h4>
|
||||
<span class="lite-count-badge">${task.acceptance.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<ul class="lite-checklist">
|
||||
${task.acceptance.map(a => `
|
||||
<li class="lite-check-item">
|
||||
<span class="lite-check-icon">○</span>
|
||||
<span class="lite-check-text">${escapeHtml(a)}</span>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Reference Card
|
||||
if (task.reference) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📚</span>
|
||||
<h4 class="lite-card-title">Reference</h4>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
${task.reference.pattern ? `
|
||||
<div class="lite-ref-section">
|
||||
<span class="lite-ref-label">Pattern:</span>
|
||||
<span class="lite-ref-value">${escapeHtml(task.reference.pattern)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${task.reference.files && task.reference.files.length > 0 ? `
|
||||
<div class="lite-ref-section">
|
||||
<span class="lite-ref-label">Files:</span>
|
||||
<div class="lite-ref-files">
|
||||
${task.reference.files.map(f => `<code class="lite-file-tag">${escapeHtml(f)}</code>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${task.reference.examples ? `
|
||||
<div class="lite-ref-section">
|
||||
<span class="lite-ref-label">Examples:</span>
|
||||
<span class="lite-ref-value">${escapeHtml(task.reference.examples)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Dependencies Card
|
||||
if (task.depends_on && task.depends_on.length > 0) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">🔗</span>
|
||||
<h4 class="lite-card-title">Dependencies</h4>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="lite-deps-tags">
|
||||
${task.depends_on.map(dep => `<span class="lite-dep-tag">${escapeHtml(dep)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No overview data</div>';
|
||||
}
|
||||
|
||||
// Render lite task implementation steps
|
||||
function renderLiteTaskImplementation(task) {
|
||||
let sections = [];
|
||||
|
||||
// Implementation Steps Card
|
||||
if (task.implementation && task.implementation.length > 0) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📋</span>
|
||||
<h4 class="lite-card-title">Implementation Steps</h4>
|
||||
<span class="lite-count-badge">${task.implementation.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="lite-impl-steps">
|
||||
${task.implementation.map((step, idx) => `
|
||||
<div class="lite-impl-step">
|
||||
<div class="lite-step-num">${idx + 1}</div>
|
||||
<div class="lite-step-content">
|
||||
<p class="lite-step-text">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Modification Points Card
|
||||
if (task.modification_points && task.modification_points.length > 0) {
|
||||
sections.push(`
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">🔧</span>
|
||||
<h4 class="lite-card-title">Modification Points</h4>
|
||||
<span class="lite-count-badge">${task.modification_points.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="lite-mod-points">
|
||||
${task.modification_points.map(mp => `
|
||||
<div class="lite-mod-card">
|
||||
<div class="lite-mod-header">
|
||||
<code class="lite-mod-file">${escapeHtml(mp.file || '')}</code>
|
||||
</div>
|
||||
${mp.target ? `
|
||||
<div class="lite-mod-target">
|
||||
<span class="lite-mod-label">Target:</span>
|
||||
<span class="lite-mod-value">${escapeHtml(mp.target)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${mp.change ? `
|
||||
<div class="lite-mod-change">${escapeHtml(mp.change)}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No implementation data</div>';
|
||||
}
|
||||
|
||||
// Render lite task files
|
||||
function renderLiteTaskFiles(task) {
|
||||
const files = [];
|
||||
|
||||
// Collect from modification_points
|
||||
if (task.modification_points) {
|
||||
task.modification_points.forEach(mp => {
|
||||
if (mp.file && !files.includes(mp.file)) files.push(mp.file);
|
||||
});
|
||||
}
|
||||
|
||||
// Collect from scope
|
||||
if (task.scope && !files.includes(task.scope)) {
|
||||
files.push(task.scope);
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return '<div class="empty-section">No files specified</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">Target Files</h4>
|
||||
<ul class="target-files-list">
|
||||
${files.map(f => `
|
||||
<li class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<code>${escapeHtml(f)}</code>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
// ==========================================
|
||||
// TASK DRAWER RENDERERS
|
||||
// ==========================================
|
||||
// Detailed content renderers and helper functions for task drawer
|
||||
|
||||
function renderPreAnalysisSteps(preAnalysis) {
|
||||
if (!Array.isArray(preAnalysis) || preAnalysis.length === 0) {
|
||||
return '<div class="empty-section">No pre-analysis steps</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">🔍</span>
|
||||
<h4 class="lite-card-title">Pre-Analysis Steps</h4>
|
||||
<span class="lite-count-badge">${preAnalysis.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="lite-impl-steps">
|
||||
${preAnalysis.map((item, idx) => `
|
||||
<div class="lite-impl-step">
|
||||
<div class="lite-step-num">${idx + 1}</div>
|
||||
<div class="lite-step-content">
|
||||
<p class="lite-step-text">${escapeHtml(item.step || item.action || 'Step ' + (idx + 1))}</p>
|
||||
${item.action && item.action !== item.step ? `
|
||||
<div class="lite-step-meta">
|
||||
<span class="lite-step-label">Action:</span>
|
||||
<span class="lite-step-value">${escapeHtml(item.action)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${item.commands?.length ? `
|
||||
<div class="lite-step-commands">
|
||||
${item.commands.map(c => `<code class="lite-cmd-tag">${escapeHtml(typeof c === 'string' ? c : JSON.stringify(c))}</code>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
${item.output_to ? `
|
||||
<div class="lite-step-meta">
|
||||
<span class="lite-step-label">Output:</span>
|
||||
<code class="lite-file-tag">${escapeHtml(item.output_to)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderImplementationStepsList(steps) {
|
||||
if (!Array.isArray(steps) || steps.length === 0) {
|
||||
return '<div class="empty-section">No implementation steps</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📋</span>
|
||||
<h4 class="lite-card-title">Implementation Approach</h4>
|
||||
<span class="lite-count-badge">${steps.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="session-impl-steps">
|
||||
${steps.map((step, idx) => {
|
||||
const hasMods = step.modification_points?.length;
|
||||
const hasFlow = step.logic_flow?.length;
|
||||
|
||||
return `
|
||||
<div class="session-impl-step">
|
||||
<div class="session-step-header">
|
||||
<div class="lite-step-num">${step.step || idx + 1}</div>
|
||||
<div class="session-step-title">${escapeHtml(step.title || 'Untitled Step')}</div>
|
||||
</div>
|
||||
${step.description ? `<div class="session-step-desc">${escapeHtml(step.description)}</div>` : ''}
|
||||
${hasMods ? `
|
||||
<div class="session-step-section">
|
||||
<div class="session-section-label">
|
||||
<span class="session-section-icon">🔧</span>
|
||||
<span>Modifications</span>
|
||||
<span class="lite-count-badge">${step.modification_points.length}</span>
|
||||
</div>
|
||||
<div class="session-mods-list">
|
||||
${step.modification_points.map(mp => `
|
||||
<div class="session-mod-item">
|
||||
${typeof mp === 'string' ? `<code class="lite-file-tag">${escapeHtml(mp)}</code>` : `
|
||||
<code class="lite-file-tag">${escapeHtml(mp.file || mp.path || '')}</code>
|
||||
${mp.changes ? `<span class="session-mod-change">${escapeHtml(mp.changes)}</span>` : ''}
|
||||
`}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasFlow ? `
|
||||
<div class="session-step-section">
|
||||
<div class="session-section-label">
|
||||
<span class="session-section-icon">⚡</span>
|
||||
<span>Logic Flow</span>
|
||||
<span class="lite-count-badge">${step.logic_flow.length}</span>
|
||||
</div>
|
||||
<div class="session-flow-list">
|
||||
${step.logic_flow.map((lf, lfIdx) => `
|
||||
<div class="session-flow-item">
|
||||
<span class="session-flow-num">${lfIdx + 1}</span>
|
||||
<span class="session-flow-text">${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${step.depends_on?.length ? `
|
||||
<div class="session-step-deps">
|
||||
<span class="session-deps-label">Dependencies:</span>
|
||||
<div class="lite-deps-tags">
|
||||
${step.depends_on.map(d => `<span class="lite-dep-tag">${escapeHtml(d)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTargetFiles(files) {
|
||||
if (!Array.isArray(files) || files.length === 0) {
|
||||
return '<div class="empty-section">No target files</div>';
|
||||
}
|
||||
|
||||
// Get current project path for building full paths
|
||||
const projectPath = window.currentProjectPath || '';
|
||||
|
||||
return `
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">📁</span>
|
||||
<h4 class="lite-card-title">Target Files</h4>
|
||||
<span class="lite-count-badge">${files.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="session-files-list">
|
||||
${files.map(f => {
|
||||
const filePath = typeof f === 'string' ? f : (f.path || JSON.stringify(f));
|
||||
// Build full path for vscode link
|
||||
const fullPath = filePath.startsWith('/') || filePath.includes(':')
|
||||
? filePath
|
||||
: (projectPath ? `${projectPath}/${filePath}` : filePath);
|
||||
const vscodeUri = `vscode://file/${fullPath.replace(/\\/g, '/')}`;
|
||||
|
||||
return `
|
||||
<a href="${vscodeUri}" class="session-file-item" title="Open in VS Code: ${escapeHtml(fullPath)}">
|
||||
<span class="session-file-icon">📄</span>
|
||||
<code class="session-file-path">${escapeHtml(filePath)}</code>
|
||||
<span class="session-file-action">↗</span>
|
||||
</a>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTestCommands(testCommands) {
|
||||
if (!testCommands || typeof testCommands !== 'object') return '';
|
||||
|
||||
const entries = Object.entries(testCommands);
|
||||
if (entries.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div class="lite-card">
|
||||
<div class="lite-card-header">
|
||||
<span class="lite-card-icon">🧪</span>
|
||||
<h4 class="lite-card-title">Test Commands</h4>
|
||||
<span class="lite-count-badge">${entries.length}</span>
|
||||
</div>
|
||||
<div class="lite-card-body">
|
||||
<div class="session-test-commands">
|
||||
${entries.map(([key, val]) => `
|
||||
<div class="session-test-item">
|
||||
<span class="session-test-label">${escapeHtml(key)}</span>
|
||||
<code class="session-test-cmd">${escapeHtml(typeof val === 'string' ? val : JSON.stringify(val))}</code>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTaskDetail(sessionId, task) {
|
||||
// Get raw task data for JSON view
|
||||
const rawTask = task._raw || task;
|
||||
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
|
||||
// Store JSON in memory instead of inline script tag
|
||||
taskJsonStore[taskJsonId] = rawTask;
|
||||
|
||||
return `
|
||||
<div class="task-detail" id="task-${sessionId}-${task.id}">
|
||||
<div class="task-detail-header">
|
||||
<span class="task-id-badge">${escapeHtml(task.id)}</span>
|
||||
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
|
||||
<span class="task-status-badge ${task.status}">${task.status}</span>
|
||||
<div class="task-header-actions">
|
||||
<button class="btn-view-json" onclick="showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible: Meta -->
|
||||
<div class="collapsible-section">
|
||||
<div class="collapsible-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="section-label">meta</span>
|
||||
<span class="section-preview">${escapeHtml((task.meta?.type || task.meta?.action || '') + (task.meta?.scope ? ' | ' + task.meta.scope : ''))}</span>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
${renderDynamicFields(task.meta || rawTask, ['type', 'action', 'agent', 'scope', 'module', 'execution_group'])}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible: Context -->
|
||||
<div class="collapsible-section">
|
||||
<div class="collapsible-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="section-label">context</span>
|
||||
<span class="section-preview">${escapeHtml(getContextPreview(task.context, rawTask))}</span>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
${renderContextFields(task.context, rawTask)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible: Flow Control (with Flowchart) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="collapsible-header">
|
||||
<span class="collapse-icon">▶</span>
|
||||
<span class="section-label">flow_control</span>
|
||||
<span class="section-preview">${escapeHtml(getFlowControlPreview(task.flow_control, rawTask))}</span>
|
||||
</div>
|
||||
<div class="collapsible-content collapsed">
|
||||
<div class="flowchart-container" id="flowchart-${sessionId}-${task.id}"></div>
|
||||
${renderFlowControlDetails(task.flow_control, rawTask)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getContextPreview(context, rawTask) {
|
||||
const items = [];
|
||||
if (context?.requirements?.length) items.push(`${context.requirements.length} reqs`);
|
||||
if (context?.acceptance?.length) items.push(`${context.acceptance.length} acceptance`);
|
||||
if (context?.focus_paths?.length) items.push(`${context.focus_paths.length} paths`);
|
||||
if (rawTask?.modification_points?.length) items.push(`${rawTask.modification_points.length} mods`);
|
||||
return items.join(' | ') || 'No context';
|
||||
}
|
||||
|
||||
function getFlowControlPreview(flowControl, rawTask) {
|
||||
const steps = flowControl?.implementation_approach?.length || rawTask?.implementation?.length || 0;
|
||||
return steps > 0 ? `${steps} steps` : 'No steps';
|
||||
}
|
||||
|
||||
function renderDynamicFields(obj, priorityKeys = []) {
|
||||
if (!obj || typeof obj !== 'object') return '<div class="field-value json-value-null">null</div>';
|
||||
|
||||
const entries = Object.entries(obj).filter(([k, v]) => v !== null && v !== undefined && k !== '_raw');
|
||||
if (entries.length === 0) return '<div class="field-value json-value-null">Empty</div>';
|
||||
|
||||
// Sort: priority keys first, then alphabetically
|
||||
entries.sort(([a], [b]) => {
|
||||
const aIdx = priorityKeys.indexOf(a);
|
||||
const bIdx = priorityKeys.indexOf(b);
|
||||
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
|
||||
if (aIdx !== -1) return -1;
|
||||
if (bIdx !== -1) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return `<div class="field-group">${entries.map(([key, value]) => renderFieldRow(key, value)).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderFieldRow(key, value) {
|
||||
return `
|
||||
<div class="field-row">
|
||||
<span class="field-label">${escapeHtml(key)}:</span>
|
||||
<div class="field-value">${renderFieldValue(key, value)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFieldValue(key, value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '<span class="json-value-null">null</span>';
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return `<span class="json-value-boolean">${value}</span>`;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return `<span class="json-value-number">${value}</span>`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Check if it's a path
|
||||
if (key.includes('path') || key.includes('file') || value.includes('/') || value.includes('\\')) {
|
||||
return `<span class="array-item path-item">${escapeHtml(value)}</span>`;
|
||||
}
|
||||
return `<span class="json-value-string">${escapeHtml(value)}</span>`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return '<span class="json-value-null">[]</span>';
|
||||
|
||||
// Check if array contains objects or strings
|
||||
if (typeof value[0] === 'object') {
|
||||
return `<div class="nested-array">${value.map((item, i) => `
|
||||
<div class="array-object">
|
||||
<div class="array-object-header">[${i + 1}]</div>
|
||||
${renderDynamicFields(item)}
|
||||
</div>
|
||||
`).join('')}</div>`;
|
||||
}
|
||||
|
||||
// Array of strings/primitives
|
||||
const isPathArray = key.includes('path') || key.includes('file');
|
||||
return `<div class="array-value">${value.map(v =>
|
||||
`<span class="array-item ${isPathArray ? 'path-item' : ''}">${escapeHtml(String(v))}</span>`
|
||||
).join('')}</div>`;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return renderDynamicFields(value);
|
||||
}
|
||||
|
||||
return escapeHtml(String(value));
|
||||
}
|
||||
|
||||
function renderContextFields(context, rawTask) {
|
||||
const sections = [];
|
||||
|
||||
// Requirements / Description
|
||||
const requirements = context?.requirements || [];
|
||||
const description = rawTask?.description;
|
||||
if (requirements.length > 0 || description) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>requirements:</label>
|
||||
${description ? `<p style="margin-bottom: 8px;">${escapeHtml(description)}</p>` : ''}
|
||||
${requirements.length > 0 ? `<ul>${requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>` : ''}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Focus paths / Modification points
|
||||
const focusPaths = context?.focus_paths || [];
|
||||
const modPoints = rawTask?.modification_points || [];
|
||||
if (focusPaths.length > 0 || modPoints.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>${modPoints.length > 0 ? 'modification_points:' : 'focus_paths:'}</label>
|
||||
${modPoints.length > 0 ? `
|
||||
<div class="mod-points">
|
||||
${modPoints.map(m => `
|
||||
<div class="mod-point">
|
||||
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
|
||||
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
|
||||
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="path-tags">${focusPaths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
|
||||
`}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Acceptance criteria
|
||||
const acceptance = context?.acceptance || rawTask?.acceptance || [];
|
||||
if (acceptance.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>acceptance:</label>
|
||||
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
const depends = context?.depends_on || rawTask?.depends_on || [];
|
||||
if (depends.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>depends_on:</label>
|
||||
<div class="path-tags">${depends.map(d => `<span class="array-item depends-badge">${escapeHtml(d)}</span>`).join('')}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Reference
|
||||
const reference = rawTask?.reference;
|
||||
if (reference) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>reference:</label>
|
||||
${renderDynamicFields(reference)}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.length > 0
|
||||
? `<div class="context-fields">${sections.join('')}</div>`
|
||||
: '<div class="field-value json-value-null">No context data</div>';
|
||||
}
|
||||
|
||||
function renderFlowControlDetails(flowControl, rawTask) {
|
||||
const sections = [];
|
||||
|
||||
// Pre-analysis
|
||||
const preAnalysis = flowControl?.pre_analysis || rawTask?.pre_analysis || [];
|
||||
if (preAnalysis.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field" style="margin-top: 16px;">
|
||||
<label>pre_analysis:</label>
|
||||
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Target files
|
||||
const targetFiles = flowControl?.target_files || rawTask?.target_files || [];
|
||||
if (targetFiles.length > 0) {
|
||||
sections.push(`
|
||||
<div class="context-field">
|
||||
<label>target_files:</label>
|
||||
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return sections.join('');
|
||||
}
|
||||
21
ccw/src/templates/dashboard-js/components/theme.js
Normal file
21
ccw/src/templates/dashboard-js/components/theme.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// ==========================================
|
||||
// THEME MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
updateThemeIcon(saved);
|
||||
|
||||
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateThemeIcon(next);
|
||||
});
|
||||
}
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
document.getElementById('themeToggle').textContent = theme === 'light' ? '🌙' : '☀️';
|
||||
}
|
||||
Reference in New Issue
Block a user