Skip to content

Commit f5dc9bc

Browse files
brichetdlqqq
andauthored
Implement message attachments (#148)
* Add attachments in the chat model and the ychat * Add a first UI to attach file from jupyter-chat * Ability to add attachements from jupyterlab-chat extension input * Display attachments with the messages * Allow to send a message with only attachment, and open attachments file on click * Rename attachments component according to review suggestion * Emit signal if attachmenets list changed Co-authored-by: David L. Qiu <[email protected]> * Use await to select files Co-authored-by: David L. Qiu <[email protected]> * Rename updateAttachments() to clearAttachments() * Add attachment opener registry, with a default opener for file * Add tests on attachments * Remove a left over method * Add the attachments in the lite deployment * Some style on the attachments * Documentation on attachments * update snapshot * Apply renaming suggestion * Emphasis changed lines in documentation examples * Skipping the notification test, to be restored in follow up PR --------- Co-authored-by: David L. Qiu <[email protected]> Co-authored-by: David L. Qiu <[email protected]>
1 parent 988f1f3 commit f5dc9bc

File tree

31 files changed

+988
-55
lines changed

31 files changed

+988
-55
lines changed

docs/jupyter-chat-example/src/index.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
import {
77
ActiveCellManager,
8+
AttachmentOpenerRegistry,
89
buildChatSidebar,
910
ChatModel,
11+
IAttachment,
1012
IChatMessage,
1113
INewMessage,
1214
SelectionWatcher
@@ -16,6 +18,7 @@ import {
1618
JupyterFrontEndPlugin
1719
} from '@jupyterlab/application';
1820
import { IThemeManager } from '@jupyterlab/apputils';
21+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
1922
import { INotebookTracker } from '@jupyterlab/notebook';
2023
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2124
import { ISettingRegistry } from '@jupyterlab/settingregistry';
@@ -25,15 +28,16 @@ class MyChatModel extends ChatModel {
2528
sendMessage(
2629
newMessage: INewMessage
2730
): Promise<boolean | void> | boolean | void {
28-
console.log(`New Message:\n${newMessage.body}`);
2931
const message: IChatMessage = {
3032
body: newMessage.body,
3133
id: newMessage.id ?? UUID.uuid4(),
3234
type: 'msg',
3335
time: Date.now() / 1000,
34-
sender: { username: 'me' }
36+
sender: { username: 'me' },
37+
attachments: this.inputAttachments
3538
};
3639
this.messageAdded(message);
40+
this.clearAttachments();
3741
}
3842
}
3943

@@ -45,10 +49,16 @@ const plugin: JupyterFrontEndPlugin<void> = {
4549
description: 'The chat panel widget.',
4650
autoStart: true,
4751
requires: [IRenderMimeRegistry],
48-
optional: [INotebookTracker, ISettingRegistry, IThemeManager],
52+
optional: [
53+
IDefaultFileBrowser,
54+
INotebookTracker,
55+
ISettingRegistry,
56+
IThemeManager
57+
],
4958
activate: (
5059
app: JupyterFrontEnd,
5160
rmRegistry: IRenderMimeRegistry,
61+
filebrowser: IDefaultFileBrowser | null,
5262
notebookTracker: INotebookTracker | null,
5363
settingRegistry: ISettingRegistry | null,
5464
themeManager: IThemeManager | null
@@ -102,7 +112,19 @@ const plugin: JupyterFrontEndPlugin<void> = {
102112
});
103113
}
104114

105-
const panel = buildChatSidebar({ model, rmRegistry, themeManager });
115+
// Create the attachment opener registry.
116+
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
117+
attachmentOpenerRegistry.set('file', (attachment: IAttachment) => {
118+
app.commands.execute('docmanager:open', { path: attachment.value });
119+
});
120+
121+
const panel = buildChatSidebar({
122+
model,
123+
rmRegistry,
124+
themeManager,
125+
documentManager: filebrowser?.model.manager,
126+
attachmentOpenerRegistry
127+
});
106128
app.shell.add(panel, 'left');
107129
}
108130
};

docs/source/README.ipynb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@
9191
" <img src=\"https://raw.githubusercontent.com/jupyterlab/jupyter-chat/c310f16bf2c784bcaed5ca0e3cd1e3996d812ec7/packages/jupyter-chat/style/icons/replace-cell.svg\" width=\"24px\" title=\"code toolbar cell replace\">"
9292
]
9393
},
94+
{
95+
"cell_type": "markdown",
96+
"id": "9cd5f2a6-25d1-4b5c-b774-1527d724c829",
97+
"metadata": {},
98+
"source": [
99+
"## Attachments"
100+
]
101+
},
102+
{
103+
"cell_type": "markdown",
104+
"id": "93642e43-c4da-4260-84b8-d6fdab556e8a",
105+
"metadata": {},
106+
"source": [
107+
"Files can be attached to the messages using the clip icon next to the send icon in the input.\n",
108+
"It opens a Dialog allowing to select and atach files.\n",
109+
"\n",
110+
"Attachments can then be opened by clicking on the preview icon."
111+
]
112+
},
94113
{
95114
"cell_type": "markdown",
96115
"id": "40240824-8b1e-4da7-bfd7-40e87ff47383",

docs/source/developers/developing_extensions/extension-providing-chat.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ The rendermime registry is required to display the messages using markdown synta
7979
This registry is provided by jupyterlab with a token, and must be required by the
8080
extension.
8181

82-
### A full example
82+
### A minimal full extension
8383

8484
The example below adds a new chat to the right panel.
8585

@@ -346,6 +346,8 @@ the [Material UI API](https://mui.com/material-ui/api/autocomplete/).
346346

347347
Here is a simple example using a commands list (commands list copied from _jupyter-ai_):
348348

349+
{emphasize-lines="2,6,12,13,14,15,16,17,18,25,26,32"}
350+
349351
```typescript
350352
import {
351353
AutocompletionRegistry,
@@ -385,3 +387,79 @@ const myChatExtension: JupyterFrontEndPlugin<void> = {
385387
}
386388
};
387389
```
390+
391+
(attachment-opener-registry)=
392+
393+
### attachmentOpenerRegistry
394+
395+
The `attachmentOpenerRegistry` provides a way to open attachments for a given type.
396+
A simple example is to handle the attached files, by opening them using a command.
397+
398+
```{tip}
399+
To be able to attach files from the chat, you must provide a `IDocumentManager` that will
400+
be used to select the files to attach.
401+
By default the `IDefaultFileBrowser.model.manager` can be used.
402+
```
403+
404+
The default registry is not much than a `Map<string, () => void>`, allowing setting a
405+
specific function for an attachment type.
406+
407+
{emphasize-lines="2,5,9,23,26,34,38,40,41,42,43,49,50"}
408+
409+
```typescript
410+
import {
411+
AttachmentOpenerRegistry,
412+
ChatModel,
413+
ChatWidget,
414+
IAttachment,
415+
IChatMessage,
416+
INewMessage
417+
} from '@jupyter/chat';
418+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
419+
420+
...
421+
422+
class MyModel extends ChatModel {
423+
sendMessage(
424+
newMessage: INewMessage
425+
): Promise<boolean | void> | boolean | void {
426+
const message: IChatMessage = {
427+
body: newMessage.body,
428+
id: newMessage.id ?? UUID.uuid4(),
429+
type: 'msg',
430+
time: Date.now() / 1000,
431+
sender: { username: 'me' },
432+
attachments: this.inputAttachments
433+
};
434+
this.messageAdded(message);
435+
this.clearAttachments();
436+
}
437+
}
438+
439+
const myChatExtension: JupyterFrontEndPlugin<void> = {
440+
id: 'myExtension:plugin',
441+
autoStart: true,
442+
requires: [IRenderMimeRegistry],
443+
optional: [IDefaultFileBrowser],
444+
activate: (
445+
app: JupyterFrontEnd,
446+
rmRegistry: IRenderMimeRegistry,
447+
filebrowser: IDefaultFileBrowser | null
448+
): void => {
449+
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
450+
attachmentOpenerRegistry.set('file', (attachment: IAttachment) => {
451+
app.commands.execute('docmanager:open', { path: attachment.value });
452+
});
453+
454+
const model = new MyModel();
455+
const widget = new ChatWidget({
456+
model,
457+
rmRegistry,
458+
documentManager: filebrowser?.model.manager,
459+
attachmentOpenerRegistry
460+
});
461+
462+
app.shell.add(widget, 'right');
463+
}
464+
};
465+
```

docs/source/users/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ available:
9898

9999
(chat-settings)=
100100

101+
### Attachments
102+
103+
Files can be attached to the messages using the clip icon next to the send icon in the input.
104+
It opens a Dialog allowing to select and atach files.
105+
106+
Attachments can then be opened by clicking on the preview icon.
107+
101108
## Chat settings
102109

103110
Some jupyterlab settings are available for the chats in the setting panel

packages/jupyter-chat/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"@jupyter/react-components": "^0.15.2",
4949
"@jupyterlab/application": "^4.2.0",
5050
"@jupyterlab/apputils": "^4.3.0",
51+
"@jupyterlab/docmanager": "^4.2.0",
52+
"@jupyterlab/filebrowser": "^4.2.0",
5153
"@jupyterlab/fileeditor": "^4.2.0",
5254
"@jupyterlab/notebook": "^4.2.0",
5355
"@jupyterlab/rendermime": "^4.2.0",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
// import { IDocumentManager } from '@jupyterlab/docmanager';
7+
import CloseIcon from '@mui/icons-material/Close';
8+
import { Box } from '@mui/material';
9+
import React, { useContext } from 'react';
10+
11+
import { TooltippedButton } from './mui-extras/tooltipped-button';
12+
import { IAttachment } from '../types';
13+
import { AttachmentOpenerContext } from '../context';
14+
15+
const ATTACHMENTS_CLASS = 'jp-chat-attachments';
16+
const ATTACHMENT_CLASS = 'jp-chat-attachment';
17+
const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
18+
const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
19+
20+
/**
21+
* The attachments props.
22+
*/
23+
export type AttachmentsProps = {
24+
attachments: IAttachment[];
25+
onRemove?: (attachment: IAttachment) => void;
26+
};
27+
28+
/**
29+
* The Attachments component.
30+
*/
31+
export function AttachmentPreviewList(props: AttachmentsProps): JSX.Element {
32+
return (
33+
<Box className={ATTACHMENTS_CLASS}>
34+
{props.attachments.map(attachment => (
35+
<AttachmentPreview {...props} attachment={attachment} />
36+
))}
37+
</Box>
38+
);
39+
}
40+
41+
/**
42+
* The attachment props.
43+
*/
44+
export type AttachmentProps = AttachmentsProps & {
45+
attachment: IAttachment;
46+
};
47+
48+
/**
49+
* The Attachment component.
50+
*/
51+
export function AttachmentPreview(props: AttachmentProps): JSX.Element {
52+
const remove_tooltip = 'Remove attachment';
53+
const attachmentOpenerRegistry = useContext(AttachmentOpenerContext);
54+
55+
return (
56+
<Box className={ATTACHMENT_CLASS}>
57+
<span
58+
className={
59+
attachmentOpenerRegistry?.get(props.attachment.type)
60+
? ATTACHMENT_CLICKABLE_CLASS
61+
: ''
62+
}
63+
onClick={() =>
64+
attachmentOpenerRegistry?.get(props.attachment.type)?.(
65+
props.attachment
66+
)
67+
}
68+
>
69+
{props.attachment.value}
70+
</span>
71+
{props.onRemove && (
72+
<TooltippedButton
73+
onClick={() => props.onRemove!(props.attachment)}
74+
tooltip={remove_tooltip}
75+
buttonProps={{
76+
size: 'small',
77+
title: remove_tooltip,
78+
className: REMOVE_BUTTON_CLASS
79+
}}
80+
sx={{
81+
minWidth: 'unset',
82+
padding: '0',
83+
color: 'inherit'
84+
}}
85+
>
86+
<CloseIcon />
87+
</TooltippedButton>
88+
)}
89+
</Box>
90+
);
91+
}

0 commit comments

Comments
 (0)