Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Layout } from './components/Layout';
import { AppStateProvider } from './hooks/useAppState';
import { AuthProvider } from './hooks/useAuth';
import { Demo } from './routes/Demo';
import { Landing } from './routes/Landing';
import { Circle } from './routes/console/Circle';
import { Console } from './routes/console/Console';
import { Dashboard } from './routes/console/Dashboard';
Expand All @@ -11,14 +12,15 @@ import { Holds } from './routes/console/Holds';
import { Inbox } from './routes/console/Inbox';
import { InsightsPage } from './routes/console/Insights';
import { PolicyPage } from './routes/console/PolicyPage';
import { Profile } from './routes/console/Profile';

export function App() {
return (
<AuthProvider>
<AppStateProvider>
<Layout>
<Routes>
<Route path="/" element={<Navigate to="/demo" replace />} />
<Route path="/" element={<Landing />} />
<Route path="/demo" element={<Demo />} />
<Route path="/family" element={<Console />}>
<Route index element={<Dashboard />} />
Expand All @@ -28,8 +30,9 @@ export function App() {
<Route path="policy" element={<PolicyPage />} />
<Route path="insights" element={<InsightsPage />} />
<Route path="inbox" element={<Inbox />} />
<Route path="profile" element={<Profile />} />
</Route>
<Route path="*" element={<Navigate to="/demo" replace />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</AppStateProvider>
Expand Down
54 changes: 27 additions & 27 deletions apps/web/src/components/ElderDevice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSpeech } from '../hooks/useSpeech';
import type { Card, TurnResult } from '../types';
import { RiskCard } from './RiskCard';
import { ErrorNote, TraceView } from './parts';
import { IconArrowRight, IconMic, IconVolume } from './icons';

export interface Message {
id: number;
Expand All @@ -15,13 +16,13 @@ export interface Message {

export const HERO_TEXT = 'The bank says I have to buy five $200 gift cards today or my account is frozen.';

export const SCENARIOS: Array<{ label: string; text: string; icon: string }> = [
{ label: 'Buy gift cards (a scam)', text: HERO_TEXT, icon: '🚨' },
{ label: 'Pay the pharmacy $24', text: 'Pay Greenleaf Pharmacy $24', icon: '💊' },
{ label: 'Is this text a scam?', text: 'I got a text saying my package is held and I must pay $2 to release it. Is this a scam?', icon: '📦' },
{ label: 'Pay my neighbour $50', text: 'Send $50 to my neighbour', icon: '🤝' },
{ label: "What's pending?", text: "What's pending?", icon: '📋' },
{ label: 'Try to approve it myself', text: 'approve it', icon: '🛡️' },
export const SCENARIOS: Array<{ label: string; text: string }> = [
{ label: 'Buy gift cards (a scam)', text: HERO_TEXT },
{ label: 'Pay the pharmacy $24', text: 'Pay Greenleaf Pharmacy $24' },
{ label: 'Is this text a scam?', text: 'I got a text saying my package is held and I must pay $2 to release it. Is this a scam?' },
{ label: 'Pay my neighbour $50', text: 'Send $50 to my neighbour' },
{ label: "What's pending?", text: "What's pending?" },
{ label: 'Try to approve it myself', text: 'approve it' },
];

interface Props {
Expand Down Expand Up @@ -81,37 +82,39 @@ export function ElderDevice({ messages, onMessages, onHeld, readAloud }: Props)
<span
aria-hidden="true"
style={{
fontSize: '1.4rem',
width: 38,
height: 38,
borderRadius: '50%',
borderRadius: 10,
background: 'var(--brand-soft)',
border: '1px solid var(--brand-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--brand)',
boxShadow: '0 0 12px var(--brand-glow)',
flexShrink: 0,
}}
>
🔊
<IconVolume size={18} aria-hidden="true" />
</span>
<div className="grow">
<div className="row" style={{ gap: 'var(--space-2)' }}>
<h2 id="elder-title" style={{ margin: 0, fontSize: 'var(--text-lg)' }}>
Dorothy's Echo
</h2>
<span
className="badge ok"
style={{ fontSize: '0.72rem', padding: '2px 8px' }}
>
<span className="badge ok" style={{ fontSize: '0.72rem', padding: '2px 8px' }}>
Alexa+ Active
</span>
</div>
<span className="muted small">Simulated Alexa+ in the kitchen · Hands-free voice assistant</span>
</div>
<div className="row" aria-hidden="true" style={{ gap: 6, opacity: 0.7 }}>
<span style={{ fontSize: '0.85rem' }}>📶</span>
<span style={{ fontSize: '0.85rem' }}>🔋 100%</span>
{/* Signal / battery indicators — text only, no emoji */}
<div
className="row"
aria-hidden="true"
style={{ gap: 6, opacity: 0.6, fontSize: '0.78rem', fontWeight: 600, fontFamily: 'var(--mono)' }}
>
<span>Wi-Fi</span>
<span>100%</span>
</div>
</div>

Expand Down Expand Up @@ -152,8 +155,8 @@ export function ElderDevice({ messages, onMessages, onHeld, readAloud }: Props)

<div className="device-body caption-size" role="log" aria-live="polite" aria-label="Conversation">
{messages.length === 0 && (
<div className="bubble note" style={{ padding: 'var(--space-4)' }}>
<span aria-hidden="true" style={{ fontSize: '1.8rem', display: 'block', marginBottom: 8 }}>🎙️</span>
<div className="bubble note" style={{ padding: 'var(--space-4)', display: 'grid', justifyItems: 'center', gap: 8 }}>
<IconMic size={28} aria-hidden="true" style={{ color: 'var(--muted)', opacity: 0.5 }} />
Press the microphone, type, or tap an example below.
</div>
)}
Expand Down Expand Up @@ -194,7 +197,6 @@ export function ElderDevice({ messages, onMessages, onHeld, readAloud }: Props)
disabled={busy}
onClick={() => void send(s.text)}
>
<span aria-hidden="true">{s.icon}</span>
{s.label}
</button>
))}
Expand All @@ -209,7 +211,7 @@ export function ElderDevice({ messages, onMessages, onHeld, readAloud }: Props)
disabled={busy}
onClick={() => speech.listen((t) => void send(t))}
>
🎤
<IconMic size={20} aria-hidden="true" />
</button>
)}
<label className="grow" style={{ minWidth: 0 }}>
Expand All @@ -231,14 +233,12 @@ export function ElderDevice({ messages, onMessages, onHeld, readAloud }: Props)
type="submit"
className="btn primary"
disabled={busy || !text.trim()}
style={{ borderRadius: 'var(--radius-full)', padding: '10px 22px' }}
style={{ borderRadius: 'var(--radius-full)', padding: '10px 22px', gap: 8 }}
>
{busy ? (
'Checking…'
) : (
{busy ? 'Checking…' : (
<>
<span>Send</span>
<span aria-hidden="true">→</span>
<IconArrowRight size={14} aria-hidden="true" />
</>
)}
</button>
Expand Down
133 changes: 77 additions & 56 deletions apps/web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@ import { useEffect, useState, type ReactNode } from 'react';
import { NavLink } from 'react-router-dom';
import { api } from '../api';
import { useAppState } from '../hooks/useAppState';
import { Badge } from './parts';
import { IconActivity, IconServer, IconShieldCheck } from './icons';

function StatusDot({ ok }: { ok: boolean | null }) {
const color = ok === null ? 'var(--muted)' : ok ? 'var(--ok)' : 'var(--danger)';
return (
<span
aria-hidden="true"
style={{
display: 'inline-block',
width: 7,
height: 7,
borderRadius: '50%',
background: color,
boxShadow: ok ? `0 0 6px ${color}` : 'none',
flexShrink: 0,
}}
/>
);
}

function HealthPill() {
const { live } = useAppState();
Expand All @@ -20,25 +38,42 @@ function HealthPill() {
}, []);

return (
<div className="row" aria-label="System status" style={{ gap: 'var(--space-2)' }}>
<Badge tone={mcp === null ? 'neutral' : mcp ? 'ok' : 'danger'}>
<div
className="row"
aria-label="System status"
style={{ gap: 'var(--space-2)', fontSize: 'var(--text-xs)', fontWeight: 600 }}
>
<span
className="row"
style={{
gap: 6,
padding: '5px 10px',
borderRadius: 'var(--radius-full)',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
color: mcp ? 'var(--ok)' : mcp === false ? 'var(--danger)' : 'var(--muted)',
}}
>
<IconServer size={12} aria-hidden="true" />
<StatusDot ok={mcp} />
{mcp === null ? 'Checking…' : mcp ? 'MCP server connected' : 'MCP server unreachable'}
</Badge>
<Badge tone={live ? 'info' : 'neutral'}>
<span
aria-hidden="true"
style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: live ? '#00caee' : 'var(--muted)',
boxShadow: live ? '0 0 8px #00caee' : 'none',
marginRight: 2,
}}
/>
</span>

<span
className="row"
style={{
gap: 6,
padding: '5px 10px',
borderRadius: 'var(--radius-full)',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
color: live ? 'var(--info)' : 'var(--muted)',
}}
>
<IconActivity size={12} aria-hidden="true" />
<StatusDot ok={live ? true : null} />
{live ? 'Live' : 'Offline'}
</Badge>
</span>
</div>
);
}
Expand All @@ -51,56 +86,42 @@ export function Layout({ children }: { children: ReactNode }) {
</a>
<header className="app-header">
<NavLink to="/demo" className="brand" aria-label="Second Opinion home">
<svg className="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
<defs>
<linearGradient id="shieldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#1d4ed8" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="10" fill="url(#shieldGrad)" />
<path
d="M16 6l8 3.2v6.4c0 5.4-3.6 9.4-8 11.4-4.4-2-8-6-8-11.4V9.2z"
fill="#ffffff"
/>
<path
d="M16 9.5v13.5c3.2-1.6 5.8-4.7 5.8-8.8V11.8z"
fill="rgba(255, 255, 255, 0.25)"
/>
<path
d="M13.5 15.5l2 2 3.5-3.5"
stroke="#2563eb"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ lineHeight: 1.15 }}>Second Opinion</span>
<span
aria-hidden="true"
style={{
width: 34,
height: 34,
borderRadius: 10,
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: '0 2px 8px rgba(37,99,235,0.35)',
}}
>
<IconShieldCheck size={18} style={{ color: '#fff', strokeWidth: '2' }} aria-hidden="true" />
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<span style={{ lineHeight: 1.15, letterSpacing: '-0.02em' }}>Second Opinion</span>
<span
style={{
fontSize: '0.68rem',
fontSize: '0.65rem',
fontWeight: 600,
color: 'var(--muted)',
letterSpacing: '0.04em',
letterSpacing: '0.06em',
textTransform: 'uppercase',
}}
>
Payment Guard for Alexa+
Payment Guard · Alexa+
</span>
</div>
</NavLink>

<nav className="nav" aria-label="Main">
<NavLink to="/demo">
<span aria-hidden="true">🎙️</span>
Live demo
</NavLink>
<NavLink to="/family">
<span aria-hidden="true">🛡️</span>
Family console
</NavLink>
<NavLink to="/" end>Overview</NavLink>
<NavLink to="/demo">Live demo</NavLink>
<NavLink to="/family">Family console</NavLink>
</nav>

<div className="right">
Expand Down
23 changes: 15 additions & 8 deletions apps/web/src/components/RiskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import type { Card } from '../types';
import { IconAlertTriangle, IconCheckCircle, IconXCircle, IconLightbulb } from './icons';

const ICON: Record<Card['level'], string> = { hold: '✕', caution: '!', allow: '✓' };
const WORD: Record<Card['level'], string> = { hold: 'Held', caution: 'Careful', allow: 'Looks OK' };

function LevelIcon({ level }: { level: Card['level'] }) {
if (level === 'hold') return <IconXCircle size={18} style={{ color: 'var(--danger)' }} />;
if (level === 'caution') return <IconAlertTriangle size={18} style={{ color: 'var(--warn)' }} />;
return <IconCheckCircle size={18} style={{ color: 'var(--ok)' }} />;
}

/** The red-flag card. Colour is never the only signal: every level also has an icon and a word. */
export function RiskCard({ card, className = '' }: { card: Card; className?: string }) {
const iconEmoji = card.level === 'hold' ? '🚨' : card.level === 'caution' ? '⚠️' : '✅';
return (
<section className={`risk ${card.level} ${className}`} aria-label={`${WORD[card.level]}: ${card.title}`}>
<div className="row" style={{ justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-2)' }}>
<h3 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '6px' }}>
<span aria-hidden="true" style={{ fontWeight: 800 }}>{ICON[card.level]} </span>
<h3 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
<LevelIcon level={card.level} />
<span>{card.title}</span>
</h3>
<span aria-hidden="true" style={{ fontSize: '1.15rem' }}>{iconEmoji}</span>
<span className={`badge ${card.level === 'hold' ? 'danger' : card.level === 'caution' ? 'warn' : 'ok'}`} style={{ fontSize: 'var(--text-xs)' }}>
{WORD[card.level]}
</span>
</div>
{card.flags.length > 0 && (
<ul style={{ margin: 'var(--space-2) 0', paddingLeft: '1.4em', display: 'grid', gap: 4 }}>
Expand All @@ -25,9 +32,9 @@ export function RiskCard({ card, className = '' }: { card: Card; className?: str
</ul>
)}
{card.advice && (
<p className="advice">
<span aria-hidden="true" style={{ marginRight: 4 }}>💡</span>
{card.advice}
<p className="advice" style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<IconLightbulb size={16} style={{ flexShrink: 0, marginTop: 2, color: 'var(--brand)' }} />
<span>{card.advice}</span>
</p>
)}
</section>
Expand Down
Loading
Loading