()
| 81 | } |
| 82 | |
| 83 | export default function ModelEditor() { |
| 84 | const { t } = useTranslation('modelEditor') |
| 85 | const { name } = useParams() |
| 86 | const [searchParams] = useSearchParams() |
| 87 | const navigate = useNavigate() |
| 88 | const location = useLocation() |
| 89 | // Where the Back button returns to. Set by whichever page linked here (see |
| 90 | // utils/editorNav); falls back to the historical defaults for direct visits. |
| 91 | const backState = location.state && location.state.from ? location.state : null |
| 92 | const { addToast } = useOutletContext() |
| 93 | const { sections, fields, loading: metaLoading, error: metaError } = useConfigMetadata() |
| 94 | |
| 95 | // Registered schema leaf paths. flattenConfig stops recursing at these so |
| 96 | // map-typed fields (e.g. pii_detection.entity_actions) bind as a whole |
| 97 | // object to their registered editor instead of vanishing into sub-paths. |
| 98 | const leafPaths = useMemo(() => new Set(fields.map(f => f.path)), [fields]) |
| 99 | |
| 100 | // The parsed (not-yet-flattened) config loaded from the server. Flattening |
| 101 | // is deferred to a separate effect keyed on leafPaths so the schema metadata |
| 102 | // can arrive after the config without a fetch race re-clobbering values. |
| 103 | const [loadedConfig, setLoadedConfig] = useState(null) |
| 104 | |
| 105 | const isCreateMode = !name |
| 106 | const [selectedTemplate, setSelectedTemplate] = useState(null) |
| 107 | |
| 108 | const [tab, setTab] = useState('interactive') // 'interactive' | 'yaml' |
| 109 | const [yamlText, setYamlText] = useState('') |
| 110 | const [savedYamlText, setSavedYamlText] = useState('') |
| 111 | const [values, setValues] = useState({}) |
| 112 | const [initialValues, setInitialValues] = useState({}) |
| 113 | const [activeFieldPaths, setActiveFieldPaths] = useState(new Set()) |
| 114 | const [collapsedSections, setCollapsedSections] = useState(new Set()) |
| 115 | const [configLoading, setConfigLoading] = useState(!isCreateMode) |
| 116 | const [saving, setSaving] = useState(false) |
| 117 | const [activeSection, setActiveSection] = useState(null) |
| 118 | const [tabSwitchWarning, setTabSwitchWarning] = useState(false) |
| 119 | |
| 120 | const sectionRefs = useRef({}) |
| 121 | |
| 122 | const vramEstimate = useVramEstimate({ |
| 123 | model: name, |
| 124 | contextSize: values['context_size'], |
| 125 | gpuLayers: values['gpu_layers'], |
| 126 | }) |
| 127 | |
| 128 | const handleSelectTemplate = useCallback((template) => { |
| 129 | setSelectedTemplate(template) |
| 130 | const flat = { ...template.fields } |
| 131 | setValues(flat) |
| 132 | setInitialValues({}) |
| 133 | setActiveFieldPaths(new Set(Object.keys(flat))) |
| 134 | }, []) |
| 135 | |
| 136 | // Auto-select template from URL query param (e.g. ?template=pipeline) |
| 137 | useEffect(() => { |
| 138 | if (!isCreateMode) return |
| 139 | const templateId = searchParams.get('template') |
| 140 | if (templateId) { |
nothing calls this directly
no test coverage detected