ddbb79b9f6
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
118 lines
4.1 KiB
TypeScript
118 lines
4.1 KiB
TypeScript
"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>
|
|
);
|
|
}
|