Skip to content

Commit 3bf22de

Browse files
committed
feat:-Adding command
1 parent 258d16d commit 3bf22de

File tree

3 files changed

+241
-4
lines changed

3 files changed

+241
-4
lines changed

src/chat-handler.ts

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts';
2727
import { jupyternautLiteIcon } from './icons';
2828
import { IAIProviderRegistry } from './tokens';
2929
import { AIChatModel } from './types/ai-model';
30+
import { ContentsManager } from '@jupyterlab/services';
3031

3132
/**
3233
* The base64 encoded SVG string of the jupyternaut lite icon.
@@ -39,12 +40,14 @@ export const welcomeMessage = (providers: string[]) => `
3940
#### Ask JupyterLite AI
4041
4142
42-
The provider to use can be set in the <button data-commandLinker-command="settingeditor:open" data-commandLinker-args='{"query": "AI provider"}' href="#">settings editor</button>, by selecting it from
43-
the <img src="${AI_AVATAR}" width="16" height="16"> _AI provider_ settings.
43+
The provider to use can be set in the <button data-commandLinker-command="settingeditor:open" data-commandLinker-args='{"query": "AI providers"}' href="#">settings editor</button>, by selecting it from
44+
the <img src="${AI_AVATAR}" width="16" height="16"> _AI providers_ settings.
4445
4546
The current providers that are available are _${providers.sort().join('_, _')}_.
4647
47-
To clear the chat, you can use the \`/clear\` command from the chat input.
48+
- To clear the chat, you can use the \`/clear\` command from the chat input.
49+
50+
- To insert file contents into the chat, use the \`/file\` command.
4851
`;
4952

5053
export type ConnectionMessage = {
@@ -252,6 +255,137 @@ export namespace ChatHandler {
252255
}
253256
}
254257

258+
export class FileCommandProvider implements IChatCommandProvider {
259+
public id: string = '@jupyterlite/ai:file-commands';
260+
private _contents = new ContentsManager();
261+
262+
private _slash_commands: ChatCommand[] = [
263+
{
264+
name: '/file',
265+
providerId: this.id,
266+
replaceWith: '/file',
267+
description: 'Include contents of a selected file'
268+
}
269+
];
270+
271+
async listCommandCompletions(inputModel: IInputModel) {
272+
const match = inputModel.currentWord?.match(/^\/\w*/)?.[0];
273+
return match
274+
? this._slash_commands.filter(cmd => cmd.name.startsWith(match))
275+
: [];
276+
}
277+
278+
async onSubmit(inputModel: IInputModel): Promise<void> {
279+
const inputText = inputModel.value?.trim() ?? '';
280+
281+
const fileMentioned = inputText.match(/\/file\s+`[^`]+`/);
282+
const hasFollowUp = inputText.replace(fileMentioned?.[0] || '', '').trim();
283+
284+
if (inputText.startsWith('/file') && !fileMentioned) {
285+
await this._showFileBrowser(inputModel);
286+
} else {
287+
return;
288+
}
289+
290+
if (fileMentioned && hasFollowUp) {
291+
console.log(inputText);
292+
} else {
293+
console.log('Waiting for follow-up text.');
294+
throw new Error('Incomplete /file command');
295+
}
296+
}
297+
298+
private async _showFileBrowser(inputModel: IInputModel): Promise<void> {
299+
return new Promise(resolve => {
300+
const modal = document.createElement('div');
301+
modal.className = 'file-browser-modal';
302+
modal.innerHTML = `
303+
<div class="file-browser-panel">
304+
<h3>Select a File</h3>
305+
<ul class="file-list"></ul>
306+
<div class="button-row">
307+
<button class="back-btn">Back</button>
308+
<button class="close-btn">Close</button>
309+
</div>
310+
</div>
311+
`;
312+
document.body.appendChild(modal);
313+
314+
const fileList = modal.querySelector('.file-list')!;
315+
const closeBtn = modal.querySelector('.close-btn') as HTMLButtonElement;
316+
const backBtn = modal.querySelector('.back-btn') as HTMLButtonElement;
317+
let currentPath = '';
318+
319+
const listDir = async (path = '') => {
320+
try {
321+
const dir = await this._contents.get(path, { content: true });
322+
323+
fileList.innerHTML = '';
324+
325+
for (const item of dir.content) {
326+
const li = document.createElement('li');
327+
if (item.type === 'directory') {
328+
li.textContent = `${item.name}/`;
329+
li.className = 'directory';
330+
} else if (item.type === 'file' || item.type === 'notebook') {
331+
li.textContent = item.name;
332+
li.className = 'file';
333+
}
334+
335+
fileList.appendChild(li);
336+
337+
li.onclick = async () => {
338+
try {
339+
if (item.type === 'directory') {
340+
currentPath = item.path;
341+
await listDir(item.path);
342+
} else if (item.type === 'file' || item.type === 'notebook') {
343+
const existingText = inputModel.value?.trim();
344+
const updatedText =
345+
existingText === '/file'
346+
? `/file \`${item.path}\` `
347+
: `${existingText} \`${item.path}\``;
348+
349+
inputModel.value = updatedText.trim();
350+
li.style.backgroundColor = '#d2f8d2';
351+
352+
document.body.removeChild(modal);
353+
resolve();
354+
}
355+
} catch (error) {
356+
console.error(error);
357+
document.body.removeChild(modal);
358+
resolve();
359+
}
360+
};
361+
362+
fileList.appendChild(li);
363+
}
364+
} catch (err) {
365+
console.error(err);
366+
}
367+
};
368+
369+
closeBtn.onclick = () => {
370+
document.body.removeChild(modal);
371+
resolve();
372+
};
373+
backBtn.onclick = () => {
374+
if (!currentPath || currentPath === '') {
375+
return;
376+
}
377+
378+
const parts = currentPath.split('/');
379+
parts.pop();
380+
currentPath = parts.join('/');
381+
listDir(currentPath);
382+
};
383+
384+
listDir();
385+
});
386+
}
387+
}
388+
255389
namespace Private {
256390
/**
257391
* Return the current timestamp in milliseconds.

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { IFormRendererRegistry } from '@jupyterlab/ui-components';
2121
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
2222
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
2323

24-
import { ChatHandler, welcomeMessage } from './chat-handler';
24+
import {
25+
ChatHandler,
26+
welcomeMessage,
27+
FileCommandProvider
28+
} from './chat-handler';
2529
import { CompletionProvider } from './completion-provider';
2630
import { defaultProviderPlugins } from './default-providers';
2731
import { AIProviderRegistry } from './provider';
@@ -37,6 +41,7 @@ const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
3741
activate: () => {
3842
const registry = new ChatCommandRegistry();
3943
registry.addProvider(new ChatHandler.ClearCommandProvider());
44+
registry.addProvider(new FileCommandProvider());
4045
return registry;
4146
}
4247
};

style/base.css

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,101 @@
4343
border-color: var(--jp-brand-color1);
4444
color: var(--jp-brand-color1);
4545
}
46+
47+
.file-browser-modal {
48+
position: fixed;
49+
top: 20%;
50+
left: 30%;
51+
width: 40%;
52+
background: rgb(255, 255, 255);
53+
border: 1px solid #414040;
54+
padding: 1em;
55+
z-index: 9999;
56+
box-shadow: 0 0 10px rgba(0,0,0,0.2);
57+
border-radius: 8px;
58+
font-family: sans-serif;
59+
}
60+
61+
.file-browser-panel {
62+
display: flex;
63+
flex-direction: column;
64+
}
65+
66+
.file-list {
67+
list-style: none;
68+
padding-left: 0;
69+
max-height: 200px;
70+
overflow-y: auto;
71+
margin: 1em 0;
72+
}
73+
74+
.file-list li {
75+
padding: 6px 8px;
76+
cursor: pointer;
77+
border-bottom: 1px solid #eee;
78+
}
79+
80+
.file-list li:hover {
81+
background-color: #f5f5f5;
82+
}
83+
84+
.file-browser-panel .button-row {
85+
display: flex;
86+
justify-content: space-between;
87+
margin-top: 1rem;
88+
gap: 0.5rem;
89+
}
90+
91+
.back-btn,
92+
.close-btn {
93+
background: #f9f9f9;
94+
border: 1px solid #888;
95+
padding: 6px 12px;
96+
border-radius: 4px;
97+
cursor: pointer;
98+
font-size: 14px;
99+
color: #333;
100+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
101+
}
102+
103+
.back-btn:hover,
104+
.close-btn:hover {
105+
background-color: #e6e6e6;
106+
color: #111;
107+
border-color: #666;
108+
}
109+
110+
.close-btn {
111+
color: #a00;
112+
border-color: #a00;
113+
}
114+
115+
.close-btn:hover {
116+
background-color: #fcecec;
117+
color: #700;
118+
border-color: #700;
119+
}
120+
121+
.file {
122+
color: #145196;
123+
font-weight: bold;
124+
position: relative;
125+
}
126+
127+
.file::after {
128+
content: " —— File";
129+
font-weight: normal;
130+
color: #867f7fda;
131+
}
132+
133+
.directory {
134+
color: #0f9145;
135+
font-weight: bold;
136+
position: relative;
137+
}
138+
139+
.directory::after {
140+
content: " —— Directory";
141+
font-weight: normal;
142+
color: #867f7fda;
143+
}

0 commit comments

Comments
 (0)