From 650d877430a39efac553cc2d8481c895e7bef035 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 7 Dec 2025 20:07:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Dashboard=20=E5=A2=9E=E5=BC=BA=20-=20MC?= =?UTF-8?q?P=E7=AE=A1=E7=90=86=E5=99=A8=E3=80=81Review=20Session=20?= =?UTF-8?q?=E5=92=8C=20UI=20=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 MCP Manager 组件,支持服务器状态管理 - 增强 Review Session 视图,添加 conflict/review tabs - 新增 _conflict_tab.js 和 _review_tab.js 组件 - 改进 carousel、tabs-other 等组件 - 大量 CSS 样式更新和优化 - home.js 添加新功能支持 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ccw/src/core/server.js | 2 + .../dashboard-js/components/_conflict_tab.js | 112 ++ .../dashboard-js/components/_exp_helpers.js | 2 + .../dashboard-js/components/_review_tab.js | 640 ++++++++ .../dashboard-js/components/carousel.js | 51 +- .../dashboard-js/components/mcp-manager.js | 221 +++ .../dashboard-js/components/navigation.js | 2 + .../dashboard-js/components/tabs-context.js | 6 +- .../dashboard-js/components/tabs-other.js | 65 +- ccw/src/templates/dashboard-js/views/home.js | 65 + .../dashboard-js/views/lite-tasks.js | 4 + .../dashboard-js/views/review-session.js | 689 ++++++++- .../dashboard-js/views/session-detail.js | 6 + ccw/src/templates/dashboard.css | 1327 ++++++++++++++--- ccw/src/templates/dashboard.html | 34 +- 15 files changed, 2879 insertions(+), 347 deletions(-) create mode 100644 ccw/src/templates/dashboard-js/components/_conflict_tab.js create mode 100644 ccw/src/templates/dashboard-js/components/_review_tab.js diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 1e29c2b6..1da4c0b6 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -61,6 +61,8 @@ const MODULE_FILES = [ 'components/_exp_helpers.js', 'components/tabs-other.js', 'components/tabs-context.js', + 'components/_conflict_tab.js', + 'components/_review_tab.js', 'components/task-drawer-core.js', 'components/task-drawer-renderers.js', 'components/flowchart.js', diff --git a/ccw/src/templates/dashboard-js/components/_conflict_tab.js b/ccw/src/templates/dashboard-js/components/_conflict_tab.js new file mode 100644 index 00000000..78b66104 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/_conflict_tab.js @@ -0,0 +1,112 @@ +// ========================================== +// Conflict Resolution Tab +// ========================================== + +async function loadAndRenderConflictTab(session, contentArea) { + contentArea.innerHTML = '
Loading conflict resolution...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=conflict`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderConflictCards(data.conflictResolution); + return; + } + } + contentArea.innerHTML = ` +
+
⚖️
+
No Conflict Resolution
+
No conflict-resolution-decisions.json found for this session.
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load conflict resolution: ${err.message}
`; + } +} + +function renderConflictCards(conflictResolution) { + if (!conflictResolution) { + return ` +
+
⚖️
+
No Conflict Resolution
+
No conflict decisions found for this session.
+
+ `; + } + + let cards = []; + + // Header info + cards.push(` +
+

⚖️ Conflict Resolution Decisions

+
+ Session: ${escapeHtml(conflictResolution.session_id || 'N/A')} + ${conflictResolution.resolved_at ? `Resolved: ${formatDate(conflictResolution.resolved_at)}` : ''} +
+
+ `); + + // User Decisions as cards + if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) { + const decisions = Object.entries(conflictResolution.user_decisions); + + cards.push(`
🎯 User Decisions (${decisions.length})
`); + cards.push('
'); + + for (const [key, decision] of decisions) { + const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + cards.push(` +
+
+ ${escapeHtml(label)} +
+
+ Choice: + ${escapeHtml(decision.choice || 'N/A')} +
+ ${decision.description ? ` +
${escapeHtml(decision.description)}
+ ` : ''} + ${decision.implications && decision.implications.length > 0 ? ` +
+ Implications: +
    + ${decision.implications.map(impl => `
  • ${escapeHtml(impl)}
  • `).join('')} +
+
+ ` : ''} +
+ `); + } + cards.push('
'); + } + + // Resolved Conflicts as cards + if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) { + cards.push(`
✅ Resolved Conflicts (${conflictResolution.resolved_conflicts.length})
`); + cards.push('
'); + + for (const conflict of conflictResolution.resolved_conflicts) { + cards.push(` +
+
+ ${escapeHtml(conflict.id || 'N/A')} + ${escapeHtml(conflict.category || 'General')} +
+
${escapeHtml(conflict.brief || '')}
+
+ Strategy: + ${escapeHtml(conflict.strategy || 'N/A')} +
+
+ `); + } + cards.push('
'); + } + + return `
${cards.join('')}
`; +} diff --git a/ccw/src/templates/dashboard-js/components/_exp_helpers.js b/ccw/src/templates/dashboard-js/components/_exp_helpers.js index 551a5374..07c44e7d 100644 --- a/ccw/src/templates/dashboard-js/components/_exp_helpers.js +++ b/ccw/src/templates/dashboard-js/components/_exp_helpers.js @@ -1,3 +1,5 @@ +// Exploration helpers loaded + // Helper: Render exploration field with smart type detection function renderExpField(label, value) { if (value === null || value === undefined) return ''; diff --git a/ccw/src/templates/dashboard-js/components/_review_tab.js b/ccw/src/templates/dashboard-js/components/_review_tab.js new file mode 100644 index 00000000..67b92e1f --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/_review_tab.js @@ -0,0 +1,640 @@ +// ========================================== +// Enhanced Review Tab with Multi-Select & Preview +// ========================================== + +// Review tab state +let reviewTabState = { + allFindings: [], + filteredFindings: [], + selectedFindings: new Set(), + currentFilters: { + dimension: 'all', + severities: new Set(), + search: '' + }, + sortConfig: { + field: 'severity', + order: 'desc' + }, + previewFinding: null, + sessionPath: null, + sessionId: null +}; + +// ========================================== +// Main Review Tab Render +// ========================================== + +function renderReviewContent(review) { + if (!review || !review.dimensions) { + return ` +
+
🔍
+
No Review Data
+
No review findings in .review/
+
+ `; + } + + // Convert dimensions object to flat findings array + const findings = []; + let findingIndex = 0; + + Object.entries(review.dimensions).forEach(([dim, rawFindings]) => { + let dimFindings = []; + if (Array.isArray(rawFindings)) { + dimFindings = rawFindings; + } else if (rawFindings && typeof rawFindings === 'object') { + if (Array.isArray(rawFindings.findings)) { + dimFindings = rawFindings.findings; + } + } + + dimFindings.forEach(f => { + findings.push({ + id: f.id || `finding-${findingIndex++}`, + title: f.title || 'Finding', + description: f.description || '', + severity: (f.severity || 'medium').toLowerCase(), + dimension: dim, + category: f.category || '', + file: f.file || '', + line: f.line || '', + code_context: f.code_context || f.snippet || '', + recommendations: f.recommendations || (f.recommendation ? [f.recommendation] : []), + root_cause: f.root_cause || '', + impact: f.impact || '', + references: f.references || [], + metadata: f.metadata || {} + }); + }); + }); + + if (findings.length === 0) { + return ` +
+
🔍
+
No Findings
+
No review findings found.
+
+ `; + } + + // Store findings in state + reviewTabState.allFindings = findings; + reviewTabState.filteredFindings = [...findings]; + reviewTabState.selectedFindings.clear(); + reviewTabState.previewFinding = null; + + // Get dimensions for tabs + const dimensions = [...new Set(findings.map(f => f.dimension))]; + + // Count by severity + const severityCounts = { + critical: findings.filter(f => f.severity === 'critical').length, + high: findings.filter(f => f.severity === 'high').length, + medium: findings.filter(f => f.severity === 'medium').length, + low: findings.filter(f => f.severity === 'low').length + }; + + return ` +
+ +
+
+ + 🔴 ${severityCounts.critical} + + + 🟠 ${severityCounts.high} + + + 🟡 ${severityCounts.medium} + + + 🟢 ${severityCounts.low} + +
+ + + +
+ 0 selected + + + +
+ + +
+ + +
+
+ Severity: +
+ + + + +
+
+ +
+ Sort: + + +
+ + +
+ + +
+ + ${dimensions.map(dim => ` + + `).join('')} +
+ + +
+ +
+
+ ${findings.length} findings +
+
+ ${renderReviewFindingsList(findings)} +
+
+ + +
+
+
👆
+
Click on a finding to preview details
+
+
+
+
+ `; +} + +// ========================================== +// Findings List Rendering +// ========================================== + +function renderReviewFindingsList(findings) { + if (findings.length === 0) { + return ` +
+ + No findings match your filters +
+ `; + } + + return findings.map(finding => ` +
+ +
+
+ ${finding.severity} + ${escapeHtml(finding.dimension)} +
+
${escapeHtml(finding.title)}
+ ${finding.file ? `
📄 ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
` : ''} +
+
+ `).join(''); +} + +// ========================================== +// Preview Panel Rendering +// ========================================== + +function previewReviewFinding(findingId) { + const finding = reviewTabState.allFindings.find(f => f.id === findingId); + if (!finding) return; + + reviewTabState.previewFinding = finding; + + // Update active state in list + document.querySelectorAll('.review-finding-item').forEach(item => { + item.classList.toggle('previewing', item.dataset.findingId === findingId); + }); + + const previewPanel = document.getElementById('reviewPreviewPanel'); + if (!previewPanel) return; + + previewPanel.innerHTML = ` +
+
+
+ ${finding.severity} + ${escapeHtml(finding.dimension)} + ${finding.category ? `${escapeHtml(finding.category)}` : ''} +
+ +
+ +

${escapeHtml(finding.title)}

+ + ${finding.file ? ` +
+
📄 Location
+
+ ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''} +
+
+ ` : ''} + +
+
📝 Description
+
${escapeHtml(finding.description)}
+
+ + ${finding.code_context ? ` +
+
💻 Code Context
+
${escapeHtml(finding.code_context)}
+
+ ` : ''} + + ${finding.recommendations && finding.recommendations.length > 0 ? ` +
+
✅ Recommendations
+
    + ${finding.recommendations.map(r => `
  • ${escapeHtml(r)}
  • `).join('')} +
+
+ ` : ''} + + ${finding.root_cause ? ` +
+
🔍 Root Cause
+
${escapeHtml(finding.root_cause)}
+
+ ` : ''} + + ${finding.impact ? ` +
+
⚠️ Impact
+
${escapeHtml(finding.impact)}
+
+ ` : ''} + + ${finding.references && finding.references.length > 0 ? ` +
+
🔗 References
+
    + ${finding.references.map(ref => { + const isUrl = ref.startsWith('http'); + return `
  • ${isUrl ? `${ref}` : ref}
  • `; + }).join('')} +
+
+ ` : ''} + + ${finding.metadata && Object.keys(finding.metadata).length > 0 ? ` +
+
ℹ️ Metadata
+ +
+ ` : ''} +
+ `; +} + +// ========================================== +// Selection Management +// ========================================== + +function toggleReviewFindingSelection(findingId, event) { + if (event) { + event.stopPropagation(); + } + + if (reviewTabState.selectedFindings.has(findingId)) { + reviewTabState.selectedFindings.delete(findingId); + } else { + reviewTabState.selectedFindings.add(findingId); + } + + updateReviewSelectionUI(); + + // Update preview panel button if this finding is being previewed + if (reviewTabState.previewFinding && reviewTabState.previewFinding.id === findingId) { + previewReviewFinding(findingId); + } +} + +function selectAllReviewFindings() { + reviewTabState.allFindings.forEach(f => reviewTabState.selectedFindings.add(f.id)); + updateReviewSelectionUI(); +} + +function selectVisibleReviewFindings() { + reviewTabState.filteredFindings.forEach(f => reviewTabState.selectedFindings.add(f.id)); + updateReviewSelectionUI(); +} + +function selectReviewBySeverity(severity) { + reviewTabState.allFindings + .filter(f => f.severity === severity) + .forEach(f => reviewTabState.selectedFindings.add(f.id)); + updateReviewSelectionUI(); +} + +function clearReviewSelection() { + reviewTabState.selectedFindings.clear(); + updateReviewSelectionUI(); +} + +function updateReviewSelectionUI() { + // Update counter + const counter = document.getElementById('reviewSelectionCounter'); + if (counter) { + counter.textContent = `${reviewTabState.selectedFindings.size} selected`; + } + + // Update export button + const exportBtn = document.getElementById('exportFixBtn'); + if (exportBtn) { + exportBtn.disabled = reviewTabState.selectedFindings.size === 0; + } + + // Update checkbox states in list + document.querySelectorAll('.review-finding-item').forEach(item => { + const findingId = item.dataset.findingId; + const isSelected = reviewTabState.selectedFindings.has(findingId); + item.classList.toggle('selected', isSelected); + const checkbox = item.querySelector('.finding-checkbox'); + if (checkbox) { + checkbox.checked = isSelected; + } + }); +} + +// ========================================== +// Filtering & Sorting +// ========================================== + +function filterReviewByDimension(dimension) { + reviewTabState.currentFilters.dimension = dimension; + + // Update tab active state + document.querySelectorAll('.dim-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.dimension === dimension); + }); + + applyReviewFilters(); +} + +function filterReviewBySeverity(severity) { + // Toggle the severity filter + if (reviewTabState.currentFilters.severities.has(severity)) { + reviewTabState.currentFilters.severities.delete(severity); + } else { + reviewTabState.currentFilters.severities.add(severity); + } + + // Update filter chip UI + const filterChip = document.getElementById(`filter-${severity}`); + if (filterChip) { + filterChip.classList.toggle('active', reviewTabState.currentFilters.severities.has(severity)); + const checkbox = filterChip.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = reviewTabState.currentFilters.severities.has(severity); + } + } + + applyReviewFilters(); +} + +function toggleReviewSeverityFilter(severity) { + filterReviewBySeverity(severity); +} + +function onReviewSearch(searchText) { + reviewTabState.currentFilters.search = searchText.toLowerCase(); + applyReviewFilters(); +} + +function applyReviewFilters() { + reviewTabState.filteredFindings = reviewTabState.allFindings.filter(finding => { + // Dimension filter + if (reviewTabState.currentFilters.dimension !== 'all') { + if (finding.dimension !== reviewTabState.currentFilters.dimension) { + return false; + } + } + + // Severity filter (multi-select) + if (reviewTabState.currentFilters.severities.size > 0) { + if (!reviewTabState.currentFilters.severities.has(finding.severity)) { + return false; + } + } + + // Search filter + if (reviewTabState.currentFilters.search) { + const searchText = `${finding.title} ${finding.description} ${finding.file} ${finding.category}`.toLowerCase(); + if (!searchText.includes(reviewTabState.currentFilters.search)) { + return false; + } + } + + return true; + }); + + sortReviewFindings(); +} + +function sortReviewFindings() { + const sortBy = document.getElementById('reviewSortSelect')?.value || 'severity'; + reviewTabState.sortConfig.field = sortBy; + + const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + + reviewTabState.filteredFindings.sort((a, b) => { + let comparison = 0; + + if (sortBy === 'severity') { + comparison = severityOrder[a.severity] - severityOrder[b.severity]; + } else if (sortBy === 'dimension') { + comparison = a.dimension.localeCompare(b.dimension); + } else if (sortBy === 'file') { + comparison = (a.file || '').localeCompare(b.file || ''); + } + + return reviewTabState.sortConfig.order === 'asc' ? comparison : -comparison; + }); + + renderFilteredReviewFindings(); +} + +function toggleReviewSortOrder() { + reviewTabState.sortConfig.order = reviewTabState.sortConfig.order === 'asc' ? 'desc' : 'asc'; + + const icon = document.getElementById('reviewSortOrderIcon'); + if (icon) { + icon.textContent = reviewTabState.sortConfig.order === 'asc' ? '↑' : '↓'; + } + + sortReviewFindings(); +} + +function resetReviewFilters() { + // Reset state + reviewTabState.currentFilters.dimension = 'all'; + reviewTabState.currentFilters.severities.clear(); + reviewTabState.currentFilters.search = ''; + reviewTabState.sortConfig.field = 'severity'; + reviewTabState.sortConfig.order = 'desc'; + + // Reset UI + document.querySelectorAll('.dim-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.dimension === 'all'); + }); + + document.querySelectorAll('.filter-chip').forEach(chip => { + chip.classList.remove('active'); + const checkbox = chip.querySelector('input[type="checkbox"]'); + if (checkbox) checkbox.checked = false; + }); + + const searchInput = document.getElementById('reviewSearchInput'); + if (searchInput) searchInput.value = ''; + + const sortSelect = document.getElementById('reviewSortSelect'); + if (sortSelect) sortSelect.value = 'severity'; + + const sortIcon = document.getElementById('reviewSortOrderIcon'); + if (sortIcon) sortIcon.textContent = '↓'; + + // Re-apply filters + reviewTabState.filteredFindings = [...reviewTabState.allFindings]; + sortReviewFindings(); +} + +function renderFilteredReviewFindings() { + const listContainer = document.getElementById('reviewFindingsList'); + const countEl = document.getElementById('reviewFindingsCount'); + + if (listContainer) { + listContainer.innerHTML = renderReviewFindingsList(reviewTabState.filteredFindings); + } + + if (countEl) { + countEl.textContent = `${reviewTabState.filteredFindings.length} findings`; + } +} + +// ========================================== +// Export Fix JSON +// ========================================== + +function exportReviewFixJson() { + if (reviewTabState.selectedFindings.size === 0) { + showToast('Please select at least one finding to export', 'error'); + return; + } + + const selectedFindingsData = reviewTabState.allFindings.filter(f => + reviewTabState.selectedFindings.has(f.id) + ); + + const session = sessionDataStore[currentSessionDetailKey]; + const sessionId = session?.session_id || 'unknown'; + const exportId = `fix-${Date.now()}`; + + const exportData = { + export_id: exportId, + export_timestamp: new Date().toISOString(), + review_id: `review-${sessionId}`, + session_id: sessionId, + findings_count: selectedFindingsData.length, + findings: selectedFindingsData.map(f => ({ + id: f.id, + title: f.title, + description: f.description, + severity: f.severity, + dimension: f.dimension, + category: f.category || 'uncategorized', + file: f.file, + line: f.line, + code_context: f.code_context || null, + recommendations: f.recommendations || [], + root_cause: f.root_cause || null + })) + }; + + // Convert to JSON and download + const jsonStr = JSON.stringify(exportData, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const filename = `fix-export-${exportId}.json`; + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // Show success notification + const severityCounts = { + critical: selectedFindingsData.filter(f => f.severity === 'critical').length, + high: selectedFindingsData.filter(f => f.severity === 'high').length, + medium: selectedFindingsData.filter(f => f.severity === 'medium').length, + low: selectedFindingsData.filter(f => f.severity === 'low').length + }; + + showToast(`Exported ${selectedFindingsData.length} findings for fixing (Critical: ${severityCounts.critical}, High: ${severityCounts.high}, Medium: ${severityCounts.medium}, Low: ${severityCounts.low})`, 'success'); +} diff --git a/ccw/src/templates/dashboard-js/components/carousel.js b/ccw/src/templates/dashboard-js/components/carousel.js index a8b27d16..93bfd15d 100644 --- a/ccw/src/templates/dashboard-js/components/carousel.js +++ b/ccw/src/templates/dashboard-js/components/carousel.js @@ -118,6 +118,14 @@ function renderCarouselSlide(direction = 'none') { } const session = carouselSessions[carouselIndex]; + const sessionType = session.type || 'workflow'; + + // Use simplified view for review sessions + if (sessionType === 'review') { + renderReviewCarouselSlide(container, session, direction); + return; + } + const tasks = session.tasks || []; const completed = tasks.filter(t => t.status === 'completed').length; const inProgress = tasks.filter(t => t.status === 'in_progress').length; @@ -126,7 +134,6 @@ function renderCarouselSlide(direction = 'none') { const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0; // Get session type badge - const sessionType = session.type || 'workflow'; const typeBadgeClass = getSessionTypeBadgeClass(sessionType); const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-'); @@ -217,6 +224,48 @@ function renderCarouselSlide(direction = 'none') { } } +// Simplified carousel slide for review sessions +function renderReviewCarouselSlide(container, session, direction) { + const typeBadgeClass = getSessionTypeBadgeClass('review'); + const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-'); + const animClass = direction === 'left' ? 'carousel-slide-left' : + direction === 'right' ? 'carousel-slide-right' : 'carousel-fade-in'; + const createdTime = session.created_at ? formatRelativeTime(session.created_at) : ''; + + container.innerHTML = ` + + `; + + // Store session data for navigation + if (!sessionDataStore[sessionKey]) { + sessionDataStore[sessionKey] = session; + } +} + function getSessionTypeBadgeClass(type) { const classes = { 'tdd': 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js index 1d3db02d..4e5ba733 100644 --- a/ccw/src/templates/dashboard-js/components/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js @@ -5,6 +5,7 @@ let mcpConfig = null; let mcpAllProjects = {}; let mcpCurrentProjectServers = {}; +let mcpCreateMode = 'form'; // 'form' or 'json' // ========== Initialization ========== function initMcpManager() { @@ -189,13 +190,21 @@ function openMcpCreateModal() { const modal = document.getElementById('mcpCreateModal'); if (modal) { modal.classList.remove('hidden'); + // Reset to form mode + mcpCreateMode = 'form'; + switchMcpCreateTab('form'); // Clear form document.getElementById('mcpServerName').value = ''; document.getElementById('mcpServerCommand').value = ''; document.getElementById('mcpServerArgs').value = ''; document.getElementById('mcpServerEnv').value = ''; + // Clear JSON input + document.getElementById('mcpServerJson').value = ''; + document.getElementById('mcpJsonPreview').classList.add('hidden'); // Focus on name input document.getElementById('mcpServerName').focus(); + // Setup JSON input listener + setupMcpJsonListener(); } } @@ -206,7 +215,139 @@ function closeMcpCreateModal() { } } +function switchMcpCreateTab(tab) { + mcpCreateMode = tab; + const formMode = document.getElementById('mcpFormMode'); + const jsonMode = document.getElementById('mcpJsonMode'); + const tabForm = document.getElementById('mcpTabForm'); + const tabJson = document.getElementById('mcpTabJson'); + + if (tab === 'form') { + formMode.classList.remove('hidden'); + jsonMode.classList.add('hidden'); + tabForm.classList.add('active'); + tabJson.classList.remove('active'); + } else { + formMode.classList.add('hidden'); + jsonMode.classList.remove('hidden'); + tabForm.classList.remove('active'); + tabJson.classList.add('active'); + } +} + +function setupMcpJsonListener() { + const jsonInput = document.getElementById('mcpServerJson'); + if (jsonInput && !jsonInput.hasAttribute('data-listener-attached')) { + jsonInput.setAttribute('data-listener-attached', 'true'); + jsonInput.addEventListener('input', () => { + updateMcpJsonPreview(); + }); + } +} + +function parseMcpJsonConfig(jsonText) { + if (!jsonText.trim()) { + return { servers: {}, error: null }; + } + + try { + const parsed = JSON.parse(jsonText); + let servers = {}; + + // Support multiple formats: + // 1. {"servers": {...}} format (claude desktop style) + // 2. {"mcpServers": {...}} format (claude.json style) + // 3. {"serverName": {command, args}} format (direct server config) + // 4. {command, args} format (single server without name) + + if (parsed.servers && typeof parsed.servers === 'object') { + servers = parsed.servers; + } else if (parsed.mcpServers && typeof parsed.mcpServers === 'object') { + servers = parsed.mcpServers; + } else if (parsed.command && typeof parsed.command === 'string') { + // Single server without name - will prompt for name + servers = { '__unnamed__': parsed }; + } else { + // Check if all values are server configs (have 'command' property) + const isDirectServerConfig = Object.values(parsed).every( + v => v && typeof v === 'object' && v.command + ); + if (isDirectServerConfig && Object.keys(parsed).length > 0) { + servers = parsed; + } else { + return { servers: {}, error: 'Invalid MCP server JSON format' }; + } + } + + // Validate each server config + for (const [name, config] of Object.entries(servers)) { + if (!config.command || typeof config.command !== 'string') { + return { servers: {}, error: `Server "${name}" missing required "command" field` }; + } + if (config.args && !Array.isArray(config.args)) { + return { servers: {}, error: `Server "${name}" has invalid "args" (must be array)` }; + } + if (config.env && typeof config.env !== 'object') { + return { servers: {}, error: `Server "${name}" has invalid "env" (must be object)` }; + } + } + + return { servers, error: null }; + } catch (e) { + return { servers: {}, error: 'Invalid JSON: ' + e.message }; + } +} + +function updateMcpJsonPreview() { + const jsonInput = document.getElementById('mcpServerJson'); + const previewContainer = document.getElementById('mcpJsonPreview'); + const previewContent = document.getElementById('mcpJsonPreviewContent'); + + const jsonText = jsonInput.value; + const { servers, error } = parseMcpJsonConfig(jsonText); + + if (!jsonText.trim()) { + previewContainer.classList.add('hidden'); + return; + } + + previewContainer.classList.remove('hidden'); + + if (error) { + previewContent.innerHTML = `
${escapeHtml(error)}
`; + return; + } + + const serverCount = Object.keys(servers).length; + if (serverCount === 0) { + previewContent.innerHTML = `
No servers found
`; + return; + } + + const previewHtml = Object.entries(servers).map(([name, config]) => { + const displayName = name === '__unnamed__' ? '(will prompt for name)' : name; + const argsPreview = config.args ? config.args.slice(0, 2).join(' ') + (config.args.length > 2 ? '...' : '') : ''; + return ` +
+ + + ${escapeHtml(displayName)} + ${escapeHtml(config.command)} ${escapeHtml(argsPreview)} +
+ `; + }).join(''); + + previewContent.innerHTML = previewHtml; +} + async function submitMcpCreate() { + if (mcpCreateMode === 'json') { + await submitMcpCreateFromJson(); + } else { + await submitMcpCreateFromForm(); + } +} + +async function submitMcpCreateFromForm() { const name = document.getElementById('mcpServerName').value.trim(); const command = document.getElementById('mcpServerCommand').value.trim(); const argsText = document.getElementById('mcpServerArgs').value.trim(); @@ -255,6 +396,86 @@ async function submitMcpCreate() { serverConfig.env = env; } + await createMcpServerWithConfig(name, serverConfig); +} + +async function submitMcpCreateFromJson() { + const jsonText = document.getElementById('mcpServerJson').value.trim(); + + if (!jsonText) { + showRefreshToast('Please enter JSON configuration', 'error'); + document.getElementById('mcpServerJson').focus(); + return; + } + + const { servers, error } = parseMcpJsonConfig(jsonText); + + if (error) { + showRefreshToast(error, 'error'); + return; + } + + if (Object.keys(servers).length === 0) { + showRefreshToast('No valid servers found in JSON', 'error'); + return; + } + + // Handle unnamed server case + if (servers['__unnamed__']) { + const serverName = prompt('Enter a name for this MCP server:'); + if (!serverName || !serverName.trim()) { + showRefreshToast('Server name is required', 'error'); + return; + } + servers[serverName.trim()] = servers['__unnamed__']; + delete servers['__unnamed__']; + } + + // Add all servers + let successCount = 0; + let failCount = 0; + const serverNames = Object.keys(servers); + + for (const [name, config] of Object.entries(servers)) { + try { + const response = await fetch('/api/mcp-copy-server', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectPath: projectPath, + serverName: name, + serverConfig: config + }) + }); + + if (!response.ok) throw new Error('Failed to create MCP server'); + + const result = await response.json(); + if (result.success) { + successCount++; + } else { + failCount++; + } + } catch (err) { + console.error(`Failed to create MCP server "${name}":`, err); + failCount++; + } + } + + closeMcpCreateModal(); + await loadMcpConfig(); + renderMcpManager(); + + if (failCount === 0) { + showRefreshToast(`${successCount} MCP server${successCount > 1 ? 's' : ''} created successfully`, 'success'); + } else if (successCount > 0) { + showRefreshToast(`${successCount} created, ${failCount} failed`, 'warning'); + } else { + showRefreshToast('Failed to create MCP servers', 'error'); + } +} + +async function createMcpServerWithConfig(name, serverConfig) { // Submit to API try { const response = await fetch('/api/mcp-copy-server', { diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 1f24b284..559605fd 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -62,6 +62,7 @@ function initNavigation() { currentView = 'sessions'; currentSessionDetailKey = null; updateContentTitle(); + showStatsAndSearch(); renderSessions(); }); }); @@ -75,6 +76,7 @@ function initNavigation() { currentView = 'liteTasks'; currentSessionDetailKey = null; updateContentTitle(); + showStatsAndSearch(); renderLiteTasks(); }); }); diff --git a/ccw/src/templates/dashboard-js/components/tabs-context.js b/ccw/src/templates/dashboard-js/components/tabs-context.js index 8550c593..85da4849 100644 --- a/ccw/src/templates/dashboard-js/components/tabs-context.js +++ b/ccw/src/templates/dashboard-js/components/tabs-context.js @@ -970,10 +970,6 @@ function renderConflictDetectionSection(conflictDetection) { function renderSessionContextContent(context, explorations, conflictResolution) { let sections = []; - // Render conflict resolution decisions if available - if (conflictResolution) { - sections.push(renderConflictResolutionContext(conflictResolution)); - } // Render explorations if available (from exploration-*.json files) if (explorations && explorations.manifest) { @@ -1002,7 +998,7 @@ function renderSessionContextContent(context, explorations, conflictResolution)
📦
No Context Data
-
No context-package.json, exploration files, or conflict resolution data found for this session.
+
No context-package.json or exploration files found for this session.
`; } diff --git a/ccw/src/templates/dashboard-js/components/tabs-other.js b/ccw/src/templates/dashboard-js/components/tabs-other.js index fd3952a6..dd1be6f1 100644 --- a/ccw/src/templates/dashboard-js/components/tabs-other.js +++ b/ccw/src/templates/dashboard-js/components/tabs-other.js @@ -89,69 +89,8 @@ function renderImplPlanContent(implPlan) { // ========================================== // Review Tab Rendering // ========================================== - -function renderReviewContent(review) { - if (!review || !review.dimensions) { - return ` -
-
🔍
-
No Review Data
-
No review findings in .review/
-
- `; - } - - const dimensions = Object.entries(review.dimensions); - if (dimensions.length === 0) { - return ` -
-
🔍
-
No Findings
-
No review findings found.
-
- `; - } - - return ` -
- ${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 ` -
-
- ${escapeHtml(dim)} - ${findings.length} finding${findings.length !== 1 ? 's' : ''} -
-
- ${findings.map(f => ` -
-
- ${f.severity || 'medium'} - ${escapeHtml(f.title || 'Finding')} -
-

${escapeHtml(f.description || '')}

- ${f.file ? `
📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''} -
- `).join('')} -
-
- `}).join('')} -
- `; -} +// NOTE: Enhanced review tab with multi-select, filtering, and preview panel +// is now in _review_tab.js - renderReviewContent() function defined there // ========================================== // Lite Context Tab Rendering diff --git a/ccw/src/templates/dashboard-js/views/home.js b/ccw/src/templates/dashboard-js/views/home.js index ac0d9ab7..17cf4604 100644 --- a/ccw/src/templates/dashboard-js/views/home.js +++ b/ccw/src/templates/dashboard-js/views/home.js @@ -20,6 +20,13 @@ function showStatsAndSearch() { if (searchInput) searchInput.parentElement.style.display = ''; } +function hideStatsAndCarousel() { + const statsGrid = document.getElementById('statsGrid'); + const searchInput = document.getElementById('searchInput'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchInput) searchInput.parentElement.style.display = 'none'; +} + function updateStats() { const stats = workflowData.statistics || {}; document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0; @@ -95,6 +102,11 @@ function renderSessionCard(session) { const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-'); sessionDataStore[sessionKey] = session; + // Special rendering for review sessions + if (sessionType === 'review') { + return renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date); + } + return `
@@ -126,3 +138,56 @@ function renderSessionCard(session) {
`; } + +// Special card rendering for review sessions +function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date) { + // Calculate findings stats from reviewDimensions + const dimensions = session.reviewDimensions || []; + let totalFindings = 0; + let criticalCount = 0; + let highCount = 0; + let mediumCount = 0; + let lowCount = 0; + + dimensions.forEach(dim => { + const findings = dim.findings || []; + totalFindings += findings.length; + criticalCount += findings.filter(f => f.severity === 'critical').length; + highCount += findings.filter(f => f.severity === 'high').length; + mediumCount += findings.filter(f => f.severity === 'medium').length; + lowCount += findings.filter(f => f.severity === 'low').length; + }); + + return ` +
+
+
${escapeHtml(session.session_id || 'Unknown')}
+
+ ${typeBadge} + + ${isActive ? 'ACTIVE' : 'ARCHIVED'} + +
+
+
+
+ 📅 ${formatDate(date)} + 🔍 ${totalFindings} findings +
+ ${totalFindings > 0 ? ` +
+
+ ${criticalCount > 0 ? `🔴 ${criticalCount}` : ''} + ${highCount > 0 ? `🟠 ${highCount}` : ''} + ${mediumCount > 0 ? `🟡 ${mediumCount}` : ''} + ${lowCount > 0 ? `🟢 ${lowCount}` : ''} +
+
+ ${dimensions.length} dimensions +
+
+ ` : ''} +
+
+ `; +} diff --git a/ccw/src/templates/dashboard-js/views/lite-tasks.js b/ccw/src/templates/dashboard-js/views/lite-tasks.js index 69979fb6..d07c6842 100644 --- a/ccw/src/templates/dashboard-js/views/lite-tasks.js +++ b/ccw/src/templates/dashboard-js/views/lite-tasks.js @@ -72,6 +72,9 @@ function showLiteTaskDetailPage(sessionKey) { currentView = 'liteTaskDetail'; currentSessionDetailKey = sessionKey; + // Hide stats grid and carousel on detail pages + hideStatsAndCarousel(); + // Also store in sessionDataStore for tab switching compatibility sessionDataStore[sessionKey] = { ...session, @@ -158,6 +161,7 @@ function goBackToLiteTasks() { currentView = 'liteTasks'; currentSessionDetailKey = null; updateContentTitle(); + showStatsAndSearch(); renderLiteTasks(); } diff --git a/ccw/src/templates/dashboard-js/views/review-session.js b/ccw/src/templates/dashboard-js/views/review-session.js index 2219baeb..7eafc282 100644 --- a/ccw/src/templates/dashboard-js/views/review-session.js +++ b/ccw/src/templates/dashboard-js/views/review-session.js @@ -1,18 +1,73 @@ // ========================================== // REVIEW SESSION DETAIL PAGE // ========================================== +// Enhanced with multi-select, filters, split preview panel, and export fix JSON + +// Review session state +let reviewSessionState = { + allFindings: [], + filteredFindings: [], + selectedFindings: new Set(), + currentFilters: { + dimension: 'all', + severities: new Set(), + search: '' + }, + sortConfig: { + field: 'severity', + order: 'desc' + }, + previewFinding: null, + dimensions: [], + session: null +}; function renderReviewSessionDetailPage(session) { const isActive = session._isActive !== false; - const tasks = session.tasks || []; const dimensions = session.reviewDimensions || []; - // Calculate review statistics - const totalFindings = dimensions.reduce((sum, d) => sum + (d.findings?.length || 0), 0); - const criticalCount = dimensions.reduce((sum, d) => - sum + (d.findings?.filter(f => f.severity === 'critical').length || 0), 0); - const highCount = dimensions.reduce((sum, d) => - sum + (d.findings?.filter(f => f.severity === 'high').length || 0), 0); + // Store session and dimensions + reviewSessionState.session = session; + reviewSessionState.dimensions = dimensions; + + // Build flat findings array with dimension info + const allFindings = []; + let findingIndex = 0; + dimensions.forEach(dim => { + (dim.findings || []).forEach(f => { + allFindings.push({ + id: f.id || `finding-${findingIndex++}`, + title: f.title || 'Finding', + description: f.description || '', + severity: (f.severity || 'medium').toLowerCase(), + dimension: dim.name || 'unknown', + category: f.category || '', + file: f.file || '', + line: f.line || '', + code_context: f.code_context || f.snippet || '', + recommendations: f.recommendations || (f.recommendation ? [f.recommendation] : []), + root_cause: f.root_cause || '', + impact: f.impact || '', + references: f.references || [], + metadata: f.metadata || {}, + fix_status: f.fix_status || null + }); + }); + }); + + reviewSessionState.allFindings = allFindings; + reviewSessionState.filteredFindings = [...allFindings]; + reviewSessionState.selectedFindings.clear(); + reviewSessionState.previewFinding = null; + + // Calculate statistics + const totalFindings = allFindings.length; + const severityCounts = { + critical: allFindings.filter(f => f.severity === 'critical').length, + high: allFindings.filter(f => f.severity === 'high').length, + medium: allFindings.filter(f => f.severity === 'medium').length, + low: allFindings.filter(f => f.severity === 'low').length + }; return `
@@ -37,7 +92,7 @@ function renderReviewSessionDetailPage(session) {

📊 Review Progress

- ${session.phase || 'In Progress'} + ${(session.phase || 'In Progress').toUpperCase()}
@@ -49,12 +104,12 @@ function renderReviewSessionDetailPage(session) {
🔴
-
${criticalCount}
+
${severityCounts.critical}
Critical
🟠
-
${highCount}
+
${severityCounts.high}
High
@@ -64,31 +119,117 @@ function renderReviewSessionDetailPage(session) {
- -
- ${dimensions.map((dim, idx) => ` -
-
D${idx + 1}
-
${escapeHtml(dim.name || 'Unknown')}
-
${dim.findings?.length || 0} findings
-
- `).join('')} -
- -
-
-

🔍 Findings by Dimension

-
- - - - + +
+ +
+
+ + 🔴 ${severityCounts.critical} + + + 🟠 ${severityCounts.high} + + + 🟡 ${severityCounts.medium} + + + 🟢 ${severityCounts.low} +
+ + + +
+ 0 selected + + + + +
+ +
-
- ${renderReviewFindingsGrid(dimensions)} + + +
+
+ Severity: +
+ + + + +
+
+ +
+ Sort: + + +
+ + +
+ + +
+ + ${dimensions.map(dim => ` + + `).join('')} +
+ + +
+ +
+
+ ${totalFindings} findings +
+
+ ${renderReviewSessionFindingsList(allFindings)} +
+
+ + +
+
+
👆
+
Click on a finding to preview details
+
+
@@ -113,64 +254,458 @@ function renderReviewSessionDetailPage(session) { `; } -function renderReviewFindingsGrid(dimensions) { - if (!dimensions || dimensions.length === 0) { +// ========================================== +// Findings List Rendering +// ========================================== + +function renderReviewSessionFindingsList(findings) { + if (findings.length === 0) { return ` -
-
🔍
-
No review dimensions found
+
+ + No findings match your filters
`; } - let html = ''; - dimensions.forEach(dim => { - const findings = dim.findings || []; - if (findings.length === 0) return; - - html += ` -
-
- ${escapeHtml(dim.name)} - ${findings.length} findings -
-
- ${findings.map(f => ` -
-
- ${f.severity || 'medium'} - ${f.fix_status ? `${f.fix_status}` : ''} -
-
${escapeHtml(f.title || 'Finding')}
-
${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}
- ${f.file ? `
📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''} -
- `).join('')} + return findings.map(finding => ` +
+ +
+
+ ${finding.severity} + ${escapeHtml(finding.dimension)} + ${finding.fix_status ? `${finding.fix_status}` : ''}
+
${escapeHtml(finding.title)}
+ ${finding.file ? `
📄 ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
` : ''}
- `; - }); - - return html || '
No findings
'; +
+ `).join(''); } -function initReviewSessionPage(session) { - // Initialize event handlers for review session page - // Filter handlers are inline onclick -} +// ========================================== +// Preview Panel +// ========================================== -function filterReviewFindings(severity) { - // Update filter buttons - document.querySelectorAll('.findings-filters .filter-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.severity === severity); +function previewReviewSessionFinding(findingId) { + const finding = reviewSessionState.allFindings.find(f => f.id === findingId); + if (!finding) return; + + reviewSessionState.previewFinding = finding; + + // Update active state in list + document.querySelectorAll('.review-finding-item').forEach(item => { + item.classList.toggle('previewing', item.dataset.findingId === findingId); }); - // Filter finding cards - document.querySelectorAll('.finding-card').forEach(card => { - if (severity === 'all' || card.dataset.severity === severity) { - card.style.display = ''; - } else { - card.style.display = 'none'; + const previewPanel = document.getElementById('reviewSessionPreviewPanel'); + if (!previewPanel) return; + + previewPanel.innerHTML = ` +
+
+
+ ${finding.severity} + ${escapeHtml(finding.dimension)} + ${finding.category ? `${escapeHtml(finding.category)}` : ''} + ${finding.fix_status ? `${finding.fix_status}` : ''} +
+ +
+ +

${escapeHtml(finding.title)}

+ + ${finding.file ? ` +
+
📄 Location
+
+ ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''} +
+
+ ` : ''} + +
+
📝 Description
+
${escapeHtml(finding.description)}
+
+ + ${finding.code_context ? ` +
+
💻 Code Context
+
${escapeHtml(finding.code_context)}
+
+ ` : ''} + + ${finding.recommendations && finding.recommendations.length > 0 ? ` +
+
✅ Recommendations
+
    + ${finding.recommendations.map(r => `
  • ${escapeHtml(r)}
  • `).join('')} +
+
+ ` : ''} + + ${finding.root_cause ? ` +
+
🔍 Root Cause
+
${escapeHtml(finding.root_cause)}
+
+ ` : ''} + + ${finding.impact ? ` +
+
⚠️ Impact
+
${escapeHtml(finding.impact)}
+
+ ` : ''} + + ${finding.references && finding.references.length > 0 ? ` +
+
🔗 References
+
    + ${finding.references.map(ref => { + const isUrl = ref.startsWith('http'); + return `
  • ${isUrl ? `${ref}` : ref}
  • `; + }).join('')} +
+
+ ` : ''} + + ${finding.metadata && Object.keys(finding.metadata).length > 0 ? ` +
+
ℹ️ Metadata
+ +
+ ` : ''} +
+ `; +} + +// ========================================== +// Selection Management +// ========================================== + +function toggleReviewSessionFindingSelection(findingId, event) { + if (event) { + event.stopPropagation(); + } + + if (reviewSessionState.selectedFindings.has(findingId)) { + reviewSessionState.selectedFindings.delete(findingId); + } else { + reviewSessionState.selectedFindings.add(findingId); + } + + updateReviewSessionSelectionUI(); + + // Update preview panel button if this finding is being previewed + if (reviewSessionState.previewFinding && reviewSessionState.previewFinding.id === findingId) { + previewReviewSessionFinding(findingId); + } +} + +function selectAllReviewSessionFindings() { + reviewSessionState.allFindings.forEach(f => reviewSessionState.selectedFindings.add(f.id)); + updateReviewSessionSelectionUI(); +} + +function selectVisibleReviewSessionFindings() { + reviewSessionState.filteredFindings.forEach(f => reviewSessionState.selectedFindings.add(f.id)); + updateReviewSessionSelectionUI(); +} + +function selectReviewSessionBySeverity(severity) { + reviewSessionState.allFindings + .filter(f => f.severity === severity) + .forEach(f => reviewSessionState.selectedFindings.add(f.id)); + updateReviewSessionSelectionUI(); +} + +function clearReviewSessionSelection() { + reviewSessionState.selectedFindings.clear(); + updateReviewSessionSelectionUI(); +} + +function updateReviewSessionSelectionUI() { + // Update counter + const counter = document.getElementById('reviewSessionSelectionCounter'); + if (counter) { + counter.textContent = `${reviewSessionState.selectedFindings.size} selected`; + } + + // Update export button + const exportBtn = document.getElementById('reviewSessionExportBtn'); + if (exportBtn) { + exportBtn.disabled = reviewSessionState.selectedFindings.size === 0; + } + + // Update checkbox states in list + document.querySelectorAll('.review-finding-item').forEach(item => { + const findingId = item.dataset.findingId; + const isSelected = reviewSessionState.selectedFindings.has(findingId); + item.classList.toggle('selected', isSelected); + const checkbox = item.querySelector('.finding-checkbox'); + if (checkbox) { + checkbox.checked = isSelected; } }); } + +// ========================================== +// Filtering & Sorting +// ========================================== + +function filterReviewSessionByDimension(dimension) { + reviewSessionState.currentFilters.dimension = dimension; + + // Update tab active state + document.querySelectorAll('.dim-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.dimension === dimension); + }); + + // Update dimension timeline highlight + document.querySelectorAll('.dimension-item').forEach(item => { + item.classList.toggle('active', dimension === 'all' || item.dataset.dimension === dimension); + }); + + applyReviewSessionFilters(); +} + +function toggleReviewSessionSeverity(severity) { + if (reviewSessionState.currentFilters.severities.has(severity)) { + reviewSessionState.currentFilters.severities.delete(severity); + } else { + reviewSessionState.currentFilters.severities.add(severity); + } + + // Update filter chip UI + const filterChip = document.getElementById(`rs-filter-${severity}`); + if (filterChip) { + filterChip.classList.toggle('active', reviewSessionState.currentFilters.severities.has(severity)); + const checkbox = filterChip.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = reviewSessionState.currentFilters.severities.has(severity); + } + } + + applyReviewSessionFilters(); +} + +function onReviewSessionSearch(searchText) { + reviewSessionState.currentFilters.search = searchText.toLowerCase(); + applyReviewSessionFilters(); +} + +function applyReviewSessionFilters() { + reviewSessionState.filteredFindings = reviewSessionState.allFindings.filter(finding => { + // Dimension filter + if (reviewSessionState.currentFilters.dimension !== 'all') { + if (finding.dimension !== reviewSessionState.currentFilters.dimension) { + return false; + } + } + + // Severity filter (multi-select) + if (reviewSessionState.currentFilters.severities.size > 0) { + if (!reviewSessionState.currentFilters.severities.has(finding.severity)) { + return false; + } + } + + // Search filter + if (reviewSessionState.currentFilters.search) { + const searchText = `${finding.title} ${finding.description} ${finding.file} ${finding.category}`.toLowerCase(); + if (!searchText.includes(reviewSessionState.currentFilters.search)) { + return false; + } + } + + return true; + }); + + sortReviewSessionFindings(); +} + +function sortReviewSessionFindings() { + const sortBy = document.getElementById('reviewSessionSortSelect')?.value || 'severity'; + reviewSessionState.sortConfig.field = sortBy; + + const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + + reviewSessionState.filteredFindings.sort((a, b) => { + let comparison = 0; + + if (sortBy === 'severity') { + comparison = severityOrder[a.severity] - severityOrder[b.severity]; + } else if (sortBy === 'dimension') { + comparison = a.dimension.localeCompare(b.dimension); + } else if (sortBy === 'file') { + comparison = (a.file || '').localeCompare(b.file || ''); + } + + return reviewSessionState.sortConfig.order === 'asc' ? comparison : -comparison; + }); + + renderFilteredReviewSessionFindings(); +} + +function toggleReviewSessionSortOrder() { + reviewSessionState.sortConfig.order = reviewSessionState.sortConfig.order === 'asc' ? 'desc' : 'asc'; + + const icon = document.getElementById('reviewSessionSortOrderIcon'); + if (icon) { + icon.textContent = reviewSessionState.sortConfig.order === 'asc' ? '↑' : '↓'; + } + + sortReviewSessionFindings(); +} + +function resetReviewSessionFilters() { + // Reset state + reviewSessionState.currentFilters.dimension = 'all'; + reviewSessionState.currentFilters.severities.clear(); + reviewSessionState.currentFilters.search = ''; + reviewSessionState.sortConfig.field = 'severity'; + reviewSessionState.sortConfig.order = 'desc'; + + // Reset UI + document.querySelectorAll('.dim-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.dimension === 'all'); + }); + + document.querySelectorAll('.filter-chip').forEach(chip => { + chip.classList.remove('active'); + const checkbox = chip.querySelector('input[type="checkbox"]'); + if (checkbox) checkbox.checked = false; + }); + + document.querySelectorAll('.dimension-item').forEach(item => { + item.classList.remove('active'); + }); + + const searchInput = document.getElementById('reviewSessionSearchInput'); + if (searchInput) searchInput.value = ''; + + const sortSelect = document.getElementById('reviewSessionSortSelect'); + if (sortSelect) sortSelect.value = 'severity'; + + const sortIcon = document.getElementById('reviewSessionSortOrderIcon'); + if (sortIcon) sortIcon.textContent = '↓'; + + // Re-apply filters + reviewSessionState.filteredFindings = [...reviewSessionState.allFindings]; + sortReviewSessionFindings(); +} + +function renderFilteredReviewSessionFindings() { + const listContainer = document.getElementById('reviewSessionFindingsList'); + const countEl = document.getElementById('reviewSessionFindingsCount'); + + if (listContainer) { + listContainer.innerHTML = renderReviewSessionFindingsList(reviewSessionState.filteredFindings); + } + + if (countEl) { + countEl.textContent = `${reviewSessionState.filteredFindings.length} findings`; + } +} + +// ========================================== +// Export Fix JSON +// ========================================== + +function exportReviewSessionFixJson() { + if (reviewSessionState.selectedFindings.size === 0) { + showToast('Please select at least one finding to export', 'error'); + return; + } + + const selectedFindingsData = reviewSessionState.allFindings.filter(f => + reviewSessionState.selectedFindings.has(f.id) + ); + + const session = reviewSessionState.session; + const sessionId = session?.session_id || 'unknown'; + const exportId = `fix-${Date.now()}`; + + const exportData = { + export_id: exportId, + export_timestamp: new Date().toISOString(), + review_id: `review-${sessionId}`, + session_id: sessionId, + findings_count: selectedFindingsData.length, + findings: selectedFindingsData.map(f => ({ + id: f.id, + title: f.title, + description: f.description, + severity: f.severity, + dimension: f.dimension, + category: f.category || 'uncategorized', + file: f.file, + line: f.line, + code_context: f.code_context || null, + recommendations: f.recommendations || [], + root_cause: f.root_cause || null + })) + }; + + // Convert to JSON and download + const jsonStr = JSON.stringify(exportData, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const filename = `fix-export-${exportId}.json`; + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // Show success notification + const severityCounts = { + critical: selectedFindingsData.filter(f => f.severity === 'critical').length, + high: selectedFindingsData.filter(f => f.severity === 'high').length, + medium: selectedFindingsData.filter(f => f.severity === 'medium').length, + low: selectedFindingsData.filter(f => f.severity === 'low').length + }; + + showToast(`Exported ${selectedFindingsData.length} findings (Critical: ${severityCounts.critical}, High: ${severityCounts.high}, Medium: ${severityCounts.medium}, Low: ${severityCounts.low})`, 'success'); +} + +// ========================================== +// Page Initialization +// ========================================== + +function initReviewSessionPage(session) { + // Reset state when page loads + reviewSessionState.session = session; + // Event handlers are inline onclick - no additional setup needed +} + +// Legacy filter function for compatibility +function filterReviewFindings(severity) { + if (severity === 'all') { + reviewSessionState.currentFilters.severities.clear(); + } else { + reviewSessionState.currentFilters.severities.clear(); + reviewSessionState.currentFilters.severities.add(severity); + } + applyReviewSessionFilters(); +} diff --git a/ccw/src/templates/dashboard-js/views/session-detail.js b/ccw/src/templates/dashboard-js/views/session-detail.js index e447d076..0911f0ee 100644 --- a/ccw/src/templates/dashboard-js/views/session-detail.js +++ b/ccw/src/templates/dashboard-js/views/session-detail.js @@ -11,6 +11,9 @@ function showSessionDetailPage(sessionKey) { currentSessionDetailKey = sessionKey; updateContentTitle(); + // Hide stats grid and carousel on detail pages + hideStatsAndCarousel(); + const container = document.getElementById('mainContent'); const sessionType = session.type || 'workflow'; @@ -94,6 +97,7 @@ function showSessionDetailPage(sessionKey) { 📐 IMPL Plan + ${session.hasReview ? ` + +
+
-
+ +
+ +