VIBN Frontend for Coolify deployment
This commit is contained in:
966
app/api/projects/[projectId]/mvp-checklist/route.ts
Normal file
966
app/api/projects/[projectId]/mvp-checklist/route.ts
Normal file
@@ -0,0 +1,966 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import admin from '@/lib/firebase/admin';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import { getApiUrl } from '@/lib/utils/api-url';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* MVP Page & Feature Checklist Generator (AI-Powered)
|
||||
* Uses Gemini AI with the Vibn MVP Planner agent spec to generate intelligent,
|
||||
* context-aware plans from project vision answers and existing work
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const db = admin.firestore();
|
||||
|
||||
// Check if we have a saved plan
|
||||
const projectDoc = await db.collection('projects').doc(projectId).get();
|
||||
const projectData = projectDoc.data();
|
||||
|
||||
if (projectData?.mvpChecklist && !request.nextUrl.searchParams.get('regenerate')) {
|
||||
console.log('Loading saved MVP checklist');
|
||||
return NextResponse.json({
|
||||
...projectData.mvpChecklist,
|
||||
cached: true,
|
||||
cachedAt: projectData.mvpChecklistGeneratedAt
|
||||
});
|
||||
}
|
||||
|
||||
// If no checklist exists and not forcing regeneration, return empty state
|
||||
if (!projectData?.mvpChecklist && !request.nextUrl.searchParams.get('regenerate')) {
|
||||
console.log('[MVP Generation] No checklist exists - returning empty state');
|
||||
return NextResponse.json({
|
||||
error: 'No MVP checklist generated yet',
|
||||
message: 'Click "Regenerate Plan" to create your MVP checklist',
|
||||
mvpChecklist: [],
|
||||
summary: { totalPages: 0, estimatedDays: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[MVP Generation] 🚀 Starting MVP checklist generation...');
|
||||
|
||||
// Load complete history
|
||||
console.log('[MVP Generation] 📊 Loading project history...');
|
||||
const historyResponse = await fetch(
|
||||
getApiUrl(`/api/projects/${projectId}/complete-history`, request)
|
||||
);
|
||||
const history = await historyResponse.json();
|
||||
console.log('[MVP Generation] ✅ History loaded');
|
||||
|
||||
// Load intelligent analysis (with fallback if project doesn't have codebase access)
|
||||
console.log('[MVP Generation] 🧠 Running intelligent analysis...');
|
||||
let analysis = null;
|
||||
try {
|
||||
const analysisResponse = await fetch(
|
||||
getApiUrl(`/api/projects/${projectId}/plan/intelligent`, request)
|
||||
);
|
||||
if (analysisResponse.ok) {
|
||||
analysis = await analysisResponse.json();
|
||||
console.log('[MVP Generation] ✅ Analysis complete');
|
||||
} else {
|
||||
console.log('[MVP Generation] ⚠️ Analysis failed (project may lack codebase access), using fallback');
|
||||
analysis = { codebaseAnalysis: null, intelligentPlan: null };
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[MVP Generation] ⚠️ Analysis error:', error instanceof Error ? error.message : String(error));
|
||||
analysis = { codebaseAnalysis: null, intelligentPlan: null };
|
||||
}
|
||||
|
||||
// Generate MVP checklist using AI
|
||||
console.log('[MVP Generation] 🤖 Calling AI to generate MVP plan...');
|
||||
const checklist = await generateAIMVPChecklist(projectId, history, analysis, projectData);
|
||||
console.log('[MVP Generation] ✅ MVP plan generated!');
|
||||
|
||||
// Save to Firestore (filter out undefined values to avoid Firestore errors)
|
||||
const cleanChecklist = JSON.parse(JSON.stringify(checklist, (key, value) =>
|
||||
value === undefined ? null : value
|
||||
));
|
||||
|
||||
await db.collection('projects').doc(projectId).update({
|
||||
mvpChecklist: cleanChecklist,
|
||||
mvpChecklistGeneratedAt: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
console.log('[MVP Generation] ✅ MVP checklist saved to Firestore');
|
||||
|
||||
return NextResponse.json(checklist);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating MVP checklist:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate MVP checklist',
|
||||
details: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST to force regeneration of the checklist
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const db = admin.firestore();
|
||||
|
||||
console.log('[MVP Generation] 🚀 Starting MVP checklist regeneration...');
|
||||
|
||||
// Re-fetch project data
|
||||
const projectDoc = await db.collection('projects').doc(projectId).get();
|
||||
const projectData = projectDoc.data();
|
||||
|
||||
// Load complete history
|
||||
console.log('[MVP Generation] 📊 Loading project history...');
|
||||
const historyResponse = await fetch(
|
||||
getApiUrl(`/api/projects/${projectId}/complete-history`, request)
|
||||
);
|
||||
const history = await historyResponse.json();
|
||||
console.log('[MVP Generation] ✅ History loaded');
|
||||
|
||||
// Load intelligent analysis (with fallback if project doesn't have codebase access)
|
||||
console.log('[MVP Generation] 🧠 Running intelligent analysis...');
|
||||
let analysis = null;
|
||||
try {
|
||||
const analysisResponse = await fetch(
|
||||
getApiUrl(`/api/projects/${projectId}/plan/intelligent`, request)
|
||||
);
|
||||
if (analysisResponse.ok) {
|
||||
analysis = await analysisResponse.json();
|
||||
console.log('[MVP Generation] ✅ Analysis complete');
|
||||
} else {
|
||||
console.log('[MVP Generation] ⚠️ Analysis failed (project may lack codebase access), using fallback');
|
||||
analysis = { codebaseAnalysis: null, intelligentPlan: null };
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[MVP Generation] ⚠️ Analysis error:', error instanceof Error ? error.message : String(error));
|
||||
analysis = { codebaseAnalysis: null, intelligentPlan: null };
|
||||
}
|
||||
|
||||
// Generate MVP checklist using AI
|
||||
console.log('[MVP Generation] 🤖 Calling AI to generate MVP plan...');
|
||||
const checklist = await generateAIMVPChecklist(projectId, history, analysis, projectData);
|
||||
console.log('[MVP Generation] ✅ MVP plan generated!');
|
||||
console.log('[MVP Generation] 📊 Summary:', JSON.stringify(checklist.summary, null, 2));
|
||||
|
||||
// Save to Firestore (filter out undefined values to avoid Firestore errors)
|
||||
const cleanChecklist = JSON.parse(JSON.stringify(checklist, (key, value) =>
|
||||
value === undefined ? null : value
|
||||
));
|
||||
|
||||
await db.collection('projects').doc(projectId).update({
|
||||
mvpChecklist: cleanChecklist,
|
||||
mvpChecklistGeneratedAt: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
console.log('[MVP Generation] ✅ MVP checklist saved to Firestore');
|
||||
|
||||
return NextResponse.json({
|
||||
...checklist,
|
||||
regenerated: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MVP Generation] ❌ Error regenerating MVP checklist:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to regenerate MVP checklist',
|
||||
details: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI-powered MVP Checklist using Gemini and the Vibn MVP Planner agent spec
|
||||
*/
|
||||
async function generateAIMVPChecklist(
|
||||
projectId: string,
|
||||
history: any,
|
||||
analysis: any,
|
||||
projectData: any
|
||||
) {
|
||||
try {
|
||||
// Check for Gemini API key
|
||||
const geminiApiKey = process.env.GEMINI_API_KEY;
|
||||
if (!geminiApiKey) {
|
||||
console.warn('[MVP Generation] ⚠️ No GEMINI_API_KEY found, falling back to template-based generation');
|
||||
return generateFallbackChecklist(history, analysis);
|
||||
}
|
||||
|
||||
console.log('[MVP Generation] 🔑 GEMINI_API_KEY found, using AI generation');
|
||||
|
||||
// Load the agent spec
|
||||
const agentSpecPath = path.join(process.cwd(), '..', 'vibn-vision', 'initial-questions.json');
|
||||
const agentSpec = JSON.parse(fs.readFileSync(agentSpecPath, 'utf-8'));
|
||||
console.log('[MVP Generation] 📋 Agent spec loaded');
|
||||
|
||||
// Initialize Gemini
|
||||
const genAI = new GoogleGenerativeAI(geminiApiKey);
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: "gemini-2.0-flash-exp",
|
||||
generationConfig: {
|
||||
temperature: 0.4,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
maxOutputTokens: 8192,
|
||||
responseMimeType: "application/json",
|
||||
},
|
||||
});
|
||||
console.log('[MVP Generation] 🤖 Gemini model initialized (gemini-2.0-flash-exp)');
|
||||
|
||||
// Prepare vision input from project data
|
||||
const visionInput = prepareVisionInput(projectData, history);
|
||||
console.log('[MVP Generation] 📝 Vision input prepared:', {
|
||||
q1: visionInput.q1_who_and_problem.raw_answer?.substring(0, 50) + '...',
|
||||
q2: visionInput.q2_story.raw_answer?.substring(0, 50) + '...',
|
||||
q3: visionInput.q3_improvement.raw_answer?.substring(0, 50) + '...'
|
||||
});
|
||||
|
||||
// Log what data we have vs missing
|
||||
console.log('[MVP Generation] 📊 Data availability check:');
|
||||
console.log(' ✅ Vision answers:', !!projectData.visionAnswers);
|
||||
console.log(' ✅ GitHub repo:', projectData.githubRepo || 'None');
|
||||
console.log(' ⚠️ GitHub userId:', projectData.userId || 'MISSING - cannot load repo code');
|
||||
console.log(' ✅ Git commits:', history.gitSummary?.totalCommits || 0);
|
||||
console.log(' ✅ Cursor sessions:', history.summary?.breakdown?.extensionSessions || 0);
|
||||
console.log(' ✅ Codebase analysis:', analysis.codebaseAnalysis?.builtFeatures?.length || 0, 'features found');
|
||||
|
||||
// Load Cursor conversation history from Firestore
|
||||
console.log('[MVP Generation] 💬 Loading Cursor conversation history...');
|
||||
const adminDb = admin.firestore();
|
||||
let cursorConversations: any[] = [];
|
||||
let cursorMessageCount = 0;
|
||||
try {
|
||||
const conversationsSnapshot = await adminDb
|
||||
.collection('projects')
|
||||
.doc(projectId)
|
||||
.collection('cursorConversations')
|
||||
.orderBy('lastUpdatedAt', 'desc')
|
||||
.limit(10) // Get most recent 10 conversations
|
||||
.get();
|
||||
|
||||
for (const convDoc of conversationsSnapshot.docs) {
|
||||
const convData = convDoc.data();
|
||||
const messagesSnapshot = await adminDb
|
||||
.collection('projects')
|
||||
.doc(projectId)
|
||||
.collection('cursorConversations')
|
||||
.doc(convDoc.id)
|
||||
.collection('messages')
|
||||
.orderBy('createdAt', 'asc')
|
||||
.limit(50) // Limit messages per conversation to avoid token bloat
|
||||
.get();
|
||||
|
||||
const messages = messagesSnapshot.docs.map(msgDoc => {
|
||||
const msg = msgDoc.data();
|
||||
return {
|
||||
role: msg.type === 1 ? 'user' : 'assistant',
|
||||
text: msg.text || '',
|
||||
createdAt: msg.createdAt
|
||||
};
|
||||
});
|
||||
|
||||
cursorMessageCount += messages.length;
|
||||
cursorConversations.push({
|
||||
name: convData.name || 'Untitled',
|
||||
messageCount: messages.length,
|
||||
messages: messages,
|
||||
createdAt: convData.createdAt,
|
||||
lastUpdatedAt: convData.lastUpdatedAt
|
||||
});
|
||||
}
|
||||
console.log('[MVP Generation] ✅ Loaded', cursorConversations.length, 'Cursor conversations with', cursorMessageCount, 'messages');
|
||||
} catch (error) {
|
||||
console.error('[MVP Generation] ⚠️ Failed to load Cursor conversations:', error);
|
||||
}
|
||||
|
||||
// Prepare work_to_date context with all available data
|
||||
const githubSummary = history.gitSummary
|
||||
? `${history.gitSummary.totalCommits || 0} commits, ${history.gitSummary.filesChanged || 0} files changed`
|
||||
: 'No Git history available';
|
||||
|
||||
const codebaseSummary = analysis.codebaseAnalysis?.summary
|
||||
|| (analysis.codebaseAnalysis?.builtFeatures?.length > 0
|
||||
? `Built: ${analysis.codebaseAnalysis.builtFeatures.map((f: any) => f.name).join(', ')}`
|
||||
: 'No codebase analysis available');
|
||||
|
||||
const cursorSessionsSummary = cursorConversations.length > 0
|
||||
? `${cursorConversations.length} Cursor conversations with ${cursorMessageCount} messages imported from Cursor IDE`
|
||||
: 'No Cursor conversation history available';
|
||||
|
||||
// Format Cursor conversations for the prompt
|
||||
const cursorContextText = cursorConversations.length > 0
|
||||
? cursorConversations.map(conv =>
|
||||
`Conversation: "${conv.name}" (${conv.messageCount} messages)\n` +
|
||||
conv.messages.slice(0, 10).map((m: any) => ` ${m.role}: ${m.text.substring(0, 200)}`).join('\n')
|
||||
).join('\n\n')
|
||||
: '';
|
||||
|
||||
const workToDate = {
|
||||
code_summary: codebaseSummary,
|
||||
github_summary: githubSummary,
|
||||
cursor_sessions_summary: cursorSessionsSummary,
|
||||
cursor_conversations: cursorContextText, // Include actual conversation snippets
|
||||
existing_assets_notes: `Built features: ${analysis.codebaseAnalysis?.builtFeatures?.length || 0}, Missing: ${analysis.codebaseAnalysis?.missingFeatures?.length || 0}`
|
||||
};
|
||||
console.log('[MVP Generation] 🔍 Work context prepared:', {
|
||||
...workToDate,
|
||||
cursor_conversations: cursorContextText.length > 0 ? `${cursorContextText.length} chars from conversations` : 'None'
|
||||
});
|
||||
|
||||
// Build the prompt with agent spec instructions
|
||||
const prompt = `${agentSpec.agent_spec.instructions_for_model}
|
||||
|
||||
Here is the input data:
|
||||
|
||||
${JSON.stringify({
|
||||
vision_input: visionInput,
|
||||
work_to_date: workToDate
|
||||
}, null, 2)}
|
||||
|
||||
Return ONLY valid JSON matching the output schema, with no additional text or markdown.`;
|
||||
|
||||
console.log('[MVP Generation] 📤 Sending prompt to Gemini (length:', prompt.length, 'chars)');
|
||||
|
||||
// Call Gemini
|
||||
const result = await model.generateContent(prompt);
|
||||
const response = result.response;
|
||||
const text = response.text();
|
||||
|
||||
console.log('[MVP Generation] 📥 Received AI response (length:', text.length, 'chars)');
|
||||
|
||||
// Parse AI response (Gemini returns JSON directly with responseMimeType set)
|
||||
const aiResponse = JSON.parse(text);
|
||||
console.log('[MVP Generation] ✅ AI response parsed successfully');
|
||||
console.log('[MVP Generation] 🔍 AI Response structure:', JSON.stringify({
|
||||
has_journey_tree: !!aiResponse.journey_tree,
|
||||
has_touchpoints_tree: !!aiResponse.touchpoints_tree,
|
||||
has_system_tree: !!aiResponse.system_tree,
|
||||
journey_nodes: aiResponse.journey_tree?.nodes?.length || 0,
|
||||
touchpoints_nodes: aiResponse.touchpoints_tree?.nodes?.length || 0,
|
||||
system_nodes: aiResponse.system_tree?.nodes?.length || 0,
|
||||
summary: aiResponse.summary
|
||||
}, null, 2));
|
||||
|
||||
// Transform AI trees into our existing format
|
||||
const checklist = transformAIResponseToChecklist(aiResponse, history, analysis);
|
||||
console.log('[MVP Generation] ✅ Checklist transformed, total pages:', checklist.summary?.totalPages || 0);
|
||||
|
||||
return checklist;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[MVP Generation] ❌ Error generating AI MVP checklist:', error);
|
||||
console.warn('[MVP Generation] ⚠️ Falling back to template-based generation');
|
||||
return generateFallbackChecklist(history, analysis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to template-based generation if AI fails
|
||||
*/
|
||||
function generateFallbackChecklist(history: any, analysis: any) {
|
||||
const vision = history.project.vision || '';
|
||||
const builtFeatures = analysis.codebaseAnalysis?.builtFeatures || [];
|
||||
const missingFeatures = analysis.codebaseAnalysis?.missingFeatures || [];
|
||||
|
||||
// Scan commit messages for evidence of pages
|
||||
const commitMessages = history.chronologicalEvents
|
||||
.filter((e: any) => e.type === 'git_commit')
|
||||
.map((e: any) => e.data.message);
|
||||
|
||||
// Simple flat taxonomy structure (existing template)
|
||||
const corePages = [
|
||||
{
|
||||
category: 'Core Features',
|
||||
pages: [
|
||||
{
|
||||
path: '/auth',
|
||||
title: 'Authentication',
|
||||
status: detectPageStatus('auth', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('auth', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/[workspace]',
|
||||
title: 'Workspace Selector',
|
||||
status: detectPageStatus('workspace', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('workspace', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/[workspace]/projects',
|
||||
title: 'Projects List',
|
||||
status: detectPageStatus('projects page', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('projects list', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/project/[id]/overview',
|
||||
title: 'Project Dashboard',
|
||||
status: detectPageStatus('overview', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('overview', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/project/[id]/mission',
|
||||
title: 'Vision/Mission Screen',
|
||||
status: detectPageStatus('mission|vision', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('vision|mission', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/project/[id]/audit',
|
||||
title: 'Project History & Audit',
|
||||
status: detectPageStatus('audit', commitMessages, builtFeatures),
|
||||
priority: 'high',
|
||||
evidence: findEvidence('audit', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/project/[id]/timeline-plan',
|
||||
title: 'MVP Timeline & Checklist',
|
||||
status: detectPageStatus('timeline-plan', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('timeline-plan', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/api/github/oauth',
|
||||
title: 'GitHub OAuth API',
|
||||
status: detectPageStatus('github/oauth', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('github oauth', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/api/projects',
|
||||
title: 'Project Management APIs',
|
||||
status: detectPageStatus('api/projects', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('project api', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/api/projects/[id]/mvp-checklist',
|
||||
title: 'MVP Checklist Generation API',
|
||||
status: detectPageStatus('mvp-checklist', commitMessages, builtFeatures),
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('mvp-checklist', commitMessages)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Flows',
|
||||
pages: [
|
||||
{
|
||||
path: 'flow/onboarding',
|
||||
title: 'User Onboarding Flow',
|
||||
status: 'in_progress',
|
||||
priority: 'critical',
|
||||
evidence: [],
|
||||
note: 'Sign Up → Workspace Creation → Connect GitHub'
|
||||
},
|
||||
{
|
||||
path: 'flow/project-creation',
|
||||
title: 'Project Creation Flow',
|
||||
status: 'in_progress',
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('project creation', commitMessages),
|
||||
note: 'Import/New Project → Repository → History Import → Vision Setup'
|
||||
},
|
||||
{
|
||||
path: 'flow/plan-generation',
|
||||
title: 'Plan Generation Flow',
|
||||
status: 'in_progress',
|
||||
priority: 'critical',
|
||||
evidence: findEvidence('plan', commitMessages),
|
||||
note: 'Context Analysis → MVP Checklist → Timeline View'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Marketing',
|
||||
pages: [
|
||||
{
|
||||
path: '/project/[id]/marketing',
|
||||
title: 'Marketing Dashboard',
|
||||
status: 'missing',
|
||||
priority: 'high',
|
||||
evidence: [],
|
||||
note: 'Have /plan/marketing API but no UI'
|
||||
},
|
||||
{
|
||||
path: '/api/projects/[id]/plan/marketing',
|
||||
title: 'Marketing Plan Generation API',
|
||||
status: detectPageStatus('marketing api', commitMessages, builtFeatures),
|
||||
priority: 'high',
|
||||
evidence: findEvidence('marketing', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
title: 'Marketing Landing Page',
|
||||
status: detectPageStatus('marketing page', commitMessages, builtFeatures),
|
||||
priority: 'high',
|
||||
evidence: findEvidence('marketing site|landing', commitMessages)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Social',
|
||||
pages: [
|
||||
{
|
||||
path: '/[workspace]/connections',
|
||||
title: 'Social Connections & Integrations',
|
||||
status: detectPageStatus('connections', commitMessages, builtFeatures),
|
||||
priority: 'medium',
|
||||
evidence: findEvidence('connections', commitMessages)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Content',
|
||||
pages: [
|
||||
{
|
||||
path: '/docs',
|
||||
title: 'Documentation Pages',
|
||||
status: 'missing',
|
||||
priority: 'medium',
|
||||
evidence: []
|
||||
},
|
||||
{
|
||||
path: '/project/[id]/getting-started',
|
||||
title: 'Getting Started Guide',
|
||||
status: detectPageStatus('getting-started', commitMessages, builtFeatures),
|
||||
priority: 'medium',
|
||||
evidence: findEvidence('getting-started|onboarding', commitMessages)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Settings',
|
||||
pages: [
|
||||
{
|
||||
path: '/project/[id]/settings',
|
||||
title: 'Project Settings',
|
||||
status: detectPageStatus('settings', commitMessages, builtFeatures),
|
||||
priority: 'high',
|
||||
evidence: findEvidence('settings', commitMessages)
|
||||
},
|
||||
{
|
||||
path: '/[workspace]/settings',
|
||||
title: 'User Settings',
|
||||
status: detectPageStatus('settings', commitMessages, builtFeatures),
|
||||
priority: 'medium',
|
||||
evidence: findEvidence('settings', commitMessages)
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Calculate statistics
|
||||
const allPages = corePages.flatMap(c => c.pages);
|
||||
const builtCount = allPages.filter(p => p.status === 'built').length;
|
||||
const inProgressCount = allPages.filter(p => p.status === 'in_progress').length;
|
||||
const missingCount = allPages.filter(p => p.status === 'missing').length;
|
||||
|
||||
return {
|
||||
project: {
|
||||
name: history.project.name,
|
||||
vision: history.project.vision,
|
||||
githubRepo: history.project.githubRepo
|
||||
},
|
||||
|
||||
summary: {
|
||||
totalPages: allPages.length,
|
||||
built: builtCount,
|
||||
inProgress: inProgressCount,
|
||||
missing: missingCount,
|
||||
completionPercentage: Math.round((builtCount / allPages.length) * 100)
|
||||
},
|
||||
|
||||
visionSummary: extractVisionPillars(vision),
|
||||
|
||||
mvpChecklist: corePages,
|
||||
|
||||
nextSteps: generateNextSteps(corePages, missingFeatures),
|
||||
|
||||
generatedAt: new Date().toISOString(),
|
||||
|
||||
// Empty trees for fallback (will be populated when AI generation works)
|
||||
journeyTree: { label: "Journey", nodes: [] },
|
||||
touchpointsTree: { label: "Touchpoints", nodes: [] },
|
||||
systemTree: { label: "System", nodes: [] },
|
||||
};
|
||||
}
|
||||
|
||||
function detectPageStatus(pagePath: string, commitMessages: string[], builtFeatures: any[]): string {
|
||||
const searchTerms = pagePath.split('|');
|
||||
|
||||
for (const term of searchTerms) {
|
||||
const hasCommit = commitMessages.some(msg =>
|
||||
msg.toLowerCase().includes(term.toLowerCase())
|
||||
);
|
||||
const hasFeature = builtFeatures.some(f =>
|
||||
f.name.toLowerCase().includes(term.toLowerCase()) ||
|
||||
f.evidence.some((e: string) => e.toLowerCase().includes(term.toLowerCase()))
|
||||
);
|
||||
|
||||
if (hasCommit || hasFeature) {
|
||||
return 'built';
|
||||
}
|
||||
}
|
||||
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
function findEvidence(searchTerm: string, commitMessages: string[]): string[] {
|
||||
const terms = searchTerm.split('|');
|
||||
const evidence: string[] = [];
|
||||
|
||||
for (const term of terms) {
|
||||
const matches = commitMessages.filter(msg =>
|
||||
msg.toLowerCase().includes(term.toLowerCase())
|
||||
);
|
||||
evidence.push(...matches.slice(0, 2));
|
||||
}
|
||||
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function extractVisionPillars(vision: string): string[] {
|
||||
const pillars = [];
|
||||
|
||||
if (vision.includes('start from scratch') || vision.includes('import')) {
|
||||
pillars.push('Project ingestion (start from scratch or import existing work)');
|
||||
}
|
||||
if (vision.includes('understand') || vision.includes('vision')) {
|
||||
pillars.push('Project understanding (vision, history, structure, metadata)');
|
||||
}
|
||||
if (vision.includes('plan') || vision.includes('checklist')) {
|
||||
pillars.push('Project planning (auto-generated v1 roadmap/checklist)');
|
||||
}
|
||||
if (vision.includes('marketing') || vision.includes('communication') || vision.includes('automation')) {
|
||||
pillars.push('Automation + AI support (marketing, chat, context-aware support)');
|
||||
}
|
||||
|
||||
return pillars;
|
||||
}
|
||||
|
||||
function generateNextSteps(corePages: any[], missingFeatures: any[]): any[] {
|
||||
const steps = [];
|
||||
|
||||
// Find critical missing pages
|
||||
const criticalMissing = corePages
|
||||
.flatMap(c => c.pages)
|
||||
.filter(p => p.status === 'missing' && p.priority === 'critical');
|
||||
|
||||
for (const page of criticalMissing.slice(0, 3)) {
|
||||
steps.push({
|
||||
priority: 1,
|
||||
task: `Build ${page.title}`,
|
||||
path: page.path || '',
|
||||
reason: page.note || 'Critical for MVP launch'
|
||||
});
|
||||
}
|
||||
|
||||
// Add missing features
|
||||
if (missingFeatures && Array.isArray(missingFeatures)) {
|
||||
for (const feature of missingFeatures.slice(0, 2)) {
|
||||
if (feature && (feature.feature || feature.task)) {
|
||||
steps.push({
|
||||
priority: 2,
|
||||
task: feature.feature || feature.task || 'Complete missing feature',
|
||||
reason: feature.reason || 'Important for MVP'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare vision input from project data
|
||||
* Maps project vision to the 3-question format
|
||||
*/
|
||||
function prepareVisionInput(projectData: any, history: any) {
|
||||
const vision = projectData.vision || history.project?.vision || '';
|
||||
|
||||
// Try to extract answers from vision field
|
||||
// If vision is structured with questions, parse them
|
||||
// Otherwise, treat entire vision as the story (q2)
|
||||
|
||||
return {
|
||||
q1_who_and_problem: {
|
||||
prompt: "Who has the problem you want to fix and what is it?",
|
||||
raw_answer: projectData.visionAnswers?.q1 || extractProblemFromVision(vision) || vision
|
||||
},
|
||||
q2_story: {
|
||||
prompt: "Tell me a story of this person using your tool and experiencing your vision?",
|
||||
raw_answer: projectData.visionAnswers?.q2 || vision
|
||||
},
|
||||
q3_improvement: {
|
||||
prompt: "How much did that improve things for them?",
|
||||
raw_answer: projectData.visionAnswers?.q3 || extractImprovementFromVision(vision) || 'Significantly faster and more efficient workflow'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract problem statement from unstructured vision
|
||||
*/
|
||||
function extractProblemFromVision(vision: string): string {
|
||||
// Simple heuristic: Look for problem-related keywords
|
||||
const problemKeywords = ['problem', 'struggle', 'difficult', 'challenge', 'pain', 'need'];
|
||||
const sentences = vision.split(/[.!?]+/);
|
||||
|
||||
for (const sentence of sentences) {
|
||||
const lowerSentence = sentence.toLowerCase();
|
||||
if (problemKeywords.some(keyword => lowerSentence.includes(keyword))) {
|
||||
return sentence.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return vision.split(/[.!?]+/)[0]?.trim() || vision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract improvement/value from unstructured vision
|
||||
*/
|
||||
function extractImprovementFromVision(vision: string): string {
|
||||
// Look for value/benefit keywords
|
||||
const valueKeywords = ['faster', 'better', 'easier', 'save', 'improve', 'automate', 'help'];
|
||||
const sentences = vision.split(/[.!?]+/);
|
||||
|
||||
for (const sentence of sentences) {
|
||||
const lowerSentence = sentence.toLowerCase();
|
||||
if (valueKeywords.some(keyword => lowerSentence.includes(keyword))) {
|
||||
return sentence.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform AI response trees into our existing checklist format
|
||||
*/
|
||||
function transformAIResponseToChecklist(aiResponse: any, history: any, analysis: any) {
|
||||
const { journey_tree, touchpoints_tree, system_tree, summary } = aiResponse;
|
||||
|
||||
// Scan commit messages for evidence
|
||||
const commitMessages = history.chronologicalEvents
|
||||
?.filter((e: any) => e.type === 'git_commit')
|
||||
?.map((e: any) => e.data.message) || [];
|
||||
|
||||
const builtFeatures = analysis.codebaseAnalysis?.builtFeatures || [];
|
||||
|
||||
// Combine touchpoints and system into categories
|
||||
const categories: any[] = [];
|
||||
|
||||
// Process Touchpoints tree
|
||||
if (touchpoints_tree?.nodes) {
|
||||
const touchpointCategories = groupAssetsByCategory(
|
||||
touchpoints_tree.nodes,
|
||||
'touchpoint',
|
||||
commitMessages,
|
||||
builtFeatures
|
||||
);
|
||||
categories.push(...touchpointCategories);
|
||||
}
|
||||
|
||||
// Process System tree
|
||||
if (system_tree?.nodes) {
|
||||
const systemCategories = groupAssetsByCategory(
|
||||
system_tree.nodes,
|
||||
'system',
|
||||
commitMessages,
|
||||
builtFeatures
|
||||
);
|
||||
categories.push(...systemCategories);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
const allPages = categories.flatMap(c => c.pages);
|
||||
const builtCount = allPages.filter((p: any) => p.status === 'built').length;
|
||||
const inProgressCount = allPages.filter((p: any) => p.status === 'in_progress').length;
|
||||
const missingCount = allPages.filter((p: any) => p.status === 'missing').length;
|
||||
|
||||
return {
|
||||
project: {
|
||||
name: history.project.name,
|
||||
vision: history.project.vision,
|
||||
githubRepo: history.project.githubRepo
|
||||
},
|
||||
summary: {
|
||||
totalPages: allPages.length,
|
||||
built: builtCount,
|
||||
inProgress: inProgressCount,
|
||||
missing: missingCount,
|
||||
completionPercentage: Math.round((builtCount / allPages.length) * 100)
|
||||
},
|
||||
visionSummary: [summary || 'AI-generated MVP plan'],
|
||||
mvpChecklist: categories,
|
||||
nextSteps: generateNextStepsFromAI(allPages),
|
||||
generatedAt: new Date().toISOString(),
|
||||
aiGenerated: true,
|
||||
// Include raw trees for Journey/Design/Tech views
|
||||
journeyTree: journey_tree,
|
||||
touchpointsTree: touchpoints_tree,
|
||||
systemTree: system_tree,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Group asset nodes by category
|
||||
*/
|
||||
function groupAssetsByCategory(
|
||||
nodes: any[],
|
||||
listType: 'touchpoint' | 'system',
|
||||
commitMessages: string[],
|
||||
builtFeatures: any[]
|
||||
) {
|
||||
const categoryMap = new Map<string, any[]>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const category = inferCategory(node, listType);
|
||||
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, []);
|
||||
}
|
||||
|
||||
const page = {
|
||||
id: node.id,
|
||||
path: inferPath(node),
|
||||
title: node.name,
|
||||
status: detectAINodeStatus(node, commitMessages, builtFeatures),
|
||||
priority: node.must_have_for_v1 ? 'critical' : 'medium',
|
||||
evidence: findEvidenceForNode(node, commitMessages),
|
||||
note: node.asset_metadata?.why_it_exists,
|
||||
metadata: node.asset_metadata,
|
||||
requirements: flattenChildrenToRequirements(node.children)
|
||||
};
|
||||
|
||||
categoryMap.get(category)!.push(page);
|
||||
}
|
||||
|
||||
return Array.from(categoryMap.entries()).map(([category, pages]) => ({
|
||||
category,
|
||||
pages
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer category from node metadata
|
||||
*/
|
||||
function inferCategory(node: any, listType: 'touchpoint' | 'system'): string {
|
||||
const assetType = node.asset_type;
|
||||
const journeyStage = node.asset_metadata?.journey_stage || '';
|
||||
|
||||
if (listType === 'system') {
|
||||
if (assetType === 'api_endpoint' || assetType === 'service') return 'Core Features';
|
||||
if (assetType === 'integration') return 'Settings';
|
||||
return 'Settings';
|
||||
}
|
||||
|
||||
// Touchpoints
|
||||
if (assetType === 'flow') return 'Flows';
|
||||
if (assetType === 'social_post') return 'Social';
|
||||
if (assetType === 'document') return 'Content';
|
||||
if (assetType === 'email') return 'Marketing';
|
||||
if (journeyStage.toLowerCase().includes('aware') || journeyStage.toLowerCase().includes('discover')) {
|
||||
return 'Marketing';
|
||||
}
|
||||
|
||||
return 'Core Features';
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer path from node
|
||||
*/
|
||||
function inferPath(node: any): string {
|
||||
// Try to extract path from implementation_notes or name
|
||||
const implNotes = node.asset_metadata?.implementation_notes || '';
|
||||
const pathMatch = implNotes.match(/\/[\w\-\/\[\]]+/);
|
||||
if (pathMatch) return pathMatch[0];
|
||||
|
||||
// Generate a reasonable path from name and type
|
||||
const slug = node.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-]/g, '');
|
||||
|
||||
if (node.asset_type === 'api_endpoint') return `/api/${slug}`;
|
||||
if (node.asset_type === 'flow') return `flow/${slug}`;
|
||||
|
||||
return `/${slug}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect status of AI node based on existing work
|
||||
*/
|
||||
function detectAINodeStatus(node: any, commitMessages: string[], builtFeatures: any[]): string {
|
||||
const name = node.name.toLowerCase();
|
||||
const path = inferPath(node).toLowerCase();
|
||||
|
||||
// Check commit messages
|
||||
const hasCommit = commitMessages.some(msg =>
|
||||
msg.toLowerCase().includes(name) || msg.toLowerCase().includes(path)
|
||||
);
|
||||
|
||||
// Check built features
|
||||
const hasFeature = builtFeatures.some((f: any) =>
|
||||
f.name?.toLowerCase().includes(name) ||
|
||||
f.evidence?.some((e: string) => e.toLowerCase().includes(name))
|
||||
);
|
||||
|
||||
if (hasCommit || hasFeature) return 'built';
|
||||
|
||||
return node.must_have_for_v1 ? 'missing' : 'missing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Find evidence for a node in commit messages
|
||||
*/
|
||||
function findEvidenceForNode(node: any, commitMessages: string[]): string[] {
|
||||
const name = node.name.toLowerCase();
|
||||
const evidence = commitMessages
|
||||
.filter(msg => msg.toLowerCase().includes(name))
|
||||
.slice(0, 2);
|
||||
|
||||
return evidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten children nodes to requirements
|
||||
*/
|
||||
function flattenChildrenToRequirements(children: any[]): any[] {
|
||||
if (!children || children.length === 0) return [];
|
||||
|
||||
return children.map((child, index) => ({
|
||||
id: index + 1,
|
||||
text: child.name,
|
||||
status: 'missing'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate next steps from AI-generated pages
|
||||
*/
|
||||
function generateNextStepsFromAI(pages: any[]): any[] {
|
||||
const criticalMissing = pages
|
||||
.filter((p: any) => p.status === 'missing' && p.priority === 'critical')
|
||||
.slice(0, 5);
|
||||
|
||||
return criticalMissing.map((page: any, index: number) => ({
|
||||
priority: index + 1,
|
||||
task: `Build ${page.title}`,
|
||||
path: page.path || '',
|
||||
reason: page.note || 'Critical for MVP V1'
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user