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 ? (
);
})}
{status === 'error' && (
{error || t('generation.problemFramingFailed')}
)}
)}
{confirmDiscardOpen && (
setConfirmDiscardOpen(false)}
/>
{t('problem.discardTitle')}
{t('problem.discardDescription')}
)}
);
}