diff --git a/apps/beeai-sdk-py/examples/request_form_agent.py b/apps/beeai-sdk-py/examples/request_form_agent.py index 86c473217..9118669fc 100644 --- a/apps/beeai-sdk-py/examples/request_form_agent.py +++ b/apps/beeai-sdk-py/examples/request_form_agent.py @@ -13,6 +13,7 @@ FormRender, MultiSelectField, OptionItem, + SingleSelectField, TextField, ) from beeai_sdk.server import Server @@ -45,6 +46,15 @@ async def request_form_agent( TextField(id="text_field", label="Text Field", col_span=1), DateField(id="date_field", label="Date Field", col_span=1), FileField(id="file_field", label="File Field", accept=["*/*"], col_span=2), + SingleSelectField( + id="singleselect_field", + label="Single-Select Field", + options=[ + OptionItem(id="option1", label="Option 1"), + OptionItem(id="option2", label="Option 2"), + ], + col_span=2, + ), MultiSelectField( id="multiselect_field", label="Multi-Select Field", diff --git a/apps/beeai-sdk-py/src/beeai_sdk/a2a/extensions/ui/form.py b/apps/beeai-sdk-py/src/beeai_sdk/a2a/extensions/ui/form.py index 28f826fdf..7fbf1527c 100644 --- a/apps/beeai-sdk-py/src/beeai_sdk/a2a/extensions/ui/form.py +++ b/apps/beeai-sdk-py/src/beeai_sdk/a2a/extensions/ui/form.py @@ -52,6 +52,20 @@ class OptionItem(BaseModel): label: str +class SingleSelectField(BaseField): + type: Literal["singleselect"] = "singleselect" + options: list[OptionItem] + default_value: str | None = None + + @model_validator(mode="after") + def default_value_validator(self): + if self.default_value: + valid_values = {opt.id for opt in self.options} + if self.default_value not in valid_values: + raise ValueError(f"Invalid default_value: {self.default_value}. Must be one of {valid_values}") + return self + + class MultiSelectField(BaseField): type: Literal["multiselect"] = "multiselect" options: list[OptionItem] @@ -73,7 +87,7 @@ class CheckboxField(BaseField): default_value: bool = False -FormField = TextField | DateField | FileField | MultiSelectField | CheckboxField +FormField = TextField | DateField | FileField | SingleSelectField | MultiSelectField | CheckboxField class FormRender(BaseModel): @@ -106,6 +120,11 @@ class FileFieldValue(BaseModel): value: list[FileInfo] | None = None +class SingleSelectFieldValue(BaseModel): + type: Literal["singleselect"] = "singleselect" + value: str | None = None + + class MultiSelectFieldValue(BaseModel): type: Literal["multiselect"] = "multiselect" value: list[str] | None = None @@ -116,7 +135,14 @@ class CheckboxFieldValue(BaseModel): value: bool | None = None -FormFieldValue = TextFieldValue | DateFieldValue | FileFieldValue | MultiSelectFieldValue | CheckboxFieldValue +FormFieldValue = ( + TextFieldValue + | DateFieldValue + | FileFieldValue + | SingleSelectFieldValue + | MultiSelectFieldValue + | CheckboxFieldValue +) class FormResponse(BaseModel): diff --git a/apps/beeai-sdk-ts/src/client/a2a/extensions/ui/form.ts b/apps/beeai-sdk-ts/src/client/a2a/extensions/ui/form.ts index a3a5c2b1b..4e6129cec 100644 --- a/apps/beeai-sdk-ts/src/client/a2a/extensions/ui/form.ts +++ b/apps/beeai-sdk-ts/src/client/a2a/extensions/ui/form.ts @@ -57,6 +57,24 @@ const fileFieldValue = z.object({ .nullish(), }); +const singleSelectField = baseField.extend({ + type: z.literal('singleselect'), + options: z + .array( + z.object({ + id: z.string().nonempty(), + label: z.string().nonempty(), + }), + ) + .nonempty(), + default_value: z.string().nullish(), +}); + +const singleSelectFieldValue = z.object({ + type: singleSelectField.shape.type, + value: z.string().nullish(), +}); + const multiSelectField = baseField.extend({ type: z.literal('multiselect'), options: z @@ -86,7 +104,14 @@ const checkboxFieldValue = z.object({ value: z.boolean().nullish(), }); -const fieldSchema = z.discriminatedUnion('type', [textField, dateField, fileField, multiSelectField, checkboxField]); +const fieldSchema = z.discriminatedUnion('type', [ + textField, + dateField, + fileField, + singleSelectField, + multiSelectField, + checkboxField, +]); const renderSchema = z.object({ id: z.string().nonempty(), @@ -105,6 +130,7 @@ const responseSchema = z.object({ textFieldValue, dateFieldValue, fileFieldValue, + singleSelectFieldValue, multiSelectFieldValue, checkboxFieldValue, ]), @@ -114,6 +140,7 @@ const responseSchema = z.object({ export type TextField = z.infer; export type DateField = z.infer; export type FileField = z.infer; +export type SingleSelectField = z.infer; export type MultiSelectField = z.infer; export type CheckboxField = z.infer; diff --git a/apps/beeai-ui/src/modules/agents/components/import/ImportAgentsModal.tsx b/apps/beeai-ui/src/modules/agents/components/import/ImportAgentsModal.tsx index 02c40118d..5eb9077c9 100644 --- a/apps/beeai-ui/src/modules/agents/components/import/ImportAgentsModal.tsx +++ b/apps/beeai-ui/src/modules/agents/components/import/ImportAgentsModal.tsx @@ -107,6 +107,7 @@ export function ImportAgentsModal({ onRequestClose, ...modalProps }: ModalProps) {actionField.value === 'update_provider' && providersToUpdate && ( + {options.map(({ id, label }) => ( + + ))} + + ); +} diff --git a/apps/beeai-ui/src/modules/form/utils.ts b/apps/beeai-ui/src/modules/form/utils.ts index cf7725f6a..d60889c2b 100644 --- a/apps/beeai-ui/src/modules/form/utils.ts +++ b/apps/beeai-ui/src/modules/form/utils.ts @@ -18,6 +18,7 @@ export function getDefaultValues(fields: FormField[]) { .with( { type: 'text' }, { type: 'date' }, + { type: 'singleselect' }, { type: 'multiselect' }, { type: 'checkbox' }, ({ id, type, default_value }) => [id, { type, value: default_value }], diff --git a/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.module.scss b/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.module.scss index fa8b37a22..5a7381ea8 100644 --- a/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.module.scss +++ b/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.module.scss @@ -21,7 +21,8 @@ color: $text-secondary; } textarea, - :global(.cds--date-picker__input) { + :global(.cds--date-picker__input), + :global(.cds--select-input) { color: $text-primary; background-color: transparent; } diff --git a/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.tsx b/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.tsx index 1b5f86cce..0110ef574 100644 --- a/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.tsx +++ b/apps/beeai-ui/src/modules/messages/components/MessageFormResponse.tsx @@ -4,7 +4,15 @@ */ import { Button } from '@carbon/react'; -import type { CheckboxField, DateField, FileField, FormField, MultiSelectField, TextField } from 'beeai-sdk'; +import type { + CheckboxField, + DateField, + FileField, + FormField, + MultiSelectField, + SingleSelectField, + TextField, +} from 'beeai-sdk'; import { useMemo } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; @@ -79,6 +87,7 @@ function FormValueRenderer({ field }: { field: FieldWithValue }) { {match(field) .with({ type: 'text' }, { type: 'date' }, ({ value }) => value) .with({ type: 'checkbox' }, ({ value }) => (value ? 'yes' : 'no')) + .with({ type: 'singleselect' }, ({ value }) => value) .with({ type: 'multiselect' }, ({ value }) => value?.join(', ')) .with({ type: 'file' }, ({ value }) => value?.map(({ name }) => name).join(', ')) .otherwise(() => null)} @@ -91,5 +100,6 @@ type FieldWithValue = | FieldWithValueMapper | FieldWithValueMapper | FieldWithValueMapper + | FieldWithValueMapper | FieldWithValueMapper | FieldWithValueMapper; diff --git a/apps/beeai-ui/src/styles/components/_date-picker.scss b/apps/beeai-ui/src/styles/components/_date-picker.scss index d410180e7..c49c33ff8 100644 --- a/apps/beeai-ui/src/styles/components/_date-picker.scss +++ b/apps/beeai-ui/src/styles/components/_date-picker.scss @@ -15,7 +15,6 @@ .cds--date-picker { .cds--date-picker__input { - background-color: $background; &:disabled { background-color: $field; &, diff --git a/apps/beeai-ui/src/styles/components/_index.scss b/apps/beeai-ui/src/styles/components/_index.scss index 7d8a04ff5..42c50a7ce 100644 --- a/apps/beeai-ui/src/styles/components/_index.scss +++ b/apps/beeai-ui/src/styles/components/_index.scss @@ -71,6 +71,7 @@ @forward 'radio-button'; @forward '@carbon/styles/scss/components/search'; @forward '@carbon/styles/scss/components/select'; +@forward 'select'; @forward '@carbon/styles/scss/components/skeleton-styles'; @forward 'skeleton-styles'; // @forward '@carbon/styles/scss/components/slider'; diff --git a/apps/beeai-ui/src/styles/components/_input.scss b/apps/beeai-ui/src/styles/components/_input.scss index 0343dd884..7df0bf041 100644 --- a/apps/beeai-ui/src/styles/components/_input.scss +++ b/apps/beeai-ui/src/styles/components/_input.scss @@ -11,20 +11,19 @@ .cds--text-area, .cds--text-input, -.cds--date-picker__input { +.cds--date-picker__input, +.cds--select-input { border: 1px solid $border-subtle-00; border-radius: $border-radius; - background-color: $layer; - &, - .cds--modal & { - background-color: transparent; - } + background-color: $background; &:focus-within { outline: 2px solid $background-inverse; } } -.cds--text-input:disabled { +.cds--text-input:disabled, +.cds--select-input:disabled, +.cds--select-input:hover:disabled { border-block-end-color: $border-subtle-00; } diff --git a/apps/beeai-ui/src/styles/components/_notification.scss b/apps/beeai-ui/src/styles/components/_notification.scss index 699cf4aa2..2c5a11d03 100644 --- a/apps/beeai-ui/src/styles/components/_notification.scss +++ b/apps/beeai-ui/src/styles/components/_notification.scss @@ -42,6 +42,7 @@ inset: 0; border-block: 1px solid $support-error-30; border-inline-end: 1px solid $support-error-30; + pointer-events: none; } } diff --git a/apps/beeai-ui/src/styles/components/_select.scss b/apps/beeai-ui/src/styles/components/_select.scss new file mode 100644 index 000000000..40ed51bed --- /dev/null +++ b/apps/beeai-ui/src/styles/components/_select.scss @@ -0,0 +1,18 @@ +/** + * Copyright 2025 © BeeAI a Series of LF Projects, LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +@use 'styles/common' as *; + +.cds--select-input, +.cds--select--inline .cds--select-input { + &:hover, + &:hover:focus { + background-color: $background; + } +} + +.cds--select--inline .cds--select-input { + border: none; +} diff --git a/docs/build-agents/forms.mdx b/docs/build-agents/forms.mdx index dd8457582..1a75081cf 100644 --- a/docs/build-agents/forms.mdx +++ b/docs/build-agents/forms.mdx @@ -165,6 +165,7 @@ Here's what you need to know to add form capabilities to your agent: - **TextField/DateField**: Returns `str | None` - **FileField**: Returns `list[FileInfo] | None` where each `FileInfo` has `uri`, `name`, and `mime_type` +- **SingleSelectField**: Returns `str | None` (selected option ID) - **MultiSelectField**: Returns `list[str] | None` (list of selected option IDs) - **CheckboxField**: Returns `bool | None` @@ -236,6 +237,27 @@ FileField( ) ``` +### SingleSelectField +Single-select dropdown fields for choosing single option from a list. + +```python +from beeai_sdk.a2a.extensions.ui.form import OptionItem, SingleSelectField + +SingleSelectField( + id="contact_method", + label="Preferred Contact Method", + col_span=2, + required=False, + options=[ + OptionItem(id="email", label="Email"), + OptionItem(id="phone", label="Phone"), + OptionItem(id="sms", label="SMS"), + OptionItem(id="none", label="Do not contact") + ], + default_value="email" +) +``` + ### MultiSelectField Multi-select dropdown fields for choosing multiple options from a list.