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:
@@ -62,10 +62,26 @@ function summaryNote(reviews: ReviewInvoice[]): string {
|
|||||||
export default function OdinChat() {
|
export default function OdinChat() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { hasPermission } = useAuth();
|
const { user, hasPermission } = useAuth();
|
||||||
// Saving an extracted invoice hits /received-invoices (needs invoices.create);
|
// Saving an extracted invoice hits /received-invoices (needs invoices.create);
|
||||||
// Odin itself only needs ai.use, so the Save affordance is gated separately.
|
// Odin itself only needs ai.use, so the Save affordance is gated separately.
|
||||||
const canSave = hasPermission("invoices.create");
|
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: usage } = useQuery(aiUsageOptions());
|
||||||
const { data: convData, isPending: convPending } = useQuery(
|
const { data: convData, isPending: convPending } = useQuery(
|
||||||
aiConversationsOptions(),
|
aiConversationsOptions(),
|
||||||
@@ -425,6 +441,7 @@ export default function OdinChat() {
|
|||||||
turns={turns}
|
turns={turns}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
loading={messagesLoading}
|
loading={messagesLoading}
|
||||||
|
greeting={greeting}
|
||||||
threadRef={threadRef}
|
threadRef={threadRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface OdinThreadProps {
|
|||||||
turns: ChatTurn[];
|
turns: ChatTurn[];
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
greeting?: string;
|
||||||
threadRef: React.RefObject<HTMLDivElement | null>;
|
threadRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export default function OdinThread({
|
|||||||
turns,
|
turns,
|
||||||
busy,
|
busy,
|
||||||
loading,
|
loading,
|
||||||
|
greeting,
|
||||||
threadRef,
|
threadRef,
|
||||||
}: OdinThreadProps) {
|
}: OdinThreadProps) {
|
||||||
return (
|
return (
|
||||||
@@ -79,34 +81,50 @@ export default function OdinThread({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 2.5,
|
gap: 1.25,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
px: 2,
|
px: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
{greeting && (
|
||||||
<OdinMark size={52} />
|
<Typography
|
||||||
<Box
|
component="div"
|
||||||
component="span"
|
sx={{
|
||||||
sx={(t) => ({
|
color: "text.secondary",
|
||||||
fontSize: "clamp(2rem, 1.2rem + 2.4vw, 2.75rem)",
|
fontWeight: 500,
|
||||||
fontWeight: 700,
|
fontSize: "clamp(1rem, 0.85rem + 0.6vw, 1.25rem)",
|
||||||
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",
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
Odin
|
{greeting}
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
component="div"
|
||||||
sx={{ color: "text.secondary", maxWidth: 440 }}
|
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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user