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 application.log
backend/celerybeat-schedule.db 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.controller.chat.brainful_chat import BrainfulChat
from modules.chat.dto.chats import ChatItem, ChatQuestion from modules.chat.dto.chats import ChatItem, ChatQuestion
from modules.chat.dto.inputs import ( from modules.chat.dto.inputs import (
ChatMessageProperties,
ChatUpdatableProperties, ChatUpdatableProperties,
CreateChatProperties, CreateChatProperties,
QuestionAndAnswer, QuestionAndAnswer,
@ -152,6 +153,32 @@ async def update_chat_metadata_handler(
return chat_service.update_chat(chat_id=chat_id, chat_data=chat_data) 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 # create new chat
@chat_router.post("/chat", dependencies=[Depends(AuthBearer())], tags=["Chat"]) @chat_router.post("/chat", dependencies=[Depends(AuthBearer())], tags=["Chat"])
async def create_chat_handler( async def create_chat_handler(

View File

@ -32,3 +32,14 @@ class ChatUpdatableProperties:
def __init__(self, chat_name: Optional[str]): def __init__(self, chat_name: Optional[str]):
self.chat_name = chat_name 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 None # string because UUID is not JSON serializable
) )
metadata: Optional[dict] | None = None metadata: Optional[dict] | None = None
thumbs: Optional[bool] | None = None
def dict(self, *args, **kwargs): def dict(self, *args, **kwargs):
chat_history = super().dict(*args, **kwargs) chat_history = super().dict(*args, **kwargs)

View File

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

View File

@ -1,4 +1,5 @@
from models.settings import get_supabase_client from models.settings import get_supabase_client
from modules.chat.dto.inputs import ChatMessageProperties
from modules.chat.entity.chat import Chat from modules.chat.entity.chat import Chat
from modules.chat.repository.chats_interface import ChatsInterface from modules.chat.repository.chats_interface import ChatsInterface
@ -102,3 +103,13 @@ class Chats(ChatsInterface):
def delete_chat_history(self, chat_id): def delete_chat_history(self, chat_id):
self.db.table("chat_history").delete().match({"chat_id": chat_id}).execute() 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 typing import Optional
from uuid import UUID 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 from modules.chat.entity.chat import Chat
@ -78,3 +78,10 @@ class ChatsInterface(ABC):
Delete chat history Delete chat history
""" """
pass 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.brain.service.brain_service import BrainService
from modules.chat.dto.chats import ChatItem from modules.chat.dto.chats import ChatItem
from modules.chat.dto.inputs import ( from modules.chat.dto.inputs import (
ChatMessageProperties,
ChatUpdatableProperties, ChatUpdatableProperties,
CreateChatHistory, CreateChatHistory,
CreateChatProperties, CreateChatProperties,
@ -102,6 +103,7 @@ class ChatService:
brain_id=str(brain.id) if brain else None, brain_id=str(brain.id) if brain else None,
prompt_title=prompt.title if prompt else None, prompt_title=prompt.title if prompt else None,
metadata=message.metadata, metadata=message.metadata,
thumbs=message.thumbs,
) )
) )
return enriched_history return enriched_history
@ -193,3 +195,14 @@ class ChatService:
except Exception as e: except Exception as e:
print(e) print(e)
pass 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, prompt_title,
metadata, metadata,
brain_id, brain_id,
thumbs,
} = content; } = content;
return ( return (
@ -36,6 +37,8 @@ export const QADisplay = ({ content, index }: QADisplayProps): JSX.Element => {
brainId={brain_id} brainId={brain_id}
index={index} index={index}
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment 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 { CopyButton } from "@/lib/components/ui/CopyButton";
import Icon from "@/lib/components/ui/Icon/Icon"; import Icon from "@/lib/components/ui/Icon/Icon";
import { useChatContext } from "@/lib/context"; import { useChatContext } from "@/lib/context";
@ -23,6 +25,8 @@ type MessageRowProps = {
}; };
brainId?: string; brainId?: string;
index?: number; index?: number;
messageId?: string;
thumbs?: boolean;
}; };
export const MessageRow = React.forwardRef( export const MessageRow = React.forwardRef(
@ -35,6 +39,8 @@ export const MessageRow = React.forwardRef(
children, children,
brainId, brainId,
index, index,
messageId,
thumbs: initialThumbs,
}: MessageRowProps, }: MessageRowProps,
ref: React.Ref<HTMLDivElement> ref: React.Ref<HTMLDivElement>
) => { ) => {
@ -44,9 +50,97 @@ export const MessageRow = React.forwardRef(
}); });
const { setSourcesMessageIndex, sourcesMessageIndex } = useChatContext(); const { setSourcesMessageIndex, sourcesMessageIndex } = useChatContext();
const { isMobile } = useDevice(); 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 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_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>
);
}
};
const renderIcons = () => {
if (!isUserSpeaker && messageContent !== "🧠") {
return (
<div className={styles.icons_wrapper}>
<CopyButton handleCopy={handleCopy} size="normal" />
{!isMobile && (
<div className={styles.sources_icon_wrapper}>
<Icon
name="file"
handleHover={true}
color={sourcesMessageIndex === index ? "primary" : "black"}
size="normal"
onClick={() => {
setSourcesMessageIndex(
sourcesMessageIndex === index ? undefined : index
);
}}
/>
</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 ( return (
<div <div
className={` className={`
@ -54,44 +148,12 @@ export const MessageRow = React.forwardRef(
${isUserSpeaker ? styles.user : styles.brain} ${isUserSpeaker ? styles.user : styles.brain}
`} `}
> >
{!isUserSpeaker ? ( {renderMessageHeader()}
<div className={styles.message_header}>
<QuestionBrain brainName={brainName} brainId={brainId} />
<QuestionPrompt promptName={promptName} />
</div>
) : (
<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}> <div ref={ref} className={styles.message_row_content}>
{children ?? ( {children ?? (
<> <>
<MessageContent text={messageContent} isUser={isUserSpeaker} /> <MessageContent text={messageContent} isUser={isUserSpeaker} />
{!isUserSpeaker && messageContent !== "🧠" && ( {renderIcons()}
<div className={styles.icons_wrapper}>
<CopyButton handleCopy={handleCopy} size="normal" />
{!isMobile && (
<div className={styles.sources_icon_wrapper}>
<Icon
name="file"
handleHover={true}
color={
sourcesMessageIndex === index ? "primary" : "black"
}
size="normal"
onClick={() => {
setSourcesMessageIndex(
sourcesMessageIndex === index ? undefined : index
);
}}
/>
</div>
)}
</div>
)}
</> </>
)} )}
</div> </div>

View File

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

View File

@ -7,6 +7,14 @@ import {
ChatQuestion, ChatQuestion,
} from "@/app/chat/[chatId]/types"; } from "@/app/chat/[chatId]/types";
export type ChatUpdatableProperties = {
chat_name?: string;
};
export type ChatMessageUpdatableProperties = {
thumbs?: boolean | null;
};
export const createChat = async ( export const createChat = async (
name: string, name: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
@ -59,9 +67,6 @@ export const getChatItems = async (
): Promise<ChatItem[]> => ): Promise<ChatItem[]> =>
(await axiosInstance.get<ChatItem[]>(`/chat/${chatId}/history`)).data; (await axiosInstance.get<ChatItem[]>(`/chat/${chatId}/history`)).data;
export type ChatUpdatableProperties = {
chat_name?: string;
};
export const updateChat = async ( export const updateChat = async (
chatId: string, chatId: string,
chat: ChatUpdatableProperties, chat: ChatUpdatableProperties,
@ -70,3 +75,17 @@ export const updateChat = async (
return (await axiosInstance.put<ChatEntity>(`/chat/${chatId}/metadata`, chat)) return (await axiosInstance.put<ChatEntity>(`/chat/${chatId}/metadata`, chat))
.data; .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 { import {
addQuestion, addQuestion,
AddQuestionParams, AddQuestionParams,
ChatMessageUpdatableProperties,
ChatUpdatableProperties, ChatUpdatableProperties,
createChat, createChat,
deleteChat, deleteChat,
getChatItems, getChatItems,
getChats, getChats,
updateChat, updateChat,
updateChatMessage,
} from "./chat"; } from "./chat";
// TODO: split './chat.ts' into multiple files, per function for example // TODO: split './chat.ts' into multiple files, per function for example
@ -33,5 +35,10 @@ export const useChatApi = () => {
chatId: string, chatId: string,
questionAndAnswer: QuestionAndAnwser questionAndAnswer: QuestionAndAnwser
) => addQuestionAndAnswer(chatId, questionAndAnswer, axiosInstance), ) => 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; classname?: string;
hovered?: boolean; hovered?: boolean;
handleHover?: boolean; handleHover?: boolean;
onClick?: () => void; onClick?: () => void | Promise<void>;
} }
export const Icon = ({ export const Icon = ({

View File

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