"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([]); const [project, setProject] = useState(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([]); 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) => { 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 ; case "extension": return ; case "chat": return ; case "file": return ; case "document": return ; default: return ; } }; 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 (
Loading context sources...
); } // 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 (
{/* Header */}

Context Sources

Add Context Upload documents or paste text to give the AI more context about your project.
{/* Mode Selector */}
{uploadMode === "file" ? ( /* File Upload Mode */
{selectedFiles.length > 0 && (
Selected: {selectedFiles.map(f => f.name).join(", ")}
)}

Documents will be stored for the Extractor AI to review and process. Supported formats: TXT, MD, PDF, DOC, JSON, CSV, XML

) : ( /* Text Paste Mode */ <>
setChatTitle(e.target.value)} />