From 6b35ca99400a514b6d800d0d8bdefc4eb4f08ae9 Mon Sep 17 00:00:00 2001 From: Nakul Date: Thu, 25 Sep 2025 19:31:17 +0530 Subject: [PATCH 1/5] Add support for drag and drop files while editing messages in chat input. --- packages/jupyter-chat/src/input-model.ts | 5 +++++ packages/jupyter-chat/src/widgets/chat-widget.tsx | 11 +++++++++-- packages/jupyterlab-chat/src/model.ts | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/jupyter-chat/src/input-model.ts b/packages/jupyter-chat/src/input-model.ts index 2d6c04b9..a5466102 100644 --- a/packages/jupyter-chat/src/input-model.ts +++ b/packages/jupyter-chat/src/input-model.ts @@ -116,6 +116,11 @@ export interface IInputModel extends IDisposable { */ clearAttachments(): void; + /** + * If the user is editing a message, this points to the active edition input model. + */ + currentEdition?: IInputModel; + /** * A signal emitting when the attachment list has changed. */ diff --git a/packages/jupyter-chat/src/widgets/chat-widget.tsx b/packages/jupyter-chat/src/widgets/chat-widget.tsx index 9073d862..17542047 100644 --- a/packages/jupyter-chat/src/widgets/chat-widget.tsx +++ b/packages/jupyter-chat/src/widgets/chat-widget.tsx @@ -20,6 +20,7 @@ import { INotebookAttachmentCell } from '../types'; import { ActiveCellManager } from '../active-cell-manager'; +import { IInputModel } from '../input-model'; // MIME type constant for file browser drag events const FILE_BROWSER_MIME = 'application/x-jupyter-icontentsrich'; @@ -183,6 +184,10 @@ export class ChatWidget extends ReactWidget { } } + private _getActiveInput(): IInputModel { + return this.model.input.currentEdition ?? this.model.input; + } + /** * Process dropped files */ @@ -201,7 +206,8 @@ export class ChatWidget extends ReactWidget { value: data.model.path, mimetype: data.model.mimetype }; - this.model.input.addAttachment?.(attachment); + const activeInput = this._getActiveInput(); + activeInput.addAttachment?.(attachment); } /** @@ -263,7 +269,8 @@ export class ChatWidget extends ReactWidget { value: notebookPath, cells: validCells }; - this.model.input.addAttachment?.(attachment); + const activeInput = this._getActiveInput(); + activeInput.addAttachment?.(attachment); } } catch (error) { console.error('Failed to process cell drop: ', error); diff --git a/packages/jupyterlab-chat/src/model.ts b/packages/jupyterlab-chat/src/model.ts index 2ab14afb..b89eb0c2 100644 --- a/packages/jupyterlab-chat/src/model.ts +++ b/packages/jupyterlab-chat/src/model.ts @@ -335,6 +335,12 @@ export class LabChatModel }; edition.model.valueChanged.connect(_onInputChanged); + + this.input.currentEdition = edition.model; + + edition.model.onDisposed.connect(() => { + this.input.currentEdition = undefined; + }); } }; From b08a9fd924e8911f58b9458f0589692aecdb6560 Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 27 Sep 2025 19:28:31 +0530 Subject: [PATCH 2/5] Refactor drag event handling to support multiple input containers in chat widget --- .../jupyter-chat/src/widgets/chat-widget.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/jupyter-chat/src/widgets/chat-widget.tsx b/packages/jupyter-chat/src/widgets/chat-widget.tsx index 17542047..93aa3841 100644 --- a/packages/jupyter-chat/src/widgets/chat-widget.tsx +++ b/packages/jupyter-chat/src/widgets/chat-widget.tsx @@ -122,12 +122,19 @@ export class ChatWidget extends ReactWidget { * Handle drag over events */ private _handleDrag(event: Drag.Event): void { - const inputContainer = this.node.querySelector(`.${INPUT_CONTAINER_CLASS}`); + const inputContainers = this.node.querySelectorAll( + `.${INPUT_CONTAINER_CLASS}` + ); const target = event.target as HTMLElement; - const isOverInput = - inputContainer?.contains(target) || inputContainer === target; + let overInput: HTMLElement | null = null; + for (const container of inputContainers) { + if (container.contains(target)) { + overInput = container; + break; + } + } - if (!isOverInput) { + if (!overInput) { this._removeDragHoverClass(); return; } @@ -140,12 +147,9 @@ export class ChatWidget extends ReactWidget { event.stopPropagation(); event.dropAction = 'move'; - if ( - inputContainer && - !inputContainer.classList.contains(DRAG_HOVER_CLASS) - ) { - inputContainer.classList.add(DRAG_HOVER_CLASS); - this._dragTarget = inputContainer as HTMLElement; + if (!overInput.classList.contains(DRAG_HOVER_CLASS)) { + overInput.classList.add(DRAG_HOVER_CLASS); + this._dragTarget = overInput; } } From 0318b2f1b3dc3e7b1b3732cd048e71480b50763a Mon Sep 17 00:00:00 2001 From: Nakul Date: Sun, 28 Sep 2025 20:32:00 +0530 Subject: [PATCH 3/5] Enhance drag-and-drop support by adding unique id --- packages/jupyter-chat/src/components/chat.tsx | 26 +++++----- .../src/components/messages/message.tsx | 17 ++++--- packages/jupyter-chat/src/input-model.ts | 48 ++++++++++++++++++- .../jupyter-chat/src/widgets/chat-widget.tsx | 31 +++++++++--- packages/jupyterlab-chat/src/model.ts | 11 ++--- 5 files changed, 101 insertions(+), 32 deletions(-) diff --git a/packages/jupyter-chat/src/components/chat.tsx b/packages/jupyter-chat/src/components/chat.tsx index 019c0acd..eb23d33a 100644 --- a/packages/jupyter-chat/src/components/chat.tsx +++ b/packages/jupyter-chat/src/components/chat.tsx @@ -43,18 +43,20 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element { messageFooterRegistry={props.messageFooterRegistry} welcomeMessage={props.welcomeMessage} /> - +
+ +
); } diff --git a/packages/jupyter-chat/src/components/messages/message.tsx b/packages/jupyter-chat/src/components/messages/message.tsx index 4d5fdd99..9daea3aa 100644 --- a/packages/jupyter-chat/src/components/messages/message.tsx +++ b/packages/jupyter-chat/src/components/messages/message.tsx @@ -129,12 +129,17 @@ export const ChatMessage = forwardRef( ) : (
{edit && canEdit && model.getEditionModel(message.id) ? ( - cancelEdition()} - model={model.getEditionModel(message.id)!} - chatCommandRegistry={props.chatCommandRegistry} - toolbarRegistry={props.inputToolbarRegistry} - /> +
+ cancelEdition()} + model={model.getEditionModel(message.id)!} + chatCommandRegistry={props.chatCommandRegistry} + toolbarRegistry={props.inputToolbarRegistry} + /> +
) : ( void) | undefined; + /** + * Unique identifier for the input (needed for drag-and-drop). + */ + public id: string; + /** * The entire input value. */ @@ -451,6 +467,21 @@ export class InputModel implements IInputModel { this._mentions = []; }; + /** + * Register the input model. + */ + registerInput(inputModel: IInputModel): IInputModel { + this._inputMap.set(inputModel.id, inputModel); + inputModel.onDisposed.connect(() => { + this._inputMap.delete(inputModel.id); + }); + return inputModel; + } + + getInput(id: string): IInputModel | undefined { + return this._inputMap.get(id); + } + /** * Dispose the input model. */ @@ -487,6 +518,7 @@ export class InputModel implements IInputModel { private _selectionWatcher: ISelectionWatcher | null; private _documentManager: IDocumentManager | null; private _config: InputModel.IConfig; + private _inputMap: Map = new Map(); private _valueChanged = new Signal(this); private _cursorIndexChanged = new Signal(this); private _currentWordChanged = new Signal(this); @@ -537,6 +569,12 @@ export namespace InputModel { */ cursorIndex?: number; + /** + * Optional unique identifier for this input model. + * If not provided, one will be generated automatically. + */ + id?: string; + /** * The configuration for the input component. */ @@ -617,6 +655,12 @@ namespace Private { return [start, end]; } + let _counter = 0; + export function generateUniqueId(): string { + _counter += 1; + return `${_counter}`; + } + /** * Gets the current (space-separated) word around the user's cursor. The current * word is used to generate a list of matching chat commands. diff --git a/packages/jupyter-chat/src/widgets/chat-widget.tsx b/packages/jupyter-chat/src/widgets/chat-widget.tsx index 93aa3841..219bb8eb 100644 --- a/packages/jupyter-chat/src/widgets/chat-widget.tsx +++ b/packages/jupyter-chat/src/widgets/chat-widget.tsx @@ -188,8 +188,27 @@ export class ChatWidget extends ReactWidget { } } - private _getActiveInput(): IInputModel { - return this.model.input.currentEdition ?? this.model.input; + /** + * Get the input model associated with the event target and input ids. + */ + private _getInputFromEvent(event: Drag.Event): IInputModel | null { + let element = event.target as HTMLElement | null; + + while (element) { + if ( + element.classList.contains(INPUT_CONTAINER_CLASS) && + element.dataset.inputId + ) { + const inputId = element.dataset.inputId; + const inputModel = + this.model.input.getInput(inputId) ?? + (inputId === this.model.input.id ? this.model.input : null); + return inputModel; + } + element = element.parentElement; + } + + return null; } /** @@ -210,8 +229,8 @@ export class ChatWidget extends ReactWidget { value: data.model.path, mimetype: data.model.mimetype }; - const activeInput = this._getActiveInput(); - activeInput.addAttachment?.(attachment); + const inputModel = this._getInputFromEvent(event); + inputModel?.addAttachment?.(attachment); } /** @@ -273,8 +292,8 @@ export class ChatWidget extends ReactWidget { value: notebookPath, cells: validCells }; - const activeInput = this._getActiveInput(); - activeInput.addAttachment?.(attachment); + const inputModel = this._getInputFromEvent(event); + inputModel?.addAttachment?.(attachment); } } catch (error) { console.error('Failed to process cell drop: ', error); diff --git a/packages/jupyterlab-chat/src/model.ts b/packages/jupyterlab-chat/src/model.ts index b89eb0c2..90563082 100644 --- a/packages/jupyterlab-chat/src/model.ts +++ b/packages/jupyterlab-chat/src/model.ts @@ -334,13 +334,12 @@ export class LabChatModel this.onInputChanged(value, edition.id); }; - edition.model.valueChanged.connect(_onInputChanged); - - this.input.currentEdition = edition.model; - - edition.model.onDisposed.connect(() => { - this.input.currentEdition = undefined; + edition.model.valueChanged.connect((_, value) => { + this.onInputChanged(value, edition.id); }); + + this.input.registerInput(edition.model); + edition.model.valueChanged.connect(_onInputChanged); } }; From 928984ce64dce215978dc4aa2d18a7c7a04c555f Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 9 Oct 2025 15:56:09 +0200 Subject: [PATCH 4/5] Simplify the code base --- .../src/components/input/chat-input.tsx | 6 ++- .../src/components/messages/message.tsx | 17 +++----- packages/jupyter-chat/src/input-model.ts | 42 ++++--------------- packages/jupyter-chat/src/model.ts | 12 ++++++ .../jupyter-chat/src/widgets/chat-widget.tsx | 9 ++-- packages/jupyterlab-chat/src/model.ts | 5 --- 6 files changed, 35 insertions(+), 56 deletions(-) diff --git a/packages/jupyter-chat/src/components/input/chat-input.tsx b/packages/jupyter-chat/src/components/input/chat-input.tsx index 99b61ccc..57543fe5 100644 --- a/packages/jupyter-chat/src/components/input/chat-input.tsx +++ b/packages/jupyter-chat/src/components/input/chat-input.tsx @@ -177,7 +177,11 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element { ); return ( - + ( ) : (
{edit && canEdit && model.getEditionModel(message.id) ? ( -
- cancelEdition()} - model={model.getEditionModel(message.id)!} - chatCommandRegistry={props.chatCommandRegistry} - toolbarRegistry={props.inputToolbarRegistry} - /> -
+ cancelEdition()} + model={model.getEditionModel(message.id)!} + chatCommandRegistry={props.chatCommandRegistry} + toolbarRegistry={props.inputToolbarRegistry} + /> ) : ( { - this._inputMap.delete(inputModel.id); - }); - return inputModel; - } - - getInput(id: string): IInputModel | undefined { - return this._inputMap.get(id); - } - /** * Dispose the input model. */ @@ -507,6 +485,7 @@ export class InputModel implements IInputModel { return this._isDisposed; } + private _id: string; private _onSend: (input: string, model?: InputModel) => void; private _chatContext?: IChatContext; private _value: string; @@ -518,7 +497,6 @@ export class InputModel implements IInputModel { private _selectionWatcher: ISelectionWatcher | null; private _documentManager: IDocumentManager | null; private _config: InputModel.IConfig; - private _inputMap: Map = new Map(); private _valueChanged = new Signal(this); private _cursorIndexChanged = new Signal(this); private _currentWordChanged = new Signal(this); @@ -655,12 +633,6 @@ namespace Private { return [start, end]; } - let _counter = 0; - export function generateUniqueId(): string { - _counter += 1; - return `${_counter}`; - } - /** * Gets the current (space-separated) word around the user's cursor. The current * word is used to generate a list of matching chat commands. diff --git a/packages/jupyter-chat/src/model.ts b/packages/jupyter-chat/src/model.ts index 90a50399..40f80634 100644 --- a/packages/jupyter-chat/src/model.ts +++ b/packages/jupyter-chat/src/model.ts @@ -201,6 +201,11 @@ export interface IChatModel extends IDisposable { */ getEditionModel(messageID: string): IInputModel | undefined; + /** + * Get the input models of all edited messages. + */ + getEditionModels(): IInputModel[]; + /** * Add an input model of the edited message. */ @@ -637,6 +642,13 @@ export abstract class AbstractChatModel implements IChatModel { return this._messageEditions.get(messageID); } + /** + * Get the input models of all edited messages. + */ + getEditionModels(): IInputModel[] { + return Array.from(this._messageEditions.values()); + } + /** * Add an input model of the edited message. */ diff --git a/packages/jupyter-chat/src/widgets/chat-widget.tsx b/packages/jupyter-chat/src/widgets/chat-widget.tsx index 219bb8eb..4c6d4bab 100644 --- a/packages/jupyter-chat/src/widgets/chat-widget.tsx +++ b/packages/jupyter-chat/src/widgets/chat-widget.tsx @@ -191,7 +191,7 @@ export class ChatWidget extends ReactWidget { /** * Get the input model associated with the event target and input ids. */ - private _getInputFromEvent(event: Drag.Event): IInputModel | null { + private _getInputFromEvent(event: Drag.Event): IInputModel | undefined { let element = event.target as HTMLElement | null; while (element) { @@ -201,14 +201,15 @@ export class ChatWidget extends ReactWidget { ) { const inputId = element.dataset.inputId; const inputModel = - this.model.input.getInput(inputId) ?? - (inputId === this.model.input.id ? this.model.input : null); + this.model.input.id === inputId + ? this.model.input + : this.model.getEditionModels().find(model => model.id === inputId); return inputModel; } element = element.parentElement; } - return null; + return; } /** diff --git a/packages/jupyterlab-chat/src/model.ts b/packages/jupyterlab-chat/src/model.ts index 90563082..2ab14afb 100644 --- a/packages/jupyterlab-chat/src/model.ts +++ b/packages/jupyterlab-chat/src/model.ts @@ -334,11 +334,6 @@ export class LabChatModel this.onInputChanged(value, edition.id); }; - edition.model.valueChanged.connect((_, value) => { - this.onInputChanged(value, edition.id); - }); - - this.input.registerInput(edition.model); edition.model.valueChanged.connect(_onInputChanged); } }; From 8e4c48fb8d0e567fcf56d92b6f6de04b2a80873d Mon Sep 17 00:00:00 2001 From: Nakul Date: Sat, 11 Oct 2025 10:59:14 +0530 Subject: [PATCH 5/5] Refactoring --- packages/jupyter-chat/src/components/chat.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/jupyter-chat/src/components/chat.tsx b/packages/jupyter-chat/src/components/chat.tsx index eb23d33a..019c0acd 100644 --- a/packages/jupyter-chat/src/components/chat.tsx +++ b/packages/jupyter-chat/src/components/chat.tsx @@ -43,20 +43,18 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element { messageFooterRegistry={props.messageFooterRegistry} welcomeMessage={props.welcomeMessage} /> -
- -
+ ); }