Skip to content

Commit d5a8ef8

Browse files
ellisonbgdlqqqgithub-actions[bot]
authored
UX Improvements for Jupyter Chat Interface (#298)
* Misc UI improvements. * Add .env got .gitignore * Remove attach button from input as drag and drop now works * Improve chat styling * Clean up css. * Add TS logic to track which area the chat is in * Improve attachment visual design and functionality. * Remove chat input top padding. * Improve focus UX of chat input * Draft stop button, minor styling of chat input and hr. * Draft of new writers UI * Minor style updates * Work on UI improvements including buttons and message editing. * More work on button and tooltips. * Fix input TextField horizontal scrolling * Make sure that Enter picks item in autocomplete * Fix autocomplete behavior with Enter * Improve autocomplete styling and behavior * Simplify autocomplete styling * Allow multiple @ mentions anywhere in a chat message * Fix whitespace * Remove border on code toolbar in chat * Minor change to placeholder text in chat input * Don't render the message header for your own messages * Improve styling for chat UI * Editing of messages is broken, comment out until we can fix the behavior * Run linter * add back attach & tooltipped buttons * make attach button gray & same size as send button * fix bug where chat input is cleared on blur * fix send button not sending on click * run command providers when sending via click * add back writers element class for e2e tests * fix code toolbar e2e test selectors * remove 'send with ...' dropdown e2e tests * add back attachment component classnames used in E2E tests * remove message edit E2E tests, update message delete E2E test * add back chat command classname used in E2E tests * remove raw time tests * update input toolbar E2E test * Update Playwright Snapshots * fix delete button selector * remove other user editing E2E test * fix typing indicator regex * fix multiple user typing regex * expect writing indicator element to always be attached --------- Co-authored-by: David L. Qiu <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 37205de commit d5a8ef8

37 files changed

+886
-1054
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,6 @@ Untitled*.ipynb
135135

136136
# Chat files
137137
*.chat
138+
139+
# Ignore secrets in '.env'
140+
.env

packages/jupyter-chat/src/components/attachments.tsx

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
*/
55

66
import CloseIcon from '@mui/icons-material/Close';
7-
import { Box } from '@mui/material';
7+
import { Box, Button, Tooltip } from '@mui/material';
88
import React, { useContext } from 'react';
99
import { PathExt } from '@jupyterlab/coreutils';
1010
import { UUID } from '@lumino/coreutils';
1111

12-
import { TooltippedButton } from './mui-extras/tooltipped-button';
1312
import { IAttachment } from '../types';
1413
import { AttachmentOpenerContext } from '../context';
1514

16-
const ATTACHMENTS_CLASS = 'jp-chat-attachments';
1715
const ATTACHMENT_CLASS = 'jp-chat-attachment';
1816
const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
1917
const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
@@ -57,7 +55,15 @@ export type AttachmentsProps = {
5755
*/
5856
export function AttachmentPreviewList(props: AttachmentsProps): JSX.Element {
5957
return (
60-
<Box className={ATTACHMENTS_CLASS}>
58+
<Box
59+
sx={{
60+
display: 'flex',
61+
flexWrap: 'wrap',
62+
gap: 1,
63+
rowGap: 1,
64+
columnGap: 2
65+
}}
66+
>
6167
{props.attachments.map(attachment => (
6268
<AttachmentPreview
6369
key={`${PathExt.basename(attachment.value)}-${UUID.uuid4()}`}
@@ -82,40 +88,71 @@ export type AttachmentProps = AttachmentsProps & {
8288
export function AttachmentPreview(props: AttachmentProps): JSX.Element {
8389
const remove_tooltip = 'Remove attachment';
8490
const attachmentOpenerRegistry = useContext(AttachmentOpenerContext);
91+
const isClickable = !!attachmentOpenerRegistry?.get(props.attachment.type);
8592

8693
return (
87-
<Box className={ATTACHMENT_CLASS}>
88-
<span
89-
className={
90-
attachmentOpenerRegistry?.get(props.attachment.type)
91-
? ATTACHMENT_CLICKABLE_CLASS
92-
: ''
93-
}
94-
onClick={() =>
95-
attachmentOpenerRegistry?.get(props.attachment.type)?.(
96-
props.attachment
97-
)
98-
}
99-
>
100-
{getAttachmentDisplayName(props.attachment)}
101-
</span>
102-
{props.onRemove && (
103-
<TooltippedButton
104-
onClick={() => props.onRemove!(props.attachment)}
105-
tooltip={remove_tooltip}
106-
buttonProps={{
107-
size: 'small',
108-
title: remove_tooltip,
109-
className: REMOVE_BUTTON_CLASS
110-
}}
94+
<Box
95+
className={ATTACHMENT_CLASS}
96+
sx={{
97+
border: '1px solid var(--jp-border-color1)',
98+
borderRadius: '2px',
99+
px: 1,
100+
py: 0.5,
101+
backgroundColor: 'var(--jp-layout-color2)',
102+
display: 'flex',
103+
alignItems: 'center',
104+
gap: 0.5,
105+
fontSize: '0.8125rem'
106+
}}
107+
>
108+
<Tooltip title={props.attachment.value} placement="top" arrow>
109+
<Box
110+
className={
111+
attachmentOpenerRegistry?.get(props.attachment.type)
112+
? ATTACHMENT_CLICKABLE_CLASS
113+
: ''
114+
}
115+
component="span"
116+
onClick={() =>
117+
attachmentOpenerRegistry?.get(props.attachment.type)?.(
118+
props.attachment
119+
)
120+
}
111121
sx={{
112-
minWidth: 'unset',
113-
padding: '0',
114-
color: 'inherit'
122+
cursor: isClickable ? 'pointer' : 'default',
123+
'&:hover': isClickable
124+
? {
125+
textDecoration: 'underline'
126+
}
127+
: {}
115128
}}
116129
>
117-
<CloseIcon />
118-
</TooltippedButton>
130+
{getAttachmentDisplayName(props.attachment)}
131+
</Box>
132+
</Tooltip>
133+
{props.onRemove && (
134+
<Tooltip title={remove_tooltip} placement="top" arrow>
135+
<span>
136+
<Button
137+
onClick={() => props.onRemove!(props.attachment)}
138+
size="small"
139+
className={REMOVE_BUTTON_CLASS}
140+
aria-label={remove_tooltip}
141+
sx={{
142+
minWidth: 'unset',
143+
padding: 0,
144+
lineHeight: 0,
145+
color: 'var(--jp-ui-font-color2)',
146+
'&:hover': {
147+
color: 'var(--jp-ui-font-color0)',
148+
backgroundColor: 'transparent'
149+
}
150+
}}
151+
>
152+
<CloseIcon fontSize="small" />
153+
</Button>
154+
</span>
155+
</Tooltip>
119156
)}
120157
</Box>
121158
);

packages/jupyter-chat/src/components/chat.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ import {
2525
IChatCommandRegistry,
2626
IMessageFooterRegistry
2727
} from '../registers';
28+
import { ChatArea } from '../types';
2829

2930
export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
3031
const { model } = props;
3132
let { inputToolbarRegistry } = props;
3233
if (!inputToolbarRegistry) {
3334
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
3435
}
36+
// const horizontalPadding = props.area === 'main' ? 8 : 4;
37+
const horizontalPadding = 4;
3538

3639
return (
3740
<AttachmentOpenerContext.Provider value={props.attachmentOpenerRegistry}>
@@ -42,18 +45,20 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
4245
inputToolbarRegistry={inputToolbarRegistry}
4346
messageFooterRegistry={props.messageFooterRegistry}
4447
welcomeMessage={props.welcomeMessage}
48+
area={props.area}
4549
/>
4650
<ChatInput
4751
sx={{
48-
paddingLeft: 4,
49-
paddingRight: 4,
52+
paddingLeft: horizontalPadding,
53+
paddingRight: horizontalPadding,
5054
paddingTop: 0,
51-
paddingBottom: 0,
52-
borderTop: '1px solid var(--jp-border-color1)'
55+
paddingBottom: 0
5356
}}
5457
model={model.input}
5558
chatCommandRegistry={props.chatCommandRegistry}
5659
toolbarRegistry={inputToolbarRegistry}
60+
area={props.area}
61+
chatModel={model}
5762
/>
5863
</AttachmentOpenerContext.Provider>
5964
);
@@ -141,6 +146,10 @@ export namespace Chat {
141146
* The welcome message.
142147
*/
143148
welcomeMessage?: string;
149+
/**
150+
* The area where the chat is displayed.
151+
*/
152+
area?: ChatArea;
144153
}
145154

146155
/**

packages/jupyter-chat/src/components/code-blocks/code-toolbar.tsx

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
*/
55

66
import { addAboveIcon, addBelowIcon } from '@jupyterlab/ui-components';
7-
import { Box } from '@mui/material';
7+
import { Box, IconButton, Tooltip } from '@mui/material';
88
import React, { useEffect, useState } from 'react';
99

1010
import { CopyButton } from './copy-button';
11-
import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
1211
import { IActiveCellManager } from '../../active-cell-manager';
1312
import { replaceCellIcon } from '../../icons';
1413
import { IChatModel } from '../../model';
@@ -82,8 +81,7 @@ export function CodeToolbar(props: CodeToolbarProps): JSX.Element {
8281
alignItems: 'center',
8382
padding: '2px 2px',
8483
marginBottom: '1em',
85-
border: '1px solid var(--jp-cell-editor-border-color)',
86-
borderTop: 'none'
84+
border: 'none'
8785
}}
8886
className={CODE_TOOLBAR_CLASS}
8987
>
@@ -116,14 +114,24 @@ function InsertAboveButton(props: ToolbarButtonProps) {
116114
: 'Insert above active cell (no active cell)';
117115

118116
return (
119-
<TooltippedIconButton
120-
className={props.className}
121-
tooltip={tooltip}
122-
onClick={() => props.activeCellManager?.insertAbove(props.content)}
123-
disabled={!props.activeCellAvailable}
124-
>
125-
<addAboveIcon.react height="16px" width="16px" />
126-
</TooltippedIconButton>
117+
<Tooltip title={tooltip} placement="top" arrow>
118+
<span>
119+
<IconButton
120+
className={props.className}
121+
onClick={() => props.activeCellManager?.insertAbove(props.content)}
122+
disabled={!props.activeCellAvailable}
123+
aria-label={tooltip}
124+
sx={{
125+
lineHeight: 0,
126+
'&.Mui-disabled': {
127+
opacity: 0.5
128+
}
129+
}}
130+
>
131+
<addAboveIcon.react height="16px" width="16px" />
132+
</IconButton>
133+
</span>
134+
</Tooltip>
127135
);
128136
}
129137

@@ -133,14 +141,24 @@ function InsertBelowButton(props: ToolbarButtonProps) {
133141
: 'Insert below active cell (no active cell)';
134142

135143
return (
136-
<TooltippedIconButton
137-
className={props.className}
138-
tooltip={tooltip}
139-
disabled={!props.activeCellAvailable}
140-
onClick={() => props.activeCellManager?.insertBelow(props.content)}
141-
>
142-
<addBelowIcon.react height="16px" width="16px" />
143-
</TooltippedIconButton>
144+
<Tooltip title={tooltip} placement="top" arrow>
145+
<span>
146+
<IconButton
147+
className={props.className}
148+
disabled={!props.activeCellAvailable}
149+
onClick={() => props.activeCellManager?.insertBelow(props.content)}
150+
aria-label={tooltip}
151+
sx={{
152+
lineHeight: 0,
153+
'&.Mui-disabled': {
154+
opacity: 0.5
155+
}
156+
}}
157+
>
158+
<addBelowIcon.react height="16px" width="16px" />
159+
</IconButton>
160+
</span>
161+
</Tooltip>
144162
);
145163
}
146164

@@ -169,13 +187,23 @@ function ReplaceButton(props: ToolbarButtonProps) {
169187
};
170188

171189
return (
172-
<TooltippedIconButton
173-
className={props.className}
174-
tooltip={tooltip}
175-
disabled={disabled}
176-
onClick={replace}
177-
>
178-
<replaceCellIcon.react height="16px" width="16px" />
179-
</TooltippedIconButton>
190+
<Tooltip title={tooltip} placement="top" arrow>
191+
<span>
192+
<IconButton
193+
className={props.className}
194+
disabled={disabled}
195+
onClick={replace}
196+
aria-label={tooltip}
197+
sx={{
198+
lineHeight: 0,
199+
'&.Mui-disabled': {
200+
opacity: 0.5
201+
}
202+
}}
203+
>
204+
<replaceCellIcon.react height="16px" width="16px" />
205+
</IconButton>
206+
</span>
207+
</Tooltip>
180208
);
181209
}

packages/jupyter-chat/src/components/code-blocks/copy-button.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import React, { useState, useCallback, useRef } from 'react';
77

88
import { copyIcon } from '@jupyterlab/ui-components';
9-
10-
import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button';
9+
import { IconButton, Tooltip } from '@mui/material';
1110

1211
enum CopyStatus {
1312
None,
@@ -59,16 +58,26 @@ export function CopyButton(props: CopyButtonProps): JSX.Element {
5958
);
6059
}, [copyStatus, props.value]);
6160

61+
const tooltip = COPYBTN_TEXT_BY_STATUS[copyStatus];
62+
6263
return (
63-
<TooltippedIconButton
64-
disabled={isCopyDisabled}
65-
className={props.className}
66-
tooltip={COPYBTN_TEXT_BY_STATUS[copyStatus]}
67-
placement="top"
68-
onClick={copy}
69-
aria-label="Copy to clipboard"
70-
>
71-
<copyIcon.react height="16px" width="16px" />
72-
</TooltippedIconButton>
64+
<Tooltip title={tooltip} placement="top" arrow>
65+
<span>
66+
<IconButton
67+
disabled={isCopyDisabled}
68+
className={props.className}
69+
onClick={copy}
70+
aria-label="Copy to clipboard"
71+
sx={{
72+
lineHeight: 0,
73+
'&.Mui-disabled': {
74+
opacity: 0.5
75+
}
76+
}}
77+
>
78+
<copyIcon.react height="16px" width="16px" />
79+
</IconButton>
80+
</span>
81+
</Tooltip>
7382
);
7483
}

packages/jupyter-chat/src/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@ export * from './code-blocks';
99
export * from './input';
1010
export * from './jl-theme-provider';
1111
export * from './messages';
12-
export * from './mui-extras';
1312
export * from './scroll-container';

packages/jupyter-chat/src/components/input/buttons/attach-button.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ export function AttachButton(
5252
tooltip={tooltip}
5353
buttonProps={{
5454
size: 'small',
55-
variant: 'contained',
55+
variant: 'text',
5656
title: tooltip,
5757
className: ATTACH_BUTTON_CLASS
5858
}}
59+
sx={{
60+
width: '24px',
61+
height: '24px',
62+
minWidth: '24px',
63+
color: 'gray'
64+
}}
5965
>
60-
<AttachFileIcon />
66+
<AttachFileIcon sx={{ fontSize: '16px ' }} />
6167
</TooltippedButton>
6268
);
6369
}

0 commit comments

Comments
 (0)