import { NextRequest, NextResponse } from 'next/server'; import { adminDb } from '@/lib/firebase/admin'; import { getApiUrl } from '@/lib/utils/api-url'; /** * Complete Project Context API * Returns everything an AI needs to understand the project state */ export async function GET( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; // 1. Load project metadata const projectDoc = await adminDb .collection('projects') .doc(projectId) .get(); if (!projectDoc.exists) { return NextResponse.json( { error: 'Project not found' }, { status: 404 } ); } const projectData = projectDoc.data(); // 2. Load timeline data const timelineResponse = await fetch( getApiUrl(`/api/projects/${projectId}/timeline`, request) ); const timeline = timelineResponse.ok ? await timelineResponse.json() : null; // 3. Load Git history summary const gitResponse = await fetch( getApiUrl(`/api/projects/${projectId}/git-history`, request) ); const gitHistory = gitResponse.ok ? await gitResponse.json() : null; // 4. Load extension activity const activityResponse = await fetch( getApiUrl(`/api/projects/${projectId}/activity`, request) ); const activity = activityResponse.ok ? await activityResponse.json() : null; // 5. Load uploaded documents const documentsSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('documents') .orderBy('uploadedAt', 'desc') .get(); const documents = documentsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // 6. Get recent conversations (last 7 days) const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const conversationsSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .where('createdAt', '>=', sevenDaysAgo.toISOString()) .orderBy('createdAt', 'desc') .limit(10) .get(); const recentConversations = []; for (const convDoc of conversationsSnapshot.docs) { const conv = convDoc.data(); const messagesSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .doc(convDoc.id) .collection('messages') .orderBy('createdAt', 'desc') .limit(5) .get(); recentConversations.push({ id: convDoc.id, name: conv.name, createdAt: conv.createdAt, recentMessages: messagesSnapshot.docs.map(m => ({ type: m.data().type === 1 ? 'user' : 'assistant', text: m.data().text?.substring(0, 200) + '...', createdAt: m.data().createdAt })) }); } // 7. Calculate key metrics const activeDays = timeline?.days?.filter((d: any) => d.summary.totalGitCommits > 0 || d.summary.totalExtensionSessions > 0 || d.summary.totalCursorMessages > 0 ).length || 0; const topFiles = activity?.fileActivity?.slice(0, 10) || []; // 8. Extract key milestones (commits with significant changes) const keyMilestones = gitHistory?.commits ?.filter((c: any) => c.insertions + c.deletions > 1000) .slice(0, 5) .map((c: any) => ({ date: c.date, message: c.message, author: c.author, impact: `+${c.insertions}/-${c.deletions} lines` })) || []; // 9. Generate AI-friendly summary const context = { project: { id: projectId, name: projectData?.name || 'Untitled Project', vision: projectData?.vision || null, description: projectData?.description || null, createdAt: projectData?.createdAt || null, githubRepo: projectData?.githubRepo || null }, timeline: { dateRange: { earliest: timeline?.dateRange?.earliest, latest: timeline?.dateRange?.latest, totalDays: timeline?.dateRange?.totalDays || 0, activeDays }, dataSources: { git: { available: timeline?.dataSources?.git?.available || false, totalCommits: timeline?.dataSources?.git?.totalRecords || 0, dateRange: { first: timeline?.dataSources?.git?.firstDate, last: timeline?.dataSources?.git?.lastDate } }, extension: { available: timeline?.dataSources?.extension?.available || false, totalSessions: timeline?.dataSources?.extension?.totalRecords || 0, dateRange: { first: timeline?.dataSources?.extension?.firstDate, last: timeline?.dataSources?.extension?.lastDate } }, cursor: { available: timeline?.dataSources?.cursor?.available || false, totalMessages: timeline?.dataSources?.cursor?.totalRecords || 0, dateRange: { first: timeline?.dataSources?.cursor?.firstDate, last: timeline?.dataSources?.cursor?.lastDate } } } }, codebase: { totalCommits: gitHistory?.totalCommits || 0, totalLinesAdded: gitHistory?.totalInsertions || 0, totalLinesRemoved: gitHistory?.totalDeletions || 0, contributors: gitHistory?.authors || [], topFiles: gitHistory?.topFiles?.slice(0, 20) || [] }, activity: { totalSessions: activity?.totalSessions || 0, uniqueFilesEdited: activity?.fileActivity?.length || 0, topEditedFiles: topFiles, recentConversations }, milestones: keyMilestones, documents: documents.map(doc => ({ id: doc.id, title: doc.title, type: doc.type, uploadedAt: doc.uploadedAt, contentPreview: doc.content?.substring(0, 500) + '...' })), summary: generateProjectSummary({ projectData, timeline, gitHistory, activity, documents }) }; return NextResponse.json(context); } catch (error) { console.error('Error loading project context:', error); return NextResponse.json( { error: 'Failed to load project context', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // Helper to generate human-readable summary function generateProjectSummary(data: any): string { const { projectData, timeline, gitHistory, activity, documents } = data; const parts = []; // Project basics if (projectData?.name) { parts.push(`Project: ${projectData.name}`); } if (projectData?.vision) { parts.push(`Vision: ${projectData.vision}`); } // Timeline if (timeline?.dateRange?.totalDays) { parts.push(`Development span: ${timeline.dateRange.totalDays} days`); } // Git stats if (gitHistory?.totalCommits) { parts.push( `Code: ${gitHistory.totalCommits} commits, ` + `+${gitHistory.totalInsertions.toLocaleString()}/-${gitHistory.totalDeletions.toLocaleString()} lines` ); } // Activity if (activity?.totalSessions) { parts.push(`Activity: ${activity.totalSessions} development sessions`); } // Documents if (documents?.length) { parts.push(`Documentation: ${documents.length} documents uploaded`); } return parts.join(' | '); }