Skip to content

With Feedback

Add feedback buttons to messages.

Full Example

tsx
import { Chat, ThumbsUpIcon, ThumbsDownIcon } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";
import { useState } from "react";

export default function ChatWithFeedback() {
  const [feedbackMap, setFeedbackMap] = useState<Record<string, "up" | "down">>({});

  const handleFeedback = async (messageId: string, type: "up" | "down") => {
    // Toggle feedback
    const current = feedbackMap[messageId];
    if (current === type) {
      // Remove feedback
      setFeedbackMap(prev => {
        const next = { ...prev };
        delete next[messageId];
        return next;
      });
      await removeFeedback(messageId);
    } else {
      // Set feedback
      setFeedbackMap(prev => ({ ...prev, [messageId]: type }));
      await submitFeedback(messageId, type);
    }
  };

  return (
    <div className="h-screen bg-gray-900">
      <Chat
        baseUrl="/api/agents"
        agentId="assistant"
        renderMessageActions={({ message, isStreaming }) => {
          if (isStreaming) return null;
          if (message.role !== "assistant") return null;

          const feedback = feedbackMap[message.id];

          return (
            <div className="flex gap-2 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
              <button
                onClick={() => handleFeedback(message.id, "up")}
                className={`p-1 rounded hover:bg-gray-700 ${
                  feedback === "up" ? "text-green-500" : "text-gray-400"
                }`}
                title="Good response"
              >
                <ThumbsUpIcon className="w-4 h-4" />
              </button>
              <button
                onClick={() => handleFeedback(message.id, "down")}
                className={`p-1 rounded hover:bg-gray-700 ${
                  feedback === "down" ? "text-red-500" : "text-gray-400"
                }`}
                title="Bad response"
              >
                <ThumbsDownIcon className="w-4 h-4" />
              </button>
            </div>
          );
        }}
      />
    </div>
  );
}

async function submitFeedback(messageId: string, type: "up" | "down") {
  await fetch("/api/feedback", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messageId, type }),
  });
}

async function removeFeedback(messageId: string) {
  await fetch(`/api/feedback/${messageId}`, { method: "DELETE" });
}

With Detailed Feedback Modal

tsx
import { useState } from "react";
import { Chat, ThumbsUpIcon, ThumbsDownIcon } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";

function FeedbackModal({ 
  type, 
  onSubmit, 
  onClose 
}: { 
  type: "up" | "down";
  onSubmit: (comment: string) => void;
  onClose: () => void;
}) {
  const [comment, setComment] = useState("");

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-gray-800 p-6 rounded-lg w-96">
        <h3 className="text-lg font-bold text-white mb-4">
          {type === "up" ? "What did you like?" : "What was wrong?"}
        </h3>
        
        <textarea
          value={comment}
          onChange={(e) => setComment(e.target.value)}
          className="w-full p-2 rounded bg-gray-700 text-white mb-4"
          placeholder="Optional feedback..."
          rows={3}
        />
        
        <div className="flex justify-end gap-2">
          <button
            onClick={onClose}
            className="px-4 py-2 rounded bg-gray-600 text-white"
          >
            Cancel
          </button>
          <button
            onClick={() => onSubmit(comment)}
            className="px-4 py-2 rounded bg-green-600 text-white"
          >
            Submit
          </button>
        </div>
      </div>
    </div>
  );
}

export default function ChatWithDetailedFeedback() {
  const [modal, setModal] = useState<{
    messageId: string;
    type: "up" | "down";
  } | null>(null);

  const handleSubmit = async (comment: string) => {
    if (modal) {
      await submitFeedback(modal.messageId, modal.type, comment);
      setModal(null);
    }
  };

  return (
    <div className="h-screen bg-gray-900">
      <Chat
        baseUrl="/api/agents"
        agentId="assistant"
        renderMessageActions={({ message, isStreaming }) => {
          if (isStreaming || message.role !== "assistant") return null;

          return (
            <div className="flex gap-2 mt-2">
              <button
                onClick={() => setModal({ messageId: message.id, type: "up" })}
                className="p-1 text-gray-400 hover:text-green-500"
              >
                <ThumbsUpIcon className="w-4 h-4" />
              </button>
              <button
                onClick={() => setModal({ messageId: message.id, type: "down" })}
                className="p-1 text-gray-400 hover:text-red-500"
              >
                <ThumbsDownIcon className="w-4 h-4" />
              </button>
            </div>
          );
        }}
      />
      
      {modal && (
        <FeedbackModal
          type={modal.type}
          onSubmit={handleSubmit}
          onClose={() => setModal(null)}
        />
      )}
    </div>
  );
}

Simple Copy + Feedback

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  renderMessageActions={({ message, isStreaming }) => {
    if (isStreaming || message.role !== "assistant") return null;

    const handleCopy = async () => {
      const text = typeof message.content === "string"
        ? message.content
        : message.content.map(p => p.type === "text" ? p.text : "").join("");
      await navigator.clipboard.writeText(text);
    };

    return (
      <div className="flex gap-1 mt-2">
        <button onClick={handleCopy} title="Copy">📋</button>
        <button onClick={() => sendFeedback(message.id, "up")} title="Good">👍</button>
        <button onClick={() => sendFeedback(message.id, "down")} title="Bad">👎</button>
      </div>
    );
  }}
/>

Released under the MIT License.