From e6100f93058ba9b507bd76fbcc21ac26241efabd Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Wed, 8 Jul 2026 16:46:30 +0100 Subject: [PATCH 1/2] Add warning message --- src/index.ts | 94 ++++++++++++++++++++++++++++++++++++++++++++------ style/base.css | 51 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 82bd3d1..1464ec4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,12 +10,13 @@ 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'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { infoIcon } from '@jupyterlab/ui-components'; +import { Panel, Widget } from '@lumino/widgets'; import { clearItem, stopItem } from './components'; import { TUTOR_USER, TutorChatModel } from './model'; @@ -123,15 +124,83 @@ const plugin: JupyterFrontEndPlugin = { attachmentOpenerRegistry, inputToolbarRegistry }); - chatWidget.id = 'jupyter-ai-tutor-panel'; - chatWidget.title.label = trans.__('Tutor'); - chatWidget.title.caption = trans.__('Tutor'); - chatWidget.title.closable = true; - app.shell.add(chatWidget, 'right'); + chatWidget.id = 'jupyter-ai-tutor-chat'; + + // Track cell currently being explained + let lastExplainedCellId: string | null = null; + + // Create the warning banner element + const warningBanner = document.createElement('div'); + warningBanner.className = 'jp-jupyter-ai-tutor-warning-banner'; + + const warningText = document.createElement('span'); + warningText.className = 'jp-jupyter-ai-tutor-warning-text'; + warningText.textContent = trans.__( + 'Warning: You have moved away from the cell for which this feedback is generated.' + ); + warningBanner.appendChild(warningText); + + const warningButton = document.createElement('button'); + warningButton.className = 'jp-jupyter-ai-tutor-warning-btn'; + warningButton.textContent = trans.__('Go to Cell'); + warningBanner.appendChild(warningButton); + + // Create the warning banner widget wrapping the warningBanner node + const warningWidget = new Widget({ node: warningBanner }); + warningWidget.id = 'jupyter-ai-tutor-warning-widget'; + + // Wrap the warning widget and chat widget in a main panel using Panel + const mainPanel = new Panel(); + mainPanel.id = 'jupyter-ai-tutor-panel'; + mainPanel.title.label = trans.__('Tutor'); + mainPanel.title.caption = trans.__('Tutor'); + mainPanel.title.closable = true; + + // Add widgets to panel + mainPanel.addWidget(warningWidget); + mainPanel.addWidget(chatWidget); + + warningWidget.hide(); + app.shell.add(mainPanel, 'right'); + + // Handle "Go to Cell" click to scroll back to relevant cell + warningButton.addEventListener('click', () => { + if (!lastExplainedCellId || !notebookTracker) { + return; + } + const notebookPanel = notebookTracker.find((panel: NotebookPanel) => + panel.content.widgets.some( + (widget: Cell) => widget.model.id === lastExplainedCellId + ) + ); + if (notebookPanel) { + app.shell.activateById(notebookPanel.id); + const index = notebookPanel.content.widgets.findIndex( + (widget: Cell) => widget.model.id === lastExplainedCellId + ); + if (index !== -1) { + notebookPanel.content.activeCellIndex = index; + notebookPanel.content.widgets[index].node.scrollIntoView({ + block: 'nearest' + }); + } + } + }); - // Keep the enabled state in sync when the active cell changes. notebookTracker?.activeCellChanged.connect(() => { commands.notifyCommandChanged(CommandIDs.explainCode); + + const activeCell = notebookTracker?.activeCell; + + if (lastExplainedCellId && activeCell) { + if (activeCell.model.id !== lastExplainedCellId) { + warningWidget.show(); + } else { + warningWidget.hide(); + } + } else { + warningWidget.hide(); + } }); // Listen for writers change to display the stop button. @@ -155,6 +224,8 @@ const plugin: JupyterFrontEndPlugin = { inputToolbarRegistry?.show('clear'); } else { inputToolbarRegistry?.hide('clear'); + lastExplainedCellId = null; + warningWidget.hide(); } } @@ -174,6 +245,9 @@ const plugin: JupyterFrontEndPlugin = { const cell = notebookTracker?.activeCell; if (!cell || cell.model.type !== 'code') return; + lastExplainedCellId = cell.model.id; + warningWidget.hide(); + const source = cell.model.sharedModel.source.trim(); if (!source) return; @@ -247,10 +321,10 @@ const plugin: JupyterFrontEndPlugin = { : 'Can you explain this code?'; const body = `${question}\n\n\`\`\`${language}\n${source}\n\`\`\`${errorSection}\n`; - if (!chatWidget.isAttached) { - app.shell.add(chatWidget, 'right'); + if (!mainPanel.isAttached) { + app.shell.add(mainPanel, 'right'); } - app.shell.activateById(chatWidget.id); + app.shell.activateById(mainPanel.id); await tutorModel.sendMessageToAI({ body, diff --git a/style/base.css b/style/base.css index ac0d54f..f6c128e 100644 --- a/style/base.css +++ b/style/base.css @@ -19,3 +19,54 @@ #jupyter-ai-tutor-panel .jp-chat-input-container .jp-chat-input-toolbar { border-top: none; } + +/* Flex layout for sidebar panel and chat */ +/* stylelint-disable-next-line selector-class-pattern */ +#jupyter-ai-tutor-panel { + display: flex; + flex-direction: column; + height: 100%; +} + +/* stylelint-disable-next-line selector-class-pattern */ +#jupyter-ai-tutor-chat { + flex: 1; + min-height: 0; +} + +/* Warning banner for focus transitions */ +/* stylelint-disable-next-line selector-class-pattern */ +.jp-jupyter-ai-tutor-warning-banner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background-color: var(--jp-warn-color3, #fffbeb); + border-bottom: 1px solid var(--jp-warn-color1, #f59e0b); + color: var(--jp-ui-font-color1, #78350f); + font-size: var(--jp-ui-font-size1, 13px); +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jp-jupyter-ai-tutor-warning-text { + flex: 1; + margin-right: 8px; +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jp-jupyter-ai-tutor-warning-btn { + background: var(--jp-brand-color1, #2196f3); + color: var(--jp-layout-color0, #ffffff); + border: none; + border-radius: 3px; + padding: 4px 8px; + cursor: pointer; + font-weight: 500; + font-size: var(--jp-ui-font-size0, 11px); + white-space: nowrap; +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jp-jupyter-ai-tutor-warning-btn:hover { + background: var(--jp-brand-color0, #1976d2); +} From df4f5a4c02cf629591ec92a2129f48a550425357 Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Thu, 9 Jul 2026 12:55:45 +0100 Subject: [PATCH 2/2] Implement 'Go To Cell' link for agent responses --- src/index.ts | 117 ++++++++++++++++--------------------------------- src/model.ts | 5 +++ style/base.css | 52 +++++----------------- 3 files changed, 52 insertions(+), 122 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1464ec4..8e36138 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,6 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { infoIcon } from '@jupyterlab/ui-components'; -import { Panel, Widget } from '@lumino/widgets'; import { clearItem, stopItem } from './components'; import { TUTOR_USER, TutorChatModel } from './model'; @@ -124,83 +123,45 @@ const plugin: JupyterFrontEndPlugin = { attachmentOpenerRegistry, inputToolbarRegistry }); - chatWidget.id = 'jupyter-ai-tutor-chat'; - - // Track cell currently being explained - let lastExplainedCellId: string | null = null; - - // Create the warning banner element - const warningBanner = document.createElement('div'); - warningBanner.className = 'jp-jupyter-ai-tutor-warning-banner'; - - const warningText = document.createElement('span'); - warningText.className = 'jp-jupyter-ai-tutor-warning-text'; - warningText.textContent = trans.__( - 'Warning: You have moved away from the cell for which this feedback is generated.' - ); - warningBanner.appendChild(warningText); - - const warningButton = document.createElement('button'); - warningButton.className = 'jp-jupyter-ai-tutor-warning-btn'; - warningButton.textContent = trans.__('Go to Cell'); - warningBanner.appendChild(warningButton); - - // Create the warning banner widget wrapping the warningBanner node - const warningWidget = new Widget({ node: warningBanner }); - warningWidget.id = 'jupyter-ai-tutor-warning-widget'; - - // Wrap the warning widget and chat widget in a main panel using Panel - const mainPanel = new Panel(); - mainPanel.id = 'jupyter-ai-tutor-panel'; - mainPanel.title.label = trans.__('Tutor'); - mainPanel.title.caption = trans.__('Tutor'); - mainPanel.title.closable = true; - - // Add widgets to panel - mainPanel.addWidget(warningWidget); - mainPanel.addWidget(chatWidget); - - warningWidget.hide(); - app.shell.add(mainPanel, 'right'); - - // Handle "Go to Cell" click to scroll back to relevant cell - warningButton.addEventListener('click', () => { - if (!lastExplainedCellId || !notebookTracker) { - return; - } - const notebookPanel = notebookTracker.find((panel: NotebookPanel) => - panel.content.widgets.some( - (widget: Cell) => widget.model.id === lastExplainedCellId - ) - ); - if (notebookPanel) { - app.shell.activateById(notebookPanel.id); - const index = notebookPanel.content.widgets.findIndex( - (widget: Cell) => widget.model.id === lastExplainedCellId - ); - if (index !== -1) { - notebookPanel.content.activeCellIndex = index; - notebookPanel.content.widgets[index].node.scrollIntoView({ - block: 'nearest' - }); + chatWidget.id = 'jupyter-ai-tutor-panel'; + chatWidget.title.label = trans.__('Tutor'); + chatWidget.title.caption = trans.__('Tutor'); + chatWidget.title.closable = true; + app.shell.add(chatWidget, 'right'); + + 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); - - const activeCell = notebookTracker?.activeCell; - - if (lastExplainedCellId && activeCell) { - if (activeCell.model.id !== lastExplainedCellId) { - warningWidget.show(); - } else { - warningWidget.hide(); - } - } else { - warningWidget.hide(); - } }); // Listen for writers change to display the stop button. @@ -224,8 +185,6 @@ const plugin: JupyterFrontEndPlugin = { inputToolbarRegistry?.show('clear'); } else { inputToolbarRegistry?.hide('clear'); - lastExplainedCellId = null; - warningWidget.hide(); } } @@ -245,9 +204,6 @@ const plugin: JupyterFrontEndPlugin = { const cell = notebookTracker?.activeCell; if (!cell || cell.model.type !== 'code') return; - lastExplainedCellId = cell.model.id; - warningWidget.hide(); - const source = cell.model.sharedModel.source.trim(); if (!source) return; @@ -321,14 +277,15 @@ const plugin: JupyterFrontEndPlugin = { : 'Can you explain this code?'; const body = `${question}\n\n\`\`\`${language}\n${source}\n\`\`\`${errorSection}\n`; - if (!mainPanel.isAttached) { - app.shell.add(mainPanel, 'right'); + if (!chatWidget.isAttached) { + app.shell.add(chatWidget, 'right'); } - app.shell.activateById(mainPanel.id); + app.shell.activateById(chatWidget.id); await tutorModel.sendMessageToAI({ body, description, + cellId: cell.model.id, attachments: attachment ? [attachment] : undefined }); }, diff --git a/src/model.ts b/src/model.ts index fb1aa42..4f5a030 100644 --- a/src/model.ts +++ b/src/model.ts @@ -15,6 +15,7 @@ import { AI_AVATAR } from './icons'; interface ITutorNewMessage extends INewMessage { attachments?: IAttachment[]; description?: string; + cellId?: string; } export const TUTOR_USER: IUser = { @@ -95,6 +96,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 f6c128e..9670d1b 100644 --- a/style/base.css +++ b/style/base.css @@ -20,53 +20,21 @@ border-top: none; } -/* Flex layout for sidebar panel and chat */ -/* stylelint-disable-next-line selector-class-pattern */ -#jupyter-ai-tutor-panel { - display: flex; - flex-direction: column; - height: 100%; -} - -/* stylelint-disable-next-line selector-class-pattern */ -#jupyter-ai-tutor-chat { - flex: 1; - min-height: 0; -} - -/* Warning banner for focus transitions */ -/* stylelint-disable-next-line selector-class-pattern */ -.jp-jupyter-ai-tutor-warning-banner { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background-color: var(--jp-warn-color3, #fffbeb); - border-bottom: 1px solid var(--jp-warn-color1, #f59e0b); - color: var(--jp-ui-font-color1, #78350f); - font-size: var(--jp-ui-font-size1, 13px); -} - -/* stylelint-disable-next-line selector-class-pattern */ -.jp-jupyter-ai-tutor-warning-text { - flex: 1; - margin-right: 8px; -} - -/* stylelint-disable-next-line selector-class-pattern */ -.jp-jupyter-ai-tutor-warning-btn { - background: var(--jp-brand-color1, #2196f3); - color: var(--jp-layout-color0, #ffffff); - border: 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; - cursor: pointer; font-weight: 500; font-size: var(--jp-ui-font-size0, 11px); white-space: nowrap; + margin-top: 8px; } -/* stylelint-disable-next-line selector-class-pattern */ -.jp-jupyter-ai-tutor-warning-btn:hover { - background: var(--jp-brand-color0, #1976d2); +#jupyter-ai-tutor-panel a[href^="#tutor-cell-"]:hover { + background-color: var(--jp-brand-color0, #1976d2); + text-decoration: none; }