VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
"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<Set<string>>(
new Set()
);
const [sections, setSections] = useState<CategorySection[]>([
{ 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 (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Search */}
<div className="mb-3">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-7 h-8 text-sm"
/>
</div>
</div>
{/* File Tree */}
<div className="flex-1 overflow-auto space-y-0.5">
{filteredSections.map((section) => {
const isExpanded = expandedSections.has(section.id);
return (
<div key={section.id}>
{/* Section Header */}
<button
onClick={() => toggleSection(section.id)}
className="w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4 shrink-0" />
) : (
<ChevronRight className="h-4 w-4 shrink-0" />
)}
<FolderOpen className="h-4 w-4 shrink-0 text-blue-500" />
<span className="truncate">{section.label}</span>
</button>
{/* Section Items */}
{isExpanded && (
<div className="ml-6 space-y-0.5">
{section.items.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground italic">
No {section.label.toLowerCase()} yet
</div>
) : 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 (
<div key={themeKey} className="space-y-0.5">
{/* Theme Header */}
<button
onClick={() => toggleSection(themeKey)}
className="w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors"
>
{isThemeExpanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<FolderOpen className="h-3 w-3 shrink-0 text-amber-500" />
<span className="truncate font-medium text-xs">{theme.theme}</span>
<span className="text-[10px] text-muted-foreground">({theme.insights.length})</span>
</button>
{/* Theme Insights */}
{isThemeExpanded && (
<div className="ml-5 space-y-0.5">
{theme.insights.map((item) => (
<button
key={item.id}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-xs hover:bg-muted rounded transition-colors text-left",
"text-muted-foreground"
)}
title={item.content || item.title}
>
<span className="truncate">{item.title}</span>
</button>
))}
</div>
)}
</div>
);
})
) : (
// Render items normally for non-insights sections
section.items.map((item) => (
<button
key={item.id}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted rounded transition-colors text-left",
"text-muted-foreground"
)}
title={item.title}
>
<span className="truncate">{item.title}</span>
</button>
))
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
}