"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>( new Set() ); const [sections, setSections] = useState([ { 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 (
); } return (
{/* Search */}
setSearchQuery(e.target.value)} className="pl-7 h-8 text-sm" />
{/* File Tree */}
{filteredSections.map((section) => { const isExpanded = expandedSections.has(section.id); return (
{/* Section Header */} {/* Section Items */} {isExpanded && (
{section.items.length === 0 ? (
No {section.label.toLowerCase()} yet
) : 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 (
{/* Theme Header */} {/* Theme Insights */} {isThemeExpanded && (
{theme.insights.map((item) => ( ))}
)}
); }) ) : ( // Render items normally for non-insights sections section.items.map((item) => ( )) )}
)}
); })}
); }