0a9588abb7
- 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
115 lines
5.2 KiB
TypeScript
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
|
|
);
|
|
};
|