Commit 1d166532 by CaIon

fix: update section titles and improve layout in channel components

parent 2d5a0416
...@@ -29,8 +29,14 @@ import { zodResolver } from '@hookform/resolvers/zod' ...@@ -29,8 +29,14 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { import {
ArrowRight, ArrowRight,
AlertCircle,
Boxes,
CheckCircle2,
Circle,
HelpCircle, HelpCircle,
KeyRound,
Loader2, Loader2,
Server,
Sparkles, Sparkles,
Trash2, Trash2,
Copy, Copy,
...@@ -47,15 +53,17 @@ import { ...@@ -47,15 +53,17 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { import {
ADMIN_PERMISSION_ACTIONS, sideDrawerContentClassName,
ADMIN_PERMISSION_RESOURCES, sideDrawerFooterClassName,
hasPermission, sideDrawerFormClassName,
} from '@/lib/admin-permissions' sideDrawerHeaderClassName,
import { getLobeIcon } from '@/lib/lobe-icon' sideDrawerSectionClassName,
import { ROLE } from '@/lib/roles' sideDrawerSwitchItemClassName,
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' } from '@/components/drawer-layout'
import { useHiddenClickUnlock } from '@/hooks/use-hidden-click-unlock' import { JsonEditor } from '@/components/json-editor'
import { MultiSelect } from '@/components/multi-select'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
...@@ -97,20 +105,21 @@ import { ...@@ -97,20 +105,21 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { import {
sideDrawerContentClassName,
sideDrawerFooterClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
sideDrawerSectionClassName,
sideDrawerSwitchItemClassName,
} from '@/components/drawer-layout'
import { JsonEditor } from '@/components/json-editor'
import { MultiSelect } from '@/components/multi-select'
import {
SecureVerificationDialog, SecureVerificationDialog,
useSecureVerification, useSecureVerification,
} from '@/features/auth/secure-verification' } from '@/features/auth/secure-verification'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { useHiddenClickUnlock } from '@/hooks/use-hidden-click-unlock'
import {
ADMIN_PERMISSION_ACTIONS,
ADMIN_PERMISSION_RESOURCES,
hasPermission,
} from '@/lib/admin-permissions'
import { getLobeIcon } from '@/lib/lobe-icon'
import { ROLE } from '@/lib/roles'
import { cn } from '@/lib/utils'
import { useAuthStore } from '@/stores/auth-store' import { useAuthStore } from '@/stores/auth-store'
import { import {
fetchModels, fetchModels,
getAllModels, getAllModels,
...@@ -122,6 +131,7 @@ import { ...@@ -122,6 +131,7 @@ import {
} from '../../api' } from '../../api'
import { import {
ADD_MODE_OPTIONS, ADD_MODE_OPTIONS,
CHANNEL_STATUS_LABELS,
CHANNEL_TYPE_OPTIONS, CHANNEL_TYPE_OPTIONS,
CHANNEL_TYPE_WARNINGS, CHANNEL_TYPE_WARNINGS,
ERROR_MESSAGES, ERROR_MESSAGES,
...@@ -187,6 +197,17 @@ type ModelMappingGuardrail = { ...@@ -187,6 +197,17 @@ type ModelMappingGuardrail = {
exposedTargetModels: string[] exposedTargetModels: string[]
} }
type ChannelEditorSectionStatus = 'complete' | 'configured' | 'error' | 'idle'
type ChannelEditorNavItem = {
id: string
title: string
description?: string
statusLabel: string
status: ChannelEditorSectionStatus
icon: ReactNode
}
// Helper functions // Helper functions
const createEmptyModelMappingGuardrail = (): ModelMappingGuardrail => ({ const createEmptyModelMappingGuardrail = (): ModelMappingGuardrail => ({
invalidJson: false, invalidJson: false,
...@@ -313,6 +334,150 @@ function SubHeading({ title, icon }: { title: string; icon?: ReactNode }) { ...@@ -313,6 +334,150 @@ function SubHeading({ title, icon }: { title: string; icon?: ReactNode }) {
) )
} }
function ChannelTypeLogo(props: {
type: number
size?: number
className?: string
}) {
const isKnownType = CHANNEL_TYPE_OPTIONS.some(
(option) => option.value === props.type
)
if (!isKnownType) {
return (
<Server
className={cn('text-muted-foreground shrink-0', props.className)}
style={{
width: props.size ?? 16,
height: props.size ?? 16,
}}
aria-hidden='true'
/>
)
}
return (
<span className={cn('inline-flex shrink-0', props.className)}>
{getLobeIcon(`${getChannelTypeIcon(props.type)}.Color`, props.size ?? 16)}
</span>
)
}
function getSectionStatusIcon(status: ChannelEditorSectionStatus): ReactNode {
if (status === 'error') {
return <AlertCircle className='h-3.5 w-3.5' aria-hidden='true' />
}
if (status === 'complete' || status === 'configured') {
return <CheckCircle2 className='h-3.5 w-3.5' aria-hidden='true' />
}
return <Circle className='h-3.5 w-3.5' aria-hidden='true' />
}
function getCompletionStatus(
hasErrors: boolean,
isComplete: boolean
): ChannelEditorSectionStatus {
if (hasErrors) return 'error'
if (isComplete) return 'complete'
return 'idle'
}
function getSectionStatusLabel(
status: ChannelEditorSectionStatus,
t: (key: string) => string
): string {
if (status === 'error') return t('Error')
if (status === 'complete' || status === 'configured') return t('Ready')
return t('Incomplete')
}
function ChannelEditorNav(props: {
providerLogo: ReactNode
providerLabel: string
statusLabel: string
progressLabel: string
navigationLabel: string
items: ChannelEditorNavItem[]
}) {
return (
<aside className='hidden self-start lg:sticky lg:top-4 lg:z-20 lg:block'>
<div className='flex max-h-[calc(100dvh-12rem)] flex-col gap-3 overflow-y-auto overscroll-contain pr-1'>
<div className='border-border/60 bg-muted/20 rounded-lg border p-3'>
<div className='flex min-w-0 items-center gap-2'>
<span className='bg-background flex size-8 shrink-0 items-center justify-center rounded-md border'>
{props.providerLogo}
</span>
<div className='min-w-0'>
<p className='truncate text-sm font-medium'>
{props.providerLabel}
</p>
<p className='text-muted-foreground truncate text-xs'>
{props.statusLabel} · {props.progressLabel}
</p>
</div>
</div>
</div>
<nav
className='border-border/60 bg-background rounded-lg border p-1'
aria-label={props.navigationLabel}
>
{props.items.map((item) => {
const isError = item.status === 'error'
const isDone =
item.status === 'complete' || item.status === 'configured'
return (
<button
key={item.id}
type='button'
className={cn(
'hover:bg-muted/60 flex w-full items-start gap-2 rounded-md px-2 py-2 text-left transition-colors',
isError && 'text-destructive hover:bg-destructive/10'
)}
onClick={() => {
document
.querySelector<HTMLElement>(`#${item.id}`)
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}}
>
<span
className={cn(
'bg-muted text-muted-foreground mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md',
isError && 'bg-destructive/10 text-destructive',
isDone && !isError && 'text-primary'
)}
>
{item.icon}
</span>
<span className='min-w-0 flex-1'>
<span className='block truncate text-sm font-medium'>
{item.title}
</span>
{item.description && (
<span className='text-muted-foreground block truncate text-xs'>
{item.description}
</span>
)}
</span>
<span
className={cn(
'text-muted-foreground mt-1 shrink-0',
isError && 'text-destructive',
isDone && !isError && 'text-primary'
)}
aria-label={item.statusLabel}
>
{getSectionStatusIcon(item.status)}
</span>
</button>
)
})}
</nav>
</div>
</aside>
)
}
export function ChannelMutateDrawer({ export function ChannelMutateDrawer({
open, open,
onOpenChange, onOpenChange,
...@@ -359,9 +524,9 @@ export function ChannelMutateDrawer({ ...@@ -359,9 +524,9 @@ export function ChannelMutateDrawer({
// Fetch channel details if editing // Fetch channel details if editing
const { data: channelData, isLoading: isChannelLoading } = useQuery({ const { data: channelData, isLoading: isChannelLoading } = useQuery({
queryKey: channelsQueryKeys.detail(currentRow?.id || 0), queryKey: channelsQueryKeys.detail(channelId || 0),
queryFn: () => getChannel(currentRow!.id), queryFn: () => getChannel(channelId || 0),
enabled: isEditing && Boolean(currentRow?.id), enabled: isEditing && Boolean(channelId),
}) })
// Fetch available groups // Fetch available groups
...@@ -420,7 +585,10 @@ export function ChannelMutateDrawer({ ...@@ -420,7 +585,10 @@ export function ChannelMutateDrawer({
const keyMode = form.watch('key_mode') const keyMode = form.watch('key_mode')
const currentGroups = form.watch('group') const currentGroups = form.watch('group')
const currentType = form.watch('type') const currentType = form.watch('type')
const currentStatus = form.watch('status')
const currentBaseUrl = form.watch('base_url') const currentBaseUrl = form.watch('base_url')
const currentKey = form.watch('key')
const currentOther = form.watch('other')
const currentModels = form.watch('models') const currentModels = form.watch('models')
const currentName = form.watch('name') const currentName = form.watch('name')
const currentModelMapping = form.watch('model_mapping') const currentModelMapping = form.watch('model_mapping')
...@@ -496,7 +664,7 @@ export function ChannelMutateDrawer({ ...@@ -496,7 +664,7 @@ export function ChannelMutateDrawer({
const groupOptions = useMemo(() => { const groupOptions = useMemo(() => {
if (!groupsData?.data) return [] if (!groupsData?.data) return []
const allGroups = new Set([...groupsData.data, ...(currentGroups || [])]) const allGroups = new Set([...groupsData.data, ...(currentGroups || [])])
return Array.from(allGroups).map((group) => ({ return [...allGroups].map((group) => ({
value: group, value: group,
label: group, label: group,
})) }))
...@@ -519,18 +687,110 @@ export function ChannelMutateDrawer({ ...@@ -519,18 +687,110 @@ export function ChannelMutateDrawer({
const options = CHANNEL_TYPE_OPTIONS.map((option) => ({ const options = CHANNEL_TYPE_OPTIONS.map((option) => ({
value: String(option.value), value: String(option.value),
label: t(option.label), label: t(option.label),
icon: getLobeIcon(`${getChannelTypeIcon(option.value)}.Color`, 16), icon: <ChannelTypeLogo type={option.value} size={16} />,
})) }))
if (!options.some((option) => Number(option.value) === currentType)) { if (!options.some((option) => Number(option.value) === currentType)) {
options.push({ options.push({
value: String(currentType), value: String(currentType),
label: `#${currentType}`, label: `#${currentType}`,
icon: getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 16), icon: <ChannelTypeLogo type={currentType} size={16} />,
}) })
} }
return options return options
}, [currentType, t]) }, [currentType, t])
const formErrors = form.formState.errors
const identityHasErrors = Boolean(
formErrors.name ||
formErrors.type ||
formErrors.status ||
formErrors.openai_organization
)
const credentialsHaveErrors = Boolean(
formErrors.key ||
formErrors.base_url ||
formErrors.other ||
formErrors.multi_key_mode ||
formErrors.multi_key_type ||
formErrors.key_mode ||
formErrors.vertex_key_type ||
formErrors.aws_key_type ||
formErrors.azure_responses_version
)
const modelsHaveErrors = Boolean(
formErrors.models || formErrors.group || formErrors.model_mapping
)
const advancedHaveErrors =
hasAdvancedSettingsErrors(formErrors) || Boolean(formErrors.advanced_custom)
const providerRequiresBaseUrl = [3, 8, 36, 45].includes(currentType)
const providerRequiresOther = [3, 18, 21, 39, 41, 49].includes(currentType)
const identityComplete = Boolean(currentName?.trim() && currentType > 0)
const credentialsComplete = Boolean(
(isEditing || currentKey?.trim()) &&
(!providerRequiresBaseUrl || currentBaseUrl?.trim()) &&
(!providerRequiresOther || currentOther?.trim())
)
const modelsComplete = Boolean(
currentModelsArray.length > 0 && currentGroups?.length
)
const requiredCompletedCount = [
identityComplete,
credentialsComplete,
modelsComplete,
].filter(Boolean).length
const currentStatusLabel =
CHANNEL_STATUS_LABELS[
currentStatus as keyof typeof CHANNEL_STATUS_LABELS
] || 'Unknown'
const progressLabel = `${requiredCompletedCount}/3`
const identityStatus = getCompletionStatus(
identityHasErrors,
identityComplete
)
const credentialsStatus = getCompletionStatus(
credentialsHaveErrors,
credentialsComplete
)
const modelsStatus = getCompletionStatus(modelsHaveErrors, modelsComplete)
const advancedStatus: ChannelEditorSectionStatus = advancedHaveErrors
? 'error'
: 'idle'
const advancedSummary = advancedHaveErrors ? t('Error') : undefined
const editorNavItems: ChannelEditorNavItem[] = [
{
id: 'channel-section-identity',
title: t('Basic Information'),
description: getSectionStatusLabel(identityStatus, t),
statusLabel: getSectionStatusLabel(identityStatus, t),
status: identityStatus,
icon: <Server className='h-4 w-4' aria-hidden='true' />,
},
{
id: 'channel-section-credentials',
title: t('Credentials'),
description: getSectionStatusLabel(credentialsStatus, t),
statusLabel: getSectionStatusLabel(credentialsStatus, t),
status: credentialsStatus,
icon: <KeyRound className='h-4 w-4' aria-hidden='true' />,
},
{
id: 'channel-section-models',
title: t('Models & Groups'),
description: getSectionStatusLabel(modelsStatus, t),
statusLabel: getSectionStatusLabel(modelsStatus, t),
status: modelsStatus,
icon: <Boxes className='h-4 w-4' aria-hidden='true' />,
},
{
id: 'channel-section-advanced',
title: t('Advanced Settings'),
description: advancedSummary,
statusLabel: advancedSummary ?? t('Advanced Settings'),
status: advancedStatus,
icon: <Settings className='h-4 w-4' aria-hidden='true' />,
},
]
// Extract redirect models from model_mapping (target values) // Extract redirect models from model_mapping (target values)
const redirectModelList = useMemo( const redirectModelList = useMemo(
() => extractRedirectModels(currentModelMapping || ''), () => extractRedirectModels(currentModelMapping || ''),
...@@ -546,7 +806,7 @@ export function ChannelMutateDrawer({ ...@@ -546,7 +806,7 @@ export function ChannelMutateDrawer({
// Transform models to multi-select options // Transform models to multi-select options
const modelOptions = useMemo(() => { const modelOptions = useMemo(() => {
const allModels = new Set([...allModelsList, ...currentModelsArray]) const allModels = new Set([...allModelsList, ...currentModelsArray])
return Array.from(allModels).map((model) => ({ return [...allModels].map((model) => ({
value: model, value: model,
label: model, label: model,
})) }))
...@@ -577,8 +837,8 @@ export function ChannelMutateDrawer({ ...@@ -577,8 +837,8 @@ export function ChannelMutateDrawer({
return acc return acc
}, []) }, [])
const missingSourceModels = Array.from( const missingSourceModels = [
new Set( ...new Set(
entries entries
.filter( .filter(
(entry) => (entry) =>
...@@ -586,11 +846,11 @@ export function ChannelMutateDrawer({ ...@@ -586,11 +846,11 @@ export function ChannelMutateDrawer({
!currentModelsArray.includes(entry.source) !currentModelsArray.includes(entry.source)
) )
.map((entry) => entry.source) .map((entry) => entry.source)
) ),
) ]
const exposedTargetModels = Array.from( const exposedTargetModels = [
new Set( ...new Set(
entries entries
.filter( .filter(
(entry) => (entry) =>
...@@ -598,8 +858,8 @@ export function ChannelMutateDrawer({ ...@@ -598,8 +858,8 @@ export function ChannelMutateDrawer({
currentModelsArray.includes(entry.target) currentModelsArray.includes(entry.target)
) )
.map((entry) => entry.target) .map((entry) => entry.target)
) ),
) ]
return { return {
invalidJson: false, invalidJson: false,
...@@ -633,7 +893,7 @@ export function ChannelMutateDrawer({ ...@@ -633,7 +893,7 @@ export function ChannelMutateDrawer({
return { return {
lastCheckTime: settings.upstream_model_update_last_check_time, lastCheckTime: settings.upstream_model_update_last_check_time,
detectedModels: Array.from(new Set(detectedModels)), detectedModels: [...new Set(detectedModels)],
} }
}, [currentSettings]) }, [currentSettings])
...@@ -1063,9 +1323,10 @@ export function ChannelMutateDrawer({ ...@@ -1063,9 +1323,10 @@ export function ChannelMutateDrawer({
const hasModelMapping = const hasModelMapping =
typeof data.model_mapping === 'string' && typeof data.model_mapping === 'string' &&
data.model_mapping.trim() !== '' data.model_mapping.trim() !== ''
const modelMappingValue = data.model_mapping || ''
if (hasModelMapping) { if (hasModelMapping) {
const validation = validateModelMappingJson(data.model_mapping!) const validation = validateModelMappingJson(modelMappingValue)
if (!validation.valid) { if (!validation.valid) {
toast.error(t(validation.error || 'Invalid model mapping')) toast.error(t(validation.error || 'Invalid model mapping'))
return return
...@@ -1078,7 +1339,7 @@ export function ChannelMutateDrawer({ ...@@ -1078,7 +1339,7 @@ export function ChannelMutateDrawer({
// Check for missing models in model_mapping // Check for missing models in model_mapping
if (hasModelMapping) { if (hasModelMapping) {
const missingModels = findMissingModelsInMapping( const missingModels = findMissingModelsInMapping(
data.model_mapping!, modelMappingValue,
normalizedModels normalizedModels
) )
...@@ -1097,9 +1358,9 @@ export function ChannelMutateDrawer({ ...@@ -1097,9 +1358,9 @@ export function ChannelMutateDrawer({
return return
} }
if (confirmAction === 'add') { if (confirmAction === 'add') {
const updatedModels = Array.from( const updatedModels = [
new Set([...normalizedModels, ...missingModels]) ...new Set([...normalizedModels, ...missingModels]),
) ]
data.models = formatModelsArray(updatedModels) data.models = formatModelsArray(updatedModels)
form.setValue('models', data.models) form.setValue('models', data.models)
} }
...@@ -1154,11 +1415,11 @@ export function ChannelMutateDrawer({ ...@@ -1154,11 +1415,11 @@ export function ChannelMutateDrawer({
return ( return (
<> <>
<Sheet open={open} onOpenChange={handleOpenChange}> <Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent className={sideDrawerContentClassName('sm:max-w-3xl')}> <SheetContent className={sideDrawerContentClassName('sm:max-w-5xl')}>
<SheetHeader className={sideDrawerHeaderClassName()}> <SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle className='flex items-center gap-3'> <SheetTitle className='flex items-center gap-3'>
<span className='bg-muted flex size-9 shrink-0 items-center justify-center rounded-md'> <span className='bg-muted flex size-9 shrink-0 items-center justify-center rounded-md'>
{getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)} <ChannelTypeLogo type={currentType} size={22} />
</span> </span>
<span> <span>
{isEditing ? t('Edit Channel') : t('Create Channel')} {isEditing ? t('Edit Channel') : t('Create Channel')}
...@@ -1181,7 +1442,9 @@ export function ChannelMutateDrawer({ ...@@ -1181,7 +1442,9 @@ export function ChannelMutateDrawer({
{sensitiveLocked && ( {sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'> <Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<AlertDescription> <AlertDescription>
{t('Sensitive channel settings are read-only for your account.')}{' '} {t(
'Sensitive channel settings are read-only for your account.'
)}{' '}
{t( {t(
'You can still edit non-sensitive operations fields such as models, groups, priority, and weight.' 'You can still edit non-sensitive operations fields such as models, groups, priority, and weight.'
)} )}
...@@ -1198,27 +1461,22 @@ export function ChannelMutateDrawer({ ...@@ -1198,27 +1461,22 @@ export function ChannelMutateDrawer({
{isChannelDetailLoading ? ( {isChannelDetailLoading ? (
<ChannelEditorLoadingState /> <ChannelEditorLoadingState />
) : ( ) : (
<> <div className='grid gap-5 lg:grid-cols-[13rem_minmax(0,1fr)] lg:items-start'>
<ChannelEditorNav
providerLogo={
<ChannelTypeLogo type={currentType} size={18} />
}
providerLabel={t(currentTypeLabel)}
statusLabel={t(currentStatusLabel)}
progressLabel={progressLabel}
navigationLabel={t('Channels')}
items={editorNavItems}
/>
<div className='flex min-w-0 flex-col gap-5'>
{/* ── Basic Information ── */} {/* ── Basic Information ── */}
<div id='channel-section-identity' className='scroll-mt-4'>
<ChannelBasicSection> <ChannelBasicSection>
<div className='grid gap-4 sm:grid-cols-2'> <div className='grid gap-4 sm:grid-cols-2'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Name *')}</FormLabel>
<FormControl>
<Input
placeholder={t(FIELD_PLACEHOLDERS.NAME)}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<fieldset <fieldset
disabled={sensitiveLocked} disabled={sensitiveLocked}
className='min-w-0 disabled:opacity-60' className='min-w-0 disabled:opacity-60'
...@@ -1230,6 +1488,13 @@ export function ChannelMutateDrawer({ ...@@ -1230,6 +1488,13 @@ export function ChannelMutateDrawer({
<FormItem> <FormItem>
<FormLabel>{t('Type *')}</FormLabel> <FormLabel>{t('Type *')}</FormLabel>
<FormControl> <FormControl>
<div className='relative'>
<span className='pointer-events-none absolute top-1/2 left-3 z-10 flex -translate-y-1/2'>
<ChannelTypeLogo
type={Number(field.value)}
size={18}
/>
</span>
<Combobox <Combobox
options={channelTypeOptions} options={channelTypeOptions}
value={String(field.value)} value={String(field.value)}
...@@ -1247,8 +1512,10 @@ export function ChannelMutateDrawer({ ...@@ -1247,8 +1512,10 @@ export function ChannelMutateDrawer({
'Search channel type...' 'Search channel type...'
)} )}
emptyText={t('No channel type found.')} emptyText={t('No channel type found.')}
className='pl-10'
allowCustomValue allowCustomValue
/> />
</div>
</FormControl> </FormControl>
{sensitiveLocked && ( {sensitiveLocked && (
<FormDescription> <FormDescription>
...@@ -1262,6 +1529,23 @@ export function ChannelMutateDrawer({ ...@@ -1262,6 +1529,23 @@ export function ChannelMutateDrawer({
)} )}
/> />
</fieldset> </fieldset>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Name *')}</FormLabel>
<FormControl>
<Input
placeholder={t(FIELD_PLACEHOLDERS.NAME)}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
{!isEditing && ( {!isEditing && (
...@@ -1269,7 +1553,9 @@ export function ChannelMutateDrawer({ ...@@ -1269,7 +1553,9 @@ export function ChannelMutateDrawer({
control={form.control} control={form.control}
name='status' name='status'
render={({ field }) => ( render={({ field }) => (
<FormItem className={sideDrawerSwitchItemClassName()}> <FormItem
className={sideDrawerSwitchItemClassName()}
>
<div className='flex flex-col gap-0.5'> <div className='flex flex-col gap-0.5'>
<FormLabel>{t('Enabled')}</FormLabel> <FormLabel>{t('Enabled')}</FormLabel>
<FormDescription className='text-xs'> <FormDescription className='text-xs'>
...@@ -1299,9 +1585,14 @@ export function ChannelMutateDrawer({ ...@@ -1299,9 +1585,14 @@ export function ChannelMutateDrawer({
name='openai_organization' name='openai_organization'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('OpenAI Organization')}</FormLabel> <FormLabel>
{t('OpenAI Organization')}
</FormLabel>
<FormControl> <FormControl>
<Input placeholder={t('org-...')} {...field} /> <Input
placeholder={t('org-...')}
{...field}
/>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{sensitiveLocked {sensitiveLocked
...@@ -1317,8 +1608,13 @@ export function ChannelMutateDrawer({ ...@@ -1317,8 +1608,13 @@ export function ChannelMutateDrawer({
</fieldset> </fieldset>
)} )}
</ChannelBasicSection> </ChannelBasicSection>
</div>
{/* ── API Access ── */} {/* ── API Access ── */}
<div
id='channel-section-credentials'
className='scroll-mt-4'
>
<ChannelApiAccessSection> <ChannelApiAccessSection>
{CHANNEL_TYPE_WARNINGS[currentType] && ( {CHANNEL_TYPE_WARNINGS[currentType] && (
<Alert> <Alert>
...@@ -1331,13 +1627,12 @@ export function ChannelMutateDrawer({ ...@@ -1331,13 +1627,12 @@ export function ChannelMutateDrawer({
{sensitiveLocked && ( {sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'> <Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<AlertDescription> <AlertDescription>
{t( {t('No permission to perform this action')}
'No permission to perform this action'
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<div className='border-border/60 bg-muted/10 rounded-lg border p-4'>
<fieldset <fieldset
disabled={sensitiveLocked} disabled={sensitiveLocked}
className='space-y-4 disabled:opacity-60' className='space-y-4 disabled:opacity-60'
...@@ -1378,12 +1673,16 @@ export function ChannelMutateDrawer({ ...@@ -1378,12 +1673,16 @@ export function ChannelMutateDrawer({
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t('e.g., 2025-04-01-preview')} placeholder={t(
'e.g., 2025-04-01-preview'
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('Default API version for this channel')} {t(
'Default API version for this channel'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
...@@ -1436,7 +1735,8 @@ export function ChannelMutateDrawer({ ...@@ -1436,7 +1735,8 @@ export function ChannelMutateDrawer({
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('Enter the complete URL, supports')} {'{'} {t('Enter the complete URL, supports')}{' '}
{'{'}
{t('model')} {t('model')}
{'}'} {t('variable')} {'}'} {t('variable')}
</FormDescription> </FormDescription>
...@@ -1453,9 +1753,14 @@ export function ChannelMutateDrawer({ ...@@ -1453,9 +1753,14 @@ export function ChannelMutateDrawer({
name='other' name='other'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Model Version *')}</FormLabel> <FormLabel>
{t('Model Version *')}
</FormLabel>
<FormControl> <FormControl>
<Input placeholder={t('e.g., v2.1')} {...field} /> <Input
placeholder={t('e.g., v2.1')}
{...field}
/>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t( {t(
...@@ -1476,7 +1781,9 @@ export function ChannelMutateDrawer({ ...@@ -1476,7 +1781,9 @@ export function ChannelMutateDrawer({
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center justify-between'> <FormItem className='flex items-center justify-between'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel>{t('Enterprise Account')}</FormLabel> <FormLabel>
{t('Enterprise Account')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Enable if this is an OpenRouter enterprise account with special response format' 'Enable if this is an OpenRouter enterprise account with special response format'
...@@ -1506,9 +1813,14 @@ export function ChannelMutateDrawer({ ...@@ -1506,9 +1813,14 @@ export function ChannelMutateDrawer({
items={[ items={[
{ {
value: 'ak_sk', value: 'ak_sk',
label: t('AccessKey / SecretAccessKey'), label: t(
'AccessKey / SecretAccessKey'
),
},
{
value: 'api_key',
label: t('API Key'),
}, },
{ value: 'api_key', label: t('API Key') },
]} ]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
...@@ -1520,7 +1832,9 @@ export function ChannelMutateDrawer({ ...@@ -1520,7 +1832,9 @@ export function ChannelMutateDrawer({
/> />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
<SelectItem value='ak_sk'> <SelectItem value='ak_sk'>
{t('AccessKey / SecretAccessKey')} {t('AccessKey / SecretAccessKey')}
...@@ -1551,7 +1865,9 @@ export function ChannelMutateDrawer({ ...@@ -1551,7 +1865,9 @@ export function ChannelMutateDrawer({
name='other' name='other'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Knowledge Base ID *')}</FormLabel> <FormLabel>
{t('Knowledge Base ID *')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t('e.g., 123456')} placeholder={t('e.g., 123456')}
...@@ -1574,7 +1890,9 @@ export function ChannelMutateDrawer({ ...@@ -1574,7 +1890,9 @@ export function ChannelMutateDrawer({
name='base_url' name='base_url'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Private Deployment URL')}</FormLabel> <FormLabel>
{t('Private Deployment URL')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t( placeholder={t(
...@@ -1602,7 +1920,9 @@ export function ChannelMutateDrawer({ ...@@ -1602,7 +1920,9 @@ export function ChannelMutateDrawer({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('API Base URL (Important: Not Chat API) *')} {t(
'API Base URL (Important: Not Chat API) *'
)}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
...@@ -1659,7 +1979,9 @@ export function ChannelMutateDrawer({ ...@@ -1659,7 +1979,9 @@ export function ChannelMutateDrawer({
rel='noopener noreferrer' rel='noopener noreferrer'
className='text-primary underline' className='text-primary underline'
> >
{t('https://cloud.siliconflow.cn/i/hij0YNTZ')} {t(
'https://cloud.siliconflow.cn/i/hij0YNTZ'
)}
</a> </a>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
...@@ -1673,11 +1995,16 @@ export function ChannelMutateDrawer({ ...@@ -1673,11 +1995,16 @@ export function ChannelMutateDrawer({
name='vertex_key_type' name='vertex_key_type'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Vertex AI Key Format')}</FormLabel> <FormLabel>
{t('Vertex AI Key Format')}
</FormLabel>
<Select <Select
items={[ items={[
{ value: 'json', label: t('JSON') }, { value: 'json', label: t('JSON') },
{ value: 'api_key', label: t('API Key') }, {
value: 'api_key',
label: t('API Key'),
},
]} ]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
...@@ -1687,7 +2014,9 @@ export function ChannelMutateDrawer({ ...@@ -1687,7 +2014,9 @@ export function ChannelMutateDrawer({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
<SelectItem value='json'> <SelectItem value='json'>
{t('JSON')} {t('JSON')}
...@@ -1724,13 +2053,15 @@ export function ChannelMutateDrawer({ ...@@ -1724,13 +2053,15 @@ export function ChannelMutateDrawer({
onChange={async (e) => { onChange={async (e) => {
const fileList = e.target.files const fileList = e.target.files
const files = fileList const files = fileList
? Array.from(fileList) ? [...fileList]
: [] : []
// allow re-selecting the same file // allow re-selecting the same file
e.target.value = '' e.target.value = ''
if (files.length === 0) { if (files.length === 0) {
toast.info(t('Please upload key file(s)')) toast.info(
t('Please upload key file(s)')
)
return return
} }
...@@ -1753,7 +2084,9 @@ export function ChannelMutateDrawer({ ...@@ -1753,7 +2084,9 @@ export function ChannelMutateDrawer({
} }
if (keys.length === 0) { if (keys.length === 0) {
toast.info(t('Please upload key file(s)')) toast.info(
t('Please upload key file(s)')
)
return return
} }
...@@ -1779,7 +2112,9 @@ export function ChannelMutateDrawer({ ...@@ -1779,7 +2112,9 @@ export function ChannelMutateDrawer({
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{isBatchMode {isBatchMode
? t('Upload multiple JSON files in batch modes') ? t(
'Upload multiple JSON files in batch modes'
)
: t( : t(
'Upload a single service account JSON file' 'Upload a single service account JSON file'
)} )}
...@@ -1792,7 +2127,9 @@ export function ChannelMutateDrawer({ ...@@ -1792,7 +2127,9 @@ export function ChannelMutateDrawer({
name='other' name='other'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Deployment Region *')}</FormLabel> <FormLabel>
{t('Deployment Region *')}
</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
placeholder={t( placeholder={t(
...@@ -1803,7 +2140,9 @@ export function ChannelMutateDrawer({ ...@@ -1803,7 +2140,9 @@ export function ChannelMutateDrawer({
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('Enter deployment region or JSON mapping:')}{' '} {t(
'Enter deployment region or JSON mapping:'
)}{' '}
{'{'} {'{'}
{t( {t(
'"default": "us-central1", "claude-3-5-sonnet-20240620": "europe-west1"' '"default": "us-central1", "claude-3-5-sonnet-20240620": "europe-west1"'
...@@ -1833,8 +2172,11 @@ export function ChannelMutateDrawer({ ...@@ -1833,8 +2172,11 @@ export function ChannelMutateDrawer({
<Select <Select
items={[ items={[
{ {
value: 'https://ark.cn-beijing.volces.com', value:
label: t('https://ark.cn-beijing.volces.com'), 'https://ark.cn-beijing.volces.com',
label: t(
'https://ark.cn-beijing.volces.com'
),
}, },
{ {
value: value:
...@@ -1857,10 +2199,14 @@ export function ChannelMutateDrawer({ ...@@ -1857,10 +2199,14 @@ export function ChannelMutateDrawer({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
<SelectItem value='https://ark.cn-beijing.volces.com'> <SelectItem value='https://ark.cn-beijing.volces.com'>
{t('https://ark.cn-beijing.volces.com')} {t(
'https://ark.cn-beijing.volces.com'
)}
</SelectItem> </SelectItem>
<SelectItem value='https://ark.ap-southeast.bytepluses.com'> <SelectItem value='https://ark.ap-southeast.bytepluses.com'>
{t( {t(
...@@ -1937,7 +2283,9 @@ export function ChannelMutateDrawer({ ...@@ -1937,7 +2283,9 @@ export function ChannelMutateDrawer({
<FormLabel>{t('Base URL')}</FormLabel> <FormLabel>{t('Base URL')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t(FIELD_PLACEHOLDERS.BASE_URL)} placeholder={t(
FIELD_PLACEHOLDERS.BASE_URL
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
...@@ -2002,24 +2350,29 @@ export function ChannelMutateDrawer({ ...@@ -2002,24 +2350,29 @@ export function ChannelMutateDrawer({
control={form.control} control={form.control}
name='multi_key_mode' name='multi_key_mode'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Add Mode')}</FormLabel> <FormLabel className='text-muted-foreground text-xs font-medium'>
{t('Add Mode')}
</FormLabel>
<Select <Select
items={[ items={addModeOptions.map((option) => ({
...addModeOptions.map((option) => ({
value: option.value, value: option.value,
label: t(option.label), label: t(option.label),
})), }))}
]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger
size='sm'
className='w-full sm:w-56'
>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
{addModeOptions.map((option) => ( {addModeOptions.map((option) => (
<SelectItem <SelectItem
...@@ -2032,13 +2385,6 @@ export function ChannelMutateDrawer({ ...@@ -2032,13 +2385,6 @@ export function ChannelMutateDrawer({
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription>
{t(
supportsMultiKeyAddMode
? FIELD_DESCRIPTIONS.BATCH_ADD
: FIELD_DESCRIPTIONS.KEY
)}
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
...@@ -2049,33 +2395,74 @@ export function ChannelMutateDrawer({ ...@@ -2049,33 +2395,74 @@ export function ChannelMutateDrawer({
control={form.control} control={form.control}
name='key' name='key'
render={({ field }) => { render={({ field }) => {
const keyPlaceholder = (() => { let keyPlaceholder = t(
getKeyPromptForType(currentType)
)
if (isEditing) { if (isEditing) {
return t('Leave empty to keep existing key') keyPlaceholder = t(
} 'Leave empty to keep existing key'
if (currentType === 33) { )
if (awsKeyType === 'api_key') { } else if (
return isBatchMode currentType === 33 &&
? t( awsKeyType === 'api_key' &&
isBatchMode
) {
keyPlaceholder = t(
'Enter API Key, one per line, format: APIKey|Region' 'Enter API Key, one per line, format: APIKey|Region'
) )
: t('Enter API Key, format: APIKey|Region') } else if (
} currentType === 33 &&
return isBatchMode awsKeyType === 'api_key'
? t( ) {
keyPlaceholder = t(
'Enter API Key, format: APIKey|Region'
)
} else if (
currentType === 33 &&
isBatchMode
) {
keyPlaceholder = t(
'Enter key, one per line, format: AccessKey|SecretAccessKey|Region' 'Enter key, one per line, format: AccessKey|SecretAccessKey|Region'
) )
: t( } else if (currentType === 33) {
keyPlaceholder = t(
'Enter key, format: AccessKey|SecretAccessKey|Region' 'Enter key, format: AccessKey|SecretAccessKey|Region'
) )
} } else if (isBatchMode) {
if (isBatchMode) { keyPlaceholder = t(
return t(
'Enter one key per line for batch creation' 'Enter one key per line for batch creation'
) )
} }
return t(getKeyPromptForType(currentType))
})() let keyDescription: ReactNode = t(
FIELD_DESCRIPTIONS.KEY
)
if (isEditing) {
let keyModeDescription = t(
'Append mode: New keys will be added to the end of the existing key list'
)
if (keyMode === 'replace') {
keyModeDescription = t(
'Replace mode: Will completely replace all existing keys'
)
}
keyDescription = (
<>
{t(
'Enter new key to update, or leave empty to keep current key'
)}
{isMultiKeyChannel && (
<span className='text-warning mt-1 block'>
{keyModeDescription}
</span>
)}
</>
)
} else if (isBatchMode) {
keyDescription = t(
'Enter one API key per line for batch creation'
)
}
return ( return (
<FormItem> <FormItem>
<FormLabel>{t('API Key *')}</FormLabel> <FormLabel>{t('API Key *')}</FormLabel>
...@@ -2088,31 +2475,7 @@ export function ChannelMutateDrawer({ ...@@ -2088,31 +2475,7 @@ export function ChannelMutateDrawer({
</FormControl> </FormControl>
<FormDescription> <FormDescription>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<span> <span>{keyDescription}</span>
{isEditing ? (
<>
{t(
'Enter new key to update, or leave empty to keep current key'
)}
{isMultiKeyChannel && (
<span className='text-warning mt-1 block'>
{t(
'Multi-key channel: Keys will be'
)}{' '}
{keyMode === 'replace'
? t('replaced')
: t('appended')}
</span>
)}
</>
) : isBatchMode ? (
t(
'Enter one API key per line for batch creation'
)
) : (
t(FIELD_DESCRIPTIONS.KEY)
)}
</span>
{isBatchMode && ( {isBatchMode && (
<Button <Button
type='button' type='button'
...@@ -2165,7 +2528,9 @@ export function ChannelMutateDrawer({ ...@@ -2165,7 +2528,9 @@ export function ChannelMutateDrawer({
size='sm' size='sm'
onClick={async () => { onClick={async () => {
if (channelKey) { if (channelKey) {
await copyToClipboard(channelKey) await copyToClipboard(
channelKey
)
} }
}} }}
disabled={!channelKey} disabled={!channelKey}
...@@ -2178,7 +2543,9 @@ export function ChannelMutateDrawer({ ...@@ -2178,7 +2543,9 @@ export function ChannelMutateDrawer({
<Input <Input
readOnly readOnly
value={channelKey ?? ''} value={channelKey ?? ''}
placeholder={t('Hidden — verify to reveal')} placeholder={t(
'Hidden — verify to reveal'
)}
className='font-mono' className='font-mono'
/> />
</div> </div>
...@@ -2237,7 +2604,9 @@ export function ChannelMutateDrawer({ ...@@ -2237,7 +2604,9 @@ export function ChannelMutateDrawer({
name='key_mode' name='key_mode'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Key Update Mode')}</FormLabel> <FormLabel>
{t('Key Update Mode')}
</FormLabel>
<Select <Select
items={[ items={[
{ {
...@@ -2246,7 +2615,9 @@ export function ChannelMutateDrawer({ ...@@ -2246,7 +2615,9 @@ export function ChannelMutateDrawer({
}, },
{ {
value: 'replace', value: 'replace',
label: t('Replace all existing keys'), label: t(
'Replace all existing keys'
),
}, },
]} ]}
onValueChange={field.onChange} onValueChange={field.onChange}
...@@ -2257,7 +2628,9 @@ export function ChannelMutateDrawer({ ...@@ -2257,7 +2628,9 @@ export function ChannelMutateDrawer({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
<SelectItem value='append'> <SelectItem value='append'>
{t('Append to existing keys')} {t('Append to existing keys')}
...@@ -2283,17 +2656,26 @@ export function ChannelMutateDrawer({ ...@@ -2283,17 +2656,26 @@ export function ChannelMutateDrawer({
/> />
)} )}
{!isEditing && multiKeyMode === 'multi_to_single' && ( {!isEditing &&
multiKeyMode === 'multi_to_single' && (
<FormField <FormField
control={form.control} control={form.control}
name='multi_key_type' name='multi_key_type'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Multi-Key Strategy')}</FormLabel> <FormLabel>
{t('Multi-Key Strategy')}
</FormLabel>
<Select <Select
items={[ items={[
{ value: 'random', label: t('Random') }, {
{ value: 'polling', label: t('Polling') }, value: 'random',
label: t('Random'),
},
{
value: 'polling',
label: t('Polling'),
},
]} ]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
...@@ -2303,7 +2685,9 @@ export function ChannelMutateDrawer({ ...@@ -2303,7 +2685,9 @@ export function ChannelMutateDrawer({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent alignItemWithTrigger={false}> <SelectContent
alignItemWithTrigger={false}
>
<SelectGroup> <SelectGroup>
<SelectItem value='random'> <SelectItem value='random'>
{t('Random')} {t('Random')}
...@@ -2334,9 +2718,12 @@ export function ChannelMutateDrawer({ ...@@ -2334,9 +2718,12 @@ export function ChannelMutateDrawer({
)} )}
</ChannelAuthSection> </ChannelAuthSection>
</fieldset> </fieldset>
</div>
</ChannelApiAccessSection> </ChannelApiAccessSection>
</div>
{/* ── Models & Groups ── */} {/* ── Models & Groups ── */}
<div id='channel-section-models' className='scroll-mt-4'>
<ChannelModelsSection> <ChannelModelsSection>
<div className='space-y-5'> <div className='space-y-5'>
<div className='border-border/60 bg-muted/10 rounded-lg border p-4'> <div className='border-border/60 bg-muted/10 rounded-lg border p-4'>
...@@ -2395,7 +2782,8 @@ export function ChannelMutateDrawer({ ...@@ -2395,7 +2782,8 @@ export function ChannelMutateDrawer({
) )
updateModels( updateModels(
currentModelsArray.filter( currentModelsArray.filter(
(model) => !hiddenTargets.has(model) (model) =>
!hiddenTargets.has(model)
) )
) )
}} }}
...@@ -2512,7 +2900,9 @@ export function ChannelMutateDrawer({ ...@@ -2512,7 +2900,9 @@ export function ChannelMutateDrawer({
type='button' type='button'
variant='secondary' variant='secondary'
size='sm' size='sm'
onClick={() => handleAddPrefillGroup(group)} onClick={() =>
handleAddPrefillGroup(group)
}
> >
{group.name} {group.name}
</Button> </Button>
...@@ -2562,7 +2952,8 @@ export function ChannelMutateDrawer({ ...@@ -2562,7 +2952,8 @@ export function ChannelMutateDrawer({
{t('Request flow')} {t('Request flow')}
</p> </p>
<div className='space-y-1 font-mono text-xs'> <div className='space-y-1 font-mono text-xs'>
{mappingPreviewPairs.map((pair) => ( {mappingPreviewPairs.map(
(pair) => (
<div <div
key={`${pair.source}-${pair.target}`} key={`${pair.source}-${pair.target}`}
className='flex items-center gap-1' className='flex items-center gap-1'
...@@ -2574,7 +2965,8 @@ export function ChannelMutateDrawer({ ...@@ -2574,7 +2965,8 @@ export function ChannelMutateDrawer({
/> />
<span>{pair.target}</span> <span>{pair.target}</span>
</div> </div>
))} )
)}
{remainingMappingCount > 0 && ( {remainingMappingCount > 0 && (
<div className='text-[11px] opacity-70'> <div className='text-[11px] opacity-70'>
+{remainingMappingCount}{' '} +{remainingMappingCount}{' '}
...@@ -2618,7 +3010,9 @@ export function ChannelMutateDrawer({ ...@@ -2618,7 +3010,9 @@ export function ChannelMutateDrawer({
<code className='font-mono'> <code className='font-mono'>
{'{"gpt-4":"Azure-GPT4"}'} {'{"gpt-4":"Azure-GPT4"}'}
</code> </code>
{t('. Please fix the JSON before saving.')} {t(
'. Please fix the JSON before saving.'
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
...@@ -2677,7 +3071,9 @@ export function ChannelMutateDrawer({ ...@@ -2677,7 +3071,9 @@ export function ChannelMutateDrawer({
options={groupOptions} options={groupOptions}
selected={field.value} selected={field.value}
onChange={field.onChange} onChange={field.onChange}
placeholder={t(FIELD_PLACEHOLDERS.GROUP)} placeholder={t(
FIELD_PLACEHOLDERS.GROUP
)}
/> />
)} )}
</FormControl> </FormControl>
...@@ -2688,10 +3084,13 @@ export function ChannelMutateDrawer({ ...@@ -2688,10 +3084,13 @@ export function ChannelMutateDrawer({
</div> </div>
</div> </div>
</ChannelModelsSection> </ChannelModelsSection>
</div>
<div id='channel-section-advanced' className='scroll-mt-4'>
<ChannelAdvancedSection <ChannelAdvancedSection
open={advancedSettingsOpen} open={advancedSettingsOpen}
onOpenChange={handleAdvancedSettingsOpenChange} onOpenChange={handleAdvancedSettingsOpenChange}
summary={advancedSummary}
> >
{/* ── Routing & Overrides ── */} {/* ── Routing & Overrides ── */}
<div className={sideDrawerSectionClassName()}> <div className={sideDrawerSectionClassName()}>
...@@ -2762,7 +3161,9 @@ export function ChannelMutateDrawer({ ...@@ -2762,7 +3161,9 @@ export function ChannelMutateDrawer({
<FormLabel>{t('Test Model')}</FormLabel> <FormLabel>{t('Test Model')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t(FIELD_PLACEHOLDERS.TEST_MODEL)} placeholder={t(
FIELD_PLACEHOLDERS.TEST_MODEL
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
...@@ -2832,7 +3233,9 @@ export function ChannelMutateDrawer({ ...@@ -2832,7 +3233,9 @@ export function ChannelMutateDrawer({
<FormLabel>{t('Remark')}</FormLabel> <FormLabel>{t('Remark')}</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
placeholder={t(FIELD_PLACEHOLDERS.REMARK)} placeholder={t(
FIELD_PLACEHOLDERS.REMARK
)}
rows={2} rows={2}
{...field} {...field}
/> />
...@@ -2973,7 +3376,9 @@ export function ChannelMutateDrawer({ ...@@ -2973,7 +3376,9 @@ export function ChannelMutateDrawer({
<Textarea <Textarea
value={field.value || ''} value={field.value || ''}
onChange={field.onChange} onChange={field.onChange}
disabled={sensitiveLocked || isSubmitting} disabled={
sensitiveLocked || isSubmitting
}
rows={8} rows={8}
placeholder={t( placeholder={t(
'Override request parameters. Cannot override stream parameter.' 'Override request parameters. Cannot override stream parameter.'
...@@ -3011,8 +3416,10 @@ export function ChannelMutateDrawer({ ...@@ -3011,8 +3416,10 @@ export function ChannelMutateDrawer({
{ {
'*': true, '*': true,
're:^X-Trace-.*$': true, 're:^X-Trace-.*$': true,
'X-Foo': '{client_header:X-Foo}', 'X-Foo':
Authorization: 'Bearer {api_key}', '{client_header:X-Foo}',
Authorization:
'Bearer {api_key}',
}, },
null, null,
2 2
...@@ -3028,7 +3435,11 @@ export function ChannelMutateDrawer({ ...@@ -3028,7 +3435,11 @@ export function ChannelMutateDrawer({
size='sm' size='sm'
onClick={() => onClick={() =>
field.onChange( field.onChange(
JSON.stringify({ '*': true }, null, 2) JSON.stringify(
{ '*': true },
null,
2
)
) )
} }
> >
...@@ -3046,7 +3457,7 @@ export function ChannelMutateDrawer({ ...@@ -3046,7 +3457,7 @@ export function ChannelMutateDrawer({
field.onChange( field.onChange(
JSON.stringify(parsed, null, 2) JSON.stringify(parsed, null, 2)
) )
} catch (_e) { } catch {
/* ignore invalid JSON */ /* ignore invalid JSON */
} }
}} }}
...@@ -3069,7 +3480,9 @@ export function ChannelMutateDrawer({ ...@@ -3069,7 +3480,9 @@ export function ChannelMutateDrawer({
rows={6} rows={6}
value={field.value || ''} value={field.value || ''}
onChange={field.onChange} onChange={field.onChange}
disabled={sensitiveLocked || isSubmitting} disabled={
sensitiveLocked || isSubmitting
}
placeholder={t( placeholder={t(
'Enter JSON to override request headers' 'Enter JSON to override request headers'
)} )}
...@@ -3103,9 +3516,7 @@ export function ChannelMutateDrawer({ ...@@ -3103,9 +3516,7 @@ export function ChannelMutateDrawer({
{sensitiveLocked && ( {sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'> <Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<AlertDescription> <AlertDescription>
{t( {t('No permission to perform this action')}
'No permission to perform this action'
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
...@@ -3117,7 +3528,9 @@ export function ChannelMutateDrawer({ ...@@ -3117,7 +3528,9 @@ export function ChannelMutateDrawer({
<div className='border-border/60 flex flex-col gap-3 border-y py-4'> <div className='border-border/60 flex flex-col gap-3 border-y py-4'>
<SubHeading <SubHeading
title={t('Field passthrough controls')} title={t('Field passthrough controls')}
icon={<SlidersHorizontal className='h-3.5 w-3.5' />} icon={
<SlidersHorizontal className='h-3.5 w-3.5' />
}
/> />
<div className='divide-border space-y-0 divide-y border-y'> <div className='divide-border space-y-0 divide-y border-y'>
...@@ -3128,10 +3541,14 @@ export function ChannelMutateDrawer({ ...@@ -3128,10 +3541,14 @@ export function ChannelMutateDrawer({
<FormItem className='flex items-center justify-between gap-3 px-4 py-3'> <FormItem className='flex items-center justify-between gap-3 px-4 py-3'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel className='text-sm'> <FormLabel className='text-sm'>
{t('Allow service_tier passthrough')} {t(
'Allow service_tier passthrough'
)}
</FormLabel> </FormLabel>
<FormDescription> <FormDescription>
{t('Pass through the service_tier field')} {t(
'Pass through the service_tier field'
)}
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
...@@ -3263,7 +3680,9 @@ export function ChannelMutateDrawer({ ...@@ -3263,7 +3680,9 @@ export function ChannelMutateDrawer({
<FormItem className='flex items-center justify-between gap-3 px-4 py-3'> <FormItem className='flex items-center justify-between gap-3 px-4 py-3'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel className='text-sm'> <FormLabel className='text-sm'>
{t('Allow inference_geo passthrough')} {t(
'Allow inference_geo passthrough'
)}
</FormLabel> </FormLabel>
<FormDescription> <FormDescription>
{t( {t(
...@@ -3346,7 +3765,9 @@ export function ChannelMutateDrawer({ ...@@ -3346,7 +3765,9 @@ export function ChannelMutateDrawer({
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center justify-between px-4 py-3'> <FormItem className='flex items-center justify-between px-4 py-3'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel>{t('Force Format')}</FormLabel> <FormLabel>
{t('Force Format')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Force format response to OpenAI standard (OpenAI channel only)' 'Force format response to OpenAI standard (OpenAI channel only)'
...@@ -3395,9 +3816,13 @@ export function ChannelMutateDrawer({ ...@@ -3395,9 +3816,13 @@ export function ChannelMutateDrawer({
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center justify-between px-4 py-3'> <FormItem className='flex items-center justify-between px-4 py-3'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
<FormLabel>{t('Pass Through Body')}</FormLabel> <FormLabel>
{t('Pass Through Body')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Pass request body directly to upstream')} {t(
'Pass request body directly to upstream'
)}
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
...@@ -3444,7 +3869,9 @@ export function ChannelMutateDrawer({ ...@@ -3444,7 +3869,9 @@ export function ChannelMutateDrawer({
<FormLabel>{t('Proxy Address')}</FormLabel> <FormLabel>{t('Proxy Address')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t('socks5://user:pass@host:port')} placeholder={t(
'socks5://user:pass@host:port'
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
...@@ -3474,7 +3901,9 @@ export function ChannelMutateDrawer({ ...@@ -3474,7 +3901,9 @@ export function ChannelMutateDrawer({
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('Default system prompt for this channel')} {t(
'Default system prompt for this channel'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
...@@ -3595,28 +4024,33 @@ export function ChannelMutateDrawer({ ...@@ -3595,28 +4024,33 @@ export function ChannelMutateDrawer({
<span className='text-foreground font-medium'> <span className='text-foreground font-medium'>
{t('Last check time')}: {t('Last check time')}:
</span>{' '} </span>{' '}
{formatUnixTime(upstreamUpdateMeta.lastCheckTime)} {formatUnixTime(
upstreamUpdateMeta.lastCheckTime
)}
</div> </div>
<div> <div>
<span className='text-foreground font-medium'> <span className='text-foreground font-medium'>
{t('Last detected addable models')}: {t('Last detected addable models')}:
</span>{' '} </span>{' '}
{upstreamUpdateMeta.detectedModels.length === {upstreamUpdateMeta.detectedModels
0 ? ( .length === 0 ? (
t('None') t('None')
) : ( ) : (
<> <>
<span className='break-all'> <span className='break-all'>
{upstreamDetectedModelsPreview.join(', ')} {upstreamDetectedModelsPreview.join(
', '
)}
</span> </span>
{upstreamDetectedModelsOmittedCount > 0 && ( {upstreamDetectedModelsOmittedCount >
0 && (
<span className='ml-1'> <span className='ml-1'>
{t( {t(
'({{total}} total, {{omit}} omitted)', '({{total}} total, {{omit}} omitted)',
{ {
total: total:
upstreamUpdateMeta.detectedModels upstreamUpdateMeta
.length, .detectedModels.length,
omit: upstreamDetectedModelsOmittedCount, omit: upstreamDetectedModelsOmittedCount,
} }
)} )}
...@@ -3631,7 +4065,9 @@ export function ChannelMutateDrawer({ ...@@ -3631,7 +4065,9 @@ export function ChannelMutateDrawer({
</fieldset> </fieldset>
</div> </div>
</ChannelAdvancedSection> </ChannelAdvancedSection>
</> </div>
</div>
</div>
)} )}
</form> </form>
</Form> </Form>
......
import { ChevronDown, Settings } from 'lucide-react'
/* /*
Copyright (C) 2023-2026 QuantumNous Copyright (C) 2023-2026 QuantumNous
...@@ -17,19 +18,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,19 +18,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { ChevronDown, Settings } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import { cn } from '@/lib/utils'
type ChannelAdvancedSectionProps = { type ChannelAdvancedSectionProps = {
children: ReactNode children: ReactNode
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
summary?: ReactNode
} }
export function ChannelAdvancedSection(props: ChannelAdvancedSectionProps) { export function ChannelAdvancedSection(props: ChannelAdvancedSectionProps) {
...@@ -55,7 +57,8 @@ export function ChannelAdvancedSection(props: ChannelAdvancedSectionProps) { ...@@ -55,7 +57,8 @@ export function ChannelAdvancedSection(props: ChannelAdvancedSectionProps) {
{t('Advanced Settings')} {t('Advanced Settings')}
</div> </div>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>
{t( {props.summary ??
t(
'Request overrides, routing behavior, and upstream model automation' 'Request overrides, routing behavior, and upstream model automation'
)} )}
</div> </div>
......
...@@ -17,8 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,8 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Link2 } from 'lucide-react' import { KeyRound } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
SideDrawerSection, SideDrawerSection,
SideDrawerSectionHeader, SideDrawerSectionHeader,
...@@ -34,11 +35,9 @@ export function ChannelApiAccessSection(props: ChannelApiAccessSectionProps) { ...@@ -34,11 +35,9 @@ export function ChannelApiAccessSection(props: ChannelApiAccessSectionProps) {
return ( return (
<SideDrawerSection> <SideDrawerSection>
<SideDrawerSectionHeader <SideDrawerSectionHeader
title={t('API Access')} title={t('Credentials')}
description={t( description={t('Authentication')}
'Endpoint, provider-specific settings, and credentials.' icon={<KeyRound className='h-4 w-4' aria-hidden='true' />}
)}
icon={<Link2 className='h-4 w-4' aria-hidden='true' />}
/> />
{props.children} {props.children}
</SideDrawerSection> </SideDrawerSection>
......
...@@ -28,7 +28,7 @@ export function ChannelAuthSection(props: ChannelAuthSectionProps) { ...@@ -28,7 +28,7 @@ export function ChannelAuthSection(props: ChannelAuthSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='border-border/60 flex flex-col gap-4 border-t pt-4'> <div className='border-border/60 flex flex-col gap-3 border-t pt-4'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<KeyRound <KeyRound
className='text-muted-foreground h-3.5 w-3.5' className='text-muted-foreground h-3.5 w-3.5'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment