Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/junior-dashboard/e2e/review-desk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
Expand Down
24 changes: 19 additions & 5 deletions packages/junior-dashboard/src/client/components/UserInputForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
buttonUserInputField,
isEmptyUserInputAnswer,
normalizeUserInputAnswer,
USER_INPUT_NOTE,
type UserInputAnswer,
type UserInputField,
type UserInputRequest,
Expand Down Expand Up @@ -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 = (
<legend className="mb-2 p-0 text-sm font-semibold text-dashboard-text">
{field.label}
<span className="ml-2 text-xs font-normal text-dashboard-text-muted">
{field.kind === "choice" ? "Pick one" : "Pick any"}
</span>
</legend>
);
const description = field.description ? (
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -259,16 +265,24 @@ export function UserInputForm(props: {
)}
<TextField
className="min-h-16"
description="Optional. Anything else, or an answer that is not listed."
description={USER_INPUT_NOTE.description}
disabled={sending}
label="Other"
label={USER_INPUT_NOTE.label}
onChange={setOther}
value={other}
/>
<div className="flex flex-wrap items-center gap-2">
<Button disabled={sending} type="submit" variant="primary">
Send
</Button>
{fieldCount > 1 ? (
<span
aria-live="polite"
className="text-xs text-dashboard-text-muted"
>
{answeredCount} of {fieldCount} answered
</span>
) : null}
{secondary}
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export function ConversationReviewBar(props: { conversationId: string }) {
label="Review items"
onClose={() => setOpen(false)}
openKey={props.conversationId}
width="narrow"
>
<div className="grid gap-3">
{items.map((item) => (
Expand Down
1 change: 1 addition & 0 deletions packages/junior/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
buttonUserInputField,
isEmptyUserInputAnswer,
normalizeUserInputAnswer,
USER_INPUT_NOTE,
} from "../chat/tools/runtime/user-input-request";
export type {
UserInputAnswer,
Expand Down
9 changes: 5 additions & 4 deletions packages/junior/src/chat/slack/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
buttonUserInputField,
normalizeUserInputAnswer,
USER_INPUT_LIMITS,
USER_INPUT_NOTE,
type UserInputAnswer,
type UserInputRequest,
} from "@/chat/tools/runtime/user-input-request";
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions packages/junior/src/chat/tools/runtime/user-input-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof userInputFieldSchema>;

Expand Down
Loading