Major cleanup: remove all dead pages and components
Components deleted (~27 files): - components/ai/ (9 files — collector, extraction, vision, phase sidebar) - components/assistant-ui/ (thread.tsx, markdown-text.tsx) - components/mission/, sidebar/, extension/, icons/ - layout/app-shell, left-rail, right-panel, connect-sources-modal, mcp-connect-modal, page-header, page-template, project-sidebar, workspace-left-rail, PAGE_TEMPLATE_GUIDE - chatgpt-import-card, mcp-connection-card, mcp-playground Project pages deleted (~20 dirs): - analytics, api-map, architecture, associate-sessions, audit, audit-test, automation, code, context, design-old, docs, features, getting-started, journey, market, mission, money, plan, product, progress, sandbox, sessions, tech, timeline-plan Workspace routes deleted (~12 dirs): - connections, costs, debug-projects, debug-sessions, keys, mcp, new-project, projects/new, test-api-key, test-auth, test-sessions, users Remaining: 5 components, 2 layout files, 8 project tabs, 3 workspace routes Made-with: Cursor
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { BarChart3, DollarSign, TrendingUp, Zap } from "lucide-react";
|
||||
|
||||
export default async function AnalyticsPage({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Page Header */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Analytics</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Cost analysis, token usage, and performance metrics
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
{/* Key Metrics */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Cost</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$12.50</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<TrendingUp className="mr-1 inline h-3 w-3" />
|
||||
+8% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Tokens Used</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2.5M</div>
|
||||
<p className="text-xs text-muted-foreground">Across all sessions</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Avg Cost/Session</CardTitle>
|
||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$0.30</div>
|
||||
<p className="text-xs text-muted-foreground">Per coding session</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Cost/Feature</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$1.56</div>
|
||||
<p className="text-xs text-muted-foreground">Average per feature</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Detailed Analytics */}
|
||||
<Tabs defaultValue="costs" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="costs">Costs</TabsTrigger>
|
||||
<TabsTrigger value="tokens">Tokens</TabsTrigger>
|
||||
<TabsTrigger value="performance">Performance</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="costs" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cost Breakdown</CardTitle>
|
||||
<CardDescription>
|
||||
AI usage costs over time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-[300px] items-center justify-center border-2 border-dashed rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Cost chart visualization coming soon
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cost by Model</CardTitle>
|
||||
<CardDescription>
|
||||
Breakdown by AI model used
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ model: "Claude Sonnet 4", cost: "$8.20", percentage: 66 },
|
||||
{ model: "GPT-4", cost: "$3.10", percentage: 25 },
|
||||
{ model: "Gemini Pro", cost: "$1.20", percentage: 9 },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{item.model}</span>
|
||||
<span className="text-muted-foreground">{item.cost}</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tokens" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Token Usage</CardTitle>
|
||||
<CardDescription>
|
||||
Token consumption over time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-[300px] items-center justify-center border-2 border-dashed rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Token usage chart coming soon
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="performance" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Development Velocity</CardTitle>
|
||||
<CardDescription>
|
||||
Features completed over time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-[300px] items-center justify-center border-2 border-dashed rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Velocity metrics coming soon
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Map } from "lucide-react";
|
||||
|
||||
export default async function ApiMapPage({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Page Header */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">API Map</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Auto-generated API endpoint documentation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Endpoints</CardTitle>
|
||||
<CardDescription>
|
||||
Automatically detected from your codebase
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Example endpoints */}
|
||||
{[
|
||||
{ method: "GET", path: "/api/sessions", desc: "List all sessions" },
|
||||
{ method: "POST", path: "/api/sessions", desc: "Create new session" },
|
||||
{ method: "GET", path: "/api/features", desc: "List features" },
|
||||
].map((endpoint, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge
|
||||
variant={endpoint.method === "GET" ? "outline" : "default"}
|
||||
className="font-mono"
|
||||
>
|
||||
{endpoint.method}
|
||||
</Badge>
|
||||
<div>
|
||||
<code className="text-sm font-mono">{endpoint.path}</code>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{endpoint.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { FileCode } from "lucide-react";
|
||||
|
||||
export default async function ArchitecturePage({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Page Header */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Architecture</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Living architecture documentation and ADRs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="decisions">Decisions (ADRs)</TabsTrigger>
|
||||
<TabsTrigger value="tech-stack">Tech Stack</TabsTrigger>
|
||||
<TabsTrigger value="data-model">Data Model</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Architecture Overview</CardTitle>
|
||||
<CardDescription>
|
||||
High-level system architecture
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose max-w-none">
|
||||
<p className="text-muted-foreground">
|
||||
Architecture documentation will be automatically generated
|
||||
from your code and conversations.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="decisions" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Architectural Decision Records</CardTitle>
|
||||
<CardDescription>
|
||||
Key architectural choices and their context
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mb-4 rounded-full bg-muted p-3">
|
||||
<FileCode className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-center text-muted-foreground max-w-sm">
|
||||
ADRs will be automatically detected from your AI conversations
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tech-stack" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Technology Stack</CardTitle>
|
||||
<CardDescription>
|
||||
Approved technologies and frameworks
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Frontend</h4>
|
||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||
<li>• Next.js 15</li>
|
||||
<li>• React 19</li>
|
||||
<li>• Tailwind CSS</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Backend</h4>
|
||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||
<li>• Node.js</li>
|
||||
<li>• Express</li>
|
||||
<li>• PostgreSQL</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="data-model" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Model</CardTitle>
|
||||
<CardDescription>
|
||||
Database schema and relationships
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Database schema documentation coming soon
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { auth, db } from '@/lib/firebase/config';
|
||||
import { doc, getDoc } from 'firebase/firestore';
|
||||
import { toast } from 'sonner';
|
||||
import { Loader2, Link as LinkIcon, CheckCircle2 } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
productName: string;
|
||||
githubRepo?: string;
|
||||
workspacePath?: string;
|
||||
}
|
||||
|
||||
export default function AssociateSessionsPage() {
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [associating, setAssociating] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProject();
|
||||
}, [projectId]);
|
||||
|
||||
const loadProject = async () => {
|
||||
try {
|
||||
const projectDoc = await getDoc(doc(db, 'projects', projectId));
|
||||
if (projectDoc.exists()) {
|
||||
setProject({ id: projectDoc.id, ...projectDoc.data() } as Project);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading project:', error);
|
||||
toast.error('Failed to load project');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssociateSessions = async () => {
|
||||
if (!project?.githubRepo) {
|
||||
toast.error('Project does not have a GitHub repository connected');
|
||||
return;
|
||||
}
|
||||
|
||||
setAssociating(true);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error('Please sign in');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const response = await fetch(`/api/projects/${projectId}/associate-github-sessions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
githubRepo: project.githubRepo,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setResult(data);
|
||||
|
||||
if (data.sessionsAssociated > 0) {
|
||||
toast.success(`Success!`, {
|
||||
description: `Linked ${data.sessionsAssociated} existing chat sessions to this project`,
|
||||
});
|
||||
} else {
|
||||
toast.info('No unassociated sessions found for this repository');
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast.error(error.error || 'Failed to associate sessions');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
toast.error('An error occurred');
|
||||
} finally {
|
||||
setAssociating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl mx-auto p-8 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Associate Existing Sessions</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Find and link chat sessions from this GitHub repository
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Details</CardTitle>
|
||||
<CardDescription>Current project configuration</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Product Name</p>
|
||||
<p className="font-medium">{project?.productName}</p>
|
||||
</div>
|
||||
|
||||
{project?.githubRepo && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">GitHub Repository</p>
|
||||
<p className="font-medium font-mono text-sm">{project.githubRepo}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project?.workspacePath && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Workspace Path</p>
|
||||
<p className="font-medium font-mono text-sm">{project.workspacePath}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Find Matching Sessions</CardTitle>
|
||||
<CardDescription>
|
||||
Search your database for chat sessions that match this project's GitHub repository
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-muted/50 p-4 rounded-lg space-y-2 text-sm">
|
||||
<p><strong>How it works:</strong></p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Searches for sessions with matching GitHub repository</li>
|
||||
<li>Also checks sessions from matching workspace paths</li>
|
||||
<li>Only links sessions that aren't already assigned to a project</li>
|
||||
<li>Updates all matched sessions to link to this project</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleAssociateSessions}
|
||||
disabled={!project?.githubRepo || associating}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{associating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Searching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LinkIcon className="mr-2 h-4 w-4" />
|
||||
Find and Link Sessions
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{!project?.githubRepo && (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Connect a GitHub repository first to use this feature
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{result && (
|
||||
<Card className="border-green-500/50 bg-green-50/50 dark:bg-green-950/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
Results
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Sessions Linked</p>
|
||||
<p className="text-2xl font-bold">{result.sessionsAssociated}</p>
|
||||
</div>
|
||||
|
||||
{result.details && (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Exact GitHub Matches</p>
|
||||
<p className="text-2xl font-bold">{result.details.exactMatches}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Path Matches</p>
|
||||
<p className="text-2xl font-bold">{result.details.pathMatches}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{result.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
'use client';
|
||||
|
||||
export default function AuditTestPage() {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<h1 className="text-3xl font-bold">Audit Test Page</h1>
|
||||
<p className="mt-4">If you can see this, routing is working!</p>
|
||||
<button
|
||||
onClick={() => alert('Button works!')}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
|
||||
>
|
||||
Test Button
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,956 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Zap } from "lucide-react";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
|
||||
// Mock project data
|
||||
const MOCK_PROJECT = {
|
||||
id: "1",
|
||||
name: "AI Proxy",
|
||||
emoji: "🤖",
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ projectId: string }>;
|
||||
}
|
||||
|
||||
export default async function AutomationPage({ params }: PageProps) {
|
||||
const { projectId } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
projectId={projectId}
|
||||
projectName={MOCK_PROJECT.name}
|
||||
projectEmoji={MOCK_PROJECT.emoji}
|
||||
pageName="Automation"
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="container max-w-7xl py-6 space-y-6">
|
||||
{/* Hero Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Zap className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Automation</CardTitle>
|
||||
<CardDescription>
|
||||
Create workflows, set up triggers, and automate repetitive tasks
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="mb-3 rounded-full bg-muted p-4">
|
||||
<Zap className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-medium text-lg mb-2">Coming Soon</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Build custom workflows to automate testing, deployment, notifications,
|
||||
and other development tasks to accelerate your workflow.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { JSX } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Code2,
|
||||
FolderOpen,
|
||||
File,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Search,
|
||||
Loader2,
|
||||
Github,
|
||||
RefreshCw,
|
||||
FileCode
|
||||
} from "lucide-react";
|
||||
import { auth } from "@/lib/firebase/config";
|
||||
import { db } from "@/lib/firebase/config";
|
||||
import { doc, getDoc } from "firebase/firestore";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Project {
|
||||
githubRepo?: string;
|
||||
githubRepoUrl?: string;
|
||||
githubDefaultBranch?: string;
|
||||
}
|
||||
|
||||
interface FileNode {
|
||||
path: string;
|
||||
name: string;
|
||||
type: 'file' | 'folder';
|
||||
children?: FileNode[];
|
||||
size?: number;
|
||||
sha?: string;
|
||||
}
|
||||
|
||||
interface GitHubFile {
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function CodePage() {
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingFiles, setLoadingFiles] = useState(false);
|
||||
const [fileTree, setFileTree] = useState<FileNode[]>([]);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['/']));
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string | null>(null);
|
||||
const [loadingContent, setLoadingContent] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchProject();
|
||||
}, [projectId]);
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const projectRef = doc(db, "projects", projectId);
|
||||
const projectSnap = await getDoc(projectRef);
|
||||
|
||||
if (projectSnap.exists()) {
|
||||
const projectData = projectSnap.data() as Project;
|
||||
setProject(projectData);
|
||||
|
||||
// Auto-load files if GitHub is connected
|
||||
if (projectData.githubRepo) {
|
||||
await fetchFileTree(projectData.githubRepo, projectData.githubDefaultBranch);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching project:", error);
|
||||
toast.error("Failed to load project");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileTree = async (repoFullName: string, branch = 'main') => {
|
||||
setLoadingFiles(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error("Please sign in");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const [owner, repo] = repoFullName.split('/');
|
||||
|
||||
const response = await fetch(
|
||||
`/api/github/repo-tree?owner=${owner}&repo=${repo}&branch=${branch}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch repository files");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const tree = buildFileTree(data.files);
|
||||
setFileTree(tree);
|
||||
|
||||
toast.success(`Loaded ${data.totalFiles} files from ${repoFullName}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching file tree:", error);
|
||||
toast.error("Failed to load repository files");
|
||||
} finally {
|
||||
setLoadingFiles(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buildFileTree = (files: GitHubFile[]): FileNode[] => {
|
||||
const root: FileNode = {
|
||||
path: '/',
|
||||
name: '/',
|
||||
type: 'folder',
|
||||
children: [],
|
||||
};
|
||||
|
||||
files.forEach((file) => {
|
||||
const parts = file.path.split('/');
|
||||
let currentNode = root;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
const isFile = index === parts.length - 1;
|
||||
const fullPath = parts.slice(0, index + 1).join('/');
|
||||
|
||||
if (!currentNode.children) {
|
||||
currentNode.children = [];
|
||||
}
|
||||
|
||||
let childNode = currentNode.children.find(child => child.name === part);
|
||||
|
||||
if (!childNode) {
|
||||
childNode = {
|
||||
path: fullPath,
|
||||
name: part,
|
||||
type: isFile ? 'file' : 'folder',
|
||||
...(isFile && { size: file.size, sha: file.sha }),
|
||||
...(!isFile && { children: [] }),
|
||||
};
|
||||
currentNode.children.push(childNode);
|
||||
}
|
||||
|
||||
if (!isFile) {
|
||||
currentNode = childNode;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort children recursively
|
||||
const sortNodes = (nodes: FileNode[]) => {
|
||||
nodes.sort((a, b) => {
|
||||
if (a.type === b.type) return a.name.localeCompare(b.name);
|
||||
return a.type === 'folder' ? -1 : 1;
|
||||
});
|
||||
nodes.forEach(node => {
|
||||
if (node.children) {
|
||||
sortNodes(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (root.children) {
|
||||
sortNodes(root.children);
|
||||
}
|
||||
|
||||
return root.children || [];
|
||||
};
|
||||
|
||||
const fetchFileContent = async (filePath: string) => {
|
||||
if (!project?.githubRepo) return;
|
||||
|
||||
setLoadingContent(true);
|
||||
setSelectedFile(filePath);
|
||||
setFileContent(null);
|
||||
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error("Please sign in");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const [owner, repo] = project.githubRepo.split('/');
|
||||
const branch = project.githubDefaultBranch || 'main';
|
||||
|
||||
console.log('[Code Page] Fetching file:', filePath);
|
||||
|
||||
const response = await fetch(
|
||||
`/api/github/file-content?owner=${owner}&repo=${repo}&path=${encodeURIComponent(filePath)}&branch=${branch}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error('[Code Page] Failed to fetch file:', errorData);
|
||||
throw new Error(errorData.error || "Failed to fetch file content");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[Code Page] File loaded:', data.name, `(${data.size} bytes)`);
|
||||
setFileContent(data.content);
|
||||
} catch (error) {
|
||||
console.error("Error fetching file content:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to load file content");
|
||||
setFileContent(`// Error loading file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFolder = (path: string) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
if (newExpanded.has(path)) {
|
||||
newExpanded.delete(path);
|
||||
} else {
|
||||
newExpanded.add(path);
|
||||
}
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
const renderFileTree = (nodes: FileNode[], level = 0): JSX.Element[] => {
|
||||
return nodes
|
||||
.filter(node => {
|
||||
if (!searchQuery) return true;
|
||||
return node.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
})
|
||||
.map((node) => (
|
||||
<div key={node.path}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (node.type === 'folder') {
|
||||
toggleFolder(node.path);
|
||||
} else {
|
||||
fetchFileContent(node.path);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors",
|
||||
selectedFile === node.path && "bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
||||
>
|
||||
{node.type === 'folder' ? (
|
||||
<>
|
||||
{expandedFolders.has(node.path) ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
<FolderOpen className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-4" />
|
||||
<FileCode className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
<span className="truncate">{node.name}</span>
|
||||
{node.size && (
|
||||
<span className="ml-auto text-xs text-muted-foreground shrink-0">
|
||||
{formatFileSize(node.size)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{node.type === 'folder' && expandedFolders.has(node.path) && node.children && (
|
||||
renderFileTree(node.children, level + 1)
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!project?.githubRepo) {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-14 items-center gap-2 px-6">
|
||||
<Code2 className="h-5 w-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Code</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<Card className="max-w-2xl mx-auto p-8 text-center">
|
||||
<div className="mb-4 rounded-full bg-muted p-4 w-fit mx-auto">
|
||||
<Github className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">No Repository Connected</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Connect a GitHub repository in the Context section to view your code here
|
||||
</p>
|
||||
<Button onClick={() => window.location.href = `/${params.workspace}/project/${projectId}/context`}>
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Connect Repository
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-14 items-center gap-2 px-6">
|
||||
<Code2 className="h-5 w-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Code</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<a
|
||||
href={project.githubRepoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
{project.githubRepo}
|
||||
</a>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => fetchFileTree(project.githubRepo!, project.githubDefaultBranch)}
|
||||
disabled={loadingFiles}
|
||||
>
|
||||
{loadingFiles ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* File Tree Sidebar */}
|
||||
<div className="w-80 border-r flex flex-col bg-background">
|
||||
<div className="p-3 border-b">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{loadingFiles ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : fileTree.length === 0 ? (
|
||||
<div className="text-center py-8 text-sm text-muted-foreground">
|
||||
No files found
|
||||
</div>
|
||||
) : (
|
||||
renderFileTree(fileTree)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Viewer */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-muted/30">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="px-4 py-2 border-b bg-background flex items-center gap-2">
|
||||
<FileCode className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-mono">{selectedFile}</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto bg-background">
|
||||
{loadingContent ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : fileContent ? (
|
||||
<div className="flex">
|
||||
{/* Line Numbers */}
|
||||
<div className="select-none border-r bg-muted/30 px-4 py-4 text-right text-sm font-mono text-muted-foreground">
|
||||
{fileContent.split('\n').map((_, i) => (
|
||||
<div key={i} className="leading-relaxed">
|
||||
{i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Code Content */}
|
||||
<pre className="flex-1 p-4 text-sm font-mono leading-relaxed overflow-x-auto">
|
||||
<code>{fileContent}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<p className="text-sm">Failed to load file content</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Code2 className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">Select a file to view its contents</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,590 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { FolderOpen, Plus, Github, Zap, FileText, Trash2, CheckCircle2, Upload } from "lucide-react";
|
||||
import { CursorIcon } from "@/components/icons/custom-icons";
|
||||
import { db } from "@/lib/firebase/config";
|
||||
import { collection, doc, getDoc, addDoc, deleteDoc, query, where, getDocs, updateDoc } from "firebase/firestore";
|
||||
import { toast } from "sonner";
|
||||
import { auth } from "@/lib/firebase/config";
|
||||
import { GitHubRepoPicker } from "@/components/ai/github-repo-picker";
|
||||
|
||||
interface ContextSource {
|
||||
id: string;
|
||||
type: "github" | "extension" | "chat" | "file" | "document";
|
||||
name: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
summary?: string;
|
||||
connectedAt: Date;
|
||||
metadata?: any;
|
||||
chunkCount?: number;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
githubRepo?: string;
|
||||
githubRepoUrl?: string;
|
||||
}
|
||||
|
||||
export default function ContextPage() {
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
const [sources, setSources] = useState<ContextSource[]>([]);
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [chatTitle, setChatTitle] = useState("");
|
||||
const [chatContent, setChatContent] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploadMode, setUploadMode] = useState<"text" | "file">("text");
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isGithubDialogOpen, setIsGithubDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
try {
|
||||
// Fetch project details
|
||||
const projectRef = doc(db, "projects", projectId);
|
||||
const projectSnap = await getDoc(projectRef);
|
||||
|
||||
if (projectSnap.exists()) {
|
||||
setProject(projectSnap.data() as Project);
|
||||
}
|
||||
|
||||
// Fetch context sources
|
||||
const contextRef = collection(db, "projects", projectId, "contextSources");
|
||||
const contextSnap = await getDocs(contextRef);
|
||||
|
||||
const fetchedSources: ContextSource[] = contextSnap.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
connectedAt: doc.data().connectedAt?.toDate() || new Date()
|
||||
} as ContextSource));
|
||||
|
||||
setSources(fetchedSources);
|
||||
} catch (error) {
|
||||
console.error("Error fetching context data:", error);
|
||||
toast.error("Failed to load context sources");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [projectId]);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFiles(Array.from(e.target.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddChatContent = async () => {
|
||||
if (!chatTitle.trim() || !chatContent.trim()) {
|
||||
toast.error("Please provide both a title and content");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Generate AI summary
|
||||
toast.info("Generating summary...");
|
||||
const summaryResponse = await fetch("/api/context/summarize", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content: chatContent, title: chatTitle })
|
||||
});
|
||||
|
||||
let summary = "";
|
||||
if (summaryResponse.ok) {
|
||||
const data = await summaryResponse.json();
|
||||
summary = data.summary;
|
||||
} else {
|
||||
console.error("Failed to generate summary");
|
||||
summary = `${chatContent.substring(0, 100)}...`;
|
||||
}
|
||||
|
||||
// Also create a knowledge_item so it's included in extraction and checklist
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error("Please sign in");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const importResponse = await fetch(`/api/projects/${projectId}/knowledge/import-ai-chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: chatTitle,
|
||||
transcript: chatContent, // API expects 'transcript' not 'content'
|
||||
provider: 'other',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!importResponse.ok) {
|
||||
throw new Error("Failed to save content as knowledge item");
|
||||
}
|
||||
|
||||
const contextRef = collection(db, "projects", projectId, "contextSources");
|
||||
const newSource = {
|
||||
type: "chat",
|
||||
name: chatTitle,
|
||||
content: chatContent,
|
||||
summary: summary,
|
||||
connectedAt: new Date(),
|
||||
metadata: {
|
||||
length: chatContent.length,
|
||||
addedManually: true
|
||||
}
|
||||
};
|
||||
|
||||
const docRef = await addDoc(contextRef, newSource);
|
||||
|
||||
setSources([...sources, {
|
||||
id: docRef.id,
|
||||
...newSource,
|
||||
connectedAt: new Date()
|
||||
} as ContextSource]);
|
||||
|
||||
toast.success("Chat content added successfully");
|
||||
setIsAddModalOpen(false);
|
||||
setChatTitle("");
|
||||
setChatContent("");
|
||||
} catch (error) {
|
||||
console.error("Error adding chat content:", error);
|
||||
toast.error("Failed to add chat content");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadDocuments = async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
toast.error("Please select at least one file");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error("Please sign in to upload documents");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
toast.info(`Uploading ${file.name}...`);
|
||||
|
||||
// Create FormData to send file as multipart/form-data
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('projectId', projectId);
|
||||
|
||||
// Upload to endpoint that handles file storage + chunking
|
||||
const response = await fetch(`/api/projects/${projectId}/knowledge/upload-document`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload ${file.name}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
toast.success(`${file.name} uploaded: ${result.chunkCount} chunks created`);
|
||||
}
|
||||
|
||||
// Reload sources
|
||||
const contextRef = collection(db, "projects", projectId, "contextSources");
|
||||
const contextSnap = await getDocs(contextRef);
|
||||
|
||||
const fetchedSources: ContextSource[] = contextSnap.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
connectedAt: doc.data().connectedAt?.toDate() || new Date()
|
||||
} as ContextSource));
|
||||
|
||||
setSources(fetchedSources);
|
||||
|
||||
setIsAddModalOpen(false);
|
||||
setSelectedFiles([]);
|
||||
toast.success("All documents uploaded successfully");
|
||||
} catch (error) {
|
||||
console.error("Error uploading documents:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to upload documents");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSource = async (sourceId: string) => {
|
||||
try {
|
||||
const sourceRef = doc(db, "projects", projectId, "contextSources", sourceId);
|
||||
await deleteDoc(sourceRef);
|
||||
|
||||
setSources(sources.filter(s => s.id !== sourceId));
|
||||
toast.success("Context source removed");
|
||||
} catch (error) {
|
||||
console.error("Error deleting source:", error);
|
||||
toast.error("Failed to remove source");
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "github":
|
||||
return <Github className="h-5 w-5" />;
|
||||
case "extension":
|
||||
return <CursorIcon className="h-5 w-5" />;
|
||||
case "chat":
|
||||
return <FileText className="h-5 w-5" />;
|
||||
case "file":
|
||||
return <FileText className="h-5 w-5" />;
|
||||
case "document":
|
||||
return <FileText className="h-5 w-5" />;
|
||||
default:
|
||||
return <FolderOpen className="h-5 w-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceLabel = (source: ContextSource) => {
|
||||
switch (source.type) {
|
||||
case "github":
|
||||
return `Connected GitHub: ${source.name}`;
|
||||
case "extension":
|
||||
return "Installed Vibn Extension";
|
||||
case "chat":
|
||||
return source.name;
|
||||
case "file":
|
||||
return source.name;
|
||||
default:
|
||||
return source.name;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-sm text-muted-foreground">Loading context sources...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Build sources list with auto-detected connections
|
||||
// Note: GitHub is now shown in its own section via GitHubRepoPicker component
|
||||
const allSources: ContextSource[] = [...sources];
|
||||
|
||||
// Check if extension is installed (placeholder for now)
|
||||
const extensionInstalled = true; // TODO: Detect extension
|
||||
if (extensionInstalled && !sources.find(s => s.type === "extension")) {
|
||||
allSources.unshift({
|
||||
id: "extension-auto",
|
||||
type: "extension",
|
||||
name: "Cursor Extension",
|
||||
connectedAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-14 items-center gap-2 px-6">
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Context Sources</h1>
|
||||
<div className="ml-auto">
|
||||
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Context
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Context</DialogTitle>
|
||||
<DialogDescription>
|
||||
Upload documents or paste text to give the AI more context about your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Mode Selector */}
|
||||
<div className="flex gap-2 p-1 bg-muted rounded-lg">
|
||||
<Button
|
||||
variant={uploadMode === "file" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setUploadMode("file")}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Upload Files
|
||||
</Button>
|
||||
<Button
|
||||
variant={uploadMode === "text" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setUploadMode("text")}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Paste Text
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{uploadMode === "file" ? (
|
||||
/* File Upload Mode */
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="file-upload">Select Documents</Label>
|
||||
<Input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".txt,.md,.pdf,.doc,.docx,.json,.csv,.xml"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="text-sm text-muted-foreground mt-2">
|
||||
Selected: {selectedFiles.map(f => f.name).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Documents will be stored for the Extractor AI to review and process.
|
||||
Supported formats: TXT, MD, PDF, DOC, JSON, CSV, XML
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Text Paste Mode */
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., Planning discussion with Sarah"
|
||||
value={chatTitle}
|
||||
onChange={(e) => setChatTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content">Content</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
placeholder="Paste your chat conversation or notes here..."
|
||||
value={chatContent}
|
||||
onChange={(e) => setChatContent(e.target.value)}
|
||||
className="min-h-[300px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsAddModalOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{uploadMode === "file" ? (
|
||||
<Button onClick={handleUploadDocuments} disabled={isProcessing || selectedFiles.length === 0}>
|
||||
{isProcessing ? "Processing..." : `Upload ${selectedFiles.length} File${selectedFiles.length !== 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleAddChatContent} disabled={saving}>
|
||||
{saving ? "Saving..." : "Add Context"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-4xl space-y-4">
|
||||
{/* GitHub Repository Connection */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
GitHub Repository
|
||||
</h2>
|
||||
{project?.githubRepo ? (
|
||||
// Show connected repo
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted shrink-0">
|
||||
<Github className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-sm">Connected: {project.githubRepo}</h3>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Repository connected and ready for AI access
|
||||
</p>
|
||||
{project.githubRepoUrl && (
|
||||
<a
|
||||
href={project.githubRepoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-600 hover:underline inline-block"
|
||||
>
|
||||
View on GitHub →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsGithubDialogOpen(true)}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
// Show connect button
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted shrink-0">
|
||||
<Github className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm mb-1">Connect GitHub Repository</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Give the AI access to your codebase for better context
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsGithubDialogOpen(true)}
|
||||
size="sm"
|
||||
>
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* GitHub Connection Dialog */}
|
||||
<Dialog open={isGithubDialogOpen} onOpenChange={setIsGithubDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect GitHub Repository</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect a GitHub repository to give the AI access to your codebase
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-auto">
|
||||
<GitHubRepoPicker
|
||||
projectId={projectId}
|
||||
onRepoSelected={(repo) => {
|
||||
toast.success(`Repository ${repo.full_name} connected!`);
|
||||
setIsGithubDialogOpen(false);
|
||||
// Reload project data to show the connected repo
|
||||
const fetchProject = async () => {
|
||||
const projectRef = doc(db, "projects", projectId);
|
||||
const projectSnap = await getDoc(projectRef);
|
||||
if (projectSnap.exists()) {
|
||||
setProject(projectSnap.data() as Project);
|
||||
}
|
||||
};
|
||||
fetchProject();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Other Context Sources */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||
Additional Context
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{allSources.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<FolderOpen className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Context Sources Yet</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Add context sources to help the AI understand your project better
|
||||
</p>
|
||||
<Button onClick={() => setIsAddModalOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Your First Context
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{allSources.map((source) => (
|
||||
<Card key={source.id} className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
|
||||
{getSourceIcon(source.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-sm">{getSourceLabel(source)}</h3>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connected {source.connectedAt.toLocaleDateString()}
|
||||
</p>
|
||||
{source.summary && (
|
||||
<p className="text-sm text-foreground/80 mt-2 leading-relaxed">
|
||||
{source.summary}
|
||||
</p>
|
||||
)}
|
||||
{source.url && (
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-600 hover:underline mt-1 inline-block"
|
||||
>
|
||||
{source.type === 'github' ? 'View on GitHub →' :
|
||||
source.type === 'document' ? 'Download File →' :
|
||||
'View Source →'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{!source.id.includes("auto") && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteSource(source.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Eye, MessageSquare, Copy, Share2, Sparkles, History, Loader2, Send, MousePointer2 } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Mock data for page variations
|
||||
const mockPageData: Record<string, any> = {
|
||||
"landing-hero": {
|
||||
name: "Landing Page Hero",
|
||||
emoji: "✨",
|
||||
style: "modern",
|
||||
prompt: "Create a modern landing page hero section with gradient background",
|
||||
v0Url: "https://v0.dev/chat/abc123",
|
||||
variations: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Version 1 - Blue Gradient",
|
||||
thumbnail: "https://placehold.co/800x600/1e40af/ffffff?text=Hero+V1",
|
||||
createdAt: "2025-11-11",
|
||||
views: 45,
|
||||
comments: 3,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Version 2 - Purple Gradient",
|
||||
thumbnail: "https://placehold.co/800x600/7c3aed/ffffff?text=Hero+V2",
|
||||
createdAt: "2025-11-10",
|
||||
views: 32,
|
||||
comments: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Version 3 - Minimal",
|
||||
thumbnail: "https://placehold.co/800x600/6b7280/ffffff?text=Hero+V3",
|
||||
createdAt: "2025-11-09",
|
||||
views: 28,
|
||||
comments: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
"dashboard": {
|
||||
name: "Dashboard Layout",
|
||||
emoji: "📊",
|
||||
style: "minimal",
|
||||
prompt: "Design a clean dashboard with sidebar, metrics cards, and charts",
|
||||
v0Url: "https://v0.dev/chat/def456",
|
||||
variations: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Version 1 - Default",
|
||||
thumbnail: "https://placehold.co/800x600/7c3aed/ffffff?text=Dashboard+V1",
|
||||
createdAt: "2025-11-10",
|
||||
views: 78,
|
||||
comments: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
"pricing": {
|
||||
name: "Pricing Cards",
|
||||
emoji: "💳",
|
||||
style: "colorful",
|
||||
prompt: "Three-tier pricing cards with features, hover effects, and CTA buttons",
|
||||
v0Url: "https://v0.dev/chat/ghi789",
|
||||
variations: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Version 1 - Standard",
|
||||
thumbnail: "https://placehold.co/800x600/059669/ffffff?text=Pricing+V1",
|
||||
createdAt: "2025-11-09",
|
||||
views: 102,
|
||||
comments: 12,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Version 2 - Compact",
|
||||
thumbnail: "https://placehold.co/800x600/0891b2/ffffff?text=Pricing+V2",
|
||||
createdAt: "2025-11-08",
|
||||
views: 67,
|
||||
comments: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
"user-profile": {
|
||||
name: "User Profile",
|
||||
emoji: "👤",
|
||||
style: "modern",
|
||||
prompt: "User profile page with avatar, bio, stats, and activity feed",
|
||||
v0Url: "https://v0.dev/chat/jkl012",
|
||||
variations: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Version 1 - Default",
|
||||
thumbnail: "https://placehold.co/800x600/dc2626/ffffff?text=Profile+V1",
|
||||
createdAt: "2025-11-08",
|
||||
views: 56,
|
||||
comments: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function DesignPageView({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ projectId: string; pageSlug: string }>;
|
||||
}) {
|
||||
const { projectId, pageSlug } = use(params);
|
||||
const pageData = mockPageData[pageSlug] || mockPageData["landing-hero"];
|
||||
|
||||
const [editPrompt, setEditPrompt] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState(pageData.variations[0]);
|
||||
const [versionsModalOpen, setVersionsModalOpen] = useState(false);
|
||||
const [commentsModalOpen, setCommentsModalOpen] = useState(false);
|
||||
const [chatMessage, setChatMessage] = useState("");
|
||||
const [pageName, setPageName] = useState(pageData.name);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [designModeActive, setDesignModeActive] = useState(false);
|
||||
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
||||
|
||||
const handleIterate = async () => {
|
||||
if (!editPrompt.trim()) {
|
||||
toast.error("Please enter a prompt to iterate");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
// Call v0 API to generate update
|
||||
const response = await fetch('/api/v0/iterate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chatId: pageData.v0Url.split('/').pop(),
|
||||
message: editPrompt,
|
||||
projectId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to iterate');
|
||||
}
|
||||
|
||||
toast.success("Design updated!", {
|
||||
description: "Your changes have been generated",
|
||||
});
|
||||
|
||||
// Refresh or update the current version
|
||||
setEditPrompt("");
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error iterating:', error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to iterate design");
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePushToCursor = () => {
|
||||
toast.success("Code will be pushed to Cursor", {
|
||||
description: "This feature will send the component code to your IDE",
|
||||
});
|
||||
// TODO: Implement actual push to Cursor IDE
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* Toolbar */}
|
||||
<div className="border-b bg-card/50 px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{isEditingName ? (
|
||||
<input
|
||||
type="text"
|
||||
value={pageName}
|
||||
onChange={(e) => setPageName(e.target.value)}
|
||||
onBlur={() => {
|
||||
setIsEditingName(false);
|
||||
toast.success("Page name updated");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setIsEditingName(false);
|
||||
toast.success("Page name updated");
|
||||
}
|
||||
}}
|
||||
className="text-lg font-semibold bg-transparent border-b border-primary outline-none px-1 min-w-[200px]"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h1
|
||||
className="text-lg font-semibold cursor-pointer hover:text-primary transition-colors"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
>
|
||||
{pageName}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setVersionsModalOpen(true)}
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
Versions
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCommentsModalOpen(true)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Comments
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePushToCursor}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
Push to Cursor
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Share2 className="h-4 w-4 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Preview */}
|
||||
<div className="flex-1 overflow-auto bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 relative">
|
||||
<div className="w-full h-full p-8">
|
||||
{/* Sample SaaS Dashboard Component */}
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
{/* Page Header */}
|
||||
<div
|
||||
data-element="page-header"
|
||||
className={cn(
|
||||
"flex items-center justify-between transition-all p-2 rounded-lg",
|
||||
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
|
||||
selectedElement === "page-header" && "ring-2 ring-primary ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("page-header");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
data-element="page-title"
|
||||
className={cn(
|
||||
"text-3xl font-bold transition-all rounded px-1",
|
||||
designModeActive && "hover:ring-2 hover:ring-primary/50 hover:ring-inset",
|
||||
selectedElement === "page-title" && "ring-2 ring-primary/50 ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("page-title");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Dashboard Overview
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">Welcome back! Here's what's happening today.</p>
|
||||
</div>
|
||||
<Button
|
||||
data-element="primary-action-button"
|
||||
className={cn(
|
||||
"transition-all",
|
||||
designModeActive && "hover:ring-2 hover:ring-yellow-400 hover:ring-inset",
|
||||
selectedElement === "primary-action-button" && "ring-2 ring-yellow-400 ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("primary-action-button");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create New Project
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div
|
||||
data-element="stats-grid"
|
||||
className={cn(
|
||||
"grid md:grid-cols-4 gap-4 transition-all rounded-xl",
|
||||
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
|
||||
selectedElement === "stats-grid" && "ring-2 ring-primary ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("stats-grid");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: "Total Users", value: "2,847", change: "+12.3%", trend: "up" },
|
||||
{ label: "Revenue", value: "$45,231", change: "+8.1%", trend: "up" },
|
||||
{ label: "Active Projects", value: "127", change: "-2.4%", trend: "down" },
|
||||
{ label: "Conversion Rate", value: "3.24%", change: "+0.8%", trend: "up" },
|
||||
].map((stat, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
data-element={`stat-card-${i}`}
|
||||
className={cn(
|
||||
"transition-all",
|
||||
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
|
||||
selectedElement === `stat-card-${i}` && "ring-2 ring-primary ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement(`stat-card-${i}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-xs">{stat.label}</CardDescription>
|
||||
<CardTitle className="text-2xl">{stat.value}</CardTitle>
|
||||
<span className={cn(
|
||||
"text-xs font-medium",
|
||||
stat.trend === "up" ? "text-green-600" : "text-red-600"
|
||||
)}>
|
||||
{stat.change}
|
||||
</span>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<Card
|
||||
data-element="data-table"
|
||||
className={cn(
|
||||
"transition-all",
|
||||
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
|
||||
selectedElement === "data-table" && "ring-2 ring-primary ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("data-table");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Recent Projects</CardTitle>
|
||||
<CardDescription>Your team's latest work</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-element="table-action-button"
|
||||
className={cn(
|
||||
"transition-all",
|
||||
designModeActive && "hover:ring-2 hover:ring-yellow-400 hover:ring-inset",
|
||||
selectedElement === "table-action-button" && "ring-2 ring-yellow-400 ring-inset"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement("table-action-button");
|
||||
}
|
||||
}}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: "Mobile App Redesign", status: "In Progress", team: "Design Team", updated: "2 hours ago" },
|
||||
{ name: "API Documentation", status: "Review", team: "Engineering", updated: "5 hours ago" },
|
||||
{ name: "Marketing Website", status: "Completed", team: "Marketing", updated: "1 day ago" },
|
||||
{ name: "User Dashboard v2", status: "Planning", team: "Product", updated: "3 days ago" },
|
||||
].map((project, i) => (
|
||||
<div
|
||||
key={i}
|
||||
data-element={`table-row-${i}`}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-3 rounded-lg border transition-all",
|
||||
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset hover:bg-accent",
|
||||
selectedElement === `table-row-${i}` && "ring-2 ring-primary ring-inset bg-accent"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (designModeActive) {
|
||||
e.stopPropagation();
|
||||
setSelectedElement(`table-row-${i}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{project.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{project.team}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={cn(
|
||||
"text-xs font-medium px-2 py-1 rounded-full",
|
||||
project.status === "Completed" && "bg-green-100 text-green-700",
|
||||
project.status === "In Progress" && "bg-blue-100 text-blue-700",
|
||||
project.status === "Review" && "bg-yellow-100 text-yellow-700",
|
||||
project.status === "Planning" && "bg-gray-100 text-gray-700"
|
||||
)}>
|
||||
{project.status}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground w-24 text-right">{project.updated}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Chat Interface - v0 Style */}
|
||||
<div
|
||||
className="absolute bottom-6 left-1/2 -translate-x-1/2 w-full max-w-3xl px-6"
|
||||
>
|
||||
<div className="bg-background/95 backdrop-blur-lg border border-border rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Input Area */}
|
||||
<div className="p-3 relative">
|
||||
<Textarea
|
||||
placeholder="e.g., 'Make the hero section more vibrant', 'Add a call-to-action button', 'Change the color scheme to dark mode'"
|
||||
value={chatMessage}
|
||||
onChange={(e) => setChatMessage(e.target.value)}
|
||||
className="min-h-[60px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-sm px-1"
|
||||
disabled={isGenerating}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Bar */}
|
||||
<div className="px-4 pb-3 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={designModeActive ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setDesignModeActive(!designModeActive);
|
||||
setSelectedElement(null);
|
||||
}}
|
||||
>
|
||||
<MousePointer2 className="h-4 w-4 mr-2" />
|
||||
Design Mode
|
||||
</Button>
|
||||
{selectedElement && (
|
||||
<div className="flex items-center gap-2 px-2 py-1 bg-primary/10 text-primary rounded text-xs">
|
||||
<MousePointer2 className="h-3 w-3" />
|
||||
<span className="font-medium">{selectedElement.replace(/-/g, ' ')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={isGenerating}
|
||||
onClick={() => {
|
||||
toast.info("Creating variation...");
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Variation
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const contextualPrompt = selectedElement
|
||||
? `[Targeting: ${selectedElement.replace(/-/g, ' ')}] ${chatMessage}`
|
||||
: chatMessage;
|
||||
setEditPrompt(contextualPrompt);
|
||||
handleIterate();
|
||||
}}
|
||||
disabled={isGenerating || !chatMessage.trim()}
|
||||
className="gap-2"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Generating
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{selectedElement ? 'Modify Selected' : 'Generate'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Versions Modal */}
|
||||
<Dialog open={versionsModalOpen} onOpenChange={setVersionsModalOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Version History</DialogTitle>
|
||||
<DialogDescription>
|
||||
View and switch between different versions of this design
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[60vh] pr-4">
|
||||
<div className="space-y-3">
|
||||
{pageData.variations.map((variation: any) => (
|
||||
<button
|
||||
key={variation.id}
|
||||
onClick={() => {
|
||||
setCurrentVersion(variation);
|
||||
setVersionsModalOpen(false);
|
||||
toast.success(`Switched to ${variation.name}`);
|
||||
}}
|
||||
className={`w-full text-left rounded-lg border p-4 transition-colors hover:bg-accent ${
|
||||
currentVersion.id === variation.id ? 'border-primary bg-accent' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<img
|
||||
src={variation.thumbnail}
|
||||
alt={variation.name}
|
||||
className="w-32 h-20 rounded object-cover"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-base">{variation.name}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{variation.createdAt}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Eye className="h-4 w-4" />
|
||||
{variation.views} views
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
{variation.comments} comments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Comments Modal */}
|
||||
<Dialog open={commentsModalOpen} onOpenChange={setCommentsModalOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Comments & Feedback</DialogTitle>
|
||||
<DialogDescription>
|
||||
Discuss this design with your team
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[50vh] pr-4">
|
||||
<div className="space-y-4">
|
||||
{/* Mock comments */}
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border p-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium">
|
||||
JD
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-medium">Jane Doe</span>
|
||||
<span className="text-xs text-muted-foreground ml-2">2h ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Love the gradient! Could we try a darker variant?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border p-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-green-500/10 flex items-center justify-center text-sm font-medium">
|
||||
MS
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-medium">Mike Smith</span>
|
||||
<span className="text-xs text-muted-foreground ml-2">5h ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The layout looks perfect. Spacing is on point 👍
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Add comment */}
|
||||
<div className="pt-4 border-t space-y-3">
|
||||
<Textarea
|
||||
placeholder="Add a comment..."
|
||||
className="min-h-[100px] resize-none"
|
||||
/>
|
||||
<Button className="w-full">
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Post Comment
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,558 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Sparkles, ChevronRight, ChevronDown, Folder, FileText, Palette, LayoutGrid, Workflow, Github, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { toast } from "sonner";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard as TemplateCard,
|
||||
} from "@/components/layout/page-template";
|
||||
|
||||
// Mock tree structure - Core Product screens
|
||||
const coreProductTree = [
|
||||
{
|
||||
id: "dashboard",
|
||||
name: "Dashboard",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "overview", name: "Overview", type: "page", route: "/dashboard", variations: 2 },
|
||||
{ id: "analytics", name: "Analytics", type: "page", route: "/dashboard/analytics", variations: 1 },
|
||||
{ id: "projects", name: "Projects", type: "page", route: "/dashboard/projects", variations: 2 },
|
||||
{ id: "activity", name: "Activity", type: "page", route: "/dashboard/activity", variations: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
name: "Profile & Settings",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "user-profile", name: "User Profile", type: "page", route: "/profile", variations: 2 },
|
||||
{ id: "edit-profile", name: "Edit Profile", type: "page", route: "/profile/edit", variations: 1 },
|
||||
{ id: "account", name: "Account Settings", type: "page", route: "/settings/account", variations: 1 },
|
||||
{ id: "billing", name: "Billing", type: "page", route: "/settings/billing", variations: 2 },
|
||||
{ id: "notifications", name: "Notifications", type: "page", route: "/settings/notifications", variations: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// AI-suggested screens for Core Product
|
||||
const suggestedCoreScreens = [
|
||||
{
|
||||
id: "team-management",
|
||||
name: "Team Management",
|
||||
reason: "Collaborate with team members and manage permissions",
|
||||
version: "V1",
|
||||
},
|
||||
{
|
||||
id: "reports",
|
||||
name: "Reports & Insights",
|
||||
reason: "Data-driven decision making with comprehensive reports",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "integrations",
|
||||
name: "Integrations",
|
||||
reason: "Connect with external tools and services",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "search",
|
||||
name: "Global Search",
|
||||
reason: "Quick access to any content across the platform",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "empty-states",
|
||||
name: "Empty States",
|
||||
reason: "Guide users when no data is available",
|
||||
version: "V1",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock tree structure - User Flows
|
||||
const userFlowsTree = [
|
||||
{
|
||||
id: "authentication",
|
||||
name: "Authentication",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "signup", name: "Sign Up", type: "page", route: "/signup", variations: 3 },
|
||||
{ id: "login", name: "Login", type: "page", route: "/login", variations: 2 },
|
||||
{ id: "forgot-password", name: "Forgot Password", type: "page", route: "/forgot-password", variations: 1 },
|
||||
{ id: "verify-email", name: "Verify Email", type: "page", route: "/verify-email", variations: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "onboarding",
|
||||
name: "Onboarding",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "welcome", name: "Welcome", type: "page", route: "/onboarding/welcome", variations: 2 },
|
||||
{ id: "setup-profile", name: "Setup Profile", type: "page", route: "/onboarding/profile", variations: 2 },
|
||||
{ id: "preferences", name: "Preferences", type: "page", route: "/onboarding/preferences", variations: 1 },
|
||||
{ id: "complete", name: "Complete", type: "page", route: "/onboarding/complete", variations: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// AI-suggested flows/screens
|
||||
const suggestedFlows = [
|
||||
{
|
||||
id: "password-reset",
|
||||
name: "Password Reset Flow",
|
||||
reason: "Users need a complete password reset journey",
|
||||
version: "V1",
|
||||
screens: [
|
||||
{ name: "Reset Request" },
|
||||
{ name: "Check Email" },
|
||||
{ name: "New Password" },
|
||||
{ name: "Success" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "email-verification",
|
||||
name: "Email Verification Flow",
|
||||
reason: "Enhance security with multi-step verification",
|
||||
version: "V2",
|
||||
screens: [
|
||||
{ name: "Verification Sent" },
|
||||
{ name: "Enter Code" },
|
||||
{ name: "Verified" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "two-factor-setup",
|
||||
name: "Two-Factor Auth Setup",
|
||||
reason: "Add additional security layer for users",
|
||||
version: "V2",
|
||||
screens: [
|
||||
{ name: "Enable 2FA" },
|
||||
{ name: "Setup Authenticator" },
|
||||
{ name: "Verify Code" },
|
||||
{ name: "Backup Codes" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const DESIGN_NAV_ITEMS = [
|
||||
{ title: "Core Screens", icon: LayoutGrid, href: "#screens" },
|
||||
{ title: "User Flows", icon: Workflow, href: "#flows" },
|
||||
{ title: "Style Guide", icon: Palette, href: "#style-guide" },
|
||||
];
|
||||
|
||||
export default function UIUXPage({ params }: { params: Promise<{ projectId: string }> }) {
|
||||
const { projectId } = use(params);
|
||||
const pathname = usePathname();
|
||||
const workspace = pathname.split('/')[1]; // quick hack to get workspace
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
// GitHub connection state
|
||||
const [isGithubConnected, setIsGithubConnected] = useState(false);
|
||||
const [githubRepo, setGithubRepo] = useState<string | null>(null);
|
||||
const [lastSyncTime, setLastSyncTime] = useState<string | null>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
// Tree view state
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(["authentication", "dashboard"]));
|
||||
|
||||
const toggleFolder = (folderId: string) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
if (newExpanded.has(folderId)) {
|
||||
newExpanded.delete(folderId);
|
||||
} else {
|
||||
newExpanded.add(folderId);
|
||||
}
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
const handleConnectGithub = async () => {
|
||||
toast.info("Opening GitHub OAuth...");
|
||||
setTimeout(() => {
|
||||
setIsGithubConnected(true);
|
||||
setGithubRepo("username/repo-name");
|
||||
toast.success("GitHub connected!", {
|
||||
description: "Click Sync to scan your repository",
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleSyncRepository = async () => {
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
toast.info("Syncing repository...", {
|
||||
description: "AI is analyzing your codebase",
|
||||
});
|
||||
|
||||
const response = await fetch('/api/github/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
repo: githubRepo,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to sync repository');
|
||||
}
|
||||
|
||||
setLastSyncTime(new Date().toISOString());
|
||||
toast.success("Repository synced!", {
|
||||
description: `Found ${data.pageCount} pages`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing repository:', error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to sync repository");
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
toast.error("Please enter a design prompt");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v0/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
style: selectedStyle,
|
||||
projectId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to generate design');
|
||||
}
|
||||
|
||||
toast.success("Design generated successfully!", {
|
||||
description: "Opening in v0...",
|
||||
action: {
|
||||
label: "View",
|
||||
onClick: () => window.open(data.webUrl, '_blank'),
|
||||
},
|
||||
});
|
||||
|
||||
window.open(data.webUrl, '_blank');
|
||||
|
||||
setPrompt("");
|
||||
setSelectedStyle(null);
|
||||
} catch (error) {
|
||||
console.error('Error generating design:', error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to generate design");
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sidebarItems = DESIGN_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}/design${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* GitHub Connection / Sync */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border bg-card">
|
||||
{!isGithubConnected ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Github className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Connect Repository</p>
|
||||
<p className="text-xs text-muted-foreground">Sync your GitHub repo to detect pages</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleConnectGithub} size="sm">
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Connect
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Github className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{githubRepo}</p>
|
||||
{lastSyncTime && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Synced {new Date(lastSyncTime).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSyncRepository}
|
||||
disabled={isSyncing}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{isSyncing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Syncing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Sync
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Screens - Split into two columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Core Product */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Core Product</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{coreProductTree.map((folder) => (
|
||||
<div key={folder.id}>
|
||||
{/* Folder */}
|
||||
<button
|
||||
onClick={() => toggleFolder(folder.id)}
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm font-medium"
|
||||
>
|
||||
{expandedFolders.has(folder.id) ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium truncate">{folder.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{folder.children.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Pages in folder */}
|
||||
{expandedFolders.has(folder.id) && (
|
||||
<div className="ml-6 space-y-0.5 mt-0.5">
|
||||
{folder.children.map((page: any) => (
|
||||
<button
|
||||
key={page.id}
|
||||
className="flex items-center justify-between gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm group"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate">{page.name}</span>
|
||||
</div>
|
||||
{page.variations > 0 && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{page.variations}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* AI Suggested Screens */}
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">AI Suggested</h3>
|
||||
</div>
|
||||
|
||||
{suggestedCoreScreens.map((screen) => (
|
||||
<div key={screen.id} className="px-3 py-2.5 rounded-md border border-dashed border-primary/30 bg-primary/5 hover:bg-primary/10 transition-colors">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Sparkles className="h-4 w-4 text-primary shrink-0" />
|
||||
<div className="font-medium text-sm text-primary truncate">{screen.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{screen.version}
|
||||
</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => {
|
||||
toast.success("Generating screen...", {
|
||||
description: `Creating ${screen.name}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* User Flows */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Flows</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{userFlowsTree.map((folder) => (
|
||||
<div key={folder.id}>
|
||||
{/* Folder */}
|
||||
<button
|
||||
onClick={() => toggleFolder(folder.id)}
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm font-medium"
|
||||
>
|
||||
{expandedFolders.has(folder.id) ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium truncate">{folder.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{folder.children.length} steps
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Pages in folder - with flow indicators */}
|
||||
{expandedFolders.has(folder.id) && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5">
|
||||
{folder.children.map((page: any, index: number) => (
|
||||
<div key={page.id}>
|
||||
<button
|
||||
className="flex items-center justify-between gap-3 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm group"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-semibold shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="truncate">{page.name}</span>
|
||||
</div>
|
||||
{page.variations > 0 && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{page.variations}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* AI Suggested Flows */}
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">AI Suggested</h3>
|
||||
</div>
|
||||
|
||||
{suggestedFlows.map((flow) => (
|
||||
<div key={flow.id} className="space-y-1">
|
||||
<button
|
||||
onClick={() => toggleFolder(`suggested-${flow.id}`)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2.5 rounded-md border border-dashed border-primary/30 bg-primary/5 hover:bg-primary/10 transition-colors text-sm"
|
||||
>
|
||||
{expandedFolders.has(`suggested-${flow.id}`) ? (
|
||||
<ChevronDown className="h-4 w-4 text-primary" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<div className="font-medium text-primary truncate">{flow.name}</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{flow.version}
|
||||
</Badge>
|
||||
<span className="text-xs text-primary shrink-0">
|
||||
{flow.screens.length} screens
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Suggested screens in flow */}
|
||||
{expandedFolders.has(`suggested-${flow.id}`) && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5">
|
||||
{flow.screens.map((screen: any, index: number) => (
|
||||
<div key={index}>
|
||||
<div className="flex items-center gap-3 px-3 py-1.5 rounded-md border border-dashed text-sm">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-muted text-muted-foreground text-xs font-semibold shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="font-medium text-sm truncate">{screen.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Generate button */}
|
||||
<div className="pt-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
toast.success("Generating flow...", {
|
||||
description: `Creating ${flow.screens.length} screens for ${flow.name}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Generate This Flow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState, useEffect } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
Star,
|
||||
Info,
|
||||
Share2,
|
||||
Archive,
|
||||
Loader2,
|
||||
Target,
|
||||
Lightbulb,
|
||||
MessageSquare,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
|
||||
|
||||
type DocType = "all" | "vision" | "features" | "research" | "chats";
|
||||
type ViewType = "public" | "private" | "archived";
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
title: string;
|
||||
type: DocType;
|
||||
owner: string;
|
||||
dateModified: string;
|
||||
visibility: ViewType;
|
||||
starred: boolean;
|
||||
chunkCount?: number;
|
||||
}
|
||||
|
||||
export default function DocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = use(params);
|
||||
const [activeView, setActiveView] = useState<ViewType>("public");
|
||||
const [filterType, setFilterType] = useState<DocType>("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sortBy, setSortBy] = useState<"modified" | "created">("modified");
|
||||
|
||||
useEffect(() => {
|
||||
loadDocuments();
|
||||
}, [projectId, activeView, filterType]);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
`/api/projects/${projectId}/knowledge/items?visibility=${activeView}&type=${filterType}`
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Use returned items or mock data if empty
|
||||
if (data.items && data.items.length > 0) {
|
||||
// Transform knowledge items to document format
|
||||
const docs = data.items.map((item: any, index: number) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.sourceType === 'vision' ? 'vision' :
|
||||
item.sourceType === 'feature' ? 'features' :
|
||||
item.sourceType === 'chat' ? 'chats' : 'research',
|
||||
owner: "You",
|
||||
dateModified: item.updatedAt || item.createdAt,
|
||||
visibility: activeView,
|
||||
starred: false,
|
||||
chunkCount: item.chunkCount,
|
||||
}));
|
||||
setDocuments(docs);
|
||||
} else {
|
||||
// Show mock data when no real data exists
|
||||
setDocuments([
|
||||
{
|
||||
id: "1",
|
||||
title: "Project Vision & Mission",
|
||||
type: "vision",
|
||||
owner: "You",
|
||||
dateModified: new Date().toISOString(),
|
||||
visibility: "public",
|
||||
starred: true,
|
||||
chunkCount: 12,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Core Features Specification",
|
||||
type: "features",
|
||||
owner: "You",
|
||||
dateModified: new Date(Date.now() - 86400000).toISOString(),
|
||||
visibility: "public",
|
||||
starred: false,
|
||||
chunkCount: 24,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Fallback to mock data on error
|
||||
setDocuments([
|
||||
{
|
||||
id: "1",
|
||||
title: "Project Vision & Mission",
|
||||
type: "vision",
|
||||
owner: "You",
|
||||
dateModified: new Date().toISOString(),
|
||||
visibility: "public",
|
||||
starred: true,
|
||||
chunkCount: 12,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading documents:", error);
|
||||
// Show mock data on error
|
||||
setDocuments([
|
||||
{
|
||||
id: "1",
|
||||
title: "Project Vision & Mission",
|
||||
type: "vision",
|
||||
owner: "You",
|
||||
dateModified: new Date().toISOString(),
|
||||
visibility: "public",
|
||||
starred: true,
|
||||
chunkCount: 12,
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getDocIcon = (type: DocType) => {
|
||||
switch (type) {
|
||||
case "vision":
|
||||
return <Target className="h-4 w-4 text-blue-600" />;
|
||||
case "features":
|
||||
return <Lightbulb className="h-4 w-4 text-purple-600" />;
|
||||
case "research":
|
||||
return <BookOpen className="h-4 w-4 text-green-600" />;
|
||||
case "chats":
|
||||
return <MessageSquare className="h-4 w-4 text-orange-600" />;
|
||||
default:
|
||||
return <FileText className="h-4 w-4 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredDocuments = documents.filter((doc) => {
|
||||
if (searchQuery && !doc.title.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-background overflow-hidden flex">
|
||||
{/* Left Sidebar */}
|
||||
<CollapsibleSidebar>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Document Stats</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total Docs</span>
|
||||
<span className="font-medium">{documents.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Public</span>
|
||||
<span className="font-medium">{documents.filter(d => d.visibility === 'public').length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Private</span>
|
||||
<span className="font-medium">{documents.filter(d => d.visibility === 'private').length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Starred</span>
|
||||
<span className="font-medium">{documents.filter(d => d.starred).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSidebar>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-xl font-bold">Docs</h1>
|
||||
<Badge variant="secondary" className="font-normal">
|
||||
{filteredDocuments.length} {filteredDocuments.length === 1 ? "doc" : "docs"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add page
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center gap-6 px-4">
|
||||
<button
|
||||
onClick={() => setActiveView("public")}
|
||||
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeView === "public"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Public
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("private")}
|
||||
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeView === "private"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Private
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("archived")}
|
||||
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeView === "archived"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Archived
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-4 p-4 border-b bg-muted/30">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search docs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={sortBy} onValueChange={(value: any) => setSortBy(value)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="modified">Date modified</SelectItem>
|
||||
<SelectItem value="created">Date created</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filterType} onValueChange={(value: any) => setFilterType(value)}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All types</SelectItem>
|
||||
<SelectItem value="vision">Vision</SelectItem>
|
||||
<SelectItem value="features">Features</SelectItem>
|
||||
<SelectItem value="research">Research</SelectItem>
|
||||
<SelectItem value="chats">Chats</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Document List */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : filteredDocuments.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No documents yet</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create your first document to get started
|
||||
</p>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add page
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredDocuments.map((doc) => (
|
||||
<Link
|
||||
key={doc.id}
|
||||
href={`/${workspace}/project/${projectId}/docs/${doc.id}`}
|
||||
className="block"
|
||||
>
|
||||
<Card className="p-4 hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
{getDocIcon(doc.type)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{doc.title}</h3>
|
||||
{doc.starred && (
|
||||
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-xs text-muted-foreground">{doc.owner}</span>
|
||||
<span className="text-xs text-muted-foreground">•</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(doc.dateModified).toLocaleDateString()}
|
||||
</span>
|
||||
{doc.chunkCount && (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">•</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{doc.chunkCount} chunks
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toast.info("Share functionality coming soon");
|
||||
}}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toast.info("Info panel coming soon");
|
||||
}}
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Toggle star
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
className={`h-4 w-4 ${
|
||||
doc.starred ? "fill-yellow-400 text-yellow-400" : ""
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toast.info("More options coming soon");
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Main Content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Box, Plus } from "lucide-react";
|
||||
|
||||
export default async function FeaturesPage({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Page Header */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Features</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Plan and track your product features
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Feature
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Feature List</CardTitle>
|
||||
<CardDescription>
|
||||
Features with user stories and acceptance criteria
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mb-4 rounded-full bg-muted p-3">
|
||||
<Box className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No features yet</h3>
|
||||
<p className="text-sm text-center text-muted-foreground max-w-sm mb-4">
|
||||
Start planning your features with user stories and track their progress
|
||||
</p>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create First Feature
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function AnalyzePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<div className="flex-1 p-8 space-y-8 max-w-4xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Analyzing Your Project</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Our AI is reviewing your code and documentation to understand your product
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Analysis Progress */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
Analysis in Progress
|
||||
</CardTitle>
|
||||
<CardDescription>This may take a few moments...</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm">Reading repository structure</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm">Analyzing code patterns</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="h-3 w-3 animate-spin text-primary" />
|
||||
<span className="text-sm">Processing ChatGPT conversations</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-2 w-2 rounded-full bg-muted-foreground/30" />
|
||||
<span className="text-sm text-muted-foreground">Extracting product vision</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-2 w-2 rounded-full bg-muted-foreground/30" />
|
||||
<span className="text-sm text-muted-foreground">Identifying features</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Link href={`/${workspace}/${projectId}/getting-started/summarize`}>
|
||||
<Button size="lg">
|
||||
Continue to Summary
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Github, ArrowRight, Download } from "lucide-react";
|
||||
import { CursorIcon, OpenAIIcon } from "@/components/icons/custom-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function ConnectPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<div className="flex-1 p-8 space-y-8 max-w-4xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Connect Your Sources</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Install the Cursor extension and connect your development sources. Our AI will analyze all of the information and automatically create your project for you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connection Cards */}
|
||||
<div className="space-y-4">
|
||||
{/* Cursor Extension */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-500/10">
|
||||
<CursorIcon className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Cursor Extension</CardTitle>
|
||||
<CardDescription>Install our extension to track your development sessions</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Install Extension
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>The extension will help us:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Track your coding sessions and AI interactions</li>
|
||||
<li>Monitor costs and token usage</li>
|
||||
<li>Generate automatic documentation</li>
|
||||
<li>Sync your conversations with Vib'n</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* GitHub Connection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Github className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>GitHub Repository</CardTitle>
|
||||
<CardDescription>Connect your code repository for analysis</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button>
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Connect GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>We'll need access to:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Read your repository code and structure</li>
|
||||
<li>Access to repository metadata</li>
|
||||
<li>View commit history</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ChatGPT Connection - Optional */}
|
||||
<Card className="border-dashed">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-500/10">
|
||||
<OpenAIIcon className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle>ChatGPT Project (MCP)</CardTitle>
|
||||
<span className="px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-xs font-medium">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription>Connect your ChatGPT conversations and docs</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<OpenAIIcon className="h-4 w-4 mr-2" />
|
||||
Install MCP
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Install the Model Context Protocol to:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Access your ChatGPT project conversations</li>
|
||||
<li>Read product documentation and notes</li>
|
||||
<li>Sync your product vision and requirements</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Link href={`/${workspace}/${projectId}/getting-started/analyze`}>
|
||||
<Button size="lg">
|
||||
Continue to Analyze
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
|
||||
import { RightPanel } from "@/components/layout/right-panel";
|
||||
import { ProjectSidebar } from "@/components/layout/project-sidebar";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
export default function GettingStartedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [activeSection, setActiveSection] = useState("projects");
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
const workspace = params.workspace as string;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background">
|
||||
{/* Left Rail - Workspace Navigation */}
|
||||
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
|
||||
|
||||
{/* Project Sidebar - Getting Started Steps */}
|
||||
<ProjectSidebar projectId={projectId} activeSection={activeSection} workspace={workspace} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 overflow-hidden">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Right Panel - AI Assistant */}
|
||||
<RightPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle2, ArrowRight, Sparkles } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function SetupPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<div className="flex-1 p-8 space-y-8 max-w-4xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Setup Your Project</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
We've created your project structure based on the analysis
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Setup Complete */}
|
||||
<Card className="border-green-500/50 bg-green-500/5">
|
||||
<CardContent className="pt-6 pb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-green-500/10">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-1">Project Setup Complete!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Your project has been configured with all the necessary sections
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* What We Created */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>What We've Set Up</CardTitle>
|
||||
<CardDescription>Your project is ready with these sections</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border bg-card">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Product Vision</p>
|
||||
<p className="text-sm text-muted-foreground">Your product goals and strategy</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border bg-card">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Progress Tracking</p>
|
||||
<p className="text-sm text-muted-foreground">Monitor your development progress</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border bg-card">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">UI UX Design</p>
|
||||
<p className="text-sm text-muted-foreground">Design and iterate on your screens</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border bg-card">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Code Repository</p>
|
||||
<p className="text-sm text-muted-foreground">Connected to your GitHub repo</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border bg-card">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Deployment & Automation</p>
|
||||
<p className="text-sm text-muted-foreground">CI/CD and automated workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Start Building Button */}
|
||||
<div className="flex justify-center pt-4">
|
||||
<Link href={`/${workspace}/${projectId}/product`}>
|
||||
<Button size="lg" className="gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Start Building
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle2, ArrowRight, Target, Code2, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function SummarizePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<div className="flex-1 p-8 space-y-8 max-w-4xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">Project Summary</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Here's what we learned about your product
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="space-y-4">
|
||||
{/* Product Vision */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Target className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">Product Vision</CardTitle>
|
||||
<CardDescription>What you're building</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
An AI-powered development monitoring platform that tracks coding sessions,
|
||||
analyzes conversations, and maintains living documentation.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tech Stack */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10">
|
||||
<Code2 className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">Tech Stack</CardTitle>
|
||||
<CardDescription>Technologies detected</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">Next.js</span>
|
||||
<span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">TypeScript</span>
|
||||
<span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">PostgreSQL</span>
|
||||
<span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">Node.js</span>
|
||||
<span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">Tailwind CSS</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Key Features */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-500/10">
|
||||
<Zap className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">Key Features</CardTitle>
|
||||
<CardDescription>Main capabilities identified</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5 shrink-0" />
|
||||
<span>Session tracking and cost monitoring</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5 shrink-0" />
|
||||
<span>AI-powered code analysis with Gemini 2.0 Flash</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5 shrink-0" />
|
||||
<span>Automatic documentation generation</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5 shrink-0" />
|
||||
<span>Cursor IDE extension integration</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Link href={`/${workspace}/${projectId}/getting-started/setup`}>
|
||||
<Button size="lg">
|
||||
Continue to Setup
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState, useEffect } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
GitBranch,
|
||||
ChevronRight,
|
||||
Search,
|
||||
Lightbulb,
|
||||
ShoppingCart,
|
||||
UserPlus,
|
||||
Rocket,
|
||||
Zap,
|
||||
HelpCircle,
|
||||
CreditCard,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
X,
|
||||
Palette,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
|
||||
|
||||
interface WorkItem {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
status: "built" | "in_progress" | "missing";
|
||||
category: string;
|
||||
sessionsCount: number;
|
||||
commitsCount: number;
|
||||
journeyStage?: string;
|
||||
}
|
||||
|
||||
interface AssetNode {
|
||||
id: string;
|
||||
name: string;
|
||||
asset_type: string;
|
||||
must_have_for_v1: boolean;
|
||||
asset_metadata: {
|
||||
why_it_exists: string;
|
||||
which_user_it_serves?: string;
|
||||
problem_it_helps_with?: string;
|
||||
connection_to_magic_moment: string;
|
||||
journey_stage?: string;
|
||||
visual_style_notes?: string;
|
||||
implementation_notes?: string;
|
||||
};
|
||||
children?: AssetNode[];
|
||||
}
|
||||
|
||||
interface JourneyStage {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: any;
|
||||
description: string;
|
||||
color: string;
|
||||
items: WorkItem[];
|
||||
assets: AssetNode[]; // Visual assets for this stage
|
||||
}
|
||||
|
||||
export default function JourneyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = use(params);
|
||||
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
|
||||
const [touchpointAssets, setTouchpointAssets] = useState<AssetNode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
||||
const [journeyStages, setJourneyStages] = useState<JourneyStage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadJourneyData();
|
||||
}, [projectId]);
|
||||
|
||||
const loadJourneyData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load work items for stats
|
||||
const timelineResponse = await fetch(`/api/projects/${projectId}/timeline-view`);
|
||||
if (timelineResponse.ok) {
|
||||
const timelineData = await timelineResponse.json();
|
||||
setWorkItems(timelineData.workItems);
|
||||
}
|
||||
|
||||
// Load AI-generated touchpoints tree
|
||||
const mvpResponse = await fetch(`/api/projects/${projectId}/mvp-checklist`);
|
||||
if (mvpResponse.ok) {
|
||||
const mvpData = await mvpResponse.json();
|
||||
|
||||
// Extract touchpoints from AI response if it exists
|
||||
if (mvpData.aiGenerated && mvpData.touchpointsTree) {
|
||||
const allTouchpoints = flattenAssetNodes(mvpData.touchpointsTree.nodes || []);
|
||||
setTouchpointAssets(allTouchpoints);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Build journey stages from both work items and touchpoint assets
|
||||
const stages = buildJourneyStages(workItems, touchpointAssets);
|
||||
setJourneyStages(stages);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading journey data:", error);
|
||||
toast.error("Failed to load journey data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten nested asset nodes
|
||||
const flattenAssetNodes = (nodes: AssetNode[]): AssetNode[] => {
|
||||
const flattened: AssetNode[] = [];
|
||||
|
||||
const flatten = (node: AssetNode) => {
|
||||
flattened.push(node);
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => flatten(child));
|
||||
}
|
||||
};
|
||||
|
||||
nodes.forEach(node => flatten(node));
|
||||
return flattened;
|
||||
};
|
||||
|
||||
const getJourneySection = (item: WorkItem): string => {
|
||||
const title = item.title.toLowerCase();
|
||||
const path = item.path.toLowerCase();
|
||||
|
||||
// Discovery
|
||||
if (path === '/' || title.includes('landing') || title.includes('marketing page')) return 'Discovery';
|
||||
if (item.category === 'Social' && !path.includes('settings')) return 'Discovery';
|
||||
|
||||
// Research
|
||||
if (item.category === 'Content' || path.includes('/docs')) return 'Research';
|
||||
if (title.includes('marketing dashboard')) return 'Research';
|
||||
|
||||
// Onboarding
|
||||
if (path.includes('auth') || path.includes('oauth')) return 'Onboarding';
|
||||
if (path.includes('signup') || path.includes('signin')) return 'Onboarding';
|
||||
|
||||
// First Use
|
||||
if (title.includes('onboarding')) return 'First Use';
|
||||
if (title.includes('getting started')) return 'First Use';
|
||||
if (path.includes('workspace') && !path.includes('settings')) return 'First Use';
|
||||
if (title.includes('creation flow')) return 'First Use';
|
||||
|
||||
// Active
|
||||
if (path.includes('overview') || path.includes('/dashboard')) return 'Active';
|
||||
if (path.includes('timeline-plan') || path.includes('audit') || path.includes('mission')) return 'Active';
|
||||
if (path.includes('/api/projects') || path.includes('mvp-checklist')) return 'Active';
|
||||
|
||||
// Support
|
||||
if (path.includes('settings')) return 'Support';
|
||||
|
||||
// Purchase
|
||||
if (path.includes('billing') || path.includes('payment')) return 'Purchase';
|
||||
|
||||
return 'Active';
|
||||
};
|
||||
|
||||
const buildJourneyStages = (items: WorkItem[], assets: AssetNode[]): JourneyStage[] => {
|
||||
const stageDefinitions = [
|
||||
{
|
||||
id: "discovery",
|
||||
name: "Discovery",
|
||||
stageMappings: ["Awareness", "Discovery"],
|
||||
icon: Search,
|
||||
description: "Found you online via social, blog, or ad",
|
||||
color: "bg-blue-100 border-blue-300 text-blue-900",
|
||||
},
|
||||
{
|
||||
id: "research",
|
||||
name: "Research",
|
||||
stageMappings: ["Curiosity", "Research"],
|
||||
icon: Lightbulb,
|
||||
description: "Checking out features, pricing, and value",
|
||||
color: "bg-purple-100 border-purple-300 text-purple-900",
|
||||
},
|
||||
{
|
||||
id: "onboarding",
|
||||
name: "Onboarding",
|
||||
stageMappings: ["First Try", "Onboarding"],
|
||||
icon: UserPlus,
|
||||
description: "Creating account to try the product",
|
||||
color: "bg-green-100 border-green-300 text-green-900",
|
||||
},
|
||||
{
|
||||
id: "first-use",
|
||||
name: "First Use",
|
||||
stageMappings: ["First Real Day", "First Use"],
|
||||
icon: Rocket,
|
||||
description: "Zero to experiencing the magic",
|
||||
color: "bg-orange-100 border-orange-300 text-orange-900",
|
||||
},
|
||||
{
|
||||
id: "active",
|
||||
name: "Active",
|
||||
stageMappings: ["Habit", "Active", "Post-MVP"],
|
||||
icon: Zap,
|
||||
description: "Using the magic repeatedly",
|
||||
color: "bg-yellow-100 border-yellow-300 text-yellow-900",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
name: "Support",
|
||||
stageMappings: ["Support"],
|
||||
icon: HelpCircle,
|
||||
description: "Getting help to maximize value",
|
||||
color: "bg-indigo-100 border-indigo-300 text-indigo-900",
|
||||
},
|
||||
{
|
||||
id: "purchase",
|
||||
name: "Purchase",
|
||||
stageMappings: ["Decision to Pay", "Purchase"],
|
||||
icon: CreditCard,
|
||||
description: "Time to pay to keep using",
|
||||
color: "bg-pink-100 border-pink-300 text-pink-900",
|
||||
},
|
||||
];
|
||||
|
||||
return stageDefinitions.map(stage => {
|
||||
// Get work items for this stage
|
||||
const stageItems = items.filter(item => {
|
||||
const section = getJourneySection(item);
|
||||
return section === stage.name;
|
||||
});
|
||||
|
||||
// Get touchpoint assets for this stage from AI-generated metadata
|
||||
const stageAssets = assets.filter(asset => {
|
||||
const assetJourneyStage = asset.asset_metadata?.journey_stage || '';
|
||||
return stage.stageMappings.some(mapping =>
|
||||
assetJourneyStage.toLowerCase().includes(mapping.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...stage,
|
||||
items: stageItems,
|
||||
assets: stageAssets,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
if (status === "built") return <CheckCircle2 className="h-3 w-3 text-green-600" />;
|
||||
if (status === "in_progress") return <Circle className="h-3 w-3 text-blue-600 fill-blue-600" />;
|
||||
return <Circle className="h-3 w-3 text-gray-400" />;
|
||||
};
|
||||
|
||||
const selectedStageData = journeyStages.find(s => s.id === selectedStage);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-background overflow-hidden flex">
|
||||
{/* Left Sidebar */}
|
||||
<CollapsibleSidebar>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Journey Stats</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Stages</span>
|
||||
<span className="font-medium">{journeyStages.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total Assets</span>
|
||||
<span className="font-medium">{journeyStages.reduce((sum, stage) => sum + stage.assets.length, 0)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Work Items</span>
|
||||
<span className="font-medium">{workItems.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSidebar>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col bg-background overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<GitBranch className="h-6 w-6" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Customer Journey</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Track touchpoints across the customer lifecycle
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* Journey Flow */}
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-0 overflow-x-auto pb-4">
|
||||
{journeyStages.map((stage, index) => (
|
||||
<div key={stage.id} className="flex items-center flex-shrink-0">
|
||||
{/* Stage Card */}
|
||||
<Card
|
||||
className={`w-64 border-2 cursor-pointer transition-all hover:shadow-lg ${
|
||||
stage.color
|
||||
} ${selectedStage === stage.id ? "ring-2 ring-primary" : ""}`}
|
||||
onClick={() => setSelectedStage(stage.id)}
|
||||
>
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<stage.icon className="h-5 w-5" />
|
||||
<h3 className="font-bold text-sm">{stage.name}</h3>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{stage.items.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xs opacity-80 line-clamp-2">{stage.description}</p>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
<span>
|
||||
{stage.items.filter(i => i.status === "built").length} built
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Circle className="h-3 w-3 fill-current" />
|
||||
<span>
|
||||
{stage.items.filter(i => i.status === "in_progress").length} in progress
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-white/50 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-current h-1.5 rounded-full transition-all"
|
||||
style={{
|
||||
width: `${
|
||||
stage.items.length > 0
|
||||
? (stage.items.filter(i => i.status === "built").length /
|
||||
stage.items.length) *
|
||||
100
|
||||
: 0
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Connector Arrow */}
|
||||
{index < journeyStages.length - 1 && (
|
||||
<ChevronRight className="h-8 w-8 text-muted-foreground mx-2 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stage Details Panel */}
|
||||
{selectedStageData && (
|
||||
<div className="border-t bg-muted/30 p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<selectedStageData.icon className="h-6 w-6" />
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">{selectedStageData.name} Touchpoints</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedStageData.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedStage(null)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedStageData.assets.length === 0 && selectedStageData.items.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
No assets defined for this stage yet
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => toast.info("AI will generate assets when you regenerate the plan")}>
|
||||
Generate with AI
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* AI-Generated Visual Assets */}
|
||||
{selectedStageData.assets.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-muted-foreground uppercase tracking-wide">
|
||||
Visual Assets
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{selectedStageData.assets.map((asset) => (
|
||||
<Card key={asset.id} className="overflow-hidden hover:shadow-lg transition-all group cursor-pointer">
|
||||
{/* Visual Preview */}
|
||||
<div className="aspect-video bg-gradient-to-br from-indigo-50 to-purple-50 relative overflow-hidden border-b">
|
||||
{/* Placeholder for actual design preview */}
|
||||
<div className="absolute inset-0 flex items-center justify-center p-6">
|
||||
<div className="text-center">
|
||||
<Palette className="h-10 w-10 text-indigo-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-indigo-600 font-medium line-clamp-2">
|
||||
{asset.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* V1 Badge */}
|
||||
{asset.must_have_for_v1 && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<Badge variant="default" className="shadow-sm bg-blue-600">
|
||||
V1
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Asset Type Badge */}
|
||||
<div className="absolute top-2 left-2">
|
||||
<Badge variant="secondary" className="shadow-sm text-xs">
|
||||
{asset.asset_type.replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Hover overlay */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm mb-2">{asset.name}</h3>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{asset.asset_metadata?.why_it_exists}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{asset.asset_metadata?.visual_style_notes && (
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="font-medium">Style:</span>{" "}
|
||||
{asset.asset_metadata.visual_style_notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{asset.asset_metadata?.which_user_it_serves || "All users"}
|
||||
</Badge>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 text-xs gap-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toast.info("Opening in designer...");
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Design
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Existing Work Items */}
|
||||
{selectedStageData.items.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-muted-foreground uppercase tracking-wide">
|
||||
Existing Work
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{selectedStageData.items.map((item) => (
|
||||
<Card key={item.id} className="p-4 hover:bg-accent/50 transition-colors">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(item.status)}
|
||||
<h3 className="font-semibold text-sm">{item.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground font-mono truncate">
|
||||
{item.path}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{item.sessionsCount} sessions</span>
|
||||
<span>•</span>
|
||||
<span>{item.commitsCount} commits</span>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.status === "built"
|
||||
? "Done"
|
||||
: item.status === "in_progress"
|
||||
? "In Progress"
|
||||
: "To-do"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* End Main Content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import {
|
||||
Megaphone,
|
||||
MessageSquare,
|
||||
Globe,
|
||||
Target,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Edit,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
} from "@/components/layout/page-template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const MARKET_NAV_ITEMS = [
|
||||
{ title: "Value Proposition", icon: Target, href: "/market" },
|
||||
{ title: "Messaging Framework", icon: MessageSquare, href: "/market#messaging" },
|
||||
{ title: "Website Copy", icon: Globe, href: "/market#website" },
|
||||
{ title: "Launch Strategy", icon: Rocket, href: "/market#launch" },
|
||||
{ title: "Target Channels", icon: Megaphone, href: "/market#channels" },
|
||||
];
|
||||
|
||||
export default function MarketPage() {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
const sidebarItems = MARKET_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
}}
|
||||
>
|
||||
{/* Value Proposition */}
|
||||
<PageSection
|
||||
title="Value Proposition"
|
||||
description="Your core message to the market"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Headline
|
||||
</h3>
|
||||
<p className="text-2xl font-bold">
|
||||
Build Your Product Faster with AI-Powered Insights
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Subheadline
|
||||
</h3>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Turn conversations into code, design, and marketing - all in one platform
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Key Benefits
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<span>Save weeks of planning and research</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<span>Get AI-generated designs and code structure</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<span>Launch with confidence and clarity</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Messaging Framework */}
|
||||
<PageSection title="Messaging Framework" description="How you talk about your product">
|
||||
<PageGrid cols={2}>
|
||||
<PageCard>
|
||||
<h3 className="font-semibold mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
Primary Message
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
For solo founders and small teams building their first product
|
||||
</p>
|
||||
<p className="text-base">
|
||||
"Stop getting stuck in planning. Start building with AI as your co-founder."
|
||||
</p>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<h3 className="font-semibold mb-3 flex items-center gap-2">
|
||||
<Target className="h-4 w-4 text-muted-foreground" />
|
||||
Positioning
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Different from competitors because...
|
||||
</p>
|
||||
<p className="text-base">
|
||||
"We don't just track - we actively guide you from idea to launch with AI."
|
||||
</p>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
|
||||
{/* Website Copy */}
|
||||
<PageSection
|
||||
title="Website Copy"
|
||||
description="Content for your marketing site"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Generate More
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<PageCard>
|
||||
<h3 className="font-semibold mb-3">Hero Section</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-xs text-muted-foreground mb-1">Headline</p>
|
||||
<p className="font-medium">Build Your SaaS from Idea to Launch</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-xs text-muted-foreground mb-1">CTA Button</p>
|
||||
<p className="font-medium">Start Building Free →</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<h3 className="font-semibold mb-3">Features Section</h3>
|
||||
<PageGrid cols={3}>
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-sm font-medium mb-1">🎯 AI Interview</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Chat with AI to define your product
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-sm font-medium mb-1">🎨 Auto Design</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Generate UI screens instantly
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-sm font-medium mb-1">🚀 Launch Plan</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Get a complete go-to-market strategy
|
||||
</p>
|
||||
</div>
|
||||
</PageGrid>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<h3 className="font-semibold mb-3">Social Proof</h3>
|
||||
<div className="p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
"This tool cut our planning time from 4 weeks to 2 days. Incredible."
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
- Founder Name, Company
|
||||
</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
{/* Launch Strategy */}
|
||||
<PageSection title="Launch Strategy" description="Your go-to-market plan">
|
||||
<PageCard>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Rocket className="h-4 w-4 text-muted-foreground" />
|
||||
Launch Timeline
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/30">
|
||||
<div className="text-xs font-medium text-muted-foreground w-20">
|
||||
Week 1-2
|
||||
</div>
|
||||
<div className="text-sm">Soft launch to beta testers</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/30">
|
||||
<div className="text-xs font-medium text-muted-foreground w-20">
|
||||
Week 3
|
||||
</div>
|
||||
<div className="text-sm">Product Hunt launch</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/30">
|
||||
<div className="text-xs font-medium text-muted-foreground w-20">
|
||||
Week 4+
|
||||
</div>
|
||||
<div className="text-sm">Content marketing & SEO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Target Channels */}
|
||||
<PageSection
|
||||
title="Target Channels"
|
||||
description="Where to reach your audience"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Channel
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageGrid cols={2}>
|
||||
<PageCard hover>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Globe className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Twitter/X</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Primary channel for developer audience
|
||||
</p>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-primary/10 text-primary">
|
||||
High Priority
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard hover>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Rocket className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Product Hunt</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Launch day visibility and early adopters
|
||||
</p>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-muted text-muted-foreground font-medium">
|
||||
Launch Day
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard hover>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Dev Communities</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Indie Hackers, Reddit, Discord servers
|
||||
</p>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-muted text-muted-foreground font-medium">
|
||||
Ongoing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard hover>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Globe className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Content Marketing</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Blog posts, tutorials, case studies
|
||||
</p>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-muted text-muted-foreground font-medium">
|
||||
Long-term
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import {
|
||||
Target,
|
||||
Users,
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
Lightbulb,
|
||||
Plus,
|
||||
Edit,
|
||||
Search,
|
||||
Loader2,
|
||||
Layout,
|
||||
CheckCircle,
|
||||
DollarSign,
|
||||
Link as LinkIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
PageEmptyState,
|
||||
} from "@/components/layout/page-template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MissionContextTree } from "@/components/mission/mission-context-tree";
|
||||
import { MissionIdeaSection } from "@/components/mission/mission-idea-section";
|
||||
import { auth } from "@/lib/firebase/config";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const MISSION_NAV_ITEMS = [
|
||||
{ title: "Target Customer", icon: Users, href: "/mission" },
|
||||
{ title: "Existing Solutions", icon: Layout, href: "/mission#solutions" },
|
||||
];
|
||||
|
||||
interface MissionFramework {
|
||||
targetCustomer: {
|
||||
primaryAudience: string;
|
||||
theirSituation: string;
|
||||
relatedMarkets?: string[];
|
||||
};
|
||||
existingSolutions: Array<{
|
||||
category: string;
|
||||
description: string;
|
||||
products: Array<{
|
||||
name: string;
|
||||
url?: string;
|
||||
}>;
|
||||
}>;
|
||||
innovations: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
ideaValidation: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
financialSuccess: {
|
||||
subscribers: number;
|
||||
pricePoint: number;
|
||||
retentionRate: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function MissionPage() {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
const [researchingMarket, setResearchingMarket] = useState(false);
|
||||
const [framework, setFramework] = useState<MissionFramework | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
// Fetch mission framework on mount
|
||||
useEffect(() => {
|
||||
fetchFramework();
|
||||
}, [projectId]);
|
||||
|
||||
const fetchFramework = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch project data from Firestore to get the saved framework
|
||||
const user = auth.currentUser;
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (user) {
|
||||
const token = await user.getIdToken();
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.project?.phaseData?.missionFramework) {
|
||||
setFramework(data.project.phaseData.missionFramework);
|
||||
console.log('[Mission] Loaded saved framework');
|
||||
} else {
|
||||
console.log('[Mission] No saved framework found');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Mission] Error fetching framework:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateFramework = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (user) {
|
||||
const token = await user.getIdToken();
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/mission/generate`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to generate mission framework');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setFramework(data.framework);
|
||||
toast.success('Mission framework generated successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error generating framework:', error);
|
||||
toast.error('Failed to generate mission framework');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResearchMarket = async () => {
|
||||
setResearchingMarket(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error('Please sign in');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const response = await fetch(`/api/projects/${projectId}/research/market`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to conduct market research');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
toast.success(
|
||||
`Market research complete! Found ${data.research.targetNiches.length} niches, ` +
|
||||
`${data.research.competitors.length} competitors, and ${data.research.marketGaps.length} gaps.`
|
||||
);
|
||||
|
||||
// Regenerate framework with new insights
|
||||
await handleGenerateFramework();
|
||||
} catch (error) {
|
||||
console.error('Error conducting market research:', error);
|
||||
toast.error('Failed to conduct market research');
|
||||
} finally {
|
||||
setResearchingMarket(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Build sidebar items with full hrefs and active states
|
||||
const sidebarItems = MISSION_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
customContent: <MissionContextTree projectId={projectId} />,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
if (!framework) {
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
customContent: <MissionContextTree projectId={projectId} />,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<Lightbulb className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">No Mission Framework Yet</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md">
|
||||
Generate your mission framework based on your project's insights and knowledge
|
||||
</p>
|
||||
<Button onClick={handleGenerateFramework} disabled={generating} size="lg">
|
||||
{generating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Generate Mission Framework
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
customContent: <MissionContextTree projectId={projectId} />,
|
||||
}}
|
||||
>
|
||||
{/* Target Customer */}
|
||||
<PageSection
|
||||
title="Target Customer"
|
||||
description="Who you're building for"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost" onClick={handleGenerateFramework} disabled={generating}>
|
||||
{generating ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Regenerate
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Primary Audience</h4>
|
||||
<p className="text-muted-foreground">
|
||||
{framework.targetCustomer.primaryAudience}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Their Situation</h4>
|
||||
<p className="text-muted-foreground">
|
||||
{framework.targetCustomer.theirSituation}
|
||||
</p>
|
||||
</div>
|
||||
{framework.targetCustomer.relatedMarkets && framework.targetCustomer.relatedMarkets.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Related Markets</h4>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{framework.targetCustomer.relatedMarkets.map((market, idx) => (
|
||||
<li key={idx} className="text-sm text-muted-foreground">
|
||||
{market}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Existing Solutions */}
|
||||
<PageSection
|
||||
title="Existing Solutions"
|
||||
description="What alternatives already exist"
|
||||
headerAction={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={handleResearchMarket}
|
||||
disabled={researchingMarket}
|
||||
>
|
||||
{researchingMarket ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Researching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
Research Market
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{framework.existingSolutions.map((solution, idx) => (
|
||||
<PageCard key={idx}>
|
||||
<h4 className="font-semibold text-sm mb-3">{solution.category}</h4>
|
||||
{solution.products && solution.products.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{solution.products.map((product, prodIdx) => (
|
||||
<div key={prodIdx} className="text-sm">
|
||||
{product.url ? (
|
||||
<a
|
||||
href={product.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{product.name}
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground">{product.name}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PageCard>
|
||||
))}
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import {
|
||||
DollarSign,
|
||||
Receipt,
|
||||
CreditCard,
|
||||
TrendingUp,
|
||||
Plus,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
} from "@/components/layout/page-template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MONEY_NAV_ITEMS = [
|
||||
{ title: "Expenses", icon: Receipt, href: "/money" },
|
||||
{ title: "Costs", icon: TrendingUp, href: "/money#costs" },
|
||||
{ title: "Pricing", icon: DollarSign, href: "/money#pricing" },
|
||||
{ title: "Plans", icon: CreditCard, href: "/money#plans" },
|
||||
];
|
||||
|
||||
const SAMPLE_EXPENSES = [
|
||||
{ id: 1, name: "Logo Design", amount: 299, date: "2025-01-15", category: "Design" },
|
||||
{ id: 2, name: "Domain Registration", amount: 12, date: "2025-01-10", category: "Infrastructure" },
|
||||
{ id: 3, name: "SSL Certificate", amount: 69, date: "2025-01-08", category: "Infrastructure" },
|
||||
];
|
||||
|
||||
const SAMPLE_COSTS = [
|
||||
{ id: 1, name: "Vercel Hosting", amount: 20, frequency: "monthly", category: "Infrastructure" },
|
||||
{ id: 2, name: "OpenAI API", amount: 45, frequency: "monthly", category: "Services" },
|
||||
{ id: 3, name: "SendGrid Email", amount: 15, frequency: "monthly", category: "Services" },
|
||||
{ id: 4, name: "Stripe Fees", amount: 0, frequency: "per transaction", category: "Services" },
|
||||
];
|
||||
|
||||
export default function MoneyPage() {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
const sidebarItems = MONEY_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
const totalExpenses = SAMPLE_EXPENSES.reduce((sum, e) => sum + e.amount, 0);
|
||||
const monthlyCosts = SAMPLE_COSTS.filter(c => c.frequency === "monthly").reduce((sum, c) => sum + c.amount, 0);
|
||||
const annualCosts = monthlyCosts * 12;
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
}}
|
||||
>
|
||||
{/* Financial Overview */}
|
||||
<PageSection>
|
||||
<PageGrid cols={4}>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted mx-auto mb-2 flex items-center justify-center">
|
||||
<Receipt className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-3xl font-bold">${totalExpenses}</p>
|
||||
<p className="text-sm text-muted-foreground">Total Expenses</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">One-time</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted mx-auto mb-2 flex items-center justify-center">
|
||||
<TrendingUp className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-3xl font-bold">${monthlyCosts}</p>
|
||||
<p className="text-sm text-muted-foreground">Monthly Costs</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Recurring</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted mx-auto mb-2 flex items-center justify-center">
|
||||
<Calendar className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-3xl font-bold">${annualCosts}</p>
|
||||
<p className="text-sm text-muted-foreground">Annual Costs</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Projected</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted mx-auto mb-2 flex items-center justify-center">
|
||||
<DollarSign className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-3xl font-bold">$0</p>
|
||||
<p className="text-sm text-muted-foreground">Revenue</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Not launched</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
|
||||
{/* Expenses (One-time) */}
|
||||
<PageSection
|
||||
title="Expenses"
|
||||
description="One-time costs"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Expense
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-2">
|
||||
{SAMPLE_EXPENSES.map((expense) => (
|
||||
<div
|
||||
key={expense.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-muted/50"
|
||||
>
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Receipt className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{expense.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{expense.date}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-muted font-medium">
|
||||
{expense.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">${expense.amount}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Costs (Recurring) */}
|
||||
<PageSection
|
||||
title="Costs"
|
||||
description="Recurring/ongoing expenses"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Cost
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-2">
|
||||
{SAMPLE_COSTS.map((cost) => (
|
||||
<div
|
||||
key={cost.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-muted/50"
|
||||
>
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{cost.name}</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">{cost.frequency}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-muted font-medium">
|
||||
{cost.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">
|
||||
{cost.amount === 0 ? "Variable" : `$${cost.amount}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Pricing Strategy */}
|
||||
<PageSection
|
||||
title="Pricing"
|
||||
description="Your product pricing strategy"
|
||||
>
|
||||
<PageCard>
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<DollarSign className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm mb-4">
|
||||
Define your pricing tiers and revenue model
|
||||
</p>
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Pricing Plan
|
||||
</Button>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Plans (Revenue Tiers) */}
|
||||
<PageSection
|
||||
title="Plans"
|
||||
description="Subscription tiers and offerings"
|
||||
>
|
||||
<PageGrid cols={3}>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold text-lg mb-2">Free</h3>
|
||||
<p className="text-3xl font-bold mb-1">$0</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">per month</p>
|
||||
<ul className="text-sm space-y-2 text-left mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Basic features</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Community support</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard className="border-primary">
|
||||
<div className="text-center">
|
||||
<div className="inline-block px-2 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium mb-2">
|
||||
Popular
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">Pro</h3>
|
||||
<p className="text-3xl font-bold mb-1">$29</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">per month</p>
|
||||
<ul className="text-sm space-y-2 text-left mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>All features</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Priority support</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>API access</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold text-lg mb-2">Enterprise</h3>
|
||||
<p className="text-3xl font-bold mb-1">Custom</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">contact us</p>
|
||||
<ul className="text-sm space-y-2 text-left mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Unlimited everything</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Dedicated support</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-600" />
|
||||
</div>
|
||||
<span>Custom integrations</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import {
|
||||
ClipboardList,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Clock,
|
||||
Target,
|
||||
ListTodo,
|
||||
Calendar,
|
||||
Plus,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
} from "@/components/layout/page-template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const BUILD_PLAN_NAV_ITEMS = [
|
||||
{ title: "MVP Scope", icon: Target, href: "/build-plan" },
|
||||
{ title: "Backlog", icon: ListTodo, href: "/build-plan#backlog" },
|
||||
{ title: "Milestones", icon: Calendar, href: "/build-plan#milestones" },
|
||||
{ title: "Progress", icon: Clock, href: "/build-plan#progress" },
|
||||
];
|
||||
|
||||
const SAMPLE_MVP_FEATURES = [
|
||||
{ id: 1, title: "User Authentication", status: "completed", priority: "high" },
|
||||
{ id: 2, title: "Dashboard UI", status: "in_progress", priority: "high" },
|
||||
{ id: 3, title: "Core Feature Flow", status: "in_progress", priority: "high" },
|
||||
{ id: 4, title: "Payment Integration", status: "todo", priority: "medium" },
|
||||
{ id: 5, title: "Email Notifications", status: "todo", priority: "low" },
|
||||
];
|
||||
|
||||
const SAMPLE_BACKLOG = [
|
||||
{ id: 1, title: "Advanced Analytics", priority: "medium" },
|
||||
{ id: 2, title: "Team Collaboration", priority: "high" },
|
||||
{ id: 3, title: "API Access", priority: "low" },
|
||||
{ id: 4, title: "Mobile App", priority: "medium" },
|
||||
];
|
||||
|
||||
export default function BuildPlanPage() {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
const sidebarItems = BUILD_PLAN_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
const completedCount = SAMPLE_MVP_FEATURES.filter((f) => f.status === "completed").length;
|
||||
const totalCount = SAMPLE_MVP_FEATURES.length;
|
||||
const progressPercent = Math.round((completedCount / totalCount) * 100);
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
title: "Build Plan",
|
||||
description: "Track what needs to be built",
|
||||
items: sidebarItems,
|
||||
footer: (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{completedCount} of {totalCount} MVP features done
|
||||
</p>
|
||||
<div className="h-1 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
hero={{
|
||||
icon: ClipboardList,
|
||||
title: "Build Plan",
|
||||
description: "Manage your MVP scope and track progress",
|
||||
actions: [
|
||||
{
|
||||
label: "Generate Tasks",
|
||||
onClick: () => console.log("Generate tasks with AI"),
|
||||
icon: Sparkles,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{/* Progress Overview */}
|
||||
<PageSection>
|
||||
<PageGrid cols={4}>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-600 mx-auto mb-2" />
|
||||
<p className="text-3xl font-bold">{completedCount}</p>
|
||||
<p className="text-sm text-muted-foreground">Completed</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<Clock className="h-8 w-8 text-blue-600 mx-auto mb-2" />
|
||||
<p className="text-3xl font-bold">
|
||||
{SAMPLE_MVP_FEATURES.filter((f) => f.status === "in_progress").length}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">In Progress</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<Circle className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-3xl font-bold">
|
||||
{SAMPLE_MVP_FEATURES.filter((f) => f.status === "todo").length}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">To Do</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<Target className="h-8 w-8 text-primary mx-auto mb-2" />
|
||||
<p className="text-3xl font-bold">{progressPercent}%</p>
|
||||
<p className="text-sm text-muted-foreground">Progress</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
|
||||
{/* MVP Scope */}
|
||||
<PageSection
|
||||
title="MVP Scope"
|
||||
description="Features included in your minimum viable product"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Feature
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-2">
|
||||
{SAMPLE_MVP_FEATURES.map((feature) => (
|
||||
<div
|
||||
key={feature.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 p-3 rounded-lg border transition-all hover:border-primary/50",
|
||||
feature.status === "completed" && "bg-green-50/50 dark:bg-green-950/20"
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
{feature.status === "completed" && (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
)}
|
||||
{feature.status === "in_progress" && (
|
||||
<Clock className="h-5 w-5 text-blue-600" />
|
||||
)}
|
||||
{feature.status === "todo" && (
|
||||
<Circle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
"font-medium",
|
||||
feature.status === "completed" && "line-through text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{feature.title}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-1 rounded-full",
|
||||
feature.priority === "high" &&
|
||||
"bg-red-500/10 text-red-700 dark:text-red-400",
|
||||
feature.priority === "medium" &&
|
||||
"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
|
||||
feature.priority === "low" &&
|
||||
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
>
|
||||
{feature.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-1 rounded-full",
|
||||
feature.status === "completed" &&
|
||||
"bg-green-500/10 text-green-700 dark:text-green-400",
|
||||
feature.status === "in_progress" &&
|
||||
"bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||
feature.status === "todo" &&
|
||||
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
>
|
||||
{feature.status === "in_progress" ? "in progress" : feature.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Backlog */}
|
||||
<PageSection
|
||||
title="Backlog"
|
||||
description="Features for future iterations"
|
||||
headerAction={
|
||||
<Button size="sm" variant="ghost">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add to Backlog
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-2">
|
||||
{SAMPLE_BACKLOG.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border hover:border-primary/50 transition-all"
|
||||
>
|
||||
<ListTodo className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{item.title}</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-1 rounded-full",
|
||||
item.priority === "high" &&
|
||||
"bg-red-500/10 text-red-700 dark:text-red-400",
|
||||
item.priority === "medium" &&
|
||||
"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
|
||||
item.priority === "low" &&
|
||||
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
>
|
||||
{item.priority}
|
||||
</span>
|
||||
<Button size="sm" variant="ghost">
|
||||
Move to MVP
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Milestones */}
|
||||
<PageSection title="Milestones" description="Key dates and goals">
|
||||
<PageGrid cols={3}>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Alpha Release</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">Completed</p>
|
||||
<p className="text-xs text-muted-foreground">Jan 15, 2025</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard className="border-primary">
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-blue-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Clock className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Beta Launch</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">In Progress</p>
|
||||
<p className="text-xs text-muted-foreground">Feb 1, 2025</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
|
||||
<Target className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Public Launch</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">Planned</p>
|
||||
<p className="text-xs text-muted-foreground">Mar 1, 2025</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,768 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { use, useState, useEffect, useCallback } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2, CheckCircle2, Circle, Clock, RefreshCw, Eye, Cog, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { CollapsibleSidebar } from '@/components/ui/collapsible-sidebar';
|
||||
|
||||
interface WorkItem {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
path: string;
|
||||
status: 'built' | 'missing' | 'in_progress';
|
||||
priority: string;
|
||||
assigned?: string;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
duration: number;
|
||||
sessionsCount: number;
|
||||
commitsCount: number;
|
||||
totalActivity: number;
|
||||
estimatedCost?: number;
|
||||
requirements: Array<{
|
||||
id: number;
|
||||
text: string;
|
||||
status: 'built' | 'missing' | 'in_progress';
|
||||
}>;
|
||||
evidence: string[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
interface TimelineData {
|
||||
workItems: WorkItem[];
|
||||
timeline: {
|
||||
start: string;
|
||||
end: string;
|
||||
totalDays: number;
|
||||
};
|
||||
summary: {
|
||||
totalWorkItems: number;
|
||||
withActivity: number;
|
||||
noActivity: number;
|
||||
built: number;
|
||||
missing: number;
|
||||
};
|
||||
projectCreator?: string;
|
||||
}
|
||||
|
||||
export default function TimelinePlanPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { projectId } = use(params);
|
||||
const [data, setData] = useState<TimelineData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [regenerating, setRegenerating] = useState(false);
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const [viewMode, setViewMode] = useState<'touchpoints' | 'technical' | 'journey'>('touchpoints');
|
||||
const [collapsedJourneySections, setCollapsedJourneySections] = useState<Set<string>>(new Set());
|
||||
|
||||
// Map work items to types based on path and category
|
||||
const getWorkItemType = (item: WorkItem): string => {
|
||||
// API endpoints are System
|
||||
if (item.path.startsWith('/api/')) return 'System';
|
||||
|
||||
// Flows are Flow
|
||||
if (item.path.startsWith('flow/')) return 'Flow';
|
||||
|
||||
// Auth/OAuth is System
|
||||
if (item.path.includes('auth') || item.path.includes('oauth')) return 'System';
|
||||
|
||||
// Settings is System
|
||||
if (item.path.includes('settings')) return 'System';
|
||||
|
||||
// Marketing/Content pages
|
||||
if (item.category === 'Marketing' || item.category === 'Content') return 'Screen';
|
||||
|
||||
// Social
|
||||
if (item.category === 'Social') return 'Screen';
|
||||
|
||||
// Everything else is a Screen
|
||||
return 'Screen';
|
||||
};
|
||||
|
||||
// Determine if item is a user-facing touchpoint
|
||||
const isTouchpoint = (item: WorkItem): boolean => {
|
||||
const path = item.path.toLowerCase();
|
||||
const title = item.title.toLowerCase();
|
||||
|
||||
// Exclude APIs and backend systems
|
||||
if (path.startsWith('/api/')) return false;
|
||||
if (title.includes(' api') || title.includes('api ')) return false;
|
||||
|
||||
// Exclude pure auth infrastructure (OAuth endpoints)
|
||||
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return false;
|
||||
|
||||
// Include everything else - screens, pages, social posts, blogs, invites, etc.
|
||||
return true;
|
||||
};
|
||||
|
||||
// Determine if item is technical infrastructure
|
||||
const isTechnical = (item: WorkItem): boolean => {
|
||||
const path = item.path.toLowerCase();
|
||||
const title = item.title.toLowerCase();
|
||||
|
||||
// APIs and backend
|
||||
if (path.startsWith('/api/')) return true;
|
||||
if (title.includes(' api') || title.includes('api ')) return true;
|
||||
|
||||
// Auth infrastructure
|
||||
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return true;
|
||||
|
||||
// System settings
|
||||
if (item.category === 'Settings' && title.includes('api')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Map work items to customer lifecycle journey sections
|
||||
const getJourneySection = (item: WorkItem): string => {
|
||||
const title = item.title.toLowerCase();
|
||||
const path = item.path.toLowerCase();
|
||||
|
||||
// Discovery - "I just found you online via social post, blog article, advertisement"
|
||||
if (path === '/' || title.includes('landing') || title.includes('marketing page')) return 'Discovery';
|
||||
if (item.category === 'Social' && !path.includes('settings')) return 'Discovery';
|
||||
if (item.category === 'Content' && (title.includes('blog') || title.includes('article'))) return 'Discovery';
|
||||
|
||||
// Research - "Checking out your marketing website - features, price, home page"
|
||||
if (title.includes('marketing dashboard')) return 'Research';
|
||||
if (item.category === 'Marketing' && path !== '/') return 'Research';
|
||||
if (path.includes('/features') || path.includes('/pricing') || path.includes('/about')) return 'Research';
|
||||
if (item.category === 'Content' && path.includes('/docs') && !title.includes('getting started')) return 'Research';
|
||||
|
||||
// Onboarding - "Creating an account to try the product for the first time"
|
||||
if (path.includes('auth') || path.includes('oauth')) return 'Onboarding';
|
||||
if (path.includes('signup') || path.includes('signin') || path.includes('login')) return 'Onboarding';
|
||||
if (title.includes('authentication') && !title.includes('api')) return 'Onboarding';
|
||||
|
||||
// First Use - "Zero state to experiencing the magic solution"
|
||||
if (title.includes('onboarding')) return 'First Use';
|
||||
if (title.includes('getting started')) return 'First Use';
|
||||
if (path.includes('workspace') && !path.includes('settings')) return 'First Use';
|
||||
if (title.includes('creation flow') || title.includes('project creation')) return 'First Use';
|
||||
if (path.includes('/projects') && path.match(/\/projects\/?$/)) return 'First Use'; // Projects list page
|
||||
|
||||
// Active - "I've seen the magic and come back to use it again and again"
|
||||
if (path.includes('overview') || path.includes('/dashboard')) return 'Active';
|
||||
if (path.includes('timeline-plan') || path.includes('audit') || path.includes('mission')) return 'Active';
|
||||
if (path.includes('/api/projects') || path.includes('mvp-checklist')) return 'Active';
|
||||
if (title.includes('plan generation') || title.includes('marketing plan')) return 'Active';
|
||||
if (path.includes('projects/') && path.length > '/projects/'.length) return 'Active'; // Specific project pages
|
||||
|
||||
// Support - "I've got questions, need quick answers to get back to the magic"
|
||||
if (path.includes('settings')) return 'Support';
|
||||
if (path.includes('/help') || path.includes('/faq') || path.includes('/support')) return 'Support';
|
||||
if (item.category === 'Content' && path.includes('/docs') && title.includes('help')) return 'Support';
|
||||
|
||||
// Purchase - "Time to pay so I can keep using the magic"
|
||||
if (path.includes('billing') || path.includes('payment') || path.includes('subscription')) return 'Purchase';
|
||||
if (path.includes('upgrade') || path.includes('checkout') || path.includes('pricing/buy')) return 'Purchase';
|
||||
|
||||
// Default to Active for core product features
|
||||
return 'Active';
|
||||
};
|
||||
|
||||
const toggleJourneySection = (sectionId: string) => {
|
||||
setCollapsedJourneySections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sectionId)) {
|
||||
newSet.delete(sectionId);
|
||||
} else {
|
||||
newSet.add(sectionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Get emoji icon for journey section
|
||||
const getJourneySectionIcon = (section: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
'Discovery': '🔍',
|
||||
'Research': '📚',
|
||||
'Onboarding': '🎯',
|
||||
'First Use': '🚀',
|
||||
'Active': '⚡',
|
||||
'Support': '💡',
|
||||
'Purchase': '💳'
|
||||
};
|
||||
return icons[section] || '📋';
|
||||
};
|
||||
|
||||
// Get phase status based on overall item status
|
||||
const getPhaseStatus = (itemStatus: string, phase: 'scope' | 'design' | 'code'): 'built' | 'in_progress' | 'missing' => {
|
||||
if (itemStatus === 'built') return 'built';
|
||||
if (itemStatus === 'missing') return 'missing';
|
||||
|
||||
// If in_progress, show progression through phases
|
||||
if (phase === 'scope') return 'built';
|
||||
if (phase === 'design') return 'in_progress';
|
||||
return 'missing';
|
||||
};
|
||||
|
||||
// Render status badge
|
||||
const renderStatusBadge = (status: 'built' | 'in_progress' | 'missing') => {
|
||||
if (status === 'built') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs font-medium">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Done
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'in_progress') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 text-blue-800 text-xs font-medium">
|
||||
<Clock className="h-3 w-3" />
|
||||
Started
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs font-medium">
|
||||
<Circle className="h-3 w-3" />
|
||||
To-do
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const loadTimelineData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
|
||||
const result = await response.json();
|
||||
|
||||
// Check if the response is an error
|
||||
if (result.error) {
|
||||
console.error('API Error:', result.error, result.details);
|
||||
alert(`Failed to load timeline: ${result.details || result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
console.error('Error loading timeline:', error);
|
||||
alert('Failed to load timeline data. Check console for details.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTimelineData();
|
||||
}, [loadTimelineData]);
|
||||
|
||||
const regeneratePlan = async () => {
|
||||
if (!confirm('Regenerate the plan? This will analyze your project and create a fresh MVP checklist.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setRegenerating(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to regenerate plan');
|
||||
}
|
||||
|
||||
// Reload the timeline data
|
||||
await loadTimelineData();
|
||||
} catch (error) {
|
||||
console.error('Error regenerating plan:', error);
|
||||
alert('Failed to regenerate plan. Check console for details.');
|
||||
} finally {
|
||||
setRegenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="p-8 text-center text-muted-foreground">No timeline data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-background overflow-hidden flex">
|
||||
{/* Left Sidebar */}
|
||||
<CollapsibleSidebar>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Quick Stats</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total Items</span>
|
||||
<span className="font-medium">{data.summary.totalWorkItems}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Built</span>
|
||||
<span className="font-medium text-green-600">{data.summary.built}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">In Progress</span>
|
||||
<span className="font-medium text-blue-600">{data.summary.withActivity - data.summary.built}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">To Build</span>
|
||||
<span className="font-medium text-gray-600">{data.summary.missing}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSidebar>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col p-4 space-y-3 overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">MVP Checklist</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{data.summary.built} of {data.summary.totalWorkItems} pages built •
|
||||
{data.summary.withActivity} with development activity
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex items-center border rounded-lg p-1">
|
||||
<Button
|
||||
variant={viewMode === 'touchpoints' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('touchpoints')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Touchpoints
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'technical' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('technical')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Cog className="h-4 w-4" />
|
||||
Technical
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'journey' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('journey')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Journey
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Regenerate Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={regeneratePlan}
|
||||
disabled={regenerating}
|
||||
className="gap-2"
|
||||
>
|
||||
{regenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Regenerating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Regenerate Plan
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded">
|
||||
✅ {data.summary.built} Built
|
||||
</div>
|
||||
<div className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded">
|
||||
⏳ {data.summary.missing} To Build
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Touchpoints View - What users see and engage with */}
|
||||
{viewMode === 'touchpoints' && (
|
||||
<Card className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Everything users see and engage with - screens, features, social posts, blogs, invites, and all customer-facing elements.</p>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Touchpoint</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
|
||||
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data?.workItems.filter(item => isTouchpoint(item)).map((item, index) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="hover:bg-accent/50 cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{getWorkItemType(item)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{item.title}</div>
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{item.assigned || data?.projectCreator || 'You'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.sessionsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.commitsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
|
||||
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Journey View - Customer lifecycle stages */}
|
||||
{viewMode === 'journey' && (
|
||||
<Card className="flex-1 overflow-auto p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Customer lifecycle journey from discovery to purchase - organizing all touchpoints and technical components by user stage.</p>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{/* Journey Sections - Customer Lifecycle */}
|
||||
{['Discovery', 'Research', 'Onboarding', 'First Use', 'Active', 'Support', 'Purchase'].map(sectionName => {
|
||||
const sectionItems = data.workItems.filter(item => getJourneySection(item) === sectionName);
|
||||
if (sectionItems.length === 0) return null;
|
||||
|
||||
const sectionStats = {
|
||||
done: sectionItems.filter(i => i.status === 'built').length,
|
||||
started: sectionItems.filter(i => i.status === 'in_progress').length,
|
||||
todo: sectionItems.filter(i => i.status === 'missing').length,
|
||||
total: sectionItems.length
|
||||
};
|
||||
|
||||
const isCollapsed = collapsedJourneySections.has(sectionName);
|
||||
|
||||
return (
|
||||
<div key={sectionName}>
|
||||
{/* Section Header */}
|
||||
<div
|
||||
className="bg-muted/30 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between sticky top-0 z-10"
|
||||
onClick={() => toggleJourneySection(sectionName)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-lg">{getJourneySectionIcon(sectionName)}</span>
|
||||
<h3 className="font-semibold text-base">{sectionName}</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{sectionStats.done}/{sectionStats.total} complete
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs">
|
||||
{sectionStats.done > 0 && (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-800 rounded">
|
||||
{sectionStats.done} done
|
||||
</span>
|
||||
)}
|
||||
{sectionStats.started > 0 && (
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded">
|
||||
{sectionStats.started} started
|
||||
</span>
|
||||
)}
|
||||
{sectionStats.todo > 0 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-800 rounded">
|
||||
{sectionStats.todo} to-do
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Items */}
|
||||
{!isCollapsed && (
|
||||
<div className="divide-y">
|
||||
{sectionItems.map(item => (
|
||||
<div key={item.id} className="px-4 py-3 hover:bg-accent/30 transition-colors">
|
||||
<div
|
||||
className="flex items-start justify-between cursor-pointer"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{/* Status Icon */}
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<Circle className="h-5 w-5 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{/* Title and Type */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{item.title}</span>
|
||||
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
|
||||
{getWorkItemType(item)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Phase Status */}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Spec:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Design:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Code:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Requirements */}
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-3 space-y-1 pl-4 border-l-2 border-gray-200">
|
||||
<p className="text-xs font-semibold text-muted-foreground mb-2">Requirements:</p>
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side Stats */}
|
||||
<div className="flex items-start gap-4 text-xs text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="font-medium">Sessions</div>
|
||||
<div className={item.sessionsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.sessionsCount}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-medium">Commits</div>
|
||||
<div className={item.commitsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.commitsCount}</div>
|
||||
</div>
|
||||
<div className="text-center min-w-[60px]">
|
||||
<div className="font-medium">Cost</div>
|
||||
<div>{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Technical View - Infrastructure that powers everything */}
|
||||
{viewMode === 'technical' && (
|
||||
<Card className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Technical infrastructure that powers the product - APIs, backend services, authentication, and system integrations.</p>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Technical Component</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
|
||||
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data?.workItems.filter(item => isTechnical(item)).map((item, index) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="hover:bg-accent/50 cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{getWorkItemType(item)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{item.title}</div>
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{item.assigned || data?.projectCreator || 'You'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.sessionsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.commitsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
|
||||
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
{/* End Main Content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import {
|
||||
Code2,
|
||||
Globe,
|
||||
Server,
|
||||
MessageSquare,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
} from "@/components/layout/page-template";
|
||||
|
||||
const PRODUCT_NAV_ITEMS = [
|
||||
{ title: "Code", icon: Code2, href: "/code" },
|
||||
{ title: "Website", icon: Globe, href: "/product#website" },
|
||||
{ title: "Chat Agent", icon: MessageSquare, href: "/product#agent" },
|
||||
{ title: "Deployment", icon: Server, href: "/product#deployment" },
|
||||
];
|
||||
|
||||
export default function ProductPage() {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
const sidebarItems = PRODUCT_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
}}
|
||||
>
|
||||
{/* Quick Navigation Cards */}
|
||||
<PageSection>
|
||||
<PageGrid cols={2}>
|
||||
{PRODUCT_NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
||||
|
||||
return (
|
||||
<a key={item.href} href={fullHref}>
|
||||
<PageCard hover>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Icon className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold mb-1">{item.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.title === "Code" &&
|
||||
"Browse codebase, manage repositories"}
|
||||
{item.title === "Website" &&
|
||||
"Marketing site, landing pages"}
|
||||
{item.title === "Chat Agent" &&
|
||||
"Conversational AI interface"}
|
||||
{item.title === "Deployment" &&
|
||||
"Hosting, CI/CD, environments"}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
||||
</div>
|
||||
</PageCard>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
|
||||
{/* Code Section */}
|
||||
<PageSection
|
||||
title="Code"
|
||||
description="Your application codebase"
|
||||
>
|
||||
<PageCard>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center">
|
||||
<Code2 className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Browse Repository</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
View files, commits, and code structure
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href={`/${workspace}/project/${projectId}/code`}>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
||||
</a>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Website Section */}
|
||||
<PageSection
|
||||
title="Website"
|
||||
description="Marketing site and landing pages"
|
||||
>
|
||||
<PageCard>
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Globe className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">
|
||||
Manage your marketing website and landing pages
|
||||
</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Chat Agent Section */}
|
||||
<PageSection
|
||||
title="Chat Agent"
|
||||
description="Conversational AI interface"
|
||||
>
|
||||
<PageCard>
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">
|
||||
Configure and manage your AI chat agent
|
||||
</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Deployment Section */}
|
||||
<PageSection
|
||||
title="Deployment"
|
||||
description="Hosting and CI/CD"
|
||||
>
|
||||
<PageGrid cols={3}>
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
|
||||
<Server className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Production</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">Live</p>
|
||||
<p className="text-xs text-muted-foreground">vercel.app</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
|
||||
<Server className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Staging</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">Preview</p>
|
||||
<p className="text-xs text-muted-foreground">staging.vercel.app</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard>
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
|
||||
<Server className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Development</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">Local</p>
|
||||
<p className="text-xs text-muted-foreground">localhost:3000</p>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ListChecks, Clock, DollarSign, GitBranch, ExternalLink, User } from "lucide-react";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
|
||||
// Mock data
|
||||
const PROGRESS_ITEMS = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Implemented Product Vision page with file upload",
|
||||
description: "Created dynamic layout system with file upload capabilities for ChatGPT exports",
|
||||
contributor: "Mark Henderson",
|
||||
date: "2025-11-11",
|
||||
time: "2h 15m",
|
||||
tokens: 45000,
|
||||
cost: 0.68,
|
||||
github_link: "https://github.com/user/repo/commit/abc123",
|
||||
type: "feature"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Updated left rail navigation structure",
|
||||
description: "Refactored navigation to remove rounded edges and improve active state",
|
||||
contributor: "Mark Henderson",
|
||||
date: "2025-11-11",
|
||||
time: "45m",
|
||||
tokens: 12000,
|
||||
cost: 0.18,
|
||||
github_link: "https://github.com/user/repo/commit/def456",
|
||||
type: "improvement"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Added section summaries to Overview page",
|
||||
description: "Created cards for Product Vision, Progress, UI UX, Code, Deployment, and Automation",
|
||||
contributor: "Mark Henderson",
|
||||
date: "2025-11-11",
|
||||
time: "1h 30m",
|
||||
tokens: 32000,
|
||||
cost: 0.48,
|
||||
github_link: "https://github.com/user/repo/commit/ghi789",
|
||||
type: "feature"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Fixed database connection issues",
|
||||
description: "Resolved connection pooling and error handling in API routes",
|
||||
contributor: "Mark Henderson",
|
||||
date: "2025-11-10",
|
||||
time: "30m",
|
||||
tokens: 8000,
|
||||
cost: 0.12,
|
||||
github_link: "https://github.com/user/repo/commit/jkl012",
|
||||
type: "fix"
|
||||
},
|
||||
];
|
||||
|
||||
export default async function ProgressPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ projectId: string }>;
|
||||
}) {
|
||||
const { projectId } = await params;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
projectId={projectId}
|
||||
projectName="AI Proxy"
|
||||
projectEmoji="🤖"
|
||||
pageName="Progress"
|
||||
/>
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
{/* Hero Section */}
|
||||
<div className="border-b bg-gradient-to-r from-green-500/5 to-green-500/10 p-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-500/10">
|
||||
<ListChecks className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Progress</h1>
|
||||
<p className="text-muted-foreground">Development activity and velocity</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 p-6">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
{/* Summary Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">Total Items</div>
|
||||
<div className="text-2xl font-bold">{PROGRESS_ITEMS.length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">Total Time</div>
|
||||
<div className="text-2xl font-bold">5h 0m</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">Total Cost</div>
|
||||
<div className="text-2xl font-bold">$1.46</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">Total Tokens</div>
|
||||
<div className="text-2xl font-bold">97K</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Progress List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Development Activity</CardTitle>
|
||||
<CardDescription>Sorted by latest</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Latest
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
Cost
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
Time
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{PROGRESS_ITEMS.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 rounded-lg border p-4 hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
{/* Header Row */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<Badge variant={
|
||||
item.type === 'feature' ? 'default' :
|
||||
item.type === 'fix' ? 'destructive' :
|
||||
'secondary'
|
||||
} className="text-xs">
|
||||
{item.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata Row */}
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
{/* Contributor */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{item.contributor}</span>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{item.time}</span>
|
||||
</div>
|
||||
|
||||
{/* Tokens */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-muted-foreground">{item.tokens.toLocaleString()} tokens</span>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">${item.cost.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link */}
|
||||
<div className="ml-auto">
|
||||
<Button variant="ghost" size="sm" className="h-7" asChild>
|
||||
<a href={item.github_link} target="_blank" rel="noopener noreferrer">
|
||||
<GitBranch className="mr-1.5 h-3.5 w-3.5" />
|
||||
Commit
|
||||
<ExternalLink className="ml-1.5 h-3 w-3" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(item.date).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
LayoutGrid,
|
||||
Settings,
|
||||
Users,
|
||||
BarChart,
|
||||
Box,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
} from "@/components/layout/page-template";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const SANDBOX_NAV_ITEMS = [
|
||||
{ title: "Nav Item 1", icon: LayoutGrid, href: "#item1" },
|
||||
{ title: "Nav Item 2", icon: Box, href: "#item2" },
|
||||
{ title: "Nav Item 3", icon: Users, href: "#item3" },
|
||||
{ title: "Nav Item 4", icon: Settings, href: "#item4" },
|
||||
];
|
||||
|
||||
export default function SandboxPage() {
|
||||
// Mock navigation items for the sidebar
|
||||
const sidebarItems = SANDBOX_NAV_ITEMS.map((item) => ({
|
||||
...item,
|
||||
href: item.href,
|
||||
isActive: item.title === "Nav Item 1", // Mock active state
|
||||
badge: item.title === "Nav Item 2" ? "New" : undefined,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
customContent: (
|
||||
<div className="space-y-4">
|
||||
<div className="px-2 py-1 bg-dashed border border-dashed border-muted-foreground/30 rounded text-xs text-center text-muted-foreground uppercase tracking-wider">
|
||||
Custom Component Area
|
||||
</div>
|
||||
|
||||
{/* Mock Custom Component Example */}
|
||||
<div className="space-y-2 opacity-70">
|
||||
<h3 className="text-sm font-medium">Example Widget</h3>
|
||||
<div className="p-3 rounded bg-muted/50 text-xs text-muted-foreground">
|
||||
This area fills the remaining sidebar height and can hold any custom React component (checklists, filters, etc).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{/* Empty Main Content Area */}
|
||||
<div className="border-2 border-dashed border-muted-foreground/20 rounded-lg h-[400px] flex items-center justify-center text-muted-foreground">
|
||||
<p>Main Content Area (Empty)</p>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Clock, DollarSign, MessageSquare } from "lucide-react";
|
||||
import type { Session, DashboardStats } from "@/lib/types";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
|
||||
async function getSessions(projectId: string): Promise<Session[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/sessions?projectId=${projectId}&limit=20`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!res.ok) throw new Error('Failed to fetch sessions');
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching sessions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getStats(projectId: string): Promise<DashboardStats> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/stats?projectId=${projectId}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!res.ok) throw new Error('Failed to fetch stats');
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
return {
|
||||
totalSessions: 0,
|
||||
totalCost: 0,
|
||||
totalTokens: 0,
|
||||
totalFeatures: 0,
|
||||
completedFeatures: 0,
|
||||
totalDuration: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SessionsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
const [sessions, stats] = await Promise.all([
|
||||
getSessions(projectId),
|
||||
getStats(projectId),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
projectId={projectId}
|
||||
projectName="Project"
|
||||
projectEmoji="📦"
|
||||
pageName="Sessions"
|
||||
/>
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
{/* Stats Section */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Sessions</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Track all your AI coding sessions
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
New Session
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Sessions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.totalSessions}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Duration
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">
|
||||
{Math.round(stats.totalDuration / 60)}h
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Cost
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">
|
||||
${stats.totalCost.toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sessions List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Sessions</CardTitle>
|
||||
<CardDescription>
|
||||
Your AI coding activity with conversation history
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sessions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mb-4 rounded-full bg-muted p-3">
|
||||
<Activity className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No sessions yet</h3>
|
||||
<p className="text-sm text-center text-muted-foreground max-w-sm">
|
||||
Start coding with AI and your sessions will appear here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="flex items-center justify-between rounded-lg border p-4 hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="rounded-full bg-primary/10 p-2">
|
||||
<Activity className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{session.summary || `Session ${session.session_id.substring(0, 8)}...`}
|
||||
</h3>
|
||||
<div className="mt-1 flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{session.duration_minutes} min
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
{session.message_count} messages
|
||||
</span>
|
||||
{session.estimated_cost_usd && (
|
||||
<span className="flex items-center gap-1">
|
||||
<DollarSign className="h-3 w-3" />
|
||||
${session.estimated_cost_usd.toFixed(3)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
{session.primary_ai_model && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{session.primary_ai_model}
|
||||
</Badge>
|
||||
)}
|
||||
{session.ide_name && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{session.ide_name}
|
||||
</Badge>
|
||||
)}
|
||||
{session.github_branch && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{session.github_branch}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Clock, DollarSign, MessageSquare } from "lucide-react";
|
||||
import type { Session, DashboardStats } from "@/lib/types";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
|
||||
async function getSessions(projectId: string): Promise<Session[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`http://localhost:3000/api/sessions?projectId=${projectId}&limit=20`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!res.ok) throw new Error('Failed to fetch sessions');
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching sessions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getStats(projectId: string): Promise<DashboardStats> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`http://localhost:3000/api/stats?projectId=${projectId}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!res.ok) throw new Error('Failed to fetch stats');
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
return {
|
||||
totalSessions: 0,
|
||||
totalCost: 0,
|
||||
totalTokens: 0,
|
||||
totalFeatures: 0,
|
||||
completedFeatures: 0,
|
||||
totalDuration: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SessionsPage({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string };
|
||||
}) {
|
||||
const [sessions, stats] = await Promise.all([
|
||||
getSessions(params.projectId),
|
||||
getStats(params.projectId),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
projectId={params.projectId}
|
||||
projectName="AI Proxy"
|
||||
projectEmoji="🤖"
|
||||
pageName="Sessions"
|
||||
/>
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
{/* Stats Section */}
|
||||
<div className="border-b bg-card/50 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Sessions</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Track all your AI coding sessions
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
New Session
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Sessions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.totalSessions}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Duration
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">
|
||||
{Math.round(stats.totalDuration / 60)}h
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Cost
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">
|
||||
${stats.totalCost.toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sessions List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Sessions</CardTitle>
|
||||
<CardDescription>
|
||||
Your AI coding activity with conversation history
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sessions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mb-4 rounded-full bg-muted p-3">
|
||||
<Activity className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No sessions yet</h3>
|
||||
<p className="text-sm text-center text-muted-foreground max-w-sm">
|
||||
Start coding with AI and your sessions will appear here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="flex items-center justify-between rounded-lg border p-4 hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="rounded-full bg-primary/10 p-2">
|
||||
<Activity className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{session.summary || `Session ${session.session_id.substring(0, 8)}...`}
|
||||
</h3>
|
||||
<div className="mt-1 flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{session.duration_minutes} min
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
{session.message_count} messages
|
||||
</span>
|
||||
{session.estimated_cost_usd && (
|
||||
<span className="flex items-center gap-1">
|
||||
<DollarSign className="h-3 w-3" />
|
||||
${session.estimated_cost_usd.toFixed(3)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
{session.primary_ai_model && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{session.primary_ai_model}
|
||||
</Badge>
|
||||
)}
|
||||
{session.ide_name && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{session.ide_name}
|
||||
</Badge>
|
||||
)}
|
||||
{session.github_branch && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{session.github_branch}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Cog,
|
||||
Database,
|
||||
Github,
|
||||
Globe,
|
||||
Server,
|
||||
Code2,
|
||||
ExternalLink,
|
||||
Plus,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Clock,
|
||||
Key,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
|
||||
|
||||
interface WorkItem {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
status: "built" | "in_progress" | "missing";
|
||||
category: string;
|
||||
sessionsCount: number;
|
||||
commitsCount: number;
|
||||
estimatedCost?: number;
|
||||
}
|
||||
|
||||
interface TechResource {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "firebase" | "github" | "domain" | "api";
|
||||
status: "active" | "inactive";
|
||||
url?: string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export default function TechPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = use(params);
|
||||
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
|
||||
const [resources, setResources] = useState<TechResource[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadTechData();
|
||||
}, [projectId]);
|
||||
|
||||
const loadTechData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Filter for technical items only
|
||||
const techItems = data.workItems.filter((item: WorkItem) =>
|
||||
isTechnical(item)
|
||||
);
|
||||
setWorkItems(techItems);
|
||||
}
|
||||
|
||||
// Mock resources data
|
||||
setResources([
|
||||
{
|
||||
id: "1",
|
||||
name: "Firebase Project",
|
||||
type: "firebase",
|
||||
status: "active",
|
||||
url: "https://console.firebase.google.com",
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "GitHub Repository",
|
||||
type: "github",
|
||||
status: "active",
|
||||
url: "https://github.com",
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading tech data:", error);
|
||||
toast.error("Failed to load tech data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isTechnical = (item: WorkItem): boolean => {
|
||||
const path = item.path.toLowerCase();
|
||||
const title = item.title.toLowerCase();
|
||||
|
||||
// APIs and backend
|
||||
if (path.startsWith('/api/')) return true;
|
||||
if (title.includes(' api') || title.includes('api ')) return true;
|
||||
|
||||
// Auth infrastructure
|
||||
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return true;
|
||||
|
||||
// System settings
|
||||
if (item.category === 'Settings' && title.includes('api')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
if (status === "built" || status === "active") return <CheckCircle2 className="h-4 w-4 text-green-600" />;
|
||||
if (status === "in_progress") return <Clock className="h-4 w-4 text-blue-600" />;
|
||||
return <Circle className="h-4 w-4 text-gray-400" />;
|
||||
};
|
||||
|
||||
const getResourceIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "firebase":
|
||||
return <Zap className="h-5 w-5 text-orange-600" />;
|
||||
case "github":
|
||||
return <Github className="h-5 w-5 text-gray-900" />;
|
||||
case "domain":
|
||||
return <Globe className="h-5 w-5 text-blue-600" />;
|
||||
case "api":
|
||||
return <Code2 className="h-5 w-5 text-purple-600" />;
|
||||
default:
|
||||
return <Server className="h-5 w-5 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-background overflow-hidden flex">
|
||||
{/* Left Sidebar */}
|
||||
<CollapsibleSidebar>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Infrastructure</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Resources</span>
|
||||
<span className="font-medium">{resources.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Active</span>
|
||||
<span className="font-medium text-green-600">{resources.filter(r => r.status === 'active').length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Work Items</span>
|
||||
<span className="font-medium">{workItems.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSidebar>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Cog className="h-6 w-6" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Tech Infrastructure</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
APIs, services, and technical resources
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Resource
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-4 space-y-6">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Infrastructure Resources */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">Infrastructure Resources</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{resources.map((resource) => (
|
||||
<Card key={resource.id} className="hover:bg-accent/30 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getResourceIcon(resource.type)}
|
||||
<CardTitle className="text-base">{resource.name}</CardTitle>
|
||||
</div>
|
||||
{getStatusIcon(resource.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Badge variant="secondary" className="text-xs capitalize">
|
||||
{resource.type}
|
||||
</Badge>
|
||||
{resource.lastUpdated && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Updated {new Date(resource.lastUpdated).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
{resource.url && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
onClick={() => window.open(resource.url, "_blank")}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open Console
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Technical Work Items */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">Technical Work Items</h2>
|
||||
<Badge variant="secondary">{workItems.length} items</Badge>
|
||||
</div>
|
||||
|
||||
{workItems.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Code2 className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No technical items yet</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Technical items include APIs, services, and infrastructure
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{workItems.map((item) => (
|
||||
<Card key={item.id} className="p-4 hover:bg-accent/30 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{getStatusIcon(item.status)}
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
{/* Title and Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.status === "built" ? "Active" : item.status === "in_progress" ? "In Progress" : "Planned"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Path */}
|
||||
<p className="text-sm text-muted-foreground font-mono">
|
||||
{item.path}
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>{item.sessionsCount} sessions</span>
|
||||
<span>•</span>
|
||||
<span>{item.commitsCount} commits</span>
|
||||
{item.estimatedCost && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>${item.estimatedCost.toFixed(2)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => toast.info("Documentation coming soon")}
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{item.path.startsWith('/api/') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => toast.info("API testing coming soon")}
|
||||
>
|
||||
<Code2 className="h-4 w-4" />
|
||||
Test API
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">Quick Links</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Card className="p-4 hover:bg-accent/30 cursor-pointer transition-colors">
|
||||
<Link href={`/${workspace}/keys`} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Key className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">API Keys</p>
|
||||
<p className="text-xs text-muted-foreground">Manage service credentials</p>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 hover:bg-accent/30 cursor-pointer transition-colors">
|
||||
<a
|
||||
href="https://console.firebase.google.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="h-5 w-5 text-orange-600" />
|
||||
<div>
|
||||
<p className="font-medium">Firebase Console</p>
|
||||
<p className="text-xs text-muted-foreground">Manage database & hosting</p>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</a>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 hover:bg-accent/30 cursor-pointer transition-colors">
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Github className="h-5 w-5 text-gray-900" />
|
||||
<div>
|
||||
<p className="font-medium">GitHub</p>
|
||||
<p className="text-xs text-muted-foreground">Code repository & CI/CD</p>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</a>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 hover:bg-accent/30 cursor-pointer transition-colors">
|
||||
<a
|
||||
href="https://vercel.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">Deployment</p>
|
||||
<p className="text-xs text-muted-foreground">Production & preview deploys</p>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Main Content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,736 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { use, useState, useEffect, useCallback } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2, CheckCircle2, Circle, Clock, RefreshCw, Eye, Cog, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface WorkItem {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
path: string;
|
||||
status: 'built' | 'missing' | 'in_progress';
|
||||
priority: string;
|
||||
assigned?: string;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
duration: number;
|
||||
sessionsCount: number;
|
||||
commitsCount: number;
|
||||
totalActivity: number;
|
||||
estimatedCost?: number;
|
||||
requirements: Array<{
|
||||
id: number;
|
||||
text: string;
|
||||
status: 'built' | 'missing' | 'in_progress';
|
||||
}>;
|
||||
evidence: string[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
interface TimelineData {
|
||||
workItems: WorkItem[];
|
||||
timeline: {
|
||||
start: string;
|
||||
end: string;
|
||||
totalDays: number;
|
||||
};
|
||||
summary: {
|
||||
totalWorkItems: number;
|
||||
withActivity: number;
|
||||
noActivity: number;
|
||||
built: number;
|
||||
missing: number;
|
||||
};
|
||||
projectCreator?: string;
|
||||
}
|
||||
|
||||
export default function TimelinePlanPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { projectId } = use(params);
|
||||
const [data, setData] = useState<TimelineData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [regenerating, setRegenerating] = useState(false);
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const [viewMode, setViewMode] = useState<'touchpoints' | 'technical' | 'journey'>('touchpoints');
|
||||
const [collapsedJourneySections, setCollapsedJourneySections] = useState<Set<string>>(new Set());
|
||||
|
||||
// Map work items to types based on path and category
|
||||
const getWorkItemType = (item: WorkItem): string => {
|
||||
// API endpoints are System
|
||||
if (item.path.startsWith('/api/')) return 'System';
|
||||
|
||||
// Flows are Flow
|
||||
if (item.path.startsWith('flow/')) return 'Flow';
|
||||
|
||||
// Auth/OAuth is System
|
||||
if (item.path.includes('auth') || item.path.includes('oauth')) return 'System';
|
||||
|
||||
// Settings is System
|
||||
if (item.path.includes('settings')) return 'System';
|
||||
|
||||
// Marketing/Content pages
|
||||
if (item.category === 'Marketing' || item.category === 'Content') return 'Screen';
|
||||
|
||||
// Social
|
||||
if (item.category === 'Social') return 'Screen';
|
||||
|
||||
// Everything else is a Screen
|
||||
return 'Screen';
|
||||
};
|
||||
|
||||
// Determine if item is a user-facing touchpoint
|
||||
const isTouchpoint = (item: WorkItem): boolean => {
|
||||
const path = item.path.toLowerCase();
|
||||
const title = item.title.toLowerCase();
|
||||
|
||||
// Exclude APIs and backend systems
|
||||
if (path.startsWith('/api/')) return false;
|
||||
if (title.includes(' api') || title.includes('api ')) return false;
|
||||
|
||||
// Exclude pure auth infrastructure (OAuth endpoints)
|
||||
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return false;
|
||||
|
||||
// Include everything else - screens, pages, social posts, blogs, invites, etc.
|
||||
return true;
|
||||
};
|
||||
|
||||
// Determine if item is technical infrastructure
|
||||
const isTechnical = (item: WorkItem): boolean => {
|
||||
const path = item.path.toLowerCase();
|
||||
const title = item.title.toLowerCase();
|
||||
|
||||
// APIs and backend
|
||||
if (path.startsWith('/api/')) return true;
|
||||
if (title.includes(' api') || title.includes('api ')) return true;
|
||||
|
||||
// Auth infrastructure
|
||||
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return true;
|
||||
|
||||
// System settings
|
||||
if (item.category === 'Settings' && title.includes('api')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Map work items to customer lifecycle journey sections
|
||||
const getJourneySection = (item: WorkItem): string => {
|
||||
const title = item.title.toLowerCase();
|
||||
const path = item.path.toLowerCase();
|
||||
|
||||
// Discovery - "I just found you online via social post, blog article, advertisement"
|
||||
if (path === '/' || title.includes('landing') || title.includes('marketing page')) return 'Discovery';
|
||||
if (item.category === 'Social' && !path.includes('settings')) return 'Discovery';
|
||||
if (item.category === 'Content' && (title.includes('blog') || title.includes('article'))) return 'Discovery';
|
||||
|
||||
// Research - "Checking out your marketing website - features, price, home page"
|
||||
if (title.includes('marketing dashboard')) return 'Research';
|
||||
if (item.category === 'Marketing' && path !== '/') return 'Research';
|
||||
if (path.includes('/features') || path.includes('/pricing') || path.includes('/about')) return 'Research';
|
||||
if (item.category === 'Content' && path.includes('/docs') && !title.includes('getting started')) return 'Research';
|
||||
|
||||
// Onboarding - "Creating an account to try the product for the first time"
|
||||
if (path.includes('auth') || path.includes('oauth')) return 'Onboarding';
|
||||
if (path.includes('signup') || path.includes('signin') || path.includes('login')) return 'Onboarding';
|
||||
if (title.includes('authentication') && !title.includes('api')) return 'Onboarding';
|
||||
|
||||
// First Use - "Zero state to experiencing the magic solution"
|
||||
if (title.includes('onboarding')) return 'First Use';
|
||||
if (title.includes('getting started')) return 'First Use';
|
||||
if (path.includes('workspace') && !path.includes('settings')) return 'First Use';
|
||||
if (title.includes('creation flow') || title.includes('project creation')) return 'First Use';
|
||||
if (path.includes('/projects') && path.match(/\/projects\/?$/)) return 'First Use'; // Projects list page
|
||||
|
||||
// Active - "I've seen the magic and come back to use it again and again"
|
||||
if (path.includes('overview') || path.includes('/dashboard')) return 'Active';
|
||||
if (path.includes('timeline-plan') || path.includes('audit') || path.includes('mission')) return 'Active';
|
||||
if (path.includes('/api/projects') || path.includes('mvp-checklist')) return 'Active';
|
||||
if (title.includes('plan generation') || title.includes('marketing plan')) return 'Active';
|
||||
if (path.includes('projects/') && path.length > '/projects/'.length) return 'Active'; // Specific project pages
|
||||
|
||||
// Support - "I've got questions, need quick answers to get back to the magic"
|
||||
if (path.includes('settings')) return 'Support';
|
||||
if (path.includes('/help') || path.includes('/faq') || path.includes('/support')) return 'Support';
|
||||
if (item.category === 'Content' && path.includes('/docs') && title.includes('help')) return 'Support';
|
||||
|
||||
// Purchase - "Time to pay so I can keep using the magic"
|
||||
if (path.includes('billing') || path.includes('payment') || path.includes('subscription')) return 'Purchase';
|
||||
if (path.includes('upgrade') || path.includes('checkout') || path.includes('pricing/buy')) return 'Purchase';
|
||||
|
||||
// Default to Active for core product features
|
||||
return 'Active';
|
||||
};
|
||||
|
||||
const toggleJourneySection = (sectionId: string) => {
|
||||
setCollapsedJourneySections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sectionId)) {
|
||||
newSet.delete(sectionId);
|
||||
} else {
|
||||
newSet.add(sectionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Get emoji icon for journey section
|
||||
const getJourneySectionIcon = (section: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
'Discovery': '🔍',
|
||||
'Research': '📚',
|
||||
'Onboarding': '🎯',
|
||||
'First Use': '🚀',
|
||||
'Active': '⚡',
|
||||
'Support': '💡',
|
||||
'Purchase': '💳'
|
||||
};
|
||||
return icons[section] || '📋';
|
||||
};
|
||||
|
||||
// Get phase status based on overall item status
|
||||
const getPhaseStatus = (itemStatus: string, phase: 'scope' | 'design' | 'code'): 'built' | 'in_progress' | 'missing' => {
|
||||
if (itemStatus === 'built') return 'built';
|
||||
if (itemStatus === 'missing') return 'missing';
|
||||
|
||||
// If in_progress, show progression through phases
|
||||
if (phase === 'scope') return 'built';
|
||||
if (phase === 'design') return 'in_progress';
|
||||
return 'missing';
|
||||
};
|
||||
|
||||
// Render status badge
|
||||
const renderStatusBadge = (status: 'built' | 'in_progress' | 'missing') => {
|
||||
if (status === 'built') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs font-medium">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Done
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'in_progress') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 text-blue-800 text-xs font-medium">
|
||||
<Clock className="h-3 w-3" />
|
||||
Started
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs font-medium">
|
||||
<Circle className="h-3 w-3" />
|
||||
To-do
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const loadTimelineData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
|
||||
const result = await response.json();
|
||||
|
||||
// Check if the response is an error
|
||||
if (result.error) {
|
||||
console.error('API Error:', result.error, result.details);
|
||||
alert(`Failed to load timeline: ${result.details || result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
console.error('Error loading timeline:', error);
|
||||
alert('Failed to load timeline data. Check console for details.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTimelineData();
|
||||
}, [loadTimelineData]);
|
||||
|
||||
const regeneratePlan = async () => {
|
||||
if (!confirm('Regenerate the plan? This will analyze your project and create a fresh MVP checklist.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setRegenerating(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to regenerate plan');
|
||||
}
|
||||
|
||||
// Reload the timeline data
|
||||
await loadTimelineData();
|
||||
} catch (error) {
|
||||
console.error('Error regenerating plan:', error);
|
||||
alert('Failed to regenerate plan. Check console for details.');
|
||||
} finally {
|
||||
setRegenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="p-8 text-center text-muted-foreground">No timeline data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4 space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">MVP Checklist</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{data.summary.built} of {data.summary.totalWorkItems} pages built •
|
||||
{data.summary.withActivity} with development activity
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex items-center border rounded-lg p-1">
|
||||
<Button
|
||||
variant={viewMode === 'touchpoints' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('touchpoints')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Touchpoints
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'technical' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('technical')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Cog className="h-4 w-4" />
|
||||
Technical
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'journey' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('journey')}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Journey
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Regenerate Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={regeneratePlan}
|
||||
disabled={regenerating}
|
||||
className="gap-2"
|
||||
>
|
||||
{regenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Regenerating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Regenerate Plan
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded">
|
||||
✅ {data.summary.built} Built
|
||||
</div>
|
||||
<div className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded">
|
||||
⏳ {data.summary.missing} To Build
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Touchpoints View - What users see and engage with */}
|
||||
{viewMode === 'touchpoints' && (
|
||||
<Card className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Everything users see and engage with - screens, features, social posts, blogs, invites, and all customer-facing elements.</p>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Touchpoint</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
|
||||
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data?.workItems.filter(item => isTouchpoint(item)).map((item, index) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="hover:bg-accent/50 cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{getWorkItemType(item)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{item.title}</div>
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{item.assigned || data?.projectCreator || 'You'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.sessionsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.commitsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
|
||||
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Journey View - Customer lifecycle stages */}
|
||||
{viewMode === 'journey' && (
|
||||
<Card className="flex-1 overflow-auto p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Customer lifecycle journey from discovery to purchase - organizing all touchpoints and technical components by user stage.</p>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{/* Journey Sections - Customer Lifecycle */}
|
||||
{['Discovery', 'Research', 'Onboarding', 'First Use', 'Active', 'Support', 'Purchase'].map(sectionName => {
|
||||
const sectionItems = data.workItems.filter(item => getJourneySection(item) === sectionName);
|
||||
if (sectionItems.length === 0) return null;
|
||||
|
||||
const sectionStats = {
|
||||
done: sectionItems.filter(i => i.status === 'built').length,
|
||||
started: sectionItems.filter(i => i.status === 'in_progress').length,
|
||||
todo: sectionItems.filter(i => i.status === 'missing').length,
|
||||
total: sectionItems.length
|
||||
};
|
||||
|
||||
const isCollapsed = collapsedJourneySections.has(sectionName);
|
||||
|
||||
return (
|
||||
<div key={sectionName}>
|
||||
{/* Section Header */}
|
||||
<div
|
||||
className="bg-muted/30 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between sticky top-0 z-10"
|
||||
onClick={() => toggleJourneySection(sectionName)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-lg">{getJourneySectionIcon(sectionName)}</span>
|
||||
<h3 className="font-semibold text-base">{sectionName}</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{sectionStats.done}/{sectionStats.total} complete
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs">
|
||||
{sectionStats.done > 0 && (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-800 rounded">
|
||||
{sectionStats.done} done
|
||||
</span>
|
||||
)}
|
||||
{sectionStats.started > 0 && (
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded">
|
||||
{sectionStats.started} started
|
||||
</span>
|
||||
)}
|
||||
{sectionStats.todo > 0 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-800 rounded">
|
||||
{sectionStats.todo} to-do
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Items */}
|
||||
{!isCollapsed && (
|
||||
<div className="divide-y">
|
||||
{sectionItems.map(item => (
|
||||
<div key={item.id} className="px-4 py-3 hover:bg-accent/30 transition-colors">
|
||||
<div
|
||||
className="flex items-start justify-between cursor-pointer"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{/* Status Icon */}
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<Circle className="h-5 w-5 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{/* Title and Type */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{item.title}</span>
|
||||
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
|
||||
{getWorkItemType(item)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Phase Status */}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Spec:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Design:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-muted-foreground">Code:</span>{' '}
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Requirements */}
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-3 space-y-1 pl-4 border-l-2 border-gray-200">
|
||||
<p className="text-xs font-semibold text-muted-foreground mb-2">Requirements:</p>
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side Stats */}
|
||||
<div className="flex items-start gap-4 text-xs text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="font-medium">Sessions</div>
|
||||
<div className={item.sessionsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.sessionsCount}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-medium">Commits</div>
|
||||
<div className={item.commitsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.commitsCount}</div>
|
||||
</div>
|
||||
<div className="text-center min-w-[60px]">
|
||||
<div className="font-medium">Cost</div>
|
||||
<div>{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Technical View - Infrastructure that powers everything */}
|
||||
{viewMode === 'technical' && (
|
||||
<Card className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">Technical infrastructure that powers the product - APIs, backend services, authentication, and system integrations.</p>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Technical Component</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
|
||||
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
|
||||
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
|
||||
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data?.workItems.filter(item => isTechnical(item)).map((item, index) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="hover:bg-accent/50 cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(item.id)) {
|
||||
newSet.delete(item.id);
|
||||
} else {
|
||||
newSet.add(item.id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{getWorkItemType(item)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.status === 'built' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
) : item.status === 'in_progress' ? (
|
||||
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{item.title}</div>
|
||||
{expandedItems.has(item.id) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{item.requirements.map((req) => (
|
||||
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
|
||||
{req.status === 'built' ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Circle className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
<span>{req.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{item.assigned || data?.projectCreator || 'You'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.sessionsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
|
||||
{item.commitsCount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
|
||||
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user