ec66ea5f83
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
43 lines
1.5 KiB
Vue
43 lines
1.5 KiB
Vue
<!--
|
|
Canonical settings toggle row (DRY pass #161): an accent icon + an uppercase
|
|
.fc-section-h label + a right-aligned switch. Hand-rolled identically in the
|
|
ML settings cards (HeadsCard x3, CropProposersCard, MLBackfillCard).
|
|
|
|
Two-way binds `modelValue` (so the parent switch state stays optimistic) AND
|
|
emits `change` with the new boolean, so the parent can persist + revert on
|
|
failure — matching the prior `v-model` + `@update:model-value=handler` pattern.
|
|
-->
|
|
<template>
|
|
<div class="d-flex align-center mb-1" style="gap: 10px;">
|
|
<v-icon v-if="icon" size="18" :color="iconColor">{{ icon }}</v-icon>
|
|
<span class="fc-section-h">{{ label }}</span>
|
|
<v-switch
|
|
:model-value="modelValue"
|
|
:loading="loading"
|
|
:disabled="disabled"
|
|
hide-details density="compact" color="success" class="ml-auto"
|
|
@update:model-value="onSwitch"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
defineProps({
|
|
modelValue: { type: Boolean, default: false },
|
|
label: { type: String, default: '' },
|
|
icon: { type: String, default: '' },
|
|
// Icon tint. Default accent; pass null for the theme default (e.g. when a row
|
|
// is off). null (not undefined) so the default doesn't override it.
|
|
iconColor: { type: String, default: 'accent' },
|
|
loading: { type: Boolean, default: false },
|
|
disabled: { type: Boolean, default: false },
|
|
})
|
|
const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
|
function onSwitch(v) {
|
|
const b = !!v
|
|
emit('update:modelValue', b)
|
|
emit('change', b)
|
|
}
|
|
</script>
|