"use client"; import { useEffect, useMemo, useState } from "react"; import * as Collapsible from "@radix-ui/react-collapsible"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { ChevronDown, ChevronUp, Download, Loader2, RefreshCcw, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { CadamSlider } from "@/components/ui/cadam-slider"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; type EditableParameter = Record; type NormalizedParameter = { id: string; name: string; displayName: string; group: string; groupDisplayName: string; value: number; defaultValue: number; min: number | null; max: number | null; step: number; precision: number; unit: string; editable: boolean; editState: string; }; type DownloadFormat = { label: string; description: string; url: string; }; function numberValue(value: unknown) { const numeric = typeof value === "number" ? value : Number(value); return Number.isFinite(numeric) ? numeric : null; } function parameterToView(parameter: EditableParameter): NormalizedParameter | null { const id = String(parameter.id || parameter.name || "").trim(); const name = String(parameter.name || id).trim(); const value = numberValue(parameter.value); const declaredRange = Array.isArray(parameter.range) ? parameter.range : []; const min = numberValue(parameter.min ?? parameter.minimum ?? declaredRange[0]); const max = numberValue(parameter.max ?? parameter.maximum ?? declaredRange[1]); if (!name || value === null) return null; return { id, name, displayName: String(parameter.display_name || parameter.displayName || parameter.label || name), group: String(parameter.group || "dimensions"), groupDisplayName: String(parameter.group_display_name || parameter.groupDisplayName || "尺寸"), value, defaultValue: numberValue(parameter.default_value ?? parameter.defaultValue) ?? value, min, max, step: numberValue(parameter.step) ?? 1, precision: Math.max(0, numberValue(parameter.precision) ?? 2), unit: String(parameter.unit || ""), editable: parameter.editable === true || parameter.edit_state === "declared_unvalidated", editState: String(parameter.edit_state || "unvalidated"), }; } function formatValue(value: number, precision: number) { if (!Number.isFinite(value)) return ""; if (Number.isInteger(value) && precision === 0) return String(value); return Number(value.toFixed(precision)).toString(); } function clampValue(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } function visualRange(parameter: NormalizedParameter) { if (parameter.min !== null && parameter.max !== null && parameter.max > parameter.min) { return { min: parameter.min, max: parameter.max }; } const span = Math.max(Math.abs(parameter.value), 1); return { min: Math.max(0, parameter.value - span), max: parameter.value + span, }; } function sectionDisplayName(group: { id: string; displayName: string }) { return group.id === "dimensions" || group.displayName === "尺寸" ? "Dimensions" : group.displayName; } export function ParameterPanel({ parameters, downloads = [], pendingParameter, error, onClose, onCommit, onReset, }: { parameters: EditableParameter[]; downloads?: DownloadFormat[]; pendingParameter?: string; error?: string; onClose: () => void; onCommit: (parameter: string, value: number) => void; onReset: (values: Record) => void; }) { const normalizedParameters = useMemo( () => parameters.map(parameterToView).filter((value): value is NormalizedParameter => Boolean(value)), [parameters], ); const [drafts, setDrafts] = useState>({}); const [openGroups, setOpenGroups] = useState>({}); const [selectedDownload, setSelectedDownload] = useState(downloads[0]?.label || ""); useEffect(() => { setDrafts(Object.fromEntries( normalizedParameters.map((parameter) => [ parameter.name, formatValue(parameter.value, parameter.precision), ]), )); setOpenGroups((current) => { const next = { ...current }; for (const parameter of normalizedParameters) { if (next[parameter.group] === undefined) next[parameter.group] = true; } return next; }); }, [normalizedParameters]); useEffect(() => { if (!downloads.some((download) => download.label === selectedDownload)) { setSelectedDownload(downloads[0]?.label || ""); } }, [downloads, selectedDownload]); const grouped = useMemo(() => { const groups = new Map(); for (const parameter of normalizedParameters) { const group = groups.get(parameter.group) || { id: parameter.group, displayName: parameter.groupDisplayName, parameters: [], }; group.parameters.push(parameter); groups.set(parameter.group, group); } return Array.from(groups.values()); }, [normalizedParameters]); const resetParameters = () => { const values = Object.fromEntries( normalizedParameters .filter((parameter) => parameter.editable && parameter.value !== parameter.defaultValue) .map((parameter) => [parameter.id, parameter.defaultValue]), ); if (Object.keys(values).length) { onReset(values); return; } setDrafts(Object.fromEntries( normalizedParameters.map((parameter) => [ parameter.name, formatValue(parameter.defaultValue, parameter.precision), ]), )); }; const selectedDownloadItem = downloads.find((download) => download.label === selectedDownload) || downloads[0] || null; const commitValue = (parameter: NormalizedParameter, rawValue: string | number) => { const numeric = numberValue(rawValue); if (!parameter.editable) { setDrafts((current) => ({ ...current, [parameter.name]: formatValue(parameter.value, parameter.precision), })); return; } if (numeric === null) { setDrafts((current) => ({ ...current, [parameter.name]: formatValue(parameter.value, parameter.precision), })); return; } const clamped = parameter.min !== null && parameter.max !== null ? clampValue(numeric, parameter.min, parameter.max) : numeric; const formatted = formatValue(clamped, parameter.precision); setDrafts((current) => ({ ...current, [parameter.name]: formatted })); if (Number(formatted) !== parameter.value) { onCommit(parameter.id, Number(formatted)); } }; return ( ); }