Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { Button } from './ui/button'; | |
| import { Input } from './ui/input'; | |
| import { Label } from './ui/label'; | |
| import { Card } from './ui/card'; | |
| import { Separator } from './ui/separator'; | |
| import { Textarea } from './ui/textarea'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; | |
| import { | |
| LogIn, | |
| LogOut, | |
| Download, | |
| Sparkles, | |
| Bookmark, | |
| Copy | |
| } from 'lucide-react'; | |
| import { Document, HeadingLevel, Packer, Paragraph, TextRun } from 'docx'; | |
| import type { User, SavedItem } from '../App'; | |
| import { toast } from 'sonner'; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogTrigger, | |
| DialogFooter, | |
| } from './ui/dialog'; | |
| interface RightPanelProps { | |
| user: User | null; | |
| onLogin: (user: User) => void; | |
| onLogout: () => void; | |
| isLoggedIn: boolean; | |
| onClose?: () => void; | |
| exportResult: string; | |
| setExportResult: (result: string) => void; | |
| resultType: 'export' | 'quiz' | 'summary' | null; | |
| setResultType: (type: 'export' | 'quiz' | 'summary' | null) => void; | |
| onExport: () => void; | |
| onSummary: () => void; | |
| onSave: (content: string, type: 'export' | 'quiz' | 'summary') => void; | |
| savedItems: SavedItem[]; | |
| } | |
| export function RightPanel({ user, onLogin, onLogout, isLoggedIn, onClose, exportResult, setExportResult, resultType, setResultType, onExport, onSummary, onSave, savedItems }: RightPanelProps) { | |
| const [showLoginForm, setShowLoginForm] = useState(false); | |
| const [name, setName] = useState(''); | |
| const [email, setEmail] = useState(''); | |
| const [isExpanded, setIsExpanded] = useState(true); | |
| const [isDownloading, setIsDownloading] = useState(false); | |
| const [copied, setCopied] = useState(false); | |
| // Check if current result is already saved | |
| const isSaved = exportResult && resultType | |
| ? savedItems.some(item => item.content === exportResult && item.type === resultType) | |
| : false; | |
| const handleLogin = () => { | |
| if (!name.trim() || !email.trim()) { | |
| toast.error('Please fill in all fields'); | |
| return; | |
| } | |
| onLogin({ name: name.trim(), email: email.trim() }); | |
| setShowLoginForm(false); | |
| setName(''); | |
| setEmail(''); | |
| toast.success(`Welcome, ${name}!`); | |
| }; | |
| const handleLogout = () => { | |
| onLogout(); | |
| setShowLoginForm(false); | |
| toast.success('Logged out successfully'); | |
| }; | |
| const scrollContainerRef = useRef<HTMLDivElement>(null); | |
| // Use native event listeners to prevent scroll propagation | |
| useEffect(() => { | |
| const container = scrollContainerRef.current; | |
| if (!container) return; | |
| const handleWheel = (e: WheelEvent) => { | |
| // Always stop propagation to prevent scrolling other panels | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| // Only prevent default if we're at the boundaries | |
| const { scrollTop, scrollHeight, clientHeight } = container; | |
| const isScrollable = scrollHeight > clientHeight; | |
| const isAtTop = scrollTop === 0; | |
| const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; | |
| // If scrolling up at top or down at bottom, prevent default to stop propagation | |
| if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) { | |
| e.preventDefault(); | |
| } | |
| }; | |
| container.addEventListener('wheel', handleWheel, { passive: false, capture: true }); | |
| return () => { | |
| container.removeEventListener('wheel', handleWheel, { capture: true }); | |
| }; | |
| }, []); | |
| const downloadBlob = (blob: Blob, filename: string) => { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| const formatDateStamp = () => { | |
| const d = new Date(); | |
| const yyyy = d.getFullYear(); | |
| const mm = String(d.getMonth() + 1).padStart(2, '0'); | |
| const dd = String(d.getDate()).padStart(2, '0'); | |
| return `${yyyy}-${mm}-${dd}`; | |
| }; | |
| const getDefaultFilenameBase = () => { | |
| const kind = | |
| resultType === 'export' ? 'export' : | |
| resultType === 'summary' ? 'summary' : | |
| 'result'; | |
| return `clare-${kind}-${formatDateStamp()}`; | |
| }; | |
| const handleDownloadMd = async () => { | |
| if (!exportResult) return; | |
| try { | |
| setIsDownloading(true); | |
| toast.message('Preparing .md…'); | |
| const blob = new Blob([exportResult], { type: 'text/markdown;charset=utf-8' }); | |
| downloadBlob(blob, `${getDefaultFilenameBase()}.md`); | |
| toast.success('Downloaded .md'); | |
| } catch (e) { | |
| console.error(e); | |
| toast.error('Failed to download .md'); | |
| } finally { | |
| setIsDownloading(false); | |
| } | |
| }; | |
| const handleDownloadDocx = async () => { | |
| if (!exportResult) return; | |
| try { | |
| setIsDownloading(true); | |
| toast.message('Preparing .docx…'); | |
| const lines = exportResult.split('\n'); | |
| const paragraphs: Paragraph[] = lines.map((line) => { | |
| const trimmed = line.trim(); | |
| if (!trimmed) return new Paragraph({ text: '' }); | |
| // Basic markdown-ish heading support | |
| if (trimmed.startsWith('### ')) { | |
| return new Paragraph({ text: trimmed.replace(/^###\s+/, ''), heading: HeadingLevel.HEADING_3 }); | |
| } | |
| if (trimmed.startsWith('## ')) { | |
| return new Paragraph({ text: trimmed.replace(/^##\s+/, ''), heading: HeadingLevel.HEADING_2 }); | |
| } | |
| if (trimmed.startsWith('# ')) { | |
| return new Paragraph({ text: trimmed.replace(/^#\s+/, ''), heading: HeadingLevel.HEADING_1 }); | |
| } | |
| return new Paragraph({ children: [new TextRun({ text: line })] }); | |
| }); | |
| const doc = new Document({ | |
| sections: [{ properties: {}, children: paragraphs }], | |
| }); | |
| const blob = await Packer.toBlob(doc); | |
| downloadBlob(blob, `${getDefaultFilenameBase()}.docx`); | |
| toast.success('Downloaded .docx'); | |
| } catch (e) { | |
| console.error(e); | |
| toast.error('Failed to download .docx'); | |
| } finally { | |
| setIsDownloading(false); | |
| } | |
| }; | |
| return ( | |
| <div | |
| ref={scrollContainerRef} | |
| className="flex-1 overflow-auto overscroll-contain flex flex-col" | |
| style={{ overscrollBehavior: 'contain' }} | |
| > | |
| <div className="p-4 space-y-4"> | |
| {isExpanded && ( | |
| <> | |
| {/* Actions Section with Results */} | |
| <div className="space-y-3"> | |
| <h3 className="text-base font-medium">Export / Summarize Conversation</h3> | |
| <Card className="p-4 bg-muted/30"> | |
| <div className="flex flex-col gap-3"> | |
| <Button | |
| variant="outline" | |
| className="w-full h-12 rounded-lg justify-start gap-3" | |
| onClick={onExport} | |
| disabled={!isLoggedIn} | |
| > | |
| <Download className="h-5 w-5" /> | |
| <span>Export</span> | |
| </Button> | |
| <Button | |
| variant="outline" | |
| className="w-full h-12 rounded-lg justify-start gap-3" | |
| onClick={onSummary} | |
| disabled={!isLoggedIn} | |
| > | |
| <Sparkles className="h-5 w-5" /> | |
| <span>Summarize</span> | |
| </Button> | |
| {/* Results - Expanded from buttons */} | |
| {exportResult && ( | |
| <> | |
| <Separator className="my-2" /> | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <h4 className="text-base font-bold"> | |
| {resultType === 'export' && 'Exported Conversation'} | |
| {resultType === 'quiz' && 'Micro-Quiz'} | |
| {resultType === 'summary' && 'Summarization'} | |
| </h4> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| disabled={isDownloading} | |
| onClick={handleDownloadMd} | |
| title="Download as .md" | |
| className="h-7 px-2 text-xs gap-1.5" | |
| > | |
| <Download className="h-3 w-3" /> | |
| .md | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| disabled={isDownloading} | |
| onClick={handleDownloadDocx} | |
| title="Download as .docx" | |
| className="h-7 px-2 text-xs gap-1.5" | |
| > | |
| <Download className="h-3 w-3" /> | |
| .docx | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={async () => { | |
| await navigator.clipboard.writeText(exportResult); | |
| setCopied(true); | |
| toast.success('Copied to clipboard!'); | |
| setTimeout(() => setCopied(false), 2000); | |
| }} | |
| disabled={isDownloading} | |
| className="h-7 px-2 text-xs gap-1.5" | |
| title="Copy" | |
| > | |
| <Copy className="h-3 w-3" /> | |
| </Button> | |
| {resultType && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => { | |
| if (resultType) { | |
| onSave(exportResult, resultType); | |
| } | |
| }} | |
| disabled={isDownloading || !resultType} | |
| className={`h-7 px-2 text-xs gap-1.5 ${isSaved ? 'bg-red-50 dark:bg-red-950/20 border-red-300 dark:border-red-800' : ''}`} | |
| title={isSaved ? 'Unsave' : 'Save for later'} | |
| > | |
| <Bookmark className={`h-3 w-3 ${isSaved ? 'fill-red-600 text-red-600' : ''}`} /> | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| <div className={`text-sm whitespace-pre-wrap text-foreground p-3 rounded-lg ${ | |
| isSaved ? 'bg-red-50/50 dark:bg-red-950/10' : '' | |
| }`}> | |
| {exportResult} | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </Card> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |