22 * Shared 点赞/点踩 (thumbs up / down) feedback control.
33 *
44 * Reused by every AI answer surface (daily chat, knowledge-space 知源, channel
5- * subscription via AiMessageBubble; linsight task mode via ResultPanel). The
6- * button visuals match the appChat MessageButtons / AiMessageBubble action row
7- * (size-6 hit area, 14px bisheng-icons Outlined glyph, #818181 idle /
8- * brand-500 active) so the whole action row reads as one consistent set.
5+ * subscription via AiMessageBubble; linsight task mode via ResultPanel; appChat
6+ * workflow/assistant via MessageButtons). The button visuals match the
7+ * AiMessageBubble action row (size-6 hit area, 14px bisheng-icons Outlined
8+ * glyph, #818181 idle / brand-500 active) so the whole action row reads as one
9+ * consistent set.
910 *
10- * State is optimistic-local: the parent injects the persistence via `onLike`
11- * (thumbs verdict) and `onDislikeComment` (reason text). `liked` seeds the
12- * initial highlight and re-syncs when history reload delivers the stored value.
11+ * Dislike is deferred: clicking thumbs-down only opens the reason dialog
12+ * (shared shell: ui/CommentDialog, which resets the draft on open) —
13+ * nothing is persisted or highlighted until the user hits submit (the comment
14+ * itself is optional). Cancel/close discards the dislike entirely. Thumbs-up
15+ * and un-toggling persist immediately. `liked` seeds the initial highlight and
16+ * re-syncs when history reload delivers the stored value.
1317 */
14- import { useEffect , useRef , useState } from "react" ;
18+ import { useEffect , useState } from "react" ;
1519import { Outlined } from "bisheng-icons" ;
16- import { Button , Dialog , DialogContent , DialogHeader , DialogTitle , Textarea } from "~/components" ;
17- import { useToastContext } from "~/Providers" ;
20+ import { CommentDialog } from "~/components" ;
1821import { useLocalize } from "~/hooks" ;
1922import { cn } from "~/utils" ;
2023
@@ -27,9 +30,9 @@ const ACTION_BTN =
2730interface MessageFeedbackButtonsProps {
2831 /** Initial / persisted verdict: 0 none, 1 up, 2 down. */
2932 liked ?: number ;
30- /** Persist the new verdict (0/1/2). Called on every toggle . */
33+ /** Persist the new verdict (0/1/2). Dislike is only sent on dialog submit . */
3134 onLike : ( liked : number ) => void ;
32- /** Persist the free-text reason when the user submits a dislike comment. */
35+ /** Persist the free-text reason when the user submits a non-empty dislike comment. */
3336 onDislikeComment ?: ( comment : string ) => void ;
3437 className ?: string ;
3538}
@@ -41,41 +44,31 @@ export function MessageFeedbackButtons({
4144 className,
4245} : MessageFeedbackButtonsProps ) {
4346 const localize = useLocalize ( ) ;
44- const { showToast } = useToastContext ( ) ;
4547 const [ state , setState ] = useState < ThumbsState > ( liked as ThumbsState ) ;
4648 const [ commentOpen , setCommentOpen ] = useState ( false ) ;
47- const [ commentError , setCommentError ] = useState ( false ) ;
48- const commentRef = useRef < HTMLTextAreaElement | null > ( null ) ;
4949
5050 // Re-sync when the persisted value arrives/changes (e.g. history reload).
5151 useEffect ( ( ) => {
5252 setState ( liked as ThumbsState ) ;
5353 } , [ liked ] ) ;
5454
5555 const handleClick = ( type : ThumbsState ) => {
56- setState ( ( prev ) => {
57- const next : ThumbsState = prev === type ? 0 : type ;
58- onLike ( next ) ;
59- // Prompt for a reason only when newly disliking.
60- if ( next === 2 && onDislikeComment ) {
61- setCommentError ( false ) ;
62- setCommentOpen ( true ) ;
63- if ( commentRef . current ) commentRef . current . value = "" ;
64- }
65- return next ;
66- } ) ;
67- } ;
68-
69- const handleSubmitComment = ( ) => {
70- const value = commentRef . current ?. value ?. trim ( ) ;
71- if ( ! value ) {
72- showToast ?.( { message : localize ( "com_feedback_required" ) , status : "warning" } ) ;
73- setCommentError ( true ) ;
56+ // Newly disliking with a reason dialog available: defer — no persist,
57+ // no highlight until the dialog is submitted.
58+ if ( type === 2 && state !== 2 && onDislikeComment ) {
59+ setCommentOpen ( true ) ;
7460 return ;
7561 }
76- onDislikeComment ?.( value ) ;
62+ const next : ThumbsState = state === type ? 0 : type ;
63+ setState ( next ) ;
64+ onLike ( next ) ;
65+ } ;
66+
67+ const handleSubmitComment = ( comment : string ) => {
68+ setState ( 2 ) ;
69+ onLike ( 2 ) ;
70+ if ( comment ) onDislikeComment ?.( comment ) ;
7771 setCommentOpen ( false ) ;
78- setCommentError ( false ) ;
7972 } ;
8073
8174 return (
@@ -110,28 +103,13 @@ export function MessageFeedbackButtons({
110103 </ div >
111104
112105 { onDislikeComment && (
113- < Dialog open = { commentOpen } onOpenChange = { setCommentOpen } >
114- < DialogContent className = "sm:max-w-[425px]" >
115- < DialogHeader >
116- < DialogTitle > { localize ( "com_feedback_title" ) } </ DialogTitle >
117- </ DialogHeader >
118- < div >
119- < Textarea
120- ref = { commentRef }
121- maxLength = { 9999 }
122- className = { cn ( "textarea" , commentError && "border border-red-400" ) }
123- />
124- < div className = "flex justify-end gap-4 mt-4" >
125- < Button className = "px-11" variant = "outline" onClick = { ( ) => setCommentOpen ( false ) } >
126- { localize ( "com_ui_cancel" ) }
127- </ Button >
128- < Button className = "px-11" onClick = { handleSubmitComment } >
129- { localize ( "com_ui_submit" ) }
130- </ Button >
131- </ div >
132- </ div >
133- </ DialogContent >
134- </ Dialog >
106+ < CommentDialog
107+ open = { commentOpen }
108+ onOpenChange = { setCommentOpen }
109+ title = { localize ( "com_feedback_title" ) }
110+ placeholder = { localize ( "com_feedback_placeholder" ) }
111+ onSubmit = { handleSubmitComment }
112+ />
135113 ) }
136114 </ >
137115 ) ;
0 commit comments