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:
parent
1062e003bf
commit
4db33399e9
5 changed files with 173 additions and 19 deletions
|
|
@ -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.0–0.19: "Encyclopedic"
|
- 0.0–0.19: "Encyclopedic"
|
||||||
|
|
|
||||||
24
.gsd/milestones/M023/slices/S04/tasks/T01-VERIFY.json
Normal file
24
.gsd/milestones/M023/slices/S04/tasks/T01-VERIFY.json
Normal 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
|
||||||
|
}
|
||||||
79
.gsd/milestones/M023/slices/S04/tasks/T02-SUMMARY.md
Normal file
79
.gsd/milestones/M023/slices/S04/tasks/T02-SUMMARY.md
Normal 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.
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue