From 86f93a18cca2165163f4ba2e80b5839ee4bc7984 Mon Sep 17 00:00:00 2001 From: rayhpeng Date: Thu, 23 Jul 2026 15:31:07 +0800 Subject: [PATCH] feat(frontend): strict feedback toggle and thumbs-down reason dialog One rating at a time: the other thumb is hidden while a rating exists and must be retracted before switching. Thumbs-down stores the rating immediately, then opens a follow-up dialog (reason chips + optional details) submitted as a second idempotent PUT. Reason slugs are language-neutral; labels localize via the i18n catalog. The buttons move to the assistant turn action bar next to copy/regenerate. --- .../workspace/messages/feedback-dialog.tsx | 113 ++++++++++++++++++ .../workspace/messages/message-list-item.tsx | 92 +++++++++----- .../workspace/messages/message-list.tsx | 20 +++- frontend/src/core/api/feedback.ts | 20 +++- frontend/src/core/i18n/locales/en-US.ts | 15 +++ frontend/src/core/i18n/locales/types.ts | 15 +++ frontend/src/core/i18n/locales/zh-CN.ts | 15 +++ 7 files changed, 260 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/workspace/messages/feedback-dialog.tsx diff --git a/frontend/src/components/workspace/messages/feedback-dialog.tsx b/frontend/src/components/workspace/messages/feedback-dialog.tsx new file mode 100644 index 000000000..4ca8a740c --- /dev/null +++ b/frontend/src/components/workspace/messages/feedback-dialog.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState } from "react"; + +import { FEEDBACK_TAG_SLUGS, type FeedbackTagSlug } from "@/core/api/feedback"; +import { useI18n } from "@/core/i18n/hooks"; +import { cn } from "@/lib/utils"; + +import { Button } from "../../ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../../ui/dialog"; +import { Textarea } from "../../ui/textarea"; + +/** Maps language-neutral tag slugs to their i18n label keys. */ +const TAG_LABEL_KEYS: Record = { + incorrect: "incorrect", + not_as_expected: "notAsExpected", + slow: "slow", + style_tone: "styleTone", + safety_legal: "safetyLegal", + other: "other", +}; + +/** + * Thumbs-down follow-up dialog (ChatGPT-style): reason chips + optional + * details. The rating itself is already stored when this opens; submitting + * issues a second idempotent PUT that enriches the same record with + * tags/comment. Closing without submitting keeps the plain thumbs-down. + */ +export function FeedbackDialog({ + open, + onOpenChange, + onSubmit, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (tags: string[], comment: string) => Promise; +}) { + const { t } = useI18n(); + const [selected, setSelected] = useState>(new Set()); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const toggleTag = (slug: FeedbackTagSlug) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(slug)) { + next.delete(slug); + } else { + next.add(slug); + } + return next; + }); + }; + + const canSubmit = (selected.size > 0 || comment.trim().length > 0) && !isSubmitting; + + const handleSubmit = async () => { + if (!canSubmit) return; + setIsSubmitting(true); + try { + await onSubmit([...selected], comment.trim()); + onOpenChange(false); + setSelected(new Set()); + setComment(""); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {t.feedback.dialogTitle} + +
+ {FEEDBACK_TAG_SLUGS.map((slug) => ( + + ))} +
+