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

Add code toolbar to Jupyter AI chat #789

Merged
merged 5 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
95 changes: 53 additions & 42 deletions packages/jupyter-ai/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import type { Awareness } from 'y-protocols/awareness';
import type { IThemeManager } from '@jupyterlab/apputils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';

import { JlThemeProvider } from './jl-theme-provider';
import { ChatMessages } from './chat-messages';
Expand All @@ -19,7 +20,10 @@ import { SelectionWatcher } from '../selection-watcher';
import { ChatHandler } from '../chat_handler';
import { CollaboratorsContextProvider } from '../contexts/collaborators-context';
import { IJaiCompletionProvider } from '../tokens';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import {
ActiveCellContextProvider,
ActiveCellManager
} from '../contexts/active-cell-context';
import { ScrollContainer } from './scroll-container';

type ChatBodyProps = {
Expand Down Expand Up @@ -188,6 +192,7 @@ export type ChatProps = {
chatView?: ChatView;
completionProvider: IJaiCompletionProvider | null;
openInlineCompleterSettings: () => void;
activeCellManager: ActiveCellManager;
};

enum ChatView {
Expand All @@ -202,51 +207,57 @@ export function Chat(props: ChatProps): JSX.Element {
<JlThemeProvider themeManager={props.themeManager}>
<SelectionContextProvider selectionWatcher={props.selectionWatcher}>
<CollaboratorsContextProvider globalAwareness={props.globalAwareness}>
<Box
// root box should not include padding as it offsets the vertical
// scrollbar to the left
sx={{
width: '100%',
height: '100%',
boxSizing: 'border-box',
background: 'var(--jp-layout-color0)',
display: 'flex',
flexDirection: 'column'
}}
<ActiveCellContextProvider
activeCellManager={props.activeCellManager}
>
{/* top bar */}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
{view !== ChatView.Chat ? (
<IconButton onClick={() => setView(ChatView.Chat)}>
<ArrowBackIcon />
</IconButton>
) : (
<Box />
<Box
// root box should not include padding as it offsets the vertical
// scrollbar to the left
sx={{
width: '100%',
height: '100%',
boxSizing: 'border-box',
background: 'var(--jp-layout-color0)',
display: 'flex',
flexDirection: 'column'
}}
>
{/* top bar */}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
{view !== ChatView.Chat ? (
<IconButton onClick={() => setView(ChatView.Chat)}>
<ArrowBackIcon />
</IconButton>
) : (
<Box />
)}
{view === ChatView.Chat ? (
<IconButton onClick={() => setView(ChatView.Settings)}>
<SettingsIcon />
</IconButton>
) : (
<Box />
)}
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
</Box>
{/* body */}
{view === ChatView.Chat && (
<ChatBody
chatHandler={props.chatHandler}
setChatView={setView}
rmRegistry={props.rmRegistry}
/>
)}
{view === ChatView.Chat ? (
<IconButton onClick={() => setView(ChatView.Settings)}>
<SettingsIcon />
</IconButton>
) : (
<Box />
{view === ChatView.Settings && (
<ChatSettings
rmRegistry={props.rmRegistry}
completionProvider={props.completionProvider}
openInlineCompleterSettings={
props.openInlineCompleterSettings
}
/>
)}
</Box>
{/* body */}
{view === ChatView.Chat && (
<ChatBody
chatHandler={props.chatHandler}
setChatView={setView}
rmRegistry={props.rmRegistry}
/>
)}
{view === ChatView.Settings && (
<ChatSettings
rmRegistry={props.rmRegistry}
completionProvider={props.completionProvider}
openInlineCompleterSettings={props.openInlineCompleterSettings}
/>
)}
</Box>
</ActiveCellContextProvider>
</CollaboratorsContextProvider>
</SelectionContextProvider>
</JlThemeProvider>
Expand Down
101 changes: 101 additions & 0 deletions packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import { Box } from '@mui/material';
import { addAboveIcon, addBelowIcon } from '@jupyterlab/ui-components';

import { CopyButton } from './copy-button';
import { replaceCellIcon } from '../../icons';

import {
ActiveCellManager,
useActiveCellContext
} from '../../contexts/active-cell-context';
import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';

export type CodeToolbarProps = {
/**
* The content of the Markdown code block this component is attached to.
*/
content: string;
};

export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
const [activeCellExists, activeCellManager] = useActiveCellContext();
const sharedToolbarButtonProps = {
content: props.content,
activeCellManager,
activeCellExists
};

return (
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '6px 2px',
marginBottom: '1em',
border: '1px solid var(--jp-cell-editor-border-color)',
borderTop: 'none'
}}
>
<InsertAboveButton {...sharedToolbarButtonProps} />
<InsertBelowButton {...sharedToolbarButtonProps} />
<ReplaceButton {...sharedToolbarButtonProps} />
<CopyButton value={props.content} />
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
</Box>
);
}

type ToolbarButtonProps = {
content: string;
activeCellExists: boolean;
activeCellManager: ActiveCellManager;
};

function InsertAboveButton(props: ToolbarButtonProps) {
const tooltip = props.activeCellExists
? 'Insert above active cell'
: 'Insert above active cell (no active cell)';

return (
<TooltippedIconButton
tooltip={tooltip}
onClick={() => props.activeCellManager.insertAbove(props.content)}
disabled={!props.activeCellExists}
>
<addAboveIcon.react height="16px" width="16px" />
</TooltippedIconButton>
);
}

function InsertBelowButton(props: ToolbarButtonProps) {
const tooltip = props.activeCellExists
? 'Insert below active cell'
: 'Insert below active cell (no active cell)';

return (
<TooltippedIconButton
tooltip={tooltip}
disabled={!props.activeCellExists}
onClick={() => props.activeCellManager.insertBelow(props.content)}
>
<addBelowIcon.react height="16px" width="16px" />
</TooltippedIconButton>
);
}

function ReplaceButton(props: ToolbarButtonProps) {
const tooltip = props.activeCellExists
? 'Replace active cell'
: 'Replace active cell (no active cell)';

return (
<TooltippedIconButton
tooltip={tooltip}
disabled={!props.activeCellExists}
onClick={() => props.activeCellManager.replace(props.content)}
>
<replaceCellIcon.react height="16px" width="16px" />
</TooltippedIconButton>
);
}
58 changes: 58 additions & 0 deletions packages/jupyter-ai/src/components/code-blocks/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState, useCallback, useRef } from 'react';

import { copyIcon } from '@jupyterlab/ui-components';

import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';

enum CopyStatus {
None,
Copying,
Copied
}

const COPYBTN_TEXT_BY_STATUS: Record<CopyStatus, string> = {
[CopyStatus.None]: 'Copy to Clipboard',
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
[CopyStatus.Copying]: 'Copying...',
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
[CopyStatus.Copied]: 'Copied!'
};

type CopyButtonProps = {
value: string;
};

export function CopyButton(props: CopyButtonProps): JSX.Element {
const [copyStatus, setCopyStatus] = useState<CopyStatus>(CopyStatus.None);
const timeoutId = useRef<number | null>(null);

const copy = useCallback(async () => {
// ignore if we are already copying
if (copyStatus === CopyStatus.Copying) {
return;
}

try {
await navigator.clipboard.writeText(props.value);
} catch (err) {
console.error('Failed to copy text: ', err);
setCopyStatus(CopyStatus.None);
return;
}

setCopyStatus(CopyStatus.Copied);
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = setTimeout(() => setCopyStatus(CopyStatus.None), 1000);
}, [copyStatus, props.value]);

return (
<TooltippedIconButton
tooltip={COPYBTN_TEXT_BY_STATUS[copyStatus]}
placement="top"
onClick={copy}
aria-label="Copy to Clipboard"
dlqqq marked this conversation as resolved.
Show resolved Hide resolved
>
<copyIcon.react height="16px" width="16px" />
</TooltippedIconButton>
);
}
50 changes: 0 additions & 50 deletions packages/jupyter-ai/src/components/copy-button.tsx

This file was deleted.

Loading
Loading