SourcePreviewDrawer.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import React from 'react';
  2. import { createPortal } from 'react-dom';
  3. import { X, FileText, Copy, Check, Star, Hash } from 'lucide-react';
  4. import { useLanguage } from '../contexts/LanguageContext';
  5. import { ChatSource } from '../services/chatService';
  6. import { useToast } from '../contexts/ToastContext';
  7. interface SourcePreviewDrawerProps {
  8. isOpen: boolean;
  9. onClose: () => void;
  10. source: ChatSource | null;
  11. onOpenFile?: (source: ChatSource) => void;
  12. }
  13. export const SourcePreviewDrawer: React.FC<SourcePreviewDrawerProps> = ({
  14. isOpen,
  15. onClose,
  16. source,
  17. onOpenFile
  18. }) => {
  19. const { t } = useLanguage();
  20. const { showSuccess } = useToast();
  21. const [isCopied, setIsCopied] = React.useState(false);
  22. React.useEffect(() => {
  23. if (isOpen) {
  24. setIsCopied(false);
  25. }
  26. }, [isOpen, source]);
  27. if (!isOpen || !source) return null;
  28. const handleCopy = () => {
  29. navigator.clipboard.writeText(source.content);
  30. setIsCopied(true);
  31. showSuccess(t('copySuccess'));
  32. setTimeout(() => setIsCopied(false), 2000);
  33. };
  34. return createPortal(
  35. <>
  36. <div
  37. className="fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm transition-opacity"
  38. onClick={onClose}
  39. />
  40. <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">
  41. {/* Header */}
  42. <div className="p-5 border-b border-slate-100 bg-slate-50 shrink-0 flex items-center justify-between">
  43. <div className="flex items-center gap-2 overflow-hidden">
  44. <FileText className="w-5 h-5 text-blue-600 shrink-0" />
  45. <div className="flex flex-col overflow-hidden">
  46. {source.fileId ? (
  47. <button
  48. onClick={() => onOpenFile?.({ ...source, pageNumber: source.pageNumber || 1 })}
  49. className="text-lg font-bold text-slate-800 truncate hover:text-blue-600 hover:underline text-left transition-colors"
  50. title={source.fileName}
  51. >
  52. {source.fileName}
  53. </button>
  54. ) : (
  55. <h2 className="text-lg font-bold text-slate-800 truncate" title={source.fileName}>
  56. {source.fileName}
  57. </h2>
  58. )}
  59. <span className="text-xs text-slate-500">{t('sourcePreview')}</span>
  60. </div>
  61. </div>
  62. <button
  63. onClick={onClose}
  64. className="p-2 hover:bg-slate-200 rounded-full transition-colors active:scale-90"
  65. >
  66. <X className="w-5 h-5 text-slate-500" />
  67. </button>
  68. </div>
  69. {/* Content */}
  70. <div className="flex-1 overflow-y-auto p-5 space-y-6">
  71. {/* Meta Info */}
  72. <div className="flex items-center gap-4 text-sm">
  73. <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">
  74. <Star className="w-4 h-4" />
  75. <span className="font-medium">{(source.score * 100).toFixed(1)}% {t('matchScore')}</span>
  76. </div>
  77. <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">
  78. <Hash className="w-4 h-4" />
  79. <span className="font-medium">#{source.chunkIndex + 1}</span>
  80. </div>
  81. {source.pageNumber !== undefined && (
  82. <div className="flex items-center gap-1.5 px-3 py-1.5 bg-purple-50 text-purple-700 rounded-lg border border-purple-100">
  83. <FileText className="w-4 h-4" />
  84. <span className="font-medium">{t('pageNumber')} {source.pageNumber}</span>
  85. </div>
  86. )}
  87. </div>
  88. {/* Main Content */}
  89. <div className="bg-slate-50 rounded-xl border border-slate-200 p-1 relative group">
  90. <div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
  91. <button
  92. onClick={handleCopy}
  93. 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"
  94. title={t('copyContent')}
  95. >
  96. {isCopied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4" />}
  97. </button>
  98. </div>
  99. <div className="p-4 overflow-x-auto whitespace-pre-wrap text-slate-700 text-sm leading-relaxed font-mono">
  100. {source.content}
  101. </div>
  102. </div>
  103. </div>
  104. </div>
  105. </>,
  106. document.body
  107. );
  108. };