Skip to content

Commit 43fc0ef

Browse files
author
Bogdan Tsechoev
committed
Merge branch 'chats_email_notifications' into 'master'
Send email notifications from AI See merge request postgres-ai/database-lab!945
2 parents 4575b4a + f2cddf6 commit 43fc0ef

File tree

13 files changed

+327
-54
lines changed

13 files changed

+327
-54
lines changed

ui/packages/platform/src/actions/actions.js

+37
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const Actions = Reflux.createActions([{
3838
ASYNC_ACTION: ASYNC_ACTION,
3939
doAuth: ASYNC_ACTION,
4040
getUserProfile: ASYNC_ACTION,
41+
updateUserProfile: ASYNC_ACTION,
4142
getAccessTokens: ASYNC_ACTION,
4243
getAccessToken: ASYNC_ACTION,
4344
hideGeneratedAccessToken: {},
@@ -267,6 +268,42 @@ Actions.getUserProfile.listen(function (token) {
267268
);
268269
});
269270

271+
Actions.updateUserProfile.listen(function (token, data) {
272+
let action = this;
273+
274+
if (!api) {
275+
settings.init(function () {
276+
api = new Api(settings);
277+
});
278+
}
279+
280+
this.progressed();
281+
282+
timeoutPromise(REQUEST_TIMEOUT, api.updateUserProfile(token, data))
283+
.then(result => {
284+
result.json()
285+
.then(json => {
286+
if (json) {
287+
action.completed({ data: json?.result });
288+
} else {
289+
action.failed(new Error('wrong_reply'));
290+
}
291+
})
292+
.catch(err => {
293+
console.error(err);
294+
action.failed(new Error('wrong_reply'));
295+
});
296+
})
297+
.catch(err => {
298+
console.error(err);
299+
if (err && err.message && err.message === 'timeout') {
300+
action.failed(new Error('failed_fetch'));
301+
} else {
302+
action.failed(new Error('wrong_reply'));
303+
}
304+
});
305+
});
306+
270307
Actions.getAccessTokens.listen(function (token, orgId) {
271308
let action = this;
272309

ui/packages/platform/src/api/api.js

+25
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,31 @@ class Api {
112112
});
113113
}
114114

115+
updateUserProfile(token, data) {
116+
let headers = {
117+
Authorization: 'Bearer ' + token,
118+
Accept: 'application/vnd.pgrst.object+json'
119+
};
120+
121+
let body = {};
122+
123+
if (data.is_chats_email_notifications_enabled !== 'undefined') {
124+
body.chats_email_notifications_enabled = data.is_chats_email_notifications_enabled;
125+
}
126+
127+
if (data.first_name !== 'undefined') {
128+
body.first_name = data.first_name;
129+
}
130+
131+
if (data.last_name !== 'undefined') {
132+
body.last_name = data.last_name;
133+
}
134+
135+
return this.post(`${this.apiServer}/rpc/update_user_profile`, body, {
136+
headers: headers
137+
});
138+
}
139+
115140
getAccessTokens(token, orgId) {
116141
let params = {};
117142
let headers = {

ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const BotSettingsForm: React.FC<BotSettingsFormProps> = (props) => {
161161
enableReinitialize: true,
162162
initialValues: {
163163
threadVisibility:
164-
data?.orgProfile?.data?.is_chat_public_by_default ? 'public' : 'private',
164+
data?.orgProfile?.data?.is_chat_public_by_default ? 'public' : 'private'
165165
},
166166
onSubmit: () => {
167167
const currentOrgId = orgId || null;

ui/packages/platform/src/pages/Bot/BotWrapper.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface BotWrapperProps {
77
orgId?: number;
88
envData: {
99
info?: {
10+
id?: number | null
1011
user_name?: string
1112
}
1213
};
@@ -38,7 +39,8 @@ export const BotWrapper = (props: BotWrapperProps) => {
3839
args={{
3940
threadId: props.match.params.threadId,
4041
orgId: props.orgData.id,
41-
isPublicByDefault: props.orgData.is_chat_public_by_default
42+
isPublicByDefault: props.orgData.is_chat_public_by_default,
43+
userId: props.envData.info?.id,
4244
}}>
4345
<BotPage {...props} />
4446
</AiBotProvider>

ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx

+44-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react'
1+
import React, { useEffect, useMemo, useRef, useState } from 'react'
22
import cn from "classnames";
33
import ReactMarkdown, { Components } from "react-markdown";
44
import rehypeRaw from "rehype-raw";
@@ -9,8 +9,9 @@ import { icons } from "@postgres.ai/shared/styles/icons";
99
import { DebugDialog } from "../../DebugDialog/DebugDialog";
1010
import { CodeBlock } from "./CodeBlock";
1111
import { disallowedHtmlTagsForMarkdown, permalinkLinkBuilder } from "../../utils";
12-
import { StateMessage } from "../../../../types/api/entities/bot";
12+
import { MessageStatus, StateMessage } from "../../../../types/api/entities/bot";
1313
import { MermaidDiagram } from "./MermaidDiagram";
14+
import { useAiBot } from "../../hooks";
1415

1516

1617
type BaseMessageProps = {
@@ -20,17 +21,19 @@ type BaseMessageProps = {
2021
name?: string;
2122
isLoading?: boolean;
2223
formattedTime?: string;
23-
aiModel?: string
24-
stateMessage?: StateMessage | null
25-
isCurrentStreamMessage?: boolean
24+
aiModel?: string;
25+
stateMessage?: StateMessage | null;
26+
isCurrentStreamMessage?: boolean;
2627
isPublic?: boolean;
28+
threadId?: string;
29+
status?: MessageStatus
2730
}
2831

2932
type AiMessageProps = BaseMessageProps & {
3033
isAi: true;
3134
content: string;
32-
aiModel: string
33-
isCurrentStreamMessage?: boolean
35+
aiModel: string;
36+
isCurrentStreamMessage?: boolean;
3437
}
3538

3639
type HumanMessageProps = BaseMessageProps & {
@@ -42,8 +45,8 @@ type HumanMessageProps = BaseMessageProps & {
4245
type LoadingMessageProps = BaseMessageProps & {
4346
isLoading: true;
4447
isAi: true;
45-
content?: undefined
46-
stateMessage: StateMessage | null
48+
content?: undefined;
49+
stateMessage: StateMessage | null;
4750
}
4851

4952
type MessageProps = AiMessageProps | HumanMessageProps | LoadingMessageProps;
@@ -261,14 +264,44 @@ export const Message = React.memo((props: MessageProps) => {
261264
aiModel,
262265
stateMessage,
263266
isCurrentStreamMessage,
264-
isPublic
267+
isPublic,
268+
threadId,
269+
status
265270
} = props;
266271

272+
const { updateMessageStatus } = useAiBot()
273+
274+
const elementRef = useRef<HTMLDivElement | null>(null);
275+
276+
267277
const [isDebugVisible, setDebugVisible] = useState(false);
268278

269279

270280
const classes = useStyles();
271281

282+
useEffect(() => {
283+
if (!isAi || isCurrentStreamMessage || status === 'read') return;
284+
285+
const observer = new IntersectionObserver(
286+
(entries) => {
287+
const entry = entries[0];
288+
if (entry.isIntersecting && threadId && id) {
289+
updateMessageStatus(threadId, id, 'read');
290+
observer.disconnect();
291+
}
292+
},
293+
{ threshold: 0.1 }
294+
);
295+
296+
if (elementRef.current) {
297+
observer.observe(elementRef.current);
298+
}
299+
300+
return () => {
301+
observer.disconnect();
302+
};
303+
}, [id, updateMessageStatus, isCurrentStreamMessage, isAi, threadId, status]);
304+
272305
const contentToRender: string = content?.replace(/\n/g, ' \n') || ''
273306

274307
const toggleDebugDialog = () => {
@@ -301,7 +334,7 @@ export const Message = React.memo((props: MessageProps) => {
301334
onClose={toggleDebugDialog}
302335
messageId={id}
303336
/>}
304-
<div className={classes.message}>
337+
<div ref={elementRef} className={classes.message}>
305338
<div className={classes.messageAvatar}>
306339
{isAi
307340
? <img

ui/packages/platform/src/pages/Bot/Messages/Messages.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ type FormattedTime = {
125125
[id: string]: Time
126126
}
127127

128-
export const Messages = React.memo(({orgId}: {orgId: number}) => {
128+
export const Messages = React.memo(({orgId, threadId}: {orgId: number, threadId?: string}) => {
129129
const {
130130
messages,
131131
loading: isLoading,
@@ -254,7 +254,8 @@ export const Messages = React.memo(({orgId}: {orgId: number}) => {
254254
created_at,
255255
content,
256256
ai_model,
257-
is_public
257+
is_public,
258+
status
258259
} = message;
259260
let name = 'You';
260261

@@ -283,6 +284,8 @@ export const Messages = React.memo(({orgId}: {orgId: number}) => {
283284
formattedTime={formattedTime}
284285
aiModel={ai_model}
285286
isPublic={is_public}
287+
threadId={threadId}
288+
status={status}
286289
/>
287290
)
288291
})}

ui/packages/platform/src/pages/Bot/hooks.tsx

+27-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
AiModel,
1515
StateMessage,
1616
StreamMessage,
17-
ErrorMessage
17+
ErrorMessage, MessageStatus
1818
} from "../../types/api/entities/bot";
1919
import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads";
2020
import {getChats} from "api/bot/getChats";
@@ -73,16 +73,18 @@ type UseAiBotReturnType = {
7373
isStreamingInProcess: boolean;
7474
currentStreamMessage: StreamMessage | null;
7575
errorMessage: ErrorMessage | null;
76+
updateMessageStatus: (threadId: string, messageId: string, status: MessageStatus) => void
7677
}
7778

7879
type UseAiBotArgs = {
7980
threadId?: string;
8081
orgId?: number
8182
isPublicByDefault?: boolean
83+
userId?: number | null
8284
}
8385

8486
export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => {
85-
const { threadId, orgId, isPublicByDefault } = args;
87+
const { threadId, orgId, isPublicByDefault, userId } = args;
8688
const { showMessage, closeSnackbar } = useAlertSnackbar();
8789
const {
8890
aiModels,
@@ -413,6 +415,27 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType =>
413415
}))
414416
}
415417

418+
const updateMessageStatus = (threadId: string, messageId: string, status: MessageStatus) => {
419+
wsSendMessage(JSON.stringify({
420+
action: 'message_status_update',
421+
payload: {
422+
thread_id: threadId,
423+
message_id: messageId,
424+
read_by: userId,
425+
status
426+
}
427+
}))
428+
if (messages && messages.length > 0) {
429+
const updatedMessages = messages.map((item) => {
430+
if (item.id === messageId) {
431+
item["status"] = status
432+
}
433+
return item
434+
});
435+
setMessages(updatedMessages)
436+
}
437+
}
438+
416439
const getDebugMessagesForWholeThread = async () => {
417440
setDebugMessagesLoading(true)
418441
if (threadId) {
@@ -478,7 +501,8 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType =>
478501
stateMessage,
479502
isStreamingInProcess,
480503
currentStreamMessage,
481-
errorMessage
504+
errorMessage,
505+
updateMessageStatus
482506
}
483507
}
484508

ui/packages/platform/src/pages/Bot/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export const BotPage = (props: BotPageProps) => {
268268
</Box>
269269
</Box>
270270
<Box className={cn(classes.contentContainer, {[classes.isChatsListVisible]: isChatsListVisible})}>
271-
<Messages orgId={orgData.id} />
271+
<Messages orgId={orgData.id} threadId={match.params.threadId} />
272272
<Command
273273
threadId={match.params.threadId}
274274
orgId={orgData.id}

ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,28 @@ export const ProfileWrapper = () => {
1717
marginLeft: theme.spacing(1),
1818
marginRight: theme.spacing(1),
1919
},
20+
formControlLabel: {
21+
marginLeft: theme.spacing(0),
22+
marginRight: theme.spacing(1),
23+
},
24+
formControlLabelCheckbox: {
25+
'& svg': {
26+
fontSize: 18
27+
}
28+
},
29+
updateButtonContainer: {
30+
marginTop: theme.spacing(3),
31+
marginLeft: theme.spacing(1),
32+
marginRight: theme.spacing(1),
33+
},
34+
label: {
35+
marginTop: theme.spacing(2),
36+
marginBottom: theme.spacing(1),
37+
marginLeft: theme.spacing(1),
38+
marginRight: theme.spacing(1),
39+
color: '#000!important',
40+
fontWeight: 'bold',
41+
},
2042
dense: {
2143
marginTop: 16,
2244
},

0 commit comments

Comments
 (0)