feat: add project overview section and enhance task item styles

- Introduced a new project overview section in the dashboard, displaying project details, technology stack, architecture, key components, and development history.
- Updated the server logic to include project overview data.
- Enhanced task item styles with status-based background colors for better visual distinction.
- Improved markdown modal functionality for viewing context and implementation plan with normalized line endings.
- Refactored task rendering logic to simplify task item display and improve performance.
This commit is contained in:
catlog22
2025-12-06 20:50:23 +08:00
parent e2c5a514cb
commit 5b14c8a832
5 changed files with 855 additions and 122 deletions

View File

@@ -19,6 +19,7 @@ export async function aggregateData(sessions, workflowDir) {
liteFix: []
},
reviewData: null,
projectOverview: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
@@ -65,6 +66,13 @@ export async function aggregateData(sessions, workflowDir) {
console.error('Error scanning lite tasks:', err.message);
}
// Load project overview from project.json
try {
data.projectOverview = loadProjectOverview(workflowDir);
} catch (err) {
console.error('Error loading project overview:', err.message);
}
return data;
}
@@ -338,3 +346,64 @@ function sortTaskIds(a, b) {
const [b1, b2] = parseId(b);
return a1 - b1 || a2 - b2;
}
/**
* Load project overview from project.json
* @param {string} workflowDir - Path to .workflow directory
* @returns {Object|null} - Project overview data or null if not found
*/
function loadProjectOverview(workflowDir) {
const projectFile = join(workflowDir, 'project.json');
if (!existsSync(projectFile)) {
console.log(`Project file not found at: ${projectFile}`);
return null;
}
try {
const fileContent = readFileSync(projectFile, 'utf8');
const projectData = JSON.parse(fileContent);
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'}`);
return {
projectName: projectData.project_name || 'Unknown',
description: projectData.overview?.description || '',
initializedAt: projectData.initialized_at || null,
technologyStack: projectData.overview?.technology_stack || {
languages: [],
frameworks: [],
build_tools: [],
test_frameworks: []
},
architecture: projectData.overview?.architecture || {
style: 'Unknown',
layers: [],
patterns: []
},
keyComponents: projectData.overview?.key_components || [],
features: projectData.features || [],
developmentIndex: projectData.development_index || {
feature: [],
enhancement: [],
bugfix: [],
refactor: [],
docs: []
},
statistics: projectData.statistics || {
total_features: 0,
total_sessions: 0,
last_updated: null
},
metadata: projectData._metadata || {
initialized_by: 'unknown',
analysis_timestamp: null,
analysis_mode: 'unknown'
}
};
} catch (err) {
console.error(`Failed to parse project.json at ${projectFile}:`, err.message);
console.error('Error stack:', err.stack);
return null;
}
}

View File

@@ -120,6 +120,7 @@ async function getWorkflowData(projectPath) {
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
reviewData: { dimensions: {} },
projectOverview: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
@@ -332,6 +333,7 @@ function generateServerDashboard(initialPath) {
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
reviewData: { dimensions: {} },
projectOverview: null,
statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 }
};

View File

@@ -879,6 +879,31 @@ code {
border-left-color: hsl(var(--muted-foreground));
}
/* Status-based background colors for task cards */
.detail-task-item.status-completed {
background: hsl(var(--success) / 0.08);
}
.detail-task-item.status-completed:hover {
background: hsl(var(--success) / 0.12);
}
.detail-task-item.status-in_progress {
background: hsl(var(--warning) / 0.08);
}
.detail-task-item.status-in_progress:hover {
background: hsl(var(--warning) / 0.12);
}
.detail-task-item.status-pending {
background: hsl(var(--muted-foreground) / 0.05);
}
.detail-task-item.status-pending:hover {
background: hsl(var(--muted-foreground) / 0.08);
}
.task-item-header {
display: flex;
align-items: center;
@@ -1687,6 +1712,21 @@ code {
line-height: 1.6;
}
.markdown-content {
background: hsl(var(--muted));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.6;
color: hsl(var(--foreground));
max-height: 600px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
/* JSON Modal */
.json-modal-overlay {
position: fixed;
@@ -3794,3 +3834,116 @@ ol.step-commands code {
.ref-files-list li {
margin-bottom: 4px;
}
/* ===================================
Markdown Modal & View Button Styles
================================== */
.btn-view-modal {
padding: 4px 12px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-left: auto;
}
.btn-view-modal:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-1px);
}
.markdown-modal .markdown-preview {
color: hsl(var(--foreground));
line-height: 1.6;
}
.markdown-modal .markdown-preview h1,
.markdown-modal .markdown-preview h2,
.markdown-modal .markdown-preview h3,
.markdown-modal .markdown-preview h4 {
color: hsl(var(--foreground));
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.markdown-modal .markdown-preview h1 { font-size: 1.8em; }
.markdown-modal .markdown-preview h2 { font-size: 1.5em; }
.markdown-modal .markdown-preview h3 { font-size: 1.3em; }
.markdown-modal .markdown-preview h4 { font-size: 1.1em; }
.markdown-modal .markdown-preview code {
background: hsl(var(--muted));
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
font-family: 'Consolas', 'Monaco', monospace;
}
.markdown-modal .markdown-preview pre {
background: hsl(var(--muted));
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
}
.markdown-modal .markdown-preview pre code {
background: transparent;
padding: 0;
}
.markdown-modal .markdown-preview ul,
.markdown-modal .markdown-preview ol {
margin: 1em 0;
padding-left: 2em;
}
.markdown-modal .markdown-preview li {
margin: 0.5em 0;
}
.markdown-modal .markdown-preview blockquote {
border-left: 4px solid hsl(var(--primary));
padding-left: 1em;
margin: 1em 0;
color: hsl(var(--muted-foreground));
}
.markdown-modal .markdown-preview table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.markdown-modal .markdown-preview th,
.markdown-modal .markdown-preview td {
border: 1px solid hsl(var(--border));
padding: 8px 12px;
text-align: left;
}
.markdown-modal .markdown-preview th {
background: hsl(var(--muted));
font-weight: 600;
}
.md-tab-btn {
cursor: pointer;
transition: all 0.2s;
}
.md-tab-btn:not(.active) {
color: hsl(var(--muted-foreground));
}
.md-tab-btn.active {
background: hsl(var(--background));
color: hsl(var(--foreground));
font-weight: 500;
}

View File

@@ -16,7 +16,8 @@
safelist: [
// Background colors
'bg-card', 'bg-background', 'bg-hover', 'bg-accent', 'bg-muted', 'bg-primary', 'bg-success', 'bg-warning',
'bg-success-light', 'bg-warning-light', 'bg-sidebar-background', 'bg-destructive',
'bg-success-light', 'bg-warning-light', 'bg-primary-light', 'bg-sidebar-background', 'bg-destructive',
'bg-destructive/5', 'bg-destructive/10', 'bg-warning/5',
// Text colors
'text-foreground', 'text-muted-foreground', 'text-primary', 'text-card-foreground', 'text-success', 'text-warning',
'text-primary-foreground', 'text-accent-foreground', 'text-sidebar-foreground', 'text-destructive',
@@ -57,6 +58,7 @@
ring: 'hsl(var(--ring))',
primary: 'hsl(var(--primary))',
'primary-foreground': 'hsl(var(--primary-foreground))',
'primary-light': 'hsl(var(--primary-light))',
secondary: 'hsl(var(--secondary))',
'secondary-foreground': 'hsl(var(--secondary-foreground))',
accent: 'hsl(var(--accent))',
@@ -99,6 +101,7 @@
--ring: 220 65% 50%;
--primary: 220 65% 50%;
--primary-foreground: 0 0% 100%;
--primary-light: 220 65% 95%;
--secondary: 220 60% 65%;
--secondary-foreground: 0 0% 100%;
--accent: 220 40% 95%;
@@ -127,6 +130,7 @@
--ring: 220 65% 55%;
--primary: 220 65% 55%;
--primary-foreground: 0 0% 100%;
--primary-light: 220 50% 25%;
--secondary: 220 60% 60%;
--secondary-foreground: 0 0% 100%;
--accent: 220 30% 20%;
@@ -202,30 +206,31 @@
</div>
</div>
<!-- Path Selector -->
<div class="flex items-center gap-2 relative">
<label class="hidden sm:inline text-sm text-muted-foreground">Project:</label>
<div class="relative">
<button class="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded text-sm text-foreground hover:bg-hover max-w-[300px]" id="pathButton">
<span class="truncate max-w-[240px]" id="currentPath">{{PROJECT_PATH}}</span>
<span class="text-xs text-muted-foreground"></span>
</button>
<div class="path-menu hidden absolute top-full left-0 mt-1 bg-card border border-border rounded-lg shadow-lg min-w-[280px] z-50" id="pathMenu">
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">Recent Projects</div>
<div id="recentPaths" class="border-t border-border">
<!-- Dynamic recent paths -->
</div>
<div class="p-2 border-t border-border">
<button class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-background border border-border rounded text-sm text-muted-foreground hover:bg-hover" id="browsePath">
📂 Browse...
</button>
<!-- Right Side Actions -->
<div class="flex items-center gap-3">
<!-- Path Selector -->
<div class="flex items-center gap-2 relative">
<label class="hidden sm:inline text-sm text-muted-foreground">Project:</label>
<div class="relative">
<button class="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded text-sm text-foreground hover:bg-hover max-w-[300px]" id="pathButton">
<span class="truncate max-w-[240px]" id="currentPath">{{PROJECT_PATH}}</span>
<span class="text-xs text-muted-foreground"></span>
</button>
<div class="path-menu hidden absolute top-full right-0 mt-1 bg-card border border-border rounded-lg shadow-lg min-w-[280px] z-50" id="pathMenu">
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">Recent Projects</div>
<div id="recentPaths" class="border-t border-border">
<!-- Dynamic recent paths -->
</div>
<div class="p-2 border-t border-border">
<button class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-background border border-border rounded text-sm text-muted-foreground hover:bg-hover" id="browsePath">
📂 Browse...
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Header Actions -->
<div class="flex items-center gap-3">
<!-- Theme Toggle -->
<button class="p-2 text-xl hover:bg-hover rounded" id="themeToggle" title="Toggle theme">🌙</button>
</div>
</header>
@@ -238,6 +243,20 @@
<!-- Sidebar -->
<aside class="sidebar w-64 bg-sidebar-background border-r border-border flex flex-col sticky top-14 h-[calc(100vh-56px)] overflow-y-auto transition-all duration-300" id="sidebar">
<nav class="flex-1 py-3">
<!-- Project Overview Section -->
<div class="mb-2" id="projectOverviewNav">
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<span class="mr-2">🏗️</span>
<span class="nav-section-title">Project</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="project-overview" data-tooltip="Project Overview">
<span>📊</span>
<span class="nav-text flex-1">Overview</span>
</li>
</ul>
</div>
<!-- Sessions Section -->
<div class="mb-2">
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">

View File

@@ -371,6 +371,19 @@ function initNavigation() {
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) {
@@ -380,7 +393,9 @@ function setActiveNavItem(item) {
function updateContentTitle() {
const titleEl = document.getElementById('contentTitle');
if (currentView === 'liteTasks') {
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') {
@@ -692,7 +707,7 @@ function renderTasksTab(session, tasks, completed, inProgress, pending) {
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
` : tasks.map(task => renderDetailTaskItem(task, false)).join(''))}
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
</div>
</div>
`;
@@ -712,11 +727,7 @@ async function loadFullTaskDetails() {
if (data.tasks && data.tasks.length > 0) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = data.tasks;
tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task, true)).join('');
// Initialize collapsible sections
tasksContainer.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task)).join('');
} else {
tasksContainer.innerHTML = `
<div class="tab-empty-state">
@@ -732,79 +743,18 @@ async function loadFullTaskDetails() {
}
}
function renderDetailTaskItem(task, showFull = false) {
const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '⟳' : '○';
function renderDetailTaskItem(task) {
const taskId = task.task_id || task.id || 'Unknown';
const status = task.status || 'pending';
if (!showFull) {
return `
<div class="detail-task-item ${task.status}" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;">
<div class="task-item-header">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
</div>
</div>
`;
}
// Full task view with collapsible sections
// Simplified task card with only essential elements: task ID badge, title, and status badge
// Includes status class for border-left color and status-${status} class for background color
return `
<div class="detail-task-item-full">
<div class="task-item-header-full" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;" title="Click to open task details">
<div class="detail-task-item ${status} status-${status}" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;">
<div class="task-item-header">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || task.meta?.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
</div>
<!-- Meta Section -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">meta</span>
<span class="section-preview">${escapeHtml(getMetaPreview(task))}</span>
</div>
<div class="collapsible-content collapsed">
${renderDynamicFields(task.meta || {}, ['type', 'action', 'agent', 'scope', 'module'])}
</div>
</div>
<!-- Context Section -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">context</span>
<span class="section-preview">${escapeHtml(getTaskContextPreview(task))}</span>
</div>
<div class="collapsible-content collapsed">
${renderTaskContext(task)}
</div>
</div>
<!-- Flow Control Section -->
${task.flow_control || task.implementation ? `
<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(getFlowPreview(task))}</span>
</div>
<div class="collapsible-content collapsed">
${renderFlowControl(task)}
</div>
</div>
` : ''}
<!-- Raw JSON Section -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">raw_json</span>
<span class="section-preview">View full JSON</span>
</div>
<div class="collapsible-content collapsed">
<pre class="json-content">${escapeHtml(JSON.stringify(task, null, 2))}</pre>
</div>
<span class="task-status-badge ${status}">${status}</span>
</div>
</div>
`;
@@ -983,13 +933,221 @@ function renderContextContent(context) {
`;
}
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 || [];
return `
<div class="context-tab-content">
<pre class="json-content">${escapeHtml(JSON.stringify(context, null, 2))}</pre>
<div class="context-tab-content space-y-6">
<!-- Header with View JSON button -->
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold text-foreground">Context Package</h3>
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors" onclick="openMarkdownModal('context-package.json', window._currentContextJson, 'json')">
👁️ View JSON
</button>
</div>
<!-- Metadata Section -->
${metadata.task_description || metadata.session_id ? `
<div class="context-section">
<h4 class="context-section-title">📋 Task Metadata</h4>
<div class="space-y-2">
${metadata.task_description ? `
<div class="context-field">
<span class="context-label">Description:</span>
<span class="context-value">${escapeHtml(metadata.task_description)}</span>
</div>
` : ''}
${metadata.session_id ? `
<div class="context-field">
<span class="context-label">Session ID:</span>
<span class="context-value font-mono text-sm">${escapeHtml(metadata.session_id)}</span>
</div>
` : ''}
${metadata.complexity ? `
<div class="context-field">
<span class="context-label">Complexity:</span>
<span class="badge badge-${metadata.complexity}">${escapeHtml(metadata.complexity)}</span>
</div>
` : ''}
${metadata.timestamp ? `
<div class="context-field">
<span class="context-label">Timestamp:</span>
<span class="context-value text-sm text-muted-foreground">${escapeHtml(metadata.timestamp)}</span>
</div>
` : ''}
${metadata.keywords && metadata.keywords.length > 0 ? `
<div class="context-field">
<span class="context-label">Keywords:</span>
<div class="flex flex-wrap gap-1 mt-1">
${metadata.keywords.map(kw => `<span class="badge badge-secondary text-xs">${escapeHtml(kw)}</span>`).join('')}
</div>
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Architecture Patterns -->
${architecturePatterns.length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">🏛️ Architecture Patterns</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
${architecturePatterns.map(p => `<li class="text-foreground">${escapeHtml(p)}</li>`).join('')}
</ul>
</div>
` : ''}
<!-- Tech Stack -->
${Object.keys(techStack).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">⚙️ Technology Stack</h4>
<div class="space-y-3">
${renderTechStackSection(techStack)}
</div>
</div>
` : ''}
<!-- Coding Conventions -->
${Object.keys(codingConventions).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">📝 Coding Conventions</h4>
<div class="space-y-3">
${renderCodingConventions(codingConventions)}
</div>
</div>
` : ''}
</div>
`;
}
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('');
}
async function loadAndRenderSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
@@ -1025,34 +1183,22 @@ function renderSummaryContent(summaries) {
`;
}
// Add event listener initialization after render
setTimeout(() => {
document.querySelectorAll('.summary-collapsible-header').forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
const icon = header.querySelector('.collapse-icon');
const isCollapsed = content.classList.contains('collapsed');
content.classList.toggle('collapsed');
header.classList.toggle('expanded');
icon.textContent = isCollapsed ? '▼' : '▶';
});
});
}, 0);
// Store summaries in global variable for modal access
window._currentSummaries = summaries;
return `
<div class="summary-tab-content">
<div class="summary-tab-content space-y-4">
${summaries.map((s, idx) => {
const preview = (s.content || '').substring(0, 100).replace(/\n/g, ' ') + (s.content?.length > 100 ? '...' : '');
const normalizedContent = normalizeLineEndings(s.content || '');
return `
<div class="summary-item-collapsible">
<div class="summary-collapsible-header ${idx === 0 ? 'expanded' : ''}">
<span class="collapse-icon">${idx === 0 ? '▼' : '▶'}</span>
<span class="summary-name">📄 ${escapeHtml(s.name || 'Summary')}</span>
<span class="summary-preview">${escapeHtml(preview)}</span>
</div>
<div class="summary-collapsible-content ${idx === 0 ? '' : 'collapsed'}">
<pre class="summary-content-pre">${escapeHtml(s.content || '')}</pre>
<div class="summary-item-direct">
<div class="flex justify-between items-center mb-2">
<h4 class="text-base font-semibold text-foreground">📄 ${escapeHtml(s.name || 'Summary')}</h4>
<button class="btn-view-modal" onclick="openMarkdownModal('${escapeHtml(s.name || 'Summary')}', window._currentSummaries[${idx}].content, 'markdown');">
👁️ View
</button>
</div>
<pre class="summary-content-pre">${escapeHtml(normalizedContent)}</pre>
</div>
`;
}).join('')}
@@ -1095,9 +1241,19 @@ function renderImplPlanContent(implPlan) {
`;
}
// Normalize and store in global variable for modal access
const normalizedContent = normalizeLineEndings(implPlan);
window._currentImplPlan = normalizedContent;
return `
<div class="impl-plan-tab-content">
<pre class="markdown-content">${escapeHtml(implPlan)}</pre>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-foreground">Implementation Plan</h3>
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors" onclick="openMarkdownModal('IMPL_PLAN.md', window._currentImplPlan, 'markdown')">
👁️ View in Modal
</button>
</div>
<pre class="markdown-content">${escapeHtml(normalizedContent)}</pre>
</div>
`;
}
@@ -3590,3 +3746,337 @@ function renderImplementationFlowchart(steps) {
}
});
}
// ========== Project Overview Rendering ==========
function renderProjectOverview() {
const container = document.getElementById('mainContent');
const project = workflowData.projectOverview;
if (!project) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="text-6xl mb-4">📋</div>
<h3 class="text-xl font-semibold text-foreground mb-2">No Project Overview</h3>
<p class="text-muted-foreground mb-4">
Run <code class="px-2 py-1 bg-muted rounded text-sm font-mono">/workflow:init</code> to initialize project analysis
</p>
</div>
`;
return;
}
container.innerHTML = `
<!-- Project Header -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-2xl font-bold text-foreground mb-2">${escapeHtml(project.projectName)}</h2>
<p class="text-muted-foreground">${escapeHtml(project.description || 'No description available')}</p>
</div>
<div class="text-sm text-muted-foreground text-right">
<div>Initialized: ${formatDate(project.initializedAt)}</div>
<div class="mt-1">Mode: <span class="font-mono text-xs px-2 py-0.5 bg-muted rounded">${escapeHtml(project.metadata?.analysis_mode || 'unknown')}</span></div>
</div>
</div>
</div>
<!-- Technology Stack -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>💻</span> Technology Stack
</h3>
<!-- Languages -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Languages</h4>
<div class="flex flex-wrap gap-3">
${project.technologyStack.languages.map(lang => `
<div class="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${lang.primary ? 'ring-2 ring-primary' : ''}">
<span class="font-semibold text-foreground">${escapeHtml(lang.name)}</span>
<span class="text-xs text-muted-foreground">${lang.file_count} files</span>
${lang.primary ? '<span class="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">Primary</span>' : ''}
</div>
`).join('') || '<span class="text-muted-foreground text-sm">No languages detected</span>'}
</div>
</div>
<!-- Frameworks -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.frameworks.map(fw => `
<span class="px-3 py-1.5 bg-success-light text-success rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No frameworks detected</span>'}
</div>
</div>
<!-- Build Tools -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Build Tools</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.build_tools.map(tool => `
<span class="px-3 py-1.5 bg-warning-light text-warning rounded-lg text-sm font-medium">${escapeHtml(tool)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No build tools detected</span>'}
</div>
</div>
<!-- Test Frameworks -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Test Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.test_frameworks.map(fw => `
<span class="px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No test frameworks detected</span>'}
</div>
</div>
</div>
<!-- Architecture -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>🏗️</span> Architecture
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<!-- Style -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Style</h4>
<div class="px-3 py-2 bg-background border border-border rounded-lg">
<span class="text-foreground font-medium">${escapeHtml(project.architecture.style)}</span>
</div>
</div>
<!-- Layers -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Layers</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.layers.map(layer => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(layer)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
<!-- Patterns -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Patterns</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.patterns.map(pattern => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(pattern)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
</div>
</div>
<!-- Key Components -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>⚙️</span> Key Components
</h3>
${project.keyComponents.length > 0 ? `
<div class="space-y-3">
${project.keyComponents.map(comp => {
const importanceColors = {
high: 'border-l-4 border-l-destructive bg-destructive/5',
medium: 'border-l-4 border-l-warning bg-warning/5',
low: 'border-l-4 border-l-muted-foreground bg-muted'
};
const importanceBadges = {
high: '<span class="px-2 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground rounded">High</span>',
medium: '<span class="px-2 py-0.5 text-xs font-semibold bg-warning text-foreground rounded">Medium</span>',
low: '<span class="px-2 py-0.5 text-xs font-semibold bg-muted text-muted-foreground rounded">Low</span>'
};
return `
<div class="p-4 ${importanceColors[comp.importance] || importanceColors.low} rounded-lg">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-foreground">${escapeHtml(comp.name)}</h4>
${importanceBadges[comp.importance] || ''}
</div>
<p class="text-sm text-muted-foreground mb-2">${escapeHtml(comp.description)}</p>
<code class="text-xs font-mono text-primary">${escapeHtml(comp.path)}</code>
</div>
`;
}).join('')}
</div>
` : '<p class="text-muted-foreground text-sm">No key components identified</p>'}
</div>
<!-- Development Index -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📝</span> Development History
</h3>
${renderDevelopmentIndex(project.developmentIndex)}
</div>
<!-- Statistics -->
<div class="bg-card border border-border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📊</span> Statistics
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-primary mb-1">${project.statistics.total_features || 0}</div>
<div class="text-sm text-muted-foreground">Total Features</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-success mb-1">${project.statistics.total_sessions || 0}</div>
<div class="text-sm text-muted-foreground">Total Sessions</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-sm text-muted-foreground mb-1">Last Updated</div>
<div class="text-sm font-medium text-foreground">${formatDate(project.statistics.last_updated)}</div>
</div>
</div>
</div>
`;
}
function renderDevelopmentIndex(devIndex) {
if (!devIndex) return '<p class="text-muted-foreground text-sm">No development history available</p>';
const categories = [
{ key: 'feature', label: 'Features', icon: '✨', badgeClass: 'bg-primary-light text-primary' },
{ key: 'enhancement', label: 'Enhancements', icon: '⚡', badgeClass: 'bg-success-light text-success' },
{ key: 'bugfix', label: 'Bug Fixes', icon: '🐛', badgeClass: 'bg-destructive/10 text-destructive' },
{ key: 'refactor', label: 'Refactorings', icon: '🔧', badgeClass: 'bg-warning-light text-warning' },
{ key: 'docs', label: 'Documentation', icon: '📚', badgeClass: 'bg-muted text-muted-foreground' }
];
const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0);
if (totalEntries === 0) {
return '<p class="text-muted-foreground text-sm">No development history entries</p>';
}
return `
<div class="space-y-4">
${categories.map(cat => {
const entries = devIndex[cat.key] || [];
if (entries.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>${cat.icon}</span>
<span>${cat.label}</span>
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
</h4>
<div class="space-y-2">
${entries.slice(0, 5).map(entry => `
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
<div class="flex items-start justify-between mb-1">
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
<span class="text-xs text-muted-foreground">${formatDate(entry.date)}</span>
</div>
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
<div class="flex items-center gap-2 text-xs">
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
</div>
</div>
`).join('')}
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
}
// ========== Helper Functions ==========
/**
* Normalize line endings in content
* Handles both literal \r\n escape sequences and actual newlines
*/
function normalizeLineEndings(content) {
if (!content) return '';
let normalized = content;
// If content has literal \r\n or \n as text (escaped), convert to actual newlines
if (normalized.includes('\\r\\n')) {
normalized = normalized.replace(/\\r\\n/g, '\n');
} else if (normalized.includes('\\n')) {
normalized = normalized.replace(/\\n/g, '\n');
}
// Normalize CRLF to LF for consistent rendering
normalized = normalized.replace(/\r\n/g, '\n');
return normalized;
}
// ========== Markdown Modal Functions ==========
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');
}
}
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeMarkdownModal();
}
});