import { useEffect, useRef, useState } from 'react'; import type { PointerEvent as ReactPointerEvent } from 'react'; import type { ProblemFramingPlan } from '../types/api'; import { useI18n } from '../i18n'; interface ProblemFramingOverlayProps { open: boolean; status: 'loading' | 'ready' | 'error'; plan: ProblemFramingPlan | null; error: string | null; adjustment: string; generating: boolean; onAdjustmentChange: (value: string) => void; onRetry: () => void; onGenerate: () => void; onClose: () => void; } const CARD_LAYOUTS = [ { x: 6, y: 14, rotate: -6 }, { x: 21, y: 57, rotate: 4 }, { x: 42, y: 22, rotate: -4 }, { x: 62, y: 52, rotate: 5 }, { x: 79, y: 18, rotate: -3 }, { x: 74, y: 66, rotate: 3 }, ]; const COLLAPSED_CARD = { width: 120, height: 86 }; const EXPANDED_CARD = { width: 272, height: 236 }; const ACTIVE_REPEL_DISTANCE = 14; const ACTIVE_CARD_CENTER_PULL = 0.28; type CardPosition = { x: number; y: number; rotate: number; }; type DragState = { index: number; pointerId: number; startClientX: number; startClientY: number; originX: number; originY: number; moved: boolean; }; function PawStamp({ delay = 0 }: { delay?: number }) { return (
); } function WaitingPaws() { return (
); } function CanvasTransition() { return (
); } export function ProblemFramingOverlay({ open, status, plan, error, adjustment, generating, onAdjustmentChange, onRetry, onGenerate, onClose, }: ProblemFramingOverlayProps) { if (!open) { return null; } const planKey = plan ? `${status}-${plan.summary}-${plan.steps.map((step) => `${step.title}:${step.content}`).join('|')}` : `empty-${status}`; return ( ); } interface ProblemFramingOverlayContentProps { status: 'loading' | 'ready' | 'error'; plan: ProblemFramingPlan | null; error: string | null; adjustment: string; generating: boolean; onAdjustmentChange: (value: string) => void; onRetry: () => void; onGenerate: () => void; onClose: () => void; } function ProblemFramingOverlayContent({ status, plan, error, adjustment, generating, onAdjustmentChange, onRetry, onGenerate, onClose, }: ProblemFramingOverlayContentProps) { const { t } = useI18n(); const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false); const [activeStepIndex, setActiveStepIndex] = useState(null); const [draftSteps, setDraftSteps] = useState>(() => plan ? plan.steps.map((step) => ({ title: step.title, content: step.content })) : [] ); const [cardPositions, setCardPositions] = useState(() => plan ? plan.steps.map((_, index) => { const layout = CARD_LAYOUTS[index] || CARD_LAYOUTS[index % CARD_LAYOUTS.length]; return { x: layout.x, y: layout.y, rotate: layout.rotate }; }) : [] ); const canvasRef = useRef(null); const dragStateRef = useRef(null); const statusKey = status === 'loading' ? 'problem.status.loading' : status === 'ready' ? 'problem.status.ready' : 'problem.status.error'; useEffect(() => { dragStateRef.current = null; }, []); const stepCount = plan?.steps.length || 4; const updateCardPosition = (index: number, clientX: number, clientY: number) => { const canvas = canvasRef.current; const current = cardPositions[index]; if (!canvas || !current) { return; } const rect = canvas.getBoundingClientRect(); const isActive = activeStepIndex === index; const cardSize = isActive ? EXPANDED_CARD : COLLAPSED_CARD; const maxX = Math.max(0, rect.width - cardSize.width); const maxY = Math.max(0, rect.height - cardSize.height); const nextX = Math.min(Math.max(0, clientX), maxX); const nextY = Math.min(Math.max(0, clientY), maxY); setCardPositions((previous) => previous.map((item, itemIndex) => itemIndex === index ? { ...item, x: rect.width > 0 ? (nextX / rect.width) * 100 : item.x, y: rect.height > 0 ? (nextY / rect.height) * 100 : item.y, } : item ) ); }; const getRenderedPosition = (index: number, position: CardPosition): CardPosition => { if (activeStepIndex === index) { const centeredX = position.x + (50 - position.x) * ACTIVE_CARD_CENTER_PULL; const centeredY = position.y + (44 - position.y) * ACTIVE_CARD_CENTER_PULL; return { x: Math.max(2, Math.min(72, centeredX)), y: Math.max(3, Math.min(58, centeredY)), rotate: 0, }; } if (activeStepIndex === null || activeStepIndex === index) { return position; } const active = cardPositions[activeStepIndex]; if (!active) { return position; } const dx = position.x - active.x; const dy = position.y - active.y; const distance = Math.hypot(dx, dy) || 1; const push = ACTIVE_REPEL_DISTANCE / distance; return { x: Math.max(0, Math.min(88, position.x + dx * push)), y: Math.max(0, Math.min(78, position.y + dy * push)), rotate: position.rotate, }; }; const handleCardPointerDown = (index: number, event: ReactPointerEvent) => { const target = event.target as HTMLElement; if (target.closest('textarea')) { return; } const canvas = canvasRef.current; const current = cardPositions[index]; if (!canvas || !current) { return; } const rect = canvas.getBoundingClientRect(); const pointerOriginX = (current.x / 100) * rect.width; const pointerOriginY = (current.y / 100) * rect.height; dragStateRef.current = { index, pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, originX: pointerOriginX, originY: pointerOriginY, moved: false, }; event.currentTarget.setPointerCapture(event.pointerId); }; const handleCardPointerMove = (event: ReactPointerEvent) => { const dragState = dragStateRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } const deltaX = event.clientX - dragState.startClientX; const deltaY = event.clientY - dragState.startClientY; if (!dragState.moved && Math.abs(deltaX) + Math.abs(deltaY) > 3) { dragState.moved = true; } updateCardPosition(dragState.index, dragState.originX + deltaX, dragState.originY + deltaY); }; const handleCardPointerUp = (index: number, event: ReactPointerEvent) => { const dragState = dragStateRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } if (!dragState.moved) { setActiveStepIndex((current) => (current === index ? null : index)); } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } dragStateRef.current = null; }; return (
{status === 'loading' && (

{t(statusKey)}

{Array.from({ length: stepCount }, (_, index) => { const layout = CARD_LAYOUTS[index] || CARD_LAYOUTS[CARD_LAYOUTS.length - 1]; return (
); })}
)} {status !== 'loading' && plan && (

{t(statusKey)}

{draftSteps.map((step, index) => { const isActive = index === activeStepIndex; const position = cardPositions[index] || CARD_LAYOUTS[index] || CARD_LAYOUTS[CARD_LAYOUTS.length - 1]; const renderedPosition = getRenderedPosition(index, position); const collapsedTitle = step.title.slice(0, 6); return (
handleCardPointerDown(index, event)} onPointerMove={handleCardPointerMove} onPointerUp={(event) => handleCardPointerUp(index, event)} onPointerCancel={(event) => handleCardPointerUp(index, event)} className={`absolute rounded-[1.2rem] border border-dashed bg-bg-primary/78 p-4 transition-all duration-500 ease-out ${ isActive ? 'z-20 h-[236px] w-[272px] border-accent/45' : 'z-10 h-[86px] w-[120px] border-text-secondary/35 cursor-grab select-none' }`} style={{ left: `${renderedPosition.x}%`, top: `${renderedPosition.y}%`, transform: isActive ? 'rotate(0deg)' : `rotate(${position.rotate}deg)`, animation: `fadeInUp 0.32s ease-out ${index * 0.12}s both` }} >

{String(index + 1).padStart(2, '0')}

{isActive ? step.title : collapsedTitle}

{isActive ? (