SourcePreviewDrawer.tsx 5.2 KB

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