{t('sparring.round.latestOpponent', 'Opponent’s last speech')}
{t(`sparring.turns.${latestOpponentSpeech.turnId}`, latestOpponentSpeech.role)}{t('sparring.round.opponentSpeaking', 'Opponent is speaking')}
{currentTurn && {t(`sparring.turns.${currentTurn.id}`, currentTurn.label)}}{aiLoading ? t('sparring.round.opponentThinking', 'Opponent is thinking') : t('sparring.round.opponentUpNext', 'Opponent is up next')}
{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.')}
> @@ -358,7 +358,7 @@ export function SparringRound({{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')}
{t(`sparring.turns.${speech.turnId}`, speech.role)}
diff --git a/src/components/features/sparring/SparringScorecard.tsx b/src/components/features/sparring/SparringScorecard.tsx index 635bcbc19..8cfd9caa2 100644 --- a/src/components/features/sparring/SparringScorecard.tsx +++ b/src/components/features/sparring/SparringScorecard.tsx @@ -78,7 +78,7 @@ export function SparringScorecard({ session, scorecard, onRestart, onExport, onE{t('sparring.scorecard.verdict', "Judge's verdict")}
@@ -179,7 +179,7 @@ export function SparringScorecard({ session, scorecard, onRestart, onExport, onE {session.speeches.map((speech) => (
diff --git a/src/components/features/tools/live-draft.tsx b/src/components/features/tools/live-draft.tsx
index 569e38ce0..3652fa9e4 100644
--- a/src/components/features/tools/live-draft.tsx
+++ b/src/components/features/tools/live-draft.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Button, ThinkingDots } from '@/components/ui/button';
-import { OwlAvatar } from '@/components/brand/Owl';
+import { OwlAvatar, type OwlPose } from '@/components/brand/Owl';
import type { LivePhase } from '@/lib/ai/client';
import { cn } from '@/lib/utils';
import { ToolRunStatus, useElapsedSeconds, type LiveRunState } from './tool-run';
@@ -59,18 +59,21 @@ export function LiveToolRun({
live,
sections,
onCancel,
+ pose,
}: {
label: string;
live: LiveRunState;
sections: DraftSection[];
onCancel?: () => void;
+ /** The tool's own owl for the whole run, in place of the phase poses. */
+ pose?: OwlPose;
}) {
const { t } = useTranslation();
const status = useLivePhaseLabel(live.phase, label);
const elapsed = useElapsedSeconds();
// The owl looks things up while researching or checking sources, and has an idea before it writes.
if (sections.length === 0) {
- return ;
+ return ;
}
const lastSection = sections.length - 1;
@@ -79,7 +82,7 @@ export function LiveToolRun({
return (
-
+
{writing ? t('tools.live.writingLive', 'Writing live') : status}
diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts
index ecfa19702..b581db4f1 100644
--- a/src/hooks/use-toast.ts
+++ b/src/hooks/use-toast.ts
@@ -8,6 +8,8 @@ export interface ToastOptions {
description?: ReactNode;
variant?: "default" | "destructive" | "success";
action?: SonnerOptions["action"];
+ /** Replaces the kind's icon, such as the owl beside a success check. */
+ icon?: ReactNode;
}
/**
@@ -15,13 +17,13 @@ export interface ToastOptions {
* shape used across the app. Replaces the previous hand-rolled toast reducer
* and its Radix primitives.
*/
-export function toast({ title, description, variant, action }: ToastOptions) {
+export function toast({ title, description, variant, action, icon }: ToastOptions) {
const render = variant === "destructive"
? sonner.error
: variant === "success"
? sonner.success
: sonner;
- return render(title ?? "", { description, action });
+ return render(title ?? "", { description, action, ...(icon ? { icon, className: "toast-owl" } : {}) });
}
export function useToast() {
diff --git a/src/index.css b/src/index.css
index 786ccb91a..f12d209aa 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1161,6 +1161,18 @@ passes AA on every surface; buttons fill with --do-primary (#0A6FF0) and white t
}
}
+/* A toast whose icon is the owl beside its check: the slot grows to fit both instead of the
+ 32 px tinted circle, and the check keeps its own circle. In the utilities layer, because the
+ Toaster's `size-8!` is a layered important rule, which an unlayered one cannot override. */
+@layer utilities {
+ [data-sonner-toast].toast-owl [data-icon] {
+ width: auto !important;
+ height: auto !important;
+ border-radius: 0 !important;
+ background: none !important;
+ }
+}
+
@keyframes motion-swap {
from {
opacity: 0;
diff --git a/src/locales/en.json b/src/locales/en.json
index 2ce3be982..bd263ca05 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1338,6 +1338,7 @@
},
"askChat": {
"title": "Ask Your Coach",
+ "coachAvatarAlt": "The owl, your debate coach",
"subtitle": "Get answers to debate questions",
"startConversation": "Start a conversation",
"startDesc": "Ask anything about debate strategy, arguments, or get help with your case.",
@@ -2019,6 +2020,7 @@
"con": "Con",
"user": "You",
"opponent": "AI opponent",
+ "opponentAvatarAlt": "The owl, your AI opponent",
"format": {
"standard": "School 4-6-6-3",
"ld": "Lincoln-Douglas",
diff --git a/src/locales/es.json b/src/locales/es.json
index 7a8addb11..879bfb95e 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -1338,6 +1338,7 @@
},
"askChat": {
"title": "Pregunta a tu coach",
+ "coachAvatarAlt": "El búho, tu coach de debate",
"subtitle": "Obtén respuestas a preguntas de debate",
"startConversation": "Inicia una conversación",
"startDesc": "Pregunta lo que sea sobre estrategia de debate, argumentos o pide ayuda con tu caso.",
@@ -2019,6 +2020,7 @@
"con": "En contra",
"user": "Tú",
"opponent": "Oponente de IA",
+ "opponentAvatarAlt": "El búho, tu oponente de IA",
"format": {
"standard": "Formato escolar 4-6-6-3",
"ld": "Lincoln-Douglas",
diff --git a/src/locales/hu.json b/src/locales/hu.json
index e99eb07ac..08953a64a 100644
--- a/src/locales/hu.json
+++ b/src/locales/hu.json
@@ -1338,6 +1338,7 @@
},
"askChat": {
"title": "Kérdezd az edződet",
+ "coachAvatarAlt": "A bagoly, a vitaedződ",
"subtitle": "Kapj válaszokat vitakérdésekre",
"startConversation": "Indíts beszélgetést",
"startDesc": "Kérdezz bármit vitastratégiáról, érvekről, vagy kérj segítséget az esetedhez.",
@@ -2019,6 +2020,7 @@
"con": "Ellene",
"user": "Te",
"opponent": "MI-ellenfél",
+ "opponentAvatarAlt": "A bagoly, az MI-ellenfeled",
"format": {
"standard": "Iskolai 4-6-6-3",
"ld": "Lincoln-Douglas",
diff --git a/src/pages/GeneratePage.tsx b/src/pages/GeneratePage.tsx
index ca4e0a9a8..44cd7f60d 100644
--- a/src/pages/GeneratePage.tsx
+++ b/src/pages/GeneratePage.tsx
@@ -19,7 +19,8 @@ import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
-import { Save, Target } from 'lucide-react';
+import { Check, Save, Target } from 'lucide-react';
+import { Owl } from '@/components/brand/Owl';
import { EmptyState } from '@/components/ui/empty-state';
import { PageHeader } from '@/components/layout/PageHeader';
import { ToolRunErrorPanel, isCanceled, useRevealResults, useToolRun } from '@/components/features/tools/tool-run';
@@ -224,7 +225,18 @@ export default function GeneratePage() {
await saveCurrentCase(title);
markResultSaved(saving);
setSaveDialogOpen(false);
- toast({ title: t('generate.saved'), description: t('generate.savedDesc') });
+ toast({
+ title: t('generate.saved'),
+ description: t('generate.savedDesc'),
+ variant: 'success',
+ // The owl celebrates beside the success check, so the meaning never rests on the owl or the colour.
+ icon: (
+
+
+
+
+ ),
+ });
} catch (error) {
console.error('Failed to save case:', error);
toast({
@@ -295,7 +307,7 @@ export default function GeneratePage() {
/>
)}
{generateLoading ? (
-
+
) : generateResult ? (
) : (
-
+
)}
diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx
index 6b2d92d98..b72f3e11b 100644
--- a/src/pages/HistoryPage.tsx
+++ b/src/pages/HistoryPage.tsx
@@ -196,7 +196,7 @@ export default function HistoryPage() {
{savedCases.length === 0 ? (
- navigate('/app/generate')} />
+ navigate('/app/generate')} />
) : (
@@ -233,7 +233,7 @@ export default function HistoryPage() {
{savedChats.length === 0 ? (
- navigate('/app/ask')} />
+ navigate('/app/ask')} />
) : (
{savedChats.map((item) => (
@@ -260,7 +260,7 @@ export default function HistoryPage() {
{savedAttacks.length === 0 ? (
- navigate('/app/attack')} />
+ navigate('/app/attack')} />
) : (
@@ -290,7 +290,7 @@ export default function HistoryPage() {
{savedBrainstorms.length === 0 ? (
- navigate('/app/brainstorm')} />
+ navigate('/app/brainstorm')} />
) : (
@@ -320,7 +320,7 @@ export default function HistoryPage() {
{savedFactoids.length === 0 ? (
- navigate('/app/factoids')} />
+ navigate('/app/factoids')} />
) : (
@@ -351,6 +351,7 @@ export default function HistoryPage() {
{sparringSessions.length === 0 ? (
{savedCrossfires.length === 0 ? (
-
- {t('debate.noCurrentTitle', 'No current debate')}
- {t('debate.noCurrentDesc', 'Start a debate to keep one motion, its sources, and your results together.')}
+
+ {/* Mirrored, so the owl points across its line at Start debate. */}
+
+
+ {t('debate.noCurrentTitle', 'No current debate')}
+ {t('debate.noCurrentDesc', 'Start a debate to keep one motion, its sources, and your results together.')}
+
diff --git a/src/pages/LibraryPage.tsx b/src/pages/LibraryPage.tsx
index 199684fdc..c4e246ff7 100644
--- a/src/pages/LibraryPage.tsx
+++ b/src/pages/LibraryPage.tsx
@@ -1067,7 +1067,7 @@ export default function LibraryPage() {
{firstLoadPending ? (
) : sourceLibrary.length === 0 ? (
-
+
) : visibleSources.length === 0 ? (
) : (
diff --git a/src/pages/TimerPage.tsx b/src/pages/TimerPage.tsx
index 1c3a63d47..d2f8b32e4 100644
--- a/src/pages/TimerPage.tsx
+++ b/src/pages/TimerPage.tsx
@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
import { Play, Pause, RotateCcw, Bell, Volume2, VolumeX, Keyboard } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import { PageHeader } from '@/components/layout/PageHeader';
+import { Owl } from '@/components/brand/Owl';
import { ClockAnnouncer, RoundClock, clockTone } from '@/components/features/practice/RoundClock';
import { displaySeconds, pauseClock, remainingMs, startClock, type ClockState } from '@/lib/timer-clock';
import { timerShortcutBlocked } from '@/lib/timer-shortcuts';
@@ -236,7 +237,11 @@ export default function TimerPage() {
return (
-
+ {t('timer.title')}}
+ />
{writing ? t('tools.live.writingLive', 'Writing live') : status}
{t('debate.noCurrentTitle', 'No current debate')}
-{t('debate.noCurrentDesc', 'Start a debate to keep one motion, its sources, and your results together.')}
+{t('debate.noCurrentTitle', 'No current debate')}
+{t('debate.noCurrentDesc', 'Start a debate to keep one motion, its sources, and your results together.')}
+