Skip to content

Commit 42cb530

Browse files
Programcodeclaude
andcommitted
One owl on each key screen
Home points at Start debate. Generate thinks while it writes and looks surprised when the run fails. History's empty states sleep, the Library reads, and the Debate Timer title holds a stopwatch. The Sparring opponent argues, and the Ask coach gives a thumbs-up. Case Saved celebrates beside its check. The 404 and the won ballot already had the right pose. Owls beside text stay decorative. The coach and opponent avatars get alt text in English, Hungarian and Spanish. No other copy changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 67628a8 commit 42cb530

16 files changed

Lines changed: 79 additions & 31 deletions

File tree

‎src/components/features/ask/AskChat.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, onSave }: { m
115115
return (
116116
<div className={cn('flex items-end gap-2.5', isUser ? 'justify-end motion-safe:animate-in fade-in-0 slide-in-from-bottom-1 duration-200 ease-out-strong' : 'motion-safe:animate-in fade-in-0 slide-in-from-bottom-1 duration-200 ease-out-strong justify-start')}>
117117
{/* The coach is the owl. */}
118-
{!isUser && <OwlAvatar pose="idea" size={32} className="mb-0.5 hidden sm:inline-flex" />}
118+
{!isUser && <OwlAvatar pose="coach" size={32} label={t('askChat.coachAvatarAlt', 'The owl, your debate coach')} className="mb-0.5 hidden sm:inline-flex" />}
119119
<div
120120
className={cn(
121121
'max-w-[85%] rounded-[20px] px-4 py-3 space-y-3 relative group',
@@ -577,7 +577,7 @@ export function AskChat({ messages, streamingMessage, isLoading, error, onDismis
577577
{/* Wraps on a phone: the Hungarian title and two buttons do not fit one 375 px row. */}
578578
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
579579
<div className="flex min-w-0 items-center gap-3">
580-
<OwlAvatar pose={waitingForText && searchingWeb ? 'skeptical' : 'idea'} size={44} />
580+
<OwlAvatar pose="coach" size={44} label={t('askChat.coachAvatarAlt', 'The owl, your debate coach')} />
581581
<div className="min-w-0">
582582
<CardTitle>{t('askChat.title')}</CardTitle>
583583
<CardDescription>{t('askChat.subtitle')}</CardDescription>
@@ -618,7 +618,8 @@ export function AskChat({ messages, streamingMessage, isLoading, error, onDismis
618618
{waitingForText && (
619619
// One true status line: what is happening, and how long it has taken once that matters.
620620
<p className="flex items-center gap-2.5 text-sm font-bold text-muted-foreground motion-safe:animate-in fade-in-0 duration-200" role="status" aria-live="polite">
621-
<OwlAvatar pose={searchingWeb ? 'skeptical' : 'idea'} size={32} />
621+
{/* Decorative: a label here would be announced with every status update. */}
622+
<OwlAvatar pose="coach" size={32} />
622623
<span className="inline-flex items-center gap-2 rounded-[20px] rounded-bl-md border-2 border-border bg-card px-3.5 py-2.5">
623624
{searchingWeb ? t('askChat.searchingWeb', 'Searching the web…') : t('askChat.working', 'Working…')}
624625
<ThinkingDots className="text-primary" />

‎src/components/features/debate/UnsortedWork.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function UnsortedWork() {
102102
].sort((a, b) => (b.kind === 'item' ? b.item.createdAt : b.createdAt) - (a.kind === 'item' ? a.item.createdAt : a.createdAt));
103103

104104
if (entries.length === 0) {
105-
return <EmptyState icon={Inbox} title={t('debate.unsortedEmpty', 'Nothing outside your debates')} description={t('debate.unsortedEmptyDesc', 'One-off results and older unsorted work appear here.')} />;
105+
return <EmptyState pose="sleepy" icon={Inbox} title={t('debate.unsortedEmpty', 'Nothing outside your debates')} description={t('debate.unsortedEmptyDesc', 'One-off results and older unsorted work appear here.')} />;
106106
}
107107

108108
return (

‎src/components/features/sparring/SparringRound.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export function SparringRound({
255255
// What you're answering stays in view above the composer, not below it in the transcript.
256256
<article className="rounded-lg border-2 border-border bg-card p-4 sm:p-5" aria-labelledby="sparring-latest-opponent">
257257
<div className="flex items-center gap-2.5">
258-
<OwlAvatar pose="skeptical" size={32} />
258+
<OwlAvatar pose="arguing" size={32} label={t('sparring.opponentAvatarAlt', 'The owl, your AI opponent')} />
259259
<h2 id="sparring-latest-opponent" className="text-sm font-extrabold text-heading">{t('sparring.round.latestOpponent', 'Opponent’s last speech')}</h2>
260260
<span className="truncate text-xs text-muted-foreground">{t(`sparring.turns.${latestOpponentSpeech.turnId}`, latestOpponentSpeech.role)}</span>
261261
</div>
@@ -304,7 +304,7 @@ export function SparringRound({
304304
// The opponent's speech as it is written, in the place it will be read.
305305
<article data-testid="sparring-live-speech" className="rounded-lg border-2 border-brand-tint-border bg-card p-4 motion-safe:animate-in fade-in-0 duration-200 sm:p-5" aria-busy="true" aria-labelledby="sparring-live-opponent">
306306
<div className="flex items-center gap-2.5">
307-
<OwlAvatar pose="point" size={32} />
307+
<OwlAvatar pose="arguing" size={32} label={t('sparring.opponentAvatarAlt', 'The owl, your AI opponent')} />
308308
<h2 id="sparring-live-opponent" className="text-sm font-extrabold text-heading">{t('sparring.round.opponentSpeaking', 'Opponent is speaking')}</h2>
309309
{currentTurn && <span className="truncate text-xs text-muted-foreground">{t(`sparring.turns.${currentTurn.id}`, currentTurn.label)}</span>}
310310
</div>
@@ -324,8 +324,8 @@ export function SparringRound({
324324
<>
325325
{/* One indicator: the owl opponent sizing up the round, bobbing while it works. */}
326326
{aiLoading
327-
? <OwlLoader pose="skeptical" size={104} />
328-
: <Owl pose="skeptical" size={104} />}
327+
? <OwlLoader pose="arguing" size={104} />
328+
: <Owl pose="arguing" size={104} />}
329329
<h2 className="mt-4 font-display text-xl font-[600] text-heading">{aiLoading ? t('sparring.round.opponentThinking', 'Opponent is thinking') : t('sparring.round.opponentUpNext', 'Opponent is up next')}</h2>
330330
<p className="mt-2 max-w-md text-sm leading-6 text-muted-foreground">{aiLoading ? t('sparring.round.opponentThinkingDescription', 'It is reading the motion and your speeches before choosing a response.') : t('sparring.round.opponentUpNextDescription', 'The next response will appear here.')}</p>
331331
</>
@@ -358,7 +358,7 @@ export function SparringRound({
358358
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand text-white">
359359
<UserRound className="h-4 w-4" aria-hidden="true" />
360360
</div>
361-
) : <OwlAvatar pose="skeptical" size={32} />}
361+
) : <OwlAvatar pose="arguing" size={32} label={t('sparring.opponentAvatarAlt', 'The owl, your AI opponent')} />}
362362
<div className="min-w-0">
363363
<p className="truncate text-sm font-extrabold text-heading">{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')}</p>
364364
<p className="truncate text-xs text-muted-foreground">{t(`sparring.turns.${speech.turnId}`, speech.role)}</p>

‎src/components/features/sparring/SparringScorecard.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function SparringScorecard({ session, scorecard, onRestart, onExport, onE
7878
</div>
7979
<div className="mt-8 grid gap-4 sm:grid-cols-2">
8080
<ScoreTotal label={t('sparring.scorecard.yourScore', 'Your score')} score={scorecard.userScore} icon={<UserRound className="h-4 w-4" aria-hidden="true" />} isWinner={scorecard.winner === 'user'} topScoreLabel={t('sparring.scorecard.topScore', 'Top score')} />
81-
<ScoreTotal label={t('sparring.scorecard.opponentScore', 'Opponent score')} score={scorecard.aiScore} icon={<OwlAvatar pose="skeptical" size={22} />} isWinner={scorecard.winner === 'ai'} topScoreLabel={t('sparring.scorecard.topScore', 'Top score')} />
81+
<ScoreTotal label={t('sparring.scorecard.opponentScore', 'Opponent score')} score={scorecard.aiScore} icon={<OwlAvatar pose="arguing" size={22} />} isWinner={scorecard.winner === 'ai'} topScoreLabel={t('sparring.scorecard.topScore', 'Top score')} />
8282
</div>
8383
<div className="mt-7 rounded-lg border-2 border-border bg-surface-sunken/60 p-4 sm:p-5">
8484
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">{t('sparring.scorecard.verdict', "Judge's verdict")}</p>
@@ -179,7 +179,7 @@ export function SparringScorecard({ session, scorecard, onRestart, onExport, onE
179179
{session.speeches.map((speech) => (
180180
<article key={speech.id} className="rounded-xl border border-border/70 p-4">
181181
<div className="flex flex-wrap items-center justify-between gap-2">
182-
<div className="flex items-center gap-2 text-sm font-semibold">{speech.speaker === 'user' ? <span className="flex h-7 w-7 items-center justify-center rounded-full bg-brand text-white" aria-hidden="true"><UserRound className="h-4 w-4" /></span> : <OwlAvatar pose="skeptical" size={28} />}{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')} <span className="font-normal text-muted-foreground">· {t(`sparring.turns.${speech.turnId}`, speech.role)}</span></div>
182+
<div className="flex items-center gap-2 text-sm font-semibold">{speech.speaker === 'user' ? <span className="flex h-7 w-7 items-center justify-center rounded-full bg-brand text-white" aria-hidden="true"><UserRound className="h-4 w-4" /></span> : <OwlAvatar pose="arguing" size={28} label={t('sparring.opponentAvatarAlt', 'The owl, your AI opponent')} />}{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')} <span className="font-normal text-muted-foreground">· {t(`sparring.turns.${speech.turnId}`, speech.role)}</span></div>
183183
<span className="font-mono text-xs tabular-nums text-muted-foreground">{speech.speaker === 'ai' ? t('sparring.round.estimatedTimeUsed', '{{time}} estimated', { time: formatRoundTime(speech.timeUsedSeconds) }) : t('sparring.round.timeUsed', '{{time}} used', { time: formatRoundTime(speech.timeUsedSeconds) })}</span>
184184
</div>
185185
<div className="mt-3 text-sm leading-7"><Markdown>{speech.content}</Markdown></div>

‎src/components/features/sparring/SparringSetup.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export function SparringSetup({ onStart, roundCost = null, remainingRequests = n
234234

235235
<Card className="relative mt-16 overflow-visible border-brand-tint-border">
236236
{/* The opponent, perched on the round card, sizes you up. */}
237-
<Owl pose="skeptical" size={80} className="absolute -top-[68px] right-4" />
237+
<Owl pose="arguing" size={80} className="absolute -top-[68px] right-4" />
238238
<CardContent className="space-y-4 pt-6">
239239
<details className="group rounded-md border-2 border-border bg-surface-sunken/60 p-3">
240240
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-sm font-semibold focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">

‎src/components/features/tools/live-draft.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useTranslation } from 'react-i18next';
22
import { Button, ThinkingDots } from '@/components/ui/button';
3-
import { OwlAvatar } from '@/components/brand/Owl';
3+
import { OwlAvatar, type OwlPose } from '@/components/brand/Owl';
44
import type { LivePhase } from '@/lib/ai/client';
55
import { cn } from '@/lib/utils';
66
import { ToolRunStatus, useElapsedSeconds, type LiveRunState } from './tool-run';
@@ -59,18 +59,21 @@ export function LiveToolRun({
5959
live,
6060
sections,
6161
onCancel,
62+
pose,
6263
}: {
6364
label: string;
6465
live: LiveRunState;
6566
sections: DraftSection[];
6667
onCancel?: () => void;
68+
/** The tool's own owl for the whole run, in place of the phase poses. */
69+
pose?: OwlPose;
6770
}) {
6871
const { t } = useTranslation();
6972
const status = useLivePhaseLabel(live.phase, label);
7073
const elapsed = useElapsedSeconds();
7174
// The owl looks things up while researching or checking sources, and has an idea before it writes.
7275
if (sections.length === 0) {
73-
return <ToolRunStatus label={status} onCancel={onCancel} pose={live.phase === 'research' || live.phase === 'checking' ? 'skeptical' : 'idea'} />;
76+
return <ToolRunStatus label={status} onCancel={onCancel} pose={pose ?? (live.phase === 'research' || live.phase === 'checking' ? 'skeptical' : 'idea')} />;
7477
}
7578

7679
const lastSection = sections.length - 1;
@@ -79,7 +82,7 @@ export function LiveToolRun({
7982
return (
8083
<div data-testid="live-draft" className="space-y-4" aria-busy="true">
8184
<div className="flex items-center gap-3 rounded-lg border-2 border-brand-tint-border bg-brand-tint px-3 py-2.5">
82-
<OwlAvatar pose={writing ? 'point' : 'idea'} size={36} />
85+
<OwlAvatar pose={pose ?? (writing ? 'point' : 'idea')} size={36} />
8386
<p className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 text-sm font-bold text-foreground">
8487
<span role="status" aria-live="polite">{writing ? t('tools.live.writingLive', 'Writing live') : status}</span>
8588
<ThinkingDots className="text-primary" />

‎src/hooks/use-toast.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ export interface ToastOptions {
88
description?: ReactNode;
99
variant?: "default" | "destructive" | "success";
1010
action?: SonnerOptions["action"];
11+
/** Replaces the kind's icon, such as the owl beside a success check. */
12+
icon?: ReactNode;
1113
}
1214

1315
/**
1416
* Adapter over sonner that keeps the `{ title, description, variant }` call
1517
* shape used across the app. Replaces the previous hand-rolled toast reducer
1618
* and its Radix primitives.
1719
*/
18-
export function toast({ title, description, variant, action }: ToastOptions) {
20+
export function toast({ title, description, variant, action, icon }: ToastOptions) {
1921
const render = variant === "destructive"
2022
? sonner.error
2123
: variant === "success"
2224
? sonner.success
2325
: sonner;
24-
return render(title ?? "", { description, action });
26+
return render(title ?? "", { description, action, ...(icon ? { icon, className: "toast-owl" } : {}) });
2527
}
2628

2729
export function useToast() {

‎src/index.css‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,18 @@ passes AA on every surface; buttons fill with --do-primary (#0A6FF0) and white t
11611161
}
11621162
}
11631163

1164+
/* A toast whose icon is the owl beside its check: the slot grows to fit both instead of the
1165+
32 px tinted circle, and the check keeps its own circle. In the utilities layer, because the
1166+
Toaster's `size-8!` is a layered important rule, which an unlayered one cannot override. */
1167+
@layer utilities {
1168+
[data-sonner-toast].toast-owl [data-icon] {
1169+
width: auto !important;
1170+
height: auto !important;
1171+
border-radius: 0 !important;
1172+
background: none !important;
1173+
}
1174+
}
1175+
11641176
@keyframes motion-swap {
11651177
from {
11661178
opacity: 0;

‎src/locales/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@
13381338
},
13391339
"askChat": {
13401340
"title": "Ask Your Coach",
1341+
"coachAvatarAlt": "The owl, your debate coach",
13411342
"subtitle": "Get answers to debate questions",
13421343
"startConversation": "Start a conversation",
13431344
"startDesc": "Ask anything about debate strategy, arguments, or get help with your case.",
@@ -2019,6 +2020,7 @@
20192020
"con": "Con",
20202021
"user": "You",
20212022
"opponent": "AI opponent",
2023+
"opponentAvatarAlt": "The owl, your AI opponent",
20222024
"format": {
20232025
"standard": "School 4-6-6-3",
20242026
"ld": "Lincoln-Douglas",

‎src/locales/es.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@
13381338
},
13391339
"askChat": {
13401340
"title": "Pregunta a tu coach",
1341+
"coachAvatarAlt": "El búho, tu entrenador de debate",
13411342
"subtitle": "Obtén respuestas a preguntas de debate",
13421343
"startConversation": "Inicia una conversación",
13431344
"startDesc": "Pregunta lo que sea sobre estrategia de debate, argumentos o pide ayuda con tu caso.",
@@ -2019,6 +2020,7 @@
20192020
"con": "En contra",
20202021
"user": "Tú",
20212022
"opponent": "Oponente de IA",
2023+
"opponentAvatarAlt": "El búho, tu oponente de IA",
20222024
"format": {
20232025
"standard": "Formato escolar 4-6-6-3",
20242026
"ld": "Lincoln-Douglas",

0 commit comments

Comments
 (0)