diff --git a/src/admin/components/odin/OdinChat.tsx b/src/admin/components/odin/OdinChat.tsx index c890d88..8950490 100644 --- a/src/admin/components/odin/OdinChat.tsx +++ b/src/admin/components/odin/OdinChat.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; @@ -109,6 +110,17 @@ export default function OdinChat() { }); const [sidebarOpen, setSidebarOpen] = useState(false); + // Mobile is immersive (no AppShell header on /odin) — this is the only way + // back. A deep link straight to /odin has no in-app history → go home. + const navigate = useNavigate(); + const goBack = () => { + if (((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0) { + navigate(-1); + } else { + navigate("/"); + } + }; + // No auto-select: like claude.ai, we land on a fresh "new chat" (activeId // null) and the sidebar lists existing conversations to open. A conversation // row in the DB is created lazily on the first message (see submit), so @@ -424,11 +436,16 @@ export default function OdinChat() { // browser-chrome height. svh sizes for the bar-visible viewport — the // page never scrolls and the chat's message list scrolls internally. // Desktop: svh === vh. - height: "calc(100svh - 100px)", + // Mobile is immersive (AppShell hides its header and main padding on + // /odin): the chat owns the whole viewport, full-bleed. + height: { xs: "100svh", md: "calc(100svh - 100px)" }, display: "flex", - border: 1, + // Longhand on purpose: a responsive `border` shorthand lands in a + // media query AFTER borderColor and resets the color to black. + borderStyle: "solid", + borderWidth: { xs: 0, md: 1 }, borderColor: "divider", - borderRadius: 3, + borderRadius: { xs: 0, md: 3 }, bgcolor: "background.paper", overflow: "hidden", }} @@ -454,15 +471,41 @@ export default function OdinChat() { flexDirection: "column", gap: 1.5, p: 2, + // Immersive mobile: respect the notch / home-indicator insets + // (env() is 0 outside standalone mode, so max() keeps the 16px). + pt: { xs: "max(16px, env(safe-area-inset-top))", md: 2 }, + pb: { xs: "max(16px, env(safe-area-inset-bottom))", md: 2 }, }} > + {isMobile && ( + + + + + + + )} {isMobile && ( setSidebarOpen(true)} aria-label="Konverzace" size="small" - sx={{ ml: -0.5, flexShrink: 0 }} + sx={{ flexShrink: 0 }} > {!isUser && } @@ -218,25 +231,79 @@ export default function OdinThread({ ); })} - {/* Busy indicator — same 28px mark and zero row padding as the bubble - rows, so the thinking mark sits exactly in the avatar column. */} - {busy && ( - - - - Pracuji… - - - )} + {/* Busy indicator — a typing bubble (three bouncing dots) anchored in + the avatar column, springing in/out where the reply will land. */} + + {busy && ( + + + + {[0, 1, 2].map((i) => ( + + ))} + + + )} + ); } diff --git a/src/admin/ui/AppShell.tsx b/src/admin/ui/AppShell.tsx index 7f3d0cf..6ba87c6 100644 --- a/src/admin/ui/AppShell.tsx +++ b/src/admin/ui/AppShell.tsx @@ -23,6 +23,11 @@ export default function AppShell() { undefined, ); + // Chat-style pages own the full mobile viewport: under md the shell header + // disappears and main loses its padding — the page brings its own back + // button. Desktop keeps the normal shell. + const immersiveOnMobile = location.pathname === "/odin"; + const handleLogout = useCallback(() => { setLoggingOut(true); setMobileOpen(false); @@ -130,7 +135,9 @@ export default function AppShell() { - +