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 ( {t`Python Code Execution`} {statusText} setIsCodeExpanded(!isCodeExpanded)}> {t`Code`} { e.stopPropagation(); copyToClipboard(code); }} title={t`Copy code`} size="small" accent="tertiary" /> {isCodeExpanded ? ( ) : ( )} {(hasOutput || isRunning) && ( setIsOutputExpanded(!isOutputExpanded)} > {t`Output`} {isOutputExpanded ? ( ) : ( )} )} {hasFiles && ( setIsFilesExpanded(!isFilesExpanded)} > {t`Generated Files`} ({files.length}) {isFilesExpanded ? ( ) : ( )} {files.map((file) => { const filename = file.filename; return ( {isPreviewableMimeType(file.mimeType) ? ( ) : ( )} {filename} ); })} )} ); };