diff --git a/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx b/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx deleted file mode 100644 index 383e126..0000000 --- a/app/[workspace]/project/[projectId]/v_ai_chat/page.tsx +++ /dev/null @@ -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 = { - 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 }))} - /> -
-
- - -
-
-
- -