sel-chat-coach / src /components /CoachAdPromptModal.tsx
james-d-taboola's picture
feat: defer coach selection to ad prompt with two-step UI
25ed8f3
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>
);
}