diff --git a/src/admin/components/odin/OdinChat.tsx b/src/admin/components/odin/OdinChat.tsx index e37ee18..cdf85f1 100644 --- a/src/admin/components/odin/OdinChat.tsx +++ b/src/admin/components/odin/OdinChat.tsx @@ -2,6 +2,9 @@ import { useState, useRef, useEffect } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import Drawer from "@mui/material/Drawer"; +import IconButton from "@mui/material/IconButton"; +import useMediaQuery from "@mui/material/useMediaQuery"; import apiFetch from "../../utils/api"; import { useAlert } from "../../context/AlertContext"; import { useAuth } from "../../context/AuthContext"; @@ -100,6 +103,12 @@ export default function OdinChat() { const threadRef = useRef(null); const seededId = useRef(null); + // Below md the conversation list collapses into a slide-in drawer. + const isMobile = useMediaQuery((t) => t.breakpoints.down("md"), { + noSsr: true, + }); + const [sidebarOpen, setSidebarOpen] = useState(false); + // 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 @@ -161,6 +170,7 @@ export default function OdinChat() { setAttachments((a) => a.filter((s) => s.id !== id)); const onSelect = (id: number) => { + setSidebarOpen(false); if (id === activeId || busy) return; setActiveId(id); setTurns([]); @@ -329,6 +339,7 @@ export default function OdinChat() { // "Nová konverzace" just opens a fresh, empty composer — no DB row is created // until the first message, so repeated clicks never spawn empty conversations. const onNew = () => { + setSidebarOpen(false); if (busy) return; setActiveId(null); setTurns([]); @@ -374,6 +385,18 @@ export default function OdinChat() { const activeTitle = conversations.find((c) => c.id === activeId)?.title ?? "Nová konverzace"; + const sidebar = ( + + ); + return ( - + {isMobile ? ( + setSidebarOpen(false)} + ModalProps={{ keepMounted: true }} + > + {sidebar} + + ) : ( + sidebar + )} {/* Chat column */} - + + {isMobile && ( + setSidebarOpen(true)} + aria-label="Konverzace" + size="small" + sx={{ ml: -0.5, flexShrink: 0 }} + > + + + + + + + )} {activeTitle} @@ -426,7 +466,7 @@ export default function OdinChat() { Utraceno: ${usage.month_spend_usd.toFixed(2)} / $ {usage.budget_usd.toFixed(2)} diff --git a/src/admin/components/odin/OdinComposer.tsx b/src/admin/components/odin/OdinComposer.tsx index 317760c..f90c280 100644 --- a/src/admin/components/odin/OdinComposer.tsx +++ b/src/admin/components/odin/OdinComposer.tsx @@ -1,6 +1,7 @@ import Box from "@mui/material/Box"; import Chip from "@mui/material/Chip"; -import { Button, TextField } from "../../ui"; +import IconButton from "@mui/material/IconButton"; +import TextField from "@mui/material/TextField"; import type { StagedFile } from "./types"; interface OdinComposerProps { @@ -43,26 +44,57 @@ export default function OdinComposer({ )} - {/* Composer row */} - - } - type="file" - accept="application/pdf,image/*" - multiple - hidden - onChange={(e) => onFiles(e.target.files)} - /> - + + + + + onInput(e.target.value)} @@ -72,10 +104,43 @@ export default function OdinComposer({ onSubmit(); } }} + fullWidth + slotProps={{ input: { disableUnderline: true } }} + sx={{ flex: 1, "& textarea": { py: 0.75, lineHeight: 1.5 } }} /> - + + + + + + + ); diff --git a/src/admin/components/odin/OdinSidebar.tsx b/src/admin/components/odin/OdinSidebar.tsx index 6ea2f8b..16ce6c8 100644 --- a/src/admin/components/odin/OdinSidebar.tsx +++ b/src/admin/components/odin/OdinSidebar.tsx @@ -65,7 +65,7 @@ export default function OdinSidebar({ return ( { const isUser = t.role === "user"; return ( - - + ); })}