Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGridPremium] Control grid with prompts #16992

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 373 additions & 0 deletions docs/data/data-grid/ask-your-table/AssistantPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
import * as React from 'react';
import {
useGridApiContext,
PromptField,
PromptFieldRecord,
PromptFieldControl,
PromptFieldSend,
IS_SPEECH_RECOGNITION_SUPPORTED,
GridShadowScrollArea,
} from '@mui/x-data-grid-premium';
import { mockPromptResolver } from '@mui/x-data-grid-generator';
import Tooltip from '@mui/material/Tooltip';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import MicIcon from '@mui/icons-material/Mic';
import MicOffIcon from '@mui/icons-material/MicOff';
import SendIcon from '@mui/icons-material/Send';
import CloseIcon from '@mui/icons-material/Close';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import Popover from '@mui/material/Popover';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton, { iconButtonClasses } from '@mui/material/IconButton';
import ReplayIcon from '@mui/icons-material/Replay';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
import Fade from '@mui/material/Fade';
import Chip from '@mui/material/Chip';

import CircularProgress from '@mui/material/CircularProgress';

const PROMPT_SUGGESTIONS = [
'Sort by name',
'Show people from the EU',
'Sort by company name and employee name',
'Order companies by amount of people',
];

function AssistantPanel({ open, onClose, anchorEl, allowDataSampling = false }) {
const apiRef = useGridApiContext();
const [promptHistory, setPromptHistory] = React.useState([]);
const [suggestionsExpanded, setSuggestionsExpanded] = React.useState(false);
const promptHistoryScrollAreaRef = React.useRef(null);

const context = React.useMemo(
() => apiRef.current.unstable_aiAssistant.getPromptContext(allowDataSampling),
[apiRef, allowDataSampling],
);

const suggestions = React.useMemo(() => {
return suggestionsExpanded ? PROMPT_SUGGESTIONS : PROMPT_SUGGESTIONS.slice(0, 2);
}, [suggestionsExpanded]);

const handlePrompt = React.useCallback(
async (prompt, promptContext = context) => {
const promptId = Date.now();
apiRef.current.setLoading(true);

setPromptHistory((prev) => [
...prev,
{
value: prompt,
time: new Date(promptId),
status: 'pending',
},
]);

try {
const result = await mockPromptResolver(prompt, promptContext);
apiRef.current.unstable_aiAssistant.applyPromptResult(result);
setPromptHistory((prev) =>
prev.map((item) =>
item.time.getTime() === promptId ? { ...item, status: 'success' } : item,
),
);
return result;
} catch (error) {
console.error(error);
setPromptHistory((prev) =>
prev.map((item) =>
item.time.getTime() === promptId ? { ...item, status: 'error' } : item,
),
);
return undefined;
} finally {
apiRef.current.setLoading(false);
}
},
[apiRef, context],
);

// Scroll to the bottom of the prompt history when the panel opens
React.useEffect(() => {
if (open) {
setTimeout(() => {
promptHistoryScrollAreaRef.current?.scrollTo({
top: promptHistoryScrollAreaRef.current?.scrollHeight,
behavior: 'instant',
});
}, 0);
}
}, [open]);

// Scroll to the bottom of the prompt history when the prompt history changes
React.useEffect(() => {
if (promptHistoryScrollAreaRef.current) {
promptHistoryScrollAreaRef.current.scrollTo({
top: promptHistoryScrollAreaRef.current.scrollHeight,
behavior: 'smooth',
});
}
}, [promptHistory]);

return (
<Popover
open={open}
onClose={onClose}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
slotProps={{
paper: {
sx: {
width: 400,
},
},
}}
disableEnforceFocus
disableRestoreFocus
>
<Stack>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{
pl: 2,
pr: 1.5,
py: 0.75,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="body2" fontWeight="medium">
AI Assistant
</Typography>
<IconButton onClick={onClose}>
<CloseIcon fontSize="small" />
</IconButton>
</Stack>

<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 220,
}}
>
{promptHistory.length > 0 ? (
<GridShadowScrollArea
ref={promptHistoryScrollAreaRef}
style={{ flexShrink: 0, height: '100%' }}
>
<List dense sx={{ py: 0 }}>
{promptHistory.map((prompt, index) => {
const isMostRecent = index === promptHistory.length - 1;
const time = prompt.time.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
let iconColor = isMostRecent ? 'primary' : 'action';

if (prompt.status === 'error') {
iconColor = 'error';
}

return (
<Fade
in
timeout={200}
easing="ease-in-out"
key={`${prompt.value}-${index}`}
>
<ListItem
secondaryAction={
<Tooltip title="Run again">
<IconButton
size="small"
onClick={() => handlePrompt(prompt.value)}
>
<ReplayIcon fontSize="small" />
</IconButton>
</Tooltip>
}
sx={(theme) => ({
opacity: prompt.status === 'pending' ? 0.5 : 1,
[`& .${iconButtonClasses.root}`]: {
transition: theme.transitions.create(['opacity'], {
duration: 200,
easing: 'ease-in-out',
}),
},
[`&:not(:hover) .${iconButtonClasses.root}`]: {
opacity: 0,
},
})}
>
<ListItemIcon
sx={{ pt: 2, minWidth: 36, alignSelf: 'flex-start' }}
>
{prompt.status === 'pending' ? (
<CircularProgress size={20} thickness={6} />
) : (
<AutoAwesomeIcon fontSize="small" color={iconColor} />
)}
</ListItemIcon>
<ListItemText
primary={prompt.value}
secondary={
<Stack component="span" direction="column">
{time}
<Typography variant="caption" color="error">
{prompt.status === 'error'
? 'Failed to process prompt'
: ''}
</Typography>
</Stack>
}
/>
</ListItem>
</Fade>
);
})}
</List>
</GridShadowScrollArea>
) : (
<Typography
variant="body2"
color="text.secondary"
sx={{ textAlign: 'center' }}
>
No prompt history
</Typography>
)}
</Box>

<Box sx={{ p: 1, borderTop: '1px solid', borderColor: 'divider' }}>
<PromptField
onPrompt={handlePrompt}
onError={console.error}
allowDataSampling={allowDataSampling}
>
<PromptFieldControl
onKeyDown={(event) => {
if (event.key === 'Enter') {
// Prevents the `multiline` TextField from adding a new line
event.preventDefault();
}
}}
render={({ ref, ...controlProps }, state) => (
<TextField
{...controlProps}
fullWidth
inputRef={ref}
aria-label="Prompt"
placeholder={
state.recording
? 'Listening for prompt…'
: 'Type or record a prompt…'
}
size="small"
multiline
autoFocus
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
{IS_SPEECH_RECOGNITION_SUPPORTED ? (
<Tooltip
title={state.recording ? 'Stop recording' : 'Record'}
>
<PromptFieldRecord
size="small"
edge="start"
color={state.recording ? 'primary' : 'default'}
>
<MicIcon fontSize="small" />
</PromptFieldRecord>
</Tooltip>
) : (
<Tooltip title="Speech recognition is not supported in this browser">
<MicOffIcon fontSize="small" />
</Tooltip>
)}
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<Tooltip title="Send">
<span>
<PromptFieldSend
size="small"
edge="end"
color="primary"
>
<SendIcon fontSize="small" />
</PromptFieldSend>
</span>
</Tooltip>
</InputAdornment>
),
...controlProps.slotProps?.input,
},
...controlProps.slotProps,
}}
/>
)}
/>
</PromptField>
</Box>

<Stack
direction="column"
sx={{
p: 1,
pb: 1.25,
borderTop: '1px solid',
borderColor: 'divider',
gap: 0.75,
}}
>
<Typography variant="caption">Suggestions for you</Typography>
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
{suggestions.map((suggestion) => (
<Chip
key={suggestion}
variant="outlined"
label={suggestion}
icon={<AutoAwesomeIcon style={{ fontSize: '1rem' }} />}
onClick={() => handlePrompt(suggestion)}
/>
))}
{!suggestionsExpanded && (
<Chip
variant="outlined"
onClick={() => setSuggestionsExpanded(true)}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<span>More</span>
<ExpandMoreIcon sx={{ fontSize: '1rem', ml: 0.25, mr: -0.5 }} />
</Box>
}
/>
)}
</Stack>
</Stack>
</Stack>
</Popover>
);
}

export { AssistantPanel };
Loading
Loading