Files
aurak/web/components/SourcePreviewDrawer.tsx
T
Developer 0a9588abb7 feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
2026-04-23 17:19:11 +08:00

115 lines
5.2 KiB
TypeScript

import React from 'react';
import { createPortal } from 'react-dom';
import { X, FileText, Copy, Check, Star, Hash } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { ChatSource } from '../services/chatService';
import { useToast } from '../contexts/ToastContext';
import { copyToClipboard } from '../utils/clipboard';
interface SourcePreviewDrawerProps {
isOpen: boolean;
onClose: () => void;
source: ChatSource | null;
onOpenFile?: (source: ChatSource) => void;
}
export const SourcePreviewDrawer: React.FC<SourcePreviewDrawerProps> = ({
isOpen,
onClose,
source,
onOpenFile
}) => {
const { t } = useLanguage();
const { showSuccess } = useToast();
const [isCopied, setIsCopied] = React.useState(false);
React.useEffect(() => {
if (isOpen) {
setIsCopied(false);
}
}, [isOpen, source]);
if (!isOpen || !source) return null;
const handleCopy = async () => {
const success = await copyToClipboard(source.content);
if (success) {
setIsCopied(true);
showSuccess(t('copySuccess'));
setTimeout(() => setIsCopied(false), 2000);
}
};
return createPortal(
<>
<div
className="fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>
<div className="fixed right-0 top-0 h-full w-full max-w-lg bg-white shadow-2xl z-[101] transform transition-transform duration-300 ease-in-out animate-in slide-in-from-right flex flex-col">
{/* Header */}
<div className="p-5 border-b border-slate-100 bg-slate-50 shrink-0 flex items-center justify-between">
<div className="flex items-center gap-2 overflow-hidden">
<FileText className="w-5 h-5 text-blue-600 shrink-0" />
<div className="flex flex-col overflow-hidden">
{source.fileId ? (
<button
onClick={() => onOpenFile?.(source)}
className="text-lg font-bold text-slate-800 truncate hover:text-blue-600 hover:underline text-left transition-colors"
title={source.fileName}
>
{source.fileName}
</button>
) : (
<h2 className="text-lg font-bold text-slate-800 truncate" title={source.fileName}>
{source.fileName}
</h2>
)}
<span className="text-xs text-slate-500">{t('sourcePreview')}</span>
</div>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-200 rounded-full transition-colors active:scale-90"
>
<X className="w-5 h-5 text-slate-500" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-5 space-y-6">
{/* Meta Info */}
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg border border-blue-100">
<Star className="w-4 h-4" />
<span className="font-medium">{(source.score * 100).toFixed(1)}% {t('matchScore')}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 text-slate-600 rounded-lg border border-slate-200">
<Hash className="w-4 h-4" />
<span className="font-medium">#{source.chunkIndex + 1}</span>
</div>
</div>
{/* Main Content */}
<div className="bg-slate-50 rounded-xl border border-slate-200 p-1 relative group">
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={handleCopy}
className="p-2 bg-white/80 backdrop-blur hover:bg-white text-slate-500 hover:text-blue-600 rounded-lg border border-slate-200 shadow-sm transition-all"
title={t('copyContent')}
>
{isCopied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4" />}
</button>
</div>
<div className="p-4 overflow-x-auto whitespace-pre-wrap text-slate-700 text-sm leading-relaxed font-mono">
{source.content}
</div>
</div>
</div>
</div>
</>,
document.body
);
};