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 = (
);
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;