Spaces:
Running
Running
| import { RefObject, useState, useEffect } from 'react'; | |
| import { SpeechBubbleTail } from './SpeechBubbleTail'; | |
| import { useCoachModalAnimation } from '@/hooks/useCoachModalAnimation'; | |
| interface Coach { | |
| id: string; | |
| name: string; | |
| description: string; | |
| } | |
| interface CoachAdPromptModalProps { | |
| show: boolean; | |
| onShowChange: (show: boolean) => void; | |
| triggerRef: RefObject<HTMLButtonElement | null>; | |
| isGenerating: boolean; | |
| onCoachSelect: (coachId: string) => void; | |
| coaches: Coach[]; | |
| } | |
| export function CoachAdPromptModal({ | |
| show, | |
| onShowChange, | |
| triggerRef, | |
| isGenerating, | |
| onCoachSelect, | |
| coaches, | |
| }: CoachAdPromptModalProps) { | |
| const [step, setStep] = useState<'prompt' | 'selection'>('prompt'); | |
| // Reset step when modal is closed | |
| useEffect(() => { | |
| if (!show) { | |
| setStep('prompt'); | |
| } | |
| }, [show]); | |
| const { | |
| tailTarget, | |
| coachBubbleRef, | |
| coachBubbleContentRef, | |
| handleCloseModal, | |
| getOverlayClassName, | |
| getBubbleClassName, | |
| getBubbleStyle, | |
| } = useCoachModalAnimation(show, onShowChange, triggerRef); | |
| // Close modal (step reset is handled by useEffect when show changes) | |
| const handleClose = () => { | |
| handleCloseModal(); | |
| }; | |
| const handleConfirmPrompt = () => { | |
| setStep('selection'); | |
| }; | |
| const handleSelectCoach = (coachId: string) => { | |
| onCoachSelect(coachId); | |
| }; | |
| const handleBackToPrompt = () => { | |
| setStep('prompt'); | |
| }; | |
| return ( | |
| <div | |
| className={getOverlayClassName('fixed inset-0 bg-black/30 z-50 flex items-center justify-center px-4')} | |
| onClick={() => !isGenerating && handleClose()} | |
| > | |
| <div | |
| ref={coachBubbleRef} | |
| className={getBubbleClassName('relative w-full max-w-sm')} | |
| style={getBubbleStyle()} | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| {/* Main Bubble */} | |
| <div | |
| ref={coachBubbleContentRef} | |
| className="bg-white rounded-[2rem] shadow-2xl p-6 relative overflow-visible" | |
| style={{ border: '3px solid rgb(216, 180, 254)' }} | |
| > | |
| {step === 'prompt' ? ( | |
| <> | |
| {/* Step 1: Initial Prompt */} | |
| <div className="flex flex-col items-center text-center"> | |
| <div className="w-20 h-20 bg-gradient-to-br from-purple-400 to-purple-500 rounded-full flex items-center justify-center mb-4 shadow-lg"> | |
| <span className="text-4xl">👨🏫</span> | |
| </div> | |
| <p className="text-lg text-gray-700 leading-relaxed mb-2"> | |
| 您已經與學生對話一陣子了! | |
| </p> | |
| <p className="text-xl font-bold text-purple-600 mb-6"> | |
| 需要向我尋求建議嗎? | |
| </p> | |
| </div> | |
| {/* Actions */} | |
| <div className="flex gap-3"> | |
| <button | |
| onClick={handleClose} | |
| disabled={isGenerating} | |
| className={`flex-1 py-4 rounded-full font-semibold text-base transition-all ${ | |
| isGenerating | |
| ? 'bg-gray-100 text-gray-400 cursor-not-allowed' | |
| : 'bg-gray-100 text-gray-600 hover:bg-gray-200 active:scale-[0.98]' | |
| }`} | |
| > | |
| 先不用 | |
| </button> | |
| <button | |
| onClick={handleConfirmPrompt} | |
| disabled={isGenerating} | |
| className={`flex-1 py-4 rounded-full font-bold text-base shadow-lg active:scale-[0.98] transition-all ${ | |
| isGenerating | |
| ? 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
| : 'bg-gradient-to-r from-purple-500 to-pink-500 text-white' | |
| }`} | |
| > | |
| 好,諮詢教練 | |
| </button> | |
| </div> | |
| </> | |
| ) : ( | |
| <> | |
| {/* Step 2: Coach Selection */} | |
| <div className="flex flex-col"> | |
| <h3 className="text-xl font-bold text-gray-900 text-center mb-4"> | |
| 選擇您的教練 | |
| </h3> | |
| {/* Loading State */} | |
| {isGenerating && ( | |
| <div className="mb-4 text-center text-sm text-gray-600"> | |
| <div className="inline-flex items-center gap-2"> | |
| <div className="w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" /> | |
| 設定教練中... | |
| </div> | |
| </div> | |
| )} | |
| {/* Coach Cards */} | |
| <div className="space-y-3 mb-4 max-h-64 overflow-y-auto"> | |
| {coaches.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-500"> | |
| <p>目前沒有可用的教練</p> | |
| </div> | |
| ) : ( | |
| coaches.map((coach) => ( | |
| <button | |
| key={coach.id} | |
| onClick={() => handleSelectCoach(coach.id)} | |
| disabled={isGenerating} | |
| className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ | |
| isGenerating | |
| ? 'border-gray-200 bg-gray-50 cursor-not-allowed opacity-50' | |
| : 'border-purple-200 bg-purple-50 hover:border-purple-400 hover:bg-purple-100 active:scale-[0.98]' | |
| }`} | |
| > | |
| <div className="flex items-start gap-3"> | |
| <div className="w-10 h-10 bg-gradient-to-br from-purple-400 to-purple-500 rounded-full flex items-center justify-center flex-shrink-0"> | |
| <span className="text-xl">👨🏫</span> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="font-bold text-gray-900">{coach.name}</div> | |
| <div className="text-sm text-gray-600 mt-1 line-clamp-2"> | |
| {coach.description} | |
| </div> | |
| </div> | |
| </div> | |
| </button> | |
| )) | |
| )} | |
| </div> | |
| {/* Back Button */} | |
| <button | |
| onClick={handleBackToPrompt} | |
| disabled={isGenerating} | |
| className={`w-full py-3 rounded-full font-semibold text-base transition-all ${ | |
| isGenerating | |
| ? 'bg-gray-100 text-gray-400 cursor-not-allowed' | |
| : 'bg-gray-100 text-gray-600 hover:bg-gray-200 active:scale-[0.98]' | |
| }`} | |
| > | |
| 返回 | |
| </button> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| {/* Speech Bubble Tail - only show on prompt step */} | |
| {step === 'prompt' && tailTarget && <SpeechBubbleTail target={tailTarget} />} | |
| </div> | |
| </div> | |
| ); | |
| } | |