Skip to content

Commit 2b54aa4

Browse files
andrii-idlqqq
andauthored
Create chat message attachments via drag-and-drop from files, notebook cells (#248)
* dragndrop cells, files into chat as attachments * use JSON comparison to detect duplicates * use updated types INotebookAttachment, INotebookAttachmentCell * use PathExt for filepaths manipulation * add a notebook to attachmet opener registry * ensure that attachment keys are unique * replace Array.from().find() with direct iterator loop * Fix attachment chips / pills component layout with padding and horizontal scroll * update navigation top icon snapshot (1px differnece) * merge cells from same notebook added via multiple drags into the same attachment * fix input container drag hover effect during attachment addition * fix input container and attachemtns vertical spacing and centering * Implement nit suggestions * update snapshot * use @lumino/dragdrop Drag.Event instead of depreciated IDragEvent * Update packages/jupyter-chat/src/widgets/chat-widget.tsx Co-authored-by: David L. Qiu <[email protected]> * remove unneeded type casting * use DirListing.IContentsThunk type for file drop instead of any * Update packages/jupyter-chat/src/widgets/chat-widget.tsx Co-authored-by: David L. Qiu <[email protected]> * Update packages/jupyter-chat/src/widgets/chat-widget.tsx Co-authored-by: David L. Qiu <[email protected]> * use JupyterLab interfaces for file and cell drag data instead of `any` * use JupyterLab type guards and early returns in _findNotebookPath * calculate `notebookPath` once, trust that all cells are from the same notebook --------- Co-authored-by: David L. Qiu <[email protected]>
1 parent f24e62f commit 2b54aa4

File tree

10 files changed

+421
-23
lines changed

10 files changed

+421
-23
lines changed

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
* Distributed under the terms of the Modified BSD License.
44
*/
55

6-
// import { IDocumentManager } from '@jupyterlab/docmanager';
76
import CloseIcon from '@mui/icons-material/Close';
87
import { Box } from '@mui/material';
98
import React, { useContext } from 'react';
9+
import { PathExt } from '@jupyterlab/coreutils';
10+
import { UUID } from '@lumino/coreutils';
1011

1112
import { TooltippedButton } from './mui-extras/tooltipped-button';
1213
import { IAttachment } from '../types';
@@ -18,8 +19,34 @@ const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
1819
const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove';
1920

2021
/**
21-
* The attachments props.
22+
* Generate a user-friendly display name for an attachment
2223
*/
24+
function getAttachmentDisplayName(attachment: IAttachment): string {
25+
if (attachment.type === 'notebook') {
26+
// Extract notebook filename with extension
27+
const notebookName =
28+
PathExt.basename(attachment.value) || 'Unknown notebook';
29+
30+
// Show info about attached cells if there are any
31+
if (attachment.cells?.length === 1) {
32+
return `${notebookName}: ${attachment.cells[0].input_type} cell`;
33+
} else if (attachment.cells && attachment.cells.length > 1) {
34+
return `${notebookName}: ${attachment.cells.length} cells`;
35+
}
36+
37+
return notebookName;
38+
}
39+
40+
if (attachment.type === 'file') {
41+
// Extract filename with extension
42+
const fileName = PathExt.basename(attachment.value) || 'Unknown file';
43+
44+
return fileName;
45+
}
46+
47+
return (attachment as any).value || 'Unknown attachment';
48+
}
49+
2350
export type AttachmentsProps = {
2451
attachments: IAttachment[];
2552
onRemove?: (attachment: IAttachment) => void;
@@ -32,7 +59,11 @@ export function AttachmentPreviewList(props: AttachmentsProps): JSX.Element {
3259
return (
3360
<Box className={ATTACHMENTS_CLASS}>
3461
{props.attachments.map(attachment => (
35-
<AttachmentPreview {...props} attachment={attachment} />
62+
<AttachmentPreview
63+
key={`${PathExt.basename(attachment.value)}-${UUID.uuid4()}`}
64+
{...props}
65+
attachment={attachment}
66+
/>
3667
))}
3768
</Box>
3869
);
@@ -66,7 +97,7 @@ export function AttachmentPreview(props: AttachmentProps): JSX.Element {
6697
)
6798
}
6899
>
69-
{props.attachment.value}
100+
{getAttachmentDisplayName(props.attachment)}
70101
</span>
71102
{props.onRemove && (
72103
<TooltippedButton

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
4747
sx={{
4848
paddingLeft: 4,
4949
paddingRight: 4,
50-
paddingTop: 1,
50+
paddingTop: 0,
5151
paddingBottom: 0,
5252
borderTop: '1px solid var(--jp-border-color1)'
5353
}}

packages/jupyter-chat/src/input-model.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ISignal, Signal } from '@lumino/signaling';
99
import { IActiveCellManager } from './active-cell-manager';
1010
import { ISelectionWatcher } from './selection-watcher';
1111
import { IChatContext } from './model';
12-
import { IAttachment, IUser } from './types';
12+
import { IAttachment, INotebookAttachment, IUser } from './types';
1313

1414
/**
1515
* The chat input interface.
@@ -318,13 +318,46 @@ export class InputModel implements IInputModel {
318318
* Add attachment to send with next message.
319319
*/
320320
addAttachment = (attachment: IAttachment): void => {
321+
// Use JSON comparison to detect duplicates (same as in YChat.setAttachment)
322+
const attachmentJson = JSON.stringify(attachment);
321323
const duplicateAttachment = this._attachments.find(
322-
att => att.type === attachment.type && att.value === attachment.value
324+
att => JSON.stringify(att) === attachmentJson
323325
);
324326
if (duplicateAttachment) {
325327
return;
326328
}
327329

330+
// Merge cells from same notebook into the same attachment
331+
if (attachment.type === 'notebook' && attachment.cells) {
332+
const existingNotebookIndex = this._attachments.findIndex(
333+
att => att.type === 'notebook' && att.value === attachment.value
334+
);
335+
336+
if (existingNotebookIndex !== -1) {
337+
const existingAttachment = this._attachments[
338+
existingNotebookIndex
339+
] as INotebookAttachment;
340+
const existingCells = existingAttachment.cells || [];
341+
342+
// Filter out duplicate cells
343+
const newCells = attachment.cells.filter(
344+
newCell =>
345+
!existingCells.some(existingCell => existingCell.id === newCell.id)
346+
);
347+
348+
if (!newCells.length) {
349+
return;
350+
}
351+
352+
this._attachments[existingNotebookIndex] = {
353+
...existingAttachment,
354+
cells: [...existingCells, ...newCells]
355+
};
356+
this._attachmentsChanged.emit([...this._attachments]);
357+
return;
358+
}
359+
}
360+
328361
this._attachments.push(attachment);
329362
this._attachmentsChanged.emit([...this._attachments]);
330363
};
@@ -333,8 +366,10 @@ export class InputModel implements IInputModel {
333366
* Remove attachment to be sent.
334367
*/
335368
removeAttachment = (attachment: IAttachment): void => {
369+
// Use JSON comparison to detect duplicates (same as in YChat.setAttachment)
370+
const attachmentJson = JSON.stringify(attachment);
336371
const attachmentIndex = this._attachments.findIndex(
337-
att => att.type === attachment.type && att.value === attachment.value
372+
att => JSON.stringify(att) === attachmentJson
338373
);
339374
if (attachmentIndex === -1) {
340375
return;

0 commit comments

Comments
 (0)