feature: Add message feedback and update frontend interface (#5022)

* add style for message feedback

* add backend suprot to new feature

* update frontend interface and add handle function

* [autofix.ci] apply automated fixes

* Update tooltip content for bot messages

* Update evaluation icons styling

* Add custom thumb icons for thumbs up and thumbs down

* Add custom thumb icons for thumbs up and thumbs down

* Update thumb icons based on evaluation value

* [autofix.ci] apply automated fixes

* Update property name for positive feedback

* Update property name for positive feedback

* feat: Add data-testid attributes to helpful and not helpful buttons and update test of playground to include new functionality

* update test to include new message features
This commit is contained in:
anovazzi1 2024-12-17 16:09:06 -03:00 committed by GitHub
commit 02fbb450db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 258 additions and 61 deletions

View file

@ -92,14 +92,14 @@ async def update_message(
try:
message_dict = message.model_dump(exclude_unset=True, exclude_none=True)
message_dict["edit"] = True
if "text" in message_dict and message_dict["text"] != db_message.text:
message_dict["edit"] = True
db_message.sqlmodel_update(message_dict)
session.add(db_message)
await session.commit()
await session.refresh(db_message)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return db_message

View file

@ -19,6 +19,7 @@ class Properties(BaseModel):
source: Source = Field(default_factory=Source)
icon: str | None = None
allow_markdown: bool = False
positive_feedback: bool | None = None
state: Literal["partial", "complete"] = "complete"
targets: list = []

View file

@ -160,3 +160,4 @@ class MessageUpdate(SQLModel):
files: list[str] | None = None
edit: bool | None = None
error: bool | None = None
properties: Properties | None = None

View file

@ -52,6 +52,7 @@ class TestChatInput(ComponentTestBaseWithClient):
"background_color": default_kwargs["background_color"],
"text_color": default_kwargs["text_color"],
"icon": default_kwargs["chat_icon"],
"positive_feedback": None,
"edited": False,
"source": {"id": None, "display_name": None, "source": None},
"allow_markdown": False,

View file

@ -0,0 +1,17 @@
import React, { forwardRef } from "react";
import ThumbDownFilled from "./thumbDown";
import ThumbUpFilled from "./thumbUp";
export const ThumbUpIconCustom = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <ThumbUpFilled ref={ref} {...props} />;
});
export const ThumbDownIconCustom = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <ThumbDownFilled ref={ref} {...props} />;
});

View file

@ -0,0 +1,25 @@
const ThumbDownFilled = (props) => (
<svg
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6.74945 13.59L7.49945 10.5H3.12695C2.89409 10.5 2.66442 10.4458 2.45613 10.3416C2.24785 10.2375 2.06667 10.0863 1.92695 9.9C1.78723 9.71371 1.6928 9.49744 1.65115 9.26833C1.60949 9.03922 1.62175 8.80355 1.68695 8.58L3.43445 2.58C3.52533 2.26843 3.71481 1.99473 3.97445 1.8C4.2341 1.60527 4.5499 1.5 4.87445 1.5H14.9995C15.3973 1.5 15.7788 1.65804 16.0601 1.93934C16.3414 2.22064 16.4995 2.60218 16.4995 3V9C16.4995 9.39782 16.3414 9.77936 16.0601 10.0607C15.7788 10.342 15.3973 10.5 14.9995 10.5H12.9295C12.6504 10.5001 12.3769 10.5781 12.1397 10.7252C11.9026 10.8723 11.7111 11.0826 11.587 11.3325L8.99945 16.5C8.64577 16.4956 8.29765 16.4114 7.9811 16.2536C7.66455 16.0957 7.38776 15.8684 7.1714 15.5886C6.95504 15.3088 6.80472 14.9837 6.73165 14.6376C6.65859 14.2915 6.66467 13.9334 6.74945 13.59Z"
fill="#FEE2E2"
stroke="#DC2626"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12.75 10.5V1.5"
stroke="#DC2626"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
export default ThumbDownFilled;

View file

@ -0,0 +1,25 @@
const ThumbUpFilled = (props) => (
<svg
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11.25 4.41L10.5 7.5H14.8725C15.1054 7.5 15.335 7.55422 15.5433 7.65836C15.7516 7.7625 15.9328 7.91371 16.0725 8.1C16.2122 8.28629 16.3066 8.50256 16.3483 8.73167C16.39 8.96078 16.3777 9.19645 16.3125 9.42L14.565 15.42C14.4741 15.7316 14.2846 16.0053 14.025 16.2C13.7654 16.3947 13.4496 16.5 13.125 16.5H3C2.60218 16.5 2.22064 16.342 1.93934 16.0607C1.65804 15.7794 1.5 15.3978 1.5 15V9C1.5 8.60218 1.65804 8.22064 1.93934 7.93934C2.22064 7.65804 2.60218 7.5 3 7.5H5.07C5.34906 7.49985 5.62255 7.42186 5.85972 7.27479C6.09688 7.12772 6.28832 6.91741 6.4125 6.6675L9 1.5C9.35368 1.50438 9.7018 1.58863 10.0184 1.74645C10.3349 1.90427 10.6117 2.13158 10.8281 2.4114C11.0444 2.69122 11.1947 3.01632 11.2678 3.3624C11.3409 3.70848 11.3348 4.0666 11.25 4.41Z"
fill="#D1FAE5"
stroke="#059669"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.25 7.5V16.5"
stroke="#059669"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
export default ThumbUpFilled;

View file

@ -1,25 +1,35 @@
import IconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import { cn } from "@/utils/utils";
import { ButtonHTMLAttributes, useState } from "react";
export function EditMessageButton({
onEdit,
onCopy,
onDelete,
onEvaluate,
isBotMessage,
evaluation,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
onEdit: () => void;
onCopy: () => void;
onDelete: () => void;
onEvaluate?: (value: boolean | null) => void;
isBotMessage?: boolean;
evaluation?: boolean | null;
}) {
const [isCopied, setIsCopied] = useState(false);
const handleCopy = () => {
onCopy();
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
setTimeout(() => setIsCopied(false), 2000);
};
const handleEvaluate = (value: boolean) => {
onEvaluate?.(evaluation === value ? null : value);
};
return (
@ -56,6 +66,46 @@ export function EditMessageButton({
</Button>
</div>
</ShadTooltip>
{isBotMessage && (
<div className="flex">
<ShadTooltip styleClasses="z-50" content="Helpful" side="top">
<div className="p-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEvaluate(true)}
className="h-8 w-8"
data-testid="helpful-button"
>
<IconComponent
name={evaluation === true ? "ThumbUpIconCustom" : "ThumbsUp"}
className={cn("h-4 w-4")}
/>
</Button>
</div>
</ShadTooltip>
<ShadTooltip styleClasses="z-50" content="Not helpful" side="top">
<div className="p-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEvaluate(false)}
className="h-8 w-8"
data-testid="not-helpful-button"
>
<IconComponent
name={
evaluation === false ? "ThumbDownIconCustom" : "ThumbsDown"
}
className={cn("h-4 w-4")}
/>
</Button>
</div>
</ShadTooltip>
</div>
)}
</div>
);
}

View file

@ -212,6 +212,35 @@ export default function ChatMessage({
},
);
};
const handleEvaluateAnswer = (evaluation: boolean | null) => {
updateMessageMutation(
{
message: {
...chat,
files: convertFiles(chat.files),
sender_name: chat.sender_name ?? "AI",
text: chat.message.toString(),
sender: chat.isSend ? "User" : "Machine",
flow_id,
session_id: chat.session ?? "",
properties: {
...chat.properties,
positive_feedback: evaluation,
},
},
refetch: true,
},
{
onError: () => {
setErrorData({
title: "Error updating messages.",
});
},
},
);
};
const editedFlag = chat.edit ? (
<div className="text-sm text-muted-foreground">(Edited)</div>
) : null;
@ -742,6 +771,9 @@ export default function ChatMessage({
onDelete={() => {}}
onEdit={() => setEditMessage(true)}
className="h-fit group-hover:visible"
isBotMessage={!chat.isSend}
onEvaluate={handleEvaluateAnswer}
evaluation={chat.properties?.positive_feedback}
/>
</div>
</div>

View file

@ -177,7 +177,10 @@ export default function ChatView({
<h3 className="mt-2 pb-2 text-2xl font-semibold text-primary">
New chat
</h3>
<p className="text-lg text-muted-foreground">
<p
className="text-lg text-muted-foreground"
data-testid="new-chat-text"
>
<TextEffectPerChar>
Test your flow with a chat prompt
</TextEffectPerChar>

View file

@ -4,7 +4,7 @@ import {
useGetMessagesQuery,
} from "@/controllers/API/queries/messages";
import { useUtilityStore } from "@/stores/utilityStore";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import IconComponent from "../../components/common/genericIconComponent";
import ShadTooltip from "../../components/common/shadTooltipComponent";
import { Button } from "../../components/ui/button";
@ -118,7 +118,6 @@ export default function IOModal({
const setLockChat = useFlowStore((state) => state.setLockChat);
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
const setNode = useFlowStore((state) => state.setNode);
const messages = useMessagesStore((state) => state.messages);
const [sessions, setSessions] = useState<string[]>(
Array.from(
@ -129,7 +128,6 @@ export default function IOModal({
),
),
);
const flowPool = useFlowStore((state) => state.flowPool);
const [sessionId, setSessionId] = useState<string>(currentFlowId);
const { isFetched: messagesFetched } = useGetMessagesQuery(
{
@ -139,33 +137,44 @@ export default function IOModal({
{ enabled: open },
);
async function sendMessage({
repeat = 1,
files,
}: {
repeat: number;
files?: string[];
}): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
setChatValue("");
for (let i = 0; i < repeat; i++) {
await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
files: files,
silent: true,
session: sessionId,
setLockChat,
}).catch((err) => {
console.error(err);
setLockChat(false);
});
}
// refetch();
setLockChat(false);
}
const sendMessage = useCallback(
async ({
repeat = 1,
files,
}: {
repeat: number;
files?: string[];
}): Promise<void> => {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
setChatValue("");
for (let i = 0; i < repeat; i++) {
await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
files: files,
silent: true,
session: sessionId,
setLockChat,
}).catch((err) => {
console.error(err);
setLockChat(false);
});
}
// refetch();
setLockChat(false);
},
[
isBuilding,
setIsBuilding,
setLockChat,
chatValue,
chatInput?.id,
sessionId,
buildFlow,
],
);
useEffect(() => {
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);

View file

@ -37,6 +37,7 @@ export type PropertiesType = {
edited?: boolean;
allow_markdown?: boolean;
state?: string;
positive_feedback?: boolean | null;
};
export type ChatOutputType = {

View file

@ -15,6 +15,7 @@ import { ZepMemoryIcon } from "@/icons/ZepMemory";
import { AthenaIcon } from "@/icons/athena/index";
import { freezeAllIcon } from "@/icons/freezeAll";
import { GlobeOkIcon } from "@/icons/globe-ok";
import { ThumbDownIconCustom, ThumbUpIconCustom } from "@/icons/thumbs";
import { TwitterLogoIcon } from "@radix-ui/react-icons";
import {
AlertCircle,
@ -205,6 +206,8 @@ import {
TextCursorInput,
TextSearch,
TextSearchIcon,
ThumbsDown,
ThumbsUp,
ToyBrick,
Trash2,
Type,
@ -927,4 +930,8 @@ export const nodeIconsLucide: iconsType = {
Cog,
ArrowRightLeft,
FolderSync,
ThumbsUp,
ThumbsDown,
ThumbDownIconCustom,
ThumbUpIconCustom,
};

View file

@ -22,7 +22,9 @@ test(
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="outputsChat Output"]', {
timeout: 100000,
});
await page
.getByTestId("outputsChat Output")
@ -39,7 +41,9 @@ test(
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="inputsChat Input"]', {
timeout: 100000,
});
await page
.getByTestId("inputsChat Input")
@ -53,7 +57,9 @@ test(
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="outputsText Output"]', {
timeout: 100000,
});
await page
.getByTestId("outputsText Output")
@ -168,7 +174,6 @@ test(
.filter({ hasText: /^Usermessage 1$/ })
.getByTestId("icon-Pen")
.click();
await page.waitForTimeout(500);
await page.getByTestId("textarea").fill("edit_1");
await page.getByTestId("save-button").click();
@ -177,8 +182,6 @@ test(
// check cancel edit
await page.getByTestId("sender_name_user").hover();
await page.getByTestId("icon-Pen").first().click();
await page.waitForTimeout(500);
await page.getByTestId("textarea").fill("cancel_edit");
await page.getByTestId("cancel-button").click();
await page.getByTestId("chat-message-User-edit_1").click();
@ -190,7 +193,6 @@ test(
.click();
await page.getByTestId("chat-message-AI-message 1").hover();
await page.getByTestId("icon-Pen").last().click();
await page.waitForTimeout(500);
await page.getByTestId("textarea").fill("edit_bot_1");
await page.getByTestId("save-button").click();
@ -198,7 +200,6 @@ test(
// check cancel edit bot
await page.getByTestId("chat-message-AI-edit_bot_1").hover();
await page.getByTestId("icon-Pen").last().click();
await page.waitForTimeout(500);
await page.getByTestId("textarea").fill("edit_bot_cancel");
await page.getByTestId("cancel-button").click();
@ -241,23 +242,47 @@ test(
await page.getByTestId("chat-message-User-session_after_delete").click();
await expect(page.getByTestId("session-selector")).toBeVisible();
// check new chat
await page.getByTestId("new-chat").click();
await page.waitForTimeout(5000);
await page.getByText("New chat").click();
await page.getByTestId("input-chat-playground").click();
await page.getByTestId("input-chat-playground").fill("second session");
await page.keyboard.press("Enter");
await page.waitForTimeout(5000);
await page.getByTestId("chat-message-User-second session").click();
await page
.getByTestId("chat-message-AI-second session")
.getByText("second session")
.click();
expect(await page.getByTestId("session-selector").count()).toBe(2);
const sessionElements = await page.getByTestId("session-selector").all();
expect(sessionElements.length).toBe(2);
// check helpful button
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await page.getByTestId("helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbUpIconCustom")).toBeVisible({
timeout: 10000,
});
await page.getByTestId("helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbUpIconCustom")).toBeVisible({
timeout: 10000,
visible: false,
});
// check not helpful button
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await page.getByTestId("not-helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbDownIconCustom")).toBeVisible({
timeout: 10000,
});
await page.getByTestId("not-helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbDownIconCustom")).toBeVisible({
timeout: 10000,
visible: false,
});
// check switch feedback
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await page.getByTestId("helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbUpIconCustom")).toBeVisible({
timeout: 10000,
});
await page.getByTestId("not-helpful-button").click();
await page.getByTestId("chat-message-AI-session_after_delete").hover();
await expect(page.getByTestId("icon-ThumbDownIconCustom")).toBeVisible({
timeout: 10000,
});
await expect(page.getByTestId("icon-ThumbUpIconCustom")).toBeVisible({
timeout: 10000,
visible: false,
});
},
);