Skip to content

Commit f20f9f3

Browse files
feat(lightspeed): implement Notebooks list/dashboard view (#2537)
* feat(lightspeed): add notebooks UI and hooks Made-with: Cursor * feat(lightspeed): sync notebooks updates Resolve PR-branch conflicts and include notebooks components, i18n, and utility changes aligned with latest fixes. Made-with: Cursor * fix(lightspeed): show pointer cursor on tabs Resolve merge conflicts and update tab styles to use a pointer cursor on hover. Made-with: Cursor * fix(lightspeed): resolve notebooks UI conflicts Made-with: Cursor
1 parent 476c4e8 commit f20f9f3

23 files changed

Lines changed: 1675 additions & 134 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 AI notebooks UI, hooks, and backend support for listing/renaming/deleting sessions.

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,30 @@ readonly "prompts.rhdh.title": string;
3737
readonly "prompts.rhdh.message": string;
3838
readonly "page.title": string;
3939
readonly "page.subtitle": string;
40+
readonly "tabs.ariaLabel": string;
41+
readonly "tabs.chat": string;
42+
readonly "tabs.notebooks": string;
43+
readonly "tabs.notebooks.empty": string;
44+
readonly "notebooks.title": string;
45+
readonly "notebooks.empty.title": string;
46+
readonly "notebooks.empty.description": string;
47+
readonly "notebooks.empty.action": string;
48+
readonly "notebooks.documents": string;
49+
readonly "notebooks.actions.rename": string;
50+
readonly "notebooks.actions.delete": string;
51+
readonly "notebooks.rename.title": string;
52+
readonly "notebooks.rename.description": string;
53+
readonly "notebooks.rename.label": string;
54+
readonly "notebooks.rename.placeholder": string;
55+
readonly "notebooks.rename.action": string;
56+
readonly "notebooks.delete.title": string;
57+
readonly "notebooks.delete.message": string;
58+
readonly "notebooks.delete.action": string;
59+
readonly "notebooks.delete.toast": string;
60+
readonly "notebooks.updated.today": string;
61+
readonly "notebooks.updated.yesterday": string;
62+
readonly "notebooks.updated.days": string;
63+
readonly "notebooks.updated.on": string;
4064
readonly "conversation.delete.confirm.title": string;
4165
readonly "conversation.delete.confirm.message": string;
4266
readonly "conversation.delete.confirm.action": string;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ConfigApi, FetchApi } from '@backstage/core-plugin-api';
18+
19+
import { NotebookSession } from '../types';
20+
import { NotebooksAPI } from './notebooksApi';
21+
22+
/**
23+
* @public
24+
* AI Notebooks API client options
25+
*/
26+
export type NotebooksOptions = {
27+
configApi: ConfigApi;
28+
fetchApi: FetchApi;
29+
};
30+
31+
/**
32+
* @public
33+
* AI Notebooks API client implementation
34+
*/
35+
export class NotebooksApiClient implements NotebooksAPI {
36+
private readonly configApi: ConfigApi;
37+
private readonly fetchApi: FetchApi;
38+
39+
constructor(options: NotebooksOptions) {
40+
this.configApi = options.configApi;
41+
this.fetchApi = options.fetchApi;
42+
}
43+
44+
async getBaseUrl() {
45+
return `${this.configApi.getString('backend.baseUrl')}/api/lightspeed/ai-notebooks`;
46+
}
47+
48+
private async fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
49+
const response = await this.fetchApi.fetch(url, {
50+
headers: {
51+
'Content-Type': 'application/json',
52+
},
53+
...init,
54+
});
55+
56+
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);
75+
}
76+
77+
const text = await response.text();
78+
if (!text) {
79+
return {} as T;
80+
}
81+
return JSON.parse(text) as T;
82+
}
83+
84+
async listSessions() {
85+
const baseUrl = await this.getBaseUrl();
86+
const response = await this.fetchJson<{ sessions?: NotebookSession[] }>(
87+
`${baseUrl}/v1/sessions`,
88+
);
89+
return response?.sessions ?? [];
90+
}
91+
92+
async renameSession(sessionId: string, name: string) {
93+
const baseUrl = await this.getBaseUrl();
94+
await this.fetchJson(
95+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
96+
{
97+
method: 'PUT',
98+
body: JSON.stringify({ name }),
99+
},
100+
);
101+
}
102+
103+
async deleteSession(sessionId: string) {
104+
const baseUrl = await this.getBaseUrl();
105+
await this.fetchJson(
106+
`${baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
107+
{
108+
method: 'DELETE',
109+
},
110+
);
111+
}
112+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { createApiRef } from '@backstage/core-plugin-api';
18+
19+
import { NotebookSession } from '../types';
20+
21+
/**
22+
* @public
23+
* AI Notebooks API
24+
*/
25+
export type NotebooksAPI = {
26+
listSessions: () => Promise<NotebookSession[]>;
27+
renameSession: (sessionId: string, name: string) => Promise<void>;
28+
deleteSession: (sessionId: string) => Promise<void>;
29+
};
30+
31+
/**
32+
* @public
33+
* AI Notebooks API interface
34+
*/
35+
export const notebooksApiRef = createApiRef<NotebooksAPI>({
36+
id: 'plugin.lightspeed.notebooks.service',
37+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { makeStyles } from '@material-ui/core/styles';
18+
import CloseIcon from '@mui/icons-material/Close';
19+
import Alert from '@mui/material/Alert';
20+
import Box from '@mui/material/Box';
21+
import Button from '@mui/material/Button';
22+
import Dialog from '@mui/material/Dialog';
23+
import DialogActions from '@mui/material/DialogActions';
24+
import DialogContent from '@mui/material/DialogContent';
25+
import DialogTitle from '@mui/material/DialogTitle';
26+
import IconButton from '@mui/material/IconButton';
27+
import Typography from '@mui/material/Typography';
28+
29+
import { useDeleteNotebook } from '../hooks/useDeleteNotebook';
30+
import { useTranslation } from '../hooks/useTranslation';
31+
32+
const useStyles = makeStyles(theme => ({
33+
dialogPaper: {
34+
borderRadius: 16,
35+
},
36+
dialogTitle: {
37+
padding: '16px 20px',
38+
fontStyle: 'inherit',
39+
},
40+
dialogContent: {
41+
paddingTop: 0,
42+
paddingBottom: theme.spacing(5),
43+
},
44+
titleRow: {
45+
display: 'flex',
46+
alignItems: 'center',
47+
gap: theme.spacing(1),
48+
},
49+
titleText: {
50+
fontWeight: 'bold',
51+
},
52+
closeButton: {
53+
position: 'absolute',
54+
right: theme.spacing(1),
55+
top: theme.spacing(1),
56+
color: theme.palette.grey[700],
57+
},
58+
errorBox: {
59+
maxWidth: 650,
60+
marginLeft: theme.spacing(2.5),
61+
marginRight: theme.spacing(2.5),
62+
},
63+
dialogActions: {
64+
justifyContent: 'left',
65+
padding: theme.spacing(2.5),
66+
gap: theme.spacing(1),
67+
},
68+
deleteButton: {
69+
textTransform: 'none',
70+
borderRadius: 999,
71+
},
72+
cancelButton: {
73+
textTransform: 'none',
74+
borderRadius: 999,
75+
},
76+
}));
77+
78+
export const DeleteNotebookModal = ({
79+
isOpen,
80+
onClose,
81+
onDeleted,
82+
sessionId,
83+
name,
84+
}: {
85+
isOpen: boolean;
86+
onClose: () => void;
87+
onDeleted: () => void;
88+
sessionId: string;
89+
name: string;
90+
}) => {
91+
const classes = useStyles();
92+
const { t } = useTranslation();
93+
const { mutateAsync: deleteNotebook, isError, error } = useDeleteNotebook();
94+
95+
const handleDelete = async () => {
96+
try {
97+
await deleteNotebook(sessionId);
98+
onDeleted();
99+
onClose();
100+
} catch (e) {
101+
// eslint-disable-next-line no-console
102+
console.warn(e);
103+
}
104+
};
105+
106+
return (
107+
<Dialog
108+
open={isOpen}
109+
onClose={onClose}
110+
aria-labelledby="delete-notebook-modal"
111+
aria-describedby="delete-notebook-modal-body"
112+
fullWidth
113+
PaperProps={{
114+
className: classes.dialogPaper,
115+
}}
116+
>
117+
<DialogTitle className={classes.dialogTitle}>
118+
<Box className={classes.titleRow}>
119+
<Typography component="span" className={classes.titleText}>
120+
{t('notebooks.delete.title', { name } as any)}
121+
</Typography>
122+
<IconButton
123+
aria-label="close"
124+
onClick={onClose}
125+
title={t('common.close')}
126+
size="large"
127+
className={classes.closeButton}
128+
>
129+
<CloseIcon />
130+
</IconButton>
131+
</Box>
132+
</DialogTitle>
133+
<DialogContent
134+
id="delete-notebook-modal-body"
135+
className={classes.dialogContent}
136+
>
137+
<Typography variant="body2">{t('notebooks.delete.message')}</Typography>
138+
</DialogContent>
139+
{isError && (
140+
<Box className={classes.errorBox}>
141+
<Alert severity="error">{String(error)}</Alert>
142+
</Box>
143+
)}
144+
<DialogActions className={classes.dialogActions}>
145+
<Button
146+
variant="contained"
147+
color="error"
148+
className={classes.deleteButton}
149+
onClick={handleDelete}
150+
>
151+
{t('notebooks.delete.action')}
152+
</Button>
153+
<Button
154+
key="cancel"
155+
variant="outlined"
156+
className={classes.cancelButton}
157+
onClick={onClose}
158+
>
159+
{t('common.cancel')}
160+
</Button>
161+
</DialogActions>
162+
</Dialog>
163+
);
164+
};

0 commit comments

Comments
 (0)