Skip to content

Commit 82a9ee8

Browse files
feat(lightspeed): creating a new notebook and upload the documents (#2704)
* feat(lightspeed): create notebook flow Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * feat(lightspeed): creating new notebook Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * updating status of documents Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * updating the unit tests Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * handling overwrite in create flow Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * adding changeset Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * adding subroute for notebook Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * addressing the comments Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * updating the test Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * updating test Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> * refetch notebooks on close notebook Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com> --------- Signed-off-by: its-mitesh-kumar <itsmiteshkumar98@gmail.com>
1 parent 501e099 commit 82a9ee8

39 files changed

Lines changed: 3052 additions & 72 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-lightspeed': minor
3+
---
4+
5+
Add notebook creation and document upload flow with file type validation, overwrite confirmation, collapsible document sidebar, file type icons, i18n support, and unit tests.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ describe('SessionService', () => {
250250
await service.createSession(mockUserId, 'Session 1', 'Description 1');
251251
await new Promise(resolve => setTimeout(resolve, 10));
252252
await service.createSession(mockUserId, 'Session 2', 'Description 2');
253+
await new Promise(resolve => setTimeout(resolve, 10));
253254
await service.createSession(mockUserId2, 'Other Session', 'Other');
254255

255256
const sessions = await service.listSessions(mockUserId);

workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,30 @@ export const lightspeedTranslationRef: TranslationRef<
223223
readonly 'notebooks.updated.yesterday': string;
224224
readonly 'notebooks.updated.days': string;
225225
readonly 'notebooks.updated.on': string;
226+
readonly 'notebook.view.title': string;
227+
readonly 'notebook.view.close': string;
228+
readonly 'notebook.view.documents.count': string;
229+
readonly 'notebook.view.documents.add': string;
230+
readonly 'notebook.view.upload.heading': string;
231+
readonly 'notebook.view.upload.action': string;
232+
readonly 'notebook.view.input.placeholder': string;
233+
readonly 'notebook.view.sidebar.collapse': string;
234+
readonly 'notebook.view.sidebar.expand': string;
235+
readonly 'notebook.view.sidebar.resize': string;
236+
readonly 'notebook.view.documents.uploading': string;
237+
readonly 'notebook.upload.success': string;
238+
readonly 'notebook.upload.failed': string;
239+
readonly 'notebook.upload.modal.title': string;
240+
readonly 'notebook.upload.modal.dragDropTitle': string;
241+
readonly 'notebook.upload.modal.browseButton': string;
242+
readonly 'notebook.upload.modal.separator': string;
243+
readonly 'notebook.upload.modal.infoText': string;
244+
readonly 'notebook.upload.error.unsupportedType': string;
245+
readonly 'notebook.upload.error.fileTooLarge': string;
246+
readonly 'notebook.upload.error.tooManyFiles': string;
247+
readonly 'notebook.overwrite.modal.title': string;
248+
readonly 'notebook.overwrite.modal.description': string;
249+
readonly 'notebook.overwrite.modal.action': string;
226250
readonly 'conversation.delete.confirm.title': string;
227251
readonly 'conversation.delete.confirm.message': string;
228252
readonly 'conversation.delete.confirm.action': string;

workspaces/lightspeed/plugins/lightspeed/report.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const lightspeedPlugin: BackstagePlugin<
5151
lightspeedConversation: SubRouteRef<
5252
PathParams<'/conversation/:conversationId'>
5353
>;
54+
lightspeedNotebooks: SubRouteRef<undefined>;
5455
},
5556
{},
5657
{}

workspaces/lightspeed/plugins/lightspeed/src/api/NotebooksApiClient.ts

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616

1717
import { ConfigApi, FetchApi } from '@backstage/core-plugin-api';
1818

19-
import { NotebookSession } from '../types';
19+
import {
20+
DocumentListResponse,
21+
DocumentStatus,
22+
NotebookSession,
23+
SessionResponse,
24+
UploadDocumentResponse,
25+
} from '../types';
2026
import { NotebooksAPI } from './notebooksApi';
2127

2228
/**
@@ -45,6 +51,27 @@ export class NotebooksApiClient implements NotebooksAPI {
4551
return `${this.configApi.getString('backend.baseUrl')}/api/lightspeed/ai-notebooks`;
4652
}
4753

54+
private async handleResponseError(response: Response): Promise<string> {
55+
let errorMessage = `failed to fetch data, status ${response.status}: ${response.statusText}`;
56+
try {
57+
const errorText = await response.text();
58+
if (errorText) {
59+
try {
60+
const errorBody = JSON.parse(errorText);
61+
if (errorBody?.error) {
62+
errorMessage = errorBody.error;
63+
}
64+
} catch {
65+
errorMessage = errorText;
66+
}
67+
}
68+
} catch (e) {
69+
// eslint-disable-next-line no-console
70+
console.warn(e);
71+
}
72+
return errorMessage;
73+
}
74+
4875
private async fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
4976
const response = await this.fetchApi.fetch(url, {
5077
headers: {
@@ -54,24 +81,7 @@ export class NotebooksApiClient implements NotebooksAPI {
5481
});
5582

5683
if (!response.ok) {
57-
let errorMessage = `failed to fetch data, status ${response.status}: ${response.statusText}`;
58-
try {
59-
const errorText = await response.text();
60-
if (errorText) {
61-
try {
62-
const errorBody = JSON.parse(errorText);
63-
if (errorBody?.error) {
64-
errorMessage = errorBody.error;
65-
}
66-
} catch {
67-
errorMessage = errorText;
68-
}
69-
}
70-
} catch (e) {
71-
// eslint-disable-next-line no-console
72-
console.warn(e);
73-
}
74-
throw new Error(errorMessage);
84+
throw new Error(await this.handleResponseError(response));
7585
}
7686

7787
const text = await response.text();
@@ -81,6 +91,42 @@ export class NotebooksApiClient implements NotebooksAPI {
8191
return JSON.parse(text) as T;
8292
}
8393

94+
private async fetchFormData<T>(
95+
url: string,
96+
formData: FormData,
97+
method: string = 'PUT',
98+
): Promise<T> {
99+
const response = await this.fetchApi.fetch(url, {
100+
method,
101+
body: formData,
102+
});
103+
104+
if (!response.ok && response.status !== 202) {
105+
throw new Error(await this.handleResponseError(response));
106+
}
107+
108+
const text = await response.text();
109+
if (!text) {
110+
return {} as T;
111+
}
112+
return JSON.parse(text) as T;
113+
}
114+
115+
async createSession(name: string, description?: string) {
116+
const baseUrl = await this.getBaseUrl();
117+
const response = await this.fetchJson<SessionResponse>(
118+
`${baseUrl}/v1/sessions`,
119+
{
120+
method: 'POST',
121+
body: JSON.stringify({ name, description }),
122+
},
123+
);
124+
if (!response.session) {
125+
throw new Error(response.error ?? 'Failed to create session');
126+
}
127+
return response.session;
128+
}
129+
84130
async listSessions() {
85131
const baseUrl = await this.getBaseUrl();
86132
const response = await this.fetchJson<{ sessions?: NotebookSession[] }>(
@@ -109,4 +155,54 @@ export class NotebooksApiClient implements NotebooksAPI {
109155
},
110156
);
111157
}
158+
159+
async uploadDocument(
160+
sessionId: string,
161+
file: File,
162+
fileType: string,
163+
title: string,
164+
newTitle?: string,
165+
) {
166+
const baseUrl = await this.getBaseUrl();
167+
const formData = new FormData();
168+
formData.append('file', file);
169+
formData.append('fileType', fileType);
170+
formData.append('title', title);
171+
if (newTitle) {
172+
formData.append('newTitle', newTitle);
173+
}
174+
return this.fetchFormData<UploadDocumentResponse>(
175+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}/documents`,
176+
formData,
177+
);
178+
}
179+
180+
async listDocuments(sessionId: string) {
181+
const baseUrl = await this.getBaseUrl();
182+
const response = await this.fetchJson<DocumentListResponse>(
183+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}/documents`,
184+
);
185+
const docs = response?.documents ?? [];
186+
return docs.map(doc => ({
187+
...doc,
188+
title: (doc as any).title ?? doc.document_id,
189+
}));
190+
}
191+
192+
async deleteDocument(sessionId: string, documentId: string) {
193+
const baseUrl = await this.getBaseUrl();
194+
await this.fetchJson(
195+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}/documents/${encodeURIComponent(documentId)}`,
196+
{
197+
method: 'DELETE',
198+
},
199+
);
200+
}
201+
202+
async getDocumentStatus(sessionId: string, documentId: string) {
203+
const baseUrl = await this.getBaseUrl();
204+
return this.fetchJson<DocumentStatus>(
205+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}/documents/${encodeURIComponent(documentId)}/status`,
206+
);
207+
}
112208
}

workspaces/lightspeed/plugins/lightspeed/src/api/notebooksApi.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,38 @@
1616

1717
import { createApiRef, type ApiRef } from '@backstage/core-plugin-api';
1818

19-
import { NotebookSession } from '../types';
19+
import {
20+
DocumentStatus,
21+
NotebookSession,
22+
SessionDocument,
23+
UploadDocumentResponse,
24+
} from '../types';
2025

2126
/**
2227
* @public
2328
* AI Notebooks API
2429
*/
2530
export type NotebooksAPI = {
31+
createSession: (
32+
name: string,
33+
description?: string,
34+
) => Promise<NotebookSession>;
2635
listSessions: () => Promise<NotebookSession[]>;
2736
renameSession: (sessionId: string, name: string) => Promise<void>;
2837
deleteSession: (sessionId: string) => Promise<void>;
38+
uploadDocument: (
39+
sessionId: string,
40+
file: File,
41+
fileType: string,
42+
title: string,
43+
newTitle?: string,
44+
) => Promise<UploadDocumentResponse>;
45+
listDocuments: (sessionId: string) => Promise<SessionDocument[]>;
46+
deleteDocument: (sessionId: string, documentId: string) => Promise<void>;
47+
getDocumentStatus: (
48+
sessionId: string,
49+
documentId: string,
50+
) => Promise<DocumentStatus>;
2951
};
3052

3153
/**

0 commit comments

Comments
 (0)