// web/src/components/RightPanel.tsx import React, { useEffect, useRef, useState } 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, FileText, MessageSquare, Download, ClipboardList, Sparkles, Volume2, Podcast } from 'lucide-react'; import type { User } from '../App'; import { toast } from 'sonner'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from './ui/dialog'; interface RightPanelProps { user: User | null; onLogin: (name: string, emailOrId: string) => 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; // ✅ Actions buttons callbacks (来自 App.tsx) onExport: () => void; onQuiz: () => void; onSummary: () => void; } export function RightPanel({ user, onLogin, onLogout, isLoggedIn, exportResult, setExportResult, resultType, setResultType, onExport, onQuiz, onSummary, }: RightPanelProps) { const [showLoginForm, setShowLoginForm] = useState(false); const [name, setName] = useState(''); const [emailOrId, setEmailOrId] = useState(''); const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false); const [feedbackText, setFeedbackText] = useState(''); const [feedbackCategory, setFeedbackCategory] = useState<'general' | 'bug' | 'feature'>('general'); const [audioUrl, setAudioUrl] = useState(null); const [loadingTts, setLoadingTts] = useState(false); const [loadingPodcast, setLoadingPodcast] = useState(false); const audioRef = useRef(null); useEffect(() => { return () => { if (audioUrl) URL.revokeObjectURL(audioUrl); }; }, [audioUrl]); async function postAudio(path: string, payload: any): Promise { const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const txt = await res.text().catch(() => ''); throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`); } return await res.blob(); } const handleListenTts = async () => { if (!isLoggedIn || !user?.user_id || !exportResult?.trim()) return; if (audioUrl) URL.revokeObjectURL(audioUrl); setAudioUrl(null); try { setLoadingTts(true); toast.message('Generating speech…'); const blob = await postAudio('/api/tts', { user_id: user.user_id, text: exportResult, voice: 'nova' }); const url = URL.createObjectURL(blob); setAudioUrl(url); toast.success('Ready. Use the player below.'); setTimeout(() => audioRef.current?.play(), 100); } catch (e) { console.error(e); toast.error(e instanceof Error ? e.message : 'TTS failed'); } finally { setLoadingTts(false); } }; const handleGeneratePodcast = async (source: 'summary' | 'conversation') => { if (!isLoggedIn || !user?.user_id) return; if (audioUrl) URL.revokeObjectURL(audioUrl); setAudioUrl(null); try { setLoadingPodcast(true); toast.message(source === 'summary' ? 'Generating podcast from summary…' : 'Generating podcast from conversation…'); const blob = await postAudio('/api/podcast', { user_id: user.user_id, source, voice: 'nova' }); const url = URL.createObjectURL(blob); setAudioUrl(url); toast.success('Podcast ready. Use the player below.'); setTimeout(() => audioRef.current?.play(), 100); } catch (e) { console.error(e); toast.error(e instanceof Error ? e.message : 'Podcast failed'); } finally { setLoadingPodcast(false); } }; const handleLoginClick = () => { if (!name.trim() || !emailOrId.trim()) { toast.error('Please fill in all fields'); return; } onLogin(name.trim(), emailOrId.trim()); setShowLoginForm(false); setName(''); setEmailOrId(''); }; const handleLogoutClick = () => { onLogout(); setShowLoginForm(false); }; const handleFeedbackSubmit = () => { if (!feedbackText.trim()) { toast.error('Please provide feedback text'); return; } console.log('Feedback submitted:', feedbackText, feedbackCategory); setFeedbackDialogOpen(false); setFeedbackText(''); toast.success('Feedback submitted!'); }; return (
{/* Account */} {!isLoggedIn ? (

Welcome to Clare!

Log in to start your learning session

{!showLoginForm ? ( ) : (
setName(e.target.value)} placeholder="Enter your name" />
setEmailOrId(e.target.value)} placeholder="Enter your email or ID" />
)}
) : (
{user?.name?.charAt(0).toUpperCase()}

{user?.name}

{user?.email}

user_id: {user?.user_id}

)}
{/* ✅ Actions:三按钮并排(同宽) */} {/* Actions */} {/* Actions */}

Actions TEST 123

{/* 强制一排三个:不用 grid,直接 flex,最稳 */}
{/* Results */}

{resultType === 'export' && 'Exported Conversation'} {resultType === 'quiz' && 'Micro-Quiz'} {resultType === 'summary' && 'Summarization'} {!resultType && 'Results'}

{exportResult ? (
{audioUrl && (
{exportResult}
) : (
Results (export / summary / quiz) will appear here after actions run
)}
{/* Feedback */}

Feedback

Provide Feedback Help us improve Clare by sharing your thoughts and suggestions.