Skip to content

Commit

Permalink
Add executable code cell for ipywidgets output
Browse files Browse the repository at this point in the history
Temporary - it will properly work on future ipywidgets release
  see jupyter-widgets/ipywidgets#3004)
  • Loading branch information
JoaoFelipe committed Apr 7, 2022
1 parent f671bcb commit 0abc886
Show file tree
Hide file tree
Showing 8 changed files with 2,608 additions and 14 deletions.
1,909 changes: 1,905 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@
"dependencies": {
"@jupyterlab/application": "^3.1.0",
"@jupyterlab/apputils": "^3.2.8",
"@jupyterlab/cells": "^3.3.2",
"@jupyterlab/coreutils": "^5.1.0",
"@jupyterlab/docmanager": "^3.2.8",
"@jupyterlab/notebook": "^3.2.8",
"@jupyterlab/observables": "^4.3.2",
"@jupyterlab/rendermime": "^3.3.2",
"@jupyterlab/services": "^6.2.8",
"@lumino/algorithm": "^1.9.1",
"@lumino/coreutils": "^1.12.0",
"@lumino/disposable": "^1.10.1",
"@lumino/messaging": "^1.10.1",
Expand Down
24 changes: 22 additions & 2 deletions src/anachat.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ISessionContext } from '@jupyterlab/apputils';
import { ActivityMonitor } from '@jupyterlab/coreutils';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
//import { Message } from '@lumino/messaging';

import { Panel, Widget } from '@lumino/widgets';
Expand Down Expand Up @@ -29,6 +31,7 @@ export class AnaChat extends Panel {
private _mainWidget: Panel | null;
private _monitor: ActivityMonitor<any, any> | null;
private _tracker: INotebookTracker | null;
private _rendermime: IRenderMimeRegistry;
private _status: IAnaChatStatus;
private _eh: ErrorHandler;
private _chatWidget: ChatWidget | null;
Expand All @@ -37,12 +40,14 @@ export class AnaChat extends Panel {
constructor(
docmanager: IDocumentManager,
tracker: INotebookTracker,
rendermime: IRenderMimeRegistry,
eh: ErrorHandler
) {
super();
this._mainWidget = null;
this._docmanager = docmanager;
this._tracker = tracker;
this._rendermime = rendermime;
this._eh = eh;
this.handlers = {};

Expand Down Expand Up @@ -119,6 +124,19 @@ export class AnaChat extends Panel {
}
}

partialRefresh(): void {
try {
console.log('!!!!!! UPDATE2')
if (!this.currentHandler || !this._chatWidget) {
return this.refreshInterfaceFully();
}
let messages: IChatMessage[] = this.currentHandler.chatHistory;
this._chatWidget.refresh(messages);
} catch (error) {
throw this._report(error, 'partialUpdate', []);
}
}

refreshInterfaceFully(): void {
try {
console.log('!!!!!! UPDATE')
Expand Down Expand Up @@ -173,7 +191,9 @@ export class AnaChat extends Panel {
this.refreshAnaChat.bind(this)
);
const sendText = this.sendText.bind(this);
this._chatWidget = new ChatWidget({ messages, sendText });
const rendermime = this._rendermime;
const sessionContext: ISessionContext | null = this.currentHandler?.nbPanel?.sessionContext || null;
this._chatWidget = new ChatWidget({ messages, sendText, rendermime, sessionContext });
this._mainWidget = new Panel();
this._mainWidget.addClass('jp-AnaChat');
this._mainWidget.addWidget(headerWidget);
Expand Down Expand Up @@ -279,7 +299,7 @@ export class AnaChat extends Panel {
signal: context.model.contentChanged,
timeout: RENDER_TIMEOUT
});
this._monitor.activityStopped.connect(this.monitorUpdate, this);
this._monitor.activityStopped.connect(this.partialRefresh, this);
}
console.log('set currentHandler 2');
this.refreshInterfaceFully();
Expand Down
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,27 @@ import { AnaChat } from './anachat';
import { requestAPI } from './server';
import { anaChatIcon } from './iconimports';
import { ErrorHandler } from './errorhandler';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';

/**
* Initialization data for the anachat extension.
*/
const plugin: JupyterFrontEndPlugin<void> = {
id: 'anachat:plugin',
autoStart: true,
requires: [IDocumentManager, ILabShell, ILayoutRestorer, INotebookTracker],
requires: [IDocumentManager, ILabShell, ILayoutRestorer, INotebookTracker, IRenderMimeRegistry],
activate: (
app: JupyterFrontEnd,
docmanager: IDocumentManager,
labShell: ILabShell,
restorer: ILayoutRestorer,
notebookTracker: INotebookTracker
notebookTracker: INotebookTracker,
rendermime: IRenderMimeRegistry,
) => {
const eh = new ErrorHandler();
try {
// Create the widget
const anaChat = new AnaChat(docmanager, notebookTracker, eh);
const anaChat = new AnaChat(docmanager, notebookTracker, rendermime, eh);

// Add the widget to the right area
anaChat.title.icon = anaChatIcon.bindprops({ stylesheet: 'sideBar' });
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export interface IOptionItem {

export interface IChatMessage {
text: string | IOptionItem[];
type: 'user' | 'bot' | 'error' | 'options';
type: 'user' | 'bot' | 'error' | 'options' | 'cell';
timestamp: number;
}
103 changes: 103 additions & 0 deletions src/view/cellutils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { CodeCell, Cell, RawCell, CodeCellModel, IRawCellModel, RawCellModel, ICodeCellModel, CellModel } from '@jupyterlab/cells';

export interface IContentFactory extends Cell.IContentFactory {
/**
* Create a new code cell widget.
*/
createCodeCell(options: CodeCell.IOptions): CodeCell;

/**
* Create a new raw cell widget.
*/
createRawCell(options: RawCell.IOptions): RawCell;
}

export class ContentFactory
extends Cell.ContentFactory
implements IContentFactory {
/**
* Create a new code cell widget.
*
* #### Notes
* If no cell content factory is passed in with the options, the one on the
* notebook content factory is used.
*/
createCodeCell(options: CodeCell.IOptions): CodeCell {
if (!options.contentFactory) {
options.contentFactory = this;
}
return new CodeCell(options).initializeState();
}

/**
* Create a new raw cell widget.
*
* #### Notes
* If no cell content factory is passed in with the options, the one on the
* notebook content factory is used.
*/
createRawCell(options: RawCell.IOptions): RawCell {
if (!options.contentFactory) {
options.contentFactory = this;
}
return new RawCell(options).initializeState();
}
}


/**
* The default implementation of an `IModelFactory`.
*/
export class ModelFactory {
/**
* Create a new cell model factory.
*/
constructor(options: IModelFactoryOptions = {}) {
this.codeCellContentFactory =
options.codeCellContentFactory || CodeCellModel.defaultContentFactory;
}

/**
* The factory for output area models.
*/
readonly codeCellContentFactory: CodeCellModel.IContentFactory;

/**
* Create a new code cell.
*
* @param source - The data to use for the original source data.
*
* @returns A new code cell. If a source cell is provided, the
* new cell will be initialized with the data from the source.
* If the contentFactory is not provided, the instance
* `codeCellContentFactory` will be used.
*/
createCodeCell(options: CodeCellModel.IOptions): ICodeCellModel {
if (!options.contentFactory) {
options.contentFactory = this.codeCellContentFactory;
}
return new CodeCellModel(options);
}

/**
* Create a new raw cell.
*
* @param source - The data to use for the original source data.
*
* @returns A new raw cell. If a source cell is provided, the
* new cell will be initialized with the data from the source.
*/
createRawCell(options: CellModel.IOptions): IRawCellModel {
return new RawCellModel(options);
}
}

/**
* The options used to initialize a `ModelFactory`.
*/
export interface IModelFactoryOptions {
/**
* The factory for output area models.
*/
codeCellContentFactory?: CodeCellModel.IContentFactory;
}
89 changes: 89 additions & 0 deletions src/view/chatwidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,42 @@ import { IChatMessage } from '../interfaces';
import { MessageWidget } from './messagewidget';
import { Panel, Widget } from '@lumino/widgets';
import { OptionsMessageWidget } from './optionsmessagewidget';
import { CodeCell } from '@jupyterlab/cells';
import { ContentFactory, IContentFactory, ModelFactory } from './cellutils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { KernelMessage } from '@jupyterlab/services';
import { each } from '@lumino/algorithm';
import { IObservableList, ObservableList } from '@jupyterlab/observables';
import { Signal } from '@lumino/signaling';
import { ISessionContext } from '@jupyterlab/apputils';

interface IChatProps {
messages: IChatMessage[];
sendText: (text: string) => void;
rendermime: IRenderMimeRegistry;
sessionContext: ISessionContext | null;
}


export class ChatWidget extends Panel {
private messages: IChatMessage[];
private sendText: (text: string) => void;
private contentFactory: IContentFactory;
private modelFactory: ModelFactory;
private rendermime: IRenderMimeRegistry;
private _cells: IObservableList<CodeCell>;
private _executed = new Signal<this, Date>(this);
private sessionContext: ISessionContext | null;

constructor(options: IChatProps) {
super();
this.messages = options.messages;
this.sendText = options.sendText;
this.rendermime = options.rendermime;
this.sessionContext = options.sessionContext;
this.contentFactory = new ContentFactory();
this.modelFactory = new ModelFactory({});
this._cells = new ObservableList<CodeCell>();
this.create();
}

Expand All @@ -29,15 +51,82 @@ export class ChatWidget extends Panel {
this.addWidget(
new OptionsMessageWidget({ message, sendText: this.sendText })
);
} else if ((message.type === 'cell') && (this.sessionContext != null)) {
let cell: CodeCell = this._createCodeCell();
cell.model.value.text = message.text as string;

cell.model.contentChanged.connect(this.update, this);
const onSuccess = (value: void | KernelMessage.IExecuteReplyMsg) => {
if (this.isDisposed) {
return;
}
if (value && value.content.status === 'ok') {
const content = value.content;
// Use deprecated payloads for backwards compatibility.
if (content.payload && content.payload.length) {
const setNextInput = content.payload.filter(i => {
return (i as any).source === 'set_next_input';
})[0];
if (setNextInput) {
const text = (setNextInput as any).text;
// Ignore the `replace` value and always set the next cell.
cell.model.value.text = text;
}
}
} else if (value && value.content.status === 'error') {
each(this._cells, (cell: CodeCell) => {
if (cell.model.executionCount === null) {
cell.setPrompt('');
}
});
}
cell.model.contentChanged.disconnect(this.update, this);
this.update();
this._executed.emit(new Date());
};
const onFailure = () => {
if (this.isDisposed) {
return;
}
cell.model.contentChanged.disconnect(this.update, this);
this.update();
};
CodeCell.execute(cell, this.sessionContext).then(
onSuccess,
onFailure
);
this.addWidget(cell);
} else {
this.addWidget(new MessageWidget({ message }));
}
}

refresh(messages: IChatMessage[]) {
let diff = messages.filter(x => !this.messages.includes(x) );
diff.forEach(this.addMessage.bind(this));
}

protected onChildAdded(msg: Widget.ChildMessage): void {
setTimeout(() => {
const node = this.node;
node.scrollTop = node.scrollHeight - node.clientHeight;
}, 300);
}

private _createCodeCellOptions(): CodeCell.IOptions {
const contentFactory = this.contentFactory;
const modelFactory = this.modelFactory;
const model = modelFactory.createCodeCell({});
const rendermime = this.rendermime;
return { model, rendermime, contentFactory, placeholder: false };
}

private _createCodeCell(): CodeCell {
const factory = this.contentFactory;
const options = this._createCodeCellOptions();
const cell = factory.createCodeCell(options);
cell.readOnly = true;
//cell.model.mimeType = this._mimetype;
return cell;
}
}
Loading

0 comments on commit 0abc886

Please sign in to comment.