Tier-3 frontend DRY for the ML settings cards, plus the F-D2 clamp bug and the F-D3 card misgrouping. New primitives (components/common + composables): - <SettingToggleRow> — the accent-icon + .fc-section-h label + right-aligned switch row (HeadsCard x3, CropProposersCard). iconColor prop absorbs the on/off dim. - <SettingNumberField> — compact numeric field that CLAMPS to [min,max] on commit. This fixes F-D2: HeadsCard/CropProposersCard previously sent Number(raw) straight to the API, so an out-of-range threshold bounced off the 400 validator (only TranslationCard clamped). density prop for the grid cards. - useSettingSave(patchFn) — the busy + patch + toast + revert-on-failure flow each card hand-rolled (HeadsCard x6 handlers, CropProposersCard, MLBackfillCard, VideoEmbeddingCard). Returns ok/false for the optimistic-switch revert. Adopted in HeadsCard, CropProposersCard, MLBackfillCard (handler only — its plain labelled switch is a different affordance), VideoEmbeddingCard. F-D3: MLThresholdSliders.vue actually rendered a "Video embedding" (frame- sampling) card but sat under "Tagging → Suggestion thresholds". Renamed it VideoEmbeddingCard.vue and moved it to the "GPU agent & embeddings" section. Left deliberately (over-DRY guard): TranslationCard uses an inline error ALERT (not a toast), already clamps its confidence with a NaN fallback, and lives on the ImportStore — a genuinely different save pattern, so forcing it onto useSettingSave would change its UX. Behaviour-preserving refactor; CI has no Vue type-check so this needs a live UI pass (toggles persist + revert on failure, thresholds clamp on blur, video card now under Embeddings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi
57 lines
2.1 KiB
Vue
57 lines
2.1 KiB
Vue
<!--
|
|
Canonical settings number field (DRY pass #161): a compact numeric v-text-field
|
|
with a built-in clamp to [min,max] on commit. Hand-rolled identically across the
|
|
ML settings cards (HeadsCard x6, CropProposersCard, VideoEmbeddingCard).
|
|
|
|
The clamp is the point: the cards previously sent Number(raw) straight to the
|
|
API, so an out-of-range value bounced off the API's 400 validator (only
|
|
TranslationCard clamped). This is now the single home for that clamp.
|
|
|
|
Binds `modelValue` (v-model) and emits `change` on blur/enter AFTER clamping, so
|
|
the parent's save reads the already-clamped value — same as the prior
|
|
`v-model.number` + `@change=save` pattern.
|
|
-->
|
|
<template>
|
|
<v-text-field
|
|
:model-value="modelValue"
|
|
:label="label"
|
|
type="number"
|
|
:min="min"
|
|
:max="max"
|
|
:step="step"
|
|
:disabled="disabled"
|
|
:density="density" hide-details
|
|
:style="{ maxWidth }"
|
|
@update:model-value="v => emit('update:modelValue', v)"
|
|
@change="onCommit"
|
|
/>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
modelValue: { type: [Number, String], default: null },
|
|
label: { type: String, default: '' },
|
|
min: { type: [Number, String], default: null },
|
|
max: { type: [Number, String], default: null },
|
|
step: { type: [Number, String], default: 1 },
|
|
maxWidth: { type: String, default: '200px' },
|
|
density: { type: String, default: 'compact' },
|
|
disabled: { type: Boolean, default: false },
|
|
})
|
|
const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
|
function onCommit() {
|
|
// On blur/enter: coerce to a number and clamp to [min,max] so an out-of-range
|
|
// value never reaches the API. props.modelValue reflects the latest keystroke
|
|
// (kept in sync by the passthrough above); re-emit the clamped number, then let
|
|
// the parent persist.
|
|
let n = Number(props.modelValue)
|
|
if (!Number.isNaN(n)) {
|
|
if (props.min !== null && props.min !== '') n = Math.max(Number(props.min), n)
|
|
if (props.max !== null && props.max !== '') n = Math.min(Number(props.max), n)
|
|
if (n !== Number(props.modelValue)) emit('update:modelValue', n)
|
|
}
|
|
emit('change')
|
|
}
|
|
</script>
|