diff --git a/mito-ai/src/Extensions/AiChat/ChatMessage/ChatMessage.tsx b/mito-ai/src/Extensions/AiChat/ChatMessage/ChatMessage.tsx index acaad090e..f5d969e12 100644 --- a/mito-ai/src/Extensions/AiChat/ChatMessage/ChatMessage.tsx +++ b/mito-ai/src/Extensions/AiChat/ChatMessage/ChatMessage.tsx @@ -106,6 +106,11 @@ const ChatMessage: React.FC = ({ code={messagePart} role={message.role} renderMimeRegistry={renderMimeRegistry} + previewAICode={previewAICode} + acceptAICode={acceptAICode} + rejectAICode={rejectAICode} + isLastAiMessage={isLastAiMessage} + codeReviewStatus={codeReviewStatus} /> {isLastAiMessage && codeReviewStatus === 'chatPreview' && diff --git a/mito-ai/src/Extensions/AiChat/ChatMessage/CodeBlock.tsx b/mito-ai/src/Extensions/AiChat/ChatMessage/CodeBlock.tsx index 9be20c938..c25c9b030 100644 --- a/mito-ai/src/Extensions/AiChat/ChatMessage/CodeBlock.tsx +++ b/mito-ai/src/Extensions/AiChat/ChatMessage/CodeBlock.tsx @@ -3,18 +3,34 @@ import PythonCode from './PythonCode'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import '../../../../style/CodeBlock.css' import copyToClipboard from '../../../utils/copyToClipboard'; +import IconButton from '../../../components/IconButton'; +import CopyIcon from '../../../icons/CopyIcon'; +import PlayButtonIcon from '../../../icons/PlayButtonIcon'; +import { CodeReviewStatus } from '../ChatTaskpane'; +import AcceptIcon from '../../../icons/AcceptIcon'; +import RejectIcon from '../../../icons/RejectIcon'; interface ICodeBlockProps { code: string, role: 'user' | 'assistant' renderMimeRegistry: IRenderMimeRegistry + previewAICode: () => void + acceptAICode: () => void + rejectAICode: () => void + isLastAiMessage: boolean + codeReviewStatus: CodeReviewStatus } const CodeBlock: React.FC = ({ code, role, renderMimeRegistry, + previewAICode, + acceptAICode, + rejectAICode, + isLastAiMessage, + codeReviewStatus, }): JSX.Element => { if (role === 'user') { @@ -31,9 +47,53 @@ const CodeBlock: React.FC = ({ if (role === 'assistant') { return (
-
- -
+ <> + {/* The code block toolbar for the last AI message */} + {isLastAiMessage && +
+ {codeReviewStatus === 'chatPreview' && + } + title="Overwrite Active Cell" + onClick={() => {previewAICode()}} + /> + } + {codeReviewStatus === 'codeCellPreview' && + } + title="Accept AI Generated Code" + onClick={() => {acceptAICode()}} + style={{color: 'var(--green-700)'}} + /> + } + {codeReviewStatus === 'codeCellPreview' && + } + title="Reject AI Generated Code" + onClick={() => {rejectAICode()}} + style={{color: 'var(--red-700)'}} + /> + } + {codeReviewStatus !== 'codeCellPreview' && + } + title="Copy" + onClick={() => {copyToClipboard(code)}} + /> + } +
+ } + {/* The code block toolbar for every other AI message */} + {!isLastAiMessage && +
+ } + title="Copy" + onClick={() => {copyToClipboard(code)}} + /> +
+ } + = ({ Register the code cell toolbar buttons for accepting and rejecting code. */ app.commands.addCommand(COMMAND_MITO_AI_CELL_TOOLBAR_ACCEPT_CODE, { - label: `Accept AI edits ${operatingSystem === 'mac' ? '⌘Y' : 'Ctrl+Y'}`, - className: 'text-and-icon-button green', + label: `Accept ${operatingSystem === 'mac' ? '⌘Y' : 'Ctrl+Y'}`, + className: 'text-and-icon-button button-green small', caption: 'Accept Code', execute: () => {acceptAICode()}, // We use the cellStateBeforeDiff because it contains the code cell ID that we want to write to @@ -404,8 +404,8 @@ const ChatTaskpane: React.FC = ({ }); app.commands.addCommand(COMMAND_MITO_AI_CELL_TOOLBAR_REJECT_CODE, { - label: `Reject AI edits ${operatingSystem === 'mac' ? '⌘U' : 'Ctrl+U'}`, - className: 'text-and-icon-button red', + label: `Reject ${operatingSystem === 'mac' ? '⌘U' : 'Ctrl+U'}`, + className: 'text-and-icon-button button-red small', caption: 'Reject Code', execute: () => {rejectAICode()}, isVisible: () => { diff --git a/mito-ai/src/components/IconButton.tsx b/mito-ai/src/components/IconButton.tsx index 24fd890cd..2b460964f 100644 --- a/mito-ai/src/components/IconButton.tsx +++ b/mito-ai/src/components/IconButton.tsx @@ -1,16 +1,19 @@ import React from 'react'; import '../../style/IconButton.css'; +import '../../style/button.css'; +import { classNames } from '../utils/classNames'; interface IconButtonProps { icon: React.ReactNode; onClick: () => void; title: string; + style?: React.CSSProperties; } -const IconButton: React.FC = ({ icon, onClick, title }) => { +const IconButton: React.FC = ({ icon, onClick, title, style }) => { return ( - ) diff --git a/mito-ai/src/components/TextAndIconButton.tsx b/mito-ai/src/components/TextAndIconButton.tsx index 042fb5385..0719daa16 100644 --- a/mito-ai/src/components/TextAndIconButton.tsx +++ b/mito-ai/src/components/TextAndIconButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; import '../../style/TextAndIconButton.css'; +import '../../style/button.css'; import { ButtonProps } from './TextButton'; import { classNames } from '../utils/classNames'; @@ -11,7 +12,7 @@ interface TextAndIconButtonProps extends ButtonProps { const TextAndIconButton: React.FC = ({ text, icon: Icon, onClick, title, variant }) => { return ( - ) diff --git a/mito-ai/src/icons/AcceptIcon.tsx b/mito-ai/src/icons/AcceptIcon.tsx new file mode 100644 index 000000000..7735761b5 --- /dev/null +++ b/mito-ai/src/icons/AcceptIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AcceptIcon: React.FC = () => ( + + + + + + +); + +export default AcceptIcon; \ No newline at end of file diff --git a/mito-ai/src/icons/CopyIcon.tsx b/mito-ai/src/icons/CopyIcon.tsx index eba9efa41..f6b770134 100644 --- a/mito-ai/src/icons/CopyIcon.tsx +++ b/mito-ai/src/icons/CopyIcon.tsx @@ -1,8 +1,3 @@ - - - - - import React from 'react'; const CopyIcon: React.FC = () => ( diff --git a/mito-ai/src/icons/RejectIcon.tsx b/mito-ai/src/icons/RejectIcon.tsx new file mode 100644 index 000000000..e77027170 --- /dev/null +++ b/mito-ai/src/icons/RejectIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const RejectIcon: React.FC = () => ( + + + + + + +); + +export default RejectIcon; \ No newline at end of file diff --git a/mito-ai/style/ChatTaskpane.css b/mito-ai/style/ChatTaskpane.css index 77b54f2c2..5cb25947b 100644 --- a/mito-ai/style/ChatTaskpane.css +++ b/mito-ai/style/ChatTaskpane.css @@ -147,23 +147,6 @@ } .code-block-toolbar button { + height: 16px; font-size: 12px !important; -} - -.code-block-accept-button { - background-color: var(--green-600) !important; - color: white !important; -} - -.code-block-accept-button:hover { - background-color: var(--green-700) !important; -} - -.code-block-deny-button { - background-color: var(--red-600) !important; - color: white !important; -} - -.code-block-deny-button:hover { - background-color: var(--red-700) !important; -} +} \ No newline at end of file diff --git a/mito-ai/style/IconButton.css b/mito-ai/style/IconButton.css index 47fa61fe7..9a7a683ed 100644 --- a/mito-ai/style/IconButton.css +++ b/mito-ai/style/IconButton.css @@ -1,4 +1,7 @@ .icon-button { + display: flex; + align-items: center; + justify-content: center; background-color: transparent; border: none; cursor: pointer; diff --git a/mito-ai/style/button.css b/mito-ai/style/button.css new file mode 100644 index 000000000..e650a6425 --- /dev/null +++ b/mito-ai/style/button.css @@ -0,0 +1,32 @@ +/* + Classes that can be used for any button, making it easier to keep + the theme consistent. For example, use them to make a textButton or + textAndIconButton green. +*/ + +.button-green { + background-color: var(--green-400); + color: var(--green-900) !important; +} + +.button-green:hover { + background-color: var(--green-500); +} + +.button-red { + background-color: var(--red-400); + color: var(--red-900) !important; +} + +.button-red:hover { + background-color: var(--red-500); +} + +.button-gray { + background-color: var(--jp-layout-color2); + color: var(--jp-content-font-color1) !important; +} + +.button-gray:hover { + background-color: var(--jp-layout-color3); +} \ No newline at end of file diff --git a/tests/mitoai_ui_tests/utils.ts b/tests/mitoai_ui_tests/utils.ts index 3a454b41b..be2d91d1f 100644 --- a/tests/mitoai_ui_tests/utils.ts +++ b/tests/mitoai_ui_tests/utils.ts @@ -67,7 +67,7 @@ export const editMitoAIMessage = async ( } export const clickPreviewButton = async (page: IJupyterLabPageFixture) => { - await page.getByRole('button', { name: 'Overwrite Active Cell' }).click(); + await page.locator('.chat-message-buttons').getByRole('button', { name: 'Overwrite Active Cell' }).click(); await waitForIdle(page); } @@ -78,9 +78,9 @@ export const clickAcceptButton = async ( { useCellToolbar = false }: { useCellToolbar?: boolean } = {useCellToolbar: false} ) => { if (useCellToolbar) { - await page.getByText('Accept AI edits').click(); + await page.locator('.jp-ToolbarButtonComponent-label').filter({ hasText: 'Accept' }).click(); } else { - await page.locator('.chat-taskpane').getByRole('button', { name: 'Accept code' }).click(); + await page.locator('.chat-message-buttons').getByRole('button', { name: 'Accept code' }).click(); } await waitForIdle(page); } @@ -92,9 +92,9 @@ export const clickDenyButton = async ( { useCellToolbar = false }: { useCellToolbar?: boolean } = {useCellToolbar: false} ) => { if (useCellToolbar) { - await page.getByText('Reject AI edits').click(); + await page.locator('.jp-ToolbarButtonComponent-label').filter({ hasText: 'Reject' }).click(); } else { - await page.locator('.chat-taskpane').getByRole('button', { name: 'Reject code' }).click(); + await page.locator('.chat-message-buttons').getByRole('button', { name: 'Reject code' }).click(); } await waitForIdle(page); }