diff --git a/index.html b/index.html index 6ecf1b1..53ad3dc 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ diff --git a/src/admin/components/odin/OdinChat.tsx b/src/admin/components/odin/OdinChat.tsx index 82ed623..e37ee18 100644 --- a/src/admin/components/odin/OdinChat.tsx +++ b/src/admin/components/odin/OdinChat.tsx @@ -67,21 +67,18 @@ export default function OdinChat() { // Odin itself only needs ai.use, so the Save affordance is gated separately. const canSave = hasPermission("invoices.create"); // Time-based Czech greeting + the user's first name (client-local time). - const greeting = (() => { - const h = new Date().getHours(); - const part = - h < 9 - ? "Dobré ráno" - : h < 12 - ? "Dobrý den" - : h < 18 - ? "Dobré odpoledne" - : "Dobrý večer"; - const name = (user?.fullName || user?.username || "") - .trim() - .split(/\s+/)[0]; - return name ? `${part}, ${name}` : part; - })(); + const greetingHour = new Date().getHours(); + const greetingPhrase = + greetingHour < 9 + ? "Dobré ráno" + : greetingHour < 12 + ? "Dobrý den" + : greetingHour < 18 + ? "Dobré odpoledne" + : "Dobrý večer"; + const userName = (user?.fullName || user?.username || "") + .trim() + .split(/\s+/)[0]; const { data: usage } = useQuery(aiUsageOptions()); const { data: convData, isPending: convPending } = useQuery( aiConversationsOptions(), @@ -441,7 +438,8 @@ export default function OdinChat() { turns={turns} busy={busy} loading={messagesLoading} - greeting={greeting} + greetingPhrase={greetingPhrase} + userName={userName} threadRef={threadRef} /> diff --git a/src/admin/components/odin/OdinThread.tsx b/src/admin/components/odin/OdinThread.tsx index ef821e5..9a85646 100644 --- a/src/admin/components/odin/OdinThread.tsx +++ b/src/admin/components/odin/OdinThread.tsx @@ -1,13 +1,31 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import { motion, useReducedMotion, type Variants } from "framer-motion"; import type { ChatTurn } from "./types"; import OdinMark from "./OdinMark"; +const MotionBox = motion.create(Box); + +// Staggered hero entrance (skipped under prefers-reduced-motion). +const heroContainer: Variants = { + hidden: {}, + show: { transition: { staggerChildren: 0.12, delayChildren: 0.06 } }, +}; +const heroItem: Variants = { + hidden: { opacity: 0, y: 14 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.55, ease: [0.16, 1, 0.3, 1] }, + }, +}; + interface OdinThreadProps { turns: ChatTurn[]; busy: boolean; loading: boolean; - greeting?: string; + greetingPhrase?: string; + userName?: string; threadRef: React.RefObject; } @@ -38,9 +56,15 @@ export default function OdinThread({ turns, busy, loading, - greeting, + greetingPhrase, + userName, threadRef, }: OdinThreadProps) { + const reduce = useReducedMotion(); + const containerMotion = reduce + ? {} + : { variants: heroContainer, initial: "hidden", animate: "show" }; + const itemMotion = reduce ? {} : { variants: heroItem }; return ( - - {greeting || "Dobrý den"} - - - Váš firemní AI asistent — zeptejte se na cokoli, nebo přiložte - fakturu (PDF) k importu. - - + + + {greetingPhrase || "Dobrý den"} + {userName && ( + <> + ,{" "} + ({ + fontStyle: "italic", + backgroundImage: `linear-gradient(120deg, ${t.vars!.palette.primary.light}, ${t.vars!.palette.primary.main})`, + WebkitBackgroundClip: "text", + backgroundClip: "text", + WebkitTextFillColor: "transparent", + })} + > + {userName} + + + )} + + + + + Zeptejte se na cokoli, nebo přiložte fakturu — Odin ji přečte a + připraví k uložení. + + + )} {/* Message bubbles */}