feat: Added personality weight slider (0.0–1.0) to ChatWidget header an…
- "frontend/src/api/chat.ts" - "frontend/src/components/ChatWidget.tsx" - "frontend/src/components/ChatWidget.module.css" GSD-Task: S02/T02
This commit is contained in:
parent
d1efdbb3fa
commit
3bb0266a19
6 changed files with 187 additions and 2 deletions
|
|
@ -47,7 +47,7 @@
|
||||||
- Estimate: 1h
|
- Estimate: 1h
|
||||||
- Files: backend/routers/chat.py, backend/chat_service.py, backend/tests/test_chat.py
|
- Files: backend/routers/chat.py, backend/chat_service.py, backend/tests/test_chat.py
|
||||||
- Verify: cd backend && python -m pytest tests/test_chat.py -v
|
- Verify: cd backend && python -m pytest tests/test_chat.py -v
|
||||||
- [ ] **T02: Frontend — personality slider UI + API wiring in ChatWidget** — Add a personality weight slider to ChatWidget and thread it through the streamChat() API call.
|
- [x] **T02: Added personality weight slider (0.0–1.0) to ChatWidget header and wired it through streamChat() to the chat API** — Add a personality weight slider to ChatWidget and thread it through the streamChat() API call.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
|
|
|
||||||
24
.gsd/milestones/M023/slices/S02/tasks/T01-VERIFY.json
Normal file
24
.gsd/milestones/M023/slices/S02/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T01",
|
||||||
|
"unitId": "M023/S02/T01",
|
||||||
|
"timestamp": 1775294915224,
|
||||||
|
"passed": false,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd backend",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 9,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "python -m pytest tests/test_chat.py -v",
|
||||||
|
"exitCode": 4,
|
||||||
|
"durationMs": 231,
|
||||||
|
"verdict": "fail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"retryAttempt": 1,
|
||||||
|
"maxRetries": 2
|
||||||
|
}
|
||||||
79
.gsd/milestones/M023/slices/S02/tasks/T02-SUMMARY.md
Normal file
79
.gsd/milestones/M023/slices/S02/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S02
|
||||||
|
milestone: M023
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["frontend/src/api/chat.ts", "frontend/src/components/ChatWidget.tsx", "frontend/src/components/ChatWidget.module.css"]
|
||||||
|
key_decisions: ["Slider placed below header as its own row with border-bottom separator for clean layout at 400px panel width"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "Frontend build passes with zero errors (184 modules, 2.88s). Backend tests all pass (22/22) including T01's personality weight tests."
|
||||||
|
completed_at: 2026-04-04T09:30:50.961Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added personality weight slider (0.0–1.0) to ChatWidget header and wired it through streamChat() to the chat API
|
||||||
|
|
||||||
|
> Added personality weight slider (0.0–1.0) to ChatWidget header and wired it through streamChat() to the chat API
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S02
|
||||||
|
milestone: M023
|
||||||
|
key_files:
|
||||||
|
- frontend/src/api/chat.ts
|
||||||
|
- frontend/src/components/ChatWidget.tsx
|
||||||
|
- frontend/src/components/ChatWidget.module.css
|
||||||
|
key_decisions:
|
||||||
|
- Slider placed below header as its own row with border-bottom separator for clean layout at 400px panel width
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-04-04T09:30:50.962Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added personality weight slider (0.0–1.0) to ChatWidget header and wired it through streamChat() to the chat API
|
||||||
|
|
||||||
|
**Added personality weight slider (0.0–1.0) to ChatWidget header and wired it through streamChat() to the chat API**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Added personalityWeight optional parameter to streamChat() in chat.ts, sent as personality_weight in the POST body. In ChatWidget.tsx, added slider state and a styled range input between the header and messages area, bounded by 'Encyclopedic' and 'Creator Voice' labels. The value flows through sendMessage to streamChat as the 5th argument. Added CSS for slider row with custom thumb/track styling matching the dark theme.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Frontend build passes with zero errors (184 modules, 2.88s). Backend tests all pass (22/22) including T01's personality weight tests.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 6500ms |
|
||||||
|
| 2 | `cd backend && python -m pytest tests/test_chat.py -v` | 0 | ✅ pass | 3800ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Slice-level verification command must run from backend/ directory, not project root — the original failure was a path issue.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `frontend/src/api/chat.ts`
|
||||||
|
- `frontend/src/components/ChatWidget.tsx`
|
||||||
|
- `frontend/src/components/ChatWidget.module.css`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Slice-level verification command must run from backend/ directory, not project root — the original failure was a path issue.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
|
|
@ -42,6 +42,7 @@ export function streamChat(
|
||||||
callbacks: ChatCallbacks,
|
callbacks: ChatCallbacks,
|
||||||
creator?: string,
|
creator?: string,
|
||||||
conversationId?: string,
|
conversationId?: string,
|
||||||
|
personalityWeight?: number,
|
||||||
): AbortController {
|
): AbortController {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
|
@ -67,6 +68,7 @@ export function streamChat(
|
||||||
query,
|
query,
|
||||||
creator: creator ?? null,
|
creator: creator ?? null,
|
||||||
conversation_id: conversationId ?? null,
|
conversation_id: conversationId ?? null,
|
||||||
|
personality_weight: personalityWeight ?? 0,
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,68 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Personality slider ───────────────────────────────────── */
|
||||||
|
|
||||||
|
.sliderRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderLabel {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
flex: 1;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--color-border);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent);
|
||||||
|
border: 2px solid var(--color-bg-surface);
|
||||||
|
box-shadow: 0 1px 4px var(--color-shadow-heavy);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent);
|
||||||
|
border: 2px solid var(--color-bg-surface);
|
||||||
|
box-shadow: 0 1px 4px var(--color-shadow-heavy);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-track {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
.headerTitle {
|
.headerTitle {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [streaming, setStreaming] = useState(false);
|
const [streaming, setStreaming] = useState(false);
|
||||||
const [conversationId, setConversationId] = useState<string | undefined>(undefined);
|
const [conversationId, setConversationId] = useState<string | undefined>(undefined);
|
||||||
|
const [personalityWeight, setPersonalityWeight] = useState(0);
|
||||||
|
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -224,9 +225,10 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
|
||||||
},
|
},
|
||||||
creatorName,
|
creatorName,
|
||||||
cid,
|
cid,
|
||||||
|
personalityWeight,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[messages, streaming, creatorName, conversationId],
|
[messages, streaming, creatorName, conversationId, personalityWeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
|
@ -272,6 +274,22 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Personality slider */}
|
||||||
|
<div className={styles.sliderRow}>
|
||||||
|
<span className={styles.sliderLabel}>Encyclopedic</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className={styles.slider}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
value={personalityWeight}
|
||||||
|
onChange={(e) => setPersonalityWeight(parseFloat(e.target.value))}
|
||||||
|
aria-label="Personality weight"
|
||||||
|
/>
|
||||||
|
<span className={styles.sliderLabel}>Creator Voice</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className={styles.messages}>
|
<div className={styles.messages}>
|
||||||
{showSuggestions && (
|
{showSuggestions && (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue