♻️ 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
@@ -2172,6 +2172,7 @@ export function ChannelMutateDrawer({
)}
allowCreate
createLabel='Add custom model "{{value}}"'
maxVisibleChips={8}
/>
</FormControl>
{modelMappingGuardrail.exposedTargetModels
+24 -27
View File
@@ -17,11 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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 = () => (
<svg
className='size-6 shrink-0 text-muted-foreground/60 transition-colors group-hover:text-foreground'
className='text-muted-foreground/60 group-hover:text-foreground size-6 shrink-0 transition-colors'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -46,7 +46,8 @@ 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 (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
render={
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
/>
<a href={docsUrl} target='_blank' rel='noopener noreferrer' />
}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<BookOpen className='text-muted-foreground/80 group-hover:text-foreground size-4 transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
@@ -71,10 +68,10 @@ export function Hero(props: HeroProps) {
return (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
render={<Link to={docsUrl} />}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<BookOpen className='text-muted-foreground/80 group-hover:text-foreground size-4 transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
@@ -105,7 +102,7 @@ export function Hero(props: HeroProps) {
<div className='flex flex-col items-start text-left lg:col-span-6'>
{/* Top Pill Badge */}
<div
className='landing-animate-fade-up mb-5 inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/5 px-3 py-1.5 text-[11px] font-medium text-blue-600 dark:border-blue-400/20 dark:bg-blue-400/5 dark:text-blue-400 opacity-0 shadow-xs'
className='landing-animate-fade-up mb-5 inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/5 px-3 py-1.5 text-[11px] font-medium text-blue-600 opacity-0 shadow-xs dark:border-blue-400/20 dark:bg-blue-400/5 dark:text-blue-400'
style={{ animationDelay: '0ms' }}
>
<span className='relative flex size-1.5'>
@@ -141,7 +138,7 @@ export function Hero(props: HeroProps) {
{props.isAuthenticated ? (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
className='group h-11 rounded-lg px-5 text-sm font-medium'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
@@ -152,7 +149,7 @@ export function Hero(props: HeroProps) {
) : (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
className='group h-11 rounded-lg px-5 text-sm font-medium'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
@@ -160,7 +157,7 @@ export function Hero(props: HeroProps) {
</Button>
<Button
variant='outline'
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium'
className='border-border/50 hover:border-border hover:bg-muted/50 h-11 rounded-lg px-5 text-sm font-medium'
render={<Link to='/pricing' />}
>
{t('View Pricing')}
@@ -175,12 +172,14 @@ export function Hero(props: HeroProps) {
className='landing-animate-fade-up mt-10 w-full max-w-xl opacity-0'
style={{ animationDelay: '240ms' }}
>
<div className='flex flex-col gap-1 mb-4'>
<span className='text-[10px] font-bold tracking-[0.15em] text-muted-foreground/50 uppercase'>
<div className='mb-4 flex flex-col gap-1'>
<span className='text-muted-foreground/50 text-[10px] font-bold tracking-[0.15em] uppercase'>
{t('Supported Applications')}
</span>
<p className='text-xs text-muted-foreground/60 leading-relaxed'>
{t('Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.')}
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
{t(
'Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.'
)}
</p>
</div>
<div className='flex flex-wrap items-center gap-3'>
@@ -189,7 +188,7 @@ export function Hero(props: HeroProps) {
href='https://cherry-ai.com'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
className='group border-border/40 bg-muted/15 text-foreground/80 hover:border-border hover:bg-muted/30 hover:text-foreground flex items-center gap-3 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'
>
<CherryStudio.Color size={24} className='shrink-0' />
<span>Cherry Studio</span>
@@ -200,7 +199,7 @@ export function Hero(props: HeroProps) {
href='https://ccswitch.io'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
className='group border-border/40 bg-muted/15 text-foreground/80 hover:border-border hover:bg-muted/30 hover:text-foreground flex items-center gap-3 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'
>
<img
src='https://ccswitch.io/favicon.png'
@@ -223,9 +222,7 @@ export function Hero(props: HeroProps) {
</a>
{/* "更多" */}
<div
className='group flex items-center gap-2.5 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/55 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02] cursor-default'
>
<div className='group border-border/40 bg-muted/15 text-foreground/55 hover:border-border hover:bg-muted/30 hover:text-foreground flex cursor-default items-center gap-2.5 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'>
<MoreIcon />
<span>{t('More Apps')}</span>
</div>
@@ -235,7 +232,7 @@ export function Hero(props: HeroProps) {
{/* Right Column: Hero Terminal API Demo */}
<div
className='landing-animate-fade-up flex justify-center w-full opacity-0 lg:col-span-6'
className='landing-animate-fade-up flex w-full justify-center opacity-0 lg:col-span-6'
style={{ animationDelay: '320ms' }}
>
<HeroTerminalDemo className='mt-8 lg:mt-0' />