"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 = { 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 (
{MODE_LABELS[mode] ?? mode} {phase ? {phase} : null}
{artifacts && artifacts.length > 0 ? ( Using: {artifacts.join(', ')} ) : null}
); } 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([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(true); const [isInitialized, setIsInitialized] = useState(false); const [isSending, setIsSending] = useState(false); const [attachedFiles, setAttachedFiles] = useState>([]); const [routerMode, setRouterMode] = useState(null); const [routerPhase, setRouterPhase] = useState(null); const [routerArtifacts, setRouterArtifacts] = useState([]); const messagesEndRef = useRef(null); const fileInputRef = useRef(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(null); const [lastExtraction, setLastExtraction] = useState(null); const [currentPhase, setCurrentPhase] = useState("collector"); const [hasVisionAnswers, setHasVisionAnswers] = useState(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) => { 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((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) => { 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 (
); } if (!hasVisionAnswers) { return (
{/* Left Sidebar */} {/* Vision Form */}

Let's Start with Your Vision

Answer 3 quick questions to generate your MVP plan

{ setHasVisionAnswers(true); toast.success('Vision saved! MVP plan generated.'); }} />
); } return (
{/* Left Sidebar - Phase-based content */} {/* Main Chat Area */}
{/* Header with Reset Button */}

AI Assistant

Building your project step-by-step

{/* Messages Container - Scrollable */}
{messages.map((message) => (
{/* Avatar */} {message.role === 'assistant' ? (
AI
) : (
You
)} {/* Message Bubble */}
{message.content}
{/* GitHub Repo Picker (if AI requested it) */} {message.role === 'assistant' && message.showGitHubPicker && (
{ const confirmMessage = `Yes, I connected ${repo.full_name}`; sendChatMessage(confirmMessage); }} />
)}

{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

))} {isSending && (
AI thinking
Assistant is thinking…
)} {/* Show Extraction Results at bottom if in extraction_review phase */} {(currentPhase === "extraction_review" || currentPhase === "analyzed") && (
)}
{/* Floating Chat Input - Fixed at Bottom */}
{false && showExtractor && ( Paste AI chat transcript → extract signals
setExtractForm((prev) => ({ ...prev, title: e.target.value }))} />