Skip to content

Commit f561e98

Browse files
author
AvatarOS Developer
committed
feat(empathy+hume): add EVI config UI in sidebar; server proxy routes; client helpers; fix Hume connect/session timing; wrap viewer panel
1 parent 159803b commit f561e98

27 files changed

+806
-154
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
## Recent Changes & Rationale
4848
- For a concise summary of the latest implementation updates (Markdown rendering, mock persistence, drawer UI, centered layouts, headers), see `docs/IMPLEMENTATION_NOTES.md`.
4949
- UI primitives and usage are documented in `docs/ui/components.md`; tokens are described in `docs/ui/tokens.md`.
50+
- Sidebar improvements: Calendar sidebar now provides a day-actions tooltip (create/see) and integrates with the main view. See `docs/ui/sidebar.md`.
5051

5152
## Changelog Process (All Agents)
5253
- Before committing and pushing changes:

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,16 @@
5151
2025-10-03: Sidebar tooltip now closes on outside click and Escape; added dialog role and ref containment.
5252

5353
2025-10-03: Sidebar tooltip positioned to the right of day; vertical stack; added caret arrow matching theme.
54+
2025-10-03: Docs: added sidebar pattern and Storybook Episode 2 plan; linked from README and AGENTS.md.
55+
2025-10-03: Storybook: added AppSwitcher and UserBar stories using real store/actions (no mocks).
56+
2025-10-03: Storybook: added small sidebar element stories (MiniCalendarHeader, DayActionsTooltip, SidebarCategoryItem, CalendarConnectionStatus).
57+
2025-10-03: EmpathyLab: extracted VideoFrame and RecordingIndicator; replaced inline markup; added aria-live to error display; added presentational components + Storybook stories (ConsentItem, UseCaseItem).
58+
2025-10-03: EmpathyLabSidebar layout: enforce single-column rows (full-width panels), remove nested scrolling for cleaner sidebar behavior.
59+
2025-10-03: EmpathyLabSidebar restructure: sticky top preset bar, Tracking Permissions panel only, sticky privacy banner; removed Use Cases/About from sidebar.
60+
2025-10-03: EmpathyLab: wrapped VideoFrame in Panel titled 'Webcam Viewer'; sidebar now shows individual consent mini-cards (no permissions panel wrapper).
61+
2025-10-03: Fix HumeTest rendering: guard against object children (status, message.role/content); normalize to strings to avoid React object child error).
62+
2025-10-03: Fix Hume connect(): pass default audioConstraints to avoid destructuring error in VoiceProvider.
63+
2025-10-03: Docs: added Hume EVI technical guide (server token endpoint, client VoiceProvider, session settings timing, function calling, TTS).
64+
2025-10-03: Server: added Hume proxy routes (/api/hume/configs|prompts|tools) using server-side API key; kept existing /api/services/hume/token.
65+
2025-10-03: Client: added src/lib/services/hume.js (token + configs/prompts/tools helpers).
66+
2025-10-03: EmpathyLab sidebar: added EVI Configuration accordion with form + Saved EVI Configs list; uses /api/hume/configs via client helper; persisted to localStorage.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ npm run dev
203203
- Stories live under `src/**/*.stories.jsx` (examples: `src/components/BoothHeader.stories.jsx`, `src/components/ui/*.stories.jsx`, `src/stories/Tokens.stories.jsx`).
204204
- Use the “Accessibility” panel to scan for WCAG issues; the “Interactions” panel can step through play functions.
205205
- Tokens: inspect color/typography via `Tokens/Overview` story.
206-
- Docs: UI tokens (`docs/ui/tokens.md`) and components (`docs/ui/components.md`).
206+
- Docs: UI tokens (`docs/ui/tokens.md`) and components (`docs/ui/components.md`).
207+
- Sidebars: see `docs/ui/sidebar.md`; upcoming plan in `docs/ui/storybook-episode-02.md`.
208+
- Voice/EVI: Hume integration guide in `docs/integrations/hume-evi.md`.
207209

208210
## Deployment
209211

server.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,79 @@ app.get('/api/services/hume/token', requireAuth, async (req, res) => {
13691369
}
13701370
});
13711371

1372+
// Hume API proxy routes (server-side key usage)
1373+
app.post('/api/hume/configs', requireAuth, async (req, res) => {
1374+
try {
1375+
const apiKey = process.env.HUME_API_KEY;
1376+
if (!apiKey) return res.status(500).json({ error: 'HUME_API_KEY not configured' });
1377+
const r = await fetch('https://api.hume.ai/v0/evi/configs', {
1378+
method: 'POST',
1379+
headers: {
1380+
'Content-Type': 'application/json',
1381+
'X-Hume-Api-Key': apiKey
1382+
},
1383+
body: JSON.stringify(req.body)
1384+
});
1385+
const data = await r.json().catch(() => ({}));
1386+
if (!r.ok) {
1387+
logger.error('Hume create config failed', { status: r.status, data });
1388+
return res.status(r.status).json(data);
1389+
}
1390+
res.json(data);
1391+
} catch (err) {
1392+
logger.error('Hume create config error:', err);
1393+
res.status(500).json({ error: 'Failed to create Hume config', details: err.message });
1394+
}
1395+
});
1396+
1397+
app.post('/api/hume/prompts', requireAuth, async (req, res) => {
1398+
try {
1399+
const apiKey = process.env.HUME_API_KEY;
1400+
if (!apiKey) return res.status(500).json({ error: 'HUME_API_KEY not configured' });
1401+
const r = await fetch('https://api.hume.ai/v0/evi/prompts', {
1402+
method: 'POST',
1403+
headers: {
1404+
'Content-Type': 'application/json',
1405+
'X-Hume-Api-Key': apiKey
1406+
},
1407+
body: JSON.stringify(req.body)
1408+
});
1409+
const data = await r.json().catch(() => ({}));
1410+
if (!r.ok) {
1411+
logger.error('Hume create prompt failed', { status: r.status, data });
1412+
return res.status(r.status).json(data);
1413+
}
1414+
res.json(data);
1415+
} catch (err) {
1416+
logger.error('Hume create prompt error:', err);
1417+
res.status(500).json({ error: 'Failed to create Hume prompt', details: err.message });
1418+
}
1419+
});
1420+
1421+
app.post('/api/hume/tools', requireAuth, async (req, res) => {
1422+
try {
1423+
const apiKey = process.env.HUME_API_KEY;
1424+
if (!apiKey) return res.status(500).json({ error: 'HUME_API_KEY not configured' });
1425+
const r = await fetch('https://api.hume.ai/v0/evi/tools', {
1426+
method: 'POST',
1427+
headers: {
1428+
'Content-Type': 'application/json',
1429+
'X-Hume-Api-Key': apiKey
1430+
},
1431+
body: JSON.stringify(req.body)
1432+
});
1433+
const data = await r.json().catch(() => ({}));
1434+
if (!r.ok) {
1435+
logger.error('Hume create tool failed', { status: r.status, data });
1436+
return res.status(r.status).json(data);
1437+
}
1438+
res.json(data);
1439+
} catch (err) {
1440+
logger.error('Hume create tool error:', err);
1441+
res.status(500).json({ error: 'Failed to create Hume tool', details: err.message });
1442+
}
1443+
});
1444+
13721445
// Expose the /metrics endpoint for Prometheus scraping
13731446
app.get('/metrics', async (req, res) => {
13741447
try {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import AppSwitcher from './AppSwitcher.jsx';
3+
import useStore from '../lib/store.js';
4+
import { switchApp } from '../lib/actions.js';
5+
6+
export default {
7+
title: 'Layout/AppSwitcher',
8+
component: AppSwitcher,
9+
parameters: { layout: 'centered' }
10+
};
11+
12+
export const Default = {
13+
render: () => {
14+
// Set an initial app; rely on real store/actions (no mocks)
15+
useStore.setState((s) => { s.activeApp = 'archiva'; });
16+
return (
17+
<div style={{ padding: 16 }}>
18+
<AppSwitcher />
19+
<div style={{ marginTop: 12, fontSize: 12, opacity: 0.8 }}>
20+
Active: {useStore.getState().activeApp}
21+
</div>
22+
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
23+
<button onClick={() => { switchApp('prev'); }}>Prev (action)</button>
24+
<button onClick={() => { switchApp('next'); }}>Next (action)</button>
25+
</div>
26+
</div>
27+
);
28+
}
29+
};
30+

src/components/CalendarAISidebar.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,4 @@ const CalendarConnectionStatus = () => {
367367
};
368368

369369
export default CalendarAISidebar;
370+
export { CalendarConnectionStatus };
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { CalendarConnectionStatus } from './CalendarAISidebar.jsx';
3+
import useStore from '../lib/store.js';
4+
5+
export default {
6+
title: 'Sidebar/CalendarConnectionStatus',
7+
component: CalendarConnectionStatus,
8+
parameters: { layout: 'centered' }
9+
};
10+
11+
export const Connected = {
12+
render: () => {
13+
useStore.setState((s) => {
14+
s.connectedServices.googleCalendar = { connected: true, status: 'connected', info: { name: 'googleCalendar' } };
15+
});
16+
return <div style={{ width: 360 }}><CalendarConnectionStatus /></div>;
17+
}
18+
};
19+
20+
export const Disconnected = {
21+
render: () => {
22+
useStore.setState((s) => {
23+
s.connectedServices.googleCalendar = { connected: false, status: 'disconnected', info: null };
24+
});
25+
return <div style={{ width: 360 }}><CalendarConnectionStatus /></div>;
26+
}
27+
};
28+

src/components/EmpathyLab.jsx

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import BoothHeader from './BoothHeader.jsx';
77
import Button from './ui/Button.jsx';
88
import Panel from './ui/Panel.jsx';
99
import FormField from './ui/FormField.jsx';
10+
import VideoFrame from './empathy/VideoFrame.jsx';
11+
import RecordingIndicator from './empathy/RecordingIndicator.jsx';
1012

1113
export default function EmpathyLab() {
1214
const videoRef = useRef(null);
@@ -496,7 +498,7 @@ export default function EmpathyLab() {
496498

497499
<div className="empathy-lab-main">
498500
{error && (
499-
<div className="error-display">
501+
<div className="error-display" aria-live="polite" role="status">
500502
<span className="icon">error</span>
501503
<div>
502504
<h4>Error</h4>
@@ -508,30 +510,17 @@ export default function EmpathyLab() {
508510
</div>
509511
)}
510512

511-
<div className="video-container">
512-
<video
513-
ref={videoRef}
514-
style={{ display: 'none' }}
515-
playsInline
516-
muted
517-
/>
518-
<canvas ref={canvasRef} className="video-canvas" />
519-
<canvas ref={overlayCanvasRef} className="overlay-canvas" />
520-
521-
{isTracking && (
522-
<div className="recording-indicator">
523-
<span className="recording-dot"></span>
524-
<span>Tracking Active</span>
525-
</div>
526-
)}
527-
528-
{!isTracking && !error && (
529-
<div className="placeholder">
530-
<span className="icon">videocam_off</span>
531-
<p>Configure privacy settings and click "Start Tracking"</p>
532-
</div>
533-
)}
534-
</div>
513+
<Panel title="Webcam Viewer" info={isTracking ? 'Live' : 'Idle'}>
514+
<VideoFrame
515+
videoRef={videoRef}
516+
canvasRef={canvasRef}
517+
overlayCanvasRef={overlayCanvasRef}
518+
active={isTracking}
519+
error={!!error}
520+
>
521+
<RecordingIndicator active={isTracking} />
522+
</VideoFrame>
523+
</Panel>
535524

536525
{results && (
537526
<ResultsPanel

0 commit comments

Comments
 (0)