mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(dashboard): add plan data loading and lite task detail styling
- Add plan.json data loading to getSessionDetailData function for lite tasks - Implement plan tab content styling with summary and approach sections - Add plan metadata grid layout for displaying task planning information - Create lite task detail page styles with task stats bar and status indicators - Add context tab content styling with improved section hierarchy - Implement path tags and JSON content display styles for plan details - Add collapsible sections for organizing plan information - Create comprehensive styling for context fields, modification points, and implementation steps - Add array and nested object rendering styles for JSON data visualization - Implement button styles for JSON view toggle functionality
This commit is contained in:
@@ -214,6 +214,18 @@ async function getSessionDetailData(sessionPath, dataType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load plan.json (for lite tasks)
|
||||||
|
if (dataType === 'plan' || dataType === 'all') {
|
||||||
|
const planFile = join(normalizedPath, 'plan.json');
|
||||||
|
if (existsSync(planFile)) {
|
||||||
|
try {
|
||||||
|
result.plan = JSON.parse(readFileSync(planFile, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
result.plan = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load IMPL_PLAN.md
|
// Load IMPL_PLAN.md
|
||||||
if (dataType === 'impl-plan' || dataType === 'all') {
|
if (dataType === 'impl-plan' || dataType === 'all') {
|
||||||
const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md');
|
const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md');
|
||||||
|
|||||||
@@ -601,15 +601,104 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-section,
|
.context-section,
|
||||||
.summary-section {
|
.summary-section,
|
||||||
|
.plan-section {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-section:last-child,
|
.context-section:last-child,
|
||||||
.summary-section:last-child {
|
.summary-section:last-child,
|
||||||
|
.plan-section:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plan Tab Styles */
|
||||||
|
.plan-tab-content {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-summary-text,
|
||||||
|
.plan-approach-text {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border-left: 3px solid hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-meta-grid .meta-item {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-meta-grid .meta-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lite Task Detail Page */
|
||||||
|
.lite-task-detail-page .detail-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lite-task-detail-page .task-stats-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lite-task-detail-page .task-stat {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.lite-task-detail-page .task-stat.completed {
|
||||||
|
color: hsl(var(--success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.lite-task-detail-page .task-stat.in-progress {
|
||||||
|
color: hsl(var(--warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Tab Content */
|
||||||
|
.context-tab-content {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-tab-content .context-section h4 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-tab-content .context-section p {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1152,3 +1241,440 @@ code {
|
|||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
Lite Task Detail Page Additions
|
||||||
|
=================================== */
|
||||||
|
|
||||||
|
/* Path Tags */
|
||||||
|
.path-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-tag {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON Content */
|
||||||
|
.json-content {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button View JSON */
|
||||||
|
.btn-view-json {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-family: monospace;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-json:hover {
|
||||||
|
background: hsl(var(--hover));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Fields */
|
||||||
|
.context-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-field {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-field p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-field ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-field ul li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modification Points */
|
||||||
|
.mod-points {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-point {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-target {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-change {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Implementation Steps */
|
||||||
|
.impl-steps {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.impl-steps li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field Groups */
|
||||||
|
.field-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value-null {
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value-boolean {
|
||||||
|
color: hsl(220 80% 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value-number {
|
||||||
|
color: hsl(142 71% 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value-string {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Array Items */
|
||||||
|
.array-value {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-item {
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-item.path-item {
|
||||||
|
font-family: monospace;
|
||||||
|
background: hsl(var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested Array */
|
||||||
|
.nested-array {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-object {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-object-header {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible Sections */
|
||||||
|
.collapsible-section {
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header:hover {
|
||||||
|
background: hsl(var(--hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header.expanded .collapse-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-preview {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
padding: 1rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary Tab */
|
||||||
|
.summary-tab-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item-collapsible {
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-collapsible-header:hover {
|
||||||
|
background: hsl(var(--hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-preview {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-collapsible-content {
|
||||||
|
padding: 1rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-collapsible-content.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content-pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON Modal */
|
||||||
|
.json-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal {
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 700px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 8px 24px rgb(0 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-close {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-close:hover {
|
||||||
|
background: hsl(var(--hover));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-content {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-modal-footer {
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-json {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
color: hsl(var(--primary-foreground));
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy-json:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flowchart Fallback */
|
||||||
|
.flowchart-fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -385,6 +385,8 @@ function updateContentTitle() {
|
|||||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||||
} else if (currentView === 'sessionDetail') {
|
} else if (currentView === 'sessionDetail') {
|
||||||
titleEl.textContent = 'Session Detail';
|
titleEl.textContent = 'Session Detail';
|
||||||
|
} else if (currentView === 'liteTaskDetail') {
|
||||||
|
titleEl.textContent = 'Lite Task Detail';
|
||||||
} else {
|
} else {
|
||||||
const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
|
const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
|
||||||
titleEl.textContent = names[currentFilter] || 'Sessions';
|
titleEl.textContent = names[currentFilter] || 'Sessions';
|
||||||
@@ -1613,12 +1615,19 @@ function renderLiteTasks() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store lite task session data for detail page access
|
||||||
|
const liteTaskDataStore = {};
|
||||||
|
|
||||||
function renderLiteTaskCard(session) {
|
function renderLiteTaskCard(session) {
|
||||||
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
|
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
|
||||||
const tasks = session.tasks || [];
|
const tasks = session.tasks || [];
|
||||||
|
|
||||||
|
// Store session data for detail page
|
||||||
|
const sessionKey = `lite-${session.type}-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||||
|
liteTaskDataStore[sessionKey] = session;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="session-card lite-task-card">
|
<div class="session-card lite-task-card" onclick="showLiteTaskDetailPage('${sessionKey}')" style="cursor: pointer;">
|
||||||
<div class="session-header">
|
<div class="session-header">
|
||||||
<div class="session-title">${escapeHtml(session.id)}</div>
|
<div class="session-title">${escapeHtml(session.id)}</div>
|
||||||
<span class="session-status ${session.type}">
|
<span class="session-status ${session.type}">
|
||||||
@@ -1641,16 +1650,410 @@ function renderLiteTaskCard(session) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${tasks.length > 0 ? `
|
|
||||||
<div class="tasks-detail-list">
|
|
||||||
${tasks.map(task => renderTaskDetail(session.id, task)).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lite Task Detail Page
|
||||||
|
function showLiteTaskDetailPage(sessionKey) {
|
||||||
|
const session = liteTaskDataStore[sessionKey];
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
currentView = 'liteTaskDetail';
|
||||||
|
currentSessionDetailKey = sessionKey;
|
||||||
|
|
||||||
|
// Also store in sessionDataStore for tab switching compatibility
|
||||||
|
sessionDataStore[sessionKey] = {
|
||||||
|
...session,
|
||||||
|
session_id: session.id,
|
||||||
|
created_at: session.createdAt,
|
||||||
|
path: session.path,
|
||||||
|
type: session.type
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = document.getElementById('mainContent');
|
||||||
|
const tasks = session.tasks || [];
|
||||||
|
const plan = session.plan || {};
|
||||||
|
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
|
||||||
|
|
||||||
|
const completed = tasks.filter(t => t.status === 'completed').length;
|
||||||
|
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
|
||||||
|
const pending = tasks.filter(t => t.status === 'pending').length;
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="session-detail-page lite-task-detail-page">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<button class="btn-back" onclick="goBackToLiteTasks()">
|
||||||
|
<span class="back-icon">←</span>
|
||||||
|
<span>Back to ${session.type === 'lite-plan' ? 'Lite Plan' : 'Lite Fix'}</span>
|
||||||
|
</button>
|
||||||
|
<div class="detail-title-row">
|
||||||
|
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '📝' : '🔧'} ${escapeHtml(session.id)}</h2>
|
||||||
|
<div class="detail-badges">
|
||||||
|
<span class="session-type-badge ${session.type}">${session.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Info Bar -->
|
||||||
|
<div class="detail-info-bar">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Created:</span>
|
||||||
|
<span class="info-value">${formatDate(session.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Tasks:</span>
|
||||||
|
<span class="info-value">${completed}/${tasks.length} completed</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Progress:</span>
|
||||||
|
<span class="info-value">${progress.percentage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="detail-tabs">
|
||||||
|
<button class="detail-tab active" data-tab="tasks" onclick="switchLiteDetailTab('tasks')">
|
||||||
|
<span class="tab-icon">📋</span>
|
||||||
|
<span class="tab-text">Tasks</span>
|
||||||
|
<span class="tab-count">${tasks.length}</span>
|
||||||
|
</button>
|
||||||
|
<button class="detail-tab" data-tab="plan" onclick="switchLiteDetailTab('plan')">
|
||||||
|
<span class="tab-icon">📐</span>
|
||||||
|
<span class="tab-text">Plan</span>
|
||||||
|
</button>
|
||||||
|
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
|
||||||
|
<span class="tab-icon">📦</span>
|
||||||
|
<span class="tab-text">Context</span>
|
||||||
|
</button>
|
||||||
|
<button class="detail-tab" data-tab="summary" onclick="switchLiteDetailTab('summary')">
|
||||||
|
<span class="tab-icon">📝</span>
|
||||||
|
<span class="tab-text">Summary</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="detail-tab-content" id="liteDetailTabContent">
|
||||||
|
${renderLiteTasksTab(session, tasks, completed, inProgress, pending)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Initialize collapsible sections
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll('.collapsible-header').forEach(header => {
|
||||||
|
header.addEventListener('click', () => toggleSection(header));
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBackToLiteTasks() {
|
||||||
|
currentView = 'liteTasks';
|
||||||
|
currentSessionDetailKey = null;
|
||||||
|
updateContentTitle();
|
||||||
|
renderLiteTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchLiteDetailTab(tabName) {
|
||||||
|
// Update active tab
|
||||||
|
document.querySelectorAll('.detail-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = liteTaskDataStore[currentSessionDetailKey];
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
const contentArea = document.getElementById('liteDetailTabContent');
|
||||||
|
const tasks = session.tasks || [];
|
||||||
|
const completed = tasks.filter(t => t.status === 'completed').length;
|
||||||
|
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
|
||||||
|
const pending = tasks.filter(t => t.status === 'pending').length;
|
||||||
|
|
||||||
|
switch (tabName) {
|
||||||
|
case 'tasks':
|
||||||
|
contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending);
|
||||||
|
// Re-initialize collapsible sections
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll('.collapsible-header').forEach(header => {
|
||||||
|
header.addEventListener('click', () => toggleSection(header));
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
break;
|
||||||
|
case 'plan':
|
||||||
|
contentArea.innerHTML = renderLitePlanTab(session);
|
||||||
|
break;
|
||||||
|
case 'context':
|
||||||
|
loadAndRenderLiteContextTab(session, contentArea);
|
||||||
|
break;
|
||||||
|
case 'summary':
|
||||||
|
loadAndRenderLiteSummaryTab(session, contentArea);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
|
||||||
|
// Populate drawer tasks for click-to-open functionality
|
||||||
|
currentDrawerTasks = tasks;
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="tab-empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<div class="empty-title">No Tasks</div>
|
||||||
|
<div class="empty-text">This session has no tasks defined.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tasks-tab-content">
|
||||||
|
<div class="task-stats-bar">
|
||||||
|
<span class="task-stat completed">✓ ${completed} completed</span>
|
||||||
|
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
|
||||||
|
<span class="task-stat pending">○ ${pending} pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="tasks-list" id="liteTasksListContent">
|
||||||
|
${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLiteTaskDetailItem(sessionId, task) {
|
||||||
|
const rawTask = task._raw || task;
|
||||||
|
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||||
|
taskJsonStore[taskJsonId] = rawTask;
|
||||||
|
|
||||||
|
const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '⟳' : '○';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="detail-task-item-full ${task.status}">
|
||||||
|
<div class="task-item-header-full" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to open task details">
|
||||||
|
<span class="task-status-icon">${statusIcon}</span>
|
||||||
|
<span class="task-id-badge">${escapeHtml(task.id)}</span>
|
||||||
|
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
|
||||||
|
<span class="task-status-badge ${task.status}">${task.status}</span>
|
||||||
|
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible: Meta -->
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<div class="collapsible-header">
|
||||||
|
<span class="collapse-icon">▶</span>
|
||||||
|
<span class="section-label">meta</span>
|
||||||
|
<span class="section-preview">${escapeHtml(getMetaPreviewForLite(task, rawTask))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content collapsed">
|
||||||
|
${renderDynamicFields(task.meta || rawTask, ['type', 'action', 'agent', 'scope', 'module'])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible: Context -->
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<div class="collapsible-header">
|
||||||
|
<span class="collapse-icon">▶</span>
|
||||||
|
<span class="section-label">context</span>
|
||||||
|
<span class="section-preview">${escapeHtml(getContextPreview(task.context, rawTask))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content collapsed">
|
||||||
|
${renderContextFields(task.context, rawTask)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible: Flow Control (with Flowchart) -->
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<div class="collapsible-header">
|
||||||
|
<span class="collapse-icon">▶</span>
|
||||||
|
<span class="section-label">flow_control</span>
|
||||||
|
<span class="section-preview">${escapeHtml(getFlowControlPreview(task.flow_control, rawTask))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content collapsed">
|
||||||
|
<div class="flowchart-container" id="flowchart-${sessionId}-${task.id}"></div>
|
||||||
|
${renderFlowControlDetails(task.flow_control, rawTask)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMetaPreviewForLite(task, rawTask) {
|
||||||
|
const meta = task.meta || {};
|
||||||
|
const parts = [];
|
||||||
|
if (meta.type || rawTask.action) parts.push(meta.type || rawTask.action);
|
||||||
|
if (meta.scope || rawTask.scope) parts.push(meta.scope || rawTask.scope);
|
||||||
|
return parts.join(' | ') || 'No meta';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTaskDrawerForLite(sessionId, taskId) {
|
||||||
|
const session = liteTaskDataStore[currentSessionDetailKey];
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
const task = session.tasks?.find(t => t.id === taskId);
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
// Set current drawer tasks
|
||||||
|
currentDrawerTasks = session.tasks || [];
|
||||||
|
|
||||||
|
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
|
||||||
|
document.getElementById('drawerContent').innerHTML = renderTaskDrawerContent(task);
|
||||||
|
document.getElementById('taskDetailDrawer').classList.add('open');
|
||||||
|
document.getElementById('drawerOverlay').classList.add('active');
|
||||||
|
|
||||||
|
// Initialize flowchart after DOM is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
renderFullFlowchart(task.flow_control || task._raw?.flow_control);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLitePlanTab(session) {
|
||||||
|
const plan = session.plan;
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
return `
|
||||||
|
<div class="tab-empty-state">
|
||||||
|
<div class="empty-icon">📐</div>
|
||||||
|
<div class="empty-title">No Plan Data</div>
|
||||||
|
<div class="empty-text">No plan.json found for this session.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="plan-tab-content">
|
||||||
|
<!-- Summary -->
|
||||||
|
${plan.summary ? `
|
||||||
|
<div class="plan-section">
|
||||||
|
<h4 class="plan-section-title">📋 Summary</h4>
|
||||||
|
<p class="plan-summary-text">${escapeHtml(plan.summary)}</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Approach -->
|
||||||
|
${plan.approach ? `
|
||||||
|
<div class="plan-section">
|
||||||
|
<h4 class="plan-section-title">🎯 Approach</h4>
|
||||||
|
<p class="plan-approach-text">${escapeHtml(plan.approach)}</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Focus Paths -->
|
||||||
|
${plan.focus_paths?.length ? `
|
||||||
|
<div class="plan-section">
|
||||||
|
<h4 class="plan-section-title">📁 Focus Paths</h4>
|
||||||
|
<div class="path-tags">
|
||||||
|
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="plan-section">
|
||||||
|
<h4 class="plan-section-title">ℹ️ Metadata</h4>
|
||||||
|
<div class="plan-meta-grid">
|
||||||
|
${plan.estimated_time ? `<div class="meta-item"><span class="meta-label">Estimated Time:</span> ${escapeHtml(plan.estimated_time)}</div>` : ''}
|
||||||
|
${plan.complexity ? `<div class="meta-item"><span class="meta-label">Complexity:</span> ${escapeHtml(plan.complexity)}</div>` : ''}
|
||||||
|
${plan.recommended_execution ? `<div class="meta-item"><span class="meta-label">Execution:</span> ${escapeHtml(plan.recommended_execution)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Raw JSON -->
|
||||||
|
<div class="plan-section">
|
||||||
|
<h4 class="plan-section-title">{ } Raw JSON</h4>
|
||||||
|
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAndRenderLiteContextTab(session, contentArea) {
|
||||||
|
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.SERVER_MODE && session.path) {
|
||||||
|
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
contentArea.innerHTML = renderLiteContextContent(data.context, session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: show plan context if available
|
||||||
|
contentArea.innerHTML = renderLiteContextContent(null, session);
|
||||||
|
} catch (err) {
|
||||||
|
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLiteContextContent(context, session) {
|
||||||
|
const plan = session.plan || {};
|
||||||
|
|
||||||
|
// If we have context from context-package.json
|
||||||
|
if (context) {
|
||||||
|
return `
|
||||||
|
<div class="context-tab-content">
|
||||||
|
<pre class="json-content">${escapeHtml(JSON.stringify(context, null, 2))}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: show context from plan
|
||||||
|
if (plan.focus_paths?.length || plan.summary) {
|
||||||
|
return `
|
||||||
|
<div class="context-tab-content">
|
||||||
|
${plan.summary ? `
|
||||||
|
<div class="context-section">
|
||||||
|
<h4>Summary</h4>
|
||||||
|
<p>${escapeHtml(plan.summary)}</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${plan.focus_paths?.length ? `
|
||||||
|
<div class="context-section">
|
||||||
|
<h4>Focus Paths</h4>
|
||||||
|
<div class="path-tags">
|
||||||
|
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tab-empty-state">
|
||||||
|
<div class="empty-icon">📦</div>
|
||||||
|
<div class="empty-title">No Context Data</div>
|
||||||
|
<div class="empty-text">No context-package.json found for this session.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAndRenderLiteSummaryTab(session, contentArea) {
|
||||||
|
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.SERVER_MODE && session.path) {
|
||||||
|
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
contentArea.innerHTML = renderSummaryContent(data.summaries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback
|
||||||
|
contentArea.innerHTML = `
|
||||||
|
<div class="tab-empty-state">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<div class="empty-title">No Summaries</div>
|
||||||
|
<div class="empty-text">No summaries found in .summaries/</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store task JSON data in a global map instead of inline script tags
|
// Store task JSON data in a global map instead of inline script tags
|
||||||
const taskJsonStore = {};
|
const taskJsonStore = {};
|
||||||
|
|
||||||
@@ -2000,13 +2403,38 @@ function toggleSection(header) {
|
|||||||
const implId = parts.pop();
|
const implId = parts.pop();
|
||||||
const sessionId = parts.join('-');
|
const sessionId = parts.join('-');
|
||||||
|
|
||||||
const session = [...(workflowData.liteTasks?.litePlan || []), ...(workflowData.liteTasks?.liteFix || [])]
|
// Try to find task from multiple sources
|
||||||
.find(s => s.id === sessionId);
|
let task = null;
|
||||||
const task = session?.tasks?.find(t => t.id === implId || t.id === 'IMPL-' + implId);
|
|
||||||
|
|
||||||
if (task?.flow_control?.implementation_approach) {
|
// 1. Try liteTaskDataStore (for lite task detail page)
|
||||||
renderFlowchart(container.id, task.flow_control.implementation_approach);
|
if (currentSessionDetailKey && liteTaskDataStore[currentSessionDetailKey]) {
|
||||||
container.dataset.rendered = 'true';
|
const session = liteTaskDataStore[currentSessionDetailKey];
|
||||||
|
task = session?.tasks?.find(t => t.id === implId || t.id === 'IMPL-' + implId || t.id === 'T' + implId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try workflowData.liteTasks (for lite tasks list view)
|
||||||
|
if (!task) {
|
||||||
|
const session = [...(workflowData.liteTasks?.litePlan || []), ...(workflowData.liteTasks?.liteFix || [])]
|
||||||
|
.find(s => s.id === sessionId);
|
||||||
|
task = session?.tasks?.find(t => t.id === implId || t.id === 'IMPL-' + implId || t.id === 'T' + implId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try sessionDataStore (for regular session detail page)
|
||||||
|
if (!task && currentSessionDetailKey && sessionDataStore[currentSessionDetailKey]) {
|
||||||
|
const session = sessionDataStore[currentSessionDetailKey];
|
||||||
|
task = session?.tasks?.find(t => (t.task_id || t.id) === implId || (t.task_id || t.id) === 'IMPL-' + implId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render flowchart if task found
|
||||||
|
if (task) {
|
||||||
|
const flowSteps = task.flow_control?.implementation_approach ||
|
||||||
|
task._raw?.flow_control?.implementation_approach ||
|
||||||
|
task._raw?.implementation ||
|
||||||
|
task.implementation;
|
||||||
|
if (flowSteps && flowSteps.length > 0) {
|
||||||
|
renderFlowchart(container.id, flowSteps);
|
||||||
|
container.dataset.rendered = 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2050,7 +2478,7 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('orient', 'auto')
|
.attr('orient', 'auto')
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
.attr('d', 'M0,-5L10,0L0,5')
|
||||||
.attr('fill', 'var(--border-color)');
|
.attr('fill', 'hsl(var(--border))');
|
||||||
|
|
||||||
// Draw arrows
|
// Draw arrows
|
||||||
for (let i = 0; i < steps.length - 1; i++) {
|
for (let i = 0; i < steps.length - 1; i++) {
|
||||||
@@ -2062,7 +2490,7 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('y1', y1)
|
.attr('y1', y1)
|
||||||
.attr('x2', width / 2)
|
.attr('x2', width / 2)
|
||||||
.attr('y2', y2)
|
.attr('y2', y2)
|
||||||
.attr('stroke', 'var(--border-color)')
|
.attr('stroke', 'hsl(var(--border))')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('marker-end', 'url(#arrow-' + containerId + ')');
|
.attr('marker-end', 'url(#arrow-' + containerId + ')');
|
||||||
}
|
}
|
||||||
@@ -2080,8 +2508,8 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('width', nodeWidth)
|
.attr('width', nodeWidth)
|
||||||
.attr('height', nodeHeight)
|
.attr('height', nodeHeight)
|
||||||
.attr('rx', 6)
|
.attr('rx', 6)
|
||||||
.attr('fill', (d, i) => i === 0 ? 'var(--accent-color)' : 'var(--bg-card)')
|
.attr('fill', (d, i) => i === 0 ? 'hsl(var(--primary))' : 'hsl(var(--card))')
|
||||||
.attr('stroke', 'var(--border-color)')
|
.attr('stroke', 'hsl(var(--border))')
|
||||||
.attr('stroke-width', 1);
|
.attr('stroke-width', 1);
|
||||||
|
|
||||||
// Step number circle
|
// Step number circle
|
||||||
@@ -2089,7 +2517,7 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('cx', 20)
|
.attr('cx', 20)
|
||||||
.attr('cy', nodeHeight / 2)
|
.attr('cy', nodeHeight / 2)
|
||||||
.attr('r', 12)
|
.attr('r', 12)
|
||||||
.attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'var(--bg-secondary)');
|
.attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'hsl(var(--muted))');
|
||||||
|
|
||||||
nodes.append('text')
|
nodes.append('text')
|
||||||
.attr('x', 20)
|
.attr('x', 20)
|
||||||
@@ -2097,7 +2525,7 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('dominant-baseline', 'central')
|
.attr('dominant-baseline', 'central')
|
||||||
.attr('font-size', '11px')
|
.attr('font-size', '11px')
|
||||||
.attr('fill', (d, i) => i === 0 ? 'white' : 'var(--text-secondary)')
|
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--muted-foreground))')
|
||||||
.text((d, i) => i + 1);
|
.text((d, i) => i + 1);
|
||||||
|
|
||||||
// Node text (step name)
|
// Node text (step name)
|
||||||
@@ -2105,7 +2533,7 @@ function renderFlowchart(containerId, steps) {
|
|||||||
.attr('x', 45)
|
.attr('x', 45)
|
||||||
.attr('y', nodeHeight / 2)
|
.attr('y', nodeHeight / 2)
|
||||||
.attr('dominant-baseline', 'central')
|
.attr('dominant-baseline', 'central')
|
||||||
.attr('fill', (d, i) => i === 0 ? 'white' : 'var(--text-primary)')
|
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--foreground))')
|
||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.text(d => {
|
.text(d => {
|
||||||
const text = d.step || d.action || 'Step';
|
const text = d.step || d.action || 'Step';
|
||||||
@@ -2434,7 +2862,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('orient', 'auto')
|
.attr('orient', 'auto')
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
.attr('d', 'M0,-5L10,0L0,5')
|
||||||
.attr('fill', 'var(--accent-color)');
|
.attr('fill', 'hsl(var(--primary))');
|
||||||
|
|
||||||
let currentY = 20;
|
let currentY = 20;
|
||||||
|
|
||||||
@@ -2476,7 +2904,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('width', nodeWidth)
|
.attr('width', nodeWidth)
|
||||||
.attr('height', nodeHeight)
|
.attr('height', nodeHeight)
|
||||||
.attr('rx', 10)
|
.attr('rx', 10)
|
||||||
.attr('fill', 'var(--bg-card)')
|
.attr('fill', 'hsl(var(--card))')
|
||||||
.attr('stroke', '#f59e0b')
|
.attr('stroke', '#f59e0b')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('stroke-dasharray', '5,3');
|
.attr('stroke-dasharray', '5,3');
|
||||||
@@ -2502,7 +2930,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 50)
|
.attr('x', 50)
|
||||||
.attr('y', 28)
|
.attr('y', 28)
|
||||||
.attr('fill', 'var(--text-primary)')
|
.attr('fill', 'hsl(var(--foreground))')
|
||||||
.attr('font-weight', '600')
|
.attr('font-weight', '600')
|
||||||
.attr('font-size', '13px')
|
.attr('font-size', '13px')
|
||||||
.text(truncateText(stepName, 40));
|
.text(truncateText(stepName, 40));
|
||||||
@@ -2512,7 +2940,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 15)
|
.attr('x', 15)
|
||||||
.attr('y', 52)
|
.attr('y', 52)
|
||||||
.attr('fill', 'var(--text-secondary)')
|
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||||
.attr('font-size', '11px')
|
.attr('font-size', '11px')
|
||||||
.text(truncateText(step.action, 50));
|
.text(truncateText(step.action, 50));
|
||||||
}
|
}
|
||||||
@@ -2539,7 +2967,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('y1', currentY)
|
.attr('y1', currentY)
|
||||||
.attr('x2', width - 40)
|
.attr('x2', width - 40)
|
||||||
.attr('y2', currentY)
|
.attr('y2', currentY)
|
||||||
.attr('stroke', 'var(--border-color)')
|
.attr('stroke', 'hsl(var(--border))')
|
||||||
.attr('stroke-width', 1)
|
.attr('stroke-width', 1)
|
||||||
.attr('stroke-dasharray', '4,4');
|
.attr('stroke-dasharray', '4,4');
|
||||||
|
|
||||||
@@ -2549,7 +2977,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('y1', currentY - nodeGap + 5)
|
.attr('y1', currentY - nodeGap + 5)
|
||||||
.attr('x2', width / 2)
|
.attr('x2', width / 2)
|
||||||
.attr('y2', currentY + sectionGap - 5)
|
.attr('y2', currentY + sectionGap - 5)
|
||||||
.attr('stroke', 'var(--accent-color)')
|
.attr('stroke', 'hsl(var(--primary))')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('marker-end', 'url(#arrowhead-impl)');
|
.attr('marker-end', 'url(#arrowhead-impl)');
|
||||||
|
|
||||||
@@ -2562,7 +2990,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
svg.append('text')
|
svg.append('text')
|
||||||
.attr('x', 20)
|
.attr('x', 20)
|
||||||
.attr('y', currentY)
|
.attr('y', currentY)
|
||||||
.attr('fill', 'var(--accent-color)')
|
.attr('fill', 'hsl(var(--primary))')
|
||||||
.attr('font-weight', 'bold')
|
.attr('font-weight', 'bold')
|
||||||
.attr('font-size', '13px')
|
.attr('font-size', '13px')
|
||||||
.text('🔧 Implementation Steps');
|
.text('🔧 Implementation Steps');
|
||||||
@@ -2579,7 +3007,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('y1', currentY + nodeHeight)
|
.attr('y1', currentY + nodeHeight)
|
||||||
.attr('x2', width / 2)
|
.attr('x2', width / 2)
|
||||||
.attr('y2', currentY + nodeHeight + nodeGap - 10)
|
.attr('y2', currentY + nodeHeight + nodeGap - 10)
|
||||||
.attr('stroke', 'var(--accent-color)')
|
.attr('stroke', 'hsl(var(--primary))')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('marker-end', 'url(#arrowhead-impl)');
|
.attr('marker-end', 'url(#arrowhead-impl)');
|
||||||
}
|
}
|
||||||
@@ -2594,8 +3022,8 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('width', nodeWidth)
|
.attr('width', nodeWidth)
|
||||||
.attr('height', nodeHeight)
|
.attr('height', nodeHeight)
|
||||||
.attr('rx', 10)
|
.attr('rx', 10)
|
||||||
.attr('fill', 'var(--bg-card)')
|
.attr('fill', 'hsl(var(--card))')
|
||||||
.attr('stroke', 'var(--accent-color)')
|
.attr('stroke', 'hsl(var(--primary))')
|
||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
// Step badge
|
// Step badge
|
||||||
@@ -2603,7 +3031,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
.attr('cx', 25)
|
.attr('cx', 25)
|
||||||
.attr('cy', 25)
|
.attr('cy', 25)
|
||||||
.attr('r', 15)
|
.attr('r', 15)
|
||||||
.attr('fill', 'var(--accent-color)');
|
.attr('fill', 'hsl(var(--primary))');
|
||||||
|
|
||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 25)
|
.attr('x', 25)
|
||||||
@@ -2618,7 +3046,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 50)
|
.attr('x', 50)
|
||||||
.attr('y', 28)
|
.attr('y', 28)
|
||||||
.attr('fill', 'var(--text-primary)')
|
.attr('fill', 'hsl(var(--foreground))')
|
||||||
.attr('font-weight', '600')
|
.attr('font-weight', '600')
|
||||||
.attr('font-size', '13px')
|
.attr('font-size', '13px')
|
||||||
.text(truncateText(step.title || 'Step ' + (idx + 1), 40));
|
.text(truncateText(step.title || 'Step ' + (idx + 1), 40));
|
||||||
@@ -2628,7 +3056,7 @@ function renderFullFlowchart(flowControl) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 15)
|
.attr('x', 15)
|
||||||
.attr('y', 52)
|
.attr('y', 52)
|
||||||
.attr('fill', 'var(--text-secondary)')
|
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||||
.attr('font-size', '11px')
|
.attr('font-size', '11px')
|
||||||
.text(truncateText(step.description, 50));
|
.text(truncateText(step.description, 50));
|
||||||
}
|
}
|
||||||
@@ -2681,7 +3109,7 @@ function renderImplementationFlowchart(steps) {
|
|||||||
.attr('orient', 'auto')
|
.attr('orient', 'auto')
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', 'M0,-5L10,0L0,5')
|
.attr('d', 'M0,-5L10,0L0,5')
|
||||||
.attr('fill', 'var(--accent-color)');
|
.attr('fill', 'hsl(var(--primary))');
|
||||||
|
|
||||||
// Draw nodes and connections
|
// Draw nodes and connections
|
||||||
steps.forEach((step, idx) => {
|
steps.forEach((step, idx) => {
|
||||||
@@ -2695,7 +3123,7 @@ function renderImplementationFlowchart(steps) {
|
|||||||
.attr('y1', y + nodeHeight)
|
.attr('y1', y + nodeHeight)
|
||||||
.attr('x2', width / 2)
|
.attr('x2', width / 2)
|
||||||
.attr('y2', y + nodeHeight + nodeGap - 10)
|
.attr('y2', y + nodeHeight + nodeGap - 10)
|
||||||
.attr('stroke', 'var(--accent-color)')
|
.attr('stroke', 'hsl(var(--primary))')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('marker-end', 'url(#arrowhead)');
|
.attr('marker-end', 'url(#arrowhead)');
|
||||||
}
|
}
|
||||||
@@ -2710,8 +3138,8 @@ function renderImplementationFlowchart(steps) {
|
|||||||
.attr('width', nodeWidth)
|
.attr('width', nodeWidth)
|
||||||
.attr('height', nodeHeight)
|
.attr('height', nodeHeight)
|
||||||
.attr('rx', 10)
|
.attr('rx', 10)
|
||||||
.attr('fill', 'var(--bg-card)')
|
.attr('fill', 'hsl(var(--card))')
|
||||||
.attr('stroke', 'var(--accent-color)')
|
.attr('stroke', 'hsl(var(--primary))')
|
||||||
.attr('stroke-width', 2)
|
.attr('stroke-width', 2)
|
||||||
.attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
|
.attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
|
||||||
|
|
||||||
@@ -2720,7 +3148,7 @@ function renderImplementationFlowchart(steps) {
|
|||||||
.attr('cx', 25)
|
.attr('cx', 25)
|
||||||
.attr('cy', 25)
|
.attr('cy', 25)
|
||||||
.attr('r', 15)
|
.attr('r', 15)
|
||||||
.attr('fill', 'var(--accent-color)');
|
.attr('fill', 'hsl(var(--primary))');
|
||||||
|
|
||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 25)
|
.attr('x', 25)
|
||||||
@@ -2735,7 +3163,7 @@ function renderImplementationFlowchart(steps) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 50)
|
.attr('x', 50)
|
||||||
.attr('y', 30)
|
.attr('y', 30)
|
||||||
.attr('fill', 'var(--text-primary)')
|
.attr('fill', 'hsl(var(--foreground))')
|
||||||
.attr('font-weight', '600')
|
.attr('font-weight', '600')
|
||||||
.attr('font-size', '14px')
|
.attr('font-size', '14px')
|
||||||
.text(truncateText(step.title || 'Step ' + (idx + 1), 35));
|
.text(truncateText(step.title || 'Step ' + (idx + 1), 35));
|
||||||
@@ -2745,7 +3173,7 @@ function renderImplementationFlowchart(steps) {
|
|||||||
nodeG.append('text')
|
nodeG.append('text')
|
||||||
.attr('x', 15)
|
.attr('x', 15)
|
||||||
.attr('y', 55)
|
.attr('y', 55)
|
||||||
.attr('fill', 'var(--text-secondary)')
|
.attr('fill', 'hsl(var(--muted-foreground))')
|
||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.text(truncateText(step.description, 45));
|
.text(truncateText(step.description, 45));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user