diff --git a/docs/architecture.md b/docs/architecture.md index 61ded4f20..1abc4b228 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -25,7 +25,7 @@ Collaborative Workspace (`/app/workspaces/*`, the `workspace-invites` function, Styles are Tailwind CSS 4, configured in [`src/index.css`](../src/index.css) (`@theme`, `@custom-variant`, `@utility`) and built by `@tailwindcss/vite`; there is no `tailwind.config`. Tailwind 4 needs Safari 16.4, Chrome 111 or Firefox 128. Older browsers lose cascade layers, and translucent colours such as `bg-primary/10` render solid where `color-mix()` is missing. -The visual design (the DebateOwl style) comes from the design tokens in [`src/styles/tokens.css`](../src/styles/tokens.css): plume blue, beak gold, mint for success (always with a check mark), coral, and blue-tinted ink, with dark values on `.dark`. [`src/index.css`](../src/index.css) maps the shadcn roles and the palette families the app names directly onto them, and defines the tactile button (`.btn`, a ledge that presses down) and pressable tiles (`.tile`). Headings and buttons use Fredoka, body text Nunito, both self-hosted through Fontsource with metric-matched fallbacks. The owl's poses are transparent WebP cutouts in [`src/assets/owl/`](../src/assets/owl/), drawn by [`src/components/brand/Owl.tsx`](../src/components/brand/Owl.tsx) (`Owl`, `OwlAvatar`, `OwlMark`), which crossfade between poses and respect reduced motion. The theme defaults to light; Dark and System are a choice in Settings and the header. +The visual design (the DebateOwl style) comes from the design tokens in [`src/styles/tokens.css`](../src/styles/tokens.css): plume blue, beak gold, mint for success (always with a check mark), coral, and blue-tinted ink, with dark values on `.dark`. [`src/index.css`](../src/index.css) maps the shadcn roles and the palette families the app names directly onto them, and defines the tactile button (`.btn`, a ledge that presses down) and pressable tiles (`.tile`). Headings and buttons use Fredoka, body text Nunito, both self-hosted through Fontsource with metric-matched fallbacks. The owl's poses are hand-traced SVGs in [`src/assets/owl/`](../src/assets/owl/), one per screen that shows an owl (see the pose list in `Owl.tsx`), drawn by [`src/components/brand/Owl.tsx`](../src/components/brand/Owl.tsx) (`Owl`, `OwlAvatar`, `OwlMark`), which crossfade between poses and respect reduced motion. The theme defaults to light; Dark and System are a choice in Settings and the header. ## Workspace layout diff --git a/src/assets/owl/owl-arguing.svg b/src/assets/owl/owl-arguing.svg new file mode 100644 index 000000000..946cdcea7 --- /dev/null +++ b/src/assets/owl/owl-arguing.svg @@ -0,0 +1,26 @@ +DebateOwl, arguing + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-celebrating-160.webp b/src/assets/owl/owl-celebrating-160.webp deleted file mode 100644 index a4302d0f9..000000000 Binary files a/src/assets/owl/owl-celebrating-160.webp and /dev/null differ diff --git a/src/assets/owl/owl-celebrating-320.webp b/src/assets/owl/owl-celebrating-320.webp deleted file mode 100644 index 1637b0174..000000000 Binary files a/src/assets/owl/owl-celebrating-320.webp and /dev/null differ diff --git a/src/assets/owl/owl-celebrating-480.webp b/src/assets/owl/owl-celebrating-480.webp deleted file mode 100644 index 37f91b177..000000000 Binary files a/src/assets/owl/owl-celebrating-480.webp and /dev/null differ diff --git a/src/assets/owl/owl-celebrating.svg b/src/assets/owl/owl-celebrating.svg new file mode 100644 index 000000000..b965080d9 --- /dev/null +++ b/src/assets/owl/owl-celebrating.svg @@ -0,0 +1,26 @@ +DebateOwl, celebrating + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-coach.svg b/src/assets/owl/owl-coach.svg new file mode 100644 index 000000000..846253269 --- /dev/null +++ b/src/assets/owl/owl-coach.svg @@ -0,0 +1,31 @@ +DebateOwl, coach + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/owl/owl-idea-160.webp b/src/assets/owl/owl-idea-160.webp deleted file mode 100644 index 140bbab42..000000000 Binary files a/src/assets/owl/owl-idea-160.webp and /dev/null differ diff --git a/src/assets/owl/owl-idea-320.webp b/src/assets/owl/owl-idea-320.webp deleted file mode 100644 index 9f5cb22c5..000000000 Binary files a/src/assets/owl/owl-idea-320.webp and /dev/null differ diff --git a/src/assets/owl/owl-idea-480.webp b/src/assets/owl/owl-idea-480.webp deleted file mode 100644 index 5b3c1f6de..000000000 Binary files a/src/assets/owl/owl-idea-480.webp and /dev/null differ diff --git a/src/assets/owl/owl-idea-bust-192.webp b/src/assets/owl/owl-idea-bust-192.webp deleted file mode 100644 index ae6bb8373..000000000 Binary files a/src/assets/owl/owl-idea-bust-192.webp and /dev/null differ diff --git a/src/assets/owl/owl-idea-bust-96.webp b/src/assets/owl/owl-idea-bust-96.webp deleted file mode 100644 index 7622b6764..000000000 Binary files a/src/assets/owl/owl-idea-bust-96.webp and /dev/null differ diff --git a/src/assets/owl/owl-idea.svg b/src/assets/owl/owl-idea.svg new file mode 100644 index 000000000..590f6d3f5 --- /dev/null +++ b/src/assets/owl/owl-idea.svg @@ -0,0 +1,26 @@ +DebateOwl, idea, pointing up + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-point-160.webp b/src/assets/owl/owl-point-160.webp deleted file mode 100644 index 89c798bc3..000000000 Binary files a/src/assets/owl/owl-point-160.webp and /dev/null differ diff --git a/src/assets/owl/owl-point-320.webp b/src/assets/owl/owl-point-320.webp deleted file mode 100644 index 8cf0cc852..000000000 Binary files a/src/assets/owl/owl-point-320.webp and /dev/null differ diff --git a/src/assets/owl/owl-point-640.webp b/src/assets/owl/owl-point-640.webp deleted file mode 100644 index 680a6a3c8..000000000 Binary files a/src/assets/owl/owl-point-640.webp and /dev/null differ diff --git a/src/assets/owl/owl-point-bust-192.webp b/src/assets/owl/owl-point-bust-192.webp deleted file mode 100644 index 400345098..000000000 Binary files a/src/assets/owl/owl-point-bust-192.webp and /dev/null differ diff --git a/src/assets/owl/owl-point-bust-96.webp b/src/assets/owl/owl-point-bust-96.webp deleted file mode 100644 index ce7e76f96..000000000 Binary files a/src/assets/owl/owl-point-bust-96.webp and /dev/null differ diff --git a/src/assets/owl/owl-point.svg b/src/assets/owl/owl-point.svg new file mode 100644 index 000000000..d41ab76d5 --- /dev/null +++ b/src/assets/owl/owl-point.svg @@ -0,0 +1,26 @@ +DebateOwl, pointing + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-reading.svg b/src/assets/owl/owl-reading.svg new file mode 100644 index 000000000..ada112eaf --- /dev/null +++ b/src/assets/owl/owl-reading.svg @@ -0,0 +1,26 @@ +DebateOwl, reading + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/owl/owl-skeptical-160.webp b/src/assets/owl/owl-skeptical-160.webp deleted file mode 100644 index cda402f42..000000000 Binary files a/src/assets/owl/owl-skeptical-160.webp and /dev/null differ diff --git a/src/assets/owl/owl-skeptical-320.webp b/src/assets/owl/owl-skeptical-320.webp deleted file mode 100644 index 59e481b94..000000000 Binary files a/src/assets/owl/owl-skeptical-320.webp and /dev/null differ diff --git a/src/assets/owl/owl-skeptical-480.webp b/src/assets/owl/owl-skeptical-480.webp deleted file mode 100644 index a5be70ca7..000000000 Binary files a/src/assets/owl/owl-skeptical-480.webp and /dev/null differ diff --git a/src/assets/owl/owl-skeptical-bust-192.webp b/src/assets/owl/owl-skeptical-bust-192.webp deleted file mode 100644 index 8c41b391f..000000000 Binary files a/src/assets/owl/owl-skeptical-bust-192.webp and /dev/null differ diff --git a/src/assets/owl/owl-skeptical-bust-96.webp b/src/assets/owl/owl-skeptical-bust-96.webp deleted file mode 100644 index 9c1c28834..000000000 Binary files a/src/assets/owl/owl-skeptical-bust-96.webp and /dev/null differ diff --git a/src/assets/owl/owl-skeptical.svg b/src/assets/owl/owl-skeptical.svg new file mode 100644 index 000000000..62f8d3d85 --- /dev/null +++ b/src/assets/owl/owl-skeptical.svg @@ -0,0 +1,26 @@ +DebateOwl, skeptical, arms crossed + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-sleepy.svg b/src/assets/owl/owl-sleepy.svg new file mode 100644 index 000000000..784417c4b --- /dev/null +++ b/src/assets/owl/owl-sleepy.svg @@ -0,0 +1,26 @@ +DebateOwl, sleepy + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/owl/owl-surprised-160.webp b/src/assets/owl/owl-surprised-160.webp deleted file mode 100644 index 1d5347443..000000000 Binary files a/src/assets/owl/owl-surprised-160.webp and /dev/null differ diff --git a/src/assets/owl/owl-surprised-320.webp b/src/assets/owl/owl-surprised-320.webp deleted file mode 100644 index cf6685d1d..000000000 Binary files a/src/assets/owl/owl-surprised-320.webp and /dev/null differ diff --git a/src/assets/owl/owl-surprised-480.webp b/src/assets/owl/owl-surprised-480.webp deleted file mode 100644 index 16eb72e1a..000000000 Binary files a/src/assets/owl/owl-surprised-480.webp and /dev/null differ diff --git a/src/assets/owl/owl-surprised.svg b/src/assets/owl/owl-surprised.svg new file mode 100644 index 000000000..d6dc9c1dc --- /dev/null +++ b/src/assets/owl/owl-surprised.svg @@ -0,0 +1,26 @@ +DebateOwl, surprised, pointing at you + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/owl/owl-thinking.svg b/src/assets/owl/owl-thinking.svg new file mode 100644 index 000000000..ada1ea62d --- /dev/null +++ b/src/assets/owl/owl-thinking.svg @@ -0,0 +1,26 @@ +DebateOwl, thinking + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/owl/owl-timekeeper.svg b/src/assets/owl/owl-timekeeper.svg new file mode 100644 index 000000000..d2b58af21 --- /dev/null +++ b/src/assets/owl/owl-timekeeper.svg @@ -0,0 +1,26 @@ +DebateOwl, timekeeper + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/brand/Owl.tsx b/src/components/brand/Owl.tsx index eedeb4aaa..a8e6ee211 100644 --- a/src/components/brand/Owl.tsx +++ b/src/components/brand/Owl.tsx @@ -1,56 +1,26 @@ import { AnimatePresence, motion } from 'framer-motion'; import { cn } from '@/lib/utils'; -import point160 from '@/assets/owl/owl-point-160.webp'; -import point320 from '@/assets/owl/owl-point-320.webp'; -import point640 from '@/assets/owl/owl-point-640.webp'; -import idea160 from '@/assets/owl/owl-idea-160.webp'; -import idea320 from '@/assets/owl/owl-idea-320.webp'; -import idea480 from '@/assets/owl/owl-idea-480.webp'; -import skeptical160 from '@/assets/owl/owl-skeptical-160.webp'; -import skeptical320 from '@/assets/owl/owl-skeptical-320.webp'; -import skeptical480 from '@/assets/owl/owl-skeptical-480.webp'; -import surprised160 from '@/assets/owl/owl-surprised-160.webp'; -import surprised320 from '@/assets/owl/owl-surprised-320.webp'; -import surprised480 from '@/assets/owl/owl-surprised-480.webp'; -import celebrating160 from '@/assets/owl/owl-celebrating-160.webp'; -import celebrating320 from '@/assets/owl/owl-celebrating-320.webp'; -import celebrating480 from '@/assets/owl/owl-celebrating-480.webp'; -import pointBust96 from '@/assets/owl/owl-point-bust-96.webp'; -import pointBust192 from '@/assets/owl/owl-point-bust-192.webp'; -import ideaBust96 from '@/assets/owl/owl-idea-bust-96.webp'; -import ideaBust192 from '@/assets/owl/owl-idea-bust-192.webp'; -import skepticalBust96 from '@/assets/owl/owl-skeptical-bust-96.webp'; -import skepticalBust192 from '@/assets/owl/owl-skeptical-bust-192.webp'; +import point from '@/assets/owl/owl-point.svg'; +import idea from '@/assets/owl/owl-idea.svg'; +import skeptical from '@/assets/owl/owl-skeptical.svg'; +import surprised from '@/assets/owl/owl-surprised.svg'; +import celebrating from '@/assets/owl/owl-celebrating.svg'; +import thinking from '@/assets/owl/owl-thinking.svg'; +import reading from '@/assets/owl/owl-reading.svg'; +import timekeeper from '@/assets/owl/owl-timekeeper.svg'; +import arguing from '@/assets/owl/owl-arguing.svg'; +import coach from '@/assets/owl/owl-coach.svg'; +import sleepy from '@/assets/owl/owl-sleepy.svg'; /** - * The owl's poses, cut from the mascot art as transparent WebP at 1x to 3x: - * point (eager, pointing at the action), idea (finger up, making a point), - * skeptical (arms crossed, sizing up an argument), surprised, celebrating. + * The owl's poses, hand-traced SVGs on a 512 px square: point (eager, pointing at the action), + * idea (finger up), skeptical (arms crossed), surprised, celebrating, thinking (wing at the beak), + * reading (open book), timekeeper (stopwatch), arguing (wings out, the Sparring opponent), + * coach (thumbs up, the Ask coach) and sleepy (nothing here yet). */ -export type OwlPose = 'point' | 'idea' | 'skeptical' | 'surprised' | 'celebrating'; -export type OwlBustPose = 'point' | 'idea' | 'skeptical'; +export type OwlPose = 'point' | 'idea' | 'skeptical' | 'surprised' | 'celebrating' | 'thinking' | 'reading' | 'timekeeper' | 'arguing' | 'coach' | 'sleepy'; -const FULL: Record = { - point: `${point160} 160w, ${point320} 320w, ${point640} 640w`, - idea: `${idea160} 160w, ${idea320} 320w, ${idea480} 480w`, - skeptical: `${skeptical160} 160w, ${skeptical320} 320w, ${skeptical480} 480w`, - surprised: `${surprised160} 160w, ${surprised320} 320w, ${surprised480} 480w`, - celebrating: `${celebrating160} 160w, ${celebrating320} 320w, ${celebrating480} 480w`, -}; - -const FALLBACK: Record = { - point: point320, - idea: idea320, - skeptical: skeptical320, - surprised: surprised320, - celebrating: celebrating320, -}; - -const BUST: Record = { - point: [pointBust96, pointBust192], - idea: [ideaBust96, ideaBust192], - skeptical: [skepticalBust96, skepticalBust192], -}; +const POSES: Record = { point, idea, skeptical, surprised, celebrating, thinking, reading, timekeeper, arguing, coach, sleepy }; // A pose change crossfades with a small pop on the design system's `pop` spring. With reduced // motion, App's MotionConfig drops the scale and travel and only the fade remains. @@ -80,9 +50,7 @@ export function Owl({ pose, size, className, label, priority = false }: OwlProps {/* The coach is the owl. */} - {!isUser && } + {!isUser && }
- +
{t('askChat.title')} {t('askChat.subtitle')} @@ -618,7 +618,8 @@ export function AskChat({ messages, streamingMessage, isLoading, error, onDismis {waitingForText && ( // One true status line: what is happening, and how long it has taken once that matters.

- + {/* Decorative: a label here would be announced with every status update. */} + {searchingWeb ? t('askChat.searchingWeb', 'Searching the web…') : t('askChat.working', 'Working…')} diff --git a/src/components/features/debate/UnsortedWork.tsx b/src/components/features/debate/UnsortedWork.tsx index eb597f952..517be7bac 100644 --- a/src/components/features/debate/UnsortedWork.tsx +++ b/src/components/features/debate/UnsortedWork.tsx @@ -102,7 +102,7 @@ export function UnsortedWork() { ].sort((a, b) => (b.kind === 'item' ? b.item.createdAt : b.createdAt) - (a.kind === 'item' ? a.item.createdAt : a.createdAt)); if (entries.length === 0) { - return ; + return ; } return ( diff --git a/src/components/features/sparring/SparringRound.tsx b/src/components/features/sparring/SparringRound.tsx index 7803dbc1d..685ead930 100644 --- a/src/components/features/sparring/SparringRound.tsx +++ b/src/components/features/sparring/SparringRound.tsx @@ -255,7 +255,7 @@ export function SparringRound({ // What you're answering stays in view above the composer, not below it in the transcript.

- +

{t('sparring.round.latestOpponent', 'Opponent’s last speech')}

{t(`sparring.turns.${latestOpponentSpeech.turnId}`, latestOpponentSpeech.role)}
@@ -304,7 +304,7 @@ export function SparringRound({ // The opponent's speech as it is written, in the place it will be read.
- +

{t('sparring.round.opponentSpeaking', 'Opponent is speaking')}

{currentTurn && {t(`sparring.turns.${currentTurn.id}`, currentTurn.label)}}
@@ -324,8 +324,8 @@ export function SparringRound({ <> {/* One indicator: the owl opponent sizing up the round, bobbing while it works. */} {aiLoading - ? - : } + ? + : }

{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) => (
-
{speech.speaker === 'user' ? : }{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')} · {t(`sparring.turns.${speech.turnId}`, speech.role)}
+
{speech.speaker === 'user' ? : }{speech.speaker === 'user' ? t('sparring.user', 'You') : t('sparring.opponent', 'AI opponent')} · {t(`sparring.turns.${speech.turnId}`, speech.role)}
{speech.speaker === 'ai' ? t('sparring.round.estimatedTimeUsed', '{{time}} estimated', { time: formatRoundTime(speech.timeUsedSeconds) }) : t('sparring.round.timeUsed', '{{time}} used', { time: formatRoundTime(speech.timeUsedSeconds) })}
{speech.content}
diff --git a/src/components/features/sparring/SparringSetup.tsx b/src/components/features/sparring/SparringSetup.tsx index 0cc82bd78..adc3a4b0f 100644 --- a/src/components/features/sparring/SparringSetup.tsx +++ b/src/components/features/sparring/SparringSetup.tsx @@ -234,7 +234,7 @@ export function SparringSetup({ onStart, roundCost = null, remainingRequests = n {/* The opponent, perched on the round card, sizes you up. */} - +
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')}} + />