Skip to content

Commit a8055cc

Browse files
committed
Merge branch 'feat/2.6.0-beta4' into feat/2.6.0-beta4-cofco
2 parents 3431b85 + 71d7a3c commit a8055cc

16 files changed

Lines changed: 216 additions & 213 deletions

File tree

src/frontend/client/src/components/Chat/MessageFeedbackButtons.tsx

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
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";
1519
import { Outlined } from "bisheng-icons";
16-
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Textarea } from "~/components";
17-
import { useToastContext } from "~/Providers";
20+
import { CommentDialog } from "~/components";
1821
import { useLocalize } from "~/hooks";
1922
import { cn } from "~/utils";
2023

@@ -27,9 +30,9 @@ const ACTION_BTN =
2730
interface 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
);

src/frontend/client/src/components/Nav/Nav.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ const Nav = ({
174174
<nav
175175
id="chat-history-nav"
176176
aria-label={localize('com_ui_chat_history')}
177-
className="flex h-full min-h-0 w-full flex-col gap-0 pt-5 px-3 max-[767px]:gap-0 max-[767px]:p-0"
177+
className="flex h-full min-h-0 w-full flex-col gap-0 pt-4 px-3 max-[767px]:gap-0 max-[767px]:p-0"
178178
>
179179
{/* New chat header and buttons */}
180180
<NewChat
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Shared "title + free-text textarea + cancel/submit" dialog.
3+
*
4+
* Extracted from MessageFeedbackButtons' dislike-reason dialog so other
5+
* surfaces (e.g. menu-permission apply on MenuUnavailablePage) reuse the
6+
* exact same shell. Anatomy: the container carries no padding; header /
7+
* body / footer each own px-5 (20px). Mobile: width calc(100%-48px),
8+
* centered title, full-width button pair.
9+
*
10+
* The textarea is uncontrolled and reset on every open, so every close
11+
* path (cancel / ESC / overlay click) discards the draft. Submitting does
12+
* NOT auto-close — the caller owns `open` and closes when its side effect
13+
* settles (sync callers close immediately; async ones close on success).
14+
*/
15+
import { useEffect, useRef } from 'react';
16+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './Dialog';
17+
import { Textarea } from './Textarea';
18+
import { Button } from './Button';
19+
import { useLocalize } from '~/hooks';
20+
21+
interface CommentDialogProps {
22+
open: boolean;
23+
onOpenChange: (open: boolean) => void;
24+
title: string;
25+
placeholder?: string;
26+
/** Disables the submit button and swaps its label while an async submit runs. */
27+
submitting?: boolean;
28+
/** Submit-button label while `submitting` (defaults to the normal submit label). */
29+
submittingText?: string;
30+
/** Receives the trimmed textarea content ('' when left blank). */
31+
onSubmit: (comment: string) => void;
32+
}
33+
34+
export function CommentDialog({
35+
open,
36+
onOpenChange,
37+
title,
38+
placeholder,
39+
submitting = false,
40+
submittingText,
41+
onSubmit,
42+
}: CommentDialogProps) {
43+
const localize = useLocalize();
44+
const commentRef = useRef<HTMLTextAreaElement | null>(null);
45+
46+
// Reset the draft on every open so all close paths discard it alike.
47+
useEffect(() => {
48+
if (open && commentRef.current) {
49+
commentRef.current.value = '';
50+
}
51+
}, [open]);
52+
53+
return (
54+
<Dialog open={open} onOpenChange={onOpenChange}>
55+
{/* Don't auto-focus the textarea on open — no focus ring until the user clicks in.
56+
Anatomy: container carries no padding; header / body / footer each own px-5 (20px). */}
57+
<DialogContent
58+
className="w-[calc(100%-48px)] sm:w-full sm:max-w-[425px] rounded-xl sm:rounded-xl p-0 gap-0"
59+
onOpenAutoFocus={(e) => e.preventDefault()}
60+
close={false}
61+
>
62+
<DialogHeader className="px-5 py-4 text-center sm:text-left">
63+
<DialogTitle className="text-base leading-6 text-[#212121]">{title}</DialogTitle>
64+
</DialogHeader>
65+
<div className="px-5">
66+
{/* Focus chrome mirrors the app-center search box (ExpandableSearchField). */}
67+
<Textarea
68+
ref={commentRef}
69+
maxLength={9999}
70+
placeholder={placeholder}
71+
className="bg-white border-[#E5E6EB] shadow-none transition-[border-color,box-shadow] duration-200 focus:border-[#DDDDDD] focus:shadow-[0_0_0_2px_#F1F5F9] placeholder:text-sm placeholder:text-[#999]"
72+
/>
73+
</div>
74+
<DialogFooter className="flex-row justify-end gap-3 space-x-0 sm:space-x-0 px-5 py-4">
75+
<Button
76+
className="h-8 px-4 rounded-md text-sm font-normal flex-1 sm:flex-none"
77+
variant="outline"
78+
onClick={() => onOpenChange(false)}
79+
>
80+
{localize('com_ui_cancel')}
81+
</Button>
82+
<Button
83+
className="h-8 px-4 rounded-md text-sm font-normal flex-1 sm:flex-none"
84+
disabled={submitting}
85+
onClick={() => onSubmit(commentRef.current?.value?.trim() ?? '')}
86+
>
87+
{submitting ? (submittingText ?? localize('com_ui_submit')) : localize('com_ui_submit')}
88+
</Button>
89+
</DialogFooter>
90+
</DialogContent>
91+
</Dialog>
92+
);
93+
}

src/frontend/client/src/components/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './AlertDialog';
22
export * from './Breadcrumb';
33
export * from './Button';
44
export * from './Checkbox';
5+
export * from './CommentDialog';
56
export * from './DataTableColumnHeader';
67
export * from './Dialog';
78
export * from './ExpandableSearchField';

src/frontend/client/src/locales/en/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@
331331
"com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.",
332332
"com_error_no_base_url": "No base URL found. Please provide one and try again.",
333333
"com_error_no_user_key": "No key found. Please provide a key and try again.",
334-
"com_feedback_required": "Feedback cannot be empty",
334+
"com_feedback_placeholder": "Share your suggestions — we'll keep improving (optional)",
335335
"com_feedback_title": "Feedback",
336336
"com_file_content_exceed_tokens": "File content exceeds 30k tokens",
337337
"com_file_current_empty": "Current file is empty",

src/frontend/client/src/locales/ja/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@
317317
"com_error_moderation": "申し訳ありません。送信内容がコミュニティガイドラインに抵触したため、このトピックについては続行できません。他の質問や話題があれば、メッセージを編集するか新しい会話を開始してください。",
318318
"com_error_no_base_url": "ベース URL が見つかりません。指定してから再試行してください。",
319319
"com_error_no_user_key": "キーが見つかりません。キーを提供して再試行してください。",
320-
"com_feedback_required": "フィードバックは空にできません",
320+
"com_feedback_placeholder": "ご意見をお聞かせください。今後の改善に活かします(任意)",
321321
"com_feedback_title": "フィードバック",
322322
"com_file_content_exceed_tokens": "ファイル内容が 3 万トークンを超えています",
323323
"com_file_current_empty": "現在のファイルは空です",

src/frontend/client/src/locales/zh-Hans/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@
320320
"com_error_moderation": "很抱歉,您提交的内容被我们的审核系统标记为不符合社区指引。我们无法就此特定主题继续交流。如果您有任何其他问题或想探讨的话题,请编辑您的消息或开启新的对话。",
321321
"com_error_no_base_url": "未找到基础 URL,请提供一个后重试。",
322322
"com_error_no_user_key": "没有找到密钥。请提供密钥后重试。",
323-
"com_feedback_required": "反馈信息不能为空",
323+
"com_feedback_placeholder": "欢迎留下你的建议,我们会持续改进(选填)",
324324
"com_feedback_title": "反馈",
325325
"com_file_content_exceed_tokens": "文件内容超出3万token",
326326
"com_file_current_empty": "当前文件为空",

src/frontend/client/src/pages/MenuUnavailablePage.tsx

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NotificationSeverity } from '~/common';
66
import { useAuthContext, useLocalize } from '~/hooks';
77
import { WorkbenchEmptyIllustration } from '~/components/workbench/WorkbenchEmptyIllustration';
88
import { Button } from '~/components/ui/Button';
9+
import { CommentDialog } from '~/components/ui/CommentDialog';
910

1011
const MENU_LABEL_KEYS: Record<string, string> = {
1112
home: 'com_nav_home',
@@ -49,7 +50,6 @@ export default function MenuUnavailablePage() {
4950
const menuName = pluginId ? localize((MENU_LABEL_KEYS[pluginId] || pluginId) as any) : '';
5051

5152
const [showDialog, setShowDialog] = useState(false);
52-
const [reason, setReason] = useState('');
5353
const [submitting, setSubmitting] = useState(false);
5454
const [applied, setApplied] = useState(false);
5555

@@ -67,14 +67,14 @@ export default function MenuUnavailablePage() {
6767
return () => { cancelled = true; };
6868
}, [canApply, pluginId]);
6969

70-
const handleSubmit = async () => {
70+
const handleSubmit = async (reason: string) => {
7171
if (!canApply || !pluginId || submitting) return;
7272
setSubmitting(true);
7373
try {
7474
await applyMenuAccessApi({
7575
menu_key: pluginId,
7676
menu_name: menuName || pluginId,
77-
reason: reason.trim() || undefined,
77+
reason: reason || undefined,
7878
});
7979
setApplied(true);
8080
setShowDialog(false);
@@ -121,43 +121,16 @@ export default function MenuUnavailablePage() {
121121
</Button>
122122
)}
123123

124-
{/* Apply reason dialog */}
125-
{showDialog && (
126-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4">
127-
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
128-
<h3 className="mb-4 text-base font-semibold text-gray-900">
129-
{localize('com_menu_unavailable_apply_button')}
130-
</h3>
131-
<label className="mb-1 block text-sm text-gray-600">
132-
{localize('com_menu_unavailable_reason_label')}
133-
</label>
134-
<textarea
135-
value={reason}
136-
onChange={(e) => setReason(e.target.value)}
137-
rows={3}
138-
placeholder={localize('com_menu_unavailable_reason_placeholder') as string}
139-
className="mb-4 w-full resize-none rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-800 outline-none focus:border-blue-500"
140-
/>
141-
<div className="flex justify-end gap-3">
142-
<button
143-
type="button"
144-
onClick={() => { setShowDialog(false); setReason(''); }}
145-
className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
146-
>
147-
{localize('com_ui_cancel')}
148-
</button>
149-
<button
150-
type="button"
151-
disabled={submitting}
152-
onClick={() => void handleSubmit()}
153-
className="rounded-lg bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-60 btn-brand-primary"
154-
>
155-
{submitting ? localize('com_menu_unavailable_apply_submitting') : localize('com_ui_submit')}
156-
</button>
157-
</div>
158-
</div>
159-
</div>
160-
)}
124+
{/* Apply reason dialog — shared shell with the message-feedback dialog. */}
125+
<CommentDialog
126+
open={showDialog}
127+
onOpenChange={(open) => { if (!submitting) setShowDialog(open); }}
128+
title={localize('com_menu_unavailable_apply_button')}
129+
placeholder={localize('com_menu_unavailable_reason_placeholder') as string}
130+
submitting={submitting}
131+
submittingText={localize('com_menu_unavailable_apply_submitting')}
132+
onSubmit={(reason) => void handleSubmit(reason)}
133+
/>
161134
</div>
162135
);
163136
}

src/frontend/client/src/pages/_gallery/GalleryApp.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { OverviewSection } from './sections/OverviewSection';
1414
import { ModalSection } from './sections/ModalSection';
1515
import { ConfirmDialogSection } from './sections/ConfirmDialogSection';
1616
import { ButtonSection } from './sections/ButtonSection';
17+
import { FeedbackSection } from './sections/FeedbackSection';
1718

1819
interface NavItem {
1920
id: string;
@@ -26,6 +27,7 @@ const NAV: NavItem[] = [
2627
{ id: 'modal', label: 'Modal 弹窗', status: 'wip' },
2728
{ id: 'confirm', label: '二次确认弹窗', status: 'wip' },
2829
{ id: 'button', label: 'Button 按钮', status: 'todo' },
30+
{ id: 'feedback', label: '点赞点踩反馈', status: 'done' },
2931
];
3032

3133
const STATUS_DOT: Record<NonNullable<NavItem['status']>, string> = {
@@ -81,6 +83,7 @@ export default function GalleryApp() {
8183
<ModalSection />
8284
<ConfirmDialogSection />
8385
<ButtonSection />
86+
<FeedbackSection />
8487
</div>
8588
</main>
8689
</div>

0 commit comments

Comments
 (0)