import React from 'react'; import { Check, Zap, Layout, Trophy } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { motion } from 'framer-motion'; import type { Zone, InternalRegion, UserSelection, FrameCandidate, SlidePlan } from '../types/designAgent'; import { getSectionsForZone } from '../utils/slidePlanUtils'; interface FramePanelProps { slidePlan: SlidePlan | null; selectedZone: Zone | null; selectedRegion: InternalRegion | null; userSelection: UserSelection; onFrameSelect: (frameId: string) => void; onNoDesignToggle: () => void; } export default function FramePanel({ slidePlan, selectedZone, selectedRegion, userSelection, onFrameSelect, }: FramePanelProps) { // Zone 중심 컨셉: 선택된 존에 할당된 섹션들의 프레임 후보들을 수집 const assignedSectionIds = selectedZone ? getSectionsForZone(selectedZone, userSelection) : []; // 해당 섹션들이 포함된 원본 리전들에서 후보군 추출 const candidates: FrameCandidate[] = React.useMemo(() => { if (!slidePlan || !selectedZone) return []; // 단순화: 선택된 존의 첫 번째 리전의 후보군을 우선 사용 (Phase Z MVP1 기준) // 실제로는 할당된 섹션에 따라 동적으로 필터링된 후보군이 필요함 const targetRegion = selectedRegion || selectedZone.internal_regions[0]; return targetRegion?.frame_candidates || []; }, [slidePlan, selectedZone, selectedRegion]); const currentFrameId = React.useMemo(() => { const targetRegion = selectedRegion || selectedZone?.internal_regions[0]; if (!targetRegion) return null; return userSelection.overrides.zone_frames[targetRegion.id] || targetRegion.frame_match_strategy.frame_id; }, [selectedZone, selectedRegion, userSelection.overrides.zone_frames]); if (!selectedZone) { return (

Select a Zone
to View Designs

); } return (
{/* Header */}

Design Wheel

TOP {candidates.length}

현재 구역의 섹션({assignedSectionIds.join(', ')})에 최적화된 추천 디자인입니다.

{/* Vertical Wheel List */}
{candidates.length === 0 ? (

No Candidates Available

) : ( candidates.map((candidate, index) => { const isSelected = currentFrameId === candidate.id; const isReject = candidate.label === "reject"; // catalog 미등록 = backend Step 7-A 가 override 시도해도 skip. // catalogRegistered === false 만 체크 (undefined = 정보 없음, 일반 처리). const isCatalogMissing = candidate.catalogRegistered === false; // ─── IMP-29 u3 — IMP-05 L2 candidate_evidence surface ─────────── // All evidence fields optional; silent degradation when undefined // (pre-IMP-05 fixtures fall back to label/catalogRegistered only). const isFilteredDirect = candidate.filteredForDirectExecution === true; const hasDecision = candidate.decision === "selected" || candidate.decision === "skipped"; const isSkipped = candidate.decision === "skipped"; const isSelectedDecision = candidate.decision === "selected"; const showRouteChip = candidate.routeHint && candidate.routeHint !== "direct_render"; const showStatusChip = candidate.phaseZStatus && candidate.phaseZStatus !== "auto_renderable"; const hasCapacityFit = candidate.capacityFit && candidate.capacityFit.fit_status; const capacityMismatch = hasCapacityFit && candidate.capacityFit!.fit_status !== "ok"; // Compose evidence tooltip lines (only when at least one signal present). const evidenceLines: string[] = []; if (candidate.decision) evidenceLines.push(`decision: ${candidate.decision}`); if (candidate.reason) evidenceLines.push(`reason: ${candidate.reason}`); if (candidate.routeHint) evidenceLines.push(`route: ${candidate.routeHint}`); if (candidate.phaseZStatus) evidenceLines.push(`phase_z_status: ${candidate.phaseZStatus}`); if (hasCapacityFit) { const cf = candidate.capacityFit!; const capacityLine = cf.fit_status === "ok" ? `capacity: ok${ typeof cf.item_count === "number" ? ` (items=${cf.item_count})` : "" }` : `capacity: ${cf.fit_status}${ cf.mismatch_reason ? ` — ${cf.mismatch_reason}` : "" }`; evidenceLines.push(capacityLine); } const evidenceTooltip = evidenceLines.length > 0 ? evidenceLines.join("\n") : undefined; // Compose final tooltip: existing catalog/reject reasons first, then // evidence detail (preserves Phase Q tooltip semantics). const tooltipParts = [ isCatalogMissing ? "⚠ catalog 미등록 — render path 에서 적용 안 됨 (선택해도 backend 가 skip)" : null, isFilteredDirect ? "⚠ filtered_for_direct_execution — MVP1 직접 렌더 경로 제외" : null, isReject ? "V4 reject — render path 비추천" : null, evidenceTooltip, ].filter((s): s is string => Boolean(s)); const composedTitle = tooltipParts.length > 0 ? tooltipParts.join("\n\n") : undefined; return ( ); }) )}
); }