Files
decision/frontend/app/components/protocols/FormulaEditor.vue
Yvv cede2a585f Sprint 3 : protocoles de vote et boite a outils
Backend:
- Sessions de vote : list, close, tally, threshold details, auto-expiration
- Protocoles : update, simulate, meta-gouvernance, formulas CRUD
- Service vote enrichi : close_session, get_threshold_details, nuanced breakdown
- Schemas : ThresholdDetailOut, VoteResultOut, FormulaSimulationRequest/Result
- WebSocket broadcast sur chaque vote + fermeture session
- 25 nouveaux tests (threshold details, close, nuanced, simulation)

Frontend:
- 5 composants vote : VoteBinary, VoteNuanced, ThresholdGauge, FormulaDisplay, VoteHistory
- 3 composants protocoles : ProtocolPicker, FormulaEditor, ModeParamsDisplay
- Simulateur de formules interactif (page /protocols/formulas)
- Page detail protocole (/protocols/[id])
- Composable useWebSocket (live updates)
- Composable useVoteFormula (calcul client-side reactif)
- Integration KaTeX pour rendu LaTeX des formules

Documentation:
- API reference : 8 nouveaux endpoints documentes
- Formules : tables d'inertie, parametres detailles, simulation API
- Guide vote : vote binaire/nuance, jauge, historique, simulateur, meta-gouvernance

55 tests passes (+ 1 skipped), 126 fichiers total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 13:29:31 +01:00

244 lines
7.3 KiB
Vue

<script setup lang="ts">
/**
* Interactive formula parameter editor.
*
* Provides sliders and inputs for adjusting all formula parameters,
* emitting the updated config on each change.
*/
import type { FormulaConfig } from '~/stores/protocols'
const props = defineProps<{
modelValue: FormulaConfig
}>()
const emit = defineEmits<{
'update:modelValue': [value: FormulaConfig]
}>()
/** Local reactive copy to avoid direct prop mutation. */
const local = reactive({ ...props.modelValue })
/** Sync incoming prop changes. */
watch(() => props.modelValue, (newVal) => {
Object.assign(local, newVal)
}, { deep: true })
/** Emit on any local change. */
watch(local, () => {
emit('update:modelValue', { ...local })
}, { deep: true })
/** Track optional fields. */
const showSmith = ref(local.smith_exponent !== null)
const showTechcomm = ref(local.techcomm_exponent !== null)
const showNuancedMin = ref(local.nuanced_min_participants !== null)
const showNuancedThreshold = ref(local.nuanced_threshold_pct !== null)
watch(showSmith, (v) => {
local.smith_exponent = v ? 0.5 : null
})
watch(showTechcomm, (v) => {
local.techcomm_exponent = v ? 0.5 : null
})
watch(showNuancedMin, (v) => {
local.nuanced_min_participants = v ? 10 : null
})
watch(showNuancedThreshold, (v) => {
local.nuanced_threshold_pct = v ? 66 : null
})
interface ParamDef {
key: string
label: string
description: string
type: 'input' | 'slider'
min: number
max: number
step: number
}
const mainParams: ParamDef[] = [
{
key: 'duration_days',
label: 'Duree (jours)',
description: 'Duree du vote en jours',
type: 'input',
min: 1,
max: 365,
step: 1,
},
{
key: 'majority_pct',
label: 'Majorite (%)',
description: 'Ratio de majorite cible a haute participation',
type: 'slider',
min: 0,
max: 100,
step: 1,
},
{
key: 'base_exponent',
label: 'Exposant de base (B)',
description: 'B^W tend vers 0 si B < 1 ; plancher dynamique',
type: 'slider',
min: 0.01,
max: 1.0,
step: 0.01,
},
{
key: 'gradient_exponent',
label: 'Gradient d\'inertie (G)',
description: 'Controle la vitesse de transition vers la majorite simple',
type: 'slider',
min: 0.01,
max: 2.0,
step: 0.01,
},
{
key: 'constant_base',
label: 'Constante de base (C)',
description: 'Plancher fixe de votes requis',
type: 'input',
min: 0,
max: 100,
step: 1,
},
]
</script>
<template>
<div class="space-y-6">
<!-- Main parameters -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="param in mainParams" :key="param.key" class="space-y-2">
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ param.label }}
</label>
<span class="text-sm font-mono font-bold text-primary">
{{ (local as any)[param.key] }}
</span>
</div>
<template v-if="param.type === 'slider'">
<URange
v-model="(local as any)[param.key]"
:min="param.min"
:max="param.max"
:step="param.step"
/>
</template>
<template v-else>
<UInput
v-model.number="(local as any)[param.key]"
type="number"
:min="param.min"
:max="param.max"
:step="param.step"
/>
</template>
<p class="text-xs text-gray-500">{{ param.description }}</p>
</div>
</div>
<!-- Optional parameters -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">
Parametres optionnels
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Smith exponent -->
<div class="space-y-2">
<div class="flex items-center gap-2">
<UCheckbox v-model="showSmith" />
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Critere Smith (S)
</label>
</div>
<template v-if="showSmith && local.smith_exponent !== null">
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500">Exposant Smith</span>
<span class="text-sm font-mono font-bold text-primary">{{ local.smith_exponent }}</span>
</div>
<URange
v-model="local.smith_exponent"
:min="0.01"
:max="1.0"
:step="0.01"
/>
<p class="text-xs text-gray-500">ceil(SmithWotSize^S) votes Smith requis</p>
</template>
</div>
<!-- TechComm exponent -->
<div class="space-y-2">
<div class="flex items-center gap-2">
<UCheckbox v-model="showTechcomm" />
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Critere TechComm (T)
</label>
</div>
<template v-if="showTechcomm && local.techcomm_exponent !== null">
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500">Exposant TechComm</span>
<span class="text-sm font-mono font-bold text-primary">{{ local.techcomm_exponent }}</span>
</div>
<URange
v-model="local.techcomm_exponent"
:min="0.01"
:max="1.0"
:step="0.01"
/>
<p class="text-xs text-gray-500">ceil(CoTecSize^T) votes TechComm requis</p>
</template>
</div>
<!-- Nuanced min participants -->
<div class="space-y-2">
<div class="flex items-center gap-2">
<UCheckbox v-model="showNuancedMin" />
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Participants minimum (nuance)
</label>
</div>
<template v-if="showNuancedMin && local.nuanced_min_participants !== null">
<UInput
v-model.number="local.nuanced_min_participants"
type="number"
:min="1"
:max="1000"
:step="1"
/>
<p class="text-xs text-gray-500">Nombre minimum de participants pour un vote nuance</p>
</template>
</div>
<!-- Nuanced threshold pct -->
<div class="space-y-2">
<div class="flex items-center gap-2">
<UCheckbox v-model="showNuancedThreshold" />
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Seuil nuance (%)
</label>
</div>
<template v-if="showNuancedThreshold && local.nuanced_threshold_pct !== null">
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500">Pourcentage du seuil</span>
<span class="text-sm font-mono font-bold text-primary">{{ local.nuanced_threshold_pct }}%</span>
</div>
<URange
v-model="local.nuanced_threshold_pct"
:min="50"
:max="100"
:step="1"
/>
<p class="text-xs text-gray-500">Seuil de score moyen pour adoption en vote nuance</p>
</template>
</div>
</div>
</div>
</div>
</template>