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. */ @@ -446,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. */ @@ -482,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); @@ -532,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. */ @@ -612,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 9073d862..219bb8eb 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,29 @@ export class ChatWidget extends ReactWidget { } } + /** + * 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; + } + /** * Process dropped files */ @@ -201,7 +229,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 +292,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); diff --git a/packages/jupyterlab-chat/src/model.ts b/packages/jupyterlab-chat/src/model.ts index 2ab14afb..90563082 100644 --- a/packages/jupyterlab-chat/src/model.ts +++ b/packages/jupyterlab-chat/src/model.ts @@ -334,6 +334,11 @@ 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); } };