diff --git a/core/http/react-ui/src/pages/ImportModel.jsx b/core/http/react-ui/src/pages/ImportModel.jsx
index 340cb292a..9154737f2 100644
--- a/core/http/react-ui/src/pages/ImportModel.jsx
+++ b/core/http/react-ui/src/pages/ImportModel.jsx
@@ -106,23 +106,161 @@ parameters:
model: /path/to/model.gguf
`
+const DEFAULT_PREFS = {
+ backend: '', name: '', description: '', quantizations: '',
+ mmproj_quantizations: '', embeddings: false, type: '',
+ pipeline_type: '', scheduler_type: '', enable_parameters: '', cuda: false,
+}
+
+// Preference keys considered "advanced" — anything the Simple-mode Options
+// disclosure does NOT expose. `hasCustomPrefs` uses this list to decide
+// whether switching Power -> Simple should warn the user.
+const ADVANCED_PREF_KEYS = [
+ 'quantizations', 'mmproj_quantizations', 'embeddings', 'type',
+ 'pipeline_type', 'scheduler_type', 'enable_parameters', 'cuda',
+]
+
const hintStyle = { marginTop: '4px', fontSize: '0.75rem', color: 'var(--color-text-muted)' }
+// hasCustomPrefs returns true when the user has set any preference beyond
+// backend/name/description, added a custom key-value pref with a non-empty
+// key, or edited the YAML away from its default. That triggers the switch
+// warning so Simple mode never silently hides state.
+function hasCustomPrefs(prefs, customPrefs, yamlContent) {
+ for (const key of ADVANCED_PREF_KEYS) {
+ const v = prefs[key]
+ if (typeof v === 'boolean' ? v : (typeof v === 'string' ? v.trim() !== '' : v != null && v !== '')) {
+ return true
+ }
+ }
+ if (Array.isArray(customPrefs) && customPrefs.some(cp => (cp.key || '').trim() !== '')) {
+ return true
+ }
+ if (typeof yamlContent === 'string' && yamlContent !== DEFAULT_YAML) {
+ return true
+ }
+ return false
+}
+
+// PowerTabs renders the in-page Preferences/YAML tab strip. Kept inline
+// (not a separate component) — the strip is tiny and lives inside the
+// Power-mode card so extracting it would just add indirection.
+function PowerTabs({ value, onChange }) {
+ return (
+
+
+
+
+ )
+}
+
+// SwitchModeDialog — 3-button confirmation that fires when switching from
+// Power -> Simple with custom prefs. Not using ConfirmDialog because that
+// component is 2-button (confirm/cancel); the UX here needs Keep / Discard
+// / Cancel with distinct semantics.
+function SwitchModeDialog({ onKeep, onDiscard, onCancel }) {
+ const keepRef = useRef(null)
+ useEffect(() => {
+ keepRef.current?.focus()
+ const handleKey = (e) => { if (e.key === 'Escape') onCancel?.() }
+ document.addEventListener('keydown', handleKey)
+ return () => document.removeEventListener('keydown', handleKey)
+ }, [onCancel])
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+ Keep your custom preferences?
+
+
+ Switching to Simple mode hides preferences beyond backend, name, and description. They’ll still be sent when you import.
+
+
+
+
+
+
+
+
+ )
+}
+
export default function ImportModel() {
const navigate = useNavigate()
const { addToast } = useOutletContext()
- // Mode state replaces the old isAdvancedMode boolean. Simple is the
- // default; Power subsumes the previous Advanced surface. Persisted to
- // localStorage so reloads land on the same view. Later commits split
- // Power into tabs and add Simple-mode disclosure + switch dialog.
+ // Mode + tab state. Persisted to localStorage so reloads keep the user
+ // on the same surface they last picked. `showOptions` is Simple-mode
+ // local state — no need to persist (it's a one-click expansion).
const [mode, setMode] = useState(() => {
try { return localStorage.getItem('import-form-mode') || 'simple' } catch { return 'simple' }
})
- useEffect(() => {
- try { localStorage.setItem('import-form-mode', mode) } catch { /* ignore quota / privacy mode */ }
- }, [mode])
- const isAdvancedMode = mode === 'power'
+ const [powerTab, setPowerTab] = useState(() => {
+ try { return localStorage.getItem('import-form-power-tab') || 'preferences' } catch { return 'preferences' }
+ })
+ const [showOptions, setShowOptions] = useState(false)
+ // null | { onKeep, onDiscard, onCancel } — when non-null the dialog renders.
+ const [switchDialog, setSwitchDialog] = useState(null)
const [importUri, setImportUri] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -131,11 +269,7 @@ export default function ImportModel() {
const [estimate, setEstimate] = useState(null)
const [jobProgress, setJobProgress] = useState(null)
- const [prefs, setPrefs] = useState({
- backend: '', name: '', description: '', quantizations: '',
- mmproj_quantizations: '', embeddings: false, type: '',
- pipeline_type: '', scheduler_type: '', enable_parameters: '', cuda: false,
- })
+ const [prefs, setPrefs] = useState(DEFAULT_PREFS)
const [customPrefs, setCustomPrefs] = useState([])
// ambiguity state: { modality, candidates } when the server returns 400
// with a structured ambiguity body. Cleared on pick, dismiss, URI change,
@@ -152,6 +286,14 @@ export default function ImportModel() {
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [])
+ useEffect(() => {
+ try { localStorage.setItem('import-form-mode', mode) } catch { /* ignore quota / privacy mode */ }
+ }, [mode])
+
+ useEffect(() => {
+ try { localStorage.setItem('import-form-power-tab', powerTab) } catch { /* ignore */ }
+ }, [powerTab])
+
useEffect(() => {
let cancelled = false
setBackendsLoading(true)
@@ -183,6 +325,28 @@ export default function ImportModel() {
setCustomPrefs(p => p.map((item, idx) => idx === i ? { ...item, [field]: value } : item))
}
+ // requestModeSwitch — routed through the SimplePowerSwitch onChange. When
+ // going Power -> Simple we gate on custom prefs so the user never loses
+ // hidden state silently.
+ const requestModeSwitch = useCallback((next) => {
+ if (next === mode) return
+ if (mode === 'power' && next === 'simple' && hasCustomPrefs(prefs, customPrefs, yamlContent)) {
+ setSwitchDialog({
+ onKeep: () => { setSwitchDialog(null); setMode('simple') },
+ onDiscard: () => {
+ setSwitchDialog(null)
+ setPrefs(DEFAULT_PREFS)
+ setCustomPrefs([])
+ setYamlContent(DEFAULT_YAML)
+ setMode('simple')
+ },
+ onCancel: () => setSwitchDialog(null),
+ })
+ return
+ }
+ setMode(next)
+ }, [mode, prefs, customPrefs, yamlContent])
+
const startJobPolling = useCallback((jobId) => {
if (pollRef.current) clearInterval(pollRef.current)
pollRef.current = setInterval(async () => {
@@ -309,35 +473,262 @@ export default function ImportModel() {
}
}
+ const isSimple = mode === 'simple'
+ const isPowerYaml = mode === 'power' && powerTab === 'yaml'
+
+ const subtitle = isSimple
+ ? 'Import a model from a URI — auto-detect picks the backend.'
+ : (powerTab === 'yaml'
+ ? 'Write the full model YAML configuration.'
+ : 'Fine-grained import preferences.')
+
+ // The Ambiguity alert + URI input live at the top of both Simple and
+ // Power/Preferences modes. Extracted so both branches stay readable.
+ const renderUriAndAmbiguity = () => (
+ <>
+ {ambiguity && (
+ setAmbiguity(null)}
+ />
+ )}
+
+
+
+
setImportUri(e.target.value)}
+ placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
+ disabled={isSubmitting}
+ />
+
Enter the URI or path to the model file you want to import
+
+
+ {showGuide && (
+
+ {URI_FORMATS.map((fmt, i) => (
+
+
+
+ {fmt.title}
+
+
+ {fmt.examples.map((ex, j) => (
+
+
{ex.prefix}
+
{ex.suffix}
+
{ex.desc}
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ >
+ )
+
+ // Backend dropdown + auto-install note — shared between Simple/Options
+ // and Power/Preferences.
+ const renderBackendField = () => (
+
+
+
updatePref('backend', v)}
+ options={backendOptions}
+ allOption="Auto-detect (based on URI)"
+ placeholder={backendsLoading ? 'Loading backends…' : 'Auto-detect (based on URI)'}
+ searchPlaceholder="Search backends..."
+ disabled={isSubmitting || backendsLoading}
+ />
+
+ Force a specific backend. Leave empty to auto-detect from the URI. Items marked “manual pick” aren’t auto-detectable — pick them yourself if you know what the model needs.
+ {backendsError && (
+
+ Could not load backend list — auto-detect only.
+
+ )}
+
+ {(() => {
+ if (!prefs.backend) return null
+ const selected = backends.find(b => b.name === prefs.backend)
+ if (!selected || selected.installed) return null
+ return (
+
+
+ This backend isn’t installed yet. Submitting import will download it first.
+
+ )
+ })()}
+
+ )
+
+ const renderNameField = () => (
+
+
+
updatePref('name', e.target.value)} placeholder="Leave empty to use filename" disabled={isSubmitting} />
+
Custom name for the model. If empty, the filename will be used.
+
+ )
+
+ const renderDescriptionField = () => (
+
+ )
+
+ // Full preferences panel — identical to the previous Simple-mode panel.
+ const renderFullPreferences = () => (
+
+
+ Preferences (Optional)
+
+
+
+
+
+ Common Preferences
+
+
+
+ {renderBackendField()}
+ {renderNameField()}
+ {renderDescriptionField()}
+
+
+
+
updatePref('quantizations', e.target.value)} placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)" disabled={isSubmitting} />
+
Preferred quantizations (comma-separated). Leave empty for default (q4_k_m).
+
+
+
+
+
updatePref('mmproj_quantizations', e.target.value)} placeholder="fp16,fp32 (comma-separated)" disabled={isSubmitting} />
+
Preferred MMProj quantizations. Leave empty for default (fp16).
+
+
+
+
+
Enable embeddings support for this model.
+
+
+
+
+
updatePref('type', e.target.value)} placeholder="AutoModelForCausalLM (for transformers backend)" disabled={isSubmitting} />
+
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba.
+
+
+ {prefs.backend === 'diffusers' && (
+ <>
+
+
+
updatePref('pipeline_type', e.target.value)} placeholder="StableDiffusionPipeline" disabled={isSubmitting} />
+
Pipeline type for diffusers backend.
+
+
+
+
updatePref('scheduler_type', e.target.value)} placeholder="k_dpmpp_2m (optional)" disabled={isSubmitting} />
+
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim.
+
+
+
+
updatePref('enable_parameters', e.target.value)} placeholder="negative_prompt,num_inference_steps (comma-separated)" disabled={isSubmitting} />
+
Enabled parameters for diffusers backend (comma-separated).
+
+
+
+
Enable CUDA support for GPU acceleration.
+
+ >
+ )}
+
+
+
+ {/* Custom Preferences */}
+
+
+
+ Custom Preferences
+
+
+
+ {customPrefs.map((cp, i) => (
+
+ updateCustomPref(i, 'key', e.target.value)} placeholder="Key" disabled={isSubmitting} style={{ flex: 1 }} />
+ :
+ updateCustomPref(i, 'value', e.target.value)} placeholder="Value" disabled={isSubmitting} style={{ flex: 1 }} />
+
+
+ ))}
+
Add custom key-value pairs for advanced configuration.
+
+
+ )
+
return (
Import New Model
-
- {isAdvancedMode ? 'Fine-grained import preferences and YAML editor.' : 'Import a model from a URI — auto-detect picks the backend.'}
-
+
{subtitle}
-
- {!isAdvancedMode ? (
-
- ) : (
+
+ {isPowerYaml ? (
+ ) : (
+
)}
{/* Estimate banner */}
- {!isAdvancedMode && estimate && (
+ {!isPowerYaml && estimate && (
@@ -362,238 +753,95 @@ export default function ImportModel() {
)}
- {/* Simple Import Mode */}
- {!isAdvancedMode && (
+ {/* Simple mode */}
+ {isSimple && (
- {ambiguity && (
-
setAmbiguity(null)}
- />
- )}
+ {renderUriAndAmbiguity()}
- {/* URI Input */}
-
-
-
setImportUri(e.target.value)}
- placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
- disabled={isSubmitting}
- />
-
Enter the URI or path to the model file you want to import
-
- {/* URI format guide */}
+
- {showGuide && (
-
- {URI_FORMATS.map((fmt, i) => (
-
-
-
- {fmt.title}
-
-
- {fmt.examples.map((ex, j) => (
-
-
{ex.prefix}
-
{ex.suffix}
-
{ex.desc}
-
- ))}
-
-
- ))}
+
+ {showOptions && (
+
+ {renderBackendField()}
+ {renderNameField()}
+ {renderDescriptionField()}
)}
-
- {/* Preferences */}
-
-
- Preferences (Optional)
-
-
-
-
-
- Common Preferences
-
-
-
-
-
-
updatePref('backend', v)}
- options={backendOptions}
- allOption="Auto-detect (based on URI)"
- placeholder={backendsLoading ? 'Loading backends…' : 'Auto-detect (based on URI)'}
- searchPlaceholder="Search backends..."
- disabled={isSubmitting || backendsLoading}
- />
-
- Force a specific backend. Leave empty to auto-detect from the URI. Items marked “manual pick” aren’t auto-detectable — pick them yourself if you know what the model needs.
- {backendsError && (
-
- Could not load backend list — auto-detect only.
-
- )}
-
- {(() => {
- // Warn when the user picked a backend that is not
- // installed yet — submitting import will trigger an
- // auto-download. Hidden when auto-detect is selected
- // (prefs.backend === '') since we can't know which
- // backend the server will end up picking.
- if (!prefs.backend) return null
- const selected = backends.find(b => b.name === prefs.backend)
- if (!selected || selected.installed) return null
- return (
-
-
- This backend isn’t installed yet. Submitting import will download it first.
-
- )
- })()}
-
-
-
-
-
updatePref('name', e.target.value)} placeholder="Leave empty to use filename" disabled={isSubmitting} />
-
Custom name for the model. If empty, the filename will be used.
-
-
-
-
-
-
-
updatePref('quantizations', e.target.value)} placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)" disabled={isSubmitting} />
-
Preferred quantizations (comma-separated). Leave empty for default (q4_k_m).
-
-
-
-
-
updatePref('mmproj_quantizations', e.target.value)} placeholder="fp16,fp32 (comma-separated)" disabled={isSubmitting} />
-
Preferred MMProj quantizations. Leave empty for default (fp16).
-
-
-
-
-
Enable embeddings support for this model.
-
-
-
-
-
updatePref('type', e.target.value)} placeholder="AutoModelForCausalLM (for transformers backend)" disabled={isSubmitting} />
-
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba.
-
-
- {/* Diffusers-specific fields */}
- {prefs.backend === 'diffusers' && (
- <>
-
-
-
updatePref('pipeline_type', e.target.value)} placeholder="StableDiffusionPipeline" disabled={isSubmitting} />
-
Pipeline type for diffusers backend.
-
-
-
-
updatePref('scheduler_type', e.target.value)} placeholder="k_dpmpp_2m (optional)" disabled={isSubmitting} />
-
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim.
-
-
-
-
updatePref('enable_parameters', e.target.value)} placeholder="negative_prompt,num_inference_steps (comma-separated)" disabled={isSubmitting} />
-
Enabled parameters for diffusers backend (comma-separated).
-
-
-
-
Enable CUDA support for GPU acceleration.
-
- >
- )}
-
-
-
- {/* Custom Preferences */}
-
-
-
- Custom Preferences
-
-
-
- {customPrefs.map((cp, i) => (
-
- updateCustomPref(i, 'key', e.target.value)} placeholder="Key" disabled={isSubmitting} style={{ flex: 1 }} />
- :
- updateCustomPref(i, 'value', e.target.value)} placeholder="Value" disabled={isSubmitting} style={{ flex: 1 }} />
-
-
- ))}
-
Add custom key-value pairs for advanced configuration.
-
-
)}
- {/* Advanced YAML Editor Mode */}
- {isAdvancedMode && (
-
-
-
-
- YAML Configuration Editor
-
-
-
-
+ {/* Power mode */}
+ {mode === 'power' && (
+
+ {!isPowerYaml && (
+ <>
+
+ {renderUriAndAmbiguity()}
+ {renderFullPreferences()}
+ >
+ )}
+ {isPowerYaml && (
+ <>
+
+
+
+
+ YAML Configuration Editor
+
+
+
+
+ >
+ )}
)}
+
+ {switchDialog && (
+
+ )}
)
}