mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-01 21:57:51 +03:00
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:
parent
d7d1a0155b
commit
da8e7513e6
1
.gitignore
vendored
1
.gitignore
vendored
@ -81,3 +81,4 @@ airwallexpayouts.py
|
||||
application.log
|
||||
backend/celerybeat-schedule.db
|
||||
|
||||
backend/application.log.*
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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,9 +50,97 @@ 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_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 (
|
||||
<div
|
||||
className={`
|
||||
@ -54,44 +148,12 @@ export const MessageRow = React.forwardRef(
|
||||
${isUserSpeaker ? styles.user : styles.brain}
|
||||
`}
|
||||
>
|
||||
{!isUserSpeaker ? (
|
||||
<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>
|
||||
)}
|
||||
{}
|
||||
{renderMessageHeader()}
|
||||
<div ref={ref} className={styles.message_row_content}>
|
||||
{children ?? (
|
||||
<>
|
||||
<MessageContent text={messageContent} isUser={isUserSpeaker} />
|
||||
{!isUserSpeaker && messageContent !== "🧠" && (
|
||||
<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>
|
||||
)}
|
||||
{renderIcons()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -22,6 +22,7 @@ export type ChatMessage = {
|
||||
metadata?: {
|
||||
sources?: Source[];
|
||||
};
|
||||
thumbs?: boolean;
|
||||
};
|
||||
|
||||
type NotificationStatus = "Pending" | "Done";
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ interface IconProps {
|
||||
classname?: string;
|
||||
hovered?: boolean;
|
||||
handleHover?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const Icon = ({
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user