Files
course-agent-od/web/src/app/(main)/knowledge/page.tsx
T
pengxiao ddbb79b9f6 Initial commit: 国土空间规划课程智能体 v1.0
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:40:18 +08:00

319 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/store/auth";
import { Button } from "@/components/ui/button";
import MobileNav from "@/components/layout/mobile-nav";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import {
Upload,
FileText,
Search,
Plus,
Trash2,
Eye,
CheckCircle,
Clock,
Loader2,
BookOpen,
Settings,
FolderOpen
} from "lucide-react";
import { formatFileSize, formatDate } from "@/lib/utils";
import { knowledgeBaseAPI } from "@/lib/api";
import { KnowledgeBase } from "@/types";
export default function KnowledgePage() {
const router = useRouter();
const { isAuthenticated, isLoading: authLoading } = useAuthStore();
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [error, setError] = useState<string | null>(null);
// 创建知识库对话框状态
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [createName, setCreateName] = useState("");
const [createDescription, setCreateDescription] = useState("");
const [isCreating, setIsCreating] = useState(false);
useEffect(() => {
if (!authLoading && !isAuthenticated) {
router.push("/login");
return;
}
if (isAuthenticated) {
loadKnowledgeBases();
}
}, [isAuthenticated, authLoading, router]);
const loadKnowledgeBases = async () => {
try {
setIsLoading(true);
setError(null);
const bases = await knowledgeBaseAPI.getKnowledgeBases();
// 过滤掉系统知识库,只显示用户创建的知识库
const userBases = bases.filter(kb => !kb.is_system);
setKnowledgeBases(userBases);
} catch (err) {
console.error("加载知识库失败:", err);
setError("加载知识库失败");
} finally {
setIsLoading(false);
}
};
const handleCreateKnowledgeBase = async () => {
if (!createName.trim()) return;
try {
setIsCreating(true);
setError(null);
await knowledgeBaseAPI.createKnowledgeBase({
name: createName.trim(),
description: createDescription.trim() || undefined,
});
// 重新加载知识库列表
await loadKnowledgeBases();
// 重置表单并关闭对话框
setCreateName("");
setCreateDescription("");
setIsCreateDialogOpen(false);
} catch (err) {
console.error("创建知识库失败:", err);
setError("创建知识库失败");
} finally {
setIsCreating(false);
}
};
const handleDeleteKnowledgeBase = async (knowledgeBaseId: number) => {
if (!confirm("确定要删除这个知识库吗?这将删除其中的所有文档。")) return;
try {
await knowledgeBaseAPI.deleteKnowledgeBase(knowledgeBaseId.toString());
await loadKnowledgeBases();
} catch (err) {
console.error("删除知识库失败:", err);
setError("删除知识库失败");
}
};
const filteredKnowledgeBases = knowledgeBases.filter(kb =>
kb.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(kb.description && kb.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
if (authLoading || isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (!isAuthenticated) {
return null;
}
return (
<div className="min-h-screen bg-app pb-16 lg:pb-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* 页面头部 */}
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2"></h1>
<p className="text-muted-foreground"></p>
</div>
{/* 错误提示 */}
{error && (
<Alert variant="destructive" className="mb-6">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* 操作栏 */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6">
<div className="flex items-center space-x-4 mb-4 sm:mb-0">
{/* 创建知识库按钮 */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="kb-name"></Label>
<Input
id="kb-name"
value={createName}
onChange={(e) => setCreateName(e.target.value)}
placeholder="请输入知识库名称"
disabled={isCreating}
/>
</div>
<div>
<Label htmlFor="kb-description"></Label>
<Input
id="kb-description"
value={createDescription}
onChange={(e) => setCreateDescription(e.target.value)}
placeholder="请输入知识库描述"
disabled={isCreating}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
disabled={isCreating}
>
</Button>
<Button
onClick={handleCreateKnowledgeBase}
disabled={!createName.trim() || isCreating}
>
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"创建"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索知识库..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 w-64"
/>
</div>
<div className="text-sm text-muted-foreground">
{filteredKnowledgeBases.length}
</div>
</div>
</div>
{/* 知识库列表 */}
{filteredKnowledgeBases.length === 0 ? (
<Card className="backdrop-blur-sm bg-card/80 border-border/50 shadow-xl">
<CardContent className="text-center py-12">
<BookOpen className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">
{searchQuery ? "没有找到匹配的知识库" : "还没有创建知识库"}
</h3>
<p className="text-muted-foreground mb-4">
{searchQuery ? "尝试使用其他关键词搜索" : "创建您的第一个知识库来组织文档"}
</p>
{!searchQuery && (
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredKnowledgeBases.map((kb) => (
<Card key={kb.id} className="hover:shadow-lg transition-shadow backdrop-blur-sm bg-card/80 border-border/50">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center space-x-2">
<BookOpen className="w-5 h-5 text-blue-600" />
<div>
<CardTitle className="text-lg">{kb.name}</CardTitle>
<CardDescription className="text-sm">
{kb.description || "暂无描述"}
</CardDescription>
</div>
</div>
<div className="flex items-center space-x-1">
{kb.document_count > 0 ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<Clock className="w-4 h-4 text-gray-400" />
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span></span>
<span>{kb.document_count} </span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span></span>
<span>{formatDate(kb.created_at)}</span>
</div>
<div className="flex justify-between text-sm">
<span></span>
<span className={kb.document_count > 0 ? "text-green-600" : "text-gray-500"}>
{kb.document_count > 0 ? "已就绪" : "空知识库"}
</span>
</div>
</div>
<div className="flex space-x-2 mt-4">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => router.push(`/knowledge/${kb.id}`)}
>
<Settings className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteKnowledgeBase(kb.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
{/* 移动端导航 */}
<MobileNav />
</div>
);
}