Initial commit: 国土空间规划课程智能体 v1.0

单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:40:18 +08:00
commit ddbb79b9f6
167 changed files with 44147 additions and 0 deletions
@@ -0,0 +1,259 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import { ChevronDown, Database, FileText, BookOpen, Scale } from "lucide-react";
import { cn } from "@/lib/utils";
// 系统知识库图标映射
const getSystemKnowledgeBaseIcon = (name: string) => {
if (name.includes("法律法规")) return Scale;
if (name.includes("原理")) return BookOpen;
if (name.includes("案例")) return FileText;
return Database;
};
export interface KnowledgeBase {
id: string;
name: string;
description: string;
documentCount: number;
enabled: boolean;
isSystem?: boolean; // 是否为系统知识库
icon?: React.ComponentType<{ className?: string }>;
}
interface KnowledgeSelectorProps {
selectedBases: string[];
onBasesChange: (baseIds: string[]) => void;
systemKnowledgeBases?: KnowledgeBase[]; // 系统知识库列表(从后端动态加载)
userKnowledgeBases?: KnowledgeBase[]; // 用户的知识库列表
className?: string;
}
export default function KnowledgeSelector({
selectedBases,
onBasesChange,
systemKnowledgeBases = [],
userKnowledgeBases = [],
className
}: KnowledgeSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
// 为系统知识库添加图标(如果还没有的话)
const systemBasesWithIcons = systemKnowledgeBases.map(kb => ({
...kb,
icon: kb.icon || getSystemKnowledgeBaseIcon(kb.name),
isSystem: true,
}));
// 合并系统知识库和用户知识库
const allKnowledgeBases = [
...systemBasesWithIcons,
...userKnowledgeBases.map(kb => ({
...kb,
isSystem: false, // 用户知识库标记为非系统
}))
];
const enabledBases = allKnowledgeBases.filter(base => base.enabled);
const selectedCount = selectedBases.length;
const totalEnabled = enabledBases.length;
const handleToggleBase = (baseId: string) => {
const newSelectedBases = selectedBases.includes(baseId)
? selectedBases.filter(id => id !== baseId)
: [...selectedBases, baseId];
onBasesChange(newSelectedBases);
};
const handleSelectAll = () => {
const allEnabledIds = enabledBases.map(base => base.id);
onBasesChange(allEnabledIds);
};
const handleSelectNone = () => {
onBasesChange([]);
};
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className={cn(
"h-9 px-3 text-sm font-medium",
"border-border/50 bg-background/80 hover:bg-muted/50",
"transition-all duration-200",
className
)}
>
<Database className="w-4 h-4 mr-2" />
<span className="hidden sm:inline">
{selectedCount > 0 && `(${selectedCount})`}
</span>
<span className="sm:hidden">
{selectedCount > 0 && `(${selectedCount})`}
</span>
<ChevronDown className="w-3 h-3 ml-1 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72">
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
</div>
<DropdownMenuSeparator />
{/* 快速操作 */}
<div className="px-2 py-1.5">
<div className="flex space-x-2">
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
className="h-7 px-2 text-xs"
>
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleSelectNone}
className="h-7 px-2 text-xs"
>
</Button>
</div>
</div>
<DropdownMenuSeparator />
{/* 系统知识库 */}
{systemBasesWithIcons.length > 0 && (
<div className="px-2 py-1">
<div className="text-xs font-medium text-muted-foreground mb-2 flex items-center">
<Database className="w-3 h-3 mr-1" />
</div>
{systemBasesWithIcons.map((base) => {
const BaseIcon = base.icon || Database;
const isSelected = selectedBases.includes(base.id);
const isDisabled = !base.enabled;
return (
<DropdownMenuCheckboxItem
key={base.id}
checked={isSelected}
onCheckedChange={() => handleToggleBase(base.id)}
disabled={isDisabled}
className={cn(
"flex items-start space-x-3 p-3 cursor-pointer",
isDisabled && "opacity-50 cursor-not-allowed"
)}
>
<BaseIcon className="w-4 h-4 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="font-medium text-sm">{base.name}</span>
{isSelected && (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
{isDisabled && (
<Badge variant="outline" className="text-xs">
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5">
{base.description}
</p>
<div className="flex items-center space-x-1 mt-1">
<span className="text-xs text-muted-foreground">
{base.documentCount}
</span>
</div>
</div>
</DropdownMenuCheckboxItem>
);
})}
</div>
)}
{/* 用户知识库 */}
{userKnowledgeBases.length > 0 && (
<>
<DropdownMenuSeparator />
<div className="px-2 py-1">
<div className="text-xs font-medium text-muted-foreground mb-2 flex items-center">
<FileText className="w-3 h-3 mr-1" />
</div>
{userKnowledgeBases.map((base) => {
const isSelected = selectedBases.includes(base.id);
const isDisabled = !base.enabled;
return (
<DropdownMenuCheckboxItem
key={base.id}
checked={isSelected}
onCheckedChange={() => handleToggleBase(base.id)}
disabled={isDisabled}
className={cn(
"flex items-start space-x-3 p-3 cursor-pointer",
isDisabled && "opacity-50 cursor-not-allowed"
)}
>
<BookOpen className="w-4 h-4 mt-0.5 flex-shrink-0 text-green-500" />
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="font-medium text-sm">{base.name}</span>
{isSelected && (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
{isDisabled && (
<Badge variant="outline" className="text-xs">
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5">
{base.description}
</p>
<div className="flex items-center space-x-1 mt-1">
<span className="text-xs text-muted-foreground">
{base.documentCount}
</span>
</div>
</div>
</DropdownMenuCheckboxItem>
);
})}
</div>
</>
)}
{selectedCount > 0 && (
<>
<DropdownMenuSeparator />
<div className="px-2 py-1.5 text-xs text-muted-foreground text-center">
{selectedCount}
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}