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,408 @@
"use client";
import { use, useState, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
FileText,
Plus,
Search,
Filter,
MoreHorizontal,
Star,
Info,
Share2,
Archive,
Loader2,
Target,
Lightbulb,
MessageSquare,
BookOpen,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import { toast } from "sonner";
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
type DocType = "all" | "vision" | "features" | "research" | "chats";
type ViewType = "public" | "private" | "archived";
interface Document {
id: string;
title: string;
type: DocType;
owner: string;
dateModified: string;
visibility: ViewType;
starred: boolean;
chunkCount?: number;
}
export default function DocsPage({
params,
}: {
params: Promise<{ workspace: string; projectId: string }>;
}) {
const { workspace, projectId } = use(params);
const [activeView, setActiveView] = useState<ViewType>("public");
const [filterType, setFilterType] = useState<DocType>("all");
const [searchQuery, setSearchQuery] = useState("");
const [documents, setDocuments] = useState<Document[]>([]);
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<"modified" | "created">("modified");
useEffect(() => {
loadDocuments();
}, [projectId, activeView, filterType]);
const loadDocuments = async () => {
try {
setLoading(true);
const response = await fetch(
`/api/projects/${projectId}/knowledge/items?visibility=${activeView}&type=${filterType}`
);
if (response.ok) {
const data = await response.json();
// Use returned items or mock data if empty
if (data.items && data.items.length > 0) {
// Transform knowledge items to document format
const docs = data.items.map((item: any, index: number) => ({
id: item.id,
title: item.title,
type: item.sourceType === 'vision' ? 'vision' :
item.sourceType === 'feature' ? 'features' :
item.sourceType === 'chat' ? 'chats' : 'research',
owner: "You",
dateModified: item.updatedAt || item.createdAt,
visibility: activeView,
starred: false,
chunkCount: item.chunkCount,
}));
setDocuments(docs);
} else {
// Show mock data when no real data exists
setDocuments([
{
id: "1",
title: "Project Vision & Mission",
type: "vision",
owner: "You",
dateModified: new Date().toISOString(),
visibility: "public",
starred: true,
chunkCount: 12,
},
{
id: "2",
title: "Core Features Specification",
type: "features",
owner: "You",
dateModified: new Date(Date.now() - 86400000).toISOString(),
visibility: "public",
starred: false,
chunkCount: 24,
},
]);
}
} else {
// Fallback to mock data on error
setDocuments([
{
id: "1",
title: "Project Vision & Mission",
type: "vision",
owner: "You",
dateModified: new Date().toISOString(),
visibility: "public",
starred: true,
chunkCount: 12,
},
]);
}
} catch (error) {
console.error("Error loading documents:", error);
// Show mock data on error
setDocuments([
{
id: "1",
title: "Project Vision & Mission",
type: "vision",
owner: "You",
dateModified: new Date().toISOString(),
visibility: "public",
starred: true,
chunkCount: 12,
},
]);
} finally {
setLoading(false);
}
};
const getDocIcon = (type: DocType) => {
switch (type) {
case "vision":
return <Target className="h-4 w-4 text-blue-600" />;
case "features":
return <Lightbulb className="h-4 w-4 text-purple-600" />;
case "research":
return <BookOpen className="h-4 w-4 text-green-600" />;
case "chats":
return <MessageSquare className="h-4 w-4 text-orange-600" />;
default:
return <FileText className="h-4 w-4 text-gray-600" />;
}
};
const filteredDocuments = documents.filter((doc) => {
if (searchQuery && !doc.title.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
return true;
});
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar */}
<CollapsibleSidebar>
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-2">Document Stats</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Total Docs</span>
<span className="font-medium">{documents.length}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Public</span>
<span className="font-medium">{documents.filter(d => d.visibility === 'public').length}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Private</span>
<span className="font-medium">{documents.filter(d => d.visibility === 'private').length}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Starred</span>
<span className="font-medium">{documents.filter(d => d.starred).length}</span>
</div>
</div>
</div>
</div>
</CollapsibleSidebar>
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="border-b bg-background">
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-4">
<h1 className="text-xl font-bold">Docs</h1>
<Badge variant="secondary" className="font-normal">
{filteredDocuments.length} {filteredDocuments.length === 1 ? "doc" : "docs"}
</Badge>
</div>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Add page
</Button>
</div>
{/* Tabs */}
<div className="flex items-center gap-6 px-4">
<button
onClick={() => setActiveView("public")}
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
activeView === "public"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Public
</button>
<button
onClick={() => setActiveView("private")}
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
activeView === "private"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Private
</button>
<button
onClick={() => setActiveView("archived")}
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
activeView === "archived"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Archived
</button>
</div>
</div>
{/* Toolbar */}
<div className="flex items-center gap-4 p-4 border-b bg-muted/30">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search docs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={sortBy} onValueChange={(value: any) => setSortBy(value)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="modified">Date modified</SelectItem>
<SelectItem value="created">Date created</SelectItem>
</SelectContent>
</Select>
<Select value={filterType} onValueChange={(value: any) => setFilterType(value)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All types</SelectItem>
<SelectItem value="vision">Vision</SelectItem>
<SelectItem value="features">Features</SelectItem>
<SelectItem value="research">Research</SelectItem>
<SelectItem value="chats">Chats</SelectItem>
</SelectContent>
</Select>
</div>
{/* Document List */}
<div className="flex-1 overflow-auto p-4">
{loading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : filteredDocuments.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-center">
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No documents yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Create your first document to get started
</p>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add page
</Button>
</div>
) : (
<div className="space-y-2">
{filteredDocuments.map((doc) => (
<Link
key={doc.id}
href={`/${workspace}/project/${projectId}/docs/${doc.id}`}
className="block"
>
<Card className="p-4 hover:bg-accent/50 transition-colors cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
{getDocIcon(doc.type)}
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-sm">{doc.title}</h3>
{doc.starred && (
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
)}
</div>
<div className="flex items-center gap-3 mt-1">
<span className="text-xs text-muted-foreground">{doc.owner}</span>
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">
{new Date(doc.dateModified).toLocaleDateString()}
</span>
{doc.chunkCount && (
<>
<span className="text-xs text-muted-foreground"></span>
<Badge variant="secondary" className="text-xs">
{doc.chunkCount} chunks
</Badge>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.preventDefault();
toast.info("Share functionality coming soon");
}}
>
<Share2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.preventDefault();
toast.info("Info panel coming soon");
}}
>
<Info className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.preventDefault();
// Toggle star
}}
>
<Star
className={`h-4 w-4 ${
doc.starred ? "fill-yellow-400 text-yellow-400" : ""
}`}
/>
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.preventDefault();
toast.info("More options coming soon");
}}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
</Link>
))}
</div>
)}
</div>
</div>
{/* End Main Content */}
</div>
);
}