feat(frontend & backend): thumbs for message feedback (#2360)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):

---------

Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
This commit is contained in:
Antoine Dewez 2024-03-21 00:11:06 -07:00 committed by GitHub
parent d7d1a0155b
commit da8e7513e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 220 additions and 40 deletions

1
.gitignore vendored
View File

@ -81,3 +81,4 @@ airwallexpayouts.py
application.log
backend/celerybeat-schedule.db
backend/application.log.*

View File

@ -13,6 +13,7 @@ from modules.brain.service.brain_service import BrainService
from modules.chat.controller.chat.brainful_chat import BrainfulChat
from modules.chat.dto.chats import ChatItem, ChatQuestion
from modules.chat.dto.inputs import (
ChatMessageProperties,
ChatUpdatableProperties,
CreateChatProperties,
QuestionAndAnswer,
@ -152,6 +153,32 @@ async def update_chat_metadata_handler(
return chat_service.update_chat(chat_id=chat_id, chat_data=chat_data)
# update existing message
@chat_router.put(
"/chat/{chat_id}/{message_id}", dependencies=[Depends(AuthBearer())], tags=["Chat"]
)
async def update_chat_message(
chat_message_properties: ChatMessageProperties,
chat_id: UUID,
message_id: UUID,
current_user: UserIdentity = Depends(get_current_user),
) :
chat = chat_service.get_chat_by_id(
chat_id # pyright: ignore reportPrivateUsage=none
)
if str(current_user.id) != chat.user_id:
raise HTTPException(
status_code=403, # pyright: ignore reportPrivateUsage=none
detail="You should be the owner of the chat to update it.", # pyright: ignore reportPrivateUsage=none
)
return chat_service.update_chat_message(
chat_id=chat_id,
message_id=message_id,
chat_message_properties=chat_message_properties.dict(),
)
# create new chat
@chat_router.post("/chat", dependencies=[Depends(AuthBearer())], tags=["Chat"])
async def create_chat_handler(

View File

@ -32,3 +32,14 @@ class ChatUpdatableProperties:
def __init__(self, chat_name: Optional[str]):
self.chat_name = chat_name
class ChatMessageProperties(BaseModel, extra="ignore"):
thumbs: Optional[bool]
def dict(self, *args, **kwargs):
chat_dict = super().dict(*args, **kwargs)
if chat_dict.get("thumbs"):
# Set thumbs to boolean value or None if not present
chat_dict["thumbs"] = bool(chat_dict["thumbs"])
return chat_dict

View File

@ -16,6 +16,7 @@ class GetChatHistoryOutput(BaseModel):
None # string because UUID is not JSON serializable
)
metadata: Optional[dict] | None = None
thumbs: Optional[bool] | None = None
def dict(self, *args, **kwargs):
chat_history = super().dict(*args, **kwargs)

View File

@ -27,6 +27,7 @@ class ChatHistory:
prompt_id: Optional[UUID]
brain_id: Optional[UUID]
metadata: Optional[dict] = None
thumbs: Optional[bool] = None
def __init__(self, chat_dict: dict):
self.chat_id = chat_dict.get("chat_id", "")
@ -38,6 +39,7 @@ class ChatHistory:
self.prompt_id = chat_dict.get("prompt_id")
self.brain_id = chat_dict.get("brain_id")
self.metadata = chat_dict.get("metadata")
self.thumbs = chat_dict.get("thumbs")
def to_dict(self):
return asdict(self)

View File

@ -1,4 +1,5 @@
from models.settings import get_supabase_client
from modules.chat.dto.inputs import ChatMessageProperties
from modules.chat.entity.chat import Chat
from modules.chat.repository.chats_interface import ChatsInterface
@ -102,3 +103,13 @@ class Chats(ChatsInterface):
def delete_chat_history(self, chat_id):
self.db.table("chat_history").delete().match({"chat_id": chat_id}).execute()
def update_chat_message(self, chat_id, message_id, chat_message_properties: ChatMessageProperties ):
response = (
self.db.table("chat_history")
.update(chat_message_properties)
.match({"message_id": message_id, "chat_id": chat_id})
.execute()
)
return response

View File

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
from modules.chat.dto.inputs import CreateChatHistory, QuestionAndAnswer
from modules.chat.dto.inputs import ChatMessageProperties, CreateChatHistory, QuestionAndAnswer
from modules.chat.entity.chat import Chat
@ -78,3 +78,10 @@ class ChatsInterface(ABC):
Delete chat history
"""
pass
@abstractmethod
def update_chat_message(self, chat_id, message_id, chat_message_properties: ChatMessageProperties):
"""
Update chat message
"""
pass

View File

@ -7,6 +7,7 @@ from logger import get_logger
from modules.brain.service.brain_service import BrainService
from modules.chat.dto.chats import ChatItem
from modules.chat.dto.inputs import (
ChatMessageProperties,
ChatUpdatableProperties,
CreateChatHistory,
CreateChatProperties,
@ -102,6 +103,7 @@ class ChatService:
brain_id=str(brain.id) if brain else None,
prompt_title=prompt.title if prompt else None,
metadata=message.metadata,
thumbs=message.thumbs,
)
)
return enriched_history
@ -193,3 +195,14 @@ class ChatService:
except Exception as e:
print(e)
pass
def update_chat_message(
self, chat_id, message_id, chat_message_properties: ChatMessageProperties
):
try:
return self.repository.update_chat_message(
chat_id, message_id, chat_message_properties
).data
except Exception as e:
print(e)
pass

View File

@ -16,6 +16,7 @@ export const QADisplay = ({ content, index }: QADisplayProps): JSX.Element => {
prompt_title,
metadata,
brain_id,
thumbs,
} = content;
return (
@ -36,6 +37,8 @@ export const QADisplay = ({ content, index }: QADisplayProps): JSX.Element => {
brainId={brain_id}
index={index}
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
messageId={message_id}
thumbs={thumbs}
/>
</>
);

View File

@ -1,5 +1,7 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { useChatApi } from "@/lib/api/chat/useChatApi";
import { CopyButton } from "@/lib/components/ui/CopyButton";
import Icon from "@/lib/components/ui/Icon/Icon";
import { useChatContext } from "@/lib/context";
@ -23,6 +25,8 @@ type MessageRowProps = {
};
brainId?: string;
index?: number;
messageId?: string;
thumbs?: boolean;
};
export const MessageRow = React.forwardRef(
@ -35,6 +39,8 @@ export const MessageRow = React.forwardRef(
children,
brainId,
index,
messageId,
thumbs: initialThumbs,
}: MessageRowProps,
ref: React.Ref<HTMLDivElement>
) => {
@ -44,33 +50,57 @@ export const MessageRow = React.forwardRef(
});
const { setSourcesMessageIndex, sourcesMessageIndex } = useChatContext();
const { isMobile } = useDevice();
const { updateChatMessage } = useChatApi();
const { chatId } = useChat();
const [thumbs, setThumbs] = useState<boolean | undefined | null>(
initialThumbs
);
useEffect(() => {
setThumbs(initialThumbs);
}, [initialThumbs]);
const messageContent = text ?? "";
const thumbsUp = async () => {
if (chatId && messageId) {
await updateChatMessage(chatId, messageId, {
thumbs: thumbs ? null : true,
});
setThumbs(thumbs ? null : true);
}
};
const thumbsDown = async () => {
if (chatId && messageId) {
await updateChatMessage(chatId, messageId, {
thumbs: thumbs === false ? null : false,
});
setThumbs(thumbs === false ? null : false);
}
};
const renderMessageHeader = () => {
if (!isUserSpeaker) {
return (
<div
className={`
${styles.message_row_container}
${isUserSpeaker ? styles.user : styles.brain}
`}
>
{!isUserSpeaker ? (
<div className={styles.message_header}>
<QuestionBrain brainName={brainName} brainId={brainId} />
<QuestionPrompt promptName={promptName} />
</div>
) : (
);
} else {
return (
<div className={styles.message_header}>
<Icon name="user" color="dark-grey" size="normal" />
<span className={styles.me}>Me</span>
</div>
)}
{}
<div ref={ref} className={styles.message_row_content}>
{children ?? (
<>
<MessageContent text={messageContent} isUser={isUserSpeaker} />
{!isUserSpeaker && messageContent !== "🧠" && (
);
}
};
const renderIcons = () => {
if (!isUserSpeaker && messageContent !== "🧠") {
return (
<div className={styles.icons_wrapper}>
<CopyButton handleCopy={handleCopy} size="normal" />
{!isMobile && (
@ -78,9 +108,7 @@ export const MessageRow = React.forwardRef(
<Icon
name="file"
handleHover={true}
color={
sourcesMessageIndex === index ? "primary" : "black"
}
color={sourcesMessageIndex === index ? "primary" : "black"}
size="normal"
onClick={() => {
setSourcesMessageIndex(
@ -90,8 +118,42 @@ export const MessageRow = React.forwardRef(
/>
</div>
)}
<Icon
name="thumbsUp"
handleHover={true}
color={thumbs ? "primary" : "black"}
size="normal"
onClick={async () => {
await thumbsUp();
}}
/>
<Icon
name="thumbsDown"
handleHover={true}
color={thumbs === false ? "primary" : "black"}
size="normal"
onClick={async () => {
await thumbsDown();
}}
/>
</div>
)}
);
}
};
return (
<div
className={`
${styles.message_row_container}
${isUserSpeaker ? styles.user : styles.brain}
`}
>
{renderMessageHeader()}
<div ref={ref} className={styles.message_row_content}>
{children ?? (
<>
<MessageContent text={messageContent} isUser={isUserSpeaker} />
{renderIcons()}
</>
)}
</div>

View File

@ -22,6 +22,7 @@ export type ChatMessage = {
metadata?: {
sources?: Source[];
};
thumbs?: boolean;
};
type NotificationStatus = "Pending" | "Done";

View File

@ -7,6 +7,14 @@ import {
ChatQuestion,
} from "@/app/chat/[chatId]/types";
export type ChatUpdatableProperties = {
chat_name?: string;
};
export type ChatMessageUpdatableProperties = {
thumbs?: boolean | null;
};
export const createChat = async (
name: string,
axiosInstance: AxiosInstance
@ -59,9 +67,6 @@ export const getChatItems = async (
): Promise<ChatItem[]> =>
(await axiosInstance.get<ChatItem[]>(`/chat/${chatId}/history`)).data;
export type ChatUpdatableProperties = {
chat_name?: string;
};
export const updateChat = async (
chatId: string,
chat: ChatUpdatableProperties,
@ -70,3 +75,17 @@ export const updateChat = async (
return (await axiosInstance.put<ChatEntity>(`/chat/${chatId}/metadata`, chat))
.data;
};
export const updateChatMessage = async (
chatId: string,
messageId: string,
chatMessageUpdatableProperties: ChatMessageUpdatableProperties,
axiosInstance: AxiosInstance
): Promise<ChatItem> => {
return (
await axiosInstance.put<ChatItem>(
`/chat/${chatId}/${messageId}`,
chatMessageUpdatableProperties
)
).data;
};

View File

@ -7,12 +7,14 @@ import {
import {
addQuestion,
AddQuestionParams,
ChatMessageUpdatableProperties,
ChatUpdatableProperties,
createChat,
deleteChat,
getChatItems,
getChats,
updateChat,
updateChatMessage,
} from "./chat";
// TODO: split './chat.ts' into multiple files, per function for example
@ -33,5 +35,10 @@ export const useChatApi = () => {
chatId: string,
questionAndAnswer: QuestionAndAnwser
) => addQuestionAndAnswer(chatId, questionAndAnswer, axiosInstance),
updateChatMessage: async (
chatId: string,
messageId: string,
props: ChatMessageUpdatableProperties
) => updateChatMessage(chatId, messageId, props, axiosInstance),
};
};

View File

@ -15,7 +15,7 @@ interface IconProps {
classname?: string;
hovered?: boolean;
handleHover?: boolean;
onClick?: () => void;
onClick?: () => void | Promise<void>;
}
export const Icon = ({

View File

@ -18,6 +18,8 @@ import {
FaRegFileAlt,
FaRegKeyboard,
FaRegStar,
FaRegThumbsDown,
FaRegThumbsUp,
FaRegUserCircle,
FaSun,
FaTwitter,
@ -114,6 +116,8 @@ export const iconList: { [name: string]: IconType } = {
software: CgSoftwareDownload,
star: FaRegStar,
sun: FaSun,
thumbsDown: FaRegThumbsDown,
thumbsUp: FaRegThumbsUp,
twitter: FaTwitter,
unlock: FaUnlock,
upload: FiUpload,

View File

@ -0,0 +1,11 @@
create type "public"."user_identity_company_size" as enum ('1-10', '10-25', '25-50', '50-100', '100-250', '250-500', '500-1000', '1000-5000', '+5000');
alter table "public"."chat_history" drop column "user_feedback";
alter table "public"."chat_history" add column "thumbs" boolean;
alter table "public"."user_identity" add column "company_size" user_identity_company_size;
alter table "public"."user_identity" add column "usage_purpose" text;