feat(odin): personalized greeting hero on the new-conversation screen

Time-based Czech greeting + the user's first name (e.g. "Dobrý večer, Admin")
above the big gradient "Odin" wordmark, with a stylish description line. Drop
the redundant mark from the hero (it lives on the nav item + thinking state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-08 20:51:00 +02:00
parent ab12f9d791
commit 1e21c12f33
2 changed files with 56 additions and 21 deletions

View File

@@ -62,10 +62,26 @@ function summaryNote(reviews: ReviewInvoice[]): string {
export default function OdinChat() {
const alert = useAlert();
const qc = useQueryClient();
const { hasPermission } = useAuth();
const { user, hasPermission } = useAuth();
// Saving an extracted invoice hits /received-invoices (needs invoices.create);
// 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 { data: usage } = useQuery(aiUsageOptions());
const { data: convData, isPending: convPending } = useQuery(
aiConversationsOptions(),
@@ -425,6 +441,7 @@ export default function OdinChat() {
turns={turns}
busy={busy}
loading={messagesLoading}
greeting={greeting}
threadRef={threadRef}
/>

View File

@@ -7,6 +7,7 @@ interface OdinThreadProps {
turns: ChatTurn[];
busy: boolean;
loading: boolean;
greeting?: string;
threadRef: React.RefObject<HTMLDivElement | null>;
}
@@ -37,6 +38,7 @@ export default function OdinThread({
turns,
busy,
loading,
greeting,
threadRef,
}: OdinThreadProps) {
return (
@@ -79,34 +81,50 @@ export default function OdinThread({
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 2.5,
gap: 1.25,
textAlign: "center",
px: 2,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<OdinMark size={52} />
<Box
component="span"
sx={(t) => ({
fontSize: "clamp(2rem, 1.2rem + 2.4vw, 2.75rem)",
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: "-0.02em",
backgroundImage: `linear-gradient(120deg, ${t.vars!.palette.primary.light}, ${t.vars!.palette.primary.main})`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
})}
{greeting && (
<Typography
component="div"
sx={{
color: "text.secondary",
fontWeight: 500,
fontSize: "clamp(1rem, 0.85rem + 0.6vw, 1.25rem)",
}}
>
Odin
</Box>
{greeting}
</Typography>
)}
<Box
component="span"
sx={(t) => ({
fontSize: "clamp(2.25rem, 1.4rem + 2.6vw, 3rem)",
fontWeight: 800,
lineHeight: 1.05,
letterSpacing: "-0.03em",
backgroundImage: `linear-gradient(120deg, ${t.vars!.palette.primary.light}, ${t.vars!.palette.primary.main})`,
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
})}
>
Odin
</Box>
<Typography
variant="body2"
sx={{ color: "text.secondary", maxWidth: 440 }}
component="div"
sx={{
color: "text.secondary",
maxWidth: 460,
mt: 0.5,
fontSize: "0.95rem",
lineHeight: 1.5,
}}
>
Zeptejte se, nebo přiložte fakturu (PDF) k automatickému importu.
Váš firemní AI asistent. Zeptejte se, nebo přiložte fakturu (PDF) k
importu.
</Typography>
</Box>
)}