Ephemerals: improve layout

This commit is contained in:
Enrico Ros
2024-10-19 03:12:08 -07:00
parent fbc6265543
commit 0a07f2a447
4 changed files with 124 additions and 127 deletions
@@ -374,7 +374,6 @@ export function ChatMessageList(props: {
sx={{
mt: 'auto',
overflowY: 'auto',
minHeight: 64,
}}
/>
)}
+88 -82
View File
@@ -3,7 +3,8 @@ import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import PushPinIcon from '@mui/icons-material/PushPin';
import MaximizeIcon from '@mui/icons-material/Maximize';
import MinimizeIcon from '@mui/icons-material/Minimize';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import VerticalSplitOutlinedIcon from '@mui/icons-material/VerticalSplitOutlined';
@@ -111,107 +112,112 @@ function EphemeralItem(props: {
const { ephemeral, conversationHandler } = props;
// Event handlers
const handleDelete = React.useCallback(() => {
conversationHandler.overlayActions.ephemeralsDelete(ephemeral.id);
}, [conversationHandler, ephemeral.id]);
const handleTogglePinned = React.useCallback(() => {
conversationHandler.overlayActions.ephemeralsTogglePinned(ephemeral.id);
const handleToggleMinimized = React.useCallback(() => {
conversationHandler.overlayActions.ephemeralsToggleMinimized(ephemeral.id);
}, [conversationHandler, ephemeral.id]);
const handleToggleShowState = React.useCallback(() => {
conversationHandler.overlayActions.ephemeralsToggleShowState(ephemeral.id);
conversationHandler.overlayActions.ephemeralsToggleShowStatePane(ephemeral.id);
}, [conversationHandler, ephemeral.id]);
const showStatePane = ephemeral.showState && !!ephemeral.state;
return <Box
sx={{
p: { xs: 1, md: 2 },
position: 'relative',
const showStatePane = ephemeral.showStatePane && !!ephemeral.state;
return (
<Box sx={{
borderTop: '1px solid',
borderTopColor: 'divider',
// border: (i < ephemerals.length - 1) ? `2px solid ${theme.palette.divider}` : undefined,
}}>
{/* Title */}
{ephemeral.title && (
<Typography level='title-sm' sx={{ mt: -0.25, mb: 2, color: 'success.solidBg' }}>
{ephemeral.title} Internal Monologue
</Typography>
)}
{/* Vertical | split */}
<Grid container spacing={2}>
{/* Left pane (log) */}
<Grid xs={12} md={showStatePane ? 6 : 12}>
{/* New renderer, with */}
<Box sx={leftPaneSx}>
<ScaledTextBlockRenderer
text={ephemeral.text}
contentScaling={props.contentScaling}
textRenderVariant='markdown'
/>
</Box>
</Grid>
{/* Right pane (state) */}
{showStatePane && (
<Grid xs={12} md={6} sx={rightPaneSx}>
<StateRenderer
state={ephemeral.state}
contentScaling={props.contentScaling}
/>
</Grid>
)}
</Grid>
{/* Buttons */}
<Box sx={{
position: 'absolute',
top: 0,
right: 0,
m: 1,
display: 'flex',
gap: 1,
flexDirection: 'column',
}}>
{/* Pin */}
<IconButton
size='sm'
variant={ephemeral.pinned ? 'soft' : 'plain'}
onClick={handleTogglePinned}
sx={{
'& > *': { transition: 'transform 0.2s' },
}}
>
<PushPinIcon sx={ephemeral.pinned ? { transform: 'rotate(45deg)' } : undefined} />
</IconButton>
{/* Top Line - Title and Buttons */}
<Box sx={{
py: 1,
px: { xs: 1, md: 2 },
backgroundColor: 'success.softHoverBg',
display: 'flex',
gap: 1,
alignItems: 'center'
}}>
{/* Show State */}
<IconButton
size='sm'
variant={ephemeral.showState ? 'soft' : 'plain'}
onClick={handleToggleShowState}
>
{ephemeral.showState ? <VerticalSplitIcon /> : <VerticalSplitOutlinedIcon />}
</IconButton>
<Typography level='title-sm' sx={{ flex: 1, color: 'success.solidBg' }}>
{ephemeral.title} Internal Monologue
</Typography>
{/* Close */}
<IconButton
size='sm'
variant={ephemeral.done ? 'solid' : 'outlined'}
onClick={handleDelete}
>
<CloseRoundedIcon />
</IconButton>
{/* Show State */}
{!ephemeral.minimized && (
<IconButton
size='sm'
variant={ephemeral.showStatePane ? 'solid' : 'outlined'}
onClick={handleToggleShowState}
>
{ephemeral.showStatePane ? <VerticalSplitIcon /> : <VerticalSplitOutlinedIcon />}
</IconButton>
)}
{/* Minimize/Expand Button */}
<IconButton
size='sm'
variant={'outlined'}
onClick={handleToggleMinimized}
>
{ephemeral.minimized ? <MaximizeIcon /> : <MinimizeIcon />}
</IconButton>
{/* Close */}
<IconButton
size='sm'
variant={ephemeral.done ? 'solid' : 'outlined'}
onClick={handleDelete}
>
<CloseRoundedIcon />
</IconButton>
</Box>
{/* Content */}
{!ephemeral.minimized && <Box sx={{
py: 1,
px: { xs: 1, md: 2 },
}}>
{/* Content Grid */}
<Grid container spacing={2} sx={{ mt: 0.5 }}>
{/* Left pane (log) */}
<Grid xs={12} md={showStatePane ? 6 : 12}>
{/* New renderer, with */}
<Box sx={leftPaneSx}>
<ScaledTextBlockRenderer
text={ephemeral.text}
contentScaling={props.contentScaling}
textRenderVariant='markdown'
/>
</Box>
</Grid>
{/* Right pane (state) */}
{showStatePane && (
<Grid xs={12} md={6} sx={rightPaneSx}>
<StateRenderer
state={ephemeral.state}
contentScaling={props.contentScaling}
/>
</Grid>
)}
</Grid>
</Box>}
</Box>
</Box>;
);
}
+5 -10
View File
@@ -14,14 +14,10 @@ import { getChatLLMId } from '~/common/stores/llms/store-llms';
import { getChatAutoAI } from '../../apps/chat/store-app-chat';
import { createDEphemeral } from './store-perchat_ephemerals_slice';
import { createDEphemeral, EPHEMERALS_DEFAULT_TIMEOUT } from './store-perchat_ephemerals_slice';
import { createPerChatVanillaStore } from './store-perchat_vanilla';
// configuration
const EPHEMERAL_DELETION_DELAY = 5 * 1000;
/**
* ConversationHandler is a class to overlay state onto a conversation.
* It is a singleton per conversationId.
@@ -250,16 +246,15 @@ export class ConversationHandler {
// Ephemerals
createEphemeralHandler(title: string, initialText: string) {
const { ephemeralsAppend, ephemeralsUpdate, ephemeralsDelete, ephemeralsIsPinned } = this.overlayActions;
const { ephemeralsAppend, ephemeralsUpdate, ephemeralsDelete, getEphemeral } = this.overlayActions;
// create and append
const ephemeral = createDEphemeral(title, initialText);
const eId = ephemeral.id;
ephemeralsAppend(ephemeral);
// delete if not pinned
const deleteIfNotPinned = () => {
if (!ephemeralsIsPinned(eId))
const deleteIfMinimized = () => {
if (getEphemeral(eId)?.minimized)
ephemeralsDelete(eId);
};
@@ -269,7 +264,7 @@ export class ConversationHandler {
updateState: (state: object) => ephemeralsUpdate(eId, { state }),
markAsDone: () => {
ephemeralsUpdate(eId, { done: true });
setTimeout(deleteIfNotPinned, EPHEMERAL_DELETION_DELAY);
setTimeout(deleteIfMinimized, EPHEMERALS_DEFAULT_TIMEOUT);
},
};
}
@@ -3,6 +3,11 @@ import type { StateCreator } from 'zustand/vanilla';
import { agiUuid } from '~/common/util/idUtils';
// configuration
export const EPHEMERALS_DEFAULT_TIMEOUT = 6000;
const EPHEMERALS_DEFAULT_MINIMIZED = true;
/**
* DEphemeral: For ReAct sidebars, displayed under the chat
*/
@@ -11,9 +16,9 @@ export interface DEphemeral {
title: string;
text: string;
state: object;
done: boolean;
pinned: boolean;
showState: boolean;
done: boolean; // is complete, shall close after timeout
minimized: boolean; // collapsed to a single line
showStatePane: boolean; // show the state object
}
type DEphemeralId = string;
@@ -25,16 +30,16 @@ export function createDEphemeral(title: string, initialText: string): DEphemeral
text: initialText,
state: {},
done: false,
pinned: lastEphemeralPinned,
showState: lastEphemeralShowState,
minimized: lastMinimized,
showStatePane: lastShowStatePane,
};
}
/// Ephemerals Overlay Store ///
let lastEphemeralPinned = false;
let lastEphemeralShowState = false;
let lastMinimized = EPHEMERALS_DEFAULT_MINIMIZED;
let lastShowStatePane = false;
interface EphemeralsOverlayState {
@@ -48,11 +53,10 @@ export interface EphemeralsOverlayStore extends EphemeralsOverlayState {
ephemeralsDelete: (ephemeralId: DEphemeralId) => void;
ephemeralsUpdate: (ephemeralId: DEphemeralId, update: Partial<DEphemeral>) => void;
ephemeralsIsPinned: (ephemeralId: DEphemeralId) => boolean;
ephemeralsTogglePinned: (ephemeralId: DEphemeralId) => void;
ephemeralsToggleMinimized: (ephemeralId: DEphemeralId) => void;
ephemeralsToggleShowStatePane: (ephemeralId: DEphemeralId) => void;
ephemeralsIsShowState: (ephemeralId: DEphemeralId) => boolean;
ephemeralsToggleShowState: (ephemeralId: DEphemeralId) => void;
getEphemeral: (ephemeralId: DEphemeralId) => Readonly<DEphemeral> | undefined;
}
@@ -75,10 +79,10 @@ export const createEphemeralsOverlayStoreSlice: StateCreator<EphemeralsOverlaySt
ephemeralsUpdate: (ephemeralId, update) =>
_set((state) => {
if (update.pinned !== undefined)
lastEphemeralPinned = update.pinned;
if (update.showState !== undefined)
lastEphemeralShowState = update.showState;
if (update.minimized !== undefined)
lastMinimized = update.minimized;
if (update.showStatePane !== undefined)
lastShowStatePane = update.showStatePane;
return {
ephemerals: state.ephemerals.map((e) =>
e.id === ephemeralId
@@ -88,28 +92,21 @@ export const createEphemeralsOverlayStoreSlice: StateCreator<EphemeralsOverlaySt
};
}),
ephemeralsIsPinned: (ephemeralId) =>
_get().ephemerals.find((e) => e.id === ephemeralId)?.pinned || false,
ephemeralsTogglePinned: (ephemeralId) => {
const { ephemerals, ephemeralsDelete, ephemeralsUpdate } = _get();
const ephemeral = ephemerals.find((e) => e.id === ephemeralId);
if (ephemeral) {
if (ephemeral.pinned && ephemeral.done)
ephemeralsDelete(ephemeralId);
else
ephemeralsUpdate(ephemeralId, { pinned: !ephemeral.pinned });
}
},
ephemeralsIsShowState: (ephemeralId) =>
_get().ephemerals.find((e) => e.id === ephemeralId)?.showState || false,
ephemeralsToggleShowState: (ephemeralId) => {
ephemeralsToggleMinimized: (ephemeralId) => {
const { ephemerals, ephemeralsUpdate } = _get();
const ephemeral = ephemerals.find((e) => e.id === ephemeralId);
if (ephemeral)
ephemeralsUpdate(ephemeralId, { showState: !ephemeral.showState });
ephemeralsUpdate(ephemeralId, { minimized: !ephemeral.minimized });
},
ephemeralsToggleShowStatePane: (ephemeralId) => {
const { ephemerals, ephemeralsUpdate } = _get();
const ephemeral = ephemerals.find((e) => e.id === ephemeralId);
if (ephemeral)
ephemeralsUpdate(ephemeralId, { showStatePane: !ephemeral.showStatePane });
},
getEphemeral: (ephemeralId) =>
_get().ephemerals.find((e) => e.id === ephemeralId),
});