Skip to content

Commit dc4ff7c

Browse files
feat(lightspeed): add attachment support and disclaimers (#760)
* add attachment support and disclaimers * add changeset * update error messages and alert type
1 parent f55a5a0 commit dc4ff7c

16 files changed

Lines changed: 733 additions & 34 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
3+
---
4+
5+
Add file attachment support
6+
Update functional and legal disclaimers
7+
Add citation links in the bot response
8+
Update initial user prompts

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { ConfigApi, FetchApi } from '@backstage/core-plugin-api';
1818

1919
import { TEMP_CONVERSATION_ID } from '../const';
20+
import { Attachment } from '../types';
2021
import { LightspeedAPI } from './api';
2122

2223
export type Options = {
@@ -48,6 +49,7 @@ export class LightspeedApiClient implements LightspeedAPI {
4849
prompt: string,
4950
selectedModel: string,
5051
conversation_id: string,
52+
attachments: Attachment[],
5153
) {
5254
const baseUrl = await this.getBaseUrl();
5355

@@ -66,6 +68,7 @@ export class LightspeedApiClient implements LightspeedAPI {
6668
.getConfigArray('lightspeed.servers')[0]
6769
.getOptionalString('id'), // Currently supports a single llm server
6870
query: prompt,
71+
attachments,
6972
}),
7073
});
7174

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { createApiRef } from '@backstage/core-plugin-api';
1818

1919
import OpenAI from 'openai';
2020

21-
import { BaseMessage, ConversationList } from '../types';
21+
import { Attachment, BaseMessage, ConversationList } from '../types';
2222

2323
export type LightspeedAPI = {
2424
getAllModels: () => Promise<OpenAI.Models.Model[]>;
@@ -27,6 +27,7 @@ export type LightspeedAPI = {
2727
prompt: string,
2828
selectedModel: string,
2929
conversation_id: string,
30+
attachments: Attachment[],
3031
) => Promise<ReadableStreamDefaultReader>;
3132
deleteConversation: (
3233
conversation_id: string,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
import React from 'react';
17+
18+
import {
19+
AttachmentEdit,
20+
ChatbotDisplayMode,
21+
PreviewAttachment,
22+
} from '@patternfly/chatbot';
23+
24+
import { useFileAttachmentContext } from './AttachmentContext';
25+
26+
const Attachment = () => {
27+
const {
28+
currentFileContent,
29+
setFileContents,
30+
modalState,
31+
setCurrentFileContent,
32+
} = useFileAttachmentContext();
33+
if (!currentFileContent) {
34+
return null;
35+
}
36+
const {
37+
previewModalKey,
38+
isPreviewModalOpen,
39+
isEditModalOpen,
40+
setPreviewModalKey,
41+
setIsEditModalOpen,
42+
setIsPreviewModalOpen,
43+
} = modalState;
44+
45+
return (
46+
<>
47+
<PreviewAttachment
48+
key={previewModalKey}
49+
code={currentFileContent?.content}
50+
fileName={currentFileContent?.name}
51+
isModalOpen={isPreviewModalOpen}
52+
onEdit={() => {
53+
setIsPreviewModalOpen(false);
54+
setIsEditModalOpen(true);
55+
}}
56+
onDismiss={() => {
57+
setCurrentFileContent(undefined);
58+
setIsPreviewModalOpen(false);
59+
}}
60+
handleModalToggle={() => setIsPreviewModalOpen(false)}
61+
displayMode={ChatbotDisplayMode.fullscreen}
62+
/>
63+
<AttachmentEdit
64+
key={currentFileContent?.content}
65+
code={currentFileContent?.content}
66+
fileName={currentFileContent?.name}
67+
isModalOpen={isEditModalOpen}
68+
onSave={(_, content) => {
69+
setCurrentFileContent({
70+
...currentFileContent,
71+
content,
72+
});
73+
setFileContents(prev => {
74+
const existingIndex = prev.findIndex(
75+
f => f.name === currentFileContent?.name,
76+
);
77+
78+
if (existingIndex !== -1) {
79+
// Update the existing file's content
80+
const updated = [...prev];
81+
updated[existingIndex] = {
82+
...updated[existingIndex],
83+
content: content as string,
84+
};
85+
return updated;
86+
}
87+
88+
// File doesn't exist, add a new one
89+
return [
90+
...prev,
91+
{
92+
name: currentFileContent?.name,
93+
type: currentFileContent?.type,
94+
content: content as string,
95+
},
96+
];
97+
});
98+
99+
setPreviewModalKey(prev => prev + 1);
100+
setIsEditModalOpen(false);
101+
setIsPreviewModalOpen(true);
102+
}}
103+
onCancel={() => setCurrentFileContent(undefined)}
104+
handleModalToggle={() => setIsEditModalOpen(false)}
105+
displayMode={ChatbotDisplayMode.fullscreen}
106+
/>
107+
</>
108+
);
109+
};
110+
111+
export default Attachment;
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
import React from 'react';
17+
18+
import { FileContent } from '../types';
19+
import { isSupportedFileType, readFileAsText } from '../utils/attachment-utils';
20+
21+
type UploadError = { type?: 'info' | 'danger'; message: string | null };
22+
interface FileAttachmentContextType {
23+
showAlert: boolean;
24+
uploadError: UploadError;
25+
fileContents: FileContent[];
26+
isLoadingFile: Record<string, boolean>;
27+
handleFileUpload: (files: File[]) => void;
28+
setFileContents: React.Dispatch<React.SetStateAction<FileContent[]>>;
29+
setUploadError: React.Dispatch<React.SetStateAction<UploadError>>;
30+
currentFileContent?: FileContent;
31+
setCurrentFileContent: React.Dispatch<
32+
React.SetStateAction<FileContent | undefined>
33+
>;
34+
modalState: {
35+
previewModalKey: number;
36+
setPreviewModalKey: React.Dispatch<React.SetStateAction<number>>;
37+
isPreviewModalOpen: boolean;
38+
setIsPreviewModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
39+
isEditModalOpen: boolean;
40+
setIsEditModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
41+
};
42+
}
43+
44+
export const FileAttachmentContext =
45+
React.createContext<FileAttachmentContextType | null>(null);
46+
47+
const FileAttachmentContextProvider: React.FC<{
48+
children: React.ReactNode;
49+
}> = ({ children }) => {
50+
const [currentFileContent, setCurrentFileContent] = React.useState<
51+
FileContent | undefined
52+
>();
53+
const [isLoadingFile, setIsLoadingFile] = React.useState<
54+
Record<string, boolean>
55+
>({});
56+
const [previewModalKey, setPreviewModalKey] = React.useState<number>(0);
57+
const [showAlert, setShowAlert] = React.useState<boolean>(false);
58+
const [uploadError, setUploadError] = React.useState<UploadError>({
59+
message: null,
60+
});
61+
const [fileContents, setFileContents] = React.useState<FileContent[]>([]);
62+
const [isPreviewModalOpen, setIsPreviewModalOpen] =
63+
React.useState<boolean>(false);
64+
const [isEditModalOpen, setIsEditModalOpen] = React.useState<boolean>(false);
65+
const handleFileUpload = (fileArr: File[]) => {
66+
const existingFile = fileContents.find(
67+
file => file.name === fileArr[0].name,
68+
);
69+
if (existingFile) {
70+
setShowAlert(true);
71+
setUploadError({ type: 'info', message: 'File already exists.' });
72+
return;
73+
}
74+
75+
setIsLoadingFile({ [fileArr[0].name]: true });
76+
77+
if (fileArr.length > 1) {
78+
setShowAlert(true);
79+
setUploadError({
80+
message: 'Uploaded more than one file.',
81+
});
82+
return;
83+
}
84+
if (!isSupportedFileType(fileArr[0])) {
85+
setShowAlert(true);
86+
setUploadError({
87+
message:
88+
'Unsupported file type. Supported types are: .txt, .yaml, .json.',
89+
});
90+
return;
91+
}
92+
93+
// this is 25MB in bytes; size is in bytes
94+
if (fileArr[0].size > 25000000) {
95+
setShowAlert(true);
96+
setUploadError({
97+
message:
98+
'Your file size is too large. Please ensure that your file is less than 25 MB.',
99+
});
100+
return;
101+
}
102+
103+
readFileAsText(fileArr[0])
104+
.then(data => {
105+
setFileContents(prev => [
106+
...prev,
107+
{
108+
name: fileArr[0].name,
109+
type: fileArr[0].type,
110+
content: data as string,
111+
},
112+
]);
113+
setShowAlert(false);
114+
setUploadError({ message: null });
115+
setIsLoadingFile({ [fileArr[0].name]: false });
116+
})
117+
.catch((error: DOMException) => {
118+
setUploadError({
119+
message: `Failed to read file: ${error.message}`,
120+
});
121+
});
122+
};
123+
124+
const modalState = {
125+
isPreviewModalOpen,
126+
setIsPreviewModalOpen,
127+
isEditModalOpen,
128+
setIsEditModalOpen,
129+
previewModalKey,
130+
setPreviewModalKey,
131+
};
132+
return (
133+
<FileAttachmentContext.Provider
134+
value={{
135+
modalState,
136+
fileContents,
137+
setFileContents,
138+
handleFileUpload,
139+
isLoadingFile,
140+
showAlert,
141+
uploadError,
142+
setUploadError,
143+
currentFileContent,
144+
setCurrentFileContent,
145+
}}
146+
>
147+
{children}
148+
</FileAttachmentContext.Provider>
149+
);
150+
};
151+
152+
export default FileAttachmentContextProvider;
153+
154+
export const useFileAttachmentContext = (): FileAttachmentContextType => {
155+
const context = React.useContext<FileAttachmentContextType | null>(
156+
FileAttachmentContext,
157+
);
158+
159+
if (context === null) {
160+
throw new Error(
161+
'useFileAttachmentContext must be within a FileAttachmentContextProvider',
162+
);
163+
}
164+
165+
return context;
166+
};

0 commit comments

Comments
 (0)