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 ( - + void) | undefined; + /** + * Unique identifier for the input (needed for drag-and-drop). + */ + get id(): string { + return this._id; + } + /** * The entire input value. */ @@ -471,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; @@ -532,6 +547,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. */ 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 9073d862..4c6d4bab 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'; @@ -121,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; } @@ -139,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; } } @@ -183,6 +188,30 @@ export class ChatWidget extends ReactWidget { } } + /** + * Get the input model associated with the event target and input ids. + */ + private _getInputFromEvent(event: Drag.Event): IInputModel | undefined { + 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.id === inputId + ? this.model.input + : this.model.getEditionModels().find(model => model.id === inputId); + return inputModel; + } + element = element.parentElement; + } + + return; + } + /** * Process dropped files */ @@ -201,7 +230,8 @@ export class ChatWidget extends ReactWidget { value: data.model.path, mimetype: data.model.mimetype }; - this.model.input.addAttachment?.(attachment); + const inputModel = this._getInputFromEvent(event); + inputModel?.addAttachment?.(attachment); } /** @@ -263,7 +293,8 @@ export class ChatWidget extends ReactWidget { value: notebookPath, cells: validCells }; - this.model.input.addAttachment?.(attachment); + const inputModel = this._getInputFromEvent(event); + inputModel?.addAttachment?.(attachment); } } catch (error) { console.error('Failed to process cell drop: ', error);