From 5442b6249576e4b940f14f0b483312b4a2c9c477 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Wed, 1 Jul 2026 09:43:43 +0200 Subject: [PATCH] fix: decouple the sentiment weight from the base mix in the weights form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sentiment is now a signed adjustment (± points on top of the base), not part of the averaged dimensions — but the weights form still squeezed all five sliders to sum to 100%, so dragging sentiment rebalanced the base and you couldn't set a clean ±N. Now the four base dimensions normalize among themselves (share shown as %), and sentiment is its own "influence (± points)" control passed through raw (slider 10 → weight 0.10 → ±10), independent of the base. Co-Authored-By: Claude Opus 4.8 --- .../src/components/rankings/WeightsForm.tsx | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/rankings/WeightsForm.tsx b/frontend/src/components/rankings/WeightsForm.tsx index 9b43274..eaf2d6c 100644 --- a/frontend/src/components/rankings/WeightsForm.tsx +++ b/frontend/src/components/rankings/WeightsForm.tsx @@ -1,12 +1,16 @@ -import { useState, useMemo, type FormEvent } from 'react'; +import { useState, type FormEvent } from 'react'; import { useUpdateWeights } from '../../hooks/useScores'; interface WeightsFormProps { weights: Record; } +const SENTIMENT = 'sentiment'; + export function WeightsForm({ weights }: WeightsFormProps) { - // Convert API decimal weights (0-1) to 0-100 integer scale on mount + // API decimal weights (0-1) → 0-100 integer sliders. For the base dimensions + // that's their share of the weighted average; for sentiment it's the ± points + // it can move the composite (MAX_ADJ), decoupled from the base mix. const [sliderValues, setSliderValues] = useState>(() => Object.fromEntries( Object.entries(weights).map(([key, w]) => [key, Math.round(w * 100)]) @@ -14,10 +18,10 @@ export function WeightsForm({ weights }: WeightsFormProps) { ); const updateWeights = useUpdateWeights(); - const allZero = useMemo( - () => Object.values(sliderValues).every((v) => v === 0), - [sliderValues] - ); + const baseKeys = Object.keys(weights).filter((k) => k !== SENTIMENT); + const hasSentiment = SENTIMENT in weights; + const baseTotal = baseKeys.reduce((sum, k) => sum + (sliderValues[k] ?? 0), 0); + const sentimentPts = sliderValues[SENTIMENT] ?? 0; const handleChange = (key: string, value: string) => { const num = parseInt(value, 10); @@ -26,24 +30,35 @@ export function WeightsForm({ weights }: WeightsFormProps) { const handleSubmit = (e: FormEvent) => { e.preventDefault(); - if (allZero) return; + if (baseTotal === 0) return; - const total = Object.values(sliderValues).reduce((sum, v) => sum + v, 0); - const normalized = Object.fromEntries( - Object.entries(sliderValues).map(([key, v]) => [key, v / total]) + // Base dimensions normalize among themselves; sentiment passes through raw + // (slider value / 100) so it stays independent of the base. + const payload: Record = Object.fromEntries( + baseKeys.map((key) => [key, (sliderValues[key] ?? 0) / baseTotal]) ); - updateWeights.mutate(normalized); + if (hasSentiment) payload[SENTIMENT] = sentimentPts / 100; + updateWeights.mutate(payload); }; return (
-

+

Scoring Weights

+

+ The base dimensions are a weighted average (shares normalize to 100%). Sentiment is applied + separately as a signed adjustment on top. +

- {Object.keys(weights).map((key) => ( + {baseKeys.map((key) => (