Lots of UI fixes

This commit is contained in:
James Ketr 2025-04-26 16:06:51 -07:00
parent 889a3f9e3b
commit 5f184b4a1d
7 changed files with 230 additions and 233 deletions

View File

@ -1,5 +1,3 @@
# About Backstory
The backstory about Backstory... The backstory about Backstory...
## Backstory is two things ## Backstory is two things

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import React, { ReactElement, JSXElementConstructor, useState, useEffect, useRef, useCallback, useMemo } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
@ -15,6 +15,7 @@ import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { SxProps } from '@mui/material';
import { ResumeBuilder } from './ResumeBuilder'; import { ResumeBuilder } from './ResumeBuilder';
@ -24,6 +25,7 @@ import { VectorVisualizer } from './VectorVisualizer';
import { Controls } from './Controls'; import { Controls } from './Controls';
import { Conversation, ConversationHandle } from './Conversation'; import { Conversation, ConversationHandle } from './Conversation';
import { Scrollable } from './AutoScroll'; import { Scrollable } from './AutoScroll';
import { BackstoryTab } from './BackstoryTab';
import './App.css'; import './App.css';
import './Conversation.css'; import './Conversation.css';
@ -43,34 +45,20 @@ const getConnectionBase = (loc: any): string => {
} }
} }
interface TabProps {
interface TabPanelProps { label?: string,
children?: React.ReactNode; path: string,
index: number; tabProps?: {
tab: number; label?: string,
} sx?: SxProps,
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined,
function CustomTabPanel(props: TabPanelProps) { iconPosition?: "bottom" | "top" | "start" | "end" | undefined
const { children, tab, index, ...other } = props;
return (
<div
className="TabPanel"
role="tabpanel"
style={{ "display": tab === index ? "flex": "none" }}
id={`tabpanel-${index}`}
aria-labelledby={`tab-${index}`}
{...other}
>
{children}
</div>
);
} }
};
const App = () => { const App = () => {
const [sessionId, setSessionId] = useState<string | undefined>(undefined); const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const [connectionBase,] = useState<string>(getConnectionBase(window.location)) const [connectionBase,] = useState<string>(getConnectionBase(window.location))
const [selectedPath, setSelectedPath] = useState<string>("");
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [isMenuClosing, setIsMenuClosing] = useState(false); const [isMenuClosing, setIsMenuClosing] = useState(false);
const [activeTab, setActiveTab] = useState<number>(0); const [activeTab, setActiveTab] = useState<number>(0);
@ -132,15 +120,154 @@ const App = () => {
}; };
// Extract the sessionId from the URL if present, otherwise const tabs: TabProps[] = useMemo(() => {
// request a sessionId from the server. const backstoryPreamble: MessageList = [
const validPaths = useMemo(() => ['chat', 'notes', 'tasks'], []); // allowed paths {
role: 'content',
title: 'Welcome to Backstory',
content: `
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
It was written by James Ketrenos in order to provide answers to
questions potential employers may have about his work history.
What would you like to know about James?
`
}
];
const backstoryQuestions = [
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
</MuiMarkdown>
</Box>
];
const tabSx = { flexGrow: 1, fontSize: '1rem' };
return [{
label: "",
path: "",
tabProps: {
label: "Backstory",
sx: tabSx,
icon:
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />,
iconPosition: "start"
},
children: (
<Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<Conversation
ref={chatRef}
{...{
type: "chat",
prompt: "What would you like to know about James?",
sessionId,
connectionBase,
setSnack,
preamble: backstoryPreamble,
defaultPrompts: backstoryQuestions
}}
/>
</Scrollable>
)
}, {
label: "Resume Builder",
path: "resume-builder",
children: (
<ResumeBuilder sx={{
margin: "0 auto",
height: "calc(100vh - 72px)",
overflow: "auto",
backgroundColor: "#F5F5F5",
display: "flex",
flexGrow: 1
}} {...{ setSnack, connectionBase, sessionId }}
/>
)
}, {
label: "Context Visualizer",
path: "context-visualizer",
children:
<Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<VectorVisualizer sx={{ p: 1 }} {...{ connectionBase, sessionId, setSnack }} />
</Scrollable>
}, {
label: "About",
path: "about",
children: (
<Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
<Message
{...{
sx: {
display: 'flex',
flexDirection: 'column',
p: 1,
m: 0,
flexGrow: 0,
},
message: { role: 'content', title: "About Backstory", content: about },
submitQuery: handleSubmitChatQuery,
connectionBase,
sessionId,
setSnack
}} />
<Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0 }} />
</Scrollable>
)
}, {
path: "settings",
tabProps: {
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
icon: <SettingsIcon />
},
children: (
<Box className="ChatBox">
{sessionId !== undefined &&
<Controls {...{ sessionId, setSnack, connectionBase }} />
}
</Box >
)
}];
}, [about, connectionBase, sessionId, setSnack, isMobile]);
useEffect(() => { useEffect(() => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId] const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
const fetchSession = async (pathOverride?: string) => { const fetchSession = async () => {
try { try {
const response = await fetch(connectionBase + `/api/context`, { const response = await fetch(connectionBase + `/api/context`, {
method: 'POST', method: 'POST',
@ -155,30 +282,29 @@ const App = () => {
const data = await response.json(); const data = await response.json();
setSessionId(data.id); setSessionId(data.id);
const newPath = pathOverride || 'chat'; // default fallback const newPath = `/${data.id}`;
window.history.replaceState({}, '', `/${newPath}/${data.id}`); window.history.replaceState({}, '', newPath);
} catch (error: any) { } catch (error: any) {
console.error(error);
setSnack("Server is temporarily down", "error"); setSnack("Server is temporarily down", "error");
} }
}; };
if (pathParts.length < 2) { if (pathParts.length < 1) {
console.log("No session id or path -- creating new session"); console.log("No session id or path -- creating new session");
fetchSession(); fetchSession();
} else { } else {
const currentPath = pathParts[0]; const currentPath = pathParts.length < 2 ? '' : pathParts[0];
const session = pathParts[1]; const session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
if (!validPaths.includes(currentPath)) { if (-1 === tabIndex) {
console.log(`Invalid path "${currentPath}" -- redirecting to default`); console.log(`Invalid path "${currentPath}" -- redirecting to default`);
fetchSession(); // or you could window.location.replace if you want tabIndex = 0;
} else { }
console.log(`Path: ${currentPath}, Session id: ${session}`);
setSessionId(session); setSessionId(session);
setSelectedPath(currentPath); setActiveTab(tabIndex);
} }
} }, [setSessionId, connectionBase, setSnack, tabs]);
}, [setSessionId, setSelectedPath, connectionBase, setSnack, validPaths]);
const handleMenuClose = () => { const handleMenuClose = () => {
setIsMenuClosing(true); setIsMenuClosing(true);
@ -196,159 +322,38 @@ const App = () => {
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
if (newValue > tabs.length) {
return;
}
setActiveTab(newValue); setActiveTab(newValue);
const tabPath = tabs[newValue].path;
if (tabPath) {
window.history.pushState({}, '', `/${tabPath}/${sessionId}`);
} else {
window.history.pushState({}, '', `/${sessionId}`);
}
handleMenuClose(); handleMenuClose();
}; };
const handleTabSelect = (newPath: string) => {
if (!sessionId) return; // safety
setSelectedPath(newPath);
window.history.pushState({}, '', `/${newPath}/${sessionId}`);
};
useEffect(() => { useEffect(() => {
const handlePopState = () => { const handlePopState = () => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const pathParts = url.pathname.split('/').filter(Boolean); const pathParts = url.pathname.split('/').filter(Boolean);
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
const session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
if (pathParts.length >= 2) { let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
const path = pathParts[0]; if (-1 === tabIndex) {
const session = pathParts[1]; console.log(`Invalid path "${currentPath}" -- redirecting to default`);
tabIndex = 0;
if (validPaths.includes(path)) { }
setSelectedPath(path);
setSessionId(session); setSessionId(session);
} setActiveTab(tabIndex);
}
}; };
window.addEventListener('popstate', handlePopState); window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState);
}, [setSelectedPath, setSessionId, validPaths]); }, [setSessionId, tabs]);
const menuDrawer = (
<Card className="MenuCard">
<Tabs sx={{ display: "flex", flexGrow: 1 }}
orientation="vertical"
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
<Tab sx={{ fontSize: '1rem' }} label="Backstory"
value={0}
icon={
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
}
iconPosition="start" />
<Tab
value={1}
sx={{ fontSize: '1rem' }} wrapped
label="Resume Builder"
/>
<Tab
value={2}
sx={{ fontSize: '1rem' }} wrapped
label="Context Visualizer"
/>
<Tab
value={3}
sx={{ fontSize: '1rem' }} label="About" />
<Tab
value={4}
sx={{ fontSize: '1rem' }} icon={<SettingsIcon />} />
</Tabs>
</Card>
);
const tabs = useMemo(() => {
const chatPreamble: MessageList = [
{
role: 'content',
title: 'Welcome to Backstory',
content: `
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
It was written by James Ketrenos in order to provide answers to
questions potential employers may have about his work history.
What would you like to know about James?
`
}
];
const chatQuestions = [
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
</MuiMarkdown>
</Box>
];
return [
<Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<Conversation
ref={chatRef}
{...{
type: "chat",
prompt: "What would you like to know about James?",
sessionId,
connectionBase,
setSnack,
preamble: chatPreamble,
defaultPrompts: chatQuestions
}}
/>
</Scrollable>,
<ResumeBuilder sx={{
margin: "0 auto",
height: "calc(100vh - 72px)",
overflow: "auto",
backgroundColor: "#F5F5F5",
display: "flex",
flexGrow: 1
}} {...{ setSnack, connectionBase, sessionId }} />,
<Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<VectorVisualizer {...{ connectionBase, sessionId, setSnack }} />
</Scrollable>,
<Box className="ChatBox">
<Box className="Conversation">
<Message {...{ message: { role: 'content', content: about }, submitQuery: handleSubmitChatQuery, connectionBase, sessionId, setSnack }} />
</Box>
</Box>,
<Box className="ChatBox">
{sessionId !== undefined &&
<Controls {...{ sessionId, setSnack, connectionBase }} />
}
</Box>
];
}, [about, connectionBase, sessionId, setSnack, isMobile]);
/* toolbar height is 64px + 8px margin-top */ /* toolbar height is 64px + 8px margin-top */
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' })); const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
@ -406,34 +411,7 @@ const App = () => {
allowScrollButtonsMobile allowScrollButtonsMobile
onChange={handleTabChange} onChange={handleTabChange}
aria-label="Backstory navigation"> aria-label="Backstory navigation">
<Tab sx={{ fontSize: '1rem' }} label="Backstory" {tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
value={0}
icon={
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
}
iconPosition="start" />
<Tab
value={1}
sx={{ fontSize: '1rem' }} wrapped
label="Resume Builder"
/>
<Tab
value={2}
sx={{ fontSize: '1rem' }} wrapped
label="Context Visualizer"
/>
<Tab
value={3}
sx={{ fontSize: '1rem' }} label="About" />
<Tab
value={4}
sx={{ flexShrink: 1, flexGrow: 0, fontSize: '1rem' }} icon={<SettingsIcon />} />
</Tabs> </Tabs>
} }
</Box> </Box>
@ -467,12 +445,24 @@ const App = () => {
}} }}
> >
<Toolbar /> <Toolbar />
{menuDrawer} <Card className="MenuCard">
<Tabs sx={{ display: "flex", flexGrow: 1 }}
orientation="vertical"
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
</Tabs>
</Card>
</Drawer> </Drawer>
</Box> </Box>
{ {
tabs.map((tab: any, i: number) => tabs.map((tab: any, i: number) =>
<CustomTabPanel key={i} tab={activeTab} index={i}>{tab}</CustomTabPanel> <BackstoryTab key={i} active={i === activeTab}>{tab.children}</BackstoryTab>
) )
} }
</Box> </Box>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, RefObject } from 'react'; import { useEffect, useRef, RefObject } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
@ -53,15 +53,15 @@ const useAutoScrollToBottom = (
behavior: smooth ? 'smooth' : 'auto' behavior: smooth ? 'smooth' : 'auto'
}); });
} else { } else {
// console.log('Not scrolling', { console.log('Not scrolling', {
// isNearBottom, isNearBottom,
// isUserScrollingUp, isUserScrollingUp,
// scrollHeight: container.scrollHeight, scrollHeight: container.scrollHeight,
// scrollTop: container.scrollTop, scrollTop: container.scrollTop,
// clientHeight: container.clientHeight, clientHeight: container.clientHeight,
// threshold, threshold,
// delta: container.scrollHeight - container.scrollTop - container.clientHeight delta: container.scrollHeight - container.scrollTop - container.clientHeight
// }); });
} }
}; };

View File

@ -19,7 +19,8 @@ interface ChatBubbleProps {
title?: string; title?: string;
} }
function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatBubbleProps) { function ChatBubble(props: ChatBubbleProps) {
const { role, isFullWidth, children, sx, className, title } = props;
const theme = useTheme(); const theme = useTheme();
const defaultRadius = '16px'; const defaultRadius = '16px';
@ -122,7 +123,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatB
<Accordion <Accordion
defaultExpanded defaultExpanded
className={className} className={className}
sx={{ ...styles[role] }} sx={{ ...styles[role], ...sx }}
> >
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
@ -134,9 +135,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatB
{children} {children}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );
} }
return ( return (

View File

@ -444,7 +444,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
<Box className={className || "Conversation"} <Box className={className || "Conversation"}
ref={scrollRef} ref={scrollRef}
sx={{ sx={{
display: "flex", flexDirection: "column", flexGrow: 1, p: 1, mt: 0, p: 1,
mt: 0,
...sx ...sx
}}> }}>
{ {

View File

@ -18,6 +18,7 @@ import Collapse from '@mui/material/Collapse';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ExpandMore } from './ExpandMore'; import { ExpandMore } from './ExpandMore';
import { SxProps, Theme } from '@mui/material';
import { ChatBubble } from './ChatBubble'; import { ChatBubble } from './ChatBubble';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
@ -61,12 +62,14 @@ interface MessageMetaProps {
type MessageList = MessageData[]; type MessageList = MessageData[];
interface MessageProps { interface MessageProps {
sx?: SxProps<Theme>,
message?: MessageData, message?: MessageData,
isFullWidth?: boolean, isFullWidth?: boolean,
submitQuery?: (text: string) => void, submitQuery?: (text: string) => void,
sessionId?: string, sessionId?: string,
connectionBase: string, connectionBase: string,
setSnack: SetSnackType, setSnack: SetSnackType,
className?: string,
}; };
interface ChatQueryInterface { interface ChatQueryInterface {
@ -216,7 +219,8 @@ const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => {
); );
} }
const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase }: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase, sx, className } = props;
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
@ -237,7 +241,7 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne
return ( return (
<ChatBubble <ChatBubble
className="Message" className={className || "Message"}
isFullWidth={isFullWidth} isFullWidth={isFullWidth}
role={message.role} role={message.role}
title={message.title} title={message.title}
@ -246,9 +250,10 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne
flexDirection: "column", flexDirection: "column",
pb: message.metadata ? 0 : "8px", pb: message.metadata ? 0 : "8px",
m: 0, m: 0,
mb: 1,
mt: 1, mt: 1,
marginBottom: "0px !important", // Remove whitespace from expanded Accordion
// overflowX: "auto" // overflowX: "auto"
...sx,
}}> }}>
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0 }}> <CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0 }}>
<CopyBubble content={message?.content} /> <CopyBubble content={message?.content} />

View File

@ -9,6 +9,7 @@ import Button from '@mui/material/Button';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { SxProps, Theme } from '@mui/material';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
@ -47,6 +48,7 @@ interface VectorVisualizerProps {
setSnack: SetSnackType; setSnack: SetSnackType;
inline?: boolean; inline?: boolean;
rag?: any; rag?: any;
sx?: SxProps<Theme>;
} }
interface ChromaResult { interface ChromaResult {
@ -98,7 +100,8 @@ const symbolMap: Record<string, string> = {
'query': 'circle', 'query': 'circle',
}; };
const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inline, connectionBase, sessionId }) => { const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { setSnack, rag, inline, connectionBase, sessionId, sx } = props;
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>(''); const [newQuery, setNewQuery] = useState<string>('');
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined); const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
@ -305,7 +308,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inli
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1 flexGrow: 1,
...sx
}}> }}>
{ {
!inline && !inline &&