File size: 4,482 Bytes
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
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" },
];

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>
);

/** Thumbs up or 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 }: { messageId: string; initial?: Feedback | null }) {
  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 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">
        <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>
  );
}