Skip to content

Commit d1b7a9b

Browse files
mabry1985claude
andauthored
feat: redesign project management UI with wizard-based flow (#2870)
* feat: redesign project management UI with wizard-based flow and dashboard Replace the bloated tab-based project detail view with a 6-step wizard (Define, Research, PRD, Plan, Review, Launch) for projects in pipeline, and a monitoring dashboard for active/completed projects. Key changes: - Add generate-prd endpoint (POST /api/projects/lifecycle/generate-prd) - Extend initiate endpoint to accept color/priority/description - Move EnhanceWithAI to shared location with project-specific modes - Build wizard store, step indicator, and 6 step components - Build project dashboard with card grid and filter pills - Build active project monitoring view with milestone progress - Add metadata Sheet (slide-out drawer) replacing sidebar - Add Storybook stories for all new wizard components - Remove old project-detail, projects-list, sidebar, tabs, dialog - Remove completed .automaker/projects/ files (now gitignored) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve eslint no-unused-expressions in review-step ternary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aebd6bb commit d1b7a9b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3577
-2328
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* POST /lifecycle/generate-prd - Generate SPARC PRD from project context using AI
3+
*/
4+
5+
import type { Request, Response } from 'express';
6+
import type { ProjectLifecycleService } from '../../../services/project-lifecycle-service.js';
7+
import type { ProjectService } from '../../../services/project-service.js';
8+
import { resolveModelString } from '@protolabsai/model-resolver';
9+
import { streamingQuery } from '../../../providers/simple-query-service.js';
10+
import { getErrorMessage, logError } from '../common.js';
11+
12+
const PRD_MODEL = resolveModelString('sonnet');
13+
14+
const SPARC_SYSTEM_PROMPT = `You are a senior product manager generating a SPARC PRD (Product Requirements Document).
15+
16+
You MUST respond with ONLY a valid JSON object — no markdown, no code fences, no preamble. The JSON object must have exactly these 5 string fields:
17+
18+
{
19+
"situation": "Current state and context...",
20+
"problem": "The core problem to solve...",
21+
"approach": "How we'll solve it...",
22+
"results": "Expected outcomes and success criteria...",
23+
"constraints": "Technical/business constraints and non-goals..."
24+
}
25+
26+
Each section should be 2-4 paragraphs of rich, actionable content in Markdown format (within the JSON string values). Be specific, not generic. Reference the project's actual goals and context.`;
27+
28+
export function createGeneratePrdHandler(
29+
lifecycleService: ProjectLifecycleService,
30+
projectService: ProjectService
31+
) {
32+
return async (req: Request, res: Response): Promise<void> => {
33+
try {
34+
const { projectPath, projectSlug, additionalContext } = req.body as {
35+
projectPath: string;
36+
projectSlug: string;
37+
additionalContext?: string;
38+
};
39+
40+
if (!projectPath || !projectSlug) {
41+
res.status(400).json({ success: false, error: 'projectPath and projectSlug are required' });
42+
return;
43+
}
44+
45+
const project = await projectService.getProject(projectPath, projectSlug);
46+
if (!project) {
47+
res.status(404).json({ success: false, error: `Project "${projectSlug}" not found` });
48+
return;
49+
}
50+
51+
// Build context from project data
52+
const contextParts = [
53+
`**Project:** ${project.title}`,
54+
project.goal ? `**Goal:** ${project.goal}` : '',
55+
project.description ? `**Description:** ${project.description}` : '',
56+
project.researchSummary ? `**Research Summary:**\n${project.researchSummary}` : '',
57+
additionalContext ? `**Additional Context:**\n${additionalContext}` : '',
58+
].filter(Boolean);
59+
60+
const prompt = `Generate a SPARC PRD for this project. Respond with ONLY a JSON object.\n\n${contextParts.join('\n\n')}`;
61+
62+
// Update status to drafting
63+
await projectService.updateProject(projectPath, projectSlug, { status: 'drafting' });
64+
65+
const result = await streamingQuery({
66+
prompt,
67+
systemPrompt: SPARC_SYSTEM_PROMPT,
68+
model: PRD_MODEL,
69+
cwd: projectPath,
70+
maxTurns: 1,
71+
});
72+
73+
const text = result.text || '';
74+
75+
// Parse JSON from the response — handle potential markdown code fences
76+
let prd: {
77+
situation: string;
78+
problem: string;
79+
approach: string;
80+
results: string;
81+
constraints: string;
82+
};
83+
try {
84+
const jsonMatch = text.match(/\{[\s\S]*\}/);
85+
if (!jsonMatch) throw new Error('No JSON object found in response');
86+
prd = JSON.parse(jsonMatch[0]);
87+
} catch {
88+
res.status(500).json({
89+
success: false,
90+
error: 'Failed to parse PRD from AI response',
91+
rawText: text.slice(0, 2000),
92+
});
93+
return;
94+
}
95+
96+
// Save PRD to project
97+
await projectService.updateProject(projectPath, projectSlug, {
98+
prd: {
99+
situation: prd.situation || '',
100+
problem: prd.problem || '',
101+
approach: prd.approach || '',
102+
results: prd.results || '',
103+
constraints: prd.constraints || '',
104+
generatedAt: new Date().toISOString(),
105+
},
106+
status: 'reviewing',
107+
});
108+
109+
res.json({ success: true, prd });
110+
} catch (error) {
111+
logError(error, 'Generate PRD failed');
112+
res.status(500).json({ success: false, error: getErrorMessage(error) });
113+
}
114+
};
115+
}

apps/server/src/routes/projects/lifecycle/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { ProjectLifecycleService } from '../../../services/project-lifecycl
1111
import type { ProjectService } from '../../../services/project-service.js';
1212
import { createInitiateHandler } from './initiate.js';
1313
import { createApprovePrdHandler } from './approve-prd.js';
14+
import { createGeneratePrdHandler } from './generate-prd.js';
1415
import { createLaunchHandler } from './launch.js';
1516
import { createStatusHandler } from './status.js';
1617
import { createRequestChangesHandler } from './request-changes.js';
@@ -27,7 +28,7 @@ export function createLifecycleRoutes(
2728
router.post(
2829
'/initiate',
2930
validatePathParams('projectPath'),
30-
createInitiateHandler(lifecycleService)
31+
createInitiateHandler(lifecycleService, projectService)
3132
);
3233

3334
router.post(
@@ -65,5 +66,12 @@ export function createLifecycleRoutes(
6566
createSaveMilestonesHandler(lifecycleService)
6667
);
6768

69+
router.post(
70+
'/generate-prd',
71+
validatePathParams('projectPath'),
72+
validateSlugs('projectSlug'),
73+
createGeneratePrdHandler(lifecycleService, projectService)
74+
);
75+
6876
return router;
6977
}

apps/server/src/routes/projects/lifecycle/initiate.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,31 @@
44

55
import type { Request, Response } from 'express';
66
import type { ProjectLifecycleService } from '../../../services/project-lifecycle-service.js';
7+
import type { ProjectService } from '../../../services/project-service.js';
78
import { getErrorMessage, logError } from '../common.js';
89

9-
export function createInitiateHandler(lifecycleService: ProjectLifecycleService) {
10+
export function createInitiateHandler(
11+
lifecycleService: ProjectLifecycleService,
12+
projectService: ProjectService
13+
) {
1014
return async (req: Request, res: Response): Promise<void> => {
1115
try {
12-
const { projectPath, title, ideaDescription } = req.body as {
16+
const {
17+
projectPath,
18+
title,
19+
ideaDescription,
20+
color,
21+
priority,
22+
description,
23+
researchOnCreate,
24+
} = req.body as {
1325
projectPath: string;
1426
title: string;
1527
ideaDescription: string;
28+
color?: string;
29+
priority?: string;
30+
description?: string;
31+
researchOnCreate?: boolean;
1632
};
1733

1834
if (!projectPath) {
@@ -28,7 +44,19 @@ export function createInitiateHandler(lifecycleService: ProjectLifecycleService)
2844
return;
2945
}
3046

31-
const result = await lifecycleService.initiate(projectPath, title, ideaDescription);
47+
const result = await lifecycleService.initiate(projectPath, title, ideaDescription, {
48+
researchOnCreate,
49+
});
50+
51+
// Persist color, priority, and description if provided
52+
if (result.localSlug && (color || priority || description)) {
53+
const updates: Record<string, unknown> = {};
54+
if (color) updates.color = color;
55+
if (priority) updates.priority = priority;
56+
if (description) updates.description = description;
57+
await projectService.updateProject(projectPath, result.localSlug, updates);
58+
}
59+
3260
res.json({ success: true, ...result });
3361
} catch (error) {
3462
logError(error, 'Lifecycle initiate failed');
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useState } from 'react';
2+
import { createLogger } from '@protolabsai/utils/logger';
3+
import { Button } from '@protolabsai/ui/atoms';
4+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@protolabsai/ui/atoms';
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuTrigger,
10+
} from '@protolabsai/ui/atoms';
11+
import { Sparkles, ChevronDown, ChevronRight } from 'lucide-react';
12+
import { toast } from 'sonner';
13+
import { getElectronAPI } from '@/lib/electron';
14+
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
15+
import type { EnhancementMode as CoreEnhancementMode } from '@protolabsai/types';
16+
import { type EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
17+
import { useAppStore } from '@/store/app-store';
18+
19+
const logger = createLogger('EnhanceWithAI');
20+
21+
interface EnhanceWithAIProps {
22+
/** Current text value to enhance */
23+
value: string;
24+
/** Callback when text is enhanced */
25+
onChange: (enhancedText: string) => void;
26+
/** Optional callback to track enhancement in history.
27+
* mode is typed as core EnhancementMode for backward compatibility with board-view consumers. */
28+
onHistoryAdd?: (entry: {
29+
mode: CoreEnhancementMode;
30+
originalText: string;
31+
enhancedText: string;
32+
}) => void;
33+
/** Disable the enhancement feature */
34+
disabled?: boolean;
35+
/** Additional CSS classes */
36+
className?: string;
37+
/** Restrict available modes (defaults to all modes) */
38+
modes?: EnhancementMode[];
39+
}
40+
41+
/**
42+
* Reusable "Enhance with AI" component
43+
*
44+
* Provides AI-powered text enhancement with multiple modes.
45+
* Used in feature dialogs, project wizard steps, and PRD editing.
46+
*/
47+
export function EnhanceWithAI({
48+
value,
49+
onChange,
50+
onHistoryAdd,
51+
disabled = false,
52+
className,
53+
modes,
54+
}: EnhanceWithAIProps) {
55+
const [isEnhancing, setIsEnhancing] = useState(false);
56+
const [enhancementMode, setEnhancementMode] = useState<EnhancementMode>(modes?.[0] ?? 'improve');
57+
const [enhanceOpen, setEnhanceOpen] = useState(false);
58+
59+
// Get current project path for per-project Claude API profile
60+
const currentProjectPath = useAppStore((state) => state.currentProject?.path);
61+
62+
// Enhancement model override
63+
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
64+
65+
const availableModes = modes
66+
? (Object.entries(ENHANCEMENT_MODE_LABELS) as [EnhancementMode, string][]).filter(([mode]) =>
67+
modes.includes(mode)
68+
)
69+
: (Object.entries(ENHANCEMENT_MODE_LABELS) as [EnhancementMode, string][]);
70+
71+
const handleEnhance = async () => {
72+
if (!value.trim() || isEnhancing || disabled) return;
73+
74+
setIsEnhancing(true);
75+
try {
76+
const api = getElectronAPI();
77+
const result = await api.enhancePrompt?.enhance(
78+
value,
79+
enhancementMode,
80+
enhancementOverride.effectiveModel,
81+
enhancementOverride.effectiveModelEntry.thinkingLevel,
82+
currentProjectPath
83+
);
84+
85+
if (result?.success && result.enhancedText) {
86+
const originalText = value;
87+
const enhancedText = result.enhancedText;
88+
onChange(enhancedText);
89+
90+
// Track in history if callback provided (includes original for restoration)
91+
onHistoryAdd?.({
92+
mode: enhancementMode as CoreEnhancementMode,
93+
originalText,
94+
enhancedText,
95+
});
96+
97+
toast.success('Enhanced successfully!');
98+
} else {
99+
toast.error(result?.error || 'Failed to enhance');
100+
}
101+
} catch (error) {
102+
logger.error('Enhancement failed:', error);
103+
toast.error('Failed to enhance');
104+
} finally {
105+
setIsEnhancing(false);
106+
}
107+
};
108+
109+
return (
110+
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen} className={className}>
111+
<CollapsibleTrigger asChild>
112+
<button
113+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"
114+
disabled={disabled}
115+
>
116+
{enhanceOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
117+
<Sparkles className="w-4 h-4" />
118+
<span>Enhance with AI</span>
119+
</button>
120+
</CollapsibleTrigger>
121+
<CollapsibleContent className="pt-3">
122+
<div className="flex flex-wrap items-center gap-2 pl-6">
123+
<DropdownMenu>
124+
<DropdownMenuTrigger asChild>
125+
<Button variant="outline" size="sm" className="h-8 text-xs" disabled={disabled}>
126+
{ENHANCEMENT_MODE_LABELS[enhancementMode]}
127+
<ChevronDown className="w-3 h-3 ml-1" />
128+
</Button>
129+
</DropdownMenuTrigger>
130+
<DropdownMenuContent align="start">
131+
{availableModes.map(([mode, label]) => (
132+
<DropdownMenuItem key={mode} onClick={() => setEnhancementMode(mode)}>
133+
{label}
134+
</DropdownMenuItem>
135+
))}
136+
</DropdownMenuContent>
137+
</DropdownMenu>
138+
139+
<Button
140+
type="button"
141+
variant="default"
142+
size="sm"
143+
className="h-8 text-xs"
144+
onClick={handleEnhance}
145+
disabled={!value.trim() || isEnhancing || disabled}
146+
loading={isEnhancing}
147+
>
148+
<Sparkles className="w-3 h-3 mr-1" />
149+
Enhance
150+
</Button>
151+
152+
<ModelOverrideTrigger
153+
currentModelEntry={enhancementOverride.effectiveModelEntry}
154+
onModelChange={enhancementOverride.setOverride}
155+
phase="enhancementModel"
156+
isOverridden={enhancementOverride.isOverridden}
157+
size="sm"
158+
variant="icon"
159+
/>
160+
</div>
161+
</CollapsibleContent>
162+
</Collapsible>
163+
);
164+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { EnhancementMode as CoreEnhancementMode } from '@protolabsai/types';
2+
3+
/** Enhancement modes — extends core modes with project-specific ones */
4+
export type EnhancementMode = CoreEnhancementMode | 'expand' | 'research';
5+
6+
/** Labels for enhancement modes displayed in the UI */
7+
export const ENHANCEMENT_MODE_LABELS: Record<EnhancementMode, string> = {
8+
improve: 'Improve Clarity',
9+
technical: 'Add Technical Details',
10+
simplify: 'Simplify',
11+
acceptance: 'Add Acceptance Criteria',
12+
'ux-reviewer': 'User Experience',
13+
expand: 'Expand & Detail',
14+
research: 'Research & Enrich',
15+
};
16+
17+
/** Descriptions for enhancement modes (for tooltips/accessibility) */
18+
export const ENHANCEMENT_MODE_DESCRIPTIONS: Record<EnhancementMode, string> = {
19+
improve: 'Make the prompt clearer and more concise',
20+
technical: 'Add implementation details and specifications',
21+
simplify: 'Reduce complexity while keeping the core intent',
22+
acceptance: 'Add specific acceptance criteria and test cases',
23+
'ux-reviewer': 'Add user experience considerations and flows',
24+
expand: 'Expand with more detail, context, and specificity',
25+
research: 'Enrich with research findings and industry best practices',
26+
};

0 commit comments

Comments
 (0)