Open, close, and feedback
Open and close the prebuilt chat from your UI, and capture assistant message feedback.
Two common controls for the prebuilt chat are opening it from your own UI and capturing thumbs-up / thumbs-down feedback on assistant messages.
Control the open state from your own UI#
Pass open and onOpenChange to <CopilotSidebar> or <CopilotPopup> to own
the open state yourself. This is the controlled pattern: the surface renders
whatever open says, and every request to open or close (the toggle button,
click-outside on the popup) arrives on onOpenChange instead of moving the
surface directly.
import { useState } from "react";
import { CopilotSidebar } from "@copilotkit/react-core/v2";
function Layout() {
const [chatOpen, setChatOpen] = useState(false);
return (
<>
<nav>
<button onClick={() => setChatOpen(true)}>Ask the assistant</button>
</nav>
<CopilotSidebar open={chatOpen} onOpenChange={setChatOpen} />
</>
);
}The button lives outside the sidebar, which is the point: nothing has to be
rendered inside the chat subtree to open or close it. Because open is the
source of truth, you can also keep the chat in sync with a route, a keyboard
shortcut, or any other state you already have.
onOpenChange reports a request, not a change. If you do not feed the new value
back into open, the surface stays where it is. That is what makes it possible
to gate opening the chat, for example behind a sign-in check. You can also pass
onOpenChange on its own, without open: the surface keeps managing itself and
you are notified each time it opens or closes.
Programmatically open or close the chat#
If you would rather not lift the state into your own component, the open state
for <CopilotPopup> and <CopilotSidebar> also lives in the chat
configuration context. Read it with useCopilotChatConfiguration() and call
setModalOpen(true | false) from anywhere inside the provider:
import { useCopilotChatConfiguration } from "@copilotkit/react-core/v2";
function OpenChatButton() {
const config = useCopilotChatConfiguration();
// setModalOpen is only present when a provider in the tree owns modal state
// (the prebuilt CopilotPopup / CopilotSidebar create it for you).
if (!config?.setModalOpen) return null;
return (
<button onClick={() => config.setModalOpen(true)}>
Ask the assistant
</button>
);
}To toggle, read config.isModalOpen and flip it:
<button onClick={() => config.setModalOpen(!config.isModalOpen)}>
{config.isModalOpen ? "Close chat" : "Open chat"}
</button>setModalOpen / isModalOpen are only defined when a provider in the tree owns
modal state. The prebuilt <CopilotPopup> and <CopilotSidebar> create it
automatically. If you compose chat yourself, wrap the relevant subtree in
<CopilotChatConfigurationProvider isModalDefaultOpen={false}> so the modal state
exists. See the
useCopilotChatConfiguration reference.
You can also set the initial open state declaratively, and then let the
surface manage itself. The prebuilt surfaces accept a defaultOpen prop:
<CopilotSidebar defaultOpen={false} />defaultOpen and open are the usual uncontrolled/controlled pair: use
defaultOpen for a starting point, and open when your own state decides. When
both are passed, open wins.
Capture message feedback (thumbs up / down)#
The assistant-message toolbar can show thumbs-up and thumbs-down buttons. In v2
you opt in by passing onThumbsUp / onThumbsDown to the assistant message
slot. The buttons only render when a handler is provided:
import { CopilotChat } from "@copilotkit/react-core/v2";
<CopilotChat
messageView={{
assistantMessage: {
onThumbsUp: (message) => {
analytics.track("feedback", { messageId: message.id, value: "up" });
},
onThumbsDown: (message) => {
analytics.track("feedback", { messageId: message.id, value: "down" });
},
},
}}
/>;Each handler receives the assistant message, so you can record the feedback
against the specific response (message.id). The same messageView slot works on
<CopilotPopup> and <CopilotSidebar> since they wrap <CopilotChat>.
When the slot is rendered through CopilotChatMessageView, a live assistant
message created by a direct AG-UI TEXT_MESSAGE_START can also include that
event's opaque rawEvent value. The join happens when the thumbs callback runs;
canonical messages and future run input stay unchanged. Chunk, snapshot,
persisted, legacy, and direct CopilotChatAssistantMessage paths don't provide
this callback metadata.
The button labels come from the chat labels (assistantMessageToolbarThumbsUpLabel
defaults to "Good response", assistantMessageToolbarThumbsDownLabel to
"Bad response"); override them via
chat labels.
Building a fully custom message component instead of using the slot? The
underlying CopilotChatAssistantMessage
exposes the same onThumbsUp / onThumbsDown props directly, but callback
metadata enrichment is owned by CopilotChatMessageView.
Related#
- CopilotPopup and CopilotSidebar: the prebuilt surfaces that own modal state.
useCopilotChatConfigurationreference: full context shape, modal state, and labels.- Slots: the
messageView/assistantMessageslot system used above. - Programmatic Control: driving agent runs from code without a chat UI.