VIBN Frontend for Coolify deployment
This commit is contained in:
956
app/[workspace]/project/[projectId]/audit/page.tsx
Normal file
956
app/[workspace]/project/[projectId]/audit/page.tsx
Normal file
@@ -0,0 +1,956 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user