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:
2026-03-02 19:22:13 -08:00
parent ecdeee9f1a
commit 33ec7b787f
82 changed files with 0 additions and 19583 deletions

View File

@@ -1,33 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
import { Toaster } from "sonner";
export default function ConnectionsLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("connections");
return (
<>
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -1,360 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Github, CheckCircle2, Download, Copy, Check, Eye, EyeOff } from "lucide-react";
import { CursorIcon } from "@/components/icons/custom-icons";
import { toast } from "sonner";
import { auth } from "@/lib/firebase/config";
import type { User } from "firebase/auth";
import { MCPConnectionCard } from "@/components/mcp-connection-card";
import { ChatGPTImportCard } from "@/components/chatgpt-import-card";
export default function ConnectionsPage() {
const [githubConnected, setGithubConnected] = useState(false);
const [extensionInstalled] = useState(false); // Future use: track extension installation
const [copiedApiKey, setCopiedApiKey] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
const [apiKey, setApiKey] = useState<string | null>(null);
const [loadingApiKey, setLoadingApiKey] = useState(true);
const [apiUrl, setApiUrl] = useState('https://vibnai.com');
// Set API URL on client side to avoid hydration mismatch
useEffect(() => {
if (typeof window !== 'undefined') {
setApiUrl(window.location.origin);
}
}, []);
// Fetch API key on mount
useEffect(() => {
async function fetchApiKey(user: User) {
try {
console.log('[Client] Getting ID token for user:', user.uid);
const token = await user.getIdToken();
console.log('[Client] Token received, length:', token.length);
const response = await fetch('/api/user/api-key', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
console.log('[Client] Response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('[Client] API key received');
setApiKey(data.apiKey);
} else {
const errorData = await response.json();
console.error('[Client] Failed to fetch API key:', response.status, errorData);
}
} catch (error) {
console.error('[Client] Error fetching API key:', error);
} finally {
setLoadingApiKey(false);
}
}
// Listen for auth state changes
const unsubscribe = auth.onAuthStateChanged((user) => {
if (user) {
fetchApiKey(user);
} else {
setLoadingApiKey(false);
}
});
return () => unsubscribe();
}, []);
const handleConnectGitHub = async () => {
// TODO: Implement GitHub OAuth flow
toast.success("GitHub connected successfully!");
setGithubConnected(true);
};
const handleInstallExtension = () => {
// Link to Cursor Monitor extension (update with actual marketplace URL when published)
window.open("https://marketplace.visualstudio.com/items?itemName=cursor-monitor", "_blank");
};
const handleCopyApiKey = () => {
if (apiKey) {
navigator.clipboard.writeText(apiKey);
setCopiedApiKey(true);
toast.success("API key copied to clipboard!");
setTimeout(() => setCopiedApiKey(false), 2000);
}
};
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 Tools</h1>
<p className="text-muted-foreground text-lg">
Set up your development tools to unlock the full power of Vib&apos;n
</p>
</div>
{/* Connection Cards */}
<div className="space-y-6">
{/* Cursor Extension */}
<Card className={extensionInstalled ? "border-green-500/50 bg-green-500/5" : ""}>
<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>
<div className="flex items-center gap-2">
<CardTitle>Cursor Monitor Extension</CardTitle>
{extensionInstalled && (
<CheckCircle2 className="h-4 w-4 text-green-600" />
)}
</div>
<CardDescription>
Automatically track your coding sessions, AI usage, and costs
</CardDescription>
</div>
</div>
{!extensionInstalled ? (
<Button onClick={handleInstallExtension}>
<Download className="h-4 w-4 mr-2" />
Get Extension
</Button>
) : (
<Button variant="outline" disabled>
<CheckCircle2 className="h-4 w-4 mr-2" />
Installed
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">What it does:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Tracks your coding sessions in real-time</li>
<li>Monitors AI model usage and token consumption</li>
<li>Logs file changes and conversation history</li>
<li>Calculates costs automatically</li>
</ul>
</div>
{!extensionInstalled && (
<>
<div className="rounded-lg bg-muted p-4 space-y-2">
<p className="text-sm font-medium">Installation Steps:</p>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground ml-2">
<li>Install the Cursor Monitor extension from the marketplace</li>
<li>Restart Cursor to activate the extension</li>
<li>Configure your API key (see instructions below)</li>
<li>Start coding - sessions will be tracked automatically!</li>
</ol>
</div>
<div className="rounded-lg bg-primary/10 border border-primary/20 p-4 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-medium">Your API Key</p>
{!loadingApiKey && apiKey && (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleCopyApiKey}
>
{copiedApiKey ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
)}
</div>
{loadingApiKey ? (
<div className="text-sm text-muted-foreground">Loading...</div>
) : apiKey ? (
<>
<Input
type={showApiKey ? "text" : "password"}
value={apiKey}
readOnly
className="font-mono text-xs"
/>
<p className="text-xs text-muted-foreground">
Add this key to your extension settings to connect it to your Vibn account.
</p>
</>
) : (
<p className="text-sm text-muted-foreground">
Sign in to generate your API key
</p>
)}
</div>
<div className="rounded-lg bg-muted p-4 space-y-2">
<p className="text-sm font-medium">Configure Cursor Monitor Extension:</p>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground ml-2">
<li>Open Cursor Settings (Cmd/Ctrl + ,)</li>
<li>Search for &quot;Cursor Monitor&quot;</li>
<li>Find &quot;Cursor Monitor: Vibn Api Key&quot;</li>
<li>Paste your API key (from above)</li>
<li>Verify &quot;Cursor Monitor: Vibn Api Url&quot; is set to: <code className="text-xs bg-background px-1 py-0.5 rounded">{apiUrl}/api</code></li>
<li>Make sure &quot;Cursor Monitor: Vibn Enabled&quot; is checked</li>
<li>Reload Cursor to start tracking</li>
</ol>
</div>
</>
)}
{extensionInstalled && (
<div className="rounded-lg bg-green-500/10 border border-green-500/20 p-4">
<p className="text-sm text-green-700 dark:text-green-400">
Extension is installed and tracking your sessions
</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* GitHub Connection */}
<Card className={githubConnected ? "border-green-500/50 bg-green-500/5" : ""}>
<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>
<div className="flex items-center gap-2">
<CardTitle>GitHub</CardTitle>
{githubConnected && (
<CheckCircle2 className="h-4 w-4 text-green-600" />
)}
</div>
<CardDescription>
Connect your repositories for automatic analysis
</CardDescription>
</div>
</div>
{!githubConnected ? (
<Button onClick={handleConnectGitHub}>
<Github className="h-4 w-4 mr-2" />
Connect GitHub
</Button>
) : (
<Button variant="outline" disabled>
<CheckCircle2 className="h-4 w-4 mr-2" />
Connected
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">What we&apos;ll access:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Read your repository code and structure</li>
<li>Access repository metadata and commit history</li>
<li>Analyze tech stack and dependencies</li>
<li>Identify project architecture patterns</li>
</ul>
</div>
{!githubConnected && (
<div className="rounded-lg bg-muted p-4 space-y-2">
<p className="text-sm font-medium">Why connect GitHub?</p>
<p className="text-sm text-muted-foreground">
Our AI will analyze your codebase to understand your tech stack,
architecture, and features. This helps generate better documentation
and provides smarter insights.
</p>
</div>
)}
{githubConnected && (
<div className="rounded-lg bg-green-500/10 border border-green-500/20 p-4">
<p className="text-sm text-green-700 dark:text-green-400">
GitHub connected - Your repositories are ready for analysis
</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* ChatGPT (MCP) Connection */}
<MCPConnectionCard />
{/* ChatGPT Import */}
<ChatGPTImportCard />
</div>
{/* Status Summary */}
{(githubConnected || extensionInstalled) && (
<Card className="bg-primary/5 border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Connection Status</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<CursorIcon className="h-4 w-4" />
Cursor Extension
</span>
{extensionInstalled ? (
<span className="text-green-600 flex items-center gap-1">
<CheckCircle2 className="h-4 w-4" />
Installed
</span>
) : (
<span className="text-muted-foreground">Not installed</span>
)}
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<Github className="h-4 w-4" />
GitHub
</span>
{githubConnected ? (
<span className="text-green-600 flex items-center gap-1">
<CheckCircle2 className="h-4 w-4" />
Connected
</span>
) : (
<span className="text-muted-foreground">Not connected</span>
)}
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
}

View File

@@ -1,34 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
import { Toaster } from "sonner";
export default function CostsLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("costs");
return (
<>
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -1,181 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { DollarSign, TrendingUp, TrendingDown, Calendar } from 'lucide-react';
import { useParams } from 'next/navigation';
interface CostData {
total: number;
thisMonth: number;
lastMonth: number;
byProject: Array<{
projectId: string;
projectName: string;
cost: number;
}>;
byDate: Array<{
date: string;
cost: number;
}>;
}
export default function CostsPage() {
const params = useParams();
const workspace = params.workspace as string;
const [costs, setCosts] = useState<CostData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadCosts();
}, []);
const loadCosts = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/costs', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setCosts(data);
}
} catch (error) {
console.error('Error loading costs:', error);
toast.error('Failed to load cost data');
} finally {
setLoading(false);
}
};
const percentageChange = costs && costs.lastMonth > 0
? ((costs.thisMonth - costs.lastMonth) / costs.lastMonth) * 100
: 0;
return (
<div className="flex h-full flex-col overflow-auto">
<div className="flex-1 p-8 space-y-8 max-w-7xl">
{/* Header */}
<div>
<h1 className="text-4xl font-bold mb-2">Costs</h1>
<p className="text-muted-foreground text-lg">
Track your AI usage costs across all projects
</p>
</div>
{/* Summary Cards */}
{loading ? (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading cost data...</p>
</CardContent>
</Card>
) : (
<>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Costs</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${costs?.total.toFixed(2) || '0.00'}</div>
<p className="text-xs text-muted-foreground">All time</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">This Month</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${costs?.thisMonth.toFixed(2) || '0.00'}</div>
<p className="text-xs text-muted-foreground">
{new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">vs Last Month</CardTitle>
{percentageChange >= 0 ? (
<TrendingUp className="h-4 w-4 text-red-500" />
) : (
<TrendingDown className="h-4 w-4 text-green-500" />
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{percentageChange >= 0 ? '+' : ''}{percentageChange.toFixed(1)}%
</div>
<p className="text-xs text-muted-foreground">
Last month: ${costs?.lastMonth.toFixed(2) || '0.00'}
</p>
</CardContent>
</Card>
</div>
{/* Costs by Project */}
<Card>
<CardHeader>
<CardTitle>Costs by Project</CardTitle>
<CardDescription>Your spending broken down by project</CardDescription>
</CardHeader>
<CardContent>
{costs?.byProject && costs.byProject.length > 0 ? (
<div className="space-y-3">
{costs.byProject.map((project) => (
<div key={project.projectId} className="flex items-center justify-between p-3 rounded-lg border">
<div>
<p className="font-medium">{project.projectName}</p>
<p className="text-sm text-muted-foreground">Project ID: {project.projectId}</p>
</div>
<div className="text-right">
<p className="text-lg font-semibold">${project.cost.toFixed(2)}</p>
<p className="text-xs text-muted-foreground">
{((project.cost / (costs.total || 1)) * 100).toFixed(1)}% of total
</p>
</div>
</div>
))}
</div>
) : (
<p className="text-center text-muted-foreground py-8">No project costs yet</p>
)}
</CardContent>
</Card>
{/* Info Card */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="text-base">About Cost Tracking</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
<strong>📊 Automatic Tracking:</strong> All AI API costs are automatically tracked when you use Vibn features.
</p>
<p>
<strong>💰 Your Keys, Your Costs:</strong> Costs reflect usage of your own API keys - Vibn doesn't add any markup.
</p>
<p>
<strong>📈 Project Attribution:</strong> Costs are attributed to projects based on session metadata.
</p>
</CardContent>
</Card>
</>
)}
</div>
</div>
);
}

View File

@@ -1,239 +0,0 @@
"use client";
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { auth, db } from '@/lib/firebase/config';
import { collection, query, where, getDocs } from 'firebase/firestore';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
interface ProjectDebugInfo {
id: string;
productName: string;
name: string;
slug: string;
userId: string;
workspacePath?: string;
createdAt: any;
updatedAt: any;
}
export default function DebugProjectsPage() {
const [projects, setProjects] = useState<ProjectDebugInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [userId, setUserId] = useState<string>('');
const loadProjects = async () => {
setLoading(true);
setError(null);
try {
const user = auth.currentUser;
if (!user) {
setError('Not authenticated');
return;
}
setUserId(user.uid);
const projectsRef = collection(db, 'projects');
const projectsQuery = query(
projectsRef,
where('userId', '==', user.uid)
);
const snapshot = await getDocs(projectsQuery);
const projectsData = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
productName: data.productName || 'N/A',
name: data.name || 'N/A',
slug: data.slug || 'N/A',
userId: data.userId || 'N/A',
workspacePath: data.workspacePath,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
};
});
console.log('DEBUG: All projects from Firebase:', projectsData);
setProjects(projectsData);
} catch (err: any) {
console.error('Error loading projects:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
if (user) {
loadProjects();
} else {
setError('Please sign in');
setLoading(false);
}
});
return () => unsubscribe();
}, []);
return (
<div className="min-h-screen p-8 bg-background">
<div className="max-w-6xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">🔍 Projects Debug Page</h1>
<p className="text-muted-foreground mt-2">
View all your projects and their unique IDs from Firebase
</p>
{userId && (
<p className="text-xs text-muted-foreground mt-1">
User ID: <code className="bg-muted px-2 py-1 rounded">{userId}</code>
</p>
)}
</div>
<Button onClick={loadProjects} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{error && (
<Card className="border-red-500">
<CardContent className="pt-6">
<p className="text-red-600">Error: {error}</p>
</CardContent>
</Card>
)}
{loading && !error && (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading projects from Firebase...</p>
</CardContent>
</Card>
)}
{!loading && !error && (
<>
<Card>
<CardHeader>
<CardTitle>Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">Total Projects</p>
<p className="text-2xl font-bold">{projects.length}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Unique IDs</p>
<p className="text-2xl font-bold">
{new Set(projects.map(p => p.id)).size}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Duplicate IDs?</p>
<p className={`text-2xl font-bold ${projects.length !== new Set(projects.map(p => p.id)).size ? 'text-red-500' : 'text-green-500'}`}>
{projects.length !== new Set(projects.map(p => p.id)).size ? 'YES ⚠️' : 'NO ✓'}
</p>
</div>
</div>
</CardContent>
</Card>
<div className="space-y-4">
<h2 className="text-xl font-semibold">All Projects</h2>
{projects.map((project, index) => (
<Card key={project.id + index}>
<CardHeader>
<CardTitle className="text-lg flex items-center justify-between">
<span>#{index + 1}: {project.productName}</span>
<a
href={`/marks-account/project/${project.id}/overview`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
Open Overview
</a>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Project ID</p>
<code className="block bg-muted px-2 py-1 rounded mt-1 break-all">
{project.id}
</code>
</div>
<div>
<p className="text-muted-foreground">Slug</p>
<code className="block bg-muted px-2 py-1 rounded mt-1 break-all">
{project.slug}
</code>
</div>
<div>
<p className="text-muted-foreground">Product Name</p>
<p className="font-medium mt-1">{project.productName}</p>
</div>
<div>
<p className="text-muted-foreground">Internal Name</p>
<p className="font-medium mt-1">{project.name}</p>
</div>
{project.workspacePath && (
<div className="col-span-2">
<p className="text-muted-foreground">Workspace Path</p>
<code className="block bg-muted px-2 py-1 rounded mt-1 break-all text-xs">
{project.workspacePath}
</code>
</div>
)}
</div>
<div className="flex gap-2 pt-2">
<Button
size="sm"
variant="outline"
onClick={() => {
navigator.clipboard.writeText(project.id);
alert('Project ID copied to clipboard!');
}}
>
Copy ID
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
const url = `/marks-account/project/${project.id}/v_ai_chat`;
window.open(url, '_blank');
}}
>
Open Chat
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
console.log('Full project data:', project);
alert('Check browser console for full data');
}}
>
Log to Console
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -1,279 +0,0 @@
"use client";
import { useEffect, useState, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { auth, db } from '@/lib/firebase/config';
import { collection, query, where, getDocs, orderBy, limit } from 'firebase/firestore';
import { RefreshCw, CheckCircle2, AlertCircle, Link as LinkIcon } from 'lucide-react';
interface SessionDebugInfo {
id: string;
projectId?: string;
workspacePath?: string;
workspaceName?: string;
needsProjectAssociation: boolean;
model?: string;
tokensUsed?: number;
cost?: number;
createdAt: any;
}
export default function DebugSessionsPage() {
const [sessions, setSessions] = useState<SessionDebugInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [userId, setUserId] = useState<string>('');
const loadSessions = useCallback(async () => {
setLoading(true);
setError(null);
try {
const user = auth.currentUser;
if (!user) {
setError('Not authenticated');
setLoading(false);
return;
}
setUserId(user.uid);
const sessionsRef = collection(db, 'sessions');
// Remove orderBy to avoid index issues - just get recent sessions
const sessionsQuery = query(
sessionsRef,
where('userId', '==', user.uid),
limit(50)
);
const snapshot = await getDocs(sessionsQuery);
const sessionsData = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
projectId: data.projectId || null,
workspacePath: data.workspacePath || null,
workspaceName: data.workspaceName || null,
needsProjectAssociation: data.needsProjectAssociation || false,
model: data.model,
tokensUsed: data.tokensUsed,
cost: data.cost,
createdAt: data.createdAt,
};
});
console.log('DEBUG: All sessions from Firebase:', sessionsData);
setSessions(sessionsData);
} catch (err: any) {
console.error('Error loading sessions:', err);
setError(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
let mounted = true;
const unsubscribe = auth.onAuthStateChanged((user) => {
if (!mounted) return;
if (user) {
loadSessions();
} else {
setError('Please sign in');
setLoading(false);
}
});
return () => {
mounted = false;
unsubscribe();
};
}, [loadSessions]);
const unassociatedSessions = sessions.filter(s => s.needsProjectAssociation);
const associatedSessions = sessions.filter(s => !s.needsProjectAssociation);
// Group unassociated sessions by workspace path
const sessionsByWorkspace = unassociatedSessions.reduce((acc, session) => {
const path = session.workspacePath || 'No workspace path';
if (!acc[path]) acc[path] = [];
acc[path].push(session);
return acc;
}, {} as Record<string, SessionDebugInfo[]>);
return (
<div className="min-h-screen p-8 bg-background">
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">🔍 Sessions Debug Page</h1>
<p className="text-muted-foreground mt-2">
View all your chat sessions and their workspace paths
</p>
{userId && (
<p className="text-xs text-muted-foreground mt-1">
User ID: <code className="bg-muted px-2 py-1 rounded">{userId}</code>
</p>
)}
</div>
<Button onClick={loadSessions} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{error && (
<Card className="border-red-500">
<CardContent className="pt-6">
<p className="text-red-600">Error: {error}</p>
</CardContent>
</Card>
)}
{loading && !error && (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading sessions...</p>
</CardContent>
</Card>
)}
{!loading && !error && (
<>
{/* Summary */}
<Card>
<CardHeader>
<CardTitle>Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">Total Sessions</p>
<p className="text-2xl font-bold">{sessions.length}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Linked to Projects</p>
<p className="text-2xl font-bold text-green-600">{associatedSessions.length}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Unassociated (Available)</p>
<p className="text-2xl font-bold text-orange-600">{unassociatedSessions.length}</p>
</div>
</div>
</CardContent>
</Card>
{/* Unassociated Sessions by Workspace */}
{unassociatedSessions.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-orange-600" />
Unassociated Sessions (Available to Link)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(sessionsByWorkspace).map(([path, workspaceSessions]) => {
const folderName = path !== 'No workspace path' ? path.split('/').pop() : null;
return (
<div key={path} className="border rounded-lg p-4">
<div className="mb-3">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold">📁 {folderName || 'Unknown folder'}</p>
<code className="text-xs text-muted-foreground break-all">{path}</code>
</div>
<div className="text-right">
<p className="text-2xl font-bold">{workspaceSessions.length}</p>
<p className="text-xs text-muted-foreground">sessions</p>
</div>
</div>
</div>
<div className="space-y-2">
{workspaceSessions.slice(0, 3).map((session) => (
<div key={session.id} className="text-xs bg-muted/50 p-2 rounded">
<div className="flex justify-between">
<span className="font-mono">{session.id.substring(0, 12)}...</span>
<span>{session.model || 'unknown'}</span>
</div>
<div className="text-muted-foreground">
{session.tokensUsed?.toLocaleString()} tokens ${session.cost?.toFixed(4)}
</div>
</div>
))}
{workspaceSessions.length > 3 && (
<p className="text-xs text-muted-foreground">
+ {workspaceSessions.length - 3} more sessions...
</p>
)}
</div>
<div className="mt-3 p-3 bg-blue-50 dark:bg-blue-950/20 rounded text-sm">
<p className="text-blue-600 dark:text-blue-400 font-medium mb-1">
💡 To link these sessions:
</p>
<ol className="text-xs text-muted-foreground space-y-1 ml-4 list-decimal">
<li>Create a project with workspace path: <code className="bg-muted px-1 rounded">{path}</code></li>
<li>OR connect GitHub to a project that already has this workspace path set</li>
</ol>
<p className="text-xs text-muted-foreground mt-2">
Folder name: <code className="bg-muted px-1 rounded">{folderName}</code>
</p>
</div>
</div>
);
})}
</CardContent>
</Card>
)}
{/* Associated Sessions */}
{associatedSessions.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-600" />
Linked Sessions
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
These sessions are already linked to projects
</p>
<div className="space-y-2">
{associatedSessions.slice(0, 5).map((session) => (
<div key={session.id} className="flex items-center justify-between p-2 border rounded text-sm">
<div>
<code className="text-xs">{session.id.substring(0, 12)}...</code>
<p className="text-xs text-muted-foreground">
{session.workspaceName || 'No workspace'}
</p>
</div>
<div className="text-right">
<LinkIcon className="h-4 w-4 text-green-600" />
<p className="text-xs text-muted-foreground">
Project: {session.projectId?.substring(0, 8)}...
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{sessions.length === 0 && (
<Card>
<CardContent className="pt-6 text-center">
<p className="text-muted-foreground">No sessions found. Start coding with Cursor to track sessions!</p>
</CardContent>
</Card>
)}
</>
)}
</div>
</div>
);
}

View File

@@ -1,34 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
import { Toaster } from "sonner";
export default function KeysLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("keys");
return (
<>
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -1,412 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Key, Plus, Trash2, Eye, EyeOff, ExternalLink, Save } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface ApiKey {
id: string;
service: string;
name: string;
createdAt: any;
lastUsed: any;
}
const SUPPORTED_SERVICES = [
{
id: 'openai',
name: 'OpenAI',
description: 'For ChatGPT imports and AI features',
placeholder: 'sk-...',
helpUrl: 'https://platform.openai.com/api-keys',
},
{
id: 'github',
name: 'GitHub',
description: 'Personal access token for repository access',
placeholder: 'ghp_...',
helpUrl: 'https://github.com/settings/tokens',
},
{
id: 'anthropic',
name: 'Anthropic (Claude)',
description: 'For Claude AI integrations',
placeholder: 'sk-ant-...',
helpUrl: 'https://console.anthropic.com/settings/keys',
},
];
export default function KeysPage() {
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [showAddDialog, setShowAddDialog] = useState(false);
const [selectedService, setSelectedService] = useState('');
const [keyValue, setKeyValue] = useState('');
const [showKey, setShowKey] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadKeys();
}, []);
const loadKeys = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setKeys(data.keys);
}
} catch (error) {
console.error('Error loading keys:', error);
toast.error('Failed to load API keys');
} finally {
setLoading(false);
}
};
const handleAddKey = async () => {
if (!selectedService || !keyValue) {
toast.error('Please select a service and enter a key');
return;
}
setSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
const token = await user.getIdToken();
const service = SUPPORTED_SERVICES.find(s => s.id === selectedService);
const response = await fetch('/api/keys', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
service: selectedService,
name: service?.name,
keyValue,
}),
});
if (response.ok) {
toast.success(`${service?.name} key saved successfully`);
setShowAddDialog(false);
setSelectedService('');
setKeyValue('');
loadKeys();
} else {
const error = await response.json();
toast.error(error.error || 'Failed to save key');
}
} catch (error) {
console.error('Error saving key:', error);
toast.error('Failed to save key');
} finally {
setSaving(false);
}
};
const handleDeleteKey = async (service: string, name: string) => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ service }),
});
if (response.ok) {
toast.success(`${name} key deleted`);
loadKeys();
} else {
toast.error('Failed to delete key');
}
} catch (error) {
console.error('Error deleting key:', error);
toast.error('Failed to delete key');
}
};
const getServiceConfig = (serviceId: string) => {
return SUPPORTED_SERVICES.find(s => s.id === serviceId);
};
return (
<div className="flex h-full flex-col overflow-auto">
<div className="flex-1 p-8 space-y-8 max-w-4xl">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold mb-2">API Keys</h1>
<p className="text-muted-foreground text-lg">
Manage your third-party API keys for Vibn integrations
</p>
</div>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Key
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add API Key</DialogTitle>
<DialogDescription>
Add a third-party API key for Vibn to use on your behalf
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="service">Service</Label>
<Select value={selectedService} onValueChange={setSelectedService}>
<SelectTrigger>
<SelectValue placeholder="Select a service" />
</SelectTrigger>
<SelectContent>
{SUPPORTED_SERVICES.map(service => (
<SelectItem key={service.id} value={service.id}>
{service.name}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedService && (
<p className="text-xs text-muted-foreground">
{getServiceConfig(selectedService)?.description}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="key">API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="key"
type={showKey ? 'text' : 'password'}
placeholder={getServiceConfig(selectedService)?.placeholder || 'Enter API key'}
value={keyValue}
onChange={(e) => setKeyValue(e.target.value)}
className="pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowKey(!showKey)}
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{selectedService && (
<a
href={getServiceConfig(selectedService)?.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Get your API key <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
<div className="rounded-lg border bg-muted/50 p-3">
<p className="text-sm text-muted-foreground">
<strong>🔐 Secure Storage:</strong> Your API key will be encrypted and stored securely.
Vibn will only use it when you explicitly request actions that require it.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddKey} disabled={saving || !selectedService || !keyValue}>
{saving ? 'Saving...' : 'Save Key'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Keys List */}
{loading ? (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading your API keys...</p>
</CardContent>
</Card>
) : keys.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center space-y-4">
<div className="flex justify-center">
<div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Key className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">No API keys yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Add your third-party API keys to enable Vibn features like ChatGPT imports and AI analysis
</p>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
Add Your First Key
</Button>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{keys.map((key) => {
const serviceConfig = getServiceConfig(key.service);
return (
<Card key={key.id}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Key className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-base">{key.name}</CardTitle>
<CardDescription>
{serviceConfig?.description || key.service}
</CardDescription>
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete API Key?</AlertDialogTitle>
<AlertDialogDescription>
This will remove your {key.name} API key. Features using this key will stop working until you add a new one.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDeleteKey(key.service, key.name)}>
Delete Key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<div className="space-y-1">
<p className="text-muted-foreground">
Added: {key.createdAt ? new Date(key.createdAt._seconds * 1000).toLocaleDateString() : 'Unknown'}
</p>
{key.lastUsed && (
<p className="text-muted-foreground">
Last used: {new Date(key.lastUsed._seconds * 1000).toLocaleDateString()}
</p>
)}
</div>
{serviceConfig && (
<a
href={serviceConfig.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Manage on {serviceConfig.name} <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{/* Info Card */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="text-base">How API Keys Work</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
<strong>🔐 Encrypted Storage:</strong> All API keys are encrypted before being stored in the database.
</p>
<p>
<strong>🎯 Automatic Usage:</strong> When you use Vibn features (like ChatGPT import), we'll automatically use your stored keys instead of asking each time.
</p>
<p>
<strong>🔄 Easy Updates:</strong> Add a new key with the same service name to replace an existing one.
</p>
<p>
<strong>🗑 Full Control:</strong> Delete keys anytime - you can always add them back later.
</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,21 +0,0 @@
/**
* MCP Integration Page
*
* Test and demonstrate Vibn's Model Context Protocol capabilities
*/
import { MCPPlayground } from '@/components/mcp-playground';
export const metadata = {
title: 'MCP Integration | Vibn',
description: 'Connect AI assistants to your Vibn projects using the Model Context Protocol',
};
export default function MCPPage() {
return (
<div className="container max-w-6xl py-8">
<MCPPlayground />
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function NewProjectLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -1,506 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowLeft, ArrowRight, Check, Sparkles, Code2 } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { auth } from "@/lib/firebase/config";
import { toast } from "sonner";
type ProjectType = "scratch" | "existing" | null;
export default function NewProjectPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [step, setStep] = useState(1);
const [projectName, setProjectName] = useState("");
const [projectType, setProjectType] = useState<ProjectType>(null);
const [workspacePath, setWorkspacePath] = useState<string | null>(null);
// Product vision (can skip)
const [productVision, setProductVision] = useState("");
// Product details
const [productName, setProductName] = useState("");
const [isForClient, setIsForClient] = useState<boolean | null>(null);
const [hasLogo, setHasLogo] = useState<boolean | null>(null);
const [hasDomain, setHasDomain] = useState<boolean | null>(null);
const [hasWebsite, setHasWebsite] = useState<boolean | null>(null);
const [hasGithub, setHasGithub] = useState<boolean | null>(null);
const [hasChatGPT, setHasChatGPT] = useState<boolean | null>(null);
const [isCheckingSlug, setIsCheckingSlug] = useState(false);
const [slugAvailable, setSlugAvailable] = useState<boolean | null>(null);
// Check for workspacePath query parameter
useEffect(() => {
const path = searchParams.get('workspacePath');
if (path) {
setWorkspacePath(path);
// Auto-fill project name from workspace path
const folderName = path.split('/').pop();
if (folderName && !projectName) {
setProjectName(folderName.replace(/-/g, ' ').replace(/_/g, ' '));
}
}
}, [searchParams, projectName]);
const generateSlug = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
};
const checkSlugAvailability = async (name: string) => {
const slug = generateSlug(name);
if (!slug) return;
setIsCheckingSlug(true);
// TODO: Replace with actual API call
await new Promise(resolve => setTimeout(resolve, 500));
// Mock check - in reality, check against database
const isAvailable = !["test", "demo", "admin"].includes(slug);
setSlugAvailable(isAvailable);
setIsCheckingSlug(false);
};
const handleProductNameChange = (value: string) => {
setProductName(value);
setSlugAvailable(null);
if (value.length > 2) {
checkSlugAvailability(value);
}
};
const handleNext = () => {
if (step === 1 && projectName && projectType) {
setStep(2);
} else if (step === 2) {
// Can skip questions
setStep(3);
} else if (step === 3 && productName && slugAvailable) {
handleCreateProject();
}
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
};
const handleSkipQuestions = () => {
setStep(3);
};
const handleCreateProject = async () => {
const slug = generateSlug(productName);
const projectData = {
projectName,
projectType,
slug,
vision: productVision,
product: {
name: productName,
isForClient,
hasLogo,
hasDomain,
hasWebsite,
hasGithub,
hasChatGPT,
},
workspacePath,
};
try {
const user = auth.currentUser;
if (!user) {
toast.error('You must be signed in to create a project');
return;
}
const token = await user.getIdToken();
const response = await fetch('/api/projects/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(projectData),
});
if (response.ok) {
const data = await response.json();
toast.success('Project created successfully!');
// Redirect to AI chat to start with vision questions
router.push(`/${data.workspace}/project/${data.projectId}/v_ai_chat`);
} else {
const error = await response.json();
toast.error(error.error || 'Failed to create project');
}
} catch (error) {
console.error('Error creating project:', error);
toast.error('An error occurred while creating project');
}
};
const canProceedStep1 = projectName.trim() && projectType;
const canProceedStep3 = productName.trim() && slugAvailable;
return (
<div className="min-h-screen bg-background p-6">
<div className="mx-auto max-w-2xl">
{/* Header */}
<div className="mb-8">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/projects")}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Projects
</Button>
<h1 className="text-3xl font-bold">Create New Project</h1>
<p className="text-muted-foreground mt-2">
Step {step} of 3
</p>
</div>
{/* Progress */}
<div className="flex gap-2 mb-8">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-2 flex-1 rounded-full transition-colors ${
s <= step ? "bg-primary" : "bg-muted"
}`}
/>
))}
</div>
{/* Step 1: Project Setup */}
{step === 1 && (
<Card>
<CardHeader>
<CardTitle>Project Setup</CardTitle>
<CardDescription>
Give your project a name and choose how you want to start
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="projectName">Project Name</Label>
<Input
id="projectName"
placeholder="My Awesome Project"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
/>
</div>
<div className="space-y-3">
<Label>Starting Point</Label>
<div className="grid gap-3">
<button
onClick={() => setProjectType("scratch")}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
projectType === "scratch"
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-start gap-3">
<Sparkles className="h-5 w-5 mt-0.5 text-primary" />
<div>
<div className="font-medium">Start from scratch</div>
<div className="text-sm text-muted-foreground">
Build a new project with AI assistance
</div>
</div>
{projectType === "scratch" && (
<Check className="h-5 w-5 ml-auto text-primary" />
)}
</div>
</button>
<button
onClick={() => setProjectType("existing")}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
projectType === "existing"
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-start gap-3">
<Code2 className="h-5 w-5 mt-0.5 text-primary" />
<div>
<div className="font-medium">Existing project</div>
<div className="text-sm text-muted-foreground">
Import and enhance an existing codebase
</div>
</div>
{projectType === "existing" && (
<Check className="h-5 w-5 ml-auto text-primary" />
)}
</div>
</button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Step 2: Product Vision */}
{step === 2 && (
<Card>
<CardHeader>
<CardTitle>Describe your product vision</CardTitle>
<CardDescription>
Help us understand your project (you can skip this)
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Textarea
placeholder="Describe who you're building for, what problem they have, and how you plan to solve it..."
value={productVision}
onChange={(e) => setProductVision(e.target.value)}
rows={8}
className="resize-none"
/>
</div>
<Button
variant="ghost"
className="w-full"
onClick={handleSkipQuestions}
>
Skip this step
</Button>
</CardContent>
</Card>
)}
{/* Step 3: Product Details */}
{step === 3 && (
<Card>
<CardHeader>
<CardTitle>Product Details</CardTitle>
<CardDescription>
Tell us about your product
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="productName">Product Name *</Label>
<Input
id="productName"
placeholder="Taskify"
value={productName}
onChange={(e) => handleProductNameChange(e.target.value)}
/>
{productName && (
<div className="text-xs text-muted-foreground">
{isCheckingSlug ? (
<span>Checking availability...</span>
) : slugAvailable === true ? (
<span className="text-green-600">
URL available: vibn.app/{generateSlug(productName)}
</span>
) : slugAvailable === false ? (
<span className="text-red-600">
This name is already taken
</span>
) : null}
</div>
)}
</div>
<div className="space-y-4">
{/* Client or Self */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Is this for a client or yourself?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={isForClient === true ? "default" : "outline"}
onClick={() => setIsForClient(true)}
size="sm"
className="w-20 h-8"
>
Client
</Button>
<Button
type="button"
variant={isForClient === false ? "default" : "outline"}
onClick={() => setIsForClient(false)}
size="sm"
className="w-20 h-8"
>
Myself
</Button>
</div>
</div>
{/* Logo */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a logo?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasLogo === true ? "default" : "outline"}
onClick={() => setHasLogo(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasLogo === false ? "default" : "outline"}
onClick={() => setHasLogo(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* Domain */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a domain?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasDomain === true ? "default" : "outline"}
onClick={() => setHasDomain(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasDomain === false ? "default" : "outline"}
onClick={() => setHasDomain(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* Website */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a website?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasWebsite === true ? "default" : "outline"}
onClick={() => setHasWebsite(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasWebsite === false ? "default" : "outline"}
onClick={() => setHasWebsite(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* GitHub */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Do you have a GitHub repository?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasGithub === true ? "default" : "outline"}
onClick={() => setHasGithub(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasGithub === false ? "default" : "outline"}
onClick={() => setHasGithub(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* ChatGPT */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Do you have your ideas in a ChatGPT project?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasChatGPT === true ? "default" : "outline"}
onClick={() => setHasChatGPT(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasChatGPT === false ? "default" : "outline"}
onClick={() => setHasChatGPT(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* Navigation */}
<div className="flex gap-3 mt-6">
{step > 1 && (
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
)}
<Button
className="ml-auto"
onClick={handleNext}
disabled={
(step === 1 && !canProceedStep1) ||
(step === 3 && !canProceedStep3) ||
isCheckingSlug
}
>
{step === 3 ? "Create Project" : "Next"}
{step < 3 && <ArrowRight className="h-4 w-4 ml-2" />}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;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&apos;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>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;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&apos;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>
);
}

View File

@@ -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&apos;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&apos;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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,8 +0,0 @@
export default function NewProjectLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -1,462 +0,0 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowLeft, ArrowRight, Check, Sparkles, Code2 } from "lucide-react";
import { useRouter } from "next/navigation";
type ProjectType = "scratch" | "existing" | null;
export default function NewProjectPage() {
const router = useRouter();
const [step, setStep] = useState(1);
const [projectName, setProjectName] = useState("");
const [projectType, setProjectType] = useState<ProjectType>(null);
// Product vision (can skip)
const [productVision, setProductVision] = useState("");
// Product details
const [productName, setProductName] = useState("");
const [isForClient, setIsForClient] = useState<boolean | null>(null);
const [hasLogo, setHasLogo] = useState<boolean | null>(null);
const [hasDomain, setHasDomain] = useState<boolean | null>(null);
const [hasWebsite, setHasWebsite] = useState<boolean | null>(null);
const [hasGithub, setHasGithub] = useState<boolean | null>(null);
const [hasChatGPT, setHasChatGPT] = useState<boolean | null>(null);
const [isCheckingSlug, setIsCheckingSlug] = useState(false);
const [slugAvailable, setSlugAvailable] = useState<boolean | null>(null);
const generateSlug = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
};
const checkSlugAvailability = async (name: string) => {
const slug = generateSlug(name);
if (!slug) return;
setIsCheckingSlug(true);
// TODO: Replace with actual API call
await new Promise(resolve => setTimeout(resolve, 500));
// Mock check - in reality, check against database
const isAvailable = !["test", "demo", "admin"].includes(slug);
setSlugAvailable(isAvailable);
setIsCheckingSlug(false);
};
const handleProductNameChange = (value: string) => {
setProductName(value);
setSlugAvailable(null);
if (value.length > 2) {
checkSlugAvailability(value);
}
};
const handleNext = () => {
if (step === 1 && projectName && projectType) {
setStep(2);
} else if (step === 2) {
// Can skip questions
setStep(3);
} else if (step === 3 && productName && slugAvailable) {
handleCreateProject();
}
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
};
const handleSkipQuestions = () => {
setStep(3);
};
const handleCreateProject = async () => {
const slug = generateSlug(productName);
const projectData = {
projectName,
projectType,
slug,
vision: productVision,
product: {
name: productName,
isForClient,
hasLogo,
hasDomain,
hasWebsite,
hasGithub,
hasChatGPT,
},
};
// TODO: API call to create project
console.log("Creating project:", projectData);
// Redirect to the new project
router.push(`/${slug}/overview`);
};
const canProceedStep1 = projectName.trim() && projectType;
const canProceedStep3 = productName.trim() && slugAvailable;
return (
<div className="min-h-screen bg-background p-6">
<div className="mx-auto max-w-2xl">
{/* Header */}
<div className="mb-8">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/projects")}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Projects
</Button>
<h1 className="text-3xl font-bold">Create New Project</h1>
<p className="text-muted-foreground mt-2">
Step {step} of 3
</p>
</div>
{/* Progress */}
<div className="flex gap-2 mb-8">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-2 flex-1 rounded-full transition-colors ${
s <= step ? "bg-primary" : "bg-muted"
}`}
/>
))}
</div>
{/* Step 1: Project Setup */}
{step === 1 && (
<Card>
<CardHeader>
<CardTitle>Project Setup</CardTitle>
<CardDescription>
Give your project a name and choose how you want to start
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="projectName">Project Name</Label>
<Input
id="projectName"
placeholder="My Awesome Project"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
/>
</div>
<div className="space-y-3">
<Label>Starting Point</Label>
<div className="grid gap-3">
<button
onClick={() => setProjectType("scratch")}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
projectType === "scratch"
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-start gap-3">
<Sparkles className="h-5 w-5 mt-0.5 text-primary" />
<div>
<div className="font-medium">Start from scratch</div>
<div className="text-sm text-muted-foreground">
Build a new project with AI assistance
</div>
</div>
{projectType === "scratch" && (
<Check className="h-5 w-5 ml-auto text-primary" />
)}
</div>
</button>
<button
onClick={() => setProjectType("existing")}
className={`text-left p-4 rounded-lg border-2 transition-colors ${
projectType === "existing"
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-start gap-3">
<Code2 className="h-5 w-5 mt-0.5 text-primary" />
<div>
<div className="font-medium">Existing project</div>
<div className="text-sm text-muted-foreground">
Import and enhance an existing codebase
</div>
</div>
{projectType === "existing" && (
<Check className="h-5 w-5 ml-auto text-primary" />
)}
</div>
</button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Step 2: Product Vision */}
{step === 2 && (
<Card>
<CardHeader>
<CardTitle>Describe your product vision</CardTitle>
<CardDescription>
Help us understand your project (you can skip this)
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Textarea
placeholder="Describe who you're building for, what problem they have, and how you plan to solve it..."
value={productVision}
onChange={(e) => setProductVision(e.target.value)}
rows={8}
className="resize-none"
/>
</div>
<Button
variant="ghost"
className="w-full"
onClick={handleSkipQuestions}
>
Skip this step
</Button>
</CardContent>
</Card>
)}
{/* Step 3: Product Details */}
{step === 3 && (
<Card>
<CardHeader>
<CardTitle>Product Details</CardTitle>
<CardDescription>
Tell us about your product
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="productName">Product Name *</Label>
<Input
id="productName"
placeholder="Taskify"
value={productName}
onChange={(e) => handleProductNameChange(e.target.value)}
/>
{productName && (
<div className="text-xs text-muted-foreground">
{isCheckingSlug ? (
<span>Checking availability...</span>
) : slugAvailable === true ? (
<span className="text-green-600">
URL available: vibn.app/{generateSlug(productName)}
</span>
) : slugAvailable === false ? (
<span className="text-red-600">
This name is already taken
</span>
) : null}
</div>
)}
</div>
<div className="space-y-4">
{/* Client or Self */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Is this for a client or yourself?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={isForClient === true ? "default" : "outline"}
onClick={() => setIsForClient(true)}
size="sm"
className="w-20 h-8"
>
Client
</Button>
<Button
type="button"
variant={isForClient === false ? "default" : "outline"}
onClick={() => setIsForClient(false)}
size="sm"
className="w-20 h-8"
>
Myself
</Button>
</div>
</div>
{/* Logo */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a logo?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasLogo === true ? "default" : "outline"}
onClick={() => setHasLogo(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasLogo === false ? "default" : "outline"}
onClick={() => setHasLogo(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* Domain */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a domain?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasDomain === true ? "default" : "outline"}
onClick={() => setHasDomain(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasDomain === false ? "default" : "outline"}
onClick={() => setHasDomain(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* Website */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Does it have a website?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasWebsite === true ? "default" : "outline"}
onClick={() => setHasWebsite(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasWebsite === false ? "default" : "outline"}
onClick={() => setHasWebsite(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* GitHub */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Do you have a GitHub repository?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasGithub === true ? "default" : "outline"}
onClick={() => setHasGithub(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasGithub === false ? "default" : "outline"}
onClick={() => setHasGithub(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
{/* ChatGPT */}
<div className="flex items-center justify-between">
<Label className="text-sm font-normal">Do you have your ideas in a ChatGPT project?</Label>
<div className="flex gap-2">
<Button
type="button"
variant={hasChatGPT === true ? "default" : "outline"}
onClick={() => setHasChatGPT(true)}
size="sm"
className="w-16 h-8"
>
Yes
</Button>
<Button
type="button"
variant={hasChatGPT === false ? "default" : "outline"}
onClick={() => setHasChatGPT(false)}
size="sm"
className="w-16 h-8"
>
No
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* Navigation */}
<div className="flex gap-3 mt-6">
{step > 1 && (
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
)}
<Button
className="ml-auto"
onClick={handleNext}
disabled={
(step === 1 && !canProceedStep1) ||
(step === 3 && !canProceedStep3) ||
isCheckingSlug
}
>
{step === 3 ? "Create Project" : "Next"}
{step < 3 && <ArrowRight className="h-4 w-4 ml-2" />}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,24 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
export default function TestApiKeyLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("connections");
return (
<div className="flex h-screen w-full overflow-hidden bg-background">
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
<RightPanel />
</div>
);
}

View File

@@ -1,103 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { auth } from "@/lib/firebase/config";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export default function TestApiKeyPage() {
const [results, setResults] = useState<any>(null);
const [loading, setLoading] = useState(false);
const testApiKey = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) {
setResults({ error: "Not authenticated. Please sign in first." });
return;
}
const token = await user.getIdToken();
console.log('[Test] Calling /api/user/api-key...');
console.log('[Test] Token length:', token.length);
const response = await fetch('/api/user/api-key', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
console.log('[Test] Response status:', response.status);
console.log('[Test] Response headers:', Object.fromEntries(response.headers.entries()));
const text = await response.text();
console.log('[Test] Response text:', text);
let data;
try {
data = JSON.parse(text);
} catch (e) {
data = { rawResponse: text };
}
setResults({
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
data: data,
userInfo: {
uid: user.uid,
email: user.email,
}
});
} catch (error: any) {
console.error('[Test] Error:', error);
setResults({ error: error.message, stack: error.stack });
} finally {
setLoading(false);
}
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
if (user) {
testApiKey();
}
});
return () => unsubscribe();
}, []);
return (
<div className="flex h-full flex-col overflow-auto p-8">
<div className="max-w-4xl space-y-6">
<div>
<h1 className="text-4xl font-bold mb-2">API Key Test</h1>
<p className="text-muted-foreground">Testing /api/user/api-key endpoint</p>
</div>
<Card>
<CardHeader>
<CardTitle>Test Results</CardTitle>
</CardHeader>
<CardContent>
{loading && <p>Testing API key endpoint...</p>}
{results && (
<pre className="bg-muted p-4 rounded-lg overflow-auto text-xs">
{JSON.stringify(results, null, 2)}
</pre>
)}
</CardContent>
</Card>
<Button onClick={testApiKey} disabled={loading}>
{loading ? "Testing..." : "Test Again"}
</Button>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
export default function TestAuthLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("connections");
return (
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
);
}

View File

@@ -1,99 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { auth } from "@/lib/firebase/config";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export default function TestAuthPage() {
const [results, setResults] = useState<any>(null);
const [loading, setLoading] = useState(false);
const runDiagnostics = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) {
setResults({ error: "Not authenticated. Please sign in first." });
return;
}
const token = await user.getIdToken();
// Test with token
const response = await fetch('/api/diagnose', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
const data = await response.json();
setResults({
...data,
clientInfo: {
uid: user.uid,
email: user.email,
tokenLength: token.length,
}
});
} catch (error: any) {
setResults({ error: error.message });
} finally {
setLoading(false);
}
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
console.log('[Test Auth] Auth state changed:', user ? user.uid : 'No user');
if (user) {
runDiagnostics();
} else {
setResults({
error: "Not authenticated. Please sign in first.",
note: "Redirecting to auth page...",
});
// Redirect to auth page after a delay
setTimeout(() => {
window.location.href = '/auth';
}, 2000);
}
});
return () => unsubscribe();
}, []);
return (
<div className="flex h-full flex-col overflow-auto p-8">
<div className="max-w-4xl space-y-6">
<div>
<h1 className="text-4xl font-bold mb-2">Auth Diagnostics</h1>
<p className="text-muted-foreground">Testing Firebase authentication and token verification</p>
</div>
<Card>
<CardHeader>
<CardTitle>Diagnostic Results</CardTitle>
</CardHeader>
<CardContent>
{loading && <p>Running diagnostics...</p>}
{results && (
<pre className="bg-muted p-4 rounded-lg overflow-auto text-xs">
{JSON.stringify(results, null, 2)}
</pre>
)}
{!loading && !results && (
<p className="text-muted-foreground">Click "Run Diagnostics" to test</p>
)}
</CardContent>
</Card>
<Button onClick={runDiagnostics} disabled={loading}>
{loading ? "Running..." : "Run Diagnostics Again"}
</Button>
</div>
</div>
);
}

View File

@@ -1,9 +0,0 @@
export default function TestSessionsLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -1,119 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { db, auth } from '@/lib/firebase/config';
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore';
export default function TestSessionsPage() {
const [sessions, setSessions] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (!user) {
setError('Not authenticated');
setLoading(false);
return;
}
try {
const sessionsRef = collection(db, 'sessions');
const q = query(
sessionsRef,
where('userId', '==', user.uid),
orderBy('createdAt', 'desc'),
limit(20)
);
const snapshot = await getDocs(q);
const sessionData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setSessions(sessionData);
} catch (err: any) {
console.error('Error fetching sessions:', err);
setError(err.message);
} finally {
setLoading(false);
}
});
return () => unsubscribe();
}, []);
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Recent Sessions</h1>
{loading && <p>Loading...</p>}
{error && <p className="text-red-500">Error: {error}</p>}
{!loading && sessions.length === 0 && (
<p className="text-gray-500">No sessions found yet. Make sure you&apos;re coding in Cursor with the extension enabled!</p>
)}
{sessions.length > 0 && (
<div className="space-y-4">
{sessions.map((session) => (
<div key={session.id} className="p-4 border rounded-lg bg-card">
<div className="grid grid-cols-2 gap-2 text-sm">
<div><strong>Session ID:</strong> {session.id}</div>
<div><strong>User ID:</strong> {session.userId?.substring(0, 20)}...</div>
<div className="col-span-2 mt-2">
<strong>🗂 Workspace:</strong>
<div className="font-mono text-xs bg-muted p-2 rounded mt-1">
{session.workspacePath || 'N/A'}
</div>
{session.workspaceName && (
<div className="text-muted-foreground mt-1">
Project: <span className="font-medium">{session.workspaceName}</span>
</div>
)}
</div>
<div><strong>Created:</strong> {session.createdAt?.toDate?.()?.toLocaleString() || 'N/A'}</div>
<div><strong>Duration:</strong> {session.duration ? `${session.duration}s` : 'N/A'}</div>
<div><strong>Model:</strong> {session.model || 'unknown'}</div>
<div><strong>Cost:</strong> ${session.cost?.toFixed(4) || '0.0000'}</div>
<div><strong>Tokens Used:</strong> {session.tokensUsed || 0}</div>
<div><strong>Files Modified:</strong> {session.filesModified?.length || 0}</div>
</div>
{session.filesModified && session.filesModified.length > 0 && (
<details className="mt-3">
<summary className="cursor-pointer text-primary hover:underline text-sm">
View Modified Files ({session.filesModified.length})
</summary>
<div className="mt-2 p-2 bg-muted rounded text-xs space-y-1">
{session.filesModified.map((file: string, idx: number) => (
<div key={idx} className="font-mono">{file}</div>
))}
</div>
</details>
)}
{session.conversationSummary && (
<details className="mt-3">
<summary className="cursor-pointer text-primary hover:underline text-sm">
View Conversation Summary
</summary>
<div className="mt-2 p-3 bg-muted rounded text-sm">
{session.conversationSummary}
</div>
</details>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,34 +0,0 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
import { Toaster } from "sonner";
export default function UsersLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("users");
return (
<>
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -1,190 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Users, UserPlus, Crown, Mail } from 'lucide-react';
import { useParams } from 'next/navigation';
interface WorkspaceUser {
id: string;
email: string;
displayName: string;
role: 'owner' | 'admin' | 'member';
joinedAt: any;
lastActive: any;
}
export default function UsersPage() {
const params = useParams();
const workspace = params.workspace as string;
const [users, setUsers] = useState<WorkspaceUser[]>([]);
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState<WorkspaceUser | null>(null);
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch(`/api/workspace/${workspace}/users`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setUsers(data.users);
setCurrentUser(data.currentUser);
}
} catch (error) {
console.error('Error loading users:', error);
toast.error('Failed to load workspace users');
} finally {
setLoading(false);
}
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'owner':
return 'bg-purple-500/10 text-purple-600 border-purple-500/20';
case 'admin':
return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
default:
return 'bg-gray-500/10 text-gray-600 border-gray-500/20';
}
};
return (
<div className="flex h-full flex-col overflow-auto">
<div className="flex-1 p-8 space-y-8 max-w-6xl">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold mb-2">Team Members</h1>
<p className="text-muted-foreground text-lg">
Manage workspace access and team collaboration
</p>
</div>
<Button disabled>
<UserPlus className="mr-2 h-4 w-4" />
Invite User
</Button>
</div>
{/* Current User Info */}
{currentUser && (
<Card className="border-primary/50 bg-primary/5">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Crown className="h-4 w-4" />
Your Account
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{currentUser.displayName || 'Unknown'}</p>
<p className="text-sm text-muted-foreground">{currentUser.email}</p>
</div>
<div className={`px-3 py-1 rounded-full text-xs font-medium border ${getRoleBadgeColor(currentUser.role)}`}>
{currentUser.role}
</div>
</div>
</CardContent>
</Card>
)}
{/* Users List */}
{loading ? (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading team members...</p>
</CardContent>
</Card>
) : users.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center space-y-4">
<div className="flex justify-center">
<div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Users className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">No team members yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Invite team members to collaborate on projects in this workspace
</p>
<Button disabled>
<UserPlus className="mr-2 h-4 w-4" />
Invite Your First Team Member
</Button>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{users.map((user) => (
<Card key={user.id}>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<Mail className="h-5 w-5 text-primary" />
</div>
<div>
<p className="font-medium">{user.displayName || 'Unknown'}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
{user.lastActive && (
<p className="text-xs text-muted-foreground mt-1">
Last active: {new Date(user.lastActive._seconds * 1000).toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1 rounded-full text-xs font-medium border ${getRoleBadgeColor(user.role)}`}>
{user.role}
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Info Card */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="text-base">Team Collaboration (Coming Soon)</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
<strong>👥 Team Workspaces:</strong> Invite team members to collaborate on projects together.
</p>
<p>
<strong>🔐 Role-Based Access:</strong> Control what team members can see and do with flexible permissions.
</p>
<p>
<strong>💬 Shared Context:</strong> All team members can access shared AI chat history and project documentation.
</p>
<p className="text-xs italic pt-2">
This feature is currently in development. Check back soon!
</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,385 +0,0 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Upload, Github, Plug, FileText, Download, Copy, Check } from "lucide-react";
import { useState, useRef } from "react";
import { auth } from "@/lib/firebase/config";
import { toast } from "sonner";
import { GitHubRepoPicker } from "./github-repo-picker";
import { CursorIcon } from "@/components/icons/custom-icons";
interface CollectorActionsProps {
projectId: string;
className?: string;
}
export function CollectorActions({ projectId, className }: CollectorActionsProps) {
const [uploading, setUploading] = useState(false);
const [showGithubPicker, setShowGithubPicker] = useState(false);
const [showPasteDialog, setShowPasteDialog] = useState(false);
const [showCursorImportDialog, setShowCursorImportDialog] = useState(false);
const [pasteTitle, setPasteTitle] = useState("");
const [pasteContent, setPasteContent] = useState("");
const [isPasting, setIsPasting] = useState(false);
const [copiedConfig, setCopiedConfig] = useState(false);
const [copiedCommand, setCopiedCommand] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files || files.length === 0) return;
setUploading(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error("Please sign in to upload files");
return;
}
const token = await user.getIdToken();
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append("file", file);
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}`);
}
}
toast.success(`Uploaded ${files.length} file(s)`);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
} catch (error) {
console.error("Upload error:", error);
toast.error("Failed to upload files");
} finally {
setUploading(false);
}
};
const handleExtensionClick = () => {
window.open("https://chrome.google.com/webstore", "_blank");
toast.info("Install the Vibn browser extension and link it to this project");
};
const handleCopyConfig = () => {
const vibnConfig = {
projectId: projectId,
version: "1.0.0"
};
const content = JSON.stringify(vibnConfig, null, 2);
navigator.clipboard.writeText(content);
setCopiedConfig(true);
toast.success("Configuration copied to clipboard!");
setTimeout(() => setCopiedConfig(false), 2000);
};
const handleCopyCommand = () => {
navigator.clipboard.writeText("Vibn: Import Historical Conversations");
setCopiedCommand(true);
toast.success("Command copied to clipboard!");
setTimeout(() => setCopiedCommand(false), 2000);
};
const handlePasteSubmit = async () => {
if (!pasteTitle.trim() || !pasteContent.trim()) {
toast.error("Please provide both title and content");
return;
}
setIsPasting(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error("Please sign in to save content");
return;
}
const token = await user.getIdToken();
const response = await fetch(`/api/projects/${projectId}/knowledge/import-ai-chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
title: pasteTitle,
transcript: pasteContent,
}),
});
if (!response.ok) {
throw new Error("Failed to import chat");
}
toast.success("AI chat imported successfully!");
setShowPasteDialog(false);
setPasteTitle("");
setPasteContent("");
} catch (error) {
console.error("Paste error:", error);
toast.error("Failed to import chat");
} finally {
setIsPasting(false);
}
};
return (
<>
<input
ref={fileInputRef}
type="file"
multiple
accept=".txt,.md,.json,.pdf,.doc,.docx"
onChange={handleFileSelect}
className="hidden"
/>
{/* Upload Documents */}
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
>
<Upload className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">
{uploading ? "Uploading..." : "Upload Documents"}
</span>
</Button>
{/* Connect GitHub */}
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={() => setShowGithubPicker(true)}
>
<Github className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">Connect GitHub</span>
</Button>
{/* Get Extension */}
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={handleExtensionClick}
>
<Plug className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">Get Extension</span>
</Button>
{/* Paste AI Chat */}
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={() => setShowPasteDialog(true)}
>
<FileText className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">Paste AI Chat</span>
</Button>
{/* Import Cursor History */}
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={() => setShowCursorImportDialog(true)}
>
<CursorIcon className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">Import Cursor History</span>
</Button>
{/* GitHub Picker Dialog */}
{showGithubPicker && (
<GitHubRepoPicker
projectId={projectId}
onClose={() => setShowGithubPicker(false)}
/>
)}
{/* Paste AI Chat Dialog */}
<Dialog open={showPasteDialog} onOpenChange={setShowPasteDialog}>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle>Import AI Chat</DialogTitle>
<DialogDescription>
Paste a conversation from ChatGPT, Claude, or any other AI tool
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<div>
<Label htmlFor="paste-title">Title</Label>
<Input
id="paste-title"
placeholder="e.g., Product Vision Discussion"
value={pasteTitle}
onChange={(e) => setPasteTitle(e.target.value)}
className="mt-1"
/>
</div>
<div className="flex-1">
<Label htmlFor="paste-content">Chat Content</Label>
<Textarea
id="paste-content"
placeholder="Paste your entire conversation here..."
value={pasteContent}
onChange={(e) => setPasteContent(e.target.value)}
className="mt-1 min-h-[300px] font-mono text-xs"
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setShowPasteDialog(false)}
disabled={isPasting}
>
Cancel
</Button>
<Button
onClick={handlePasteSubmit}
disabled={isPasting || !pasteTitle.trim() || !pasteContent.trim()}
>
{isPasting ? "Importing..." : "Import Chat"}
</Button>
</div>
</DialogContent>
</Dialog>
{/* Cursor Import Dialog */}
<Dialog open={showCursorImportDialog} onOpenChange={setShowCursorImportDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CursorIcon className="h-5 w-5" />
Import Cursor Conversation History
</DialogTitle>
<DialogDescription>
Import all conversations from Cursor related to this project
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-lg bg-muted p-4 space-y-3">
<p className="text-sm font-medium">Step 1: Create .vibn file</p>
<p className="text-xs text-muted-foreground mb-2">
In your project root directory (where you open Cursor), create a file named <code className="text-xs bg-background px-1 py-0.5 rounded">.vibn</code>
</p>
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs font-medium">Paste this content into the file:</p>
<Button
size="sm"
variant="ghost"
onClick={handleCopyConfig}
className="h-7 gap-2"
>
{copiedConfig ? (
<>
<Check className="h-3 w-3" />
Copied
</>
) : (
<>
<Copy className="h-3 w-3" />
Copy
</>
)}
</Button>
</div>
<pre className="text-xs bg-background p-3 rounded border overflow-x-auto">
<code>{JSON.stringify({ projectId: projectId, version: "1.0.0" }, null, 2)}</code>
</pre>
</div>
</div>
<div className="rounded-lg bg-muted p-4 space-y-3">
<p className="text-sm font-medium">Step 2: Reload Cursor window</p>
<p className="text-xs text-muted-foreground">
After creating the .vibn file, reload Cursor to register the new configuration:
</p>
<ul className="list-disc list-inside space-y-1 text-xs text-muted-foreground ml-2">
<li>Command Palette <strong>Developer: Reload Window</strong></li>
<li>Or: Close and reopen Cursor</li>
</ul>
</div>
<div className="rounded-lg bg-muted p-4 space-y-3">
<p className="text-sm font-medium">Step 3: Run import command</p>
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground ml-2">
<li>Open Command Palette (Cmd+Shift+P / Ctrl+Shift+P)</li>
<li>Run this command:</li>
</ol>
<div className="flex items-center gap-2 mt-2">
<code className="flex-1 text-xs bg-background px-3 py-2 rounded border">
Vibn: Import Historical Conversations
</code>
<Button
size="sm"
variant="ghost"
onClick={handleCopyCommand}
className="h-8"
>
{copiedCommand ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
<p className="text-sm text-blue-700 dark:text-blue-400">
💡 <strong>Note:</strong> The Cursor Monitor extension must be installed and configured with your Vibn API key.
</p>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">What gets imported:</p>
<ul className="text-xs text-muted-foreground space-y-1 ml-4">
<li> All chat sessions from the workspace</li>
<li> User prompts and AI responses</li>
<li> File edit history and context</li>
<li> Conversation timestamps and metadata</li>
</ul>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setShowCursorImportDialog(false)}
>
Close
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,225 +0,0 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle2, Circle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { db } from "@/lib/firebase/config";
import { doc, onSnapshot, collection, query, where } from "firebase/firestore";
interface CollectorChecklistProps {
projectId: string;
className?: string;
}
export function CollectorChecklist({ projectId, className }: CollectorChecklistProps) {
const [loading, setLoading] = useState(true);
const [currentPhase, setCurrentPhase] = useState<string | null>(null);
const [hasDocuments, setHasDocuments] = useState(false);
const [documentCount, setDocumentCount] = useState(0);
const [githubConnected, setGithubConnected] = useState(false);
const [githubRepo, setGithubRepo] = useState<string | null>(null);
const [extensionLinked, setExtensionLinked] = useState(false);
const [readyForNextPhase, setReadyForNextPhase] = useState(false);
useEffect(() => {
if (!projectId) return;
const unsubscribe = onSnapshot(
doc(db, "projects", projectId),
(snapshot) => {
if (snapshot.exists()) {
const data = snapshot.data();
const phase = data?.currentPhase || 'collector';
setCurrentPhase(phase);
// Get actual state from project data
const repo = data?.githubRepo || null;
setGithubConnected(!!repo);
setGithubRepo(repo);
// Check handoff for readiness
const handoff = data?.phaseData?.phaseHandoffs?.collector;
setReadyForNextPhase(handoff?.readyForNextPhase || false);
console.log("[CollectorChecklist] Project state:", {
currentPhase: phase,
githubRepo: repo,
readyForNextPhase: handoff?.readyForNextPhase,
userId: data?.userId, // Log the user ID
});
// Also log it prominently for easy copying
console.log(`🆔 YOUR USER ID: ${data?.userId}`);
} else {
console.log("[CollectorChecklist] Project not found:", projectId);
}
setLoading(false);
},
(error) => {
console.error("[CollectorChecklist] Error loading checklist:", error);
setLoading(false);
}
);
return () => unsubscribe();
}, [projectId]);
// Separate effect to check for linked sessions (extension linked)
useEffect(() => {
if (!projectId) return;
console.log(`[CollectorChecklist] Querying sessions for projectId: "${projectId}"`);
const sessionsRef = collection(db, 'sessions');
const q = query(sessionsRef, where('projectId', '==', projectId));
const unsubscribe = onSnapshot(q, (snapshot) => {
const hasLinkedSessions = !snapshot.empty;
setExtensionLinked(hasLinkedSessions);
console.log("[CollectorChecklist] Extension linked (sessions found):", hasLinkedSessions, `(${snapshot.size} sessions)`);
// Debug: Show session IDs if found
if (snapshot.size > 0) {
const sessionIds = snapshot.docs.map(doc => doc.id);
console.log("[CollectorChecklist] Found session IDs:", sessionIds);
}
}, (error) => {
console.error("[CollectorChecklist] Error querying sessions:", error);
});
return () => unsubscribe();
}, [projectId]);
// Separate effect to count documents from knowledge_items collection
// Count both uploaded documents AND pasted AI chat content
useEffect(() => {
if (!projectId) return;
const q = query(
collection(db, 'knowledge_items'),
where('projectId', '==', projectId)
);
const unsubscribe = onSnapshot(q, (snapshot) => {
// Filter for document types (uploaded files and pasted content)
const docs = snapshot.docs.filter(doc => {
const sourceType = doc.data().sourceType;
return sourceType === 'imported_document' || sourceType === 'imported_ai_chat';
});
const count = docs.length;
setHasDocuments(count > 0);
setDocumentCount(count);
console.log("[CollectorChecklist] Document count:", count, "(documents + pasted content)");
});
return () => unsubscribe();
}, [projectId]);
if (loading) {
return (
<div className={className}>
<div className="mb-3">
<h3 className="text-sm font-semibold">Setup Progress</h3>
</div>
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
</div>
);
}
const checklist = [
{
id: "documents",
label: "Documents uploaded",
checked: hasDocuments,
count: documentCount,
},
{
id: "github",
label: "GitHub connected",
checked: githubConnected,
repo: githubRepo,
},
{
id: "extension",
label: "Extension linked",
checked: extensionLinked,
},
];
const completedCount = checklist.filter((item) => item.checked).length;
const progress = (completedCount / checklist.length) * 100;
// If phase is beyond collector, show completed state
const isCompleted = currentPhase === 'analyzed' || currentPhase === 'vision' || currentPhase === 'mvp';
return (
<div className={className}>
{/* Header */}
<div className="mb-3">
<h3 className="text-sm font-semibold">
{isCompleted ? '✓ Setup Complete' : 'Setup Progress'}
</h3>
<div className="mt-2">
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${isCompleted ? 100 : progress}%` }}
/>
</div>
<p className="text-xs text-muted-foreground mt-1">
{isCompleted ? '3 of 3 complete' : `${completedCount} of ${checklist.length} complete`}
</p>
</div>
</div>
{/* Checklist Items */}
<div className="space-y-2">
{checklist.map((item) => (
<div
key={item.id}
className={`flex items-center gap-2 py-2 px-3 rounded-md border ${
(item.checked || isCompleted)
? "bg-green-50 border-green-200"
: "bg-muted/50 border-muted text-muted-foreground opacity-60"
}`}
>
{(item.checked || isCompleted) ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-muted-foreground flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium">
{item.label}
{item.checked && item.count !== undefined && (
<span className="text-muted-foreground ml-1">
({item.count})
</span>
)}
</p>
{item.checked && item.repo && (
<p className="text-[10px] text-muted-foreground truncate">
{item.repo}
</p>
)}
</div>
</div>
))}
</div>
{/* Ready indicator */}
{readyForNextPhase && (
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-green-600 font-medium">
Ready for extraction phase
</p>
</div>
)}
</div>
);
}

View File

@@ -1,318 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { CheckCircle2, AlertTriangle, HelpCircle, Users, Lightbulb, Wrench, AlertCircle, Sparkles, X, Plus, Save, Edit2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { auth } from "@/lib/firebase/config";
import { toast } from "sonner";
interface ExtractionHandoff {
phase: string;
readyForNextPhase: boolean;
confidence: number;
confirmed: {
problems?: string[];
targetUsers?: string[];
features?: string[];
constraints?: string[];
opportunities?: string[];
};
uncertain: Record<string, any>;
missing: string[];
questionsForUser: string[];
timestamp: string;
}
interface ExtractionResultsEditableProps {
projectId: string;
className?: string;
}
export function ExtractionResultsEditable({ projectId, className }: ExtractionResultsEditableProps) {
const [extraction, setExtraction] = useState<ExtractionHandoff | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Local editable state
const [editedProblems, setEditedProblems] = useState<string[]>([]);
const [editedUsers, setEditedUsers] = useState<string[]>([]);
const [editedFeatures, setEditedFeatures] = useState<string[]>([]);
const [editedConstraints, setEditedConstraints] = useState<string[]>([]);
const [editedOpportunities, setEditedOpportunities] = useState<string[]>([]);
useEffect(() => {
const fetchExtraction = async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/extraction-handoff`);
if (!response.ok) {
if (response.status === 404) {
setExtraction(null);
return;
}
throw new Error(`Failed to fetch extraction: ${response.statusText}`);
}
const data = await response.json();
const handoff = data.handoff;
setExtraction(handoff);
// Initialize editable state
setEditedProblems(handoff.confirmed.problems || []);
setEditedUsers(handoff.confirmed.targetUsers || []);
setEditedFeatures(handoff.confirmed.features || []);
setEditedConstraints(handoff.confirmed.constraints || []);
setEditedOpportunities(handoff.confirmed.opportunities || []);
} catch (err) {
console.error("[ExtractionResults] Error:", err);
setError(err instanceof Error ? err.message : "Failed to load extraction");
} finally {
setLoading(false);
}
};
if (projectId) {
fetchExtraction();
}
}, [projectId]);
const handleSave = async () => {
setIsSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error("Please sign in to save changes");
return;
}
const token = await user.getIdToken();
const response = await fetch(`/api/projects/${projectId}/extraction-handoff`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
confirmed: {
problems: editedProblems.filter(p => p.trim()),
targetUsers: editedUsers.filter(u => u.trim()),
features: editedFeatures.filter(f => f.trim()),
constraints: editedConstraints.filter(c => c.trim()),
opportunities: editedOpportunities.filter(o => o.trim()),
},
}),
});
if (!response.ok) {
throw new Error("Failed to save changes");
}
// Update local state
if (extraction) {
setExtraction({
...extraction,
confirmed: {
problems: editedProblems.filter(p => p.trim()),
targetUsers: editedUsers.filter(u => u.trim()),
features: editedFeatures.filter(f => f.trim()),
constraints: editedConstraints.filter(c => c.trim()),
opportunities: editedOpportunities.filter(o => o.trim()),
},
});
}
setIsEditing(false);
toast.success("Changes saved");
} catch (err) {
console.error("[ExtractionResults] Save error:", err);
toast.error("Failed to save changes");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
// Reset to original values
if (extraction) {
setEditedProblems(extraction.confirmed.problems || []);
setEditedUsers(extraction.confirmed.targetUsers || []);
setEditedFeatures(extraction.confirmed.features || []);
setEditedConstraints(extraction.confirmed.constraints || []);
setEditedOpportunities(extraction.confirmed.opportunities || []);
}
setIsEditing(false);
};
const renderEditableList = (
items: string[],
setItems: (items: string[]) => void,
Icon: any,
iconColor: string,
title: string
) => {
const safeItems = items || [];
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm flex items-center gap-2">
<Icon className={cn("h-4 w-4", iconColor)} />
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{safeItems.map((item, index) => (
<div key={index} className="flex items-center gap-2">
{isEditing ? (
<>
<Input
value={item}
onChange={(e) => {
const newItems = [...items];
newItems[index] = e.target.value;
setItems(newItems);
}}
className="flex-1 text-sm"
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
const newItems = items.filter((_, i) => i !== index);
setItems(newItems);
}}
>
<X className="h-4 w-4" />
</Button>
</>
) : (
<p className="text-sm text-muted-foreground">{item}</p>
)}
</div>
))}
{isEditing && (
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={() => setItems([...items, ""])}
>
<Plus className="h-4 w-4 mr-2" />
Add Item
</Button>
)}
{!isEditing && items.length === 0 && (
<p className="text-xs text-muted-foreground italic">None</p>
)}
</CardContent>
</Card>
);
};
if (loading) {
return (
<div className={cn("space-y-4", className)}>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
}
if (error) {
return (
<Card className={cn("border-destructive", className)}>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
<p>{error}</p>
</div>
</CardContent>
</Card>
);
}
if (!extraction) {
return (
<Card className={cn("border-dashed", className)}>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
<Sparkles className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No extraction results yet</p>
<p className="text-xs mt-1">Upload documents and trigger extraction to see insights</p>
</div>
</CardContent>
</Card>
);
}
return (
<div className={cn("space-y-4", className)}>
{/* Header */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Extracted Insights</CardTitle>
<p className="text-xs text-muted-foreground mt-1">
Review and edit the extracted information
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant={extraction.readyForNextPhase ? "default" : "secondary"}>
{Math.round(extraction.confidence * 100)}% confidence
</Badge>
{!isEditing ? (
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
>
<Edit2 className="h-4 w-4 mr-2" />
Edit
</Button>
) : (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCancel}
disabled={isSaving}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={isSaving}
>
<Save className="h-4 w-4 mr-2" />
{isSaving ? "Saving..." : "Save"}
</Button>
</div>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Editable Lists */}
{renderEditableList(editedProblems, setEditedProblems, AlertTriangle, "text-orange-600", "Problems & Pain Points")}
{renderEditableList(editedUsers, setEditedUsers, Users, "text-blue-600", "Target Users")}
{renderEditableList(editedFeatures, setEditedFeatures, Lightbulb, "text-yellow-600", "Key Features")}
{renderEditableList(editedConstraints, setEditedConstraints, Wrench, "text-purple-600", "Constraints & Requirements")}
{renderEditableList(editedOpportunities, setEditedOpportunities, Sparkles, "text-green-600", "Opportunities")}
</div>
);
}

View File

@@ -1,305 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { CheckCircle2, AlertTriangle, HelpCircle, Users, Lightbulb, Wrench, AlertCircle, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
interface ExtractionHandoff {
phase: string;
readyForNextPhase: boolean;
confidence: number;
confirmed: {
problems?: string[];
targetUsers?: string[];
features?: string[];
constraints?: string[];
opportunities?: string[];
};
uncertain: Record<string, any>;
missing: string[];
questionsForUser: string[];
timestamp: string;
}
interface ExtractionResultsProps {
projectId: string;
className?: string;
}
export function ExtractionResults({ projectId, className }: ExtractionResultsProps) {
const [extraction, setExtraction] = useState<ExtractionHandoff | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchExtraction = async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/extraction-handoff`);
if (!response.ok) {
if (response.status === 404) {
setExtraction(null);
return;
}
throw new Error(`Failed to fetch extraction: ${response.statusText}`);
}
const data = await response.json();
setExtraction(data.handoff);
} catch (err) {
console.error("[ExtractionResults] Error:", err);
setError(err instanceof Error ? err.message : "Failed to load extraction");
} finally {
setLoading(false);
}
};
if (projectId) {
fetchExtraction();
}
}, [projectId]);
if (loading) {
return (
<div className={cn("space-y-4", className)}>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
}
if (error) {
return (
<Card className={cn("border-destructive", className)}>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
<p>{error}</p>
</div>
</CardContent>
</Card>
);
}
if (!extraction) {
return (
<Card className={cn("border-dashed", className)}>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
<Sparkles className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No extraction results yet</p>
<p className="text-xs mt-1">Upload documents and trigger extraction to see insights</p>
</div>
</CardContent>
</Card>
);
}
const { confirmed } = extraction;
const confidencePercent = Math.round(extraction.confidence * 100);
return (
<div className={cn("space-y-4", className)}>
{/* Header with Confidence Score */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Extraction Results</CardTitle>
<div className="flex items-center gap-2">
<Badge variant={extraction.readyForNextPhase ? "default" : "secondary"}>
{extraction.readyForNextPhase ? (
<>
<CheckCircle2 className="h-3 w-3 mr-1" />
Ready
</>
) : (
<>
<HelpCircle className="h-3 w-3 mr-1" />
Incomplete
</>
)}
</Badge>
<Badge variant="outline" className={cn(
confidencePercent >= 70 ? "border-green-500 text-green-600" :
confidencePercent >= 40 ? "border-yellow-500 text-yellow-600" :
"border-red-500 text-red-600"
)}>
{confidencePercent}% confidence
</Badge>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Extracted on {new Date(extraction.timestamp).toLocaleString()}
</p>
</CardContent>
</Card>
{/* Problems / Pain Points */}
{confirmed.problems && confirmed.problems.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-orange-500" />
Problems & Pain Points
<Badge variant="secondary">{confirmed.problems.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.problems.map((problem, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-orange-500 mt-1"></span>
<span className="text-sm">{problem}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Target Users */}
{confirmed.targetUsers && confirmed.targetUsers.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Users className="h-4 w-4 text-blue-500" />
Target Users
<Badge variant="secondary">{confirmed.targetUsers.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.targetUsers.map((user, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-blue-500 mt-1"></span>
<span className="text-sm">{user}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Features */}
{confirmed.features && confirmed.features.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Lightbulb className="h-4 w-4 text-yellow-500" />
Key Features
<Badge variant="secondary">{confirmed.features.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.features.map((feature, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-yellow-500 mt-1"></span>
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Constraints */}
{confirmed.constraints && confirmed.constraints.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Wrench className="h-4 w-4 text-purple-500" />
Constraints & Requirements
<Badge variant="secondary">{confirmed.constraints.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.constraints.map((constraint, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-purple-500 mt-1"></span>
<span className="text-sm">{constraint}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Opportunities */}
{confirmed.opportunities && confirmed.opportunities.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Sparkles className="h-4 w-4 text-green-500" />
Opportunities
<Badge variant="secondary">{confirmed.opportunities.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.opportunities.map((opportunity, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-green-500 mt-1"></span>
<span className="text-sm">{opportunity}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Missing Information */}
{extraction.missing && extraction.missing.length > 0 && (
<Card className="border-yellow-200 bg-yellow-50/50">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-yellow-800">
<HelpCircle className="h-4 w-4" />
Missing Information
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-1">
{extraction.missing.map((item, idx) => (
<li key={idx} className="text-sm text-yellow-800 flex items-center gap-2">
<span>-</span>
<span>{item}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Questions for User */}
{extraction.questionsForUser && extraction.questionsForUser.length > 0 && (
<Card className="border-blue-200 bg-blue-50/50">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-blue-800">
<HelpCircle className="h-4 w-4" />
Questions for Clarification
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{extraction.questionsForUser.map((question, idx) => (
<li key={idx} className="text-sm text-blue-800 flex items-start gap-2">
<span className="font-medium">{idx + 1}.</span>
<span>{question}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -1,129 +0,0 @@
"use client";
import { CheckCircle2, Circle } from "lucide-react";
import { useEffect, useState } from "react";
import { db } from "@/lib/firebase/config";
import { doc, onSnapshot } from "firebase/firestore";
interface ExtractionReviewChecklistProps {
projectId: string;
className?: string;
}
export function ExtractionReviewChecklist({ projectId, className }: ExtractionReviewChecklistProps) {
const [hasProblems, setHasProblems] = useState(false);
const [hasUsers, setHasUsers] = useState(false);
const [hasFeatures, setHasFeatures] = useState(false);
const [reviewedWithAI, setReviewedWithAI] = useState(false);
const [readyForVision, setReadyForVision] = useState(false);
useEffect(() => {
if (!projectId) return;
const unsubscribe = onSnapshot(
doc(db, "projects", projectId),
(snapshot) => {
if (snapshot.exists()) {
const data = snapshot.data();
const extraction = data?.phaseData?.phaseHandoffs?.extraction;
if (extraction) {
// Check if we have key data
setHasProblems((extraction.confirmed?.problems?.length || 0) > 0);
setHasUsers((extraction.confirmed?.targetUsers?.length || 0) > 0);
setHasFeatures((extraction.confirmed?.features?.length || 0) > 0);
setReadyForVision(extraction.readyForNextPhase || false);
}
// Check if user has reviewed with AI (simplified: check if there are any messages in chat)
// This is a placeholder - you might track this differently
setReviewedWithAI(data?.lastChatAt ? true : false);
}
}
);
return () => unsubscribe();
}, [projectId]);
const checklist = [
{
id: "problems",
label: "Problems identified",
checked: hasProblems,
},
{
id: "users",
label: "Target users defined",
checked: hasUsers,
},
{
id: "features",
label: "Key features extracted",
checked: hasFeatures,
},
{
id: "reviewed",
label: "Reviewed with AI",
checked: reviewedWithAI,
},
];
const completedCount = checklist.filter((item) => item.checked).length;
const progress = (completedCount / checklist.length) * 100;
const isCompleted = completedCount === checklist.length || readyForVision;
return (
<div className={className}>
{/* Header */}
<div className="mb-3">
<h3 className="text-sm font-semibold">
{isCompleted ? '✓ Review Complete' : 'Extraction Review'}
</h3>
<div className="mt-2">
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${isCompleted ? 100 : progress}%` }}
/>
</div>
<p className="text-xs text-muted-foreground mt-1">
{isCompleted ? `${checklist.length} of ${checklist.length} complete` : `${completedCount} of ${checklist.length} complete`}
</p>
</div>
</div>
{/* Checklist Items */}
<div className="space-y-2">
{checklist.map((item) => (
<div
key={item.id}
className={`flex items-center gap-2 py-2 px-3 rounded-md border ${
(item.checked || isCompleted)
? "bg-green-50 border-green-200"
: "bg-muted/50 border-muted text-muted-foreground opacity-60"
}`}
>
{(item.checked || isCompleted) ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-muted-foreground flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium">{item.label}</p>
</div>
</div>
))}
</div>
{/* Ready indicator */}
{readyForVision && (
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-green-600 font-medium">
Ready for Vision phase
</p>
</div>
)}
</div>
);
}

View File

@@ -1,269 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Github, Loader2, CheckCircle2, ExternalLink, X } from 'lucide-react';
import { auth } from '@/lib/firebase/config';
import { doc, updateDoc, serverTimestamp } from 'firebase/firestore';
import { db } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { initiateGitHubOAuth } from '@/lib/github/oauth';
interface GitHubRepoPickerProps {
projectId: string;
onRepoSelected?: (repo: any) => void;
onClose?: () => void;
}
export function GitHubRepoPicker({ projectId, onRepoSelected, onClose }: GitHubRepoPickerProps) {
const [loading, setLoading] = useState(false);
const [connected, setConnected] = useState(false);
const [repos, setRepos] = useState<any[]>([]);
const [selectedRepo, setSelectedRepo] = useState<any>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
checkConnection();
}, []);
const checkConnection = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const statusResponse = await fetch('/api/github/connect', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (statusResponse.ok) {
const statusData = await statusResponse.json();
setConnected(statusData.connected);
if (statusData.connected) {
const reposResponse = await fetch('/api/github/repos', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (reposResponse.ok) {
const reposData = await reposResponse.json();
setRepos(reposData);
}
}
}
} catch (error) {
console.error('Error checking GitHub connection:', error);
} finally {
setLoading(false);
}
};
const handleConnect = () => {
const redirectUri = `${window.location.origin}/api/github/oauth/callback`;
initiateGitHubOAuth(redirectUri);
};
const handleSelectRepo = async (repo: any) => {
setSelectedRepo(repo);
setSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
// Update project with GitHub info
await updateDoc(doc(db, 'projects', projectId), {
githubRepo: repo.full_name,
githubRepoId: repo.id,
githubRepoUrl: repo.html_url,
githubDefaultBranch: repo.default_branch,
hasGithub: true,
updatedAt: serverTimestamp(),
});
// Try to automatically associate existing sessions with this repo
try {
const token = await user.getIdToken();
const associateResponse = await fetch(`/api/projects/${projectId}/associate-github-sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
githubRepo: repo.full_name,
githubRepoUrl: repo.html_url,
}),
});
if (associateResponse.ok) {
const data = await associateResponse.json();
console.log('🔗 Session association result:', data);
if (data.sessionsAssociated > 0) {
toast.success(`Connected to ${repo.full_name}!`, {
description: `Found and linked ${data.sessionsAssociated} existing chat sessions from this repository`,
duration: 5000,
});
} else {
// No sessions found - show helpful message
toast.success(`Connected to ${repo.full_name}!`, {
description: `Repository linked! Future chat sessions from this repo will be automatically tracked here.`,
duration: 5000,
});
console.log(` No matching sessions found. This could mean:
- No chat sessions exist yet for this repo
- Sessions are already linked to other projects
- Workspace folder name doesn't match repo name (${repo.name})`);
}
} else {
// Connection succeeded but session association failed - still show success
toast.success(`Connected to ${repo.full_name}!`);
console.warn('Session association failed but connection succeeded');
}
} catch (associateError) {
// Don't fail the whole operation if association fails
console.error('Error associating sessions:', associateError);
toast.success(`Connected to ${repo.full_name}!`);
}
// Notify parent component
if (onRepoSelected) {
onRepoSelected(repo);
}
} catch (error) {
console.error('Error connecting repo:', error);
toast.error('Failed to connect repository');
setSelectedRepo(null);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<Card className="my-2">
<CardContent className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</CardContent>
</Card>
);
}
if (!connected) {
return (
<Card className="my-2 border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Github className="h-5 w-5" />
Connect GitHub
</CardTitle>
<CardDescription>
Connect your GitHub account to select a repository
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={handleConnect} className="w-full">
<Github className="mr-2 h-4 w-4" />
Connect GitHub Account
</Button>
</CardContent>
</Card>
);
}
if (selectedRepo) {
return (
<Card className="my-2 border-green-500/50 bg-green-50/50 dark:bg-green-950/20">
<CardContent className="flex items-center gap-3 py-4">
<CheckCircle2 className="h-5 w-5 text-green-600" />
<div className="flex-1">
<p className="font-medium">{selectedRepo.full_name}</p>
<p className="text-sm text-muted-foreground">Repository connected!</p>
</div>
<a
href={selectedRepo.html_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
View
</a>
{onClose && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 ml-2"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
)}
</CardContent>
</Card>
);
}
return (
<Card className="my-2">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Github className="h-5 w-5" />
Select Repository
</CardTitle>
<CardDescription>
Choose which repository to connect to this project
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{repos.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
No repositories found
</p>
) : (
repos.map((repo) => (
<button
key={repo.id}
onClick={() => handleSelectRepo(repo)}
disabled={saving}
className="w-full text-left p-3 rounded-lg border-2 border-border hover:border-primary transition-all disabled:opacity-50"
>
<div className="font-medium">{repo.full_name}</div>
{repo.description && (
<div className="text-sm text-muted-foreground truncate mt-1">
{repo.description}
</div>
)}
<div className="flex items-center gap-2 mt-2">
{repo.language && (
<span className="text-xs bg-muted px-2 py-0.5 rounded">
{repo.language}
</span>
)}
{repo.private && (
<span className="text-xs bg-yellow-500/10 text-yellow-600 px-2 py-0.5 rounded">
Private
</span>
)}
</div>
</button>
))
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,90 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { db } from "@/lib/firebase/config";
import { doc, onSnapshot } from "firebase/firestore";
import { CollectorChecklist } from "./collector-checklist";
import { ExtractionReviewChecklist } from "./extraction-review-checklist";
import { ExtractionResults } from "./extraction-results";
import { CollectorActions } from "./collector-actions";
import { ProjectConfigGenerator } from "./project-config-generator";
interface PhaseSidebarProps {
projectId: string;
projectName?: string;
className?: string;
}
export function PhaseSidebar({ projectId, projectName, className }: PhaseSidebarProps) {
const [currentPhase, setCurrentPhase] = useState<string>("collector");
const [projectNameState, setProjectNameState] = useState<string>(projectName || "");
useEffect(() => {
if (!projectId) return;
const unsubscribe = onSnapshot(
doc(db, "projects", projectId),
(snapshot) => {
if (snapshot.exists()) {
const data = snapshot.data();
const phase = data?.currentPhase || "collector";
setCurrentPhase(phase);
setProjectNameState(data?.name || data?.productName || "");
}
}
);
return () => unsubscribe();
}, [projectId]);
return (
<div className={className}>
{/* Top: Show checklist based on current phase */}
{currentPhase === "collector" && (
<CollectorChecklist projectId={projectId} />
)}
{(currentPhase === "extraction_review" || currentPhase === "analyzed") && (
<ExtractionReviewChecklist projectId={projectId} />
)}
{/* Bottom: Phase-specific content */}
<div className="mt-6 pt-6 border-t">
{currentPhase === "collector" && (
<>
<h3 className="text-sm font-semibold mb-3">Quick Actions</h3>
<div className="space-y-2">
<ProjectConfigGenerator projectId={projectId} projectName={projectNameState} />
<CollectorActions projectId={projectId} />
</div>
</>
)}
{(currentPhase === "extraction_review" || currentPhase === "analyzed") && (
<p className="text-sm text-muted-foreground">
Review the extracted insights in the chat area above.
</p>
)}
{currentPhase === "vision" && (
<>
<h3 className="text-sm font-semibold mb-3">Vision Phase</h3>
<p className="text-xs text-muted-foreground">
Defining your product vision and MVP...
</p>
</>
)}
{currentPhase === "mvp" && (
<>
<h3 className="text-sm font-semibold mb-3">MVP Planning</h3>
<p className="text-xs text-muted-foreground">
Planning your minimum viable product...
</p>
</>
)}
</div>
</div>
);
}

View File

@@ -1,171 +0,0 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Download, Search, CheckCircle2, FileText } from "lucide-react";
import { toast } from "sonner";
interface ProjectConfigGeneratorProps {
projectId: string;
projectName: string;
}
export function ProjectConfigGenerator({ projectId, projectName }: ProjectConfigGeneratorProps) {
const [showDialog, setShowDialog] = useState(false);
const [searching, setSearching] = useState(false);
const [fileFound, setFileFound] = useState<boolean | null>(null);
const vibnConfig = {
projectId,
projectName,
version: "1.0",
};
const configContent = JSON.stringify(vibnConfig, null, 2);
const handleSearchFile = async () => {
setSearching(true);
setFileFound(null);
try {
// Prompt user to select the .vibn file
const input = document.createElement('input');
input.type = 'file';
input.accept = '.vibn,.json';
input.onchange = async (e: any) => {
const file = e.target?.files?.[0];
if (!file) {
setSearching(false);
return;
}
try {
const text = await file.text();
const config = JSON.parse(text);
if (config.projectId === projectId) {
setFileFound(true);
toast.success("File verified!", {
description: "Your .vibn file is correctly configured for this project",
});
} else {
setFileFound(false);
toast.error("Project ID mismatch", {
description: "This .vibn file is for a different project",
});
}
} catch (error) {
setFileFound(false);
toast.error("Invalid file", {
description: "Could not read or parse the .vibn file",
});
}
setSearching(false);
};
input.click();
} catch (error) {
toast.error("Failed to search for file");
setSearching(false);
}
};
const handleDownload = () => {
const blob = new Blob([configContent], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = ".vibn";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success("Configuration downloaded!", {
description: "Save it to your project root and restart Cursor",
});
};
return (
<>
<Button
variant="outline"
className="w-full justify-start h-auto py-2 px-3"
onClick={() => setShowDialog(true)}
>
<FileText className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="text-xs">Link Workspace</span>
</Button>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Link Your Workspace
</DialogTitle>
<DialogDescription>
Add a <code className="text-xs bg-muted px-1 py-0.5 rounded">.vibn</code> file to your project root so the Cursor extension automatically tracks sessions.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Primary Action */}
<Button onClick={handleDownload} className="w-full" size="lg">
<Download className="h-5 w-5 mr-2" />
Download .vibn
</Button>
{/* Instructions */}
<div className="text-sm space-y-3 bg-muted/50 p-3 rounded-md">
<p className="font-medium">Setup Steps:</p>
<ol className="text-xs text-muted-foreground space-y-1 list-decimal ml-4">
<li>Download the file above</li>
<li>Save it to your project root as <code className="bg-background px-1 rounded">.vibn</code></li>
<li>Commit and push to GitHub</li>
<li>Extension will auto-link sessions to this project</li>
</ol>
</div>
{/* Verification */}
<div className="pt-2 space-y-2">
<Button
onClick={handleSearchFile}
variant="outline"
className="w-full"
size="sm"
disabled={searching}
>
{searching ? (
<>
<Search className="h-4 w-4 mr-2 animate-spin" />
Verifying...
</>
) : fileFound === true ? (
<>
<CheckCircle2 className="h-4 w-4 mr-2 text-green-600" />
File Found
</>
) : (
<>
<Search className="h-4 w-4 mr-2" />
Verify File Added
</>
)}
</Button>
{fileFound === false && (
<p className="text-xs text-red-600 text-center">File not found or incorrect projectId</p>
)}
{fileFound === true && (
<p className="text-xs text-green-600 text-center">Ready! Push to GitHub and you're all set.</p>
)}
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,245 +0,0 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Loader2, CheckCircle2, Sparkles } from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
interface VisionFormProps {
projectId: string;
workspace?: string;
onComplete?: () => void;
}
export function VisionForm({ projectId, workspace, onComplete }: VisionFormProps) {
const [currentQuestion, setCurrentQuestion] = useState(1);
const [answers, setAnswers] = useState({
q1: '',
q2: '',
q3: '',
});
const [saving, setSaving] = useState(false);
const [generating, setGenerating] = useState(false);
const questions = [
{
number: 1,
key: 'q1' as const,
question: 'Who has the problem you want to fix and what is it?',
placeholder: 'E.g., Fantasy hockey GMs need an advantage over their competitors...',
},
{
number: 2,
key: 'q2' as const,
question: 'Tell me a story of this person using your tool and experiencing your vision?',
placeholder: 'E.g., The user connects their hockey pool site...',
},
{
number: 3,
key: 'q3' as const,
question: 'How much did that improve things for them?',
placeholder: 'E.g., They feel relieved and excited...',
},
];
const currentQ = questions[currentQuestion - 1];
const handleNext = () => {
if (!answers[currentQ.key].trim()) {
toast.error('Please answer the question before continuing');
return;
}
setCurrentQuestion(currentQuestion + 1);
};
const handleBack = () => {
setCurrentQuestion(currentQuestion - 1);
};
const handleSubmit = async () => {
if (!answers.q3.trim()) {
toast.error('Please answer the question');
return;
}
try {
setSaving(true);
// Save vision answers to Firestore
const response = await fetch(`/api/projects/${projectId}/vision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
visionAnswers: {
q1: answers.q1,
q2: answers.q2,
q3: answers.q3,
allAnswered: true,
updatedAt: new Date().toISOString(),
},
}),
});
if (!response.ok) {
throw new Error('Failed to save vision answers');
}
toast.success('Vision captured! Generating your MVP plan...');
setSaving(false);
setGenerating(true);
// Trigger MVP generation
const mvpResponse = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
method: 'POST',
});
if (!mvpResponse.ok) {
throw new Error('Failed to generate MVP plan');
}
setGenerating(false);
toast.success('MVP plan generated! Redirecting to plan page...');
// Redirect to plan page
if (workspace) {
setTimeout(() => {
window.location.href = `/${workspace}/project/${projectId}/plan`;
}, 1000);
}
onComplete?.();
} catch (error) {
console.error('Error saving vision:', error);
toast.error('Failed to save vision and generate plan');
setSaving(false);
setGenerating(false);
}
};
if (generating) {
return (
<div className="flex flex-col items-center justify-center h-[400px] space-y-4">
<Sparkles className="h-12 w-12 text-primary animate-pulse" />
<h3 className="text-xl font-semibold">Generating your MVP plan...</h3>
<p className="text-muted-foreground">This may take up to a minute</p>
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="max-w-2xl mx-auto py-8 space-y-6">
{/* Progress indicator */}
<div className="flex items-center justify-center space-x-4 mb-8">
{questions.map((q, idx) => (
<div key={q.number} className="flex items-center">
<div
className={cn(
'flex items-center justify-center w-8 h-8 rounded-full border-2 font-semibold text-sm transition-all',
currentQuestion > q.number
? 'bg-primary text-primary-foreground border-primary'
: currentQuestion === q.number
? 'border-primary text-primary'
: 'border-muted text-muted-foreground'
)}
>
{currentQuestion > q.number ? (
<CheckCircle2 className="h-4 w-4" />
) : (
q.number
)}
</div>
{idx < questions.length - 1 && (
<div
className={cn(
'w-12 h-0.5 mx-2 transition-all',
currentQuestion > q.number ? 'bg-primary' : 'bg-muted'
)}
/>
)}
</div>
))}
</div>
{/* Current question card */}
<Card>
<CardHeader>
<CardTitle className="text-2xl">
Question {currentQ.number} of 3
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-lg font-medium leading-relaxed">
{currentQ.question}
</label>
<Textarea
value={answers[currentQ.key]}
onChange={(e) =>
setAnswers({ ...answers, [currentQ.key]: e.target.value })
}
placeholder={currentQ.placeholder}
className="min-h-[120px] text-base"
autoFocus
/>
</div>
<div className="flex items-center justify-between pt-4">
<Button
variant="outline"
onClick={handleBack}
disabled={currentQuestion === 1}
>
Back
</Button>
{currentQuestion < 3 ? (
<Button onClick={handleNext}>
Next Question
</Button>
) : (
<Button onClick={handleSubmit} disabled={saving}>
{saving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Generate MVP Plan
</>
)}
</Button>
)}
</div>
</CardContent>
</Card>
{/* Show all answers summary */}
{currentQuestion > 1 && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Your Vision So Far
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{questions.slice(0, currentQuestion - 1).map((q) => (
<div key={q.number} className="text-sm">
<p className="font-medium text-muted-foreground mb-1">
Q{q.number}: {q.question}
</p>
<p className="text-foreground">{answers[q.key]}</p>
</div>
))}
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -1,50 +0,0 @@
"use client";
import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
import remarkGfm from "remark-gfm";
import type { FC } from "react";
import { cn } from "@/lib/utils";
export const MarkdownText: FC = () => {
return (
<MarkdownTextPrimitive
remarkPlugins={[remarkGfm]}
className="prose prose-sm dark:prose-invert max-w-none leading-relaxed"
components={{
h1: ({ className, ...props }) => (
<h1 className={cn("text-base font-bold mt-3 mb-1", className)} {...props} />
),
h2: ({ className, ...props }) => (
<h2 className={cn("text-sm font-bold mt-3 mb-1", className)} {...props} />
),
h3: ({ className, ...props }) => (
<h3 className={cn("text-sm font-semibold mt-2 mb-1", className)} {...props} />
),
p: ({ className, ...props }) => (
<p className={cn("mb-2 last:mb-0", className)} {...props} />
),
ul: ({ className, ...props }) => (
<ul className={cn("list-disc list-outside ml-4 mb-2 space-y-0.5", className)} {...props} />
),
ol: ({ className, ...props }) => (
<ol className={cn("list-decimal list-outside ml-4 mb-2 space-y-0.5", className)} {...props} />
),
li: ({ className, ...props }) => (
<li className={cn("leading-relaxed", className)} {...props} />
),
strong: ({ className, ...props }) => (
<strong className={cn("font-semibold", className)} {...props} />
),
code: ({ className, ...props }) => (
<code className={cn("bg-muted px-1 py-0.5 rounded text-xs font-mono", className)} {...props} />
),
pre: ({ className, ...props }) => (
<pre className={cn("bg-muted rounded-lg p-3 overflow-x-auto text-xs my-2", className)} {...props} />
),
blockquote: ({ className, ...props }) => (
<blockquote className={cn("border-l-2 border-muted-foreground/30 pl-3 italic text-muted-foreground my-2", className)} {...props} />
),
}}
/>
);
};

View File

@@ -1,312 +0,0 @@
"use client";
import {
ActionBarPrimitive,
BranchPickerPrimitive,
ComposerPrimitive,
MessagePrimitive,
ThreadPrimitive,
} from "@assistant-ui/react";
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
RefreshCwIcon,
SquareIcon,
} from "lucide-react";
import type { FC } from "react";
import { MarkdownText } from "./markdown-text";
// ---------------------------------------------------------------------------
// Thread root — Stackless style: flat on beige, no card
// ---------------------------------------------------------------------------
export const Thread: FC<{ userInitial?: string }> = ({ userInitial = "Y" }) => (
<ThreadPrimitive.Root
style={{
display: "flex",
flexDirection: "column",
height: "100%",
background: "#f6f4f0",
fontFamily: "Outfit, sans-serif",
}}
>
{/* Empty state */}
<ThreadPrimitive.Empty>
<div style={{
display: "flex", height: "100%",
flexDirection: "column", alignItems: "center", justifyContent: "center",
gap: 12, padding: "40px 32px",
}}>
<div style={{
width: 44, height: 44, borderRadius: 11, background: "#1a1a1a",
display: "flex", alignItems: "center", justifyContent: "center",
fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 500,
color: "#fff",
}}>
A
</div>
<div style={{ textAlign: "center" }}>
<p style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 4 }}>Atlas</p>
<p style={{ fontSize: "0.78rem", color: "#a09a90", maxWidth: 260, lineHeight: 1.5 }}>
Your product strategist. Let&apos;s define what you&apos;re building.
</p>
</div>
<div style={{ width: "100%", maxWidth: 600 }}>
<Composer userInitial={userInitial} />
</div>
</div>
</ThreadPrimitive.Empty>
{/* Messages */}
<ThreadPrimitive.Viewport style={{ flex: 1, overflowY: "auto", padding: "28px 32px" }}>
<ThreadPrimitive.Messages
components={{
UserMessage: (props) => <UserMessage {...props} userInitial={userInitial} />,
AssistantMessage,
}}
/>
</ThreadPrimitive.Viewport>
{/* Input bar */}
<div style={{ padding: "14px 32px 22px", flexShrink: 0 }}>
<Composer userInitial={userInitial} />
</div>
</ThreadPrimitive.Root>
);
// ---------------------------------------------------------------------------
// Composer — Stackless white pill input bar
// ---------------------------------------------------------------------------
const Composer: FC<{ userInitial?: string }> = () => (
<ComposerPrimitive.Root style={{ width: "100%" }}>
<div style={{
display: "flex", gap: 8,
padding: "5px 5px 5px 16px",
background: "#fff",
border: "1px solid #e0dcd4",
borderRadius: 10,
alignItems: "center",
boxShadow: "0 1px 4px #1a1a1a06",
}}>
<ComposerPrimitive.Input
placeholder="Describe your thinking..."
rows={1}
autoFocus
style={{
flex: 1,
border: "none",
background: "none",
fontSize: "0.86rem",
fontFamily: "Outfit, sans-serif",
color: "#1a1a1a",
padding: "8px 0",
resize: "none",
outline: "none",
minHeight: 24,
maxHeight: 120,
}}
/>
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<button
style={{
padding: "9px 16px",
borderRadius: 7,
border: "none",
background: "#1a1a1a",
color: "#fff",
fontSize: "0.78rem",
fontWeight: 600,
fontFamily: "Outfit, sans-serif",
cursor: "pointer",
transition: "opacity 0.15s",
flexShrink: 0,
}}
onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.8")}
onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")}
>
Send
</button>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<button
style={{
padding: "9px 16px",
borderRadius: 7,
border: "none",
background: "#eae6de",
color: "#8a8478",
fontSize: "0.78rem",
fontWeight: 600,
fontFamily: "Outfit, sans-serif",
cursor: "pointer",
flexShrink: 0,
display: "flex",
alignItems: "center",
gap: 6,
}}
>
<SquareIcon style={{ width: 10, height: 10 }} />
Stop
</button>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
</div>
</ComposerPrimitive.Root>
);
// ---------------------------------------------------------------------------
// Assistant message — black avatar, "Atlas" label, plain text
// ---------------------------------------------------------------------------
const AssistantMessage: FC = () => (
<MessagePrimitive.Root style={{ display: "flex", gap: 12, marginBottom: 22 }} className="group">
{/* Avatar */}
<div style={{
width: 28, height: 28, borderRadius: 7, flexShrink: 0, marginTop: 2,
background: "#1a1a1a",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "0.68rem", fontWeight: 700, color: "#fff",
fontFamily: "Newsreader, serif",
}}>
A
</div>
<div style={{ flex: 1, minWidth: 0 }}>
{/* Sender label */}
<div style={{
fontSize: "0.68rem", fontWeight: 600, color: "#a09a90",
marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.04em",
}}>
Atlas
</div>
{/* Message content */}
<div style={{ fontSize: "0.88rem", color: "#2a2824", lineHeight: 1.72 }}>
<MessagePrimitive.Content components={{ Text: AssistantText }} />
</div>
<AssistantActionBar />
<BranchPicker />
</div>
</MessagePrimitive.Root>
);
const AssistantText: FC = () => <MarkdownText />;
const AssistantActionBar: FC = () => (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="group-hover:opacity-100"
style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 6, opacity: 0, transition: "opacity 0.15s" }}
>
<ActionBarPrimitive.Copy asChild>
<button style={{
display: "flex", alignItems: "center", gap: 4,
fontSize: "0.68rem", color: "#b5b0a6",
background: "none", border: "none", cursor: "pointer",
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
}}>
<MessagePrimitive.If copied>
<CheckIcon style={{ width: 10, height: 10 }} /> Copied
</MessagePrimitive.If>
<MessagePrimitive.If copied={false}>
<CopyIcon style={{ width: 10, height: 10 }} /> Copy
</MessagePrimitive.If>
</button>
</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload asChild>
<button style={{
display: "flex", alignItems: "center", gap: 4,
fontSize: "0.68rem", color: "#b5b0a6",
background: "none", border: "none", cursor: "pointer",
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
}}>
<RefreshCwIcon style={{ width: 10, height: 10 }} /> Retry
</button>
</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
);
// ---------------------------------------------------------------------------
// User message — warm avatar, "You" label, same layout as Atlas
// ---------------------------------------------------------------------------
const UserMessage: FC<{ userInitial?: string }> = ({ userInitial = "Y" }) => (
<MessagePrimitive.Root style={{ display: "flex", gap: 12, marginBottom: 22 }} className="group">
{/* Avatar */}
<div style={{
width: 28, height: 28, borderRadius: 7, flexShrink: 0, marginTop: 2,
background: "#e8e4dc",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "0.68rem", fontWeight: 700, color: "#8a8478",
fontFamily: "Outfit, sans-serif",
}}>
{userInitial}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
{/* Sender label */}
<div style={{
fontSize: "0.68rem", fontWeight: 600, color: "#a09a90",
marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.04em",
}}>
You
</div>
{/* Message content */}
<div style={{ fontSize: "0.88rem", color: "#2a2824", lineHeight: 1.72, whiteSpace: "pre-wrap" }}>
<MessagePrimitive.Content components={{ Text: UserText }} />
</div>
<UserActionBar />
</div>
</MessagePrimitive.Root>
);
const UserText: FC<{ text: string }> = ({ text }) => <span>{text}</span>;
const UserActionBar: FC = () => (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="group-hover:opacity-100"
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 4, opacity: 0, transition: "opacity 0.15s" }}
>
<ActionBarPrimitive.Edit asChild>
<button style={{
fontSize: "0.68rem", color: "#b5b0a6",
background: "none", border: "none", cursor: "pointer",
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
}}>
Edit
</button>
</ActionBarPrimitive.Edit>
</ActionBarPrimitive.Root>
);
// ---------------------------------------------------------------------------
// Branch picker
// ---------------------------------------------------------------------------
const BranchPicker: FC = () => (
<BranchPickerPrimitive.Root
hideWhenSingleBranch
className="group-hover:opacity-100"
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 4, opacity: 0, transition: "opacity 0.15s" }}
>
<BranchPickerPrimitive.Previous asChild>
<button style={{ background: "none", border: "none", cursor: "pointer", color: "#b5b0a6" }}>
<ChevronLeftIcon style={{ width: 12, height: 12 }} />
</button>
</BranchPickerPrimitive.Previous>
<span style={{ fontSize: "0.68rem", color: "#b5b0a6" }}>
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
</span>
<BranchPickerPrimitive.Next asChild>
<button style={{ background: "none", border: "none", cursor: "pointer", color: "#b5b0a6" }}>
<ChevronRightIcon style={{ width: 12, height: 12 }} />
</button>
</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
);

View File

@@ -1,643 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Download, ExternalLink, Eye, EyeOff, MessageSquare, FolderKanban, Bot, Upload } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { OpenAIIcon } from '@/components/icons/custom-icons';
interface ChatGPTImportCardProps {
projectId?: string;
onImportComplete?: (importData: any) => void;
}
export function ChatGPTImportCard({ projectId, onImportComplete }: ChatGPTImportCardProps) {
const [conversationUrl, setConversationUrl] = useState('');
const [openaiApiKey, setOpenaiApiKey] = useState('');
const [showApiKey, setShowApiKey] = useState(false);
const [loading, setLoading] = useState(false);
const [importedData, setImportedData] = useState<any>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [hasStoredKey, setHasStoredKey] = useState(false);
const [checkingKey, setCheckingKey] = useState(true);
const [importType, setImportType] = useState<'conversation' | 'project' | 'gpt' | 'file'>('file');
// Check for stored OpenAI key on mount
useEffect(() => {
const checkStoredKey = async () => {
try {
const user = auth.currentUser;
if (!user) {
setCheckingKey(false);
return;
}
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
const hasOpenAI = data.keys.some((k: any) => k.service === 'openai');
setHasStoredKey(hasOpenAI);
}
} catch (error) {
console.error('Error checking for stored key:', error);
} finally {
setCheckingKey(false);
}
};
checkStoredKey();
}, []);
const extractConversationId = (urlOrId: string): { id: string | null; isShareLink: boolean } => {
// If it's already just an ID
if (!urlOrId.includes('/') && !urlOrId.includes('http')) {
const trimmed = urlOrId.trim();
// Check if it's a share link ID (UUID format)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed);
return { id: trimmed, isShareLink: isUUID };
}
// Extract from URL patterns:
// https://chat.openai.com/c/{id} - regular conversation (old)
// https://chatgpt.com/c/{id} - regular conversation (new)
// https://chat.openai.com/share/{id} - shared conversation (not supported)
// https://chatgpt.com/share/{id} - shared conversation (not supported)
// Check for share links first
const sharePatterns = [
/chat\.openai\.com\/share\/([a-zA-Z0-9-]+)/,
/chatgpt\.com\/share\/([a-zA-Z0-9-]+)/,
];
for (const pattern of sharePatterns) {
const match = urlOrId.match(pattern);
if (match) {
return { id: match[1], isShareLink: true };
}
}
// Regular conversation patterns
const conversationPatterns = [
/chat\.openai\.com\/c\/([a-zA-Z0-9-]+)/,
/chatgpt\.com\/c\/([a-zA-Z0-9-]+)/,
// GPT project conversations: https://chatgpt.com/g/g-p-[id]-[name]/c/[conversation-id]
/chatgpt\.com\/g\/g-p-[a-zA-Z0-9-]+\/c\/([a-zA-Z0-9-]+)/,
];
for (const pattern of conversationPatterns) {
const match = urlOrId.match(pattern);
if (match) {
return { id: match[1], isShareLink: false };
}
}
return { id: null, isShareLink: false };
};
const handleImport = async () => {
if (!conversationUrl.trim()) {
toast.error('Please enter a conversation ID or project ID');
return;
}
// If no stored key and no manual key provided, show error
if (!hasStoredKey && !openaiApiKey) {
toast.error('Please enter your OpenAI API key or add one in Keys page');
return;
}
setLoading(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in to import');
return;
}
const token = await user.getIdToken();
if (importType === 'file') {
// Import from uploaded conversations.json file
try {
const conversations = JSON.parse(conversationUrl);
const response = await fetch('/api/chatgpt/import-file', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversations,
projectId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.details || error.error || 'Import failed');
}
const data = await response.json();
toast.success(`✅ Imported ${data.imported} conversations!`);
setImportedData({
messageCount: data.imported,
title: `${data.imported} conversations`
});
if (onImportComplete) {
onImportComplete(data);
}
} catch (error: any) {
throw new Error(`File import failed: ${error.message}`);
}
// Reset form
setConversationUrl('');
setDialogOpen(false);
return;
}
// Determine which key to use
let keyToUse = openaiApiKey;
// If no manual key provided, try to get stored key
if (!keyToUse && hasStoredKey) {
const keyResponse = await fetch('/api/keys/get', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ service: 'openai' }),
});
if (keyResponse.ok) {
const keyData = await keyResponse.json();
keyToUse = keyData.keyValue;
}
}
if (importType === 'project') {
// Import OpenAI Project
const response = await fetch('/api/openai/projects', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
openaiApiKey: keyToUse,
projectId: conversationUrl.trim(),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.details || error.error || 'Failed to fetch project');
}
const data = await response.json();
toast.success(`Retrieved OpenAI Project: ${data.data.name || data.data.id}`);
setImportedData(data.data);
if (onImportComplete) {
onImportComplete(data.data);
}
} else {
// Import ChatGPT Conversation
const { id: conversationId, isShareLink } = extractConversationId(conversationUrl);
if (!conversationId) {
toast.error('Invalid ChatGPT URL or conversation ID');
return;
}
// Check if it's a share link
if (isShareLink) {
toast.error('Share links are not supported. Please use the direct conversation URL from your ChatGPT chat history.', {
description: 'Look for URLs like: chatgpt.com/c/... or chat.openai.com/c/...',
duration: 5000,
});
return;
}
const response = await fetch('/api/chatgpt/import', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversationId,
openaiApiKey: keyToUse,
projectId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.details || error.error || 'Import failed');
}
const data = await response.json();
setImportedData(data);
toast.success(`Imported: ${data.title} (${data.messageCount} messages)`);
if (onImportComplete) {
onImportComplete(data);
}
}
// Reset form
setConversationUrl('');
setDialogOpen(false);
} catch (error) {
console.error('Import error:', error);
toast.error(error instanceof Error ? error.message : 'Failed to import');
} finally {
setLoading(false);
}
};
return (
<>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Import from OpenAI</CardTitle>
<CardDescription>
Import ChatGPT conversations or OpenAI Projects
</CardDescription>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button>
<Download className="mr-2 h-4 w-4" />
Import from OpenAI
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Import from OpenAI</DialogTitle>
<DialogDescription>
Import ChatGPT conversations or OpenAI Projects
</DialogDescription>
</DialogHeader>
<Tabs value={importType} onValueChange={(v) => setImportType(v as 'conversation' | 'project' | 'gpt' | 'file')} className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="file" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
Upload File
</TabsTrigger>
<TabsTrigger value="conversation" className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Single Chat
</TabsTrigger>
</TabsList>
<TabsContent value="file" className="space-y-4 mt-4">
{/* File Upload */}
<div className="space-y-2">
<Label htmlFor="conversations-file">conversations.json File</Label>
<Input
id="conversations-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
const conversations = JSON.parse(event.target?.result as string);
setConversationUrl(JSON.stringify(conversations)); // Store in existing state
toast.success(`Loaded ${conversations.length} conversations`);
} catch (error) {
toast.error('Invalid JSON file');
}
};
reader.readAsText(file);
}
}}
/>
<p className="text-xs text-muted-foreground">
Upload your ChatGPT exported conversations.json file
</p>
</div>
{/* Instructions */}
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">How to export your ChatGPT data:</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Go to <a href="https://chatgpt.com/settings/data-controls" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">ChatGPT Settings Data Controls</a></li>
<li>Click "Export data"</li>
<li>Wait for the email from OpenAI (can take up to 24 hours)</li>
<li>Download and extract the ZIP file</li>
<li>Upload the <code className="text-xs">conversations.json</code> file here</li>
</ol>
</div>
{/* Info banner */}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
<p className="text-sm font-medium text-blue-700 dark:text-blue-400 mb-2">
💡 Privacy-friendly import
</p>
<p className="text-sm text-muted-foreground">
Your conversations are processed locally and only stored in your Vibn account.
We never send your data to third parties.
</p>
</div>
</TabsContent>
<TabsContent value="conversation" className="space-y-4 mt-4">
{/* Show stored key status */}
{hasStoredKey && (
<div className="rounded-lg border border-green-500/20 bg-green-500/5 p-3">
<p className="text-sm text-green-700 dark:text-green-400">
Using your stored OpenAI API key
</p>
</div>
)}
{/* OpenAI API Key (optional if stored) */}
{!hasStoredKey && (
<div className="space-y-2">
<Label htmlFor="openai-key">OpenAI API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="openai-key"
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
value={openaiApiKey}
onChange={(e) => setOpenaiApiKey(e.target.value)}
className="pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="flex items-center justify-between text-xs">
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
>
Get your API key <ExternalLink className="h-3 w-3" />
</a>
<a
href="/keys"
className="text-primary hover:underline"
>
Or save it in Keys page
</a>
</div>
</div>
)}
{/* Conversation URL */}
<div className="space-y-2">
<Label htmlFor="conversation-url">ChatGPT Conversation URL or ID</Label>
<Input
id="conversation-url"
placeholder="https://chatgpt.com/c/abc-123 or just abc-123"
value={conversationUrl}
onChange={(e) => setConversationUrl(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Copy the URL from your ChatGPT conversation or paste the conversation ID
</p>
</div>
{/* Instructions */}
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">How to find your conversation URL:</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Open the ChatGPT conversation you want to import</li>
<li>Look at the URL in your browser (must show <code className="text-xs">/c/</code>)</li>
<li>Copy the full URL: <code className="text-xs">chatgpt.com/c/...</code></li>
<li><strong>Note:</strong> Share links (<code className="text-xs">/share/</code>) won&apos;t work - you need the direct conversation URL</li>
</ol>
</div>
</TabsContent>
<TabsContent value="gpt" className="space-y-4 mt-4">
{/* GPT URL Input */}
<div className="space-y-2">
<Label htmlFor="gpt-url">ChatGPT GPT URL</Label>
<Input
id="gpt-url"
placeholder="https://chatgpt.com/g/g-p-abc123-your-gpt-name/project"
value={conversationUrl}
onChange={(e) => setConversationUrl(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Paste the full URL of your custom GPT
</p>
</div>
{/* Instructions */}
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">How to find your GPT URL:</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Open your custom GPT in ChatGPT</li>
<li>Copy the full URL from your browser</li>
<li>Paste it here</li>
<li>To import conversations with this GPT, switch to the "Chat" tab</li>
</ol>
</div>
{/* Info banner */}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
<p className="text-sm font-medium text-blue-700 dark:text-blue-400 mb-2">
💡 About GPT imports
</p>
<p className="text-sm text-muted-foreground">
This saves a reference to your custom GPT. To import actual conversations
with this GPT, go to a specific chat and use the "Chat" tab to import
that conversation.
</p>
</div>
</TabsContent>
<TabsContent value="project" className="space-y-4 mt-4">
{/* Show stored key status */}
{hasStoredKey && (
<div className="rounded-lg border border-green-500/20 bg-green-500/5 p-3">
<p className="text-sm text-green-700 dark:text-green-400">
Using your stored OpenAI API key
</p>
</div>
)}
{/* OpenAI API Key (optional if stored) */}
{!hasStoredKey && (
<div className="space-y-2">
<Label htmlFor="openai-key-project">OpenAI API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="openai-key-project"
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
value={openaiApiKey}
onChange={(e) => setOpenaiApiKey(e.target.value)}
className="pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="flex items-center justify-between text-xs">
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
>
Get your API key <ExternalLink className="h-3 w-3" />
</a>
<a
href="/keys"
className="text-primary hover:underline"
>
Or save it in Keys page
</a>
</div>
</div>
)}
{/* Project ID */}
<div className="space-y-2">
<Label htmlFor="project-id">OpenAI Project ID</Label>
<Input
id="project-id"
placeholder="proj_abc123..."
value={conversationUrl}
onChange={(e) => setConversationUrl(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Enter your OpenAI Project ID
</p>
</div>
{/* Instructions */}
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">How to find your Project ID:</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Go to <a href="https://platform.openai.com/settings/organization/projects" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">OpenAI Projects</a></li>
<li>Click on the project you want to import</li>
<li>Copy the Project ID (starts with <code className="text-xs">proj_</code>)</li>
<li>Or use the API to list all projects</li>
</ol>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={loading || !conversationUrl || (importType !== 'file' && !hasStoredKey && !openaiApiKey)}
>
{loading
? (importType === 'file' ? 'Importing...' : importType === 'project' ? 'Fetching Project...' : 'Importing Conversation...')
: (importType === 'file' ? 'Import Conversations' : importType === 'project' ? 'Fetch Project' : 'Import Conversation')
}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">What you can import:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Conversations:</strong> Full ChatGPT chat history with planning & brainstorming</li>
<li><strong>Projects:</strong> OpenAI Platform projects with API keys, usage, and settings</li>
<li>Requirements, specifications, and design decisions</li>
<li>Architecture notes and technical discussions</li>
</ul>
</div>
{importedData && (
<div className="rounded-lg border border-green-500/20 bg-green-500/5 p-4">
<div className="flex items-start gap-3">
{importedData.messageCount ? (
<MessageSquare className="h-5 w-5 text-green-600 mt-0.5" />
) : (
<FolderKanban className="h-5 w-5 text-green-600 mt-0.5" />
)}
<div className="flex-1">
<p className="text-sm font-medium text-green-700 dark:text-green-400">
Recently imported: {importedData.title || importedData.name || importedData.id}
</p>
<p className="text-xs text-muted-foreground mt-1">
{importedData.messageCount
? `${importedData.messageCount} messages • Conversation ID: ${importedData.conversationId}`
: `Project ID: ${importedData.id}`
}
</p>
</div>
</div>
</div>
)}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
<p className="text-sm font-medium text-blue-700 dark:text-blue-400 mb-2">
💡 Why import from OpenAI?
</p>
<p className="text-sm text-muted-foreground">
Connect your planning discussions from ChatGPT and your OpenAI Platform projects
with your actual coding sessions in Vibn. Our AI can reference everything to provide
better context and insights.
</p>
</div>
</div>
</CardContent>
</Card>
</>
);
}

View File

@@ -1,147 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { toast } from "sonner";
import { auth } from "@/lib/firebase/config";
import { Link2, CheckCircle2, Copy } from "lucide-react";
interface ProjectLinkerProps {
projectId: string;
projectName: string;
}
export function ProjectLinker({ projectId, projectName }: ProjectLinkerProps) {
const [workspacePath, setWorkspacePath] = useState("");
const [isLinking, setIsLinking] = useState(false);
const [isLinked, setIsLinked] = useState(false);
const handleLink = async () => {
if (!workspacePath.trim()) {
toast.error("Please enter a workspace path");
return;
}
try {
setIsLinking(true);
const user = auth.currentUser;
if (!user) {
toast.error("Not authenticated");
return;
}
const idToken = await user.getIdToken();
const response = await fetch("/api/extension/link-project", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({
projectId,
workspacePath: workspacePath.trim(),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to link extension");
}
setIsLinked(true);
toast.success("Extension linked successfully!");
} catch (error) {
console.error("Failed to link extension:", error);
toast.error(error instanceof Error ? error.message : "Failed to link extension");
} finally {
setIsLinking(false);
}
};
const copyProjectId = () => {
navigator.clipboard.writeText(projectId);
toast.success("Project ID copied to clipboard");
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Link2 className="h-5 w-5" />
Link Browser Extension
</CardTitle>
<CardDescription>
Connect your Cursor Monitor extension to this project so AI chats are automatically
captured.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isLinked ? (
<div className="flex items-center gap-2 text-green-600 bg-green-50 p-4 rounded-lg">
<CheckCircle2 className="h-5 w-5" />
<span className="font-medium">Extension linked to {projectName}</span>
</div>
) : (
<>
<div className="space-y-2">
<Label htmlFor="projectId">Project ID</Label>
<div className="flex gap-2">
<Input
id="projectId"
value={projectId}
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={copyProjectId}
title="Copy project ID"
>
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Copy this ID and add it to your Cursor Monitor extension settings.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="workspacePath">Workspace Path</Label>
<Input
id="workspacePath"
placeholder="/Users/yourname/projects/my-app"
value={workspacePath}
onChange={(e) => setWorkspacePath(e.target.value)}
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
Enter the full path to your Cursor workspace directory.
</p>
</div>
<Button onClick={handleLink} disabled={isLinking} className="w-full">
{isLinking ? "Linking..." : "Link Extension"}
</Button>
</>
)}
<div className="text-xs text-muted-foreground space-y-1 pt-2 border-t">
<p className="font-medium">How it works:</p>
<ol className="list-decimal list-inside space-y-1">
<li>Copy the Project ID above</li>
<li>Open Cursor Monitor extension settings</li>
<li>Paste the Project ID in the "Vibn Project ID" field</li>
<li>Enter your workspace path here and click "Link Extension"</li>
<li>All AI chats from Cursor will now be captured in this project</li>
</ol>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,230 +0,0 @@
import React from "react";
interface IconProps extends React.SVGProps<SVGSVGElement> {
className?: string;
}
export function CursorIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Cursor</title>
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" />
</svg>
);
}
export function V0Icon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>v0</title>
<path d="M14.066 6.028v2.22h5.729q.075-.001.148.005l-5.853 5.752a2 2 0 0 1-.024-.309V8.247h-2.353v5.45c0 2.322 1.935 4.222 4.258 4.222h5.675v-2.22h-5.675q-.03 0-.059-.003l5.729-5.629q.006.082.006.166v5.465H24v-5.465a4.204 4.204 0 0 0-4.205-4.205zM0 8.245l8.28 9.266c.839.94 2.396.346 2.396-.914V8.245H8.19v5.44l-4.86-5.44Z" />
</svg>
);
}
export function OpenAIIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
);
}
export function RailwayIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Railway</title>
<path d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z" />
</svg>
);
}
export function ThreadsIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Threads</title>
<path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z" />
</svg>
);
}
export function XIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>X</title>
<path d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z" />
</svg>
);
}
export function InstagramIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Instagram</title>
<path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077" />
</svg>
);
}
export function FacebookIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Facebook</title>
<path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z" />
</svg>
);
}
export function LinkedInIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>LinkedIn</title>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
}
export function QuickBooksIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>QuickBooks</title>
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm.642 4.1335c.9554 0 1.7296.776 1.7296 1.7332v9.0667h1.6c1.614 0 2.9275-1.3156 2.9275-2.933 0-1.6173-1.3136-2.9333-2.9276-2.9333h-.6654V7.3334h.6654c2.5722 0 4.6577 2.0897 4.6577 4.667 0 2.5774-2.0855 4.6666-4.6577 4.6666H12.642zM7.9837 7.333h3.3291v12.533c-.9555 0-1.73-.7759-1.73-1.7332V9.0662H7.9837c-1.6146 0-2.9277 1.316-2.9277 2.9334 0 1.6175 1.3131 2.9333 2.9277 2.9333h.6654v1.7332h-.6654c-2.5725 0-4.6577-2.0892-4.6577-4.6665 0-2.5771 2.0852-4.6666 4.6577-4.6666Z" />
</svg>
);
}
export function XeroIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Xero</title>
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm6.585 14.655c-1.485 0-2.69-1.206-2.69-2.689 0-1.485 1.207-2.691 2.69-2.691 1.485 0 2.69 1.207 2.69 2.691s-1.207 2.689-2.69 2.689zM7.53 14.644c-.099 0-.192-.041-.267-.116l-2.043-2.04-2.052 2.047c-.069.068-.16.108-.258.108-.202 0-.368-.166-.368-.368 0-.099.04-.191.111-.263l2.04-2.05-2.038-2.047c-.075-.069-.113-.162-.113-.261 0-.203.166-.366.368-.366.098 0 .188.037.258.105l2.055 2.048 2.048-2.045c.069-.071.162-.108.26-.108.211 0 .375.165.375.366 0 .098-.029.188-.104.258l-2.056 2.055 2.055 2.051c.068.069.104.16.104.258 0 .202-.165.368-.365.368h-.01zm8.017-4.591c-.796.101-.882.476-.882 1.404v2.787c0 .202-.165.366-.366.366-.203 0-.367-.165-.368-.366v-4.53c0-.204.16-.366.362-.366.166 0 .316.125.346.289.27-.209.6-.317.93-.317h.105c.195 0 .359.165.359.368 0 .201-.164.352-.375.359 0 0-.09 0-.164.008l.053-.002zm-3.091 2.205H8.625c0 .019.003.037.006.057.02.105.045.211.083.31.194.531.765 1.275 1.829 1.29.33-.003.631-.086.9-.229.21-.12.391-.271.525-.428.045-.058.09-.112.12-.168.18-.229.405-.186.54-.083.164.135.18.391.045.57l-.016.016c-.21.27-.435.495-.689.66-.255.164-.525.284-.811.345-.33.09-.645.104-.975.06-1.095-.135-2.01-.93-2.28-2.01-.06-.21-.09-.42-.09-.645 0-.855.421-1.695 1.125-2.205.885-.615 2.085-.66 3-.075.63.405 1.035 1.021 1.185 1.771.075.419-.21.794-.734.81l.068-.046zm6.129-2.223c-1.064 0-1.931.865-1.931 1.931 0 1.064.866 1.931 1.931 1.931s1.931-.867 1.931-1.931c0-1.065-.866-1.933-1.931-1.933v.002zm0 2.595c-.367 0-.666-.297-.666-.666 0-.367.3-.665.666-.665.367 0 .667.299.667.665 0 .369-.3.667-.667.666zm-8.04-2.603c-.91 0-1.672.623-1.886 1.466v.03h3.776c-.203-.855-.973-1.494-1.891-1.494v-.002z" />
</svg>
);
}
export function AnthropicIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Anthropic</title>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
);
}
export function GoogleGeminiIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Google Gemini</title>
<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81" />
</svg>
);
}
export function GoogleCloudIcon({ className, ...props }: IconProps) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="currentColor"
{...props}
>
<title>Google Cloud</title>
<path d="M12.19 2.38a9.344 9.344 0 0 0-9.234 6.893c.053-.02-.055.013 0 0-3.875 2.551-3.922 8.11-.247 10.941l.006-.007-.007.03a6.717 6.717 0 0 0 4.077 1.356h5.173l.03.03h5.192c6.687.053 9.376-8.605 3.835-12.35a9.365 9.365 0 0 0-2.821-4.552l-.043.043.006-.05A9.344 9.344 0 0 0 12.19 2.38zm-.358 4.146c1.244-.04 2.518.368 3.486 1.15a5.186 5.186 0 0 1 1.862 4.078v.518c3.53-.07 3.53 5.262 0 5.193h-5.193l-.008.009v-.04H6.785a2.59 2.59 0 0 1-1.067-.23h.001a2.597 2.597 0 1 1 3.437-3.437l3.013-3.012A6.747 6.747 0 0 0 8.11 8.24c.018-.01.04-.026.054-.023a5.186 5.186 0 0 1 3.67-1.69z" />
</svg>
);
}

View File

@@ -1,355 +0,0 @@
# Page Template Guide
A consistent, reusable page layout system for all pages in the application.
## Features
-**Consistent Layout**: Left rail + sidebar + main content area
-**Responsive Design**: Works on all screen sizes
-**Sidebar Navigation**: Built-in support for left sidebar with active states
-**Hero Section**: Optional hero banner with icon, title, description, and actions
-**Utility Components**: Pre-built sections, cards, grids, and empty states
-**Type-safe**: Full TypeScript support
---
## Basic Usage
```tsx
import { PageTemplate } from "@/components/layout/page-template";
import { Home } from "lucide-react";
export default function MyPage() {
return (
<PageTemplate
hero={{
icon: Home,
title: "My Page Title",
description: "A brief description of what this page does",
}}
>
<div>Your page content here</div>
</PageTemplate>
);
}
```
---
## With Sidebar Navigation
```tsx
import { PageTemplate } from "@/components/layout/page-template";
import { Home, Settings, Users } from "lucide-react";
export default function MyPage() {
return (
<PageTemplate
sidebar={{
title: "My Section",
description: "Navigate through options",
items: [
{ title: "Home", icon: Home, href: "/home", isActive: true },
{ title: "Settings", icon: Settings, href: "/settings" },
{ title: "Users", icon: Users, href: "/users", badge: "3" },
],
footer: <p className="text-xs">Footer content</p>,
}}
hero={{
icon: Home,
title: "My Page",
description: "Page description",
actions: [
{
label: "Create New",
onClick: () => console.log("Create"),
icon: Plus,
},
],
}}
>
<div>Your content</div>
</PageTemplate>
);
}
```
---
## Using Utility Components
### PageSection
Organized content sections with optional titles and actions:
```tsx
import { PageSection } from "@/components/layout/page-template";
import { Button } from "@/components/ui/button";
<PageSection
title="Recent Activity"
description="Your latest updates"
headerAction={<Button size="sm">View All</Button>}
>
<div>Section content</div>
</PageSection>
```
### PageCard
Styled cards with consistent padding and hover effects:
```tsx
import { PageCard } from "@/components/layout/page-template";
<PageCard padding="lg" hover>
<h3>Card Title</h3>
<p>Card content</p>
</PageCard>
```
### PageGrid
Responsive grid layouts:
```tsx
import { PageGrid } from "@/components/layout/page-template";
<PageGrid cols={3}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</PageGrid>
```
### PageEmptyState
Empty states with icon, title, description, and action:
```tsx
import { PageEmptyState } from "@/components/layout/page-template";
import { Inbox } from "lucide-react";
<PageEmptyState
icon={Inbox}
title="No messages yet"
description="Start a conversation to see messages here"
action={{
label: "New Message",
onClick: () => console.log("Create message"),
icon: Plus,
}}
/>
```
---
## Complete Example
Here's a full example combining all components:
```tsx
"use client";
import { useParams } from "next/navigation";
import { Home, Settings, Users, Plus, Mail } from "lucide-react";
import {
PageTemplate,
PageSection,
PageCard,
PageGrid,
PageEmptyState,
} from "@/components/layout/page-template";
export default function DashboardPage() {
const params = useParams();
const workspace = params.workspace as string;
const projectId = params.projectId as string;
return (
<PageTemplate
sidebar={{
title: "Dashboard",
description: "Overview and insights",
items: [
{
title: "Overview",
icon: Home,
href: `/${workspace}/project/${projectId}/overview`,
isActive: true,
},
{
title: "Settings",
icon: Settings,
href: `/${workspace}/project/${projectId}/settings`,
},
{
title: "Users",
icon: Users,
href: `/${workspace}/project/${projectId}/users`,
badge: "12",
},
],
footer: (
<p className="text-xs text-muted-foreground">
Last updated 5 minutes ago
</p>
),
}}
hero={{
icon: Home,
title: "Dashboard",
description: "Welcome back! Here's what's happening.",
actions: [
{
label: "Create Report",
onClick: () => console.log("Create report"),
icon: Plus,
},
],
}}
>
{/* Stats Grid */}
<PageSection title="Quick Stats">
<PageGrid cols={4}>
<PageCard>
<h3 className="text-sm font-medium text-muted-foreground">
Total Users
</h3>
<p className="text-3xl font-bold mt-2">1,234</p>
</PageCard>
<PageCard>
<h3 className="text-sm font-medium text-muted-foreground">
Active Sessions
</h3>
<p className="text-3xl font-bold mt-2">89</p>
</PageCard>
<PageCard>
<h3 className="text-sm font-medium text-muted-foreground">
Revenue
</h3>
<p className="text-3xl font-bold mt-2">$12,345</p>
</PageCard>
<PageCard>
<h3 className="text-sm font-medium text-muted-foreground">
Conversion
</h3>
<p className="text-3xl font-bold mt-2">3.2%</p>
</PageCard>
</PageGrid>
</PageSection>
{/* Recent Activity */}
<PageSection
title="Recent Activity"
description="Latest updates from your team"
>
<PageCard>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-primary/10" />
<div>
<p className="text-sm font-medium">User signed up</p>
<p className="text-xs text-muted-foreground">2 minutes ago</p>
</div>
</div>
</div>
</PageCard>
</PageSection>
{/* Empty State Example */}
<PageSection title="Messages">
<PageCard>
<PageEmptyState
icon={Mail}
title="No messages yet"
description="When you receive messages, they'll appear here"
action={{
label: "Send Message",
onClick: () => console.log("Send"),
icon: Plus,
}}
/>
</PageCard>
</PageSection>
</PageTemplate>
);
}
```
---
## Props Reference
### PageTemplate
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Main content |
| `sidebar` | `object` | Optional sidebar configuration |
| `hero` | `object` | Optional hero section configuration |
| `containerWidth` | `"default" \| "wide" \| "full"` | Content container width (default: "default") |
| `className` | `string` | Custom wrapper class |
| `contentClassName` | `string` | Custom content class |
### Sidebar Config
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Sidebar title |
| `description` | `string` | Optional subtitle |
| `items` | `array` | Navigation items |
| `footer` | `ReactNode` | Optional footer content |
### Hero Config
| Prop | Type | Description |
|------|------|-------------|
| `icon` | `LucideIcon` | Optional icon |
| `title` | `string` | Page title |
| `description` | `string` | Optional description |
| `actions` | `array` | Optional action buttons |
---
## Tips
1. **Consistent Icons**: Always use Lucide icons for consistency
2. **Active States**: Pass `isActive` to sidebar items to highlight current page
3. **Responsive**: Grid and card components are responsive by default
4. **Accessibility**: All components include proper ARIA attributes
5. **Performance**: Use "use client" only when you need client-side interactivity
---
## Migration Guide
To migrate an existing page:
1. Import `PageTemplate` and utility components
2. Wrap content in `<PageTemplate>`
3. Move navigation to `sidebar` prop
4. Move page header to `hero` prop
5. Replace div sections with `<PageSection>`
6. Replace card divs with `<PageCard>`
7. Use `<PageGrid>` for responsive grids
Before:
```tsx
<div className="flex h-full">
<div className="w-64 border-r">
{/* sidebar */}
</div>
<div className="flex-1">
{/* content */}
</div>
</div>
```
After:
```tsx
<PageTemplate sidebar={{...}} hero={{...}}>
{/* content */}
</PageTemplate>
```

View File

@@ -1,50 +0,0 @@
"use client";
import { ReactNode, useState, useMemo } from "react";
import { usePathname } from "next/navigation";
import { LeftRail } from "./left-rail";
import { RightPanel } from "./right-panel";
interface AppShellProps {
children: ReactNode;
workspace: string;
projectId: string;
projectName?: string;
}
export function AppShell({ children, workspace, projectId, projectName }: AppShellProps) {
const pathname = usePathname();
const [activeSection, setActiveSection] = useState<string>("home");
// Derive active section from pathname
const derivedSection = useMemo(() => {
if (pathname.includes('/overview')) return 'home';
if (pathname.includes('/design')) return 'design';
return activeSection;
}, [pathname, activeSection]);
const displayProjectName = projectName || "Product";
return (
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - App Navigation */}
<LeftRail
key={projectId} // Force re-render when projectId changes
activeSection={derivedSection}
onSectionChange={setActiveSection}
projectName={displayProjectName}
projectId={projectId}
workspace={workspace}
/>
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-y-auto">
{children}
</main>
{/* Right Panel - Activity & AI Chat */}
<RightPanel />
</div>
);
}

View File

@@ -1,161 +0,0 @@
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Github, Sparkles, Check } from "lucide-react";
import { toast } from "sonner";
import { CursorIcon } from "@/components/icons/custom-icons";
interface ConnectSourcesModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
projectId: string;
}
// Mock connection states - these would come from your database in production
const useConnectionStates = () => {
const [connections, setConnections] = useState({
vibn: false,
chatgpt: false,
github: false,
v0: false,
});
return { connections, setConnections };
};
export function ConnectSourcesModal({
open,
onOpenChange,
projectId,
}: ConnectSourcesModalProps) {
const { connections, setConnections } = useConnectionStates();
const handleConnect = async (source: "vibn" | "chatgpt" | "github" | "v0") => {
// Mock connection logic - replace with actual OAuth/API integration
const sourceName = source === "vibn" ? "Vib'n Extension" : source === "chatgpt" ? "ChatGPT" : source === "github" ? "GitHub" : "v0";
toast.success(`Connecting to ${sourceName}...`);
// Simulate connection delay
setTimeout(() => {
setConnections((prev) => ({ ...prev, [source]: true }));
toast.success(`${sourceName} connected successfully!`);
}, 1000);
};
const handleDisconnect = (source: "vibn" | "chatgpt" | "github" | "v0") => {
const sourceName = source === "vibn" ? "Vib'n Extension" : source === "chatgpt" ? "ChatGPT" : source === "github" ? "GitHub" : "v0";
setConnections((prev) => ({ ...prev, [source]: false }));
toast.success(`${sourceName} disconnected`);
};
const sources = [
{
id: "vibn" as const,
name: "Vib'n Extension",
description: "Connect the Vib'n extension with Cursor for seamless development tracking",
icon: CursorIcon,
color: "text-foreground",
bgColor: "bg-primary/10",
},
{
id: "chatgpt" as const,
name: "ChatGPT",
description: "Connect your ChatGPT project for AI-powered insights and context",
icon: Sparkles,
color: "text-green-500",
bgColor: "bg-green-500/10",
},
{
id: "github" as const,
name: "GitHub",
description: "Sync your repository to track code changes and generate screens",
icon: Github,
color: "text-foreground",
bgColor: "bg-foreground/10",
},
{
id: "v0" as const,
name: "v0",
description: "Connect v0 to generate and iterate on UI designs",
icon: Sparkles,
color: "text-blue-500",
bgColor: "bg-blue-500/10",
},
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Connect Sources</DialogTitle>
<DialogDescription>
Connect external sources to enhance your product development workflow
</DialogDescription>
</DialogHeader>
<div className="space-y-3 mt-4">
{sources.map((source) => {
const Icon = source.icon;
const isConnected = connections[source.id];
return (
<Card key={source.id} className={isConnected ? "border-primary" : ""}>
<CardContent className="p-4">
<div className="flex items-start gap-4">
<div className={`p-2.5 rounded-lg ${source.bgColor} shrink-0`}>
<Icon className={`h-5 w-5 ${source.color}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-sm">{source.name}</h3>
{isConnected && (
<div className="flex items-center gap-1 text-xs text-green-600 bg-green-500/10 px-2 py-0.5 rounded-full">
<Check className="h-3 w-3" />
Connected
</div>
)}
</div>
<p className="text-sm text-muted-foreground">
{source.description}
</p>
</div>
<div className="shrink-0">
{isConnected ? (
<Button
variant="outline"
size="sm"
onClick={() => handleDisconnect(source.id)}
>
Disconnect
</Button>
) : (
<Button
size="sm"
onClick={() => handleConnect(source.id)}
>
Connect
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,126 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import {
Settings,
HelpCircle,
User,
Key,
Palette,
Sparkles,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import { toast } from "sonner";
type NavSection = "home" | "ai-chat" | "docs" | "plan" | "design" | "tech" | "journey" | "settings";
const NAV_ITEMS = [
{ id: "home" as NavSection, icon: Sparkles, label: "AI Chat", href: "/project/{projectId}/overview" },
{ id: "design" as NavSection, icon: Palette, label: "Design", href: "/project/{projectId}/design" },
];
interface LeftRailProps {
activeSection: string;
onSectionChange: (section: string) => void;
projectName?: string;
projectId?: string;
workspace?: string;
}
export function LeftRail({ activeSection, onSectionChange, projectName, projectId, workspace = 'marks-account' }: LeftRailProps) {
return (
<div className="flex w-16 flex-col items-center border-r bg-card">
{/* Vib'n Logo */}
<Link href={`/${workspace}/projects`} className="flex h-14 w-16 items-center justify-center border-b">
<img
src="/vibn-black-circle-logo.png"
alt="Vib'n"
className="h-10 w-10 cursor-pointer hover:opacity-80 transition-opacity"
/>
</Link>
<div className="w-full flex flex-col items-center">
{/* Main Navigation Items */}
<div className="flex flex-col gap-2 w-full items-center">
{NAV_ITEMS.map((item) => {
if (!projectId) return null;
const fullHref = `/${workspace}${item.href.replace('{projectId}', projectId)}`;
return (
<Link
key={item.id}
href={fullHref}
onClick={() => onSectionChange(item.id)}
className={cn(
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
activeSection === item.id
? "text-primary bg-primary/5"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
title={item.label}
>
<item.icon className="h-5 w-5" />
<span className="text-[10px] font-medium">{item.label}</span>
{activeSection === item.id && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
)}
</Link>
);
})}
</div>
</div>
{/* Bottom Items */}
<div className="mt-auto flex flex-col gap-1 w-full items-center pb-4">
<Separator className="w-8 mb-1" />
<Link href={`/${workspace}/keys`}>
<Button
variant="ghost"
size="icon"
className="h-10 w-10"
title="API Keys"
>
<Key className="h-5 w-5" />
</Button>
</Link>
<Link href={`/${workspace}/connections`}>
<Button
variant="ghost"
size="icon"
className="h-10 w-10"
title="Settings & Connections"
>
<Settings className="h-5 w-5" />
</Button>
</Link>
<Button
variant="ghost"
size="icon"
className="h-10 w-10"
title="Help - Coming Soon"
onClick={() => toast.info("Help Center - Coming Soon")}
>
<HelpCircle className="h-5 w-5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-10 w-10 rounded-full"
title="Profile - Coming Soon"
onClick={() => toast.info("Profile Settings - Coming Soon")}
>
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
<User className="h-4 w-4" />
</div>
</Button>
</div>
</div>
);
}

View File

@@ -1,189 +0,0 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { ExternalLink, Copy, CheckCircle2 } from "lucide-react";
import { useState } from "react";
interface MCPConnectModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
projectId: string;
}
export function MCPConnectModal({
open,
onOpenChange,
projectId,
}: MCPConnectModalProps) {
const [copied, setCopied] = useState(false);
const mcpUrl = `https://api.vibn.co/mcp/projects/${projectId}`;
const copyToClipboard = () => {
navigator.clipboard.writeText(mcpUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-green-500/20 to-emerald-500/20">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="currentColor"
className="text-green-600"
/>
</svg>
</div>
<div>
<DialogTitle className="text-xl">Connect ChatGPT</DialogTitle>
<Badge variant="secondary" className="mt-1 text-xs">MCP Protocol</Badge>
</div>
</div>
<DialogDescription>
Link your ChatGPT to this project for real-time sync and AI-powered updates
</DialogDescription>
</DialogHeader>
<div className="space-y-6 mt-4">
{/* Step 1 */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
1
</div>
<h3 className="font-semibold">Copy your MCP Server URL</h3>
</div>
<div className="ml-8 space-y-2">
<div className="flex gap-2">
<Input
value={mcpUrl}
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={copyToClipboard}
>
{copied ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
This unique URL connects ChatGPT to your project
</p>
</div>
</div>
{/* Step 2 */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
2
</div>
<h3 className="font-semibold">Add Connector in ChatGPT</h3>
</div>
<div className="ml-8 space-y-2">
<ol className="text-sm space-y-2 text-muted-foreground list-decimal list-inside">
<li>Open ChatGPT settings</li>
<li>Navigate to <strong className="text-foreground">Connectors</strong></li>
<li>Click <strong className="text-foreground">New Connector</strong></li>
<li>Paste the MCP Server URL</li>
<li>Select <strong className="text-foreground">OAuth</strong> authentication</li>
</ol>
<Button variant="outline" size="sm" className="mt-3" asChild>
<a href="https://chatgpt.com/settings" target="_blank" rel="noopener noreferrer">
Open ChatGPT Settings
<ExternalLink className="ml-2 h-3 w-3" />
</a>
</Button>
</div>
</div>
{/* Step 3 */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
3
</div>
<h3 className="font-semibold">Authorize Access</h3>
</div>
<div className="ml-8">
<p className="text-sm text-muted-foreground">
ChatGPT will request permission to:
</p>
<ul className="mt-2 text-sm space-y-1 text-muted-foreground">
<li className="flex items-center gap-2">
<span className="text-green-600"></span>
Read your project vision and progress
</li>
<li className="flex items-center gap-2">
<span className="text-green-600"></span>
Add features and tasks
</li>
<li className="flex items-center gap-2">
<span className="text-green-600"></span>
Update documentation
</li>
</ul>
</div>
</div>
{/* Info Box */}
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
<div className="flex gap-3">
<div className="text-blue-600 shrink-0"></div>
<div className="text-sm space-y-1">
<p className="font-medium text-foreground">What happens after connecting?</p>
<p className="text-muted-foreground">
You'll be able to chat with ChatGPT about your project, and it will automatically
sync updates to your Vib'n workspace. Plan features, discuss architecture, and track
progress - all seamlessly connected.
</p>
</div>
</div>
</div>
{/* Actions */}
<div className="flex justify-between pt-4 border-t">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
<Button variant="outline" asChild>
<a
href="https://platform.openai.com/docs/mcp"
target="_blank"
rel="noopener noreferrer"
>
View MCP Docs
<ExternalLink className="ml-2 h-3 w-3" />
</a>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,38 +0,0 @@
"use client";
import { ChevronRight, Info } from "lucide-react";
import { Button } from "@/components/ui/button";
interface PageHeaderProps {
projectId: string;
projectName?: string;
projectEmoji?: string;
pageName: string;
}
export function PageHeader({
projectId,
projectName = "AI Proxy",
projectEmoji = "🤖",
pageName,
}: PageHeaderProps) {
return (
<div className="flex h-12 items-center justify-between border-b bg-card/50 px-6">
{/* Breadcrumbs */}
<div className="flex items-center gap-2 text-sm">
<span className="text-base">{projectEmoji}</span>
<span className="font-medium">{projectName}</span>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{pageName}</span>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" className="h-8 w-8">
<Info className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -1,306 +0,0 @@
"use client";
import { ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { LucideIcon } from "lucide-react";
interface PageTemplateProps {
children: ReactNode;
// Sidebar configuration
sidebar?: {
title?: string;
description?: string;
items: Array<{
title: string;
icon: LucideIcon;
href: string;
isActive?: boolean;
badge?: string | number;
}>;
footer?: ReactNode;
customContent?: ReactNode;
};
// Hero section configuration
hero?: {
icon?: LucideIcon;
iconBgColor?: string;
title: string;
description?: string;
actions?: Array<{
label: string;
onClick: () => void;
variant?: "default" | "outline" | "ghost" | "secondary";
icon?: LucideIcon;
}>;
};
// Container width
containerWidth?: "default" | "wide" | "full";
// Custom classes
className?: string;
contentClassName?: string;
}
export function PageTemplate({
children,
sidebar,
hero,
containerWidth = "default",
className,
contentClassName,
}: PageTemplateProps) {
const maxWidthClass = {
default: "max-w-5xl",
wide: "max-w-7xl",
full: "max-w-none",
}[containerWidth];
return (
<div className={cn("flex h-full w-full overflow-hidden", className)}>
{/* Left Sidebar Navigation (if provided) */}
{sidebar && (
<div className="w-64 border-r bg-muted/30 py-4">
{/* Sidebar Header */}
{sidebar.title && (
<div className="pb-4 border-b mb-4">
<h2 className="text-lg font-semibold">{sidebar.title}</h2>
{sidebar.description && (
<p className="text-sm text-muted-foreground mt-1">
{sidebar.description}
</p>
)}
</div>
)}
{/* Top Div: Navigation Items */}
<div className="px-3 py-4 space-y-1">
{sidebar.items.map((item) => {
const Icon = item.icon;
const isActive = item.isActive;
return (
<a
key={item.href}
href={item.href}
className={cn(
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-all group",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
)}
>
<div className="flex items-center gap-2 min-w-0">
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.title}</span>
</div>
{item.badge && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary shrink-0 font-medium">
{item.badge}
</span>
)}
</a>
);
})}
</div>
{/* Divider */}
<div className="border-t my-4" />
{/* Bottom Div: Custom Content */}
{sidebar.customContent && (
<div className="px-3 py-4">
{sidebar.customContent}
</div>
)}
</div>
)}
{/* Main Content Area */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Hero Section (if provided) */}
{hero && (
<div className="border-b bg-gradient-to-br from-primary/5 to-background">
<div className={cn(maxWidthClass, "mx-auto px-8 py-12")}>
<div className="flex items-start justify-between gap-6">
<div className="flex items-center gap-4 min-w-0 flex-1">
{hero.icon && (
<div
className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center shrink-0",
hero.iconBgColor || "bg-primary/10"
)}
>
<hero.icon className="h-6 w-6 text-primary" />
</div>
)}
<div className="min-w-0 flex-1">
<h1 className="text-3xl font-bold truncate">{hero.title}</h1>
{hero.description && (
<p className="text-sm text-muted-foreground mt-1">
{hero.description}
</p>
)}
</div>
</div>
{/* Hero Actions */}
{hero.actions && hero.actions.length > 0 && (
<div className="flex items-center gap-2 shrink-0">
{hero.actions.map((action, index) => (
<Button
key={index}
onClick={action.onClick}
variant={action.variant || "default"}
size="sm"
>
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
{action.label}
</Button>
))}
</div>
)}
</div>
</div>
</div>
)}
{/* Page Content */}
<div className="flex-1 overflow-auto">
<div className={cn(maxWidthClass, "mx-auto px-6 py-6", contentClassName)}>
{children}
</div>
</div>
</div>
</div>
);
}
// Utility Components for common page patterns
interface PageSectionProps {
title?: string;
description?: string;
children: ReactNode;
className?: string;
headerAction?: ReactNode;
}
export function PageSection({
title,
description,
children,
className,
headerAction,
}: PageSectionProps) {
return (
<div className={cn("space-y-4", className)}>
{(title || description || headerAction) && (
<div className="flex items-start justify-between gap-4">
<div>
{title && <h2 className="text-lg font-semibold">{title}</h2>}
{description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p>
)}
</div>
{headerAction && <div>{headerAction}</div>}
</div>
)}
{children}
</div>
);
}
interface PageCardProps {
children: ReactNode;
className?: string;
padding?: "sm" | "md" | "lg";
hover?: boolean;
}
export function PageCard({
children,
className,
padding = "md",
hover = false,
}: PageCardProps) {
const paddingClass = {
sm: "p-4",
md: "p-6",
lg: "p-8",
}[padding];
return (
<div
className={cn(
"border rounded-lg bg-card",
paddingClass,
hover && "hover:border-primary hover:shadow-md transition-all",
className
)}
>
{children}
</div>
);
}
interface PageGridProps {
children: ReactNode;
cols?: 1 | 2 | 3 | 4;
className?: string;
}
export function PageGrid({ children, cols = 2, className }: PageGridProps) {
const colsClass = {
1: "grid-cols-1",
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-2 lg:grid-cols-4",
}[cols];
return (
<div className={cn("grid grid-cols-1 gap-4", colsClass, className)}>
{children}
</div>
);
}
interface PageEmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
icon?: LucideIcon;
};
}
export function PageEmptyState({
icon: Icon,
title,
description,
action,
}: PageEmptyStateProps) {
return (
<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">
<Icon className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-xl font-semibold mb-2">{title}</h3>
{description && (
<p className="text-muted-foreground mb-6 max-w-md">{description}</p>
)}
{action && (
<Button onClick={action.onClick}>
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
{action.label}
</Button>
)}
</div>
);
}

View File

@@ -1,655 +0,0 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
LayoutDashboard,
Target,
ListChecks,
Palette,
Code2,
Server,
Zap,
ChevronDown,
ChevronRight,
Github,
MessageSquare,
Image,
Globe,
FolderOpen,
Inbox,
Users,
Eye,
Plus,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { MCPConnectModal } from "./mcp-connect-modal";
import { ConnectSourcesModal } from "./connect-sources-modal";
import { OpenAIIcon, V0Icon, CursorIcon } from "@/components/icons/custom-icons";
interface ProjectSidebarProps {
projectId: string;
activeSection?: string; // From left rail: 'projects', 'inbox', 'clients', etc.
workspace?: string;
}
// Map section IDs to display names
const SECTION_NAMES: Record<string, string> = {
home: 'Home',
product: 'Product',
site: 'Site',
pricing: 'Pricing',
content: 'Content',
social: 'Social',
inbox: 'Inbox',
people: 'People',
settings: 'Settings',
};
// Section-specific navigation items
const SECTION_ITEMS: Record<string, Array<{title: string; icon: any; href: string}>> = {
home: [
// { title: "Vision", icon: Eye, href: "/vision" }, // Hidden per user request
{ title: "Context", icon: FolderOpen, href: "/context" },
],
product: [
{ title: "Product Vision", icon: Target, href: "/plan" },
{ title: "Progress", icon: ListChecks, href: "/progress" },
{ title: "UI UX", icon: Palette, href: "/design" },
{ title: "Code", icon: Code2, href: "/code" },
{ title: "Deployment", icon: Server, href: "/deployment" },
{ title: "Automation", icon: Zap, href: "/automation" },
],
site: [
{ title: "Pages", icon: Globe, href: "/site/pages" },
{ title: "Templates", icon: Palette, href: "/site/templates" },
{ title: "Settings", icon: Target, href: "/site/settings" },
],
pricing: [
{ title: "Plans", icon: Target, href: "/pricing/plans" },
{ title: "Billing", icon: Code2, href: "/pricing/billing" },
{ title: "Invoices", icon: ListChecks, href: "/pricing/invoices" },
],
content: [
{ title: "Blog Posts", icon: Target, href: "/content/blog" },
{ title: "Case Studies", icon: Code2, href: "/content/cases" },
{ title: "Documentation", icon: ListChecks, href: "/content/docs" },
],
social: [
{ title: "Posts", icon: MessageSquare, href: "/social/posts" },
{ title: "Analytics", icon: Target, href: "/social/analytics" },
{ title: "Schedule", icon: ListChecks, href: "/social/schedule" },
],
inbox: [
{ title: "All", icon: Inbox, href: "/inbox/all" },
{ title: "Unread", icon: Target, href: "/inbox/unread" },
{ title: "Archived", icon: ListChecks, href: "/inbox/archived" },
],
people: [
{ title: "Team", icon: Users, href: "/people/team" },
{ title: "Clients", icon: Users, href: "/people/clients" },
{ title: "Contacts", icon: Users, href: "/people/contacts" },
],
};
type ConnectionStatus = 'inactive' | 'connected' | 'live';
export function ProjectSidebar({ projectId, activeSection = 'projects', workspace = 'marks-account' }: ProjectSidebarProps) {
const minWidth = 200;
const maxWidth = 500;
const [width, setWidth] = useState(minWidth);
const [isResizing, setIsResizing] = useState(false);
const [mcpModalOpen, setMcpModalOpen] = useState(false);
const [connectModalOpen, setConnectModalOpen] = useState(false);
const [isUserFlowsExpanded, setIsUserFlowsExpanded] = useState(true);
const [isProductScreensExpanded, setIsProductScreensExpanded] = useState(true);
const [getStartedCompleted, setGetStartedCompleted] = useState(false);
const pathname = usePathname();
const sidebarRef = useRef<HTMLDivElement>(null);
// Connection states - mock data, would come from API/database in production
const [connectionStates, setConnectionStates] = useState<{
github: ConnectionStatus;
openai: ConnectionStatus;
v0: ConnectionStatus;
cursor: ConnectionStatus;
}>({
github: 'connected',
openai: 'live',
v0: 'inactive',
cursor: 'connected',
});
// Helper function to get icon classes based on connection status
const getIconClasses = (status: ConnectionStatus) => {
switch (status) {
case 'inactive':
return 'text-muted-foreground/40';
case 'connected':
return 'text-muted-foreground';
case 'live':
return 'text-foreground';
default:
return 'text-muted-foreground/40';
}
};
const startResizing = useCallback((e: React.MouseEvent) => {
setIsResizing(true);
e.preventDefault();
}, []);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
const resize = useCallback(
(e: MouseEvent) => {
if (isResizing) {
const newWidth = e.clientX - 64; // Subtract left rail width (64px)
if (newWidth >= minWidth && newWidth <= maxWidth) {
setWidth(newWidth);
}
}
},
[isResizing]
);
useEffect(() => {
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResizing);
return () => {
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResizing);
};
}, [resize, stopResizing]);
// Determine header title based on active section
const isVAIPage = pathname?.includes('/v_ai_chat');
const headerTitle = isVAIPage ? 'v_ai' : (SECTION_NAMES[activeSection] || 'Home');
// Get section-specific items
const currentSectionItems = SECTION_ITEMS[activeSection] || SECTION_ITEMS['home'];
return (
<>
<aside
ref={sidebarRef}
style={{ width: `${width}px` }}
className="relative flex flex-col border-r bg-card/50"
>
{/* Header */}
<div className="flex h-14 items-center justify-between px-4 border-b">
<h2 className="font-semibold text-sm">{headerTitle}</h2>
{/* Connection icons - only show for Product section */}
{activeSection === 'product' && (
<div className="flex items-center gap-1">
{/* GitHub */}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 relative"
onClick={() => setConnectModalOpen(true)}
>
<Github className={cn("h-4 w-4", getIconClasses(connectionStates.github))} />
{connectionStates.github === 'live' && (
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
)}
</Button>
{/* OpenAI/ChatGPT */}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 relative"
onClick={() => setConnectModalOpen(true)}
>
<OpenAIIcon className={cn("h-4 w-4", getIconClasses(connectionStates.openai))} />
{connectionStates.openai === 'live' && (
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
)}
</Button>
{/* v0 */}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 relative"
onClick={() => setConnectModalOpen(true)}
>
<V0Icon className={cn("h-4 w-4", getIconClasses(connectionStates.v0))} />
{connectionStates.v0 === 'live' && (
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
)}
</Button>
{/* Cursor */}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 relative"
onClick={() => setConnectModalOpen(true)}
>
<CursorIcon className={cn("h-4 w-4", getIconClasses(connectionStates.cursor))} />
{connectionStates.cursor === 'live' && (
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
)}
</Button>
</div>
)}
</div>
{/* Section-Specific Navigation */}
<div className="px-2 py-1.5 space-y-0.5">
{/* v_ai - Persistent AI Chat - Always show for Home section */}
{activeSection === 'home' && (
<div className="mb-2">
<Link
href={`/${workspace}/project/${projectId}/v_ai_chat`}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors",
pathname === `/${workspace}/project/${projectId}/v_ai_chat`
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<img src="/vibn-logo-circle.png" alt="v_ai" className="h-4 w-4 shrink-0" />
<span className="truncate font-medium">v_ai</span>
</Link>
</div>
)}
{currentSectionItems.map((item) => {
const href = `/${workspace}/project/${projectId}${item.href}`;
const isActive = pathname === href;
return (
<Link
key={item.href}
href={href}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<item.icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.title}</span>
</Link>
);
})}
</div>
{/* Divider */}
<Separator className="my-1.5" />
{/* Context Section - Only show in Home section */}
{activeSection === 'home' && !isVAIPage && (
<div className="flex-1 overflow-hidden flex flex-col">
<div className="px-2 py-2">
<div className="flex items-center justify-between px-2 mb-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Context</h3>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={() => {
// Navigate to context page
window.location.href = `/${workspace}/project/${projectId}/context`;
}}
>
<Plus className="h-3 w-3 mr-1" />
Add
</Button>
</div>
</div>
</div>
)}
{/* Context Section - Shows items based on selected project section */}
{!isVAIPage && activeSection !== 'home' && (
<ScrollArea className="flex-1 px-2">
<div className="space-y-1 py-1.5">
{/* Show context based on current page */}
{pathname.includes('/plan') && (
<div className="space-y-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-xs font-semibold text-muted-foreground">VISION SOURCES</h3>
<button
onClick={() => setMcpModalOpen(true)}
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-xs hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
>
{/* OpenAI Icon (SVG) */}
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
className="opacity-60"
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="currentColor"
/>
</svg>
<span>Connect</span>
</button>
</div>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span className="text-xs">📄</span>
<span className="truncate">chatgpt-brainstorm.json</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span className="text-xs">📄</span>
<span className="truncate">product-notes.md</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm border border-dashed border-primary/30 text-primary hover:bg-primary/5 transition-colors">
<span className="text-xs">+</span>
<span className="truncate">Upload new file</span>
</button>
</div>
</div>
)}
{pathname.includes('/progress') && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">FILTERS</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>All Tasks</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>In Progress</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>Completed</span>
</button>
</div>
</div>
)}
{pathname.includes('/design') && (
<div className="space-y-1.5">
{/* Sandbox Indicator */}
<div className="px-2 py-1">
<h3 className="text-xs font-semibold text-muted-foreground">YOUR SANDBOX</h3>
</div>
{/* User Flows */}
<button
onClick={() => setIsUserFlowsExpanded(!isUserFlowsExpanded)}
className="flex w-full items-center justify-between px-2 py-1 rounded hover:bg-accent transition-colors"
>
<h3 className="text-xs font-semibold text-muted-foreground">USER FLOWS</h3>
{isUserFlowsExpanded ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
</button>
{isUserFlowsExpanded && (
<div className="space-y-0.5">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🚀 Onboarding</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span> Signup</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>👋 New User</span>
</button>
</div>
)}
{/* Product Screens */}
<button
onClick={() => setIsProductScreensExpanded(!isProductScreensExpanded)}
className="flex w-full items-center justify-between px-2 py-1 rounded hover:bg-accent transition-colors mt-2.5"
>
<h3 className="text-xs font-semibold text-muted-foreground">PRODUCT SCREENS</h3>
{isProductScreensExpanded ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
</button>
{isProductScreensExpanded && (
<div className="space-y-0.5">
<Link
href={`/${projectId}/design/landing-hero`}
className={cn(
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
pathname === `/${projectId}/design/landing-hero`
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<span></span>
<span>Landing Hero</span>
</div>
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">3</span>
</Link>
<Link
href={`/${projectId}/design/dashboard`}
className={cn(
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
pathname === `/${projectId}/design/dashboard`
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<span>📊</span>
<span>Dashboard</span>
</div>
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">1</span>
</Link>
<Link
href={`/${projectId}/design/pricing`}
className={cn(
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
pathname === `/${projectId}/design/pricing`
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<span>💳</span>
<span>Pricing</span>
</div>
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">2</span>
</Link>
<Link
href={`/${projectId}/design/user-profile`}
className={cn(
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
pathname === `/${projectId}/design/user-profile`
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<span>👤</span>
<span>User Profile</span>
</div>
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">1</span>
</Link>
{/* Actions */}
<div className="px-2 pt-1.5">
<button className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-primary/30 px-2 py-1.5 text-sm text-primary hover:bg-primary/5 transition-colors">
<span>+</span>
<span>New Screen</span>
</button>
</div>
</div>
)}
</div>
)}
{pathname.includes('/code') && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">QUICK LINKS</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>📦 Repository</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🌿 Branches</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🔀 Pull Requests</span>
</button>
</div>
</div>
)}
{pathname.includes('/deployment') && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">ENVIRONMENTS</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🟢 Production</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🟡 Staging</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>🔵 Development</span>
</button>
</div>
</div>
)}
{pathname.includes('/automation') && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">WORKFLOWS</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span> Active Jobs</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span> Scheduled</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
<span>📋 Logs</span>
</button>
</div>
</div>
)}
{/* Default: Left rail context sections */}
{activeSection === 'inbox' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">INBOX</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span className="text-muted-foreground">No new items</span>
</button>
</div>
</div>
)}
{activeSection === 'clients' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">PEOPLE</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Personal Projects</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>VIBN</span>
</button>
</div>
</div>
)}
{activeSection === 'invoices' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">GROW</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>No growth strategies yet</span>
</button>
</div>
</div>
)}
{activeSection === 'site' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">SITE</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Pages</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Settings</span>
</button>
</div>
</div>
)}
{activeSection === 'content' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">CONTENT</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Blog Posts</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Case Studies</span>
</button>
</div>
</div>
)}
{activeSection === 'social' && (
<div className="space-y-2">
<h3 className="px-2 text-xs font-semibold text-muted-foreground">SOCIAL</h3>
<div className="space-y-1">
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Posts</span>
</button>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
<span>Analytics</span>
</button>
</div>
</div>
)}
</div>
</ScrollArea>
)}
{/* Footer */}
<div className="flex h-10 items-center justify-between border-t px-4 text-xs text-muted-foreground">
<span>v1.0.0</span>
<button className="hover:text-foreground transition-colors">
Help
</button>
</div>
{/* Resize Handle */}
<div
onMouseDown={startResizing}
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/20 active:bg-primary/40 transition-colors"
/>
</aside>
<MCPConnectModal
open={mcpModalOpen}
onOpenChange={setMcpModalOpen}
projectId={projectId}
/>
<ConnectSourcesModal
open={connectModalOpen}
onOpenChange={setConnectModalOpen}
projectId={projectId}
/>
</>
);
}

View File

@@ -1,119 +0,0 @@
"use client";
import { useState } from "react";
import {
Sparkles,
ChevronLeft,
ChevronRight,
Send,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
export function RightPanel() {
const [isCollapsed, setIsCollapsed] = useState(true);
const [message, setMessage] = useState("");
if (isCollapsed) {
return (
<div className="relative flex w-12 flex-col items-center border-l bg-card/50 py-4">
<Button
variant="ghost"
size="icon"
onClick={() => setIsCollapsed(false)}
className="h-8 w-8"
>
<Sparkles className="h-4 w-4" />
</Button>
</div>
);
}
return (
<aside className="relative flex w-80 flex-col border-l bg-card/50">
{/* Header */}
<div className="flex h-12 items-center justify-between px-4 border-b">
<h2 className="font-semibold text-sm">AI Chat</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setIsCollapsed(true)}
className="h-7 w-7"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* Chat Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{/* Empty State */}
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="mb-3 rounded-full bg-primary/10 p-3">
<Sparkles className="h-6 w-6 text-primary" />
</div>
<h3 className="font-medium mb-1">AI Assistant</h3>
<p className="text-sm text-muted-foreground max-w-[250px]">
Ask questions about your project, get code suggestions, or
request documentation
</p>
</div>
{/* Example Chat Messages (for when conversation exists) */}
{/*
<div className="space-y-4">
<div className="flex gap-3">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
<Sparkles className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 space-y-1">
<div className="text-sm bg-muted rounded-lg p-3">
How can I help you with your project today?
</div>
</div>
</div>
<div className="flex gap-3 flex-row-reverse">
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground">
You
</div>
<div className="flex-1 space-y-1">
<div className="text-sm bg-primary text-primary-foreground rounded-lg p-3">
What's the current token usage?
</div>
</div>
</div>
</div>
*/}
</div>
</ScrollArea>
{/* Chat Input */}
<div className="border-t p-4 space-y-2">
<div className="flex gap-2">
<Textarea
placeholder="Ask AI about your project..."
value={message}
onChange={(e) => setMessage(e.target.value)}
className="min-h-[60px] resize-none"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
// TODO: Send message
setMessage("");
}
}}
/>
<Button size="icon" className="shrink-0">
<Send className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</aside>
);
}

View File

@@ -1,141 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
LayoutGrid,
Cable,
Key,
Users,
Settings,
DollarSign,
LogOut,
} from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { signOut } from "next-auth/react";
interface WorkspaceLeftRailProps {
activeSection?: string;
onSectionChange: (section: string) => void;
}
const navItems = [
{
id: 'projects',
label: 'Projects',
icon: LayoutGrid,
href: '/projects',
},
{
id: 'connections',
label: 'Connect',
icon: Cable,
href: '/connections',
},
{
id: 'keys',
label: 'Keys',
icon: Key,
href: '/keys',
},
{
id: 'costs',
label: 'Costs',
icon: DollarSign,
href: '/costs',
},
{
id: 'users',
label: 'Users',
icon: Users,
href: '/users',
},
];
export function WorkspaceLeftRail({ activeSection = 'projects', onSectionChange }: WorkspaceLeftRailProps) {
const pathname = usePathname();
// Extract workspace from pathname (e.g., /marks-account/projects -> marks-account)
const workspace = pathname?.split('/')[1] || 'marks-account';
const handleSignOut = async () => {
await signOut({ callbackUrl: "/auth" });
};
return (
<div className="flex w-16 flex-col items-center border-r bg-card">
{/* Vib'n Logo */}
<Link
href={`/${workspace}/projects`}
onClick={() => onSectionChange('projects')}
className="flex h-14 w-16 items-center justify-center border-b"
>
<img
src="/vibn-black-circle-logo.png"
alt="Vib'n"
className="h-10 w-10 cursor-pointer hover:opacity-80 transition-opacity"
/>
</Link>
<div className="pt-4 w-full flex flex-col items-center gap-2">
{/* Navigation Items */}
<div className="flex flex-col gap-3 w-full items-center">
{navItems.map((item) => {
const Icon = item.icon;
const fullHref = `/${workspace}${item.href}`;
const isActive = activeSection === item.id || pathname?.includes(item.href);
return (
<Link
key={item.id}
href={fullHref}
onClick={() => onSectionChange(item.id)}
className={cn(
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
isActive
? "text-primary bg-primary/5"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Icon className="h-5 w-5" />
<span className="text-[10px] font-medium">{item.label}</span>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
)}
</Link>
);
})}
</div>
</div>
{/* Bottom Items */}
<div className="mt-auto flex flex-col gap-1 w-full items-center pb-4">
<Separator className="w-8 mb-1" />
<Link
href={`/${workspace}/settings`}
className={cn(
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all",
"text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Settings className="h-5 w-5" />
<span className="text-[10px] font-medium">Settings</span>
</Link>
<button
onClick={handleSignOut}
className={cn(
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all",
"text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<LogOut className="h-5 w-5" />
<span className="text-[10px] font-medium">Sign Out</span>
</button>
</div>
</div>
);
}

View File

@@ -1,299 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Copy, Eye, EyeOff, RefreshCw, Trash2, ExternalLink } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
export function MCPConnectionCard() {
const [apiKey, setApiKey] = useState<string>('');
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
const [hasKey, setHasKey] = useState(false);
const mcpServerUrl = typeof window !== 'undefined'
? `${window.location.origin}/api/mcp`
: 'https://vibnai.com/api/mcp';
useEffect(() => {
loadExistingKey();
}, []);
const loadExistingKey = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/mcp/generate-key', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setApiKey(data.apiKey);
setHasKey(true);
}
} catch (error) {
console.error('Error loading MCP key:', error);
}
};
const generateKey = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in to generate an API key');
return;
}
const token = await user.getIdToken();
const response = await fetch('/api/mcp/generate-key', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to generate API key');
}
const data = await response.json();
setApiKey(data.apiKey);
setHasKey(true);
toast.success('MCP API key generated!');
} catch (error) {
console.error('Error generating API key:', error);
toast.error('Failed to generate API key');
} finally {
setLoading(false);
}
};
const revokeKey = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/mcp/generate-key', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to revoke API key');
}
setApiKey('');
setHasKey(false);
toast.success('MCP API key revoked');
} catch (error) {
console.error('Error revoking API key:', error);
toast.error('Failed to revoke API key');
} finally {
setLoading(false);
}
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text);
toast.success(`${label} copied to clipboard`);
};
const copyAllSettings = () => {
const settings = `Name: Vibn
Description: Access your Vibn coding projects, sessions, and AI conversation history
MCP Server URL: ${mcpServerUrl}
Authentication: Bearer
API Key: ${apiKey}`;
navigator.clipboard.writeText(settings);
toast.success('All settings copied! Paste into ChatGPT');
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>ChatGPT Integration (MCP)</CardTitle>
<CardDescription>
Connect ChatGPT to your Vibn data using the Model Context Protocol
</CardDescription>
</div>
<a
href="https://help.openai.com/en/articles/10206189-connecting-mcp-servers"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1"
>
Setup Guide <ExternalLink className="h-3 w-3" />
</a>
</div>
</CardHeader>
<CardContent className="space-y-6">
{!hasKey ? (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Generate an API key to connect ChatGPT to your Vibn projects. This key allows
ChatGPT to access your project data, coding sessions, and conversation history.
</p>
<Button onClick={generateKey} disabled={loading}>
{loading ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
'Generate MCP API Key'
)}
</Button>
</div>
) : (
<div className="space-y-6">
{/* MCP Server URL */}
<div className="space-y-2">
<Label htmlFor="mcp-url">MCP Server URL</Label>
<div className="flex gap-2">
<Input
id="mcp-url"
value={mcpServerUrl}
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(mcpServerUrl, 'MCP Server URL')}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
{/* API Key */}
<div className="space-y-2">
<Label htmlFor="mcp-key">API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="mcp-key"
type={showKey ? 'text' : 'password'}
value={apiKey}
readOnly
className="font-mono text-sm pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowKey(!showKey)}
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(apiKey, 'API Key')}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
This key doesn't expire. Keep it secure and never share it publicly.
</p>
</div>
{/* Quick Copy Button */}
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">Quick Setup for ChatGPT</p>
<p className="text-xs text-muted-foreground mb-3">
Click below to copy all settings, then paste them into ChatGPT's "New Connector" form
</p>
<Button onClick={copyAllSettings} className="w-full">
<Copy className="mr-2 h-4 w-4" />
Copy All Settings
</Button>
</div>
{/* Setup Instructions */}
<div className="rounded-lg border p-4 space-y-3">
<p className="text-sm font-medium">Setup Steps:</p>
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
<li>Click "Copy All Settings" above</li>
<li>Open ChatGPT and go to Settings Personalization Custom Tools</li>
<li>Click "Add New Connector"</li>
<li>Fill in the form with the copied settings</li>
<li>Set Authentication to "Bearer" and paste the API Key</li>
<li>Check "I understand" and click Create</li>
</ol>
</div>
{/* Try It */}
<div className="rounded-lg border border-green-500/20 bg-green-500/5 p-4">
<p className="text-sm font-medium text-green-700 dark:text-green-400 mb-2">
Try asking ChatGPT:
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> "Show me my Vibn projects"</li>
<li> "What are my recent coding sessions?"</li>
<li> "How much have I spent on AI this month?"</li>
</ul>
</div>
{/* Revoke Key */}
<div className="pt-4 border-t">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" disabled={loading}>
<Trash2 className="mr-2 h-4 w-4" />
Revoke API Key
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Revoke MCP API Key?</AlertDialogTitle>
<AlertDialogDescription>
This will immediately disconnect ChatGPT from your Vibn data. You'll need
to generate a new key to reconnect.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={revokeKey}>Revoke Key</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,207 +0,0 @@
"use client";
/**
* MCP Playground Component
*
* Interactive demo of Vibn's MCP capabilities
*/
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Code2, Database, MessageSquare, Zap } from 'lucide-react';
interface MCPRequest {
action: string;
params?: any;
}
export function MCPPlayground() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [selectedTool, setSelectedTool] = useState<string | null>(null);
const callMCP = async (request: MCPRequest) => {
setLoading(true);
setResult(null);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in to use MCP');
return;
}
const token = await user.getIdToken();
const response = await fetch('/api/mcp', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'MCP request failed');
}
const data = await response.json();
setResult(data);
toast.success('MCP request completed');
} catch (error) {
console.error('MCP error:', error);
toast.error(error instanceof Error ? error.message : 'MCP request failed');
} finally {
setLoading(false);
}
};
const tools = [
{
id: 'list_resources',
name: 'List Resources',
description: 'View all available MCP resources',
icon: Database,
action: () => callMCP({ action: 'list_resources' }),
},
{
id: 'read_projects',
name: 'Read Projects',
description: 'Get all your projects via MCP',
icon: Code2,
action: () => {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
callMCP({
action: 'read_resource',
params: { uri: `vibn://projects/${user.uid}` },
});
},
},
{
id: 'read_sessions',
name: 'Read Sessions',
description: 'Get all your coding sessions',
icon: Zap,
action: () => {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
callMCP({
action: 'read_resource',
params: { uri: `vibn://sessions/${user.uid}` },
});
},
},
];
return (
<div className="space-y-6">
<div className="space-y-2">
<h2 className="text-2xl font-bold">MCP Playground</h2>
<p className="text-muted-foreground">
Test Vibn's Model Context Protocol capabilities. This is what AI assistants see when they
query your project data.
</p>
</div>
{/* Tool Cards */}
<div className="grid gap-4 md:grid-cols-3">
{tools.map((tool) => (
<Card
key={tool.id}
className="cursor-pointer transition-all hover:shadow-md"
onClick={() => {
setSelectedTool(tool.id);
tool.action();
}}
>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<tool.icon className="h-5 w-5 text-primary" />
<CardTitle className="text-base">{tool.name}</CardTitle>
</div>
<CardDescription className="text-xs">{tool.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
{/* Loading State */}
{loading && (
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-center p-8">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-sm text-muted-foreground">Processing MCP request...</span>
</div>
</div>
</CardContent>
</Card>
)}
{/* Results */}
{result && (
<Card>
<CardHeader>
<CardTitle>MCP Response</CardTitle>
<CardDescription>
This is the data that would be sent to an AI assistant
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={JSON.stringify(result, null, 2)}
readOnly
className="min-h-[400px] font-mono text-xs"
/>
</CardContent>
</Card>
)}
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="text-base">About MCP</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
The Model Context Protocol (MCP) is a standard that allows AI assistants to access your
project data in a secure and structured way.
</p>
<p>
With Vibn's MCP server, AI assistants like Claude or ChatGPT can:
</p>
<ul className="ml-6 list-disc space-y-1">
<li>View your projects and coding sessions</li>
<li>Analyze your development patterns</li>
<li>Reference past AI conversations for context</li>
<li>Provide insights based on your actual work</li>
</ul>
<p className="pt-2">
<a
href="/MCP_SETUP.md"
target="_blank"
className="text-primary hover:underline"
>
Learn how to set up MCP with your AI assistant
</a>
</p>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,333 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import {
FolderOpen,
ChevronRight,
ChevronDown,
Loader2,
Search
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { auth } from "@/lib/firebase/config";
interface ContextItem {
id: string;
title: string;
type: 'insight' | 'file' | 'chat' | 'image';
timestamp?: Date;
content?: string;
}
interface InsightTheme {
theme: string;
description: string;
insights: ContextItem[];
}
interface CategorySection {
id: 'insights' | 'files' | 'chats' | 'images';
label: string;
items: ContextItem[];
themes?: InsightTheme[];
}
interface MissionContextTreeProps {
projectId: string;
}
export function MissionContextTree({ projectId }: MissionContextTreeProps) {
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set()
);
const [sections, setSections] = useState<CategorySection[]>([
{ id: 'insights', label: 'Insights', items: [] },
{ id: 'files', label: 'Files', items: [] },
{ id: 'chats', label: 'Chats', items: [] },
{ id: 'images', label: 'Images', items: [] },
]);
useEffect(() => {
if (projectId) {
fetchContextData();
}
}, [projectId]);
const fetchContextData = async () => {
setLoading(true);
try {
const user = auth.currentUser;
const headers: HeadersInit = {};
if (user) {
const token = await user.getIdToken();
headers.Authorization = `Bearer ${token}`;
} else {
console.log('[MissionContextTree] No user logged in, attempting unauthenticated fetch (development mode)');
}
// Fetch insights from AlloyDB knowledge chunks
console.log('[MissionContextTree] Fetching insights from:', `/api/projects/${projectId}/knowledge/chunks`);
const insightsResponse = await fetch(
`/api/projects/${projectId}/knowledge/chunks`,
{ headers }
);
if (!insightsResponse.ok) {
console.error('[MissionContextTree] Insights fetch failed:', insightsResponse.status, await insightsResponse.text());
}
const insightsData = insightsResponse.ok ? await insightsResponse.json() : { chunks: [] };
console.log('[MissionContextTree] Insights data:', insightsData);
const insights: ContextItem[] = insightsData.chunks?.map((chunk: any) => ({
id: chunk.id,
title: chunk.content?.substring(0, 50) || 'Untitled',
content: chunk.content,
type: 'insight' as const,
timestamp: chunk.created_at ? new Date(chunk.created_at) : undefined,
})) || [];
// Group insights into themes using AI
let insightThemes: InsightTheme[] = [];
if (insights.length > 0) {
console.log('[MissionContextTree] Grouping insights into themes...');
try {
const themesResponse = await fetch(
`/api/projects/${projectId}/knowledge/themes`,
{
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({ insights }),
}
);
if (themesResponse.ok) {
const themesData = await themesResponse.json();
console.log('[MissionContextTree] Got', themesData.themes?.length || 0, 'themes');
// Map themes to insights
insightThemes = (themesData.themes || []).map((theme: any) => ({
theme: theme.theme,
description: theme.description,
insights: insights.filter(i => theme.insightIds.includes(i.id)),
}));
} else {
console.error('[MissionContextTree] Themes fetch failed:', themesResponse.status);
}
} catch (themeError) {
console.error('[MissionContextTree] Error grouping themes:', themeError);
}
}
// Fetch files from Firebase Storage
console.log('[MissionContextTree] Fetching files from:', `/api/projects/${projectId}/storage/files`);
const filesResponse = await fetch(
`/api/projects/${projectId}/storage/files`,
{ headers }
);
if (!filesResponse.ok) {
console.error('[MissionContextTree] Files fetch failed:', filesResponse.status, await filesResponse.text());
}
const filesData = filesResponse.ok ? await filesResponse.json() : { files: [] };
console.log('[MissionContextTree] Files data:', filesData);
const files: ContextItem[] = filesData.files?.map((file: any) => ({
id: file.name,
title: file.name,
type: 'file' as const,
timestamp: file.timeCreated ? new Date(file.timeCreated) : undefined,
})) || [];
// Fetch chats and images from Firestore knowledge collection via API
console.log('[MissionContextTree] Fetching knowledge items from:', `/api/projects/${projectId}/knowledge/items`);
const knowledgeResponse = await fetch(
`/api/projects/${projectId}/knowledge/items`,
{ headers }
);
if (!knowledgeResponse.ok) {
console.error('[MissionContextTree] Knowledge items fetch failed:', knowledgeResponse.status, await knowledgeResponse.text());
}
const knowledgeData = knowledgeResponse.ok ? await knowledgeResponse.json() : { items: [] };
console.log('[MissionContextTree] Knowledge items count:', knowledgeData.items?.length || 0);
const chats: ContextItem[] = [];
const images: ContextItem[] = [];
(knowledgeData.items || []).forEach((item: any) => {
const contextItem: ContextItem = {
id: item.id,
title: item.title,
type: 'chat',
timestamp: item.createdAt ? new Date(item.createdAt) : undefined,
};
// Categorize based on sourceType
if (item.sourceType === 'imported_ai_chat' || item.sourceType === 'imported_chat' || item.sourceType === 'user_chat') {
chats.push({ ...contextItem, type: 'chat' });
} else if (item.sourceMeta?.filename?.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) {
images.push({ ...contextItem, type: 'image' });
}
});
console.log('[MissionContextTree] Final counts - Insights:', insights.length, 'Files:', files.length, 'Chats:', chats.length, 'Images:', images.length, 'Themes:', insightThemes.length);
setSections([
{ id: 'insights', label: 'Insights', items: insights, themes: insightThemes },
{ id: 'files', label: 'Files', items: files },
{ id: 'chats', label: 'Chats', items: chats },
{ id: 'images', label: 'Images', items: images },
]);
} catch (error) {
console.error('Error fetching context data:', error);
} finally {
setLoading(false);
}
};
const toggleSection = (sectionId: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(sectionId)) {
newExpanded.delete(sectionId);
} else {
newExpanded.add(sectionId);
}
setExpandedSections(newExpanded);
};
const filteredSections = sections.map(section => ({
...section,
items: section.items.filter(item =>
!searchQuery || item.title.toLowerCase().includes(searchQuery.toLowerCase())
)
}));
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Search */}
<div className="mb-3">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-7 h-8 text-sm"
/>
</div>
</div>
{/* File Tree */}
<div className="flex-1 overflow-auto space-y-0.5">
{filteredSections.map((section) => {
const isExpanded = expandedSections.has(section.id);
return (
<div key={section.id}>
{/* Section Header */}
<button
onClick={() => toggleSection(section.id)}
className="w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors"
>
{isExpanded ? (
<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" />
<span className="truncate">{section.label}</span>
</button>
{/* Section Items */}
{isExpanded && (
<div className="ml-6 space-y-0.5">
{section.items.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground italic">
No {section.label.toLowerCase()} yet
</div>
) : section.id === 'insights' && section.themes && section.themes.length > 0 ? (
// Render insights grouped by themes
section.themes.map((theme) => {
const themeKey = `theme-${section.id}-${theme.theme}`;
const isThemeExpanded = expandedSections.has(themeKey);
return (
<div key={themeKey} className="space-y-0.5">
{/* Theme Header */}
<button
onClick={() => toggleSection(themeKey)}
className="w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors"
>
{isThemeExpanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<FolderOpen className="h-3 w-3 shrink-0 text-amber-500" />
<span className="truncate font-medium text-xs">{theme.theme}</span>
<span className="text-[10px] text-muted-foreground">({theme.insights.length})</span>
</button>
{/* Theme Insights */}
{isThemeExpanded && (
<div className="ml-5 space-y-0.5">
{theme.insights.map((item) => (
<button
key={item.id}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-xs hover:bg-muted rounded transition-colors text-left",
"text-muted-foreground"
)}
title={item.content || item.title}
>
<span className="truncate">{item.title}</span>
</button>
))}
</div>
)}
</div>
);
})
) : (
// Render items normally for non-insights sections
section.items.map((item) => (
<button
key={item.id}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors text-left",
"text-muted-foreground"
)}
title={item.title}
>
<span className="truncate">{item.title}</span>
</button>
))
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -1,201 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Clock, History, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
import { auth, db } from "@/lib/firebase/config";
import { doc, getDoc, collection, query, where, orderBy, getDocs } from "firebase/firestore";
import { formatDistanceToNow } from "date-fns";
interface MissionRevision {
id: string;
content: string;
updatedAt: Date;
updatedBy: string;
source: 'ai' | 'user';
}
interface MissionIdeaSectionProps {
projectId: string;
}
export function MissionIdeaSection({ projectId }: MissionIdeaSectionProps) {
const [loading, setLoading] = useState(true);
const [content, setContent] = useState("");
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [revisions, setRevisions] = useState<MissionRevision[]>([]);
const [loadingRevisions, setLoadingRevisions] = useState(false);
useEffect(() => {
if (projectId) {
fetchMissionIdea();
}
}, [projectId]);
const fetchMissionIdea = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) return;
// Fetch current mission idea from project document
const projectRef = doc(db, 'projects', projectId);
const projectSnap = await getDoc(projectRef);
if (projectSnap.exists()) {
const data = projectSnap.data();
setContent(
data.missionIdea ||
"Help solo founders build and launch their products 10x faster by turning conversations into production-ready code, designs, and marketing."
);
setLastUpdated(data.missionIdeaUpdatedAt?.toDate() || null);
}
} catch (error) {
console.error('Error fetching mission idea:', error);
} finally {
setLoading(false);
}
};
const fetchRevisions = async () => {
setLoadingRevisions(true);
try {
const user = auth.currentUser;
if (!user) return;
// Fetch revision history
const revisionsRef = collection(db, 'missionRevisions');
const revisionsQuery = query(
revisionsRef,
where('projectId', '==', projectId),
orderBy('updatedAt', 'desc')
);
const revisionsSnap = await getDocs(revisionsQuery);
const revisionsList: MissionRevision[] = [];
revisionsSnap.forEach((doc) => {
const data = doc.data();
revisionsList.push({
id: doc.id,
content: data.content,
updatedAt: data.updatedAt?.toDate(),
updatedBy: data.updatedBy || 'AI',
source: data.source || 'ai',
});
});
setRevisions(revisionsList);
} catch (error) {
console.error('Error fetching revisions:', error);
} finally {
setLoadingRevisions(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-4">
{/* Content Card */}
<div className="rounded-lg border bg-card p-6">
<p className="text-xl font-medium leading-relaxed">
{content}
</p>
</div>
{/* Meta Information */}
<div className="flex items-center justify-between text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>
{lastUpdated
? `Last updated ${formatDistanceToNow(lastUpdated, { addSuffix: true })} by AI`
: 'Not yet updated'}
</span>
</div>
{/* Revision History */}
<Sheet>
<SheetTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={fetchRevisions}
>
<History className="h-4 w-4 mr-2" />
View History
</Button>
</SheetTrigger>
<SheetContent className="w-[500px] sm:w-[600px]">
<SheetHeader>
<SheetTitle>Revision History</SheetTitle>
<SheetDescription>
See how your mission idea has evolved over time
</SheetDescription>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-120px)] mt-6">
{loadingRevisions ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : revisions.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<History className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">No revision history yet</p>
</div>
) : (
<div className="space-y-4">
{revisions.map((revision, index) => (
<div
key={revision.id}
className="rounded-lg border bg-card p-4 space-y-2"
>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<span className="font-medium">
{revision.source === 'ai' ? 'AI Update' : 'Manual Edit'}
</span>
{index === 0 && (
<span className="px-2 py-0.5 rounded-full bg-primary/10 text-primary text-[10px] font-medium">
Current
</span>
)}
</div>
<span>
{formatDistanceToNow(revision.updatedAt, { addSuffix: true })}
</span>
</div>
<p className="text-sm leading-relaxed">
{revision.content}
</p>
<div className="text-xs text-muted-foreground">
{revision.updatedAt.toLocaleString()}
</div>
</div>
))}
</div>
)}
</ScrollArea>
</SheetContent>
</Sheet>
</div>
</div>
);
}

View File

@@ -1,130 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
LayoutDashboard,
Activity,
Box,
Map,
FileCode,
BarChart3,
Settings,
HelpCircle,
} from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
interface ProjectSidebarProps {
projectId: string;
}
const menuItems = [
{
title: "Overview",
icon: LayoutDashboard,
href: "/overview",
description: "Project dashboard and stats",
},
{
title: "Sessions",
icon: Activity,
href: "/sessions",
description: "AI coding sessions",
},
{
title: "Features",
icon: Box,
href: "/features",
description: "Feature planning and tracking",
},
{
title: "API Map",
icon: Map,
href: "/api-map",
description: "Auto-generated API docs",
},
{
title: "Architecture",
icon: FileCode,
href: "/architecture",
description: "Living architecture docs",
},
{
title: "Analytics",
icon: BarChart3,
href: "/analytics",
description: "Costs and metrics",
},
];
export function ProjectSidebar({ projectId }: ProjectSidebarProps) {
const pathname = usePathname();
return (
<div className="flex h-full flex-col">
{/* Project Header */}
<div className="flex h-14 items-center justify-between border-b px-4">
<div>
<h2 className="text-lg font-semibold">AI Proxy</h2>
<p className="text-xs text-muted-foreground">Project Dashboard</p>
</div>
</div>
{/* Navigation */}
<ScrollArea className="flex-1 px-3 py-4">
<div className="space-y-1">
{menuItems.map((item) => {
const href = `/${projectId}${item.href}`;
const isActive = pathname === href;
return (
<Link
key={item.href}
href={href}
className={cn(
"group flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-all hover:bg-accent",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:text-accent-foreground"
)}
title={item.description}
>
<item.icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.title}</span>
</Link>
);
})}
</div>
<Separator className="my-4" />
{/* Settings Section */}
<div className="space-y-1">
<Link
href={`/${projectId}/settings`}
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-all hover:bg-accent hover:text-accent-foreground"
>
<Settings className="h-4 w-4" />
<span>Settings</span>
</Link>
</div>
</ScrollArea>
{/* Footer */}
<div className="flex h-14 items-center justify-between border-t px-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>v1.0.0</span>
<Separator orientation="vertical" className="h-4" />
<span>Powered by AI</span>
</div>
<Button variant="ghost" size="icon" className="h-8 w-8">
<HelpCircle className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -1,143 +0,0 @@
"use client";
import { useState, useCallback, useEffect, useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
interface ResizableSidebarProps {
children: ReactNode;
defaultWidth?: number;
minWidth?: number;
maxWidth?: number;
}
export function ResizableSidebar({
children,
defaultWidth = 250,
minWidth = 200,
maxWidth = 400,
}: ResizableSidebarProps) {
const [width, setWidth] = useState(defaultWidth);
const [isCollapsed, setIsCollapsed] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [showPeek, setShowPeek] = useState(false);
const sidebarRef = useRef<HTMLDivElement>(null);
const initialXRef = useRef<number>(0);
const initialWidthRef = useRef<number>(defaultWidth);
const startResizing = useCallback((e: React.MouseEvent) => {
setIsResizing(true);
initialXRef.current = e.clientX;
initialWidthRef.current = width;
e.preventDefault();
}, [width]);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
const resize = useCallback(
(e: MouseEvent) => {
if (isResizing) {
const deltaX = e.clientX - initialXRef.current;
const newWidth = initialWidthRef.current + deltaX;
const clampedWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
setWidth(clampedWidth);
}
},
[isResizing, minWidth, maxWidth]
);
useEffect(() => {
if (isResizing) {
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResizing);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
return () => {
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResizing);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, resize, stopResizing]);
const handleMouseEnter = () => {
if (isCollapsed) {
setShowPeek(true);
}
};
const handleMouseLeave = () => {
if (isCollapsed) {
setTimeout(() => setShowPeek(false), 300);
}
};
return (
<>
{/* Peek trigger when collapsed */}
{isCollapsed && (
<div
className="absolute left-0 top-0 z-40 h-full w-2"
onMouseEnter={handleMouseEnter}
/>
)}
<aside
ref={sidebarRef}
style={{ width: isCollapsed ? 0 : `${width}px` }}
className={cn(
"relative flex flex-col border-r bg-card transition-all duration-300",
isCollapsed && "w-0 overflow-hidden"
)}
>
{!isCollapsed && children}
{/* Resize Handle */}
{!isCollapsed && (
<div
onMouseDown={startResizing}
className={cn(
"absolute right-0 top-0 h-full w-1 cursor-col-resize transition-colors hover:bg-primary/20",
isResizing && "bg-primary/40"
)}
/>
)}
{/* Collapse/Expand Button */}
<Button
variant="outline"
size="icon"
onClick={() => setIsCollapsed(!isCollapsed)}
className={cn(
"absolute -right-3 top-4 z-10 h-6 w-6 rounded-full shadow-sm",
isCollapsed && "left-2"
)}
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</aside>
{/* Peek Sidebar (when collapsed) */}
{isCollapsed && showPeek && (
<div
className="absolute left-0 top-0 z-30 h-full w-64 border-r bg-card shadow-lg"
style={{ width: `${defaultWidth}px` }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</div>
)}
</>
);
}