♻️ 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:
+1
@@ -2172,6 +2172,7 @@ export function ChannelMutateDrawer({
|
||||
)}
|
||||
allowCreate
|
||||
createLabel='Add custom model "{{value}}"'
|
||||
maxVisibleChips={8}
|
||||
/>
|
||||
</FormControl>
|
||||
{modelMappingGuardrail.exposedTargetModels
|
||||
|
||||
+24
-27
@@ -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' />
|
||||
|
||||
Reference in New Issue
Block a user