SarahXia0405's picture
Upload 73 files
0ef5c60 verified
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>
);
}