diff --git a/src/chat-handler.ts b/src/chat-handler.ts
index 4d10fe5..ba2a9d3 100644
--- a/src/chat-handler.ts
+++ b/src/chat-handler.ts
@@ -39,12 +39,12 @@ export const welcomeMessage = (providers: string[]) => `
#### Ask JupyterLite AI
-The provider to use can be set in the , by selecting it from
-the
_AI provider_ settings.
+The provider to use can be set in the , by selecting it from
+the
_AI providers_ settings.
The current providers that are available are _${providers.sort().join('_, _')}_.
-To clear the chat, you can use the \`/clear\` command from the chat input.
+- To clear the chat, you can use the \`/clear\` command from the chat input.
`;
export type ConnectionMessage = {
diff --git a/src/components/attach-button.tsx b/src/components/attach-button.tsx
new file mode 100644
index 0000000..c43751d
--- /dev/null
+++ b/src/components/attach-button.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+
+import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
+import AttachFileIcon from '@mui/icons-material/AttachFile';
+import React from 'react';
+import { showFileBrowserModal } from '../utils/show-file-browser';
+
+/**
+ * Properties of the attach button.
+ */
+export interface IAttachButtonProps
+ extends InputToolbarRegistry.IToolbarItemProps {}
+
+/**
+ The attach button.
+ */
+export function AttachButton(
+ props: InputToolbarRegistry.IToolbarItemProps
+): JSX.Element {
+ const tooltip = 'Attach a file';
+
+ const handleClick = async () => {
+ const inputModel = props.model;
+ if (!inputModel) {
+ console.warn('No input model found.');
+ return;
+ }
+
+ try {
+ await showFileBrowserModal(inputModel);
+ } catch (e) {
+ console.error('Error opening file modal:', e);
+ }
+ };
+
+ return (
+
+
+
+ );
+}
+
+/**
+ * Factory returning the toolbar item.
+ * This function creates an attach button that allows users to attach files.
+ *
+ * @returns An InputToolbarRegistry.IToolbarItem for the attach button.
+ */
+
+export function attachItem(): InputToolbarRegistry.IToolbarItem {
+ return {
+ element: (props: InputToolbarRegistry.IToolbarItemProps) => {
+ return ;
+ },
+ position: 40,
+ hidden: false
+ };
+}
diff --git a/src/index.ts b/src/index.ts
index 06e870d..e43cdfe 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -28,6 +28,7 @@ import { AIProviderRegistry } from './provider';
import { aiSettingsRenderer, textArea } from './settings';
import { IAIProviderRegistry, PLUGIN_IDS } from './tokens';
import { stopItem } from './components/stop-button';
+import { attachItem } from './components/attach-button';
const chatCommandRegistryPlugin: JupyterFrontEndPlugin = {
id: PLUGIN_IDS.chatCommandRegistry,
@@ -110,6 +111,8 @@ const chatPlugin: JupyterFrontEndPlugin = {
let chatWidget: ReactWidget | null = null;
const inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
+ const attachButton = attachItem();
+ inputToolbarRegistry.addItem('_attach', attachButton);
const stopButton = stopItem(() => chatHandler.stopStreaming());
inputToolbarRegistry.addItem('stop', stopButton);
diff --git a/src/utils/show-file-browser.ts b/src/utils/show-file-browser.ts
new file mode 100644
index 0000000..b3d79c9
--- /dev/null
+++ b/src/utils/show-file-browser.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+
+import { IInputModel } from '@jupyter/chat';
+import { ContentsManager } from '@jupyterlab/services';
+
+/**
+ * Opens a modal file browser and updates the inputModel with the selected file path.
+ */
+export async function showFileBrowserModal(
+ inputModel: IInputModel
+): Promise {
+ const contents = new ContentsManager();
+
+ return new Promise(resolve => {
+ const modal = document.createElement('div');
+ modal.className = 'file-browser-modal';
+ modal.innerHTML = `
+
+
Select a File
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+
+ const fileList = modal.querySelector('.file-list')!;
+ const closeBtn = modal.querySelector('.close-btn') as HTMLButtonElement;
+ const backBtn = modal.querySelector('.back-btn') as HTMLButtonElement;
+ let currentPath = '';
+
+ const listDir = async (path = '') => {
+ try {
+ const dir = await contents.get(path, { content: true });
+ fileList.innerHTML = '';
+
+ for (const item of dir.content) {
+ const li = document.createElement('li');
+ if (item.type === 'directory') {
+ li.textContent = `${item.name}/`;
+ li.className = 'directory';
+ } else if (item.type === 'file' || item.type === 'notebook') {
+ li.textContent = item.name;
+ li.className = 'file';
+ }
+
+ fileList.appendChild(li);
+
+ li.onclick = async () => {
+ try {
+ if (item.type === 'directory') {
+ currentPath = item.path;
+ await listDir(item.path);
+ } else {
+ const existingText = inputModel.value?.trim();
+ inputModel.value = `${existingText} \`${item.path}\``.trim();
+ li.style.backgroundColor = '#d2f8d2';
+ document.body.removeChild(modal);
+ resolve();
+ }
+ } catch (error) {
+ console.error(error);
+ document.body.removeChild(modal);
+ resolve();
+ }
+ };
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ closeBtn.onclick = () => {
+ document.body.removeChild(modal);
+ resolve();
+ };
+
+ backBtn.onclick = () => {
+ if (!currentPath || currentPath === '') {
+ return;
+ }
+ const parts = currentPath.split('/');
+ parts.pop();
+ currentPath = parts.join('/');
+ listDir(currentPath);
+ };
+
+ listDir();
+ });
+}
diff --git a/style/base.css b/style/base.css
index c4925ee..7e6a60f 100644
--- a/style/base.css
+++ b/style/base.css
@@ -43,3 +43,99 @@
border-color: var(--jp-brand-color1);
color: var(--jp-brand-color1);
}
+
+.file-browser-modal {
+ position: fixed;
+ top: 20%;
+ left: 30%;
+ width: 40%;
+ background: #fff;
+ border: 1px solid #414040;
+ padding: 1em;
+ z-index: 9999;
+ box-shadow: 0 0 10px #0003;
+ border-radius: 8px;
+ font-family: sans-serif;
+}
+
+.file-browser-panel {
+ display: flex;
+ flex-direction: column;
+}
+
+.file-list {
+ list-style: none;
+ padding-left: 0;
+ max-height: 200px;
+ overflow-y: auto;
+ margin: 1em 0;
+}
+
+.file-list li {
+ padding: 6px 8px;
+ cursor: pointer;
+ border-bottom: 1px solid #eee;
+}
+
+.file-list li:hover {
+ background-color: #f5f5f5;
+}
+
+.file-browser-panel .button-row {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 1rem;
+ gap: 0.5rem;
+}
+
+.back-btn,
+.close-btn {
+ background: #f9f9f9;
+ border: 1px solid #888;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ color: #333;
+}
+
+.back-btn:hover {
+ background-color: #e6e6e6;
+ color: #111;
+ border-color: #666;
+}
+
+.close-btn {
+ color: #a00;
+ border-color: #a00;
+}
+
+.close-btn:hover {
+ background-color: #fcecec;
+ color: #700;
+ border-color: #700;
+}
+
+.file {
+ color: #145196;
+ font-weight: bold;
+ position: relative;
+}
+
+.file::after {
+ content: ' —— File';
+ font-weight: normal;
+ color: #867f7fda;
+}
+
+.directory {
+ color: #0f9145;
+ font-weight: bold;
+ position: relative;
+}
+
+.directory::after {
+ content: ' —— Directory';
+ font-weight: normal;
+ color: #867f7fda;
+}