957 lines
40 KiB
TypeScript
957 lines
40 KiB
TypeScript
'use client';
|
|
|
|
import { use, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Loader2, FileText, TrendingUp, DollarSign, Code, Calendar, Clock } from 'lucide-react';
|
|
|
|
interface AuditReport {
|
|
projectId: string;
|
|
generatedAt: string;
|
|
timeline: {
|
|
firstActivity: string | null;
|
|
lastActivity: string | null;
|
|
totalDays: number;
|
|
activeDays: number;
|
|
totalSessions: number;
|
|
sessions: Array<{
|
|
sessionId: string;
|
|
date: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
duration: number;
|
|
messageCount: number;
|
|
userMessages: number;
|
|
aiMessages: number;
|
|
topics: string[];
|
|
filesWorkedOn: string[];
|
|
}>;
|
|
velocity: {
|
|
messagesPerDay: number;
|
|
averageSessionLength: number;
|
|
peakProductivityHours: number[];
|
|
};
|
|
};
|
|
costs: {
|
|
messageStats: {
|
|
totalMessages: number;
|
|
userMessages: number;
|
|
aiMessages: number;
|
|
avgMessageLength: number;
|
|
};
|
|
estimatedTokens: {
|
|
input: number;
|
|
output: number;
|
|
total: number;
|
|
};
|
|
costs: {
|
|
inputCost: number;
|
|
outputCost: number;
|
|
totalCost: number;
|
|
currency: string;
|
|
};
|
|
model: string;
|
|
pricing: {
|
|
inputPer1M: number;
|
|
outputPer1M: number;
|
|
};
|
|
};
|
|
features: Array<{
|
|
name: string;
|
|
description: string;
|
|
pages: string[];
|
|
apis: string[];
|
|
status: string;
|
|
}>;
|
|
techStack: {
|
|
frontend: Record<string, string>;
|
|
backend: Record<string, string>;
|
|
integrations: string[];
|
|
};
|
|
extensionActivity: {
|
|
totalSessions: number;
|
|
uniqueFilesEdited: number;
|
|
topFiles: Array<{ file: string; editCount: number }>;
|
|
earliestActivity: string | null;
|
|
latestActivity: string | null;
|
|
} | null;
|
|
gitHistory: {
|
|
totalCommits: number;
|
|
firstCommit: string | null;
|
|
lastCommit: string | null;
|
|
totalFilesChanged: number;
|
|
totalInsertions: number;
|
|
totalDeletions: number;
|
|
commits: Array<{
|
|
hash: string;
|
|
date: string;
|
|
author: string;
|
|
message: string;
|
|
filesChanged: number;
|
|
insertions: number;
|
|
deletions: number;
|
|
}>;
|
|
topFiles: Array<{ filePath: string; changeCount: number }>;
|
|
commitsByDay: Record<string, number>;
|
|
authors: Array<{ name: string; commitCount: number }>;
|
|
} | null;
|
|
unifiedTimeline: {
|
|
projectId: string;
|
|
dateRange: {
|
|
earliest: string;
|
|
latest: string;
|
|
totalDays: number;
|
|
};
|
|
days: Array<{
|
|
date: string;
|
|
dayOfWeek: string;
|
|
gitCommits: any[];
|
|
extensionSessions: any[];
|
|
cursorMessages: any[];
|
|
summary: {
|
|
totalGitCommits: number;
|
|
totalExtensionSessions: number;
|
|
totalCursorMessages: number;
|
|
linesAdded: number;
|
|
linesRemoved: number;
|
|
uniqueFilesModified: number;
|
|
};
|
|
}>;
|
|
dataSources: {
|
|
git: { available: boolean; firstDate: string | null; lastDate: string | null; totalRecords: number };
|
|
extension: { available: boolean; firstDate: string | null; lastDate: string | null; totalRecords: number };
|
|
cursor: { available: boolean; firstDate: string | null; lastDate: string | null; totalRecords: number };
|
|
};
|
|
} | null;
|
|
summary: {
|
|
totalConversations: number;
|
|
totalMessages: number;
|
|
developmentPeriod: number;
|
|
estimatedCost: number;
|
|
extensionSessions: number;
|
|
filesEdited: number;
|
|
gitCommits: number;
|
|
linesAdded: number;
|
|
linesRemoved: number;
|
|
timelineDays: number;
|
|
};
|
|
}
|
|
|
|
export default function ProjectAuditPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ workspace: string; projectId: string }>;
|
|
}) {
|
|
const { workspace, projectId } = use(params);
|
|
const [report, setReport] = useState<AuditReport | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const generateReport = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectId}/audit/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'Failed to generate report');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setReport(data);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateStr: string | null) => {
|
|
if (!dateStr) return 'N/A';
|
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
};
|
|
|
|
const formatNumber = (num: number) => {
|
|
return new Intl.NumberFormat('en-US').format(num);
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Project Audit Report</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Comprehensive analysis of development history, costs, and architecture
|
|
</p>
|
|
</div>
|
|
<Button onClick={generateReport} disabled={loading}>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Generating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FileText className="mr-2 h-4 w-4" />
|
|
Generate Report
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{error && (
|
|
<Card className="border-destructive">
|
|
<CardHeader>
|
|
<CardTitle className="text-destructive">Error</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>{error}</p>
|
|
{error.includes('No conversations found') && (
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Import Cursor conversations first to generate an audit report.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{!report && !loading && !error && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Ready to Generate</CardTitle>
|
|
<CardDescription>
|
|
Click the button above to analyze your project's development history,
|
|
calculate costs, and document your architecture.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div className="flex items-center space-x-3">
|
|
<Calendar className="h-8 w-8 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-semibold">Timeline Analysis</p>
|
|
<p className="text-sm text-muted-foreground">Work sessions & velocity</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<DollarSign className="h-8 w-8 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-semibold">Cost Estimation</p>
|
|
<p className="text-sm text-muted-foreground">AI & developer costs</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<Code className="h-8 w-8 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-semibold">Architecture</p>
|
|
<p className="text-sm text-muted-foreground">Features & tech stack</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{report && (
|
|
<div className="space-y-6">
|
|
{/* Summary Section */}
|
|
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-6">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Total Messages
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{formatNumber(report.summary.totalMessages)}</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{report.summary.totalConversations} conversations
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Development Period
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{report.summary.developmentPeriod} days</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{report.timeline.activeDays} active days
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Work Sessions
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{report.timeline.totalSessions}</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Avg {report.timeline.velocity.averageSessionLength} min
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
AI Cost
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{formatCurrency(report.summary.estimatedCost)}</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{report.costs.model}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Git Commits
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{formatNumber(report.summary.gitCommits)}</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Code changes
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Lines Changed
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-lg font-bold">
|
|
<span className="text-green-600">+{formatNumber(report.summary.linesAdded)}</span>
|
|
{' / '}
|
|
<span className="text-red-600">-{formatNumber(report.summary.linesRemoved)}</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Total modifications
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Unified Timeline Section */}
|
|
{report.unifiedTimeline && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Calendar className="mr-2 h-5 w-5" />
|
|
Complete Project Timeline
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Day-by-day history combining Git commits, Extension activity, and Cursor messages
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Data Source Overview */}
|
|
<div className="grid gap-4 md:grid-cols-3 mb-6">
|
|
<div className={`border rounded-lg p-3 ${report.unifiedTimeline.dataSources.git.available ? 'bg-green-50 border-green-200' : 'bg-gray-50'}`}>
|
|
<p className="text-sm font-medium mb-1">📊 Git Commits</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{report.unifiedTimeline.dataSources.git.available ? (
|
|
<>
|
|
{report.unifiedTimeline.dataSources.git.totalRecords} commits<br/>
|
|
{formatDate(report.unifiedTimeline.dataSources.git.firstDate)} to {formatDate(report.unifiedTimeline.dataSources.git.lastDate)}
|
|
</>
|
|
) : 'No data'}
|
|
</p>
|
|
</div>
|
|
<div className={`border rounded-lg p-3 ${report.unifiedTimeline.dataSources.extension.available ? 'bg-blue-50 border-blue-200' : 'bg-gray-50'}`}>
|
|
<p className="text-sm font-medium mb-1">💻 Extension Activity</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{report.unifiedTimeline.dataSources.extension.available ? (
|
|
<>
|
|
{report.unifiedTimeline.dataSources.extension.totalRecords} sessions<br/>
|
|
{formatDate(report.unifiedTimeline.dataSources.extension.firstDate)} to {formatDate(report.unifiedTimeline.dataSources.extension.lastDate)}
|
|
</>
|
|
) : 'No data'}
|
|
</p>
|
|
</div>
|
|
<div className={`border rounded-lg p-3 ${report.unifiedTimeline.dataSources.cursor.available ? 'bg-purple-50 border-purple-200' : 'bg-gray-50'}`}>
|
|
<p className="text-sm font-medium mb-1">🤖 Cursor Messages</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{report.unifiedTimeline.dataSources.cursor.available ? (
|
|
<>
|
|
{report.unifiedTimeline.dataSources.cursor.totalRecords} messages<br/>
|
|
{formatDate(report.unifiedTimeline.dataSources.cursor.firstDate)} to {formatDate(report.unifiedTimeline.dataSources.cursor.lastDate)}
|
|
</>
|
|
) : 'No data'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Timeline Days */}
|
|
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
|
{report.unifiedTimeline.days.filter(day =>
|
|
day.summary.totalGitCommits > 0 ||
|
|
day.summary.totalExtensionSessions > 0 ||
|
|
day.summary.totalCursorMessages > 0
|
|
).reverse().map((day, index) => (
|
|
<div key={index} className="border-l-4 border-primary/30 pl-4 py-3 hover:bg-accent/50 rounded-r-lg transition-colors">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div>
|
|
<h4 className="font-semibold">{formatDate(day.date)}</h4>
|
|
<p className="text-xs text-muted-foreground">{day.dayOfWeek}</p>
|
|
</div>
|
|
<div className="flex gap-2 text-xs">
|
|
{day.summary.totalGitCommits > 0 && (
|
|
<span className="px-2 py-1 bg-green-100 text-green-800 rounded">
|
|
📊 {day.summary.totalGitCommits}
|
|
</span>
|
|
)}
|
|
{day.summary.totalExtensionSessions > 0 && (
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded">
|
|
💻 {day.summary.totalExtensionSessions}
|
|
</span>
|
|
)}
|
|
{day.summary.totalCursorMessages > 0 && (
|
|
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded">
|
|
🤖 {day.summary.totalCursorMessages}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm">
|
|
{/* Git Commits */}
|
|
{day.gitCommits.length > 0 && (
|
|
<div className="bg-green-50 rounded p-2">
|
|
<p className="text-xs font-medium text-green-900 mb-1">Git Commits:</p>
|
|
{day.gitCommits.map((commit: any, idx: number) => (
|
|
<div key={idx} className="text-xs text-green-800 ml-2">
|
|
• {commit.message}
|
|
<span className="text-green-600 ml-1">
|
|
(+{commit.insertions}/-{commit.deletions})
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Extension Sessions */}
|
|
{day.extensionSessions.length > 0 && (
|
|
<div className="bg-blue-50 rounded p-2">
|
|
<p className="text-xs font-medium text-blue-900 mb-1">
|
|
Extension Sessions: {day.summary.totalExtensionSessions}
|
|
({day.summary.uniqueFilesModified} files modified)
|
|
</p>
|
|
{day.extensionSessions.slice(0, 3).map((session: any, idx: number) => (
|
|
<div key={idx} className="text-xs text-blue-800 ml-2">
|
|
• {session.duration} min session
|
|
{session.conversationSummary && (
|
|
<span className="ml-1">- {session.conversationSummary.substring(0, 50)}...</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
{day.extensionSessions.length > 3 && (
|
|
<p className="text-xs text-blue-600 ml-2 mt-1">
|
|
+{day.extensionSessions.length - 3} more sessions
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Cursor Messages */}
|
|
{day.cursorMessages.length > 0 && (
|
|
<div className="bg-purple-50 rounded p-2">
|
|
<p className="text-xs font-medium text-purple-900 mb-1">
|
|
AI Conversations: {day.summary.totalCursorMessages} messages
|
|
</p>
|
|
<div className="text-xs text-purple-800 ml-2">
|
|
• Active in: {[...new Set(day.cursorMessages.map((m: any) => m.conversationName))].join(', ')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Day Summary */}
|
|
{(day.summary.linesAdded > 0 || day.summary.linesRemoved > 0) && (
|
|
<div className="mt-2 pt-2 border-t text-xs text-muted-foreground">
|
|
Total changes: <span className="text-green-600">+{day.summary.linesAdded}</span> /
|
|
<span className="text-red-600"> -{day.summary.linesRemoved}</span> lines
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Timeline Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Calendar className="mr-2 h-5 w-5" />
|
|
Development Timeline
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Work sessions and development velocity
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Development Period</p>
|
|
<p className="text-2xl font-bold">{formatDate(report.timeline.firstActivity)}</p>
|
|
<p className="text-sm text-muted-foreground">to {formatDate(report.timeline.lastActivity)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Peak Productivity Hours</p>
|
|
<p className="text-2xl font-bold">
|
|
{report.timeline.velocity.peakProductivityHours.map(h => `${h}:00`).join(', ')}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">Most active times</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Velocity Metrics</p>
|
|
<div className="grid gap-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Messages per day:</span>
|
|
<span className="font-mono">{report.timeline.velocity.messagesPerDay.toFixed(1)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Average session length:</span>
|
|
<span className="font-mono">{report.timeline.velocity.averageSessionLength} minutes</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Total sessions:</span>
|
|
<span className="font-mono">{report.timeline.totalSessions}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Recent Sessions</p>
|
|
<div className="space-y-2">
|
|
{report.timeline.sessions.slice(-5).reverse().map((session) => (
|
|
<div key={session.sessionId} className="border rounded-lg p-3 text-sm">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium">{formatDate(session.date)}</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
<Clock className="inline h-3 w-3 mr-1" />
|
|
{session.duration} min
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{session.messageCount} messages • {session.topics.slice(0, 2).join(', ')}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Extension Activity Section */}
|
|
{report.extensionActivity && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Code className="mr-2 h-5 w-5" />
|
|
File Edit Activity
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Files you've edited tracked by the Cursor Monitor extension
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Extension Sessions</p>
|
|
<p className="text-2xl font-bold">{report.extensionActivity.totalSessions}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Work sessions logged</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Files Edited</p>
|
|
<p className="text-2xl font-bold">{report.extensionActivity.uniqueFilesEdited}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Unique files modified</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Activity Period</p>
|
|
<p className="text-sm font-bold">
|
|
{report.extensionActivity.earliestActivity
|
|
? formatDate(report.extensionActivity.earliestActivity)
|
|
: 'N/A'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
to {report.extensionActivity.latestActivity
|
|
? formatDate(report.extensionActivity.latestActivity)
|
|
: 'N/A'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Most Edited Files (Top 20)</p>
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{report.extensionActivity.topFiles.map((item, index) => (
|
|
<div key={index} className="flex items-center justify-between border-b pb-2">
|
|
<span className="text-sm font-mono truncate flex-1" title={item.file}>
|
|
{item.file.split('/').pop()}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{item.editCount} {item.editCount === 1 ? 'edit' : 'edits'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Git Commit History Section */}
|
|
{report.gitHistory && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<FileText className="mr-2 h-5 w-5" />
|
|
Git Commit History
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Complete development history from Git repository
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Total Commits</p>
|
|
<p className="text-2xl font-bold">{report.gitHistory.totalCommits}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Code changes tracked</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Lines of Code</p>
|
|
<p className="text-2xl font-bold text-green-600">
|
|
+{formatNumber(report.gitHistory.totalInsertions)}
|
|
</p>
|
|
<p className="text-2xl font-bold text-red-600">
|
|
-{formatNumber(report.gitHistory.totalDeletions)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium mb-1">Repository Period</p>
|
|
<p className="text-sm font-bold">
|
|
{report.gitHistory.firstCommit
|
|
? formatDate(report.gitHistory.firstCommit)
|
|
: 'N/A'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
to {report.gitHistory.lastCommit
|
|
? formatDate(report.gitHistory.lastCommit)
|
|
: 'N/A'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Authors */}
|
|
{report.gitHistory.authors.length > 0 && (
|
|
<>
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Contributors</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{report.gitHistory.authors.map((author, index) => (
|
|
<span key={index} className="text-xs px-3 py-1 bg-secondary rounded-full">
|
|
{author.name} ({author.commitCount} {author.commitCount === 1 ? 'commit' : 'commits'})
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
</>
|
|
)}
|
|
|
|
{/* Top Files */}
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Most Changed Files (Top 20)</p>
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{report.gitHistory.topFiles.map((item, index) => (
|
|
<div key={index} className="flex items-center justify-between border-b pb-2">
|
|
<span className="text-sm font-mono truncate flex-1" title={item.filePath}>
|
|
{item.filePath.split('/').pop()}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{item.changeCount} {item.changeCount === 1 ? 'change' : 'changes'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Recent Commits */}
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Recent Commits (Last 20)</p>
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{report.gitHistory.commits.slice(0, 20).map((commit, index) => (
|
|
<div key={index} className="border-l-2 border-primary/20 pl-3 py-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{commit.message}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{commit.author} • {formatDate(commit.date)} •
|
|
<span className="font-mono ml-1">{commit.hash}</span>
|
|
</p>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
|
<span className="text-green-600">+{commit.insertions}</span> /
|
|
<span className="text-red-600">-{commit.deletions}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Cost Analysis Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<DollarSign className="mr-2 h-5 w-5" />
|
|
AI Cost Analysis
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Estimated costs based on {report.costs.model} usage
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Message Statistics</p>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Total messages:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.messageStats.totalMessages)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">User messages:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.messageStats.userMessages)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">AI messages:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.messageStats.aiMessages)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Avg length:</span>
|
|
<span className="font-mono">{report.costs.messageStats.avgMessageLength} chars</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Token Usage</p>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Input tokens:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.estimatedTokens.input)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Output tokens:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.estimatedTokens.output)}</span>
|
|
</div>
|
|
<Separator className="my-2" />
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Total tokens:</span>
|
|
<span className="font-mono">{formatNumber(report.costs.estimatedTokens.total)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-3">Cost Breakdown</p>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">
|
|
Input cost ({formatCurrency(report.costs.pricing.inputPer1M)}/1M tokens):
|
|
</span>
|
|
<span className="font-mono">{formatCurrency(report.costs.costs.inputCost)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">
|
|
Output cost ({formatCurrency(report.costs.pricing.outputPer1M)}/1M tokens):
|
|
</span>
|
|
<span className="font-mono">{formatCurrency(report.costs.costs.outputCost)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="bg-primary/5 rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">Total AI Cost</p>
|
|
<p className="text-xs text-muted-foreground mt-1">{report.costs.model}</p>
|
|
</div>
|
|
<div className="text-3xl font-bold">{formatCurrency(report.costs.costs.totalCost)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
<p>* Token estimation: ~4 characters per token</p>
|
|
<p className="mt-1">* Costs are estimates based on message content length</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Features Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Code className="mr-2 h-5 w-5" />
|
|
Features Implemented
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Current project capabilities and status
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{report.features.map((feature, index) => (
|
|
<div key={index} className="border rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-semibold">{feature.name}</h3>
|
|
<span className={`text-xs px-2 py-1 rounded-full ${
|
|
feature.status === 'complete' ? 'bg-green-100 text-green-800' :
|
|
feature.status === 'in-progress' ? 'bg-yellow-100 text-yellow-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{feature.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mb-3">{feature.description}</p>
|
|
<div className="grid gap-2 text-xs">
|
|
{feature.pages.length > 0 && (
|
|
<div>
|
|
<span className="font-medium">Pages:</span>{' '}
|
|
<span className="text-muted-foreground">{feature.pages.join(', ')}</span>
|
|
</div>
|
|
)}
|
|
{feature.apis.length > 0 && (
|
|
<div>
|
|
<span className="font-medium">APIs:</span>{' '}
|
|
<span className="text-muted-foreground font-mono">{feature.apis.join(', ')}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tech Stack Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<TrendingUp className="mr-2 h-5 w-5" />
|
|
Technology Stack
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Frameworks, libraries, and integrations
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">Frontend</p>
|
|
<div className="grid gap-2 text-sm">
|
|
{Object.entries(report.techStack.frontend).map(([key, value]) => (
|
|
<div key={key} className="flex justify-between">
|
|
<span className="text-muted-foreground capitalize">{key.replace(/([A-Z])/g, ' $1')}:</span>
|
|
<span className="font-mono">{value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">Backend</p>
|
|
<div className="grid gap-2 text-sm">
|
|
{Object.entries(report.techStack.backend).map(([key, value]) => (
|
|
<div key={key} className="flex justify-between">
|
|
<span className="text-muted-foreground capitalize">{key}:</span>
|
|
<span className="font-mono">{value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">Integrations</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{report.techStack.integrations.map((integration) => (
|
|
<span key={integration} className="text-xs px-2 py-1 bg-secondary rounded-md">
|
|
{integration}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="text-xs text-muted-foreground text-center">
|
|
Report generated at {new Date(report.generatedAt).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|