refactor: simplify project nav to AI Chat (overview) + Design only

- AI Chat nav item now routes to /overview instead of /v_ai_chat
- Removed Plan, Docs, Tech, Journey nav items
- Deleted old v_ai_chat page
- Cleaned up unused imports and route detection logic

Made-with: Cursor
This commit is contained in:
2026-03-02 12:29:32 -08:00
parent 7be66f60b7
commit ea54440be7
3 changed files with 3 additions and 819 deletions

View File

@@ -1,765 +0,0 @@
"use client";
/* eslint-disable @next/next/no-img-element */
import { useState, useRef, useEffect } from "react";
import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Send, Loader2, Paperclip, X, FileText, RotateCcw, Upload, CheckCircle2, AlertTriangle, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { useSession } from "next-auth/react";
import { toast } from "sonner";
import { GitHubRepoPicker } from "@/components/ai/github-repo-picker";
import { PhaseSidebar } from "@/components/ai/phase-sidebar";
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
import { ExtractionResultsEditable } from "@/components/ai/extraction-results-editable";
import type { ChatExtractionData } from "@/lib/ai/chat-extraction-types";
import { VisionForm } from "@/components/ai/vision-form";
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
showGitHubPicker?: boolean;
meta?: {
mode?: string;
projectPhase?: string;
artifactsUsed?: string[];
};
}
const MODE_LABELS: Record<string, string> = {
collector_mode: "Collecting context",
extraction_review_mode: "Reviewing signals",
vision_mode: "Product vision",
mvp_mode: "MVP planning",
marketing_mode: "Marketing & launch",
general_chat_mode: "General product chat",
};
type ChatApiResponse = {
reply: string;
mode?: string;
projectPhase?: string;
artifactsUsed?: string[];
};
function ModeBadge({ mode, phase, artifacts }: { mode: string | null; phase: string | null; artifacts?: string[] }) {
if (!mode) return null;
return (
<div className="flex flex-col items-end gap-1 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<span className="rounded-full border px-2 py-0.5">
{MODE_LABELS[mode] ?? mode}
</span>
{phase ? <span>{phase}</span> : null}
</div>
{artifacts && artifacts.length > 0 ? (
<span className="text-[10px] text-muted-foreground/80">
Using: {artifacts.join(', ')}
</span>
) : null}
</div>
);
}
export default function GettingStartedPage() {
const params = useParams();
const projectId = params.projectId as string;
const workspace = params.workspace as string;
const { status: sessionStatus } = useSession();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isInitialized, setIsInitialized] = useState(false);
const [isSending, setIsSending] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<Array<{name: string, content: string, type: string}>>([]);
const [routerMode, setRouterMode] = useState<string | null>(null);
const [routerPhase, setRouterPhase] = useState<string | null>(null);
const [routerArtifacts, setRouterArtifacts] = useState<string[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [showExtractor, setShowExtractor] = useState(false);
const [extractForm, setExtractForm] = useState({
title: "",
provider: "chatgpt",
transcript: "",
sourceLink: "",
});
const [isImporting, setIsImporting] = useState(false);
const [extractionStatus, setExtractionStatus] = useState<"idle" | "importing" | "extracting" | "done" | "error">("idle");
const [extractionError, setExtractionError] = useState<string | null>(null);
const [lastExtraction, setLastExtraction] = useState<ChatExtractionData | null>(null);
const [currentPhase, setCurrentPhase] = useState<string>("collector");
const [hasVisionAnswers, setHasVisionAnswers] = useState<boolean>(false);
const [checkingVision, setCheckingVision] = useState(true);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Load project phase + vision answers from the Postgres-backed API
useEffect(() => {
if (!projectId) return;
const loadProject = async () => {
try {
const res = await fetch(`/api/projects/${projectId}`);
if (res.ok) {
const data = await res.json();
const phase = data.project?.currentPhase || 'collector';
setCurrentPhase(phase);
const hasAnswers = data.project?.visionAnswers?.allAnswered === true;
setHasVisionAnswers(hasAnswers);
}
} catch (error) {
console.error('Error loading project:', error);
} finally {
setCheckingVision(false);
}
};
loadProject();
}, [projectId]);
// Initialize with AI welcome message
useEffect(() => {
if (!isInitialized && projectId && sessionStatus !== 'loading') {
const initialize = async () => {
if (sessionStatus === 'unauthenticated') {
setIsLoading(false);
setIsInitialized(true);
setTimeout(() => sendChatMessage("Hello"), 500);
return;
}
// Signed in via NextAuth — load conversation history
try {
// Fetch existing conversation history
const historyResponse = await fetch(`/api/ai/conversation?projectId=${projectId}`);
let existingMessages: Message[] = [];
if (historyResponse.ok) {
type StoredMessage = {
role: 'user' | 'assistant';
content: string;
createdAt?: string | { _seconds: number };
};
const historyData = await historyResponse.json() as { messages: StoredMessage[] };
existingMessages = historyData.messages
.filter((msg) =>
msg.content !== '[VISION_AGENT_AUTO_START]' &&
msg.content.trim() !== "Hi! I'm here to help." &&
msg.content.trim() !== "Hello" // Filter out auto-generated greeting trigger
)
.map((msg) => ({
id: crypto.randomUUID(),
role: msg.role,
content: msg.content,
timestamp: msg.createdAt
? (typeof msg.createdAt === 'string'
? new Date(msg.createdAt)
: new Date(msg.createdAt._seconds * 1000))
: new Date(),
}));
console.log(`[Chat] Loaded ${existingMessages.length} messages from history`);
}
// If there's existing conversation, just show it
if (existingMessages.length > 0) {
setMessages(existingMessages);
setIsLoading(false);
setIsInitialized(true);
return;
}
// Otherwise, trigger AI to generate the first message
setIsLoading(false);
setIsInitialized(true);
// Automatically send a greeting to get AI's welcome message
setTimeout(() => {
sendChatMessage("Hello");
}, 500);
} catch (error) {
console.error('Error initializing chat:', error);
// Show error state but don't send automatic message
setMessages([{
id: crypto.randomUUID(),
role: 'assistant',
content: "Welcome! There was an issue loading your chat history, but let's get started. What would you like to work on?",
timestamp: new Date(),
}]);
} finally {
setIsLoading(false);
setIsInitialized(true);
}
};
initialize();
}
}, [projectId, isInitialized, sessionStatus]);
const sendChatMessage = async (messageContent: string) => {
const content = messageContent.trim();
if (!content) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setIsSending(true);
try {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId, message: content }),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Chat API error: ${response.status} ${errorText}`);
}
const data = (await response.json()) as ChatApiResponse;
const aiMessage: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: data.reply || 'No response generated.',
timestamp: new Date(),
meta: {
mode: data.mode,
projectPhase: data.projectPhase,
artifactsUsed: data.artifactsUsed,
},
};
setMessages((prev) => [...prev, aiMessage]);
setRouterMode(data.mode ?? null);
setRouterPhase(data.projectPhase ?? null);
setRouterArtifacts(data.artifactsUsed ?? []);
} catch (error) {
console.error('Chat send failed', error);
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
role: 'assistant',
content: 'Sorry, something went wrong talking to the AI.',
timestamp: new Date(),
},
]);
} finally {
setIsSending(false);
}
};
const handleSend = () => {
if ((!input.trim() && attachedFiles.length === 0) || isSending) return;
let messageContent = input.trim();
if (attachedFiles.length > 0) {
messageContent += '\n\n**Attached Files:**\n';
attachedFiles.forEach(file => {
messageContent += `\n--- ${file.name} ---\n${file.content}\n`;
});
}
setInput("");
setAttachedFiles([]);
sendChatMessage(messageContent);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleFileUpload = async (files: FileList | null) => {
if (!files) return;
const newFiles: Array<{name: string, content: string, type: string}> = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Check file size (max 100MB for large exports like ChatGPT conversations)
if (file.size > 100 * 1024 * 1024) {
toast.error(`File ${file.name} is too large (max 100MB)`);
continue;
}
// Read file content
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.onerror = reject;
reader.readAsText(file);
});
newFiles.push({
name: file.name,
content,
type: file.type,
});
}
setAttachedFiles([...attachedFiles, ...newFiles]);
toast.success(`Added ${newFiles.length} file(s)`);
};
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
const items = e.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
e.preventDefault();
const file = item.getAsFile();
if (file) {
await handleFileUpload([file] as unknown as FileList);
}
}
}
};
const removeFile = (index: number) => {
setAttachedFiles(attachedFiles.filter((_, i) => i !== index));
};
const handleResetChat = async () => {
if (!confirm('Are you sure you want to reset this conversation? This will delete all messages and start fresh.')) {
return;
}
try {
const response = await fetch(`/api/ai/conversation?projectId=${projectId}`, {
method: 'DELETE',
});
if (response.ok) {
toast.success('Chat reset! Reloading...');
// Reload the page to start fresh
setTimeout(() => window.location.reload(), 500);
} else {
toast.error('Failed to reset chat');
}
} catch (error) {
console.error('Error resetting chat:', error);
toast.error('Failed to reset chat');
}
};
const handleImportAndExtract = async () => {
if (!extractForm.transcript.trim()) {
toast.error("Please paste a transcript first");
return;
}
try {
setIsImporting(true);
setExtractionStatus("importing");
setExtractionError(null);
const importResponse = await fetch(`/api/projects/${projectId}/knowledge/import-ai-chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: extractForm.title || "Imported AI chat",
provider: extractForm.provider,
transcript: extractForm.transcript,
sourceLink: extractForm.sourceLink || null,
createdAtOriginal: new Date().toISOString(),
}),
});
if (!importResponse.ok) {
const errorData = await importResponse.json().catch(() => ({}));
throw new Error(errorData.error || "Failed to import transcript");
}
const { knowledgeItem } = await importResponse.json();
setExtractionStatus("extracting");
const extractResponse = await fetch(`/api/projects/${projectId}/extract-from-chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ knowledgeItemId: knowledgeItem.id }),
});
if (!extractResponse.ok) {
const errorData = await extractResponse.json().catch(() => ({}));
throw new Error(errorData.error || "Failed to extract signals");
}
const { extraction } = await extractResponse.json();
setLastExtraction(extraction.data);
setExtractionStatus("done");
toast.success("Signals extracted");
} catch (error) {
console.error("[chat extraction] failed", error);
setExtractionStatus("error");
setExtractionError(error instanceof Error ? error.message : "Unknown error");
toast.error("Could not extract signals");
} finally {
setIsImporting(false);
}
};
// Show vision form if no answers yet
if (checkingVision) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!hasVisionAnswers) {
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar */}
<CollapsibleSidebar>
<PhaseSidebar projectId={projectId} />
</CollapsibleSidebar>
{/* Vision Form */}
<div className="flex-1 flex flex-col overflow-auto">
<div className="border-b bg-background/95 backdrop-blur-sm">
<div className="max-w-3xl mx-auto px-4 py-3">
<h2 className="text-lg font-semibold">Let's Start with Your Vision</h2>
<p className="text-xs text-muted-foreground">Answer 3 quick questions to generate your MVP plan</p>
</div>
</div>
<div className="flex-1 overflow-auto">
<VisionForm
projectId={projectId}
workspace={workspace}
onComplete={() => {
setHasVisionAnswers(true);
toast.success('Vision saved! MVP plan generated.');
}}
/>
</div>
</div>
</div>
);
}
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar - Phase-based content */}
<CollapsibleSidebar>
<PhaseSidebar projectId={projectId} />
</CollapsibleSidebar>
{/* Main Chat Area */}
<div className="flex-1 flex flex-col overflow-hidden relative">
{/* Header with Reset Button */}
<div className="border-b bg-background/95 backdrop-blur-sm">
<div className="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between gap-3">
<div>
<h2 className="text-lg font-semibold">AI Assistant</h2>
<p className="text-xs text-muted-foreground">Building your project step-by-step</p>
</div>
<div className="flex flex-col items-end gap-2">
<ModeBadge mode={routerMode} phase={routerPhase} artifacts={routerArtifacts} />
<Button
variant="outline"
size="sm"
onClick={handleResetChat}
disabled={isLoading || isSending}
className="gap-2"
>
<RotateCcw className="h-3.5 w-3.5" />
Reset Chat
</Button>
</div>
</div>
</div>
{/* Messages Container - Scrollable */}
<div className="flex-1 overflow-y-auto pb-[200px]">
<div className="max-w-3xl mx-auto px-4 pt-8 pb-8">
<div className="space-y-6">
{messages.map((message) => (
<div
key={message.id}
className={cn(
"flex gap-3 animate-in fade-in slide-in-from-bottom-4 duration-500",
message.role === 'user' ? "flex-row-reverse" : ""
)}
>
{/* Avatar */}
{message.role === 'assistant' ? (
<div className="h-8 w-8 rounded-full shrink-0 overflow-hidden bg-white">
<img
src="/vibn-logo-circle.png"
alt="AI"
className="h-full w-full object-cover"
/>
</div>
) : (
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground shrink-0">
You
</div>
)}
{/* Message Bubble */}
<div className="flex-1 space-y-2 max-w-[85%]">
<div
className={cn(
"text-[15px] leading-relaxed rounded-2xl px-4 py-3 shadow-sm whitespace-pre-wrap",
message.role === 'assistant'
? "bg-muted/50"
: "bg-primary text-primary-foreground"
)}
>
{message.content}
</div>
{/* GitHub Repo Picker (if AI requested it) */}
{message.role === 'assistant' && message.showGitHubPicker && (
<div className="mt-2">
<GitHubRepoPicker
projectId={projectId}
onRepoSelected={(repo) => {
const confirmMessage = `Yes, I connected ${repo.full_name}`;
sendChatMessage(confirmMessage);
}}
/>
</div>
)}
<p className={cn(
"text-xs text-muted-foreground px-2",
message.role === 'user' ? "text-right" : ""
)}>
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
</div>
))}
{isSending && (
<div className="flex gap-3 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="h-8 w-8 rounded-full shrink-0 overflow-hidden bg-white">
<img
src="/vibn-logo-circle.png"
alt="AI thinking"
className="h-full w-full object-cover"
/>
</div>
<div className="flex-1 space-y-1">
<div className="text-sm bg-muted rounded-2xl px-5 py-3 w-fit">
<div className="flex items-center gap-2 text-muted-foreground">
<span className="text-xs font-medium">Assistant is thinking…</span>
</div>
</div>
</div>
</div>
)}
{/* Show Extraction Results at bottom if in extraction_review phase */}
{(currentPhase === "extraction_review" || currentPhase === "analyzed") && (
<div className="mt-8">
<ExtractionResultsEditable projectId={projectId} />
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
</div>
{/* Floating Chat Input - Fixed at Bottom */}
<div className="absolute bottom-0 left-0 right-0 border-t bg-background/95 backdrop-blur-sm shadow-lg z-10">
<div className="max-w-3xl mx-auto px-4 py-3 space-y-3">
{false && showExtractor && (
<Card className="border-primary/30 bg-primary/5">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" />
Paste AI chat transcript → extract signals
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground uppercase">Title</label>
<Input
placeholder="ChatGPT brainstorm"
value={extractForm.title}
onChange={(e) => setExtractForm((prev) => ({ ...prev, title: e.target.value }))}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground uppercase">Provider</label>
<select
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={extractForm.provider}
onChange={(e) => setExtractForm((prev) => ({ ...prev, provider: e.target.value }))}
>
{['chatgpt', 'gemini', 'claude', 'cursor', 'vibn', 'other'].map((provider) => (
<option key={provider} value={provider}>
{provider.toUpperCase()}
</option>
))}
</select>
</div>
</div>
<div className="space-y-1.5 text-sm">
<label className="text-xs text-muted-foreground uppercase">Transcript</label>
<Textarea
placeholder="Paste the AI conversation here..."
className="min-h-[120px]"
value={extractForm.transcript}
onChange={(e) => setExtractForm((prev) => ({ ...prev, transcript: e.target.value }))}
/>
</div>
<div className="flex flex-wrap items-center gap-3 text-sm">
<Button onClick={handleImportAndExtract} disabled={isImporting}>
{isImporting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Processing
</>
) : (
<>
<Upload className="h-4 w-4 mr-2" />
Import & Extract
</>
)}
</Button>
{extractionStatus === "done" && lastExtraction && (
<span className="text-emerald-600 text-xs flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" />
Signals captured below
</span>
)}
{extractionStatus === "error" && extractionError && (
<span className="text-destructive text-xs flex items-center gap-1">
<AlertTriangle className="h-3.5 w-3.5" />
{extractionError}
</span>
)}
</div>
{lastExtraction && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div className="rounded-lg border bg-background p-3">
<p className="text-xs text-muted-foreground uppercase">Working title</p>
<p className="font-medium">
{lastExtraction?.project_summary?.working_title || "Not captured"}
</p>
<p className="text-xs text-muted-foreground uppercase mt-2">One-liner</p>
<p>{lastExtraction?.project_summary?.one_liner || "—"}</p>
</div>
<div className="rounded-lg border bg-background p-3">
<p className="text-xs text-muted-foreground uppercase">Primary problem</p>
<p>{lastExtraction?.product_vision?.problem_statement?.description || "Not detected"}</p>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Attached Files Display */}
{attachedFiles.length > 0 && (
<div className="flex flex-wrap gap-2">
{attachedFiles.map((file, index) => (
<div
key={index}
className="flex items-center gap-2 bg-muted px-3 py-2 rounded-lg text-sm border"
>
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{file.name}</span>
<button
onClick={() => removeFile(index)}
className="ml-1 hover:text-destructive"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
<div className="flex gap-2 items-end">
<div className="flex-1">
<Textarea
placeholder="Message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
className="min-h-[48px] max-h-[120px] resize-none bg-background shadow-sm text-[15px]"
disabled={isLoading || isSending}
/>
</div>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
multiple
accept=".txt,.md,.json,.js,.ts,.tsx,.jsx,.py,.java,.cpp,.c,.html,.css,.xml,.yaml,.yml"
className="hidden"
onChange={(e) => handleFileUpload(e.target.files)}
/>
{/* File Upload Button */}
<Button
size="icon"
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isLoading || isSending}
className="h-[48px] w-[48px] shrink-0"
title="Attach files"
>
<Paperclip className="h-4 w-4" />
</Button>
{/* Send Button */}
<Button
size="icon"
onClick={handleSend}
disabled={(!input.trim() && attachedFiles.length === 0) || isLoading || isSending}
className="h-[48px] w-[48px] shrink-0"
>
{isSending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-1.5 px-1">
Press <kbd className="px-1 py-0.5 bg-muted rounded text-[10px] border">Enter</kbd> to send <kbd className="px-1 py-0.5 bg-muted rounded text-[10px] border">Shift+Enter</kbd> for new line
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -18,21 +18,8 @@ export function AppShell({ children, workspace, projectId, projectName }: AppShe
// Derive active section from pathname
const derivedSection = useMemo(() => {
if (pathname.includes('/v_ai_chat')) {
return 'ai-chat';
} else if (pathname.includes('/overview')) {
return 'home';
} else if (pathname.includes('/docs')) {
return 'docs';
} else if (pathname.includes('/plan') || pathname.includes('/timeline-plan')) {
return 'plan';
} else if (pathname.includes('/design')) {
return 'design';
} else if (pathname.includes('/tech')) {
return 'tech';
} else if (pathname.includes('/journey')) {
return 'journey';
}
if (pathname.includes('/overview')) return 'home';
if (pathname.includes('/design')) return 'design';
return activeSection;
}, [pathname, activeSection]);

View File

@@ -2,26 +2,12 @@
import { cn } from "@/lib/utils";
import {
LayoutGrid,
Inbox,
Users,
Receipt,
Globe,
FileText,
MessageCircle,
Settings,
HelpCircle,
User,
DollarSign,
Key,
Palette,
Target,
Megaphone,
ClipboardList,
Code2,
Sparkles,
Cog,
GitBranch,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
@@ -31,11 +17,8 @@ import { toast } from "sonner";
type NavSection = "home" | "ai-chat" | "docs" | "plan" | "design" | "tech" | "journey" | "settings";
const NAV_ITEMS = [
{ id: "plan" as NavSection, icon: ClipboardList, label: "Plan", href: "/project/{projectId}/plan" },
{ id: "docs" as NavSection, icon: FileText, label: "Docs", href: "/project/{projectId}/docs" },
{ 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" },
{ id: "tech" as NavSection, icon: Cog, label: "Tech", href: "/project/{projectId}/tech" },
{ id: "journey" as NavSection, icon: GitBranch, label: "Journey", href: "/project/{projectId}/journey" },
];
interface LeftRailProps {
@@ -61,27 +44,6 @@ export function LeftRail({ activeSection, onSectionChange, projectName, projectI
<div className="w-full flex flex-col items-center">
{/* Main Navigation Items */}
<div className="flex flex-col gap-2 w-full items-center">
{/* AI Chat - Always visible */}
{projectName && projectId && (
<Link
href={`/${workspace}/project/${projectId}/v_ai_chat`}
className={cn(
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
activeSection === "ai-chat"
? "text-primary bg-primary/5"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
title="AI Chat"
>
<Sparkles className="h-5 w-5" />
<span className="text-[10px] font-medium">AI Chat</span>
{activeSection === "ai-chat" && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
)}
</Link>
)}
{/* Other Nav Items */}
{NAV_ITEMS.map((item) => {
if (!projectId) return null;
const fullHref = `/${workspace}${item.href.replace('{projectId}', projectId)}`;