@jupyter/chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/lib/__tests__/model.spec.d.ts +1 -0
  2. package/lib/__tests__/model.spec.js +72 -0
  3. package/lib/__tests__/widgets.spec.d.ts +1 -0
  4. package/lib/__tests__/widgets.spec.js +33 -0
  5. package/lib/components/chat-input.d.ts +33 -0
  6. package/lib/components/chat-input.js +60 -0
  7. package/lib/components/chat-messages.d.ts +32 -0
  8. package/lib/components/chat-messages.js +162 -0
  9. package/lib/components/chat.d.ts +43 -0
  10. package/lib/components/chat.js +100 -0
  11. package/lib/components/copy-button.d.ts +6 -0
  12. package/lib/components/copy-button.js +35 -0
  13. package/lib/components/jl-theme-provider.d.ts +6 -0
  14. package/lib/components/jl-theme-provider.js +19 -0
  15. package/lib/components/mui-extras/stacking-alert.d.ts +28 -0
  16. package/lib/components/mui-extras/stacking-alert.js +56 -0
  17. package/lib/components/rendermime-markdown.d.ts +12 -0
  18. package/lib/components/rendermime-markdown.js +54 -0
  19. package/lib/components/scroll-container.d.ts +23 -0
  20. package/lib/components/scroll-container.js +51 -0
  21. package/lib/components/toolbar.d.ts +11 -0
  22. package/lib/components/toolbar.js +30 -0
  23. package/lib/icons.d.ts +2 -0
  24. package/lib/icons.js +11 -0
  25. package/lib/index.d.ts +6 -0
  26. package/lib/index.js +10 -0
  27. package/lib/model.d.ts +177 -0
  28. package/lib/model.js +128 -0
  29. package/lib/theme-provider.d.ts +3 -0
  30. package/lib/theme-provider.js +133 -0
  31. package/lib/types.d.ts +49 -0
  32. package/lib/types.js +5 -0
  33. package/lib/widgets/chat-error.d.ts +2 -0
  34. package/lib/widgets/chat-error.js +26 -0
  35. package/lib/widgets/chat-sidebar.d.ts +4 -0
  36. package/lib/widgets/chat-sidebar.js +15 -0
  37. package/lib/widgets/chat-widget.d.ts +19 -0
  38. package/lib/widgets/chat-widget.js +28 -0
  39. package/package.json +209 -0
  40. package/src/__tests__/model.spec.ts +84 -0
  41. package/src/__tests__/widgets.spec.ts +43 -0
  42. package/src/components/chat-input.tsx +143 -0
  43. package/src/components/chat-messages.tsx +283 -0
  44. package/src/components/chat.tsx +179 -0
  45. package/src/components/copy-button.tsx +55 -0
  46. package/src/components/jl-theme-provider.tsx +28 -0
  47. package/src/components/mui-extras/stacking-alert.tsx +105 -0
  48. package/src/components/rendermime-markdown.tsx +88 -0
  49. package/src/components/scroll-container.tsx +74 -0
  50. package/src/components/toolbar.tsx +50 -0
  51. package/src/icons.ts +15 -0
  52. package/src/index.ts +11 -0
  53. package/src/model.ts +272 -0
  54. package/src/theme-provider.ts +137 -0
  55. package/src/types/mui.d.ts +18 -0
  56. package/src/types/svg.d.ts +17 -0
  57. package/src/types.ts +58 -0
  58. package/src/widgets/chat-error.tsx +43 -0
  59. package/src/widgets/chat-sidebar.tsx +30 -0
  60. package/src/widgets/chat-widget.tsx +51 -0
  61. package/style/base.css +13 -0
  62. package/style/chat-settings.css +10 -0
  63. package/style/chat.css +53 -0
  64. package/style/icons/chat.svg +6 -0
  65. package/style/index.css +6 -0
  66. package/style/index.js +6 -0
@@ -0,0 +1,143 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useState } from 'react';
7
+
8
+ import {
9
+ Box,
10
+ SxProps,
11
+ TextField,
12
+ Theme,
13
+ IconButton,
14
+ InputAdornment
15
+ } from '@mui/material';
16
+ import { Send, Cancel } from '@mui/icons-material';
17
+ import clsx from 'clsx';
18
+
19
+ const INPUT_BOX_CLASS = 'jp-chat-input-container';
20
+ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
21
+ const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
22
+
23
+ export function ChatInput(props: ChatInput.IProps): JSX.Element {
24
+ const [input, setInput] = useState(props.value || '');
25
+
26
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
27
+ if (
28
+ event.key === 'Enter' &&
29
+ ((props.sendWithShiftEnter && event.shiftKey) ||
30
+ (!props.sendWithShiftEnter && !event.shiftKey))
31
+ ) {
32
+ onSend();
33
+ event.stopPropagation();
34
+ event.preventDefault();
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Triggered when sending the message.
40
+ */
41
+ function onSend() {
42
+ setInput('');
43
+ props.onSend(input);
44
+ }
45
+
46
+ /**
47
+ * Triggered when cancelling edition.
48
+ */
49
+ function onCancel() {
50
+ setInput(props.value || '');
51
+ props.onCancel!();
52
+ }
53
+
54
+ // Set the helper text based on whether Shift+Enter is used for sending.
55
+ const helperText = props.sendWithShiftEnter ? (
56
+ <span>
57
+ Press <b>Shift</b>+<b>Enter</b> to send message
58
+ </span>
59
+ ) : (
60
+ <span>
61
+ Press <b>Shift</b>+<b>Enter</b> to add a new line
62
+ </span>
63
+ );
64
+
65
+ return (
66
+ <Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
67
+ <Box sx={{ display: 'flex' }}>
68
+ <TextField
69
+ value={input}
70
+ onChange={e => setInput(e.target.value)}
71
+ fullWidth
72
+ variant="outlined"
73
+ multiline
74
+ onKeyDown={handleKeyDown}
75
+ placeholder="Start chatting"
76
+ InputProps={{
77
+ endAdornment: (
78
+ <InputAdornment position="end">
79
+ {props.onCancel && (
80
+ <IconButton
81
+ size="small"
82
+ color="primary"
83
+ onClick={onCancel}
84
+ disabled={!input.trim().length}
85
+ title={'Cancel edition'}
86
+ className={clsx(CANCEL_BUTTON_CLASS)}
87
+ >
88
+ <Cancel />
89
+ </IconButton>
90
+ )}
91
+ <IconButton
92
+ size="small"
93
+ color="primary"
94
+ onClick={onSend}
95
+ disabled={!input.trim().length}
96
+ title={`Send message ${props.sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
97
+ className={clsx(SEND_BUTTON_CLASS)}
98
+ >
99
+ <Send />
100
+ </IconButton>
101
+ </InputAdornment>
102
+ )
103
+ }}
104
+ FormHelperTextProps={{
105
+ sx: { marginLeft: 'auto', marginRight: 0 }
106
+ }}
107
+ helperText={input.length > 2 ? helperText : ' '}
108
+ />
109
+ </Box>
110
+ </Box>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * The chat input namespace.
116
+ */
117
+ export namespace ChatInput {
118
+ /**
119
+ * The properties of the react element.
120
+ */
121
+ export interface IProps {
122
+ /**
123
+ * The initial value of the input (default to '')
124
+ */
125
+ value?: string;
126
+ /**
127
+ * The function to be called to send the message.
128
+ */
129
+ onSend: (input: string) => unknown;
130
+ /**
131
+ * The function to be called to cancel editing.
132
+ */
133
+ onCancel?: () => unknown;
134
+ /**
135
+ * Whether using shift+enter to send the message.
136
+ */
137
+ sendWithShiftEnter: boolean;
138
+ /**
139
+ * Custom mui/material styles.
140
+ */
141
+ sx?: SxProps<Theme>;
142
+ }
143
+ }
@@ -0,0 +1,283 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
+ import { Avatar, Box, Typography } from '@mui/material';
8
+ import type { SxProps, Theme } from '@mui/material';
9
+ import clsx from 'clsx';
10
+ import React, { useState, useEffect } from 'react';
11
+
12
+ import { ChatInput } from './chat-input';
13
+ import { RendermimeMarkdown } from './rendermime-markdown';
14
+ import { IChatModel } from '../model';
15
+ import { IChatMessage, IUser } from '../types';
16
+
17
+ const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
18
+ const MESSAGE_CLASS = 'jp-chat-message';
19
+ const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
20
+ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
21
+
22
+ type BaseMessageProps = {
23
+ rmRegistry: IRenderMimeRegistry;
24
+ model: IChatModel;
25
+ };
26
+
27
+ type ChatMessageProps = BaseMessageProps & {
28
+ message: IChatMessage;
29
+ };
30
+
31
+ type ChatMessagesProps = BaseMessageProps & {
32
+ messages: IChatMessage[];
33
+ };
34
+
35
+ export type ChatMessageHeaderProps = IUser & {
36
+ timestamp: number;
37
+ rawTime?: boolean;
38
+ deleted?: boolean;
39
+ edited?: boolean;
40
+ sx?: SxProps<Theme>;
41
+ };
42
+
43
+ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
44
+ const [datetime, setDatetime] = useState<Record<number, string>>({});
45
+ const sharedStyles: SxProps<Theme> = {
46
+ height: '24px',
47
+ width: '24px'
48
+ };
49
+
50
+ /**
51
+ * Effect: update cached datetime strings upon receiving a new message.
52
+ */
53
+ useEffect(() => {
54
+ if (!datetime[props.timestamp]) {
55
+ const newDatetime: Record<number, string> = {};
56
+ let datetime: string;
57
+ const currentDate = new Date();
58
+ const sameDay = (date: Date) =>
59
+ date.getFullYear() === currentDate.getFullYear() &&
60
+ date.getMonth() === currentDate.getMonth() &&
61
+ date.getDate() === currentDate.getDate();
62
+
63
+ const msgDate = new Date(props.timestamp * 1000); // Convert message time to milliseconds
64
+
65
+ // Display only the time if the day of the message is the current one.
66
+ if (sameDay(msgDate)) {
67
+ // Use the browser's default locale
68
+ datetime = msgDate.toLocaleTimeString([], {
69
+ hour: 'numeric',
70
+ minute: '2-digit'
71
+ });
72
+ } else {
73
+ // Use the browser's default locale
74
+ datetime = msgDate.toLocaleString([], {
75
+ day: 'numeric',
76
+ month: 'numeric',
77
+ year: 'numeric',
78
+ hour: 'numeric',
79
+ minute: '2-digit'
80
+ });
81
+ }
82
+ newDatetime[props.timestamp] = datetime;
83
+ setDatetime(newDatetime);
84
+ }
85
+ });
86
+
87
+ const bgcolor = props.color;
88
+ const avatar = props.avatar_url ? (
89
+ <Avatar
90
+ sx={{
91
+ ...sharedStyles,
92
+ ...(bgcolor && { bgcolor })
93
+ }}
94
+ src={props.avatar_url}
95
+ ></Avatar>
96
+ ) : props.initials ? (
97
+ <Avatar
98
+ sx={{
99
+ ...sharedStyles,
100
+ ...(bgcolor && { bgcolor })
101
+ }}
102
+ >
103
+ <Typography
104
+ sx={{
105
+ fontSize: 'var(--jp-ui-font-size1)',
106
+ color: 'var(--jp-ui-inverse-font-color1)'
107
+ }}
108
+ >
109
+ {props.initials}
110
+ </Typography>
111
+ </Avatar>
112
+ ) : null;
113
+
114
+ const name =
115
+ props.display_name ?? props.name ?? (props.username || 'User undefined');
116
+
117
+ return (
118
+ <Box
119
+ className={MESSAGE_HEADER_CLASS}
120
+ sx={{
121
+ display: 'flex',
122
+ alignItems: 'center',
123
+ '& > :not(:last-child)': {
124
+ marginRight: 3
125
+ },
126
+ ...props.sx
127
+ }}
128
+ >
129
+ {avatar}
130
+ <Box
131
+ sx={{
132
+ display: 'flex',
133
+ flexGrow: 1,
134
+ flexWrap: 'wrap',
135
+ justifyContent: 'space-between',
136
+ alignItems: 'center'
137
+ }}
138
+ >
139
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
140
+ <Typography
141
+ sx={{ fontWeight: 700, color: 'var(--jp-ui-font-color1)' }}
142
+ >
143
+ {name}
144
+ </Typography>
145
+ {(props.deleted || props.edited) && (
146
+ <Typography
147
+ sx={{
148
+ fontStyle: 'italic',
149
+ fontSize: 'var(--jp-content-font-size0)',
150
+ paddingLeft: '0.5em'
151
+ }}
152
+ >
153
+ {props.deleted ? '(message deleted)' : '(edited)'}
154
+ </Typography>
155
+ )}
156
+ </Box>
157
+ <Typography
158
+ className={MESSAGE_TIME_CLASS}
159
+ sx={{
160
+ fontSize: '0.8em',
161
+ color: 'var(--jp-ui-font-color2)',
162
+ fontWeight: 300
163
+ }}
164
+ title={props.rawTime ? 'Unverified time' : ''}
165
+ >
166
+ {`${datetime[props.timestamp]}${props.rawTime ? '*' : ''}`}
167
+ </Typography>
168
+ </Box>
169
+ </Box>
170
+ );
171
+ }
172
+
173
+ /**
174
+ * The messages list UI.
175
+ */
176
+ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
177
+ return (
178
+ <Box
179
+ sx={{
180
+ '& > :not(:last-child)': {
181
+ borderBottom: '1px solid var(--jp-border-color2)'
182
+ }
183
+ }}
184
+ className={clsx(MESSAGES_BOX_CLASS)}
185
+ >
186
+ {props.messages.map((message, i) => {
187
+ let sender: IUser;
188
+ if (typeof message.sender === 'string') {
189
+ sender = { username: message.sender };
190
+ } else {
191
+ sender = message.sender;
192
+ }
193
+ return (
194
+ // extra div needed to ensure each bubble is on a new line
195
+ <Box
196
+ key={i}
197
+ sx={{ padding: '1em 1em 0 1em' }}
198
+ className={clsx(MESSAGE_CLASS)}
199
+ >
200
+ <ChatMessageHeader
201
+ {...sender}
202
+ timestamp={message.time}
203
+ rawTime={message.raw_time}
204
+ deleted={message.deleted}
205
+ edited={message.edited}
206
+ sx={{ marginBottom: 3 }}
207
+ />
208
+ <ChatMessage {...props} message={message} />
209
+ </Box>
210
+ );
211
+ })}
212
+ </Box>
213
+ );
214
+ }
215
+
216
+ /**
217
+ * the message UI.
218
+ */
219
+ export function ChatMessage(props: ChatMessageProps): JSX.Element {
220
+ const { message, model, rmRegistry } = props;
221
+ let canEdit = false;
222
+ let canDelete = false;
223
+ if (model.user !== undefined && !message.deleted) {
224
+ const username =
225
+ typeof message.sender === 'string'
226
+ ? message.sender
227
+ : message.sender.username;
228
+
229
+ if (model.user.username === username && model.updateMessage !== undefined) {
230
+ canEdit = true;
231
+ }
232
+ if (model.user.username === username && model.deleteMessage !== undefined) {
233
+ canDelete = true;
234
+ }
235
+ }
236
+ const [edit, setEdit] = useState<boolean>(false);
237
+
238
+ const cancelEdition = (): void => {
239
+ setEdit(false);
240
+ };
241
+
242
+ const updateMessage = (id: string, input: string): void => {
243
+ if (!canEdit) {
244
+ return;
245
+ }
246
+ // Update the message
247
+ const updatedMessage = { ...message };
248
+ updatedMessage.body = input;
249
+ model.updateMessage!(id, updatedMessage);
250
+ setEdit(false);
251
+ };
252
+
253
+ const deleteMessage = (id: string): void => {
254
+ if (!canDelete) {
255
+ return;
256
+ }
257
+ // Delete the message
258
+ model.deleteMessage!(id);
259
+ };
260
+
261
+ // Empty if the message has been deleted
262
+ return message.deleted ? (
263
+ <></>
264
+ ) : (
265
+ <div>
266
+ {edit && canEdit ? (
267
+ <ChatInput
268
+ value={message.body}
269
+ onSend={(input: string) => updateMessage(message.id, input)}
270
+ onCancel={() => cancelEdition()}
271
+ sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
272
+ />
273
+ ) : (
274
+ <RendermimeMarkdown
275
+ rmRegistry={rmRegistry}
276
+ markdownStr={message.body}
277
+ edit={canEdit ? () => setEdit(true) : undefined}
278
+ delete={canDelete ? () => deleteMessage(message.id) : undefined}
279
+ />
280
+ )}
281
+ </div>
282
+ );
283
+ }
@@ -0,0 +1,179 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IThemeManager } from '@jupyterlab/apputils';
7
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
9
+ import SettingsIcon from '@mui/icons-material/Settings';
10
+ import { IconButton } from '@mui/material';
11
+ import { Box } from '@mui/system';
12
+ import React, { useState, useEffect } from 'react';
13
+
14
+ import { JlThemeProvider } from './jl-theme-provider';
15
+ import { ChatMessages } from './chat-messages';
16
+ import { ChatInput } from './chat-input';
17
+ import { ScrollContainer } from './scroll-container';
18
+ import { IChatModel } from '../model';
19
+ import { IChatMessage } from '../types';
20
+
21
+ type ChatBodyProps = {
22
+ model: IChatModel;
23
+ rmRegistry: IRenderMimeRegistry;
24
+ };
25
+
26
+ function ChatBody({
27
+ model,
28
+ rmRegistry: renderMimeRegistry
29
+ }: ChatBodyProps): JSX.Element {
30
+ const [messages, setMessages] = useState<IChatMessage[]>([]);
31
+
32
+ /**
33
+ * Effect: fetch history and config on initial render
34
+ */
35
+ useEffect(() => {
36
+ async function fetchHistory() {
37
+ if (!model.getHistory) {
38
+ return;
39
+ }
40
+ model
41
+ .getHistory()
42
+ .then(history => setMessages(history.messages))
43
+ .catch(e => console.error(e));
44
+ }
45
+
46
+ fetchHistory();
47
+ }, [model]);
48
+
49
+ /**
50
+ * Effect: listen to chat messages
51
+ */
52
+ useEffect(() => {
53
+ function handleChatEvents(_: IChatModel) {
54
+ setMessages([...model.messages]);
55
+ }
56
+
57
+ model.messagesUpdated.connect(handleChatEvents);
58
+ return function cleanup() {
59
+ model.messagesUpdated.disconnect(handleChatEvents);
60
+ };
61
+ }, [model]);
62
+
63
+ // no need to append to messageGroups imperatively here. all of that is
64
+ // handled by the listeners registered in the effect hooks above.
65
+ const onSend = async (input: string) => {
66
+ // send message to backend
67
+ model.addMessage({ body: input });
68
+ };
69
+
70
+ return (
71
+ <>
72
+ <ScrollContainer sx={{ flexGrow: 1 }}>
73
+ <ChatMessages
74
+ messages={messages}
75
+ rmRegistry={renderMimeRegistry}
76
+ model={model}
77
+ />
78
+ </ScrollContainer>
79
+ <ChatInput
80
+ onSend={onSend}
81
+ sx={{
82
+ paddingLeft: 4,
83
+ paddingRight: 4,
84
+ paddingTop: 3.5,
85
+ paddingBottom: 0,
86
+ borderTop: '1px solid var(--jp-border-color1)'
87
+ }}
88
+ sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
89
+ />
90
+ </>
91
+ );
92
+ }
93
+
94
+ export function Chat(props: Chat.IOptions): JSX.Element {
95
+ const [view, setView] = useState<Chat.ChatView>(
96
+ props.chatView || Chat.ChatView.Chat
97
+ );
98
+ return (
99
+ <JlThemeProvider themeManager={props.themeManager ?? null}>
100
+ <Box
101
+ // root box should not include padding as it offsets the vertical
102
+ // scrollbar to the left
103
+ sx={{
104
+ width: '100%',
105
+ height: '100%',
106
+ boxSizing: 'border-box',
107
+ background: 'var(--jp-layout-color0)',
108
+ display: 'flex',
109
+ flexDirection: 'column'
110
+ }}
111
+ >
112
+ {/* top bar */}
113
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
114
+ {view !== Chat.ChatView.Chat ? (
115
+ <IconButton onClick={() => setView(Chat.ChatView.Chat)}>
116
+ <ArrowBackIcon />
117
+ </IconButton>
118
+ ) : (
119
+ <Box />
120
+ )}
121
+ {view === Chat.ChatView.Chat && props.settingsPanel ? (
122
+ <IconButton onClick={() => setView(Chat.ChatView.Settings)}>
123
+ <SettingsIcon />
124
+ </IconButton>
125
+ ) : (
126
+ <Box />
127
+ )}
128
+ </Box>
129
+ {/* body */}
130
+ {view === Chat.ChatView.Chat && (
131
+ <ChatBody model={props.model} rmRegistry={props.rmRegistry} />
132
+ )}
133
+ {view === Chat.ChatView.Settings && props.settingsPanel && (
134
+ <props.settingsPanel />
135
+ )}
136
+ </Box>
137
+ </JlThemeProvider>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * The chat UI namespace
143
+ */
144
+ export namespace Chat {
145
+ /**
146
+ * The options to build the Chat UI.
147
+ */
148
+ export interface IOptions {
149
+ /**
150
+ * The chat model.
151
+ */
152
+ model: IChatModel;
153
+ /**
154
+ * The rendermime registry.
155
+ */
156
+ rmRegistry: IRenderMimeRegistry;
157
+ /**
158
+ * The theme manager.
159
+ */
160
+ themeManager?: IThemeManager | null;
161
+ /**
162
+ * The view to render.
163
+ */
164
+ chatView?: ChatView;
165
+ /**
166
+ * A settings panel that can be used for dedicated settings (e.g. jupyter-ai)
167
+ */
168
+ settingsPanel?: () => JSX.Element;
169
+ }
170
+
171
+ /**
172
+ * The view to render.
173
+ * The settings view is available only if the settings panel is provided in options.
174
+ */
175
+ export enum ChatView {
176
+ Chat,
177
+ Settings
178
+ }
179
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useState, useCallback } from 'react';
7
+
8
+ import { Box, Button } from '@mui/material';
9
+
10
+ enum CopyStatus {
11
+ None,
12
+ Copied
13
+ }
14
+
15
+ const COPYBTN_TEXT_BY_STATUS: Record<CopyStatus, string> = {
16
+ [CopyStatus.None]: 'Copy to Clipboard',
17
+ [CopyStatus.Copied]: 'Copied!'
18
+ };
19
+
20
+ type CopyButtonProps = {
21
+ value: string;
22
+ };
23
+
24
+ export function CopyButton(props: CopyButtonProps): JSX.Element {
25
+ const [copyStatus, setCopyStatus] = useState<CopyStatus>(CopyStatus.None);
26
+
27
+ const copy = useCallback(async () => {
28
+ try {
29
+ await navigator.clipboard.writeText(props.value);
30
+ } catch (err) {
31
+ console.error('Failed to copy text: ', err);
32
+ setCopyStatus(CopyStatus.None);
33
+ return;
34
+ }
35
+
36
+ setCopyStatus(CopyStatus.Copied);
37
+ setTimeout(() => setCopyStatus(CopyStatus.None), 1000);
38
+ }, [props.value]);
39
+
40
+ return (
41
+ <Box sx={{ display: 'flex', flexDirection: 'column' }}>
42
+ <Button
43
+ onClick={copy}
44
+ disabled={copyStatus !== CopyStatus.None}
45
+ aria-label="Copy To Clipboard"
46
+ sx={{
47
+ alignSelf: 'flex-end',
48
+ textTransform: 'none'
49
+ }}
50
+ >
51
+ {COPYBTN_TEXT_BY_STATUS[copyStatus]}
52
+ </Button>
53
+ </Box>
54
+ );
55
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import React, { useState, useEffect } from 'react';
7
+ import type { IThemeManager } from '@jupyterlab/apputils';
8
+ import { Theme, ThemeProvider, createTheme } from '@mui/material/styles';
9
+
10
+ import { getJupyterLabTheme } from '../theme-provider';
11
+
12
+ export function JlThemeProvider(props: {
13
+ themeManager: IThemeManager | null;
14
+ children: React.ReactNode;
15
+ }): JSX.Element {
16
+ const [theme, setTheme] = useState<Theme>(createTheme());
17
+
18
+ useEffect(() => {
19
+ async function setJlTheme() {
20
+ setTheme(await getJupyterLabTheme());
21
+ }
22
+
23
+ setJlTheme();
24
+ props.themeManager?.themeChanged.connect(setJlTheme);
25
+ }, []);
26
+
27
+ return <ThemeProvider theme={theme}>{props.children}</ThemeProvider>;
28
+ }