diff --git a/.gitignore b/.gitignore index 396b5ef..c45b17a 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,8 @@ dmypy.json # Yarn cache .yarn/ + +# VSCode settings +*.code-workspace +.history/ +.vscode/ diff --git a/README.md b/README.md index d775a99..9ff3067 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,20 @@ The `jlpm` command is JupyterLab's pinned version of `yarn` or `npm` in lieu of `jlpm` below. ```bash -# Clone the repo to your local environment +# Clone the repo to your local environment, eg +gh repo clone jtpio/jupyterlab-ai-commands + # Change directory to the jupyterlab_ai_commands directory +cd jupyterlab-ai-commands + # Install package in development mode pip install -e "." +# or +uv pip install -e "." + # Link your development version of the extension with JupyterLab jupyter labextension develop . --overwrite + # Rebuild extension Typescript source after making changes jlpm build ``` @@ -147,6 +155,7 @@ You can watch the source directory and run JupyterLab at the same time in differ ```bash # Watch the source directory in one terminal, automatically rebuilding when needed jlpm watch + # Run JupyterLab in another terminal jupyter lab ``` diff --git a/package.json b/package.json index df663e7..ba2588c 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/json-schema": "^7.0.11", "@types/react": "^18.0.26", "@types/react-addons-linked-state-mixin": "^0.14.22", + "@types/w3c-image-capture": "^1.0.11", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", diff --git a/src/notebook-commands.ts b/src/notebook-commands.ts index fad929a..5259262 100644 --- a/src/notebook-commands.ts +++ b/src/notebook-commands.ts @@ -791,6 +791,73 @@ function registerSaveNotebookCommand( commands.addCommand(command.id, command); } +/** + * Save a specific notebook to disk + */ +function registerScreenshotCommand( + commands: CommandRegistry, + docManager: IDocumentManager, + notebookTracker?: INotebookTracker +): void { + const command = { + id: 'jupyterlab-ai-commands:screenshot-window', + label: 'Take window screenshot', + caption: 'Take a screenshot of the current browser window, return as base64 png', + describedBy: { + args: { + notebookPath: { + description: + 'Path to the notebook file. If not provided, uses the currently active notebook' + } + } + }, + execute: async (args: any) => { + const { notebookPath } = args; + + const currentWidget = await getNotebookWidget( + notebookPath, + docManager, + notebookTracker + ); + if (!currentWidget) { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + + const displayMediaOptions = { + video: true, + audio: false, + }; + const stream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); + const [track] = stream.getVideoTracks(); + const imageCapture = new ImageCapture(track); + const frame = await imageCapture.grabFrame(); + track.stop(); + + const canvas = document.createElement('canvas'); + canvas.width = frame.width; + canvas.height = frame.height; + const context = canvas.getContext('2d'); + context?.drawImage(frame, 0, 0, frame.width, frame.height); + + const dataUrl = canvas.toDataURL('image/png'); + const base64Data = dataUrl.split(',')[1]; + + return { + success: true, + message: 'Notebook saved successfully', + windowScreenshot: base64Data, + }; + } + }; + + commands.addCommand(command.id, command); +} + /** * Options for registering notebook commands */ @@ -829,4 +896,5 @@ export function registerNotebookCommands( registerRunCellCommand(commands, docManager, notebookTracker); registerDeleteCellCommand(commands, docManager, notebookTracker); registerSaveNotebookCommand(commands, docManager, notebookTracker); + registerScreenshotCommand(commands, docManager, notebookTracker); } diff --git a/yarn.lock b/yarn.lock index d328bb8..59fd0f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1769,6 +1769,15 @@ __metadata: languageName: node linkType: hard +"@types/w3c-image-capture@npm:^1.0.11": + version: 1.0.11 + resolution: "@types/w3c-image-capture@npm:1.0.11" + dependencies: + "@types/webrtc": "*" + checksum: 301fa282cf747aa74f22975515c55023f441bba1bcd545afaa49065ee6c09458d9deb4c68406a8dbbd8baace1b31aede502d1ea00f3d3b7cd607be5267f2110b + languageName: node + linkType: hard + "@types/webpack-sources@npm:^0.1.5": version: 0.1.12 resolution: "@types/webpack-sources@npm:0.1.12" @@ -1780,6 +1789,13 @@ __metadata: languageName: node linkType: hard +"@types/webrtc@npm:*": + version: 0.0.47 + resolution: "@types/webrtc@npm:0.0.47" + checksum: b50f5fdc5d0df508aa3f2e7cdc1d53c4256e4f7fa7e01ccd1e59174fc1421e10e89a176a6b96eb401e45cdea5011a761af16170ce262621b8f98eb3f95d8c074 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -4180,6 +4196,7 @@ __metadata: "@types/json-schema": ^7.0.11 "@types/react": ^18.0.26 "@types/react-addons-linked-state-mixin": ^0.14.22 + "@types/w3c-image-capture": ^1.0.11 "@typescript-eslint/eslint-plugin": ^6.1.0 "@typescript-eslint/parser": ^6.1.0 css-loader: ^6.7.1