From adb3e8b6014e8e6e2d0e588ec72bbae9fcead99a Mon Sep 17 00:00:00 2001 From: jstawskigmi Date: Wed, 1 Oct 2025 23:48:46 -0400 Subject: [PATCH 1/7] feat: added bad response --- .../HistoricalMessage/Actions/index.jsx | 38 +++++++++++++++++-- frontend/src/locales/ar/common.js | 4 ++ frontend/src/locales/da/common.js | 4 ++ frontend/src/locales/de/common.js | 4 ++ frontend/src/locales/en/common.js | 4 ++ frontend/src/locales/es/common.js | 4 ++ frontend/src/locales/et/common.js | 1 + frontend/src/locales/fa/common.js | 4 ++ frontend/src/locales/fr/common.js | 4 ++ frontend/src/locales/he/common.js | 4 ++ frontend/src/locales/it/common.js | 4 ++ frontend/src/locales/ja/common.js | 4 ++ frontend/src/locales/ko/common.js | 4 ++ frontend/src/locales/lv/common.js | 4 ++ frontend/src/locales/nl/common.js | 4 ++ frontend/src/locales/pl/common.js | 4 ++ frontend/src/locales/pt_BR/common.js | 4 ++ frontend/src/locales/ru/common.js | 4 ++ frontend/src/locales/vn/common.js | 4 ++ frontend/src/locales/zh/common.js | 4 ++ frontend/src/locales/zh_TW/common.js | 4 ++ frontend/src/models/workspace.js | 14 +++++++ server/endpoints/workspaces.js | 38 +++++++++++++++++++ server/prisma/schema.prisma | 1 + server/utils/helpers/chat/responses.js | 1 + 25 files changed, 166 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx index e0c1273fdbd..0b7e88c9e0e 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -1,7 +1,8 @@ import React, { memo, useState } from "react"; import useCopyText from "@/hooks/useCopyText"; -import { Check, ThumbsUp, ArrowsClockwise, Copy } from "@phosphor-icons/react"; +import { Check, ThumbsUp, ThumbsDown, ArrowsClockwise, Copy } from "@phosphor-icons/react"; import Workspace from "@/models/workspace"; +import ChatFeedbackModal from "@/components/Modals/ChatFeedback"; import { EditMessageAction } from "./EditMessage"; import RenderMetrics from "./RenderMetrics"; import ActionMenu from "./ActionMenu"; @@ -22,11 +23,18 @@ const Actions = ({ }) => { const { t } = useTranslation(); const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); + const [showFeedbackModal, setShowFeedbackModal] = useState(false); + const [submittingComment, setSubmittingComment] = useState(false); const handleFeedback = async (newFeedback) => { - const updatedFeedback = - selectedFeedback === newFeedback ? null : newFeedback; + const updatedFeedback = selectedFeedback === newFeedback ? null : newFeedback; + // persist feedback score first await Workspace.updateChatFeedback(chatId, slug, updatedFeedback); setSelectedFeedback(updatedFeedback); + + // If user just set negative feedback (not unsetting), show optional feedback modal + if (updatedFeedback === false) { + setShowFeedbackModal(true); + } }; return ( @@ -55,6 +63,15 @@ const Actions = ({ IconComponent={ThumbsUp} /> )} + {chatId && role !== "user" && !isEditing && ( + handleFeedback(false)} + tooltipId="feedback-button" + tooltipContent={t("chat_window.bad_response")} + IconComponent={ThumbsDown} + /> + )} + setShowFeedbackModal(false)} + chatId={chatId} + slug={slug} + onSubmitted={async (comment) => { + if (submittingComment) return; + setSubmittingComment(true); + try { + await Workspace.submitChatFeedbackComment(chatId, slug, comment); + } catch (e) {} + setSubmittingComment(false); + }} + /> ); }; @@ -151,3 +182,4 @@ function RegenerateMessage({ regenerateMessage, chatId }) { } export default memo(Actions); + diff --git a/frontend/src/locales/ar/common.js b/frontend/src/locales/ar/common.js index cc85ea8d77b..6e5e74ce947 100644 --- a/frontend/src/locales/ar/common.js +++ b/frontend/src/locales/ar/common.js @@ -660,6 +660,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/da/common.js b/frontend/src/locales/da/common.js index 51089ea38b2..441922929dd 100644 --- a/frontend/src/locales/da/common.js +++ b/frontend/src/locales/da/common.js @@ -698,6 +698,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/de/common.js b/frontend/src/locales/de/common.js index 4106d401cbe..3eb16546c27 100644 --- a/frontend/src/locales/de/common.js +++ b/frontend/src/locales/de/common.js @@ -893,6 +893,10 @@ const TRANSLATIONS = { regenerate: "Neu generieren", regenerate_response: "Antwort neu generieren", good_response: "Gute Antwort", + bad_response: "Schlechte Antwort", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "Weitere Aktionen", hide_citations: "Quellenangaben ausblenden", show_citations: "Quellenangaben anzeigen", diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js index b05399c32a6..2d0d1652ca0 100644 --- a/frontend/src/locales/en/common.js +++ b/frontend/src/locales/en/common.js @@ -950,6 +950,10 @@ const TRANSLATIONS = { regenerate: "Regenerate", regenerate_response: "Regenerate response", good_response: "Good response", + bad_response: "Bad response", + provide_feedback: "Provide Feedback", + bad_response_optional_feedback: "Optional feedback (help us improve)", + bad_response_placeholder: "Tell us what was wrong (optional)", more_actions: "More actions", hide_citations: "Hide citations", show_citations: "Show citations", diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js index 3d7e830be6b..77fe1cddffc 100644 --- a/frontend/src/locales/es/common.js +++ b/frontend/src/locales/es/common.js @@ -906,6 +906,10 @@ const TRANSLATIONS = { regenerate: "Regenerar", regenerate_response: "Regenerar respuesta", good_response: "Buena respuesta", + bad_response: "Mala respuesta", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "Más acciones", hide_citations: "Ocultar citas", show_citations: "Mostrar citas", diff --git a/frontend/src/locales/et/common.js b/frontend/src/locales/et/common.js index c6d04c4631d..8142596849d 100644 --- a/frontend/src/locales/et/common.js +++ b/frontend/src/locales/et/common.js @@ -853,6 +853,7 @@ const TRANSLATIONS = { regenerate: "Loo uuesti", regenerate_response: "Loo vastus uuesti", good_response: "Hea vastus", + bad_response: "Halb vastus", more_actions: "Rohkem toiminguid", hide_citations: "Peida viited", show_citations: "Näita viiteid", diff --git a/frontend/src/locales/fa/common.js b/frontend/src/locales/fa/common.js index 1f0cd82647b..66794a5f96c 100644 --- a/frontend/src/locales/fa/common.js +++ b/frontend/src/locales/fa/common.js @@ -652,6 +652,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js index f12183466f3..d182453403f 100644 --- a/frontend/src/locales/fr/common.js +++ b/frontend/src/locales/fr/common.js @@ -660,6 +660,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/he/common.js b/frontend/src/locales/he/common.js index 84ffe023fa9..8a04d0f8aed 100644 --- a/frontend/src/locales/he/common.js +++ b/frontend/src/locales/he/common.js @@ -860,6 +860,10 @@ const TRANSLATIONS = { regenerate: "צור מחדש", regenerate_response: "צור תגובה מחדש", good_response: "תגובה טובה", + bad_response: "תגובה רעה", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "פעולות נוספות", hide_citations: "הסתר ציטוטים", show_citations: "הצג ציטוטים", diff --git a/frontend/src/locales/it/common.js b/frontend/src/locales/it/common.js index 442a5f2c7ad..fb60f0dd81e 100644 --- a/frontend/src/locales/it/common.js +++ b/frontend/src/locales/it/common.js @@ -658,6 +658,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/ja/common.js b/frontend/src/locales/ja/common.js index 45e47e4d0bd..2c03d7d7611 100644 --- a/frontend/src/locales/ja/common.js +++ b/frontend/src/locales/ja/common.js @@ -690,6 +690,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/ko/common.js b/frontend/src/locales/ko/common.js index 28fd58d4e4a..34e1b5de2f3 100644 --- a/frontend/src/locales/ko/common.js +++ b/frontend/src/locales/ko/common.js @@ -869,6 +869,10 @@ const TRANSLATIONS = { regenerate: "다시 생성", regenerate_response: "응답 다시 생성", good_response: "좋은 답변", + bad_response: "나쁜 답변", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "더 많은 작업", hide_citations: "인용 숨기기", show_citations: "인용 보기", diff --git a/frontend/src/locales/lv/common.js b/frontend/src/locales/lv/common.js index 9786bce5c1c..07d0ffa254c 100644 --- a/frontend/src/locales/lv/common.js +++ b/frontend/src/locales/lv/common.js @@ -885,6 +885,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/nl/common.js b/frontend/src/locales/nl/common.js index 3d300d0af24..f963767b8b0 100644 --- a/frontend/src/locales/nl/common.js +++ b/frontend/src/locales/nl/common.js @@ -655,6 +655,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/pl/common.js b/frontend/src/locales/pl/common.js index d7673520a14..94c619be577 100644 --- a/frontend/src/locales/pl/common.js +++ b/frontend/src/locales/pl/common.js @@ -65,6 +65,10 @@ const TRANSLATIONS = { selection: "Wybór modelu", saving: "Zapisywanie...", save: "Zapisz zmiany", + bad_response: "Zła odpowiedź", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, previous: "Poprzednia strona", next: "Następna strona", optional: "Opcjonalnie", diff --git a/frontend/src/locales/pt_BR/common.js b/frontend/src/locales/pt_BR/common.js index 7795a3e98d4..cddf1a52763 100644 --- a/frontend/src/locales/pt_BR/common.js +++ b/frontend/src/locales/pt_BR/common.js @@ -866,6 +866,10 @@ const TRANSLATIONS = { regenerate: "Regerar", regenerate_response: "Regerar resposta", good_response: "Resposta satisfatória", + bad_response: "Resposta ruim", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "Mais ações", hide_citations: "Esconder citações", show_citations: "Exibir citações", diff --git a/frontend/src/locales/ru/common.js b/frontend/src/locales/ru/common.js index aebe5b68a98..1543eeaa5bb 100644 --- a/frontend/src/locales/ru/common.js +++ b/frontend/src/locales/ru/common.js @@ -699,6 +699,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/vn/common.js b/frontend/src/locales/vn/common.js index 6b095cd7814..98f931044d3 100644 --- a/frontend/src/locales/vn/common.js +++ b/frontend/src/locales/vn/common.js @@ -654,6 +654,10 @@ const TRANSLATIONS = { regenerate: null, regenerate_response: null, good_response: null, + bad_response: null, + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: null, hide_citations: null, show_citations: null, diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js index 95872cd59f7..31d9e4d6818 100644 --- a/frontend/src/locales/zh/common.js +++ b/frontend/src/locales/zh/common.js @@ -828,6 +828,10 @@ const TRANSLATIONS = { regenerate: "重新", regenerate_response: "重新回应", good_response: "反应良好", + bad_response: "不良回复", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "更多操作", hide_citations: "隐藏引文", show_citations: "显示引文", diff --git a/frontend/src/locales/zh_TW/common.js b/frontend/src/locales/zh_TW/common.js index f3acd752cec..a5bd524ce6c 100644 --- a/frontend/src/locales/zh_TW/common.js +++ b/frontend/src/locales/zh_TW/common.js @@ -661,6 +661,10 @@ const TRANSLATIONS = { regenerate: "重新", regenerate_response: "重新回應", good_response: "反應良好", + bad_response: "不良回應", + provide_feedback: null, + bad_response_optional_feedback: null, + bad_response_placeholder: null, more_actions: "更多操作", hide_citations: "隱藏引文", show_citations: "顯示引文", diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index 3627b4e3823..c563c95e89d 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -78,6 +78,20 @@ const Workspace = { .catch(() => false); return result; }, + submitChatFeedbackComment: async function (chatId, slug, comment) { + if (!chatId || !slug || !comment) return false; + const result = await fetch( + `${API_BASE}/workspace/${slug}/chat-feedback/${chatId}/comment`, + { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ comment }), + } + ) + .then((res) => res.ok) + .catch(() => false); + return result; + }, deleteChats: async function (slug = "", chatIds = []) { return await fetch(`${API_BASE}/workspace/${slug}/delete-chats`, { diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index af4eb9983b3..c78077bf018 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -518,6 +518,44 @@ function workspaceEndpoints(app) { } ); + app.post( + "/workspace/:slug/chat-feedback/:chatId/comment", + [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + async (request, response) => { + try { + const { chatId } = request.params; + const { comment = null } = reqBody(request); + const existingChat = await WorkspaceChats.get({ + id: Number(chatId), + workspaceId: response.locals.workspace.id, + }); + + if (!existingChat) { + response.status(404).end(); + return; + } + + if (!comment || String(comment).trim() === "") { + response.status(400).json({ success: false, message: "No comment provided" }); + return; + } + + // Save feedback comment to cache table for now (simple implementation) + await WorkspaceChats._update(Number(chatId), { + // append to response metadata as feedback_comment if desired + feedbackComment: String(comment).trim(), + lastUpdatedAt: new Date(), + }); + + // Optionally, in future persist comments in a dedicated table + response.status(200).json({ success: true }); + } catch (error) { + console.error("Error saving chat feedback comment:", error); + response.status(500).end(); + } + } + ); + app.get( "/workspace/:slug/suggested-messages", [validatedRequest, flexUserRoleValid([ROLES.all])], diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index a3db69f1e2b..ddc14baa9ed 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -193,6 +193,7 @@ model workspace_chats { createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) feedbackScore Boolean? + feedbackComment String? users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) } diff --git a/server/utils/helpers/chat/responses.js b/server/utils/helpers/chat/responses.js index 465d56fd2f6..aab015ac1e3 100644 --- a/server/utils/helpers/chat/responses.js +++ b/server/utils/helpers/chat/responses.js @@ -157,6 +157,7 @@ function convertToChatHistory(history = []) { sentAt: moment(createdAt).unix(), feedbackScore, metrics: data?.metrics || {}, + feedbackComment: record.feedbackComment || null, }, ]); } From e1b3d13c4c773e018d58d1631ff83c3c4deff4f3 Mon Sep 17 00:00:00 2001 From: jstawskigmi Date: Wed, 1 Oct 2025 23:59:41 -0400 Subject: [PATCH 2/7] feat: added bad response --- .../components/Modals/ChatFeedback/index.jsx | 80 +++++++++++++++++++ .../20251001000000_init/migration.sql | 2 + 2 files changed, 82 insertions(+) create mode 100644 frontend/src/components/Modals/ChatFeedback/index.jsx create mode 100644 server/prisma/migrations/20251001000000_init/migration.sql diff --git a/frontend/src/components/Modals/ChatFeedback/index.jsx b/frontend/src/components/Modals/ChatFeedback/index.jsx new file mode 100644 index 00000000000..69a3626be34 --- /dev/null +++ b/frontend/src/components/Modals/ChatFeedback/index.jsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import ModalWrapper from "@/components/ModalWrapper"; +import { X } from "@phosphor-icons/react"; +import Workspace from "@/models/workspace"; +import { useTranslation } from "react-i18next"; + +export default function ChatFeedbackModal({ isOpen, hideModal, chatId, slug, onSubmitted }) { + const { t } = useTranslation(); + const [comment, setComment] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!chatId || !slug) { + hideModal(); + return; + } + setSubmitting(true); + try { + // Only forward the comment to parent if there's content; feedback is optional + if (comment && comment.trim() !== "") { + onSubmitted && onSubmitted(comment.trim()); + } + } catch (err) { + // ignore errors for now + } + setSubmitting(false); + hideModal(); + }; + + return ( + +
+
+
+

+ {t("chat_window.provide_feedback")} +

+
+ +
+
+
+

+ {t("chat_window.bad_response_optional_feedback")} +

+