Skip to content

Commit 37205de

Browse files
nakul-pybrichet
andauthored
Add support for drag and drop while editing message. (#282)
* Add support for drag and drop files while editing messages in chat input. * Refactor drag event handling to support multiple input containers in chat widget * Enhance drag-and-drop support by adding unique id * Simplify the code base * Refactoring --------- Co-authored-by: Nicolas Brichet <[email protected]>
1 parent f712037 commit 37205de

File tree

4 files changed

+81
-13
lines changed

4 files changed

+81
-13
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
179179
);
180180

181181
return (
182-
<Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
182+
<Box
183+
sx={props.sx}
184+
className={clsx(INPUT_BOX_CLASS)}
185+
data-input-id={model.id}
186+
>
183187
<AttachmentPreviewList
184188
attachments={attachments}
185189
onRemove={model.removeAttachment}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { IDocumentManager } from '@jupyterlab/docmanager';
7+
import { UUID } from '@lumino/coreutils';
78
import { IDisposable } from '@lumino/disposable';
89
import { ISignal, Signal } from '@lumino/signaling';
910
import { IActiveCellManager } from './active-cell-manager';
@@ -116,6 +117,11 @@ export interface IInputModel extends IDisposable {
116117
*/
117118
clearAttachments(): void;
118119

120+
/**
121+
* Unique identifier for the input (needed for drag-and-drop).
122+
*/
123+
readonly id: string;
124+
119125
/**
120126
* A signal emitting when the attachment list has changed.
121127
*/
@@ -157,6 +163,7 @@ export interface IInputModel extends IDisposable {
157163
*/
158164
export class InputModel implements IInputModel {
159165
constructor(options: InputModel.IOptions) {
166+
this._id = options.id ?? `input-${UUID.uuid4()}`;
160167
this._onSend = options.onSend;
161168
this._chatContext = options.chatContext;
162169
this._value = options.value || '';
@@ -196,6 +203,13 @@ export class InputModel implements IInputModel {
196203
*/
197204
cancel: (() => void) | undefined;
198205

206+
/**
207+
* Unique identifier for the input (needed for drag-and-drop).
208+
*/
209+
get id(): string {
210+
return this._id;
211+
}
212+
199213
/**
200214
* The entire input value.
201215
*/
@@ -471,6 +485,7 @@ export class InputModel implements IInputModel {
471485
return this._isDisposed;
472486
}
473487

488+
private _id: string;
474489
private _onSend: (input: string, model?: InputModel) => void;
475490
private _chatContext?: IChatContext;
476491
private _value: string;
@@ -532,6 +547,12 @@ export namespace InputModel {
532547
*/
533548
cursorIndex?: number;
534549

550+
/**
551+
* Optional unique identifier for this input model.
552+
* If not provided, one will be generated automatically.
553+
*/
554+
id?: string;
555+
535556
/**
536557
* The configuration for the input component.
537558
*/

packages/jupyter-chat/src/model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ export interface IChatModel extends IDisposable {
201201
*/
202202
getEditionModel(messageID: string): IInputModel | undefined;
203203

204+
/**
205+
* Get the input models of all edited messages.
206+
*/
207+
getEditionModels(): IInputModel[];
208+
204209
/**
205210
* Add an input model of the edited message.
206211
*/
@@ -637,6 +642,13 @@ export abstract class AbstractChatModel implements IChatModel {
637642
return this._messageEditions.get(messageID);
638643
}
639644

645+
/**
646+
* Get the input models of all edited messages.
647+
*/
648+
getEditionModels(): IInputModel[] {
649+
return Array.from(this._messageEditions.values());
650+
}
651+
640652
/**
641653
* Add an input model of the edited message.
642654
*/

packages/jupyter-chat/src/widgets/chat-widget.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
INotebookAttachmentCell
2121
} from '../types';
2222
import { ActiveCellManager } from '../active-cell-manager';
23+
import { IInputModel } from '../input-model';
2324

2425
// MIME type constant for file browser drag events
2526
const FILE_BROWSER_MIME = 'application/x-jupyter-icontentsrich';
@@ -141,12 +142,19 @@ export class ChatWidget extends ReactWidget {
141142
* Handle drag over events
142143
*/
143144
private _handleDrag(event: Drag.Event): void {
144-
const inputContainer = this.node.querySelector(`.${INPUT_CONTAINER_CLASS}`);
145+
const inputContainers = this.node.querySelectorAll<HTMLElement>(
146+
`.${INPUT_CONTAINER_CLASS}`
147+
);
145148
const target = event.target as HTMLElement;
146-
const isOverInput =
147-
inputContainer?.contains(target) || inputContainer === target;
149+
let overInput: HTMLElement | null = null;
150+
for (const container of inputContainers) {
151+
if (container.contains(target)) {
152+
overInput = container;
153+
break;
154+
}
155+
}
148156

149-
if (!isOverInput) {
157+
if (!overInput) {
150158
this._removeDragHoverClass();
151159
return;
152160
}
@@ -159,12 +167,9 @@ export class ChatWidget extends ReactWidget {
159167
event.stopPropagation();
160168
event.dropAction = 'move';
161169

162-
if (
163-
inputContainer &&
164-
!inputContainer.classList.contains(DRAG_HOVER_CLASS)
165-
) {
166-
inputContainer.classList.add(DRAG_HOVER_CLASS);
167-
this._dragTarget = inputContainer as HTMLElement;
170+
if (!overInput.classList.contains(DRAG_HOVER_CLASS)) {
171+
overInput.classList.add(DRAG_HOVER_CLASS);
172+
this._dragTarget = overInput;
168173
}
169174
}
170175

@@ -203,6 +208,30 @@ export class ChatWidget extends ReactWidget {
203208
}
204209
}
205210

211+
/**
212+
* Get the input model associated with the event target and input ids.
213+
*/
214+
private _getInputFromEvent(event: Drag.Event): IInputModel | undefined {
215+
let element = event.target as HTMLElement | null;
216+
217+
while (element) {
218+
if (
219+
element.classList.contains(INPUT_CONTAINER_CLASS) &&
220+
element.dataset.inputId
221+
) {
222+
const inputId = element.dataset.inputId;
223+
const inputModel =
224+
this.model.input.id === inputId
225+
? this.model.input
226+
: this.model.getEditionModels().find(model => model.id === inputId);
227+
return inputModel;
228+
}
229+
element = element.parentElement;
230+
}
231+
232+
return;
233+
}
234+
206235
/**
207236
* Process dropped files
208237
*/
@@ -221,7 +250,8 @@ export class ChatWidget extends ReactWidget {
221250
value: data.model.path,
222251
mimetype: data.model.mimetype
223252
};
224-
this.model.input.addAttachment?.(attachment);
253+
const inputModel = this._getInputFromEvent(event);
254+
inputModel?.addAttachment?.(attachment);
225255
}
226256

227257
/**
@@ -283,7 +313,8 @@ export class ChatWidget extends ReactWidget {
283313
value: notebookPath,
284314
cells: validCells
285315
};
286-
this.model.input.addAttachment?.(attachment);
316+
const inputModel = this._getInputFromEvent(event);
317+
inputModel?.addAttachment?.(attachment);
287318
}
288319
} catch (error) {
289320
console.error('Failed to process cell drop: ', error);

0 commit comments

Comments
 (0)