377 lines
11 KiB
TypeScript
377 lines
11 KiB
TypeScript
import { TerminalOutput } from '@/ai/components/TerminalOutput';
|
|
import { styled } from '@linaria/react';
|
|
import { useContext, useState } from 'react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import {
|
|
IconChevronDown,
|
|
IconChevronUp,
|
|
IconCode,
|
|
IconCopy,
|
|
IconDownload,
|
|
IconFile,
|
|
IconPlayerPlay,
|
|
IconSquareRoundedCheck,
|
|
IconSquareRoundedX,
|
|
} from 'twenty-ui/icon';
|
|
import { CodeEditor, LightIconButton } from 'twenty-ui/input';
|
|
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
|
|
|
const StyledContainer = styled.div`
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.md};
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin: ${themeCssVariables.spacing[2]} 0;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const StyledHeader = styled.div<{ status: 'success' | 'error' | 'running' }>`
|
|
align-items: center;
|
|
background: ${themeCssVariables.background.secondary};
|
|
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
justify-content: space-between;
|
|
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledHeaderLeft = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledHeaderRight = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledStatusBadge = styled.div<{
|
|
status: 'success' | 'error' | 'running';
|
|
}>`
|
|
align-items: center;
|
|
background: ${({ status }) =>
|
|
status === 'success'
|
|
? themeCssVariables.background.transparent.success
|
|
: status === 'error'
|
|
? themeCssVariables.background.transparent.danger
|
|
: themeCssVariables.background.transparent.medium};
|
|
border-radius: ${themeCssVariables.border.radius.pill};
|
|
color: ${({ status }) =>
|
|
status === 'success'
|
|
? themeCssVariables.color.turquoise
|
|
: status === 'error'
|
|
? themeCssVariables.color.red
|
|
: themeCssVariables.font.color.secondary};
|
|
corner-shape: round;
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.xs};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
padding: ${themeCssVariables.spacing['0.5']} ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledTitle = styled.span`
|
|
color: ${themeCssVariables.font.color.primary};
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
`;
|
|
|
|
const StyledSection = styled.div`
|
|
border-top: 1px solid ${themeCssVariables.border.color.light};
|
|
`;
|
|
|
|
const StyledSectionHeader = styled.div`
|
|
align-items: center;
|
|
background: ${themeCssVariables.background.secondary};
|
|
cursor: pointer;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
justify-content: space-between;
|
|
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
|
|
transition: background calc(${themeCssVariables.animation.duration.fast} * 1s);
|
|
|
|
&:hover {
|
|
background: ${themeCssVariables.background.tertiary};
|
|
}
|
|
`;
|
|
|
|
const StyledSectionHeaderLeft = styled.div`
|
|
align-items: center;
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledCodeEditorContainer = styled.div`
|
|
max-height: 300px;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const StyledFilesGrid = styled.div`
|
|
display: grid;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
padding: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledFileCard = styled.div`
|
|
border: 1px solid ${themeCssVariables.border.color.light};
|
|
border-radius: ${themeCssVariables.border.radius.sm};
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const StyledFilePreview = styled.div`
|
|
align-items: center;
|
|
aspect-ratio: 16 / 9;
|
|
background: ${themeCssVariables.background.tertiary};
|
|
display: flex;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const StyledPreviewImage = styled.img`
|
|
height: 100%;
|
|
object-fit: contain;
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledFileInfo = styled.div`
|
|
align-items: center;
|
|
background: ${themeCssVariables.background.secondary};
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
justify-content: space-between;
|
|
padding: ${themeCssVariables.spacing['1.5']} ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledFileName = styled.span`
|
|
color: ${themeCssVariables.font.color.primary};
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const StyledDownloadLink = styled.a`
|
|
align-items: center;
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
display: flex;
|
|
|
|
&:hover {
|
|
color: ${themeCssVariables.font.color.primary};
|
|
}
|
|
`;
|
|
|
|
type CodeExecutionDisplayProps = {
|
|
code: string;
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode?: number;
|
|
files?: Array<{
|
|
fileId: string;
|
|
filename: string;
|
|
url: string;
|
|
mimeType?: string;
|
|
}>;
|
|
isRunning?: boolean;
|
|
};
|
|
|
|
const isPreviewableMimeType = (mimeType?: string): boolean => {
|
|
if (!mimeType) {
|
|
return false;
|
|
}
|
|
|
|
return ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(
|
|
mimeType,
|
|
);
|
|
};
|
|
|
|
export const CodeExecutionDisplay = ({
|
|
code,
|
|
stdout,
|
|
stderr,
|
|
exitCode,
|
|
files = [],
|
|
isRunning = false,
|
|
}: CodeExecutionDisplayProps) => {
|
|
const { theme } = useContext(ThemeContext);
|
|
const { t } = useLingui();
|
|
const { copyToClipboard } = useCopyToClipboard();
|
|
const [isCodeExpanded, setIsCodeExpanded] = useState(false);
|
|
const [isOutputExpanded, setIsOutputExpanded] = useState(true);
|
|
const [isFilesExpanded, setIsFilesExpanded] = useState(true);
|
|
|
|
const status: 'success' | 'error' | 'running' = isRunning
|
|
? 'running'
|
|
: exitCode === 0
|
|
? 'success'
|
|
: 'error';
|
|
|
|
const StatusIcon =
|
|
status === 'success'
|
|
? IconSquareRoundedCheck
|
|
: status === 'error'
|
|
? IconSquareRoundedX
|
|
: IconPlayerPlay;
|
|
|
|
const statusText = isRunning
|
|
? t`Running...`
|
|
: exitCode === 0
|
|
? t`Completed`
|
|
: t`Failed`;
|
|
|
|
const hasOutput = stdout || stderr;
|
|
const hasFiles = files.length > 0;
|
|
|
|
return (
|
|
<StyledContainer>
|
|
<StyledHeader status={status}>
|
|
<StyledHeaderLeft>
|
|
<IconCode size={theme.icon.size.md} />
|
|
<StyledTitle>{t`Python Code Execution`}</StyledTitle>
|
|
</StyledHeaderLeft>
|
|
<StyledHeaderRight>
|
|
<StyledStatusBadge status={status}>
|
|
<StatusIcon size={theme.icon.size.sm} />
|
|
{statusText}
|
|
</StyledStatusBadge>
|
|
</StyledHeaderRight>
|
|
</StyledHeader>
|
|
|
|
<StyledSection>
|
|
<StyledSectionHeader onClick={() => setIsCodeExpanded(!isCodeExpanded)}>
|
|
<StyledSectionHeaderLeft>
|
|
<IconCode size={theme.icon.size.sm} />
|
|
{t`Code`}
|
|
</StyledSectionHeaderLeft>
|
|
<StyledHeaderRight>
|
|
<LightIconButton
|
|
Icon={IconCopy}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
copyToClipboard(code);
|
|
}}
|
|
title={t`Copy code`}
|
|
size="small"
|
|
accent="tertiary"
|
|
/>
|
|
{isCodeExpanded ? (
|
|
<IconChevronUp size={theme.icon.size.sm} />
|
|
) : (
|
|
<IconChevronDown size={theme.icon.size.sm} />
|
|
)}
|
|
</StyledHeaderRight>
|
|
</StyledSectionHeader>
|
|
<AnimatedExpandableContainer
|
|
isExpanded={isCodeExpanded}
|
|
mode="fit-content"
|
|
>
|
|
<StyledCodeEditorContainer>
|
|
<CodeEditor
|
|
value={code}
|
|
language="python"
|
|
height="300px"
|
|
options={{
|
|
readOnly: true,
|
|
domReadOnly: true,
|
|
minimap: { enabled: false },
|
|
}}
|
|
/>
|
|
</StyledCodeEditorContainer>
|
|
</AnimatedExpandableContainer>
|
|
</StyledSection>
|
|
|
|
{(hasOutput || isRunning) && (
|
|
<StyledSection>
|
|
<StyledSectionHeader
|
|
onClick={() => setIsOutputExpanded(!isOutputExpanded)}
|
|
>
|
|
<StyledSectionHeaderLeft>{t`Output`}</StyledSectionHeaderLeft>
|
|
{isOutputExpanded ? (
|
|
<IconChevronUp size={theme.icon.size.sm} />
|
|
) : (
|
|
<IconChevronDown size={theme.icon.size.sm} />
|
|
)}
|
|
</StyledSectionHeader>
|
|
<AnimatedExpandableContainer
|
|
isExpanded={isOutputExpanded}
|
|
mode="fit-content"
|
|
>
|
|
<TerminalOutput
|
|
stdout={stdout}
|
|
stderr={stderr}
|
|
isRunning={isRunning}
|
|
/>
|
|
</AnimatedExpandableContainer>
|
|
</StyledSection>
|
|
)}
|
|
|
|
{hasFiles && (
|
|
<StyledSection>
|
|
<StyledSectionHeader
|
|
onClick={() => setIsFilesExpanded(!isFilesExpanded)}
|
|
>
|
|
<StyledSectionHeaderLeft>
|
|
<IconFile size={theme.icon.size.sm} />
|
|
{t`Generated Files`} ({files.length})
|
|
</StyledSectionHeaderLeft>
|
|
{isFilesExpanded ? (
|
|
<IconChevronUp size={theme.icon.size.sm} />
|
|
) : (
|
|
<IconChevronDown size={theme.icon.size.sm} />
|
|
)}
|
|
</StyledSectionHeader>
|
|
<AnimatedExpandableContainer
|
|
isExpanded={isFilesExpanded}
|
|
mode="fit-content"
|
|
>
|
|
<StyledFilesGrid>
|
|
{files.map((file) => {
|
|
const filename = file.filename;
|
|
|
|
return (
|
|
<StyledFileCard key={file.fileId}>
|
|
<StyledFilePreview>
|
|
{isPreviewableMimeType(file.mimeType) ? (
|
|
<StyledPreviewImage
|
|
src={file.url}
|
|
alt={filename}
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<IconFile size={48} color={theme.font.color.tertiary} />
|
|
)}
|
|
</StyledFilePreview>
|
|
<StyledFileInfo>
|
|
<StyledFileName title={filename}>
|
|
{filename}
|
|
</StyledFileName>
|
|
<StyledDownloadLink
|
|
href={file.url}
|
|
download={filename}
|
|
title={t`Download ${filename}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<IconDownload size={theme.icon.size.sm} />
|
|
</StyledDownloadLink>
|
|
</StyledFileInfo>
|
|
</StyledFileCard>
|
|
);
|
|
})}
|
|
</StyledFilesGrid>
|
|
</AnimatedExpandableContainer>
|
|
</StyledSection>
|
|
)}
|
|
</StyledContainer>
|
|
);
|
|
};
|