diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 4459441fd..5181a9ec4 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -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 diff --git a/src/backend/base/langflow/schema/properties.py b/src/backend/base/langflow/schema/properties.py index 1f54eb473..c8b57b49e 100644 --- a/src/backend/base/langflow/schema/properties.py +++ b/src/backend/base/langflow/schema/properties.py @@ -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 = [] diff --git a/src/backend/base/langflow/services/database/models/message/model.py b/src/backend/base/langflow/services/database/models/message/model.py index 73f715bbc..7313f6a11 100644 --- a/src/backend/base/langflow/services/database/models/message/model.py +++ b/src/backend/base/langflow/services/database/models/message/model.py @@ -160,3 +160,4 @@ class MessageUpdate(SQLModel): files: list[str] | None = None edit: bool | None = None error: bool | None = None + properties: Properties | None = None diff --git a/src/backend/tests/unit/components/inputs/test_input_components.py b/src/backend/tests/unit/components/inputs/test_input_components.py index 56b34fee6..3c97d387a 100644 --- a/src/backend/tests/unit/components/inputs/test_input_components.py +++ b/src/backend/tests/unit/components/inputs/test_input_components.py @@ -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, diff --git a/src/frontend/src/icons/thumbs/index.tsx b/src/frontend/src/icons/thumbs/index.tsx new file mode 100644 index 000000000..6e99822ae --- /dev/null +++ b/src/frontend/src/icons/thumbs/index.tsx @@ -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 ; +}); + +export const ThumbDownIconCustom = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + return ; +}); diff --git a/src/frontend/src/icons/thumbs/thumbDown.jsx b/src/frontend/src/icons/thumbs/thumbDown.jsx new file mode 100644 index 000000000..dd95eb5b2 --- /dev/null +++ b/src/frontend/src/icons/thumbs/thumbDown.jsx @@ -0,0 +1,25 @@ +const ThumbDownFilled = (props) => ( + + + + +); +export default ThumbDownFilled; diff --git a/src/frontend/src/icons/thumbs/thumbUp.jsx b/src/frontend/src/icons/thumbs/thumbUp.jsx new file mode 100644 index 000000000..9e01f0442 --- /dev/null +++ b/src/frontend/src/icons/thumbs/thumbUp.jsx @@ -0,0 +1,25 @@ +const ThumbUpFilled = (props) => ( + + + + +); +export default ThumbUpFilled; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/editMessageButton/newMessageOptions.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/editMessageButton/newMessageOptions.tsx index 2a3748163..2ec2be4a6 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/editMessageButton/newMessageOptions.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/editMessageButton/newMessageOptions.tsx @@ -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 & { 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({ + + {isBotMessage && ( +
+ +
+ +
+
+ + +
+ +
+
+
+ )} ); } diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/newChatMessage.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/newChatMessage.tsx index 75f1cb016..2d3cdc87a 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/newChatMessage.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/newChatMessage.tsx @@ -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 ? (
(Edited)
) : 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} /> diff --git a/src/frontend/src/modals/IOModal/components/chatView/newChatView.tsx b/src/frontend/src/modals/IOModal/components/chatView/newChatView.tsx index 6785ed863..551fd33d7 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/newChatView.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/newChatView.tsx @@ -177,7 +177,10 @@ export default function ChatView({

New chat

-

+

Test your flow with a chat prompt diff --git a/src/frontend/src/modals/IOModal/newModal.tsx b/src/frontend/src/modals/IOModal/newModal.tsx index 2531c0db1..10802df65 100644 --- a/src/frontend/src/modals/IOModal/newModal.tsx +++ b/src/frontend/src/modals/IOModal/newModal.tsx @@ -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( Array.from( @@ -129,7 +128,6 @@ export default function IOModal({ ), ), ); - const flowPool = useFlowStore((state) => state.flowPool); const [sessionId, setSessionId] = useState(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 { - 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 => { + 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); diff --git a/src/frontend/src/types/chat/index.ts b/src/frontend/src/types/chat/index.ts index 1bfac3d20..2fa10a036 100644 --- a/src/frontend/src/types/chat/index.ts +++ b/src/frontend/src/types/chat/index.ts @@ -37,6 +37,7 @@ export type PropertiesType = { edited?: boolean; allow_markdown?: boolean; state?: string; + positive_feedback?: boolean | null; }; export type ChatOutputType = { diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 4e7849f5e..e13b3b783 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -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, }; diff --git a/src/frontend/tests/core/features/playground.spec.ts b/src/frontend/tests/core/features/playground.spec.ts index db7eb6c27..7ad06be57 100644 --- a/src/frontend/tests/core/features/playground.spec.ts +++ b/src/frontend/tests/core/features/playground.spec.ts @@ -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, + }); }, );