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,117 @@
"use client";
import { FileText, ExternalLink, Star, Database, Globe } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface SourceReference {
title: string;
filename?: string;
page?: number;
score?: number;
preview: string;
url?: string;
source_type?: "web" | "rag";
}
interface SourceReferencesProps {
sources: SourceReference[];
maxSources?: number;
}
export default function SourceReferences({ sources, maxSources = 5 }: SourceReferencesProps) {
if (!sources || sources.length === 0) {
return null;
}
const displaySources = sources.slice(0, maxSources);
const ragSources = displaySources.filter(s => s.source_type !== "web");
const webSources = displaySources.filter(s => s.source_type === "web");
return (
<div className="mt-4 space-y-3">
{ragSources.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-blue-600">
<Database className="h-4 w-4" />
<span> ({ragSources.length})</span>
</div>
<div className="space-y-2">
{ragSources.map((source, index) => (
<Card key={index} className="border border-blue-200 bg-blue-50/30 hover:border-blue-300 transition-colors">
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<CardTitle className="text-sm font-medium line-clamp-2">
{source.title}
</CardTitle>
{source.score != null && source.score > 0 && source.score < 1 && (
<div className="flex items-center gap-1 ml-2">
<Star className="h-3 w-3 text-yellow-500" />
<span className="text-xs text-gray-500">
{(source.score * 100).toFixed(1)}%
</span>
</div>
)}
</div>
<div className="text-xs text-gray-500">
{source.filename}
{source.page && ` • 第 ${source.page}`}
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-xs text-gray-600 line-clamp-2">
{source.preview}
</p>
</CardContent>
</Card>
))}
</div>
</div>
)}
{webSources.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-green-600">
<Globe className="h-4 w-4" />
<span> ({webSources.length})</span>
</div>
<div className="space-y-2">
{webSources.map((source, index) => (
<Card key={index} className="border border-green-200 bg-green-50/30 hover:border-green-300 transition-colors">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium line-clamp-2">
{source.title}
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-xs text-gray-600 line-clamp-2 mb-2">
{source.preview}
</p>
{source.url && (
<Button
size="sm"
variant="outline"
className="h-6 text-xs"
onClick={() => window.open(source.url, '_blank')}
>
<ExternalLink className="h-3 w-3 mr-1" />
</Button>
)}
</CardContent>
</Card>
))}
</div>
</div>
)}
{sources.length > maxSources && (
<div className="text-xs text-gray-500 text-center">
{sources.length - maxSources}
</div>
)}
</div>
);
}