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:
@@ -15,28 +15,23 @@
|
||||
</p>
|
||||
|
||||
<div v-for="p in proposers" :key="p.key" class="fc-proposer">
|
||||
<div class="d-flex align-center mb-1" style="gap: 10px;">
|
||||
<v-icon size="18" :color="p.on ? 'accent' : undefined">{{ p.icon }}</v-icon>
|
||||
<span class="fc-section-h">{{ p.label }}</span>
|
||||
<v-switch
|
||||
v-model="p.on" :loading="busy" hide-details density="compact"
|
||||
color="success" class="ml-auto"
|
||||
@update:model-value="v => saveToggle(p, v)"
|
||||
/>
|
||||
</div>
|
||||
<SettingToggleRow
|
||||
v-model="p.on" :loading="busy" :icon="p.icon"
|
||||
:icon-color="p.on ? 'accent' : null" :label="p.label"
|
||||
@change="v => saveToggle(p, v)"
|
||||
/>
|
||||
<p class="fc-muted text-body-2 mb-2">{{ p.help }}</p>
|
||||
<div class="d-flex flex-wrap mb-4" style="gap: 12px;">
|
||||
<v-text-field
|
||||
v-model="p.weights" label="Weights" density="compact" hide-details
|
||||
style="min-width: 300px; flex: 1;" :disabled="busy || !p.on"
|
||||
placeholder="name | URL | hf_repo::file"
|
||||
@change="save({ [`detector_${p.key}_weights`]: p.weights })"
|
||||
@change="saveField({ [`detector_${p.key}_weights`]: p.weights })"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="p.conf" label="Confidence" type="number"
|
||||
min="0" max="1" step="0.05" density="compact" hide-details
|
||||
style="max-width: 140px;" :disabled="busy || !p.on"
|
||||
@change="save({ [`detector_${p.key}_conf`]: Number(p.conf) })"
|
||||
<SettingNumberField
|
||||
v-model="p.conf" label="Confidence" :min="0" :max="1" :step="0.05"
|
||||
max-width="140px" :disabled="busy || !p.on"
|
||||
@change="saveField({ [`detector_${p.key}_conf`]: Number(p.conf) })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,12 +43,12 @@
|
||||
storage. Dedupe IoU drops near-duplicate crops before embedding.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap" style="gap: 12px;">
|
||||
<v-text-field
|
||||
<SettingNumberField
|
||||
v-for="c in caps" :key="c.key"
|
||||
v-model.number="c.val" :label="c.label" type="number"
|
||||
:min="c.min" :max="c.max" :step="c.step || 1" density="compact"
|
||||
hide-details style="max-width: 165px;" :disabled="busy"
|
||||
@change="save({ [c.key]: Number(c.val) })"
|
||||
v-model="c.val" :label="c.label"
|
||||
:min="c.min" :max="c.max" :step="c.step || 1"
|
||||
max-width="165px" :disabled="busy"
|
||||
@change="saveField({ [c.key]: Number(c.val) })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,14 +56,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
import SettingNumberField from '../common/SettingNumberField.vue'
|
||||
import SettingToggleRow from '../common/SettingToggleRow.vue'
|
||||
import { useSettingSave } from '../../composables/useSettingSave.js'
|
||||
import { useMLStore } from '../../stores/ml.js'
|
||||
|
||||
const mlSettings = useMLStore()
|
||||
const busy = ref(false)
|
||||
const { busy, save } = useSettingSave(mlSettings.patchSettings)
|
||||
const proposers = ref([])
|
||||
const caps = ref([])
|
||||
|
||||
@@ -111,22 +108,16 @@ onMounted(async () => {
|
||||
caps.value = CAP_DEFS.map(c => ({ ...c, val: s[c.key] ?? 0 }))
|
||||
})
|
||||
|
||||
async function save(patch, revert) {
|
||||
busy.value = true
|
||||
try {
|
||||
await mlSettings.patchSettings(patch)
|
||||
toast({ text: 'Saved', type: 'success' })
|
||||
} catch (e) {
|
||||
if (revert) revert()
|
||||
toast({ text: `Could not save: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
// Field @change → persist with a "Saved" confirmation. SettingNumberField has
|
||||
// already clamped numeric values to their [min,max] before this fires.
|
||||
function saveField(patch) {
|
||||
save(patch, { successMessage: 'Saved' })
|
||||
}
|
||||
|
||||
function saveToggle (p, v) {
|
||||
async function saveToggle(p, v) {
|
||||
// Revert the switch on failure so it never lies about the persisted state.
|
||||
save({ [`detector_${p.key}_enabled`]: !!v }, () => { p.on = !v })
|
||||
const ok = await save({ [`detector_${p.key}_enabled`]: !!v }, { successMessage: 'Saved' })
|
||||
if (!ok) p.on = !v
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user