feat: Added getTierLabel() helper, gradient track fill via --slider-fil…

- "frontend/src/components/ChatWidget.tsx"
- "frontend/src/components/ChatWidget.module.css"

GSD-Task: S04/T02
This commit is contained in:
jlightner 2026-04-04 10:06:46 +00:00
parent 1062e003bf
commit 4db33399e9
5 changed files with 173 additions and 19 deletions

View file

@ -25,7 +25,7 @@ Update existing test `test_personality_prompt_injected_when_weight_and_profile`
- Estimate: 45m - Estimate: 45m
- Files: backend/chat_service.py, backend/tests/test_chat.py - Files: backend/chat_service.py, backend/tests/test_chat.py
- Verify: cd backend && python -m pytest tests/test_chat.py -v -k personality - Verify: cd backend && python -m pytest tests/test_chat.py -v -k personality
- [ ] **T02: Enhance slider UX with dynamic tier label, value indicator, and gradient track** — Enhance the personality slider in ChatWidget.tsx with three visual feedback features: - [x] **T02: Added getTierLabel() helper, gradient track fill via --slider-fill CSS custom property, and centered tier label + numeric value below the personality slider** — Enhance the personality slider in ChatWidget.tsx with three visual feedback features:
1. **Dynamic tier label**: Below the slider, show a centered label that changes based on current weight value: 1. **Dynamic tier label**: Below the slider, show a centered label that changes based on current weight value:
- 0.00.19: "Encyclopedic" - 0.00.19: "Encyclopedic"

View file

@ -0,0 +1,24 @@
{
"schemaVersion": 1,
"taskId": "T01",
"unitId": "M023/S04/T01",
"timestamp": 1775297087024,
"passed": false,
"discoverySource": "task-plan",
"checks": [
{
"command": "cd backend",
"exitCode": 0,
"durationMs": 11,
"verdict": "pass"
},
{
"command": "python -m pytest tests/test_chat.py -v -k personality",
"exitCode": 4,
"durationMs": 264,
"verdict": "fail"
}
],
"retryAttempt": 1,
"maxRetries": 2
}

View file

@ -0,0 +1,79 @@
---
id: T02
parent: S04
milestone: M023
provides: []
requires: []
affects: []
key_files: ["frontend/src/components/ChatWidget.tsx", "frontend/src/components/ChatWidget.module.css"]
key_decisions: ["Wrapped sliderRow in sliderSection container for clean grouping", "Used CSS custom property --slider-fill via inline style for gradient track", "Tabular-nums on tier value for stable width"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "Frontend build passes clean (cd frontend && npm run build, exit 0). Backend personality tests all pass (cd backend && python -m pytest tests/test_chat.py -v -k personality, 11/11 passed)."
completed_at: 2026-04-04T10:06:38.357Z
blocker_discovered: false
---
# T02: Added getTierLabel() helper, gradient track fill via --slider-fill CSS custom property, and centered tier label + numeric value below the personality slider
> Added getTierLabel() helper, gradient track fill via --slider-fill CSS custom property, and centered tier label + numeric value below the personality slider
## What Happened
---
id: T02
parent: S04
milestone: M023
key_files:
- frontend/src/components/ChatWidget.tsx
- frontend/src/components/ChatWidget.module.css
key_decisions:
- Wrapped sliderRow in sliderSection container for clean grouping
- Used CSS custom property --slider-fill via inline style for gradient track
- Tabular-nums on tier value for stable width
duration: ""
verification_result: passed
completed_at: 2026-04-04T10:06:38.357Z
blocker_discovered: false
---
# T02: Added getTierLabel() helper, gradient track fill via --slider-fill CSS custom property, and centered tier label + numeric value below the personality slider
**Added getTierLabel() helper, gradient track fill via --slider-fill CSS custom property, and centered tier label + numeric value below the personality slider**
## What Happened
Three visual feedback enhancements to the personality slider: getTierLabel() maps weight ranges to 5 tier labels matching the backend interpolation tiers; gradient track fill via --slider-fill CSS custom property on both webkit and moz pseudo-elements; centered tier info row with label and tabular-nums numeric value. Wrapped sliderRow in sliderSection container for clean grouping.
## Verification
Frontend build passes clean (cd frontend && npm run build, exit 0). Backend personality tests all pass (cd backend && python -m pytest tests/test_chat.py -v -k personality, 11/11 passed).
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 6800ms |
| 2 | `cd backend && python -m pytest tests/test_chat.py -v -k personality` | 0 | ✅ pass | 3900ms |
## Deviations
Added .sliderSection wrapper div to group slider row and tier info under one border-bottom — minor structural improvement not in original plan.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/components/ChatWidget.tsx`
- `frontend/src/components/ChatWidget.module.css`
## Deviations
Added .sliderSection wrapper div to group slider row and tier info under one border-bottom — minor structural improvement not in original plan.
## Known Issues
None.

View file

@ -69,13 +69,16 @@
/* ── Personality slider ───────────────────────────────────── */ /* ── Personality slider ───────────────────────────────────── */
.sliderSection {
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.sliderRow { .sliderRow {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
} }
.sliderLabel { .sliderLabel {
@ -91,7 +94,11 @@
appearance: none; appearance: none;
height: 4px; height: 4px;
border-radius: 2px; border-radius: 2px;
background: var(--color-border); background: linear-gradient(
to right,
var(--color-accent) var(--slider-fill, 0%),
var(--color-border) var(--slider-fill, 0%)
);
outline: none; outline: none;
cursor: pointer; cursor: pointer;
} }
@ -126,7 +133,35 @@
.slider::-moz-range-track { .slider::-moz-range-track {
height: 4px; height: 4px;
border-radius: 2px; border-radius: 2px;
background: var(--color-border); background: linear-gradient(
to right,
var(--color-accent) var(--slider-fill, 0%),
var(--color-border) var(--slider-fill, 0%)
);
}
/* ── Tier label + value ───────────────────────────────────── */
.tierInfo {
display: flex;
justify-content: center;
align-items: baseline;
gap: 0.375rem;
padding-top: 0.25rem;
}
.tierLabel {
font-size: 0.6875rem;
font-weight: 600;
color: var(--color-text-secondary);
transition: color 0.2s;
}
.tierValue {
font-size: 0.625rem;
color: var(--color-text-secondary);
opacity: 0.6;
font-variant-numeric: tabular-nums;
} }
.headerTitle { .headerTitle {

View file

@ -115,6 +115,15 @@ function parseCitations(text: string, sources: ChatSource[]): React.ReactNode[]
return nodes.length > 0 ? nodes : [text]; return nodes.length > 0 ? nodes : [text];
} }
/** Map personality weight to a human-readable tier label. */
function getTierLabel(weight: number): string {
if (weight < 0.2) return "Encyclopedic";
if (weight < 0.4) return "Subtle Reference";
if (weight < 0.6) return "Creator Tone";
if (weight < 0.8) return "Creator Voice";
return "Full Embodiment";
}
export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) { export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
@ -275,19 +284,26 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
</div> </div>
{/* Personality slider */} {/* Personality slider */}
<div className={styles.sliderRow}> <div className={styles.sliderSection}>
<span className={styles.sliderLabel}>Encyclopedic</span> <div className={styles.sliderRow}>
<input <span className={styles.sliderLabel}>Encyclopedic</span>
type="range" <input
className={styles.slider} type="range"
min={0} className={styles.slider}
max={1} min={0}
step={0.1} max={1}
value={personalityWeight} step={0.1}
onChange={(e) => setPersonalityWeight(parseFloat(e.target.value))} value={personalityWeight}
aria-label="Personality weight" onChange={(e) => setPersonalityWeight(parseFloat(e.target.value))}
/> aria-label="Personality weight"
<span className={styles.sliderLabel}>Creator Voice</span> style={{ '--slider-fill': `${personalityWeight * 100}%` } as React.CSSProperties}
/>
<span className={styles.sliderLabel}>Creator Voice</span>
</div>
<div className={styles.tierInfo}>
<span className={styles.tierLabel}>{getTierLabel(personalityWeight)}</span>
<span className={styles.tierValue}>{personalityWeight.toFixed(1)}</span>
</div>
</div> </div>
{/* Messages */} {/* Messages */}