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);
}
};