Spaces:
Runtime error
Runtime error
File size: 5,663 Bytes
b74c6ae 66de9c8 b74c6ae 0649f1b b74c6ae 0649f1b b74c6ae 0649f1b b74c6ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import { useState } from "react";
import { notebooksApi } from "../api/endpoints";
import type { AnswerFeedback as Feedback } from "../api/types";
const REASONS: { id: string; label: string }[] = [
{ id: "forgot_chat", label: "Forgot something we said" },
{ id: "mixed_chat", label: "Mixed up an earlier chat" },
{ id: "wrong", label: "Wrong or made up" },
{ id: "missed", label: "Missed something in my posts" },
{ id: "bad_sources", label: "Bad sources" },
{ id: "other", label: "Something else" },
];
export const Thumb = ({ down }: { down?: boolean }) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={down ? { transform: "rotate(180deg)" } : undefined}>
<path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3Zm0 0 4-8a2 2 0 0 1 2 2v4h5.2a2 2 0 0 1 2 2.3l-1 6A2 2 0 0 1 17.2 20H7" />
</svg>
);
const CopyGlyph = ({ done }: { done?: boolean }) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
{done ? <path d="M5 12.5l4.5 4.5L19 7.5" /> : (
<>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V6a2 2 0 0 1 2-2h9" />
</>
)}
</svg>
);
/** The answer as plain text for pasting: the [1] citation markers are left out. */
const forCopy = (text: string) => text.replace(/\s*\[\d+\](?:\[\d+\])*/g, "").trim();
/** Copy, thumbs up and thumbs down under an answer. A thumbs down opens a few reasons and an optional note. Saved as soon as it is chosen. */
export function AnswerFeedback({ messageId, initial, text }: { messageId: string; initial?: Feedback | null; text?: string }) {
const [fb, setFb] = useState<Feedback | null>(initial ?? null);
const [open, setOpen] = useState(false);
const [reasons, setReasons] = useState<string[]>(initial?.reasons ?? []);
const [comment, setComment] = useState(initial?.comment ?? "");
const [error, setError] = useState(false);
const [thanks, setThanks] = useState(false);
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(forCopy(text ?? ""));
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
setError(true); // the browser refused the clipboard
}
};
const save = async (rating: "up" | "down" | null, r: string[] = [], c = "") => {
const before = fb;
setError(false);
setFb(rating ? { rating, reasons: r, comment: c || null } : null); // shows at once; undone if the save fails
try {
const res = await notebooksApi.rate(messageId, { rating, reasons: r, comment: c || undefined });
setFb(res.feedback);
} catch {
setFb(before);
setError(true);
}
};
const choose = (rating: "up" | "down") => {
setThanks(false);
if (fb?.rating === rating) {
setOpen(false);
void save(null);
return;
}
setOpen(rating === "down");
if (rating === "up") {
setReasons([]);
setComment("");
}
void save(rating, rating === "down" ? reasons : []);
};
return (
<div className="fb">
<div className="fb-row">
{text && (
<button type="button" className="fb-btn" aria-label={copied ? "Copied" : "Copy answer"} title={copied ? "Copied" : "Copy answer"} onClick={copy}>
<CopyGlyph done={copied} />
</button>
)}
<button type="button" className={`fb-btn${fb?.rating === "up" ? " is-on" : ""}`} aria-pressed={fb?.rating === "up"} aria-label="Good answer" title="Good answer" onClick={() => choose("up")}>
<Thumb />
</button>
<button type="button" className={`fb-btn${fb?.rating === "down" ? " is-on" : ""}`} aria-pressed={fb?.rating === "down"} aria-label="Bad answer" title="Bad answer" onClick={() => choose("down")}>
<Thumb down />
</button>
{thanks && <span className="mono muted small">Thanks, saved.</span>}
{error && <span className="error-text small">Could not save that. Try again.</span>}
</div>
{open && fb?.rating === "down" && (
<div className="fb-why">
<p className="mono muted small">What went wrong? (optional)</p>
<div className="fb-chips">
{REASONS.map((r) => (
<button
key={r.id}
type="button"
className={`fb-chip${reasons.includes(r.id) ? " is-on" : ""}`}
aria-pressed={reasons.includes(r.id)}
onClick={() => setReasons((list) => (list.includes(r.id) ? list.filter((x) => x !== r.id) : [...list, r.id]))}
>
{r.label}
</button>
))}
</div>
<textarea className="input fb-note" rows={2} maxLength={1000} value={comment} onChange={(e) => setComment(e.target.value)} placeholder="Tell us more, like what it should have said" aria-label="What went wrong" />
<div className="row end">
<button type="button" className="btn btn-small" onClick={() => setOpen(false)}>
Skip
</button>
<button
type="button"
className="btn btn-small btn-primary"
onClick={async () => {
await save("down", reasons, comment.trim());
setOpen(false);
setThanks(true);
}}
>
Send
</button>
</div>
</div>
)}
</div>
);
}
|