From c2505c91bbddacbe7c0f78092ae21b0adc30ee0b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:38:07 +0000 Subject: [PATCH] fix(dashboard): Make the user input note clearly add to the answers The free-text input after a form's fields was labeled "Other" with the help text "an answer that is not listed". That reads like it replaces the picked answers. It does not: the note goes with them. Rename it to "Anything else?" and say that it is sent together with the answers. Slack and the dashboard share the same copy through USER_INPUT_NOTE. Each choice field now says "Pick one" or "Pick any", and a form with more than one field shows how many fields have an answer next to Send. The review items side panel uses the default drawer width, so option descriptions wrap less. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .../junior-dashboard/e2e/review-desk.spec.ts | 8 ++++++- .../src/client/components/UserInputForm.tsx | 24 +++++++++++++++---- .../review-desk/ConversationReviewBar.tsx | 1 - packages/junior/src/api/schema.ts | 1 + packages/junior/src/chat/slack/user-input.ts | 9 +++---- .../chat/tools/runtime/user-input-request.ts | 10 ++++++++ 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/junior-dashboard/e2e/review-desk.spec.ts b/packages/junior-dashboard/e2e/review-desk.spec.ts index d52e4bfe58..250125306d 100644 --- a/packages/junior-dashboard/e2e/review-desk.spec.ts +++ b/packages/junior-dashboard/e2e/review-desk.spec.ts @@ -160,10 +160,16 @@ test("each review item gets a fresh answer form", async ({ detail.getByRole("heading", { name: license.title }), ).toBeVisible(); await detail.getByRole("radio", { name: "Reject" }).click(); + await expect(detail.getByText("1 of 2 answered")).toBeVisible(); + // The note goes with the picked answer. It must not replace it. + await detail.getByLabel("Anything else?").fill("Ask legal first."); await detail.getByRole("button", { name: "Send" }).click(); await expect.poll(() => actions.length).toBe(2); expect(actions[1]).toMatchObject({ - body: { action: "submit", answer: { values: { decision: ["Reject"] } } }, + body: { + action: "submit", + answer: { other: "Ask legal first.", values: { decision: ["Reject"] } }, + }, id: license.id, }); }); diff --git a/packages/junior-dashboard/src/client/components/UserInputForm.tsx b/packages/junior-dashboard/src/client/components/UserInputForm.tsx index 18b9cfc380..0c60998dae 100644 --- a/packages/junior-dashboard/src/client/components/UserInputForm.tsx +++ b/packages/junior-dashboard/src/client/components/UserInputForm.tsx @@ -3,6 +3,7 @@ import { buttonUserInputField, isEmptyUserInputAnswer, normalizeUserInputAnswer, + USER_INPUT_NOTE, type UserInputAnswer, type UserInputField, type UserInputRequest, @@ -41,9 +42,13 @@ function ChoiceField(props: { }) { const id = useId(); const { field } = props; + // Say how many choices count, so a reader knows when a field is done. const legend = ( {field.label} + + {field.kind === "choice" ? "Pick one" : "Pick any"} + ); const description = field.description ? ( @@ -179,11 +184,12 @@ export function UserInputForm(props: { requestId: props.requestId, values, }; + const normalized = normalizeUserInputAnswer(props.request, answer); + const answeredCount = Object.keys(normalized.values).length; + const fieldCount = props.request.fields.length; function submit(event: FormEvent) { event.preventDefault(); - if ( - !isEmptyUserInputAnswer(normalizeUserInputAnswer(props.request, answer)) - ) { + if (!isEmptyUserInputAnswer(normalized)) { void send(answer); } } @@ -259,9 +265,9 @@ export function UserInputForm(props: { )} @@ -269,6 +275,14 @@ export function UserInputForm(props: { + {fieldCount > 1 ? ( + + {answeredCount} of {fieldCount} answered + + ) : null} {secondary} diff --git a/packages/junior-dashboard/src/client/review-desk/ConversationReviewBar.tsx b/packages/junior-dashboard/src/client/review-desk/ConversationReviewBar.tsx index 3a72a5007d..612d78b2e1 100644 --- a/packages/junior-dashboard/src/client/review-desk/ConversationReviewBar.tsx +++ b/packages/junior-dashboard/src/client/review-desk/ConversationReviewBar.tsx @@ -45,7 +45,6 @@ export function ConversationReviewBar(props: { conversationId: string }) { label="Review items" onClose={() => setOpen(false)} openKey={props.conversationId} - width="narrow" >
{items.map((item) => ( diff --git a/packages/junior/src/api/schema.ts b/packages/junior/src/api/schema.ts index 8512db7421..2568e4bb19 100644 --- a/packages/junior/src/api/schema.ts +++ b/packages/junior/src/api/schema.ts @@ -9,6 +9,7 @@ export { buttonUserInputField, isEmptyUserInputAnswer, normalizeUserInputAnswer, + USER_INPUT_NOTE, } from "../chat/tools/runtime/user-input-request"; export type { UserInputAnswer, diff --git a/packages/junior/src/chat/slack/user-input.ts b/packages/junior/src/chat/slack/user-input.ts index 551aec4e6b..047b2662a5 100644 --- a/packages/junior/src/chat/slack/user-input.ts +++ b/packages/junior/src/chat/slack/user-input.ts @@ -7,6 +7,7 @@ import { buttonUserInputField, normalizeUserInputAnswer, USER_INPUT_LIMITS, + USER_INPUT_NOTE, type UserInputAnswer, type UserInputRequest, } from "@/chat/tools/runtime/user-input-request"; @@ -67,8 +68,8 @@ function toOption(option: { /** * Render a request as a Slack modal form: one input for each field and one - * optional Other text input. Every input is optional, so a person can answer - * with free text only. `callbackId` and `privateMetadata` tell the submit + * optional note input that goes with the answers. Every input is optional, + * so a person can answer with free text only. `callbackId` and `privateMetadata` tell the submit * handler what the form answers. */ export function buildSlackUserInputForm( @@ -113,9 +114,9 @@ export function buildSlackUserInputForm( { type: "input", block_id: OTHER_BLOCK_ID, - label: plainText("Other"), + label: plainText(USER_INPUT_NOTE.label), optional: true, - hint: plainText("Anything else, or an answer that is not listed."), + hint: plainText(USER_INPUT_NOTE.description), element: { type: "plain_text_input", action_id: VALUE_ACTION_ID, diff --git a/packages/junior/src/chat/tools/runtime/user-input-request.ts b/packages/junior/src/chat/tools/runtime/user-input-request.ts index 7afadbf540..41141ee0b4 100644 --- a/packages/junior/src/chat/tools/runtime/user-input-request.ts +++ b/packages/junior/src/chat/tools/runtime/user-input-request.ts @@ -111,6 +111,16 @@ export const userInputRequestSchema = z } }); +/** + * Label and help text of the free-text input that every form adds after the + * fields. The text goes with the picked answers and does not replace them, so + * every surface says that in the same words. + */ +export const USER_INPUT_NOTE = { + label: "Anything else?", + description: "Optional. This note is sent together with your answers above.", +} as const; + /** One field of a user input request. */ export type UserInputField = z.output;