From 33608826429a540dd878e938df588939b17ce4c5 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Tue, 26 May 2026 01:55:27 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(channels):=20rebu?= =?UTF-8?q?ild=20channel=20editor=20UX=20with=20modular=20sections=20and?= =?UTF-8?q?=20Base=20UI=20multi-select?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/default/src/components/multi-select.tsx | 52 +++++++++++++++---- .../drawers/channel-mutate-drawer.tsx | 1 + .../home/components/sections/hero.tsx | 51 +++++++++--------- web/default/src/i18n/locales/en.json | 9 ++-- web/default/src/i18n/locales/fr.json | 9 ++-- web/default/src/i18n/locales/ja.json | 9 ++-- web/default/src/i18n/locales/ru.json | 9 ++-- web/default/src/i18n/locales/vi.json | 9 ++-- web/default/src/i18n/locales/zh.json | 9 ++-- 9 files changed, 102 insertions(+), 56 deletions(-) diff --git a/web/default/src/components/multi-select.tsx b/web/default/src/components/multi-select.tsx index d8309680..d1f9f178 100644 --- a/web/default/src/components/multi-select.tsx +++ b/web/default/src/components/multi-select.tsx @@ -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 \"\"" 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} > - + - {(values: string[]) => - values.map((value) => ( - - - {labelMap.get(value) ?? value} - - - )) - } + {(values: string[]) => { + const visibleValues = + typeof props.maxVisibleChips === 'number' + ? values.slice(0, props.maxVisibleChips) + : values + const hiddenCount = values.length - visibleValues.length + + return ( + <> + {visibleValues.map((value) => ( + + + {labelMap.get(value) ?? value} + + + ))} + {hiddenCount > 0 && ( + + {t('+{{count}} more', { count: hiddenCount })} + + )} + + ) + }} - + {(item: string) => { diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 9d48fb9c..92302365 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -2172,6 +2172,7 @@ export function ChannelMutateDrawer({ )} allowCreate createLabel='Add custom model "{{value}}"' + maxVisibleChips={8} /> {modelMappingGuardrail.exposedTargetModels diff --git a/web/default/src/features/home/components/sections/hero.tsx b/web/default/src/features/home/components/sections/hero.tsx index 7f138b7f..58a4ae3f 100644 --- a/web/default/src/features/home/components/sections/hero.tsx +++ b/web/default/src/features/home/components/sections/hero.tsx @@ -17,11 +17,11 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { Link } from '@tanstack/react-router' -import { ArrowRight, BookOpen } from 'lucide-react' import { CherryStudio } from '@lobehub/icons' +import { ArrowRight, BookOpen } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { useStatus } from '@/hooks/use-status' +import { Button } from '@/components/ui/button' import { HeroTerminalDemo } from '../hero-terminal-demo' interface HeroProps { @@ -32,7 +32,7 @@ interface HeroProps { // Stylized three-dots indicator representing "More" const MoreIcon = () => ( ( export function Hero(props: HeroProps) { const { t } = useTranslation() const { status } = useStatus() - const docsUrl = (status?.docs_link as string | undefined) || 'https://docs.newapi.pro' + const docsUrl = + (status?.docs_link as string | undefined) || 'https://docs.newapi.pro' const renderDocsButton = () => { const isExternal = docsUrl.startsWith('http') @@ -54,16 +55,12 @@ export function Hero(props: HeroProps) { return ( + } > - + {t('Docs')} ) @@ -71,10 +68,10 @@ export function Hero(props: HeroProps) { return ( ) @@ -105,7 +102,7 @@ export function Hero(props: HeroProps) {
{/* Top Pill Badge */}
@@ -141,7 +138,7 @@ export function Hero(props: HeroProps) { {props.isAuthenticated ? ( <>