diff --git a/src/index.ts b/src/index.ts index 9a5f0c3..2592844 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { ICodeCellModel } from '@jupyterlab/cells'; +import { Cell, ICodeCellModel } from '@jupyterlab/cells'; import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; @@ -129,7 +129,37 @@ const plugin: JupyterFrontEndPlugin = { chatWidget.title.closable = true; app.shell.add(chatWidget, 'right'); - // Keep the enabled state in sync when the active cell changes. + chatWidget.node.addEventListener('click', (event: MouseEvent) => { + const target = event.target; + if (target instanceof HTMLAnchorElement) { + const href = target.getAttribute('href'); + if (href && href.startsWith('#tutor-cell-')) { + event.preventDefault(); + const cellId = href.slice('#tutor-cell-'.length); + if (!notebookTracker) { + return; + } + const notebookPanel = notebookTracker.find((panel: NotebookPanel) => + panel.content.widgets.some( + (widget: Cell) => widget.model.id === cellId + ) + ); + if (notebookPanel) { + app.shell.activateById(notebookPanel.id); + const index = notebookPanel.content.widgets.findIndex( + (widget: Cell) => widget.model.id === cellId + ); + if (index !== -1) { + notebookPanel.content.activeCellIndex = index; + notebookPanel.content.widgets[index].node.scrollIntoView({ + block: 'nearest' + }); + } + } + } + } + }); + notebookTracker?.activeCellChanged.connect(() => { commands.notifyCommandChanged(CommandIDs.explainCode); }); @@ -315,6 +345,7 @@ const plugin: JupyterFrontEndPlugin = { body: bodyContent, formattedBody: formattedBody, notebookPath, + cellId: cell.model.id, attachments: attachment ? [attachment] : undefined }); }, diff --git a/src/model.ts b/src/model.ts index 02c012e..72fc30a 100644 --- a/src/model.ts +++ b/src/model.ts @@ -14,6 +14,7 @@ import { AI_AVATAR } from './icons'; interface ITutorNewMessage extends INewMessage { attachments?: IAttachment[]; + cellId?: string; notebookPath?: string; formattedBody?: string; } @@ -96,6 +97,10 @@ export class TutorChatModel extends AbstractChatModel { accumulated += chunk; streamingMsg.update({ body: accumulated }); } + if (message.cellId) { + accumulated += `\n\n[Go to Cell](#tutor-cell-${message.cellId})`; + streamingMsg.update({ body: accumulated }); + } } catch (err) { if (err instanceof Error && err.name === 'AbortError') return; const errorText = err instanceof Error ? err.message : String(err); diff --git a/style/base.css b/style/base.css index 05bba3d..2b65bec 100644 --- a/style/base.css +++ b/style/base.css @@ -20,6 +20,25 @@ border-top: none; } +/* Style Go to Cell links as standard buttons */ +#jupyter-ai-tutor-panel a[href^="#tutor-cell-"] { + display: inline-block; + background-color: var(--jp-brand-color1, #2196f3); + color: var(--jp-layout-color0, #ffffff) !important; + text-decoration: none; + border-radius: 3px; + padding: 4px 8px; + font-weight: 500; + font-size: var(--jp-ui-font-size0, 11px); + white-space: nowrap; + margin-top: 8px; +} + +#jupyter-ai-tutor-panel a[href^="#tutor-cell-"]:hover { + background-color: var(--jp-brand-color0, #1976d2); + text-decoration: none; +} + /* Override toolbar visibility on cell overlap */ /* stylelint-disable-next-line selector-class-pattern */ .jp-toolbar-overlap .jp-cell-toolbar {