Skip to content

Commit 078b157

Browse files
authored
UI enhancements
1 parent 569847d commit 078b157

24 files changed

+400
-229
lines changed

lambda/session/lambda_functions.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def _get_all_user_sessions(user_id: str) -> List[Dict[str, Any]]:
5050
KeyConditionExpression="userId = :user_id",
5151
ExpressionAttributeValues={":user_id": user_id},
5252
IndexName=os.environ["SESSIONS_BY_USER_ID_INDEX_NAME"],
53+
ScanIndexForward=False,
5354
)
5455
except ClientError as error:
5556
if error.response["Error"]["Code"] == "ResourceNotFoundException":
@@ -147,13 +148,17 @@ def put_session(event: dict, context: dict) -> None:
147148
messages = body["messages"]
148149

149150
try:
150-
table.put_item(
151-
Item={
152-
"sessionId": session_id,
153-
"userId": user_id,
154-
"startTime": datetime.now().isoformat(),
155-
"history": messages,
156-
}
151+
table.update_item(
152+
Key={"sessionId": session_id, "userId": user_id},
153+
UpdateExpression="SET #history = :history, #startTime = :startTime, "
154+
+ "#createTime = if_not_exists(#createTime, :createTime)",
155+
ExpressionAttributeNames={"#history": "history", "#startTime": "startTime", "#createTime": "createTime"},
156+
ExpressionAttributeValues={
157+
":history": messages,
158+
":startTime": datetime.now().isoformat(),
159+
":createTime": datetime.now().isoformat(),
160+
},
161+
ReturnValues="UPDATED_NEW",
157162
)
158163
except ClientError:
159164
logger.exception("Error updating session in DynamoDB")

lib/chat/api/session.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ export class SessionApi extends Construct {
8282
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
8383
});
8484

85+
const byUserIdIndexSorted = 'byUserIdSorted';
86+
sessionTable.addGlobalSecondaryIndex({
87+
indexName: byUserIdIndexSorted,
88+
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
89+
sortKey: { name: 'startTime', type: dynamodb.AttributeType.STRING },
90+
});
91+
8592
const restApi = RestApi.fromRestApiAttributes(this, 'RestApi', {
8693
restApiId: restApiId,
8794
rootResourceId: rootResourceId,
@@ -97,7 +104,7 @@ export class SessionApi extends Construct {
97104
method: 'GET',
98105
environment: {
99106
SESSIONS_TABLE_NAME: sessionTable.tableName,
100-
SESSIONS_BY_USER_ID_INDEX_NAME: byUserIdIndex,
107+
SESSIONS_BY_USER_ID_INDEX_NAME: byUserIdIndexSorted,
101108
},
102109
},
103110
{

lib/serve/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export class LisaServeApplicationStack extends Stack {
216216
'bedrock:InvokeModelWithResponseStream',
217217
],
218218
resources: [
219-
'arn:*:bedrock:*::foundation-model/*'
219+
'*'
220220
]
221221
}),
222222
new PolicyStatement({

lib/user-interface/react/src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { IConfiguration } from './shared/model/configuration.model';
3636
import DocumentLibrary from './pages/DocumentLibrary';
3737
import RepositoryLibrary from './pages/RepositoryLibrary';
3838
import { Breadcrumbs } from './shared/breadcrumb/breadcrumbs';
39+
import BreadcrumbsDefaultChangeListener from './shared/breadcrumb/breadcrumbs-change-listener';
3940

4041

4142
export type RouteProps = {
@@ -107,6 +108,7 @@ function App () {
107108
>
108109
<Topbar configs={config} />
109110
</div>
111+
<BreadcrumbsDefaultChangeListener />
110112
<AppLayout
111113
headerSelector='#h'
112114
footerSelector='#f'

lib/user-interface/react/src/components/Topbar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useHref, useNavigate } from 'react-router-dom';
2020
import { applyDensity, applyMode, Density, Mode } from '@cloudscape-design/global-styles';
2121
import TopNavigation from '@cloudscape-design/components/top-navigation';
2222
import { getBaseURI } from './utils';
23-
import { signOut, useAppSelector } from '../config/store';
23+
import { purgeStore, useAppSelector } from '../config/store';
2424
import { selectCurrentUserIsAdmin } from '../shared/reducers/user.reducer';
2525
import { IConfiguration } from '../shared/model/configuration.model';
2626
import { ButtonUtility } from '@cloudscape-design/components/top-navigation/interfaces';
@@ -124,7 +124,7 @@ function Topbar ({ configs }: TopbarProps): ReactElement {
124124
auth.signinRedirect({ redirect_uri: window.location.toString() });
125125
break;
126126
case 'signout':
127-
await signOut();
127+
await purgeStore();
128128
await auth.signoutSilent();
129129
break;
130130
case 'color-mode':

lib/user-interface/react/src/components/chatbot/Chat.tsx

+36-11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import { useLazyGetRelevantDocumentsQuery } from '../../shared/reducers/rag.redu
5858
import { IConfiguration } from '../../shared/model/configuration.model';
5959
import { DocumentSummarizationModal } from './DocumentSummarizationModal';
6060
import { ChatMemory } from '../../shared/util/chat-memory';
61+
import { setBreadcrumbs } from '../../shared/reducers/breadcrumbs.reducer';
62+
import { truncateText } from '../../shared/util/formats';
6163

6264
export default function Chat ({ sessionId }) {
6365
const dispatch = useAppDispatch();
@@ -87,13 +89,15 @@ export default function Chat ({ sessionId }) {
8789

8890
const modelsOptions = useMemo(() => allModels.map((model) => ({ label: model.modelId, value: model.modelId })), [allModels]);
8991
const [selectedModel, setSelectedModel] = useState<IModel>();
92+
const [dirtySession, setDirtySession] = useState(false);
9093
const [session, setSession] = useState<LisaChatSession>({
9194
history: [],
9295
sessionId: '',
9396
userId: '',
9497
startTime: new Date(Date.now()).toISOString(),
9598
});
9699
const [internalSessionId, setInternalSessionId] = useState<string | null>(null);
100+
const [loadingSession, setLoadingSession] = useState(false);
97101

98102
const [isConnected, setIsConnected] = useState(false);
99103
const [metadata, setMetadata] = useState<LisaChatMessageMetadata>({});
@@ -126,8 +130,8 @@ export default function Chat ({ sessionId }) {
126130
input: (previousOutput: any) => previousOutput.input,
127131
context: (previousOutput: any) => previousOutput.context,
128132
history: (previousOutput: any) => previousOutput.memory?.history || '',
129-
aiPrefix: (previousOutput: any) => previousOutput.aiPrefix,
130-
humanPrefix: (previousOutput: any) => previousOutput.humanPrefix,
133+
aiPrefix: () => chatConfiguration.promptConfiguration.aiPrefix,
134+
humanPrefix: () => chatConfiguration.promptConfiguration.humanPrefix,
131135
};
132136

133137
const chainSteps = [
@@ -226,8 +230,8 @@ export default function Chat ({ sessionId }) {
226230
input: (initialInput: any) => initialInput.input,
227231
memory: () => memory.loadMemoryVariables(),
228232
context: () => fileContext || '',
229-
humanPrefix: (initialInput: any) => initialInput.humanPrefix,
230-
aiPrefix: (initialInput: any) => initialInput.aiPrefix,
233+
humanPrefix: () => chatConfiguration.promptConfiguration.humanPrefix,
234+
aiPrefix: () => chatConfiguration.promptConfiguration.aiPrefix,
231235
};
232236

233237
chainSteps.unshift(nonRagStep);
@@ -322,17 +326,27 @@ export default function Chat ({ sessionId }) {
322326
}, [auth, getConfiguration]);
323327

324328
useEffect(() => {
325-
if (!isRunning && session.history.length) {
329+
if (!isRunning && session.history.length && dirtySession) {
326330
if (session.history.at(-1).type === 'ai' && !auth.isLoading) {
331+
setDirtySession(false);
327332
updateSession(session);
328333
}
329334
}
330-
// eslint-disable-next-line react-hooks/exhaustive-deps
331-
}, [isRunning, session, auth]);
335+
}, [isRunning, session, dirtySession, auth, updateSession]);
332336

333337
useEffect(() => {
334338
if (sessionId) {
335339
setInternalSessionId(sessionId);
340+
setLoadingSession(true);
341+
setSession({...session, history: []});
342+
dispatch(setBreadcrumbs([{
343+
text: 'Chatbot',
344+
href: ''
345+
}, {
346+
text: 'Loading session...',
347+
href: ''
348+
}]));
349+
336350
getSessionById(sessionId).then((resp) => {
337351
// session doesn't exist so we create it
338352
let sess: LisaChatSession = resp.data;
@@ -345,6 +359,16 @@ export default function Chat ({ sessionId }) {
345359
};
346360
}
347361
setSession(sess);
362+
363+
// override the default breadcrumbs
364+
dispatch(setBreadcrumbs([{
365+
text: 'Chatbot',
366+
href: ''
367+
}, {
368+
text: truncateText(sess.history?.[0]?.content?.toString()) || 'New Session',
369+
href: ''
370+
}]));
371+
setLoadingSession(false);
348372
});
349373
} else {
350374
const newSessionId = uuidv4();
@@ -497,6 +521,7 @@ export default function Chat ({ sessionId }) {
497521
message: message
498522
});
499523

524+
setDirtySession(true);
500525
}, [userPrompt, useRag, fileContext, chatConfiguration.promptConfiguration.aiPrefix, chatConfiguration.promptConfiguration.humanPrefix, chatConfiguration.promptConfiguration.promptTemplate, generateResponse]);
501526

502527
return (
@@ -547,9 +572,9 @@ export default function Chat ({ sessionId }) {
547572
<div className='overflow-y-auto h-[calc(100vh-25rem)] bottom-8'>
548573
<SpaceBetween direction='vertical' size='l'>
549574
{session.history.map((message, idx) => (
550-
<Message key={idx} message={message} showMetadata={chatConfiguration.sessionConfiguration.showMetadata} isRunning={false} isStreaming={isStreaming && idx === session.history.length - 1}/>
575+
<Message key={idx} message={message} showMetadata={chatConfiguration.sessionConfiguration.showMetadata} isRunning={false} isStreaming={isStreaming && idx === session.history.length - 1} markdownDisplay={chatConfiguration.sessionConfiguration.markdownDisplay}/>
551576
))}
552-
{isRunning && !isStreaming && <Message isRunning={isRunning} />}
577+
{isRunning && !isStreaming && <Message isRunning={isRunning} markdownDisplay={chatConfiguration.sessionConfiguration.markdownDisplay}/>}
553578
<div ref={bottomRef} />
554579
</SpaceBetween>
555580
</div>
@@ -609,9 +634,9 @@ export default function Chat ({ sessionId }) {
609634
placeholder={
610635
!selectedModel ? 'You must select a model before sending a message' : 'Send a message'
611636
}
612-
disabled={!selectedModel}
637+
disabled={!selectedModel || loadingSession}
613638
onChange={({ detail }) => setUserPrompt(detail.value)}
614-
onAction={userPrompt.length > 0 && !isRunning && handleSendGenerateRequest}
639+
onAction={userPrompt.length > 0 && !isRunning && !loadingSession && handleSendGenerateRequest}
615640
secondaryActions={
616641
<Box padding={{ left: 'xxs', top: 'xs' }}>
617642
<ButtonGroup

lib/user-interface/react/src/components/chatbot/DocumentSummarizationModal.tsx

+24-25
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ export type DocumentSummarizationModalProps = {
4646
setUserPrompt: (state: string) => void;
4747
userPrompt: string;
4848
selectedModel: IModel;
49-
setSelectedModel: (state: IModel ) => void;
49+
setSelectedModel: (state: IModel) => void;
5050
chatConfiguration: IChatConfiguration;
5151
setChatConfiguration: (state: IChatConfiguration) => void;
52-
setInternalSessionId: (state: string ) => void;
52+
setInternalSessionId: (state: string) => void;
5353
setSession: (state: LisaChatSession) => void;
5454
userName: string;
5555
handleSendGenerateRequest: () => void;
@@ -79,11 +79,13 @@ export function DocumentSummarizationModal ({
7979
const [summarize, setSummarize] = useState<boolean>(false);
8080
const [createNewChatSession, setCreateNewChatSession] = useState<boolean>(true);
8181

82-
const { data: allModels, isFetching: isFetchingModels } = useGetAllModelsQuery(undefined, {refetchOnMountOrArgChange: 5,
82+
const { data: allModels, isFetching: isFetchingModels } = useGetAllModelsQuery(undefined, {
83+
refetchOnMountOrArgChange: 5,
8384
selectFromResult: (state) => ({
8485
isFetching: state.isFetching,
8586
data: (state.data || []).filter((model: IModel) => model.modelType === ModelType.textgen && model.status === ModelStatus.InService && model.features && model.features.filter((feat) => feat.name === 'summarization').length > 0),
86-
})});
87+
})
88+
});
8789
const modelsOptions = useMemo(() => allModels.map((model) => ({ label: model.modelId, value: model.modelId, description: model.features.filter((feat) => feat.name === 'summarization')[0].overview })), [allModels]);
8890
const [selectedPromptType, setSelectedPromptType] = useState<string>('');
8991
const promptOptions = [
@@ -98,7 +100,7 @@ export function DocumentSummarizationModal ({
98100
}
99101

100102
async function processFile (file: File): Promise<boolean> {
101-
//File context currently only supports single files
103+
//File context currently only supports single files
102104
const fileContents = await file.text();
103105
setFileContext(`File context: ${fileContents}`);
104106
setSelectedFiles([file]);
@@ -196,7 +198,7 @@ export function DocumentSummarizationModal ({
196198
</p>
197199
</TextContent>
198200
<FileUpload
199-
onChange={async ({detail}) => {
201+
onChange={async ({ detail }) => {
200202
setSelectedFiles(detail.value);
201203
const uploads = await handleUpload(detail.value, handleError, processFile, [FileTypes.TEXT], 204800);
202204
setSuccessfulUpload(uploads);
@@ -229,10 +231,8 @@ export function DocumentSummarizationModal ({
229231
} else {
230232
const model = allModels.find((model) => model.modelId === value);
231233
if (model) {
232-
if (!model.streaming && chatConfiguration.sessionConfiguration.streaming) {
233-
setChatConfiguration({...chatConfiguration, sessionConfiguration: {...chatConfiguration.sessionConfiguration, streaming: false }});
234-
} else if (model.streaming && !chatConfiguration.sessionConfiguration.streaming) {
235-
setChatConfiguration({...chatConfiguration, sessionConfiguration: {...chatConfiguration.sessionConfiguration, streaming: true }});
234+
if (model.streaming !== chatConfiguration.sessionConfiguration.streaming) {
235+
setChatConfiguration({ ...chatConfiguration, sessionConfiguration: { ...chatConfiguration.sessionConfiguration, streaming: model.streaming } });
236236
}
237237
setSelectedModel(model);
238238
}
@@ -249,7 +249,7 @@ export function DocumentSummarizationModal ({
249249
enteredTextLabel={(text) => `Use: "${text}"`}
250250
onChange={({ detail: { value } }) => {
251251
setUserPrompt('');
252-
if (value && value.length !== 0) {
252+
if (value && value.length !== 0) {
253253
setSelectedPromptType(promptOptions.filter((option) => option.value === value)[0].label);
254254
if (value === 'concise') {
255255
setUserPrompt('Please provide a short summary of the included file context. Do not include any other information.');
@@ -285,20 +285,19 @@ Repeat the following 2 steps 5 times.
285285
options={promptOptions}
286286
/>
287287
</FormField>
288-
{selectedPromptType && <>
289-
<Textarea
290-
rows={10}
291-
disableBrowserAutocorrect={false}
292-
autoFocus
293-
onChange={(e) => setUserPrompt(e.detail.value)}
294-
onKeyDown={(e) => {
295-
if (e.detail.key === 'Enter' && !e.detail.shiftKey) {
296-
e.preventDefault();
297-
}
298-
}}
299-
value={userPrompt}
300-
/>
301-
</>}
288+
{selectedPromptType ? <Textarea
289+
key='textarea-prompt-textarea'
290+
rows={10}
291+
disableBrowserAutocorrect={false}
292+
autoFocus
293+
onChange={(e) => setUserPrompt(e.detail.value)}
294+
onKeyDown={(e) => {
295+
if (e.detail.key === 'Enter' && !e.detail.shiftKey) {
296+
e.preventDefault();
297+
}
298+
}}
299+
value={userPrompt}
300+
/> : null}
302301
<FormField label='Create new chat session'>
303302
<Toggle checked={createNewChatSession} onChange={({ detail }) =>
304303
setCreateNewChatSession(detail.checked)

0 commit comments

Comments
 (0)