输入框里左边选模型、右边发送。回答下的赞、踩、复制、重新生成和追问改用组件库按钮。不满意原因和兑换窗改用对话框,句子不变。桌面顶栏可以打开当前人物的本命盘,能拖宽,刷新后还在;手机没有这个入口。当前会话和流式状态从 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)
85 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|