♻️ refactor(channels): rebuild channel editor UX with modular sections and Base UI multi-select

Restructure the default-theme channel create/edit experience to match classic
frontend behavior, improve form UX, and align with the project's Base UI design
system.

Channel editor architecture:
- Split the monolithic channel mutate drawer into focused section components
  (basic, API access, auth, models, advanced) with shared drawer layout
  primitives
- Extract submission, toast handling, and react-query cache invalidation into
  `useChannelMutateForm`
- Add a dedicated loading skeleton for channel detail fetch during edit mode
- Remove the top-level configuration summary block

Form validation and data handling:
- Strengthen `channel-form` Zod schema with JSON, model mapping, status code
  mapping, Codex credential, and Vertex AI key refinements
- Move type-specific conditional validation into `superRefine`
- Normalize base URL formatting and tighten model mapping value validation

Model mapping editor:
- Add Visual/JSON tabbed editing with inline JSON and duplicate-key feedback
- Improve accessibility for icon-only actions and add model suggestion datalists

MultiSelect component:
- Replace the custom cmdk-based implementation with Base UI Combobox chips
- Align focus, border, ring, disabled, and invalid states with standard Input
  styling via `ComboboxChips`
- Preserve existing API for all current callers (`options`, `selected`,
  `onChange`, `allowCreate`, `createLabel`)
- Support inline custom value creation and comma/newline batch input
- Limit visible chips with a compact "+N more" overflow summary via
  `maxVisibleChips` (8 in the channel editor)
- Anchor the dropdown to the full chips container via `useComboboxAnchor` so
  the popup matches input width and long model names are no longer truncated

Models & groups UX:
- Integrate manual custom model entry directly into the model MultiSelect
- Remove the separate manual model input/button block
- Keep selected-model count badge and existing model-mapping guardrail behavior

i18n:
- Add and sync translation keys for section descriptions, validation messages,
  model mapping UI, and MultiSelect labels across en, zh, fr, ja, ru, and vi
- Fix missing translations for "Name, provider type, and availability.",
  "Endpoint, provider-specific settings, and credentials.", and "Published
  models, groups, and model remapping rules."
- Remove obsolete keys tied to the deprecated summary and manual model entry UI
This commit is contained in:
t0ng7u
2026-05-26 01:55:27 +08:00
parent b37b6d80b3
commit 3360882642
9 changed files with 102 additions and 56 deletions
+41 -11
View File
@@ -32,6 +32,7 @@ import {
ComboboxItem,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from '@/components/ui/combobox'
export type Option = {
@@ -58,6 +59,11 @@ interface MultiSelectProps {
id?: string
/** Disable the entire control. */
disabled?: boolean
/**
* Limits rendered chips while keeping all values selected.
* Hidden values remain searchable/removable from the dropdown.
*/
maxVisibleChips?: number
}
const COMMA_REGEX = /[,\n]/
@@ -87,6 +93,8 @@ function splitDraft(value: string): { completed: string[]; draft: string } {
* - A "Add \"<value>\"" item appears at the top of the dropdown when the
* typed text doesn't match any option.
* - Backspace on an empty input removes the last selected chip (Base UI default).
* - `maxVisibleChips` can cap large selections and show a compact "+N more"
* summary so forms do not grow vertically without bound.
*
* Focus/border styling is inherited from `ComboboxChips`, which uses the same
* tokens as `Input` so it stays visually consistent with other form fields.
@@ -95,6 +103,10 @@ export function MultiSelect(props: MultiSelectProps) {
const { t } = useTranslation()
const placeholder = props.placeholder ?? t('Select items...')
// Anchor the popup to the chips container so its width tracks the entire
// input row, not just the leftover space at the end of wrapped chips.
const chipsAnchorRef = useComboboxAnchor()
const [inputValue, setInputValue] = React.useState('')
const [open, setOpen] = React.useState(false)
@@ -213,17 +225,35 @@ export function MultiSelect(props: MultiSelectProps) {
onOpenChange={setOpen}
disabled={props.disabled}
>
<ComboboxChips className={cn('w-full', props.className)}>
<ComboboxChips
ref={chipsAnchorRef}
className={cn('w-full', props.className)}
>
<ComboboxValue>
{(values: string[]) =>
values.map((value) => (
<ComboboxChip key={value}>
<span className='max-w-[16rem] truncate'>
{labelMap.get(value) ?? value}
</span>
</ComboboxChip>
))
}
{(values: string[]) => {
const visibleValues =
typeof props.maxVisibleChips === 'number'
? values.slice(0, props.maxVisibleChips)
: values
const hiddenCount = values.length - visibleValues.length
return (
<>
{visibleValues.map((value) => (
<ComboboxChip key={value}>
<span className='max-w-[16rem] truncate'>
{labelMap.get(value) ?? value}
</span>
</ComboboxChip>
))}
{hiddenCount > 0 && (
<span className='bg-muted text-muted-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center rounded-sm px-1.5 text-xs font-medium whitespace-nowrap'>
{t('+{{count}} more', { count: hiddenCount })}
</span>
)}
</>
)
}}
</ComboboxValue>
<ComboboxChipsInput
id={props.id}
@@ -233,7 +263,7 @@ export function MultiSelect(props: MultiSelectProps) {
/>
</ComboboxChips>
<ComboboxContent>
<ComboboxContent anchor={chipsAnchorRef}>
<ComboboxList>
<ComboboxCollection>
{(item: string) => {