refactor(ui): settings-card primitives + fix threshold clamp / card misgroup (#161)
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
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<!--
|
||||
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>
|
||||
Reference in New Issue
Block a user