Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions packages/jupyter-chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,20 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
messageFooterRegistry={props.messageFooterRegistry}
welcomeMessage={props.welcomeMessage}
/>
<ChatInput
sx={{
paddingLeft: 4,
paddingRight: 4,
paddingTop: 0,
paddingBottom: 0,
borderTop: '1px solid var(--jp-border-color1)'
}}
model={model.input}
chatCommandRegistry={props.chatCommandRegistry}
toolbarRegistry={inputToolbarRegistry}
/>
<div className="jp-chat-input-container" data-input-id={model.input.id}>
<ChatInput
sx={{
paddingLeft: 4,
paddingRight: 4,
paddingTop: 0,
paddingBottom: 0,
borderTop: '1px solid var(--jp-border-color1)'
}}
model={model.input}
chatCommandRegistry={props.chatCommandRegistry}
toolbarRegistry={inputToolbarRegistry}
/>
</div>
</AttachmentOpenerContext.Provider>
);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/jupyter-chat/src/components/input/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
);

return (
<Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
<Box
sx={props.sx}
className={clsx(INPUT_BOX_CLASS)}
data-input-id={model.id}
>
<AttachmentPreviewList
attachments={attachments}
onRemove={model.removeAttachment}
Expand Down
21 changes: 21 additions & 0 deletions packages/jupyter-chat/src/input-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { IDocumentManager } from '@jupyterlab/docmanager';
import { UUID } from '@lumino/coreutils';
import { IDisposable } from '@lumino/disposable';
import { ISignal, Signal } from '@lumino/signaling';
import { IActiveCellManager } from './active-cell-manager';
Expand Down Expand Up @@ -116,6 +117,11 @@ export interface IInputModel extends IDisposable {
*/
clearAttachments(): void;

/**
* Unique identifier for the input (needed for drag-and-drop).
*/
readonly id: string;

/**
* A signal emitting when the attachment list has changed.
*/
Expand Down Expand Up @@ -157,6 +163,7 @@ export interface IInputModel extends IDisposable {
*/
export class InputModel implements IInputModel {
constructor(options: InputModel.IOptions) {
this._id = options.id ?? `input-${UUID.uuid4()}`;
this._onSend = options.onSend;
this._chatContext = options.chatContext;
this._value = options.value || '';
Expand Down Expand Up @@ -196,6 +203,13 @@ export class InputModel implements IInputModel {
*/
cancel: (() => void) | undefined;

/**
* Unique identifier for the input (needed for drag-and-drop).
*/
get id(): string {
return this._id;
}

/**
* The entire input value.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/jupyter-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
55 changes: 43 additions & 12 deletions packages/jupyter-chat/src/widgets/chat-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<HTMLElement>(
`.${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;
}
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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
*/
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);
Expand Down
Loading