mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
471d74bad4
Closes #2615. ## Problem The v1 thumbs callbacks were typed `(message: Message) => void`, so a consumer received the message but not *what the click did*. A custom `AssistantMessage` that keeps its own toggle state — the case in the issue — had nowhere to put that value: ```tsx onClick={() => { setReactionValue((v) => (v === "like" ? null : "like")); onThumbsUp?.(message); // 👈 no way to say whether this applied or retracted }} ``` The only workaround was counting clicks per message. The built-in path had the matching gap. `Chat.tsx` already tracked `messageFeedback`, but `handleThumbsUp` unconditionally wrote `"thumbsUp"`, so the state was write-only: clicking an active button re-applied the same value and there was no way to un-vote. These callbacks are live API — `RenderMessage` forwards `onThumbsUp`, `onThumbsDown` and `feedback` to whatever component is passed as the `AssistantMessage` prop, which is exactly the customisation the issue describes. ## Fix Add an optional second argument reporting the state the click transitions to: ```ts onThumbsUp?: (message: Message, isActive?: boolean) => void; ``` - The built-in `AssistantMessage` derives it from the message's current `feedback`, so a second click on an active button now reports `false` and **retracts** the feedback rather than re-applying it. - A custom `AssistantMessage` can pass its own value straight through. - The parameter is optional and appended, so existing one-argument handlers keep type-checking and keep working unchanged. `onFeedbackGiven` fires only on the applying click — its signature is `(messageId, "thumbsUp" | "thumbsDown")` and has no way to express a retraction, so reporting one would be a lie. Note this is a small behavioural change to the built-in buttons: previously a repeat click was a no-op, now it clears the vote. That is what makes the reported state meaningful, and it matches the toggle UX in the issue. The toggle logic lives in a new `./feedback` module. This package's vitest project runs `environment: "node"` and only collects `*.test.ts`, so there is no component-rendering harness here — extracting the pure functions is what makes the behaviour testable at all. ## Testing `packages/react-ui` — full suite, including the 10 new cases: ``` ✓ src/components/chat/feedback.test.ts (10 tests) 3ms ✓ src/components/chat/Markdown.test.ts (29 tests) 5ms ✓ src/components/chat/Markdown.xss.test.ts (13 tests) 182ms ✓ src/css/sidebar-full-height.test.ts (4 tests) 2ms Test Files 10 passed (10) Tests 68 passed (68) ``` Coverage: activation from no feedback, deactivation on the already-active button, switching to the opposite button, retraction removing the map entry rather than storing a falsy value, other messages left untouched, no mutation of the previous map, referential stability when nothing changes, and a toggle round-trip. **Mutation checks** — broke each mechanism and confirmed the tests fail, so they are not self-fulfilling: | Mutation | Result | | --- | --- | | `isActivatingClick` → `return true` | `Tests 1 failed \| 9 passed` | | `applyFeedbackClick` retraction branch → `if (false)` | `Tests 3 failed \| 7 passed` | Both were reverted and the suite returned to 68 passing. **Types/build** — `tsc --noEmit` clean, `tsdown` clean, and the widened signature reaches the published types: ``` $ grep -n "onThumbsUp" dist/index.d.mts 102: onThumbsUp?: (message: Message, isActive?: boolean) => void; 185: onThumbsUp?: (message: Message, isActive?: boolean) => void; 272: onThumbsUp?: (message: Message, isActive?: boolean) => void; ```