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 */}