| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- 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
- );
- };
|