VIBN Frontend for Coolify deployment
This commit is contained in:
408
app/[workspace]/project/[projectId]/docs/page.tsx
Normal file
408
app/[workspace]/project/[projectId]/docs/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user