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() {
-
+