Files
Jyotisha/frontend/src/components/reply-down-reason.tsx
T
jesse-uxandGrok (agent loop) d25dfdbe1a
Independent Staging Quality Gate / validate (push) Failing after 9m13s
Independent Staging Quality Gate / publish (push) Skipped
对话页换成组件库,桌面旁边可以打开本命盘(#106)。
输入框里左边选模型、右边发送。回答下的赞、踩、复制、重新生成和追问改用组件库按钮。不满意原因和兑换窗改用对话框,句子不变。桌面顶栏可以打开当前人物的本命盘,能拖宽,刷新后还在;手机没有这个入口。当前会话和流式状态从 Home() 移进已有会话 store,useState 从 33 减到 25。流式、停止、排队、撤回、退点和寒暄免费没有改。

自测:tsc 退出码 0;lint 0 error、122 条警告;定向测试 275 通过、失败 0、取消 0;npm run build 退出码 0,/ 仍是 Static。首屏 gzip-9 639127,相对基线 634528 多 0.72%。隐私标记测试退出码 0。

Co-Authored-By: Grok (agent loop)
2026-10-25 03:15:13 +08:00

85 lines
3.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
import { Toggle } from "@/components/ui/toggle";
import { REPLY_QUALITY_NOTE_MAX, REPLY_QUALITY_REASON_LABELS, REPLY_QUALITY_REASONS } from "@/lib/reply-quality-labels";
import { notifyError, notifySuccess } from "@/lib/notify";
import { saveReplyQualityReason } from "@/lib/reply-ratings";
const REASONS = REPLY_QUALITY_REASONS.map((key) => [key, REPLY_QUALITY_REASON_LABELS[key]] as const);
/**
* 不满意原因 after a 👎 (compliance round 2026-09-30). Everything here is
* optional: the 👎 and the turn's snapshot are already saved when this opens;
* closing it keeps them. One line says the turn is sent for review. The thanks
* (or a failure) is a toast; the panel closes itself on success (2026-09-30).
*/
export function ReplyDownReason({ feedbackKey, onClose }: Readonly<{ feedbackKey: string; onClose: () => void }>) {
const [picked, setPicked] = useState<ReadonlySet<string>>(() => new Set());
const [note, setNote] = useState("");
const [state, setState] = useState<"idle" | "sending" | "failed">("idle");
async function submit() {
if (state === "sending") return;
setState("sending");
const ok = await saveReplyQualityReason(feedbackKey, REASONS.map(([key]) => key).filter((key) => picked.has(key)), note);
if (ok) {
notifySuccess("已收到,谢谢。");
onClose();
return;
}
setState("failed");
notifyError("没能提交,再试一次。");
}
return (
<Dialog open onOpenChange={(next) => { if (!next) onClose(); }}>
<DialogContent className="reply-down-reason" showCloseButton={false} aria-label="不满意原因">
<DialogTitle>哪里不满意?可以不选</DialogTitle>
<div className="reply-down-reason-chips">
{REASONS.map(([key, label]) => (
<Toggle
key={key}
type="button"
variant="outline"
size="sm"
pressed={picked.has(key)}
aria-pressed={picked.has(key)}
onPressedChange={() => setPicked((current) => {
const next = new Set(current);
if (next.has(key)) next.delete(key); else next.add(key);
return next;
})}
>
{label}
</Toggle>
))}
</div>
<input
className="reply-down-reason-note"
type="text"
value={note}
maxLength={REPLY_QUALITY_NOTE_MAX}
placeholder="补充一句(可不填)"
aria-label="补充说明"
onChange={(event) => setNote(event.target.value)}
/>
<div className="reply-down-reason-foot">
<DialogDescription>会把这一轮对话发给我们排查</DialogDescription>
<Button type="button" className="reply-down-reason-send" disabled={state === "sending"} onClick={() => void submit()}>
{state === "failed" ? "再试一次" : "提交"}
</Button>
</div>
</DialogContent>
</Dialog>
);
}