Skip to Content
PatternsFeedback Vote Widget

Feedback Vote Widget

A thumbs up / thumbs down control with an optional comment textarea. Designed to capture user sentiment on AI-generated content and integrate with the UiPath Platform Feedback API via @uipath/vs-feedback.

Features

  • Two-state vote — Thumbs up / down, with aria-pressed reflecting the current selection
  • Optional comment — Toggle a textarea below the vote buttons for free-form feedback
  • Controlled — Pass value + onVoteChange (and comment + onCommentChange when showComment is true)
  • Accessible labels are requiredupLabel / downLabel (and commentPlaceholder when applicable) must be provided so consumers can plug in their own translated strings
  • Sizesdefault and sm
  • Composes existing primitives — Built on Button and Textarea
  • Test hook — Each vote button carries data-vote="up" / data-vote="down" for E2E selectors

Installation

Apollo Vertex components are published to a custom shadcn registry under the @uipath namespace. Register the namespace once in your project’s components.json:

{ "registries": { "@uipath": "https://apollo-vertex.vercel.app/r/{name}.json" } }

Then install the component:

npx shadcn@latest add @uipath/feedback-vote-widget

Dependencies

This component depends on the following registry components:

Usage

Basic

"use client"; import * as React from "react"; import { FeedbackVoteWidget } from "@/components/ui/feedback-vote-widget"; export function MyFeedback() { const [vote, setVote] = React.useState<"up" | "down" | null>(null); return ( <FeedbackVoteWidget value={vote} onVoteChange={setVote} upLabel="Vote up" downLabel="Vote down" /> ); }

The labels above are inline strings for clarity — in a real app source them from your i18n library (t("feedback.voteUp") etc.) so the widget reflects the user’s locale.

With comment

const [vote, setVote] = React.useState<"up" | "down" | null>(null); const [comment, setComment] = React.useState(""); <FeedbackVoteWidget value={vote} onVoteChange={setVote} upLabel="Vote up" downLabel="Vote down" showComment comment={comment} onCommentChange={setComment} commentPlaceholder="What could the AI have done better?" />

When showComment is true, comment, onCommentChange, and commentPlaceholder become required (enforced via the props’ discriminated union). When omitted or false, the comment-related props can’t be passed at all — the type system prevents inconsistent states.

Wiring to the Platform Feedback API

Use @uipath/vs-feedback’s PlatformFeedbackClient to submit. The vote becomes FeedbackDraft.isPositive; the comment goes into the typed comment field — your mapper stringifies it on the wire.

import type { FeedbackDraft } from "@uipath/vs-feedback"; import { client } from "./feedback-client"; // your PlatformFeedbackClient<MyCustom, MyComment> import { resolveTraceContext } from "./trace-context"; async function handleSubmit(args: { vote: "up" | "down"; comment: string; maestroInstanceId: string; sectionName: string; }) { const { traceId } = await resolveTraceContext(args.maestroInstanceId); const draft: FeedbackDraft<MyCustom, MyComment> = { traceId, spanId: "", // Feedback API auto-resolves the root span (LLMOPS-2310) isPositive: args.vote === "up", customFields: { sectionName: args.sectionName, aiProducedFields: { aiSeverity: null, aiTriaged: false }, }, comment: { text: args.comment }, }; await client.submit(draft); }

API Reference

PropTypeRequiredDefaultDescription
value"up" | "down" | nullyesCurrent vote state. null = no vote yet.
onVoteChange(vote: "up" | "down") => voidyesCalled when a vote button is clicked
upLabelstringyesAccessible label for the up-vote button (translated)
downLabelstringyesAccessible label for the down-vote button (translated)
showCommentbooleannofalseRender the comment textarea below the buttons
commentstringwhen showCommentTextarea value (controlled)
onCommentChange(value: string) => voidwhen showCommentCalled when the textarea changes
commentPlaceholderstringwhen showCommentPlaceholder for the textarea (translated)
disabledbooleannofalseDisables vote buttons and the textarea
size"default" | "sm"no"default"Visual size of the vote buttons

The widget owns its layout and does not accept className or other div props — wrap it in your own container if you need to position it.