Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
1d166532
authored
Jun 28, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: update section titles and improve layout in channel components
parent
2d5a0416
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
642 additions
and
204 deletions
+642
-204
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
+630
-194
web/default/src/features/channels/components/drawers/sections/channel-advanced-section.tsx
+6
-3
web/default/src/features/channels/components/drawers/sections/channel-api-access-section.tsx
+5
-6
web/default/src/features/channels/components/drawers/sections/channel-auth-section.tsx
+1
-1
No files found.
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
View file @
1d166532
...
...
@@ -29,8 +29,14 @@ import { zodResolver } from '@hookform/resolvers/zod'
import
{
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
ArrowRight
,
AlertCircle
,
Boxes
,
CheckCircle2
,
Circle
,
HelpCircle
,
KeyRound
,
Loader2
,
Server
,
Sparkles
,
Trash2
,
Copy
,
...
...
@@ -47,15 +53,17 @@ import {
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
ADMIN_PERMISSION_ACTIONS
,
ADMIN_PERMISSION_RESOURCES
,
hasPermission
,
}
from
'@/lib/admin-permissions'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
ROLE
}
from
'@/lib/roles'
import
{
useCopyToClipboard
}
from
'@/hooks/use-copy-to-clipboard'
import
{
useHiddenClickUnlock
}
from
'@/hooks/use-hidden-click-unlock'
sideDrawerContentClassName
,
sideDrawerFooterClassName
,
sideDrawerFormClassName
,
sideDrawerHeaderClassName
,
sideDrawerSectionClassName
,
sideDrawerSwitchItemClassName
,
}
from
'@/components/drawer-layout'
import
{
JsonEditor
}
from
'@/components/json-editor'
import
{
MultiSelect
}
from
'@/components/multi-select'
import
{
Alert
,
AlertDescription
}
from
'@/components/ui/alert'
import
{
Badge
}
from
'@/components/ui/badge'
import
{
Button
}
from
'@/components/ui/button'
...
...
@@ -97,20 +105,21 @@ import {
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
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
,
useSecureVerification
,
}
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
{
fetchModels
,
getAllModels
,
...
...
@@ -122,6 +131,7 @@ import {
}
from
'../../api'
import
{
ADD_MODE_OPTIONS
,
CHANNEL_STATUS_LABELS
,
CHANNEL_TYPE_OPTIONS
,
CHANNEL_TYPE_WARNINGS
,
ERROR_MESSAGES
,
...
...
@@ -187,6 +197,17 @@ type ModelMappingGuardrail = {
exposedTargetModels
:
string
[]
}
type
ChannelEditorSectionStatus
=
'complete'
|
'configured'
|
'error'
|
'idle'
type
ChannelEditorNavItem
=
{
id
:
string
title
:
string
description
?:
string
statusLabel
:
string
status
:
ChannelEditorSectionStatus
icon
:
ReactNode
}
// Helper functions
const
createEmptyModelMappingGuardrail
=
():
ModelMappingGuardrail
=>
({
invalidJson
:
false
,
...
...
@@ -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
({
open
,
onOpenChange
,
...
...
@@ -359,9 +524,9 @@ export function ChannelMutateDrawer({
// Fetch channel details if editing
const
{
data
:
channelData
,
isLoading
:
isChannelLoading
}
=
useQuery
({
queryKey
:
channelsQueryKeys
.
detail
(
c
urrentRow
?.
i
d
||
0
),
queryFn
:
()
=>
getChannel
(
c
urrentRow
!
.
id
),
enabled
:
isEditing
&&
Boolean
(
c
urrentRow
?.
i
d
),
queryKey
:
channelsQueryKeys
.
detail
(
c
hannelI
d
||
0
),
queryFn
:
()
=>
getChannel
(
c
hannelId
||
0
),
enabled
:
isEditing
&&
Boolean
(
c
hannelI
d
),
})
// Fetch available groups
...
...
@@ -420,7 +585,10 @@ export function ChannelMutateDrawer({
const
keyMode
=
form
.
watch
(
'key_mode'
)
const
currentGroups
=
form
.
watch
(
'group'
)
const
currentType
=
form
.
watch
(
'type'
)
const
currentStatus
=
form
.
watch
(
'status'
)
const
currentBaseUrl
=
form
.
watch
(
'base_url'
)
const
currentKey
=
form
.
watch
(
'key'
)
const
currentOther
=
form
.
watch
(
'other'
)
const
currentModels
=
form
.
watch
(
'models'
)
const
currentName
=
form
.
watch
(
'name'
)
const
currentModelMapping
=
form
.
watch
(
'model_mapping'
)
...
...
@@ -496,7 +664,7 @@ export function ChannelMutateDrawer({
const
groupOptions
=
useMemo
(()
=>
{
if
(
!
groupsData
?.
data
)
return
[]
const
allGroups
=
new
Set
([...
groupsData
.
data
,
...(
currentGroups
||
[])])
return
Array
.
from
(
allGroups
)
.
map
((
group
)
=>
({
return
[...
allGroups
]
.
map
((
group
)
=>
({
value
:
group
,
label
:
group
,
}))
...
...
@@ -519,18 +687,110 @@ export function ChannelMutateDrawer({
const
options
=
CHANNEL_TYPE_OPTIONS
.
map
((
option
)
=>
({
value
:
String
(
option
.
value
),
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
))
{
options
.
push
({
value
:
String
(
currentType
),
label
:
`#
${
currentType
}
`
,
icon
:
getLobeIcon
(
`
${
getChannelTypeIcon
(
currentType
)}
.Color`
,
16
)
,
icon
:
<
ChannelTypeLogo
type=
{
currentType
}
size=
{
16
}
/>
,
})
}
return
options
},
[
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)
const
redirectModelList
=
useMemo
(
()
=>
extractRedirectModels
(
currentModelMapping
||
''
),
...
...
@@ -546,7 +806,7 @@ export function ChannelMutateDrawer({
// Transform models to multi-select options
const
modelOptions
=
useMemo
(()
=>
{
const
allModels
=
new
Set
([...
allModelsList
,
...
currentModelsArray
])
return
Array
.
from
(
allModels
)
.
map
((
model
)
=>
({
return
[...
allModels
]
.
map
((
model
)
=>
({
value
:
model
,
label
:
model
,
}))
...
...
@@ -577,8 +837,8 @@ export function ChannelMutateDrawer({
return
acc
},
[])
const
missingSourceModels
=
Array
.
from
(
new
Set
(
const
missingSourceModels
=
[
...
new
Set
(
entries
.
filter
(
(
entry
)
=>
...
...
@@ -586,11 +846,11 @@ export function ChannelMutateDrawer({
!
currentModelsArray
.
includes
(
entry
.
source
)
)
.
map
((
entry
)
=>
entry
.
source
)
)
)
)
,
]
const
exposedTargetModels
=
Array
.
from
(
new
Set
(
const
exposedTargetModels
=
[
...
new
Set
(
entries
.
filter
(
(
entry
)
=>
...
...
@@ -598,8 +858,8 @@ export function ChannelMutateDrawer({
currentModelsArray
.
includes
(
entry
.
target
)
)
.
map
((
entry
)
=>
entry
.
target
)
)
)
)
,
]
return
{
invalidJson
:
false
,
...
...
@@ -633,7 +893,7 @@ export function ChannelMutateDrawer({
return
{
lastCheckTime
:
settings
.
upstream_model_update_last_check_time
,
detectedModels
:
Array
.
from
(
new
Set
(
detectedModels
))
,
detectedModels
:
[...
new
Set
(
detectedModels
)]
,
}
},
[
currentSettings
])
...
...
@@ -1063,9 +1323,10 @@ export function ChannelMutateDrawer({
const
hasModelMapping
=
typeof
data
.
model_mapping
===
'string'
&&
data
.
model_mapping
.
trim
()
!==
''
const
modelMappingValue
=
data
.
model_mapping
||
''
if
(
hasModelMapping
)
{
const
validation
=
validateModelMappingJson
(
data
.
model_mapping
!
)
const
validation
=
validateModelMappingJson
(
modelMappingValue
)
if
(
!
validation
.
valid
)
{
toast
.
error
(
t
(
validation
.
error
||
'Invalid model mapping'
))
return
...
...
@@ -1078,7 +1339,7 @@ export function ChannelMutateDrawer({
// Check for missing models in model_mapping
if
(
hasModelMapping
)
{
const
missingModels
=
findMissingModelsInMapping
(
data
.
model_mapping
!
,
modelMappingValue
,
normalizedModels
)
...
...
@@ -1097,9 +1358,9 @@ export function ChannelMutateDrawer({
return
}
if
(
confirmAction
===
'add'
)
{
const
updatedModels
=
Array
.
from
(
new
Set
([...
normalizedModels
,
...
missingModels
])
)
const
updatedModels
=
[
...
new
Set
([...
normalizedModels
,
...
missingModels
]),
]
data
.
models
=
formatModelsArray
(
updatedModels
)
form
.
setValue
(
'models'
,
data
.
models
)
}
...
...
@@ -1154,11 +1415,11 @@ export function ChannelMutateDrawer({
return
(
<>
<
Sheet
open=
{
open
}
onOpenChange=
{
handleOpenChange
}
>
<
SheetContent
className=
{
sideDrawerContentClassName
(
'sm:max-w-
3
xl'
)
}
>
<
SheetContent
className=
{
sideDrawerContentClassName
(
'sm:max-w-
5
xl'
)
}
>
<
SheetHeader
className=
{
sideDrawerHeaderClassName
()
}
>
<
SheetTitle
className=
'flex items-center gap-3'
>
<
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
>
{
isEditing
?
t
(
'Edit Channel'
)
:
t
(
'Create Channel'
)
}
...
...
@@ -1181,7 +1442,9 @@ export function ChannelMutateDrawer({
{
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'
>
<
AlertDescription
>
{
t
(
'Sensitive channel settings are read-only for your account.'
)
}{
' '
}
{
t
(
'Sensitive channel settings are read-only for your account.'
)
}{
' '
}
{
t
(
'You can still edit non-sensitive operations fields such as models, groups, priority, and weight.'
)
}
...
...
@@ -1198,27 +1461,22 @@ export function ChannelMutateDrawer({
{
isChannelDetailLoading
?
(
<
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 ── */
}
<
div
id=
'channel-section-identity'
className=
'scroll-mt-4'
>
<
ChannelBasicSection
>
<
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
disabled=
{
sensitiveLocked
}
className=
'min-w-0 disabled:opacity-60'
...
...
@@ -1230,6 +1488,13 @@ export function ChannelMutateDrawer({
<
FormItem
>
<
FormLabel
>
{
t
(
'Type *'
)
}
</
FormLabel
>
<
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
options=
{
channelTypeOptions
}
value=
{
String
(
field
.
value
)
}
...
...
@@ -1247,8 +1512,10 @@ export function ChannelMutateDrawer({
'Search channel type...'
)
}
emptyText=
{
t
(
'No channel type found.'
)
}
className=
'pl-10'
allowCustomValue
/>
</
div
>
</
FormControl
>
{
sensitiveLocked
&&
(
<
FormDescription
>
...
...
@@ -1262,6 +1529,23 @@ export function ChannelMutateDrawer({
)
}
/>
</
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
>
{
!
isEditing
&&
(
...
...
@@ -1269,7 +1553,9 @@ export function ChannelMutateDrawer({
control=
{
form
.
control
}
name=
'status'
render=
{
({
field
})
=>
(
<
FormItem
className=
{
sideDrawerSwitchItemClassName
()
}
>
<
FormItem
className=
{
sideDrawerSwitchItemClassName
()
}
>
<
div
className=
'flex flex-col gap-0.5'
>
<
FormLabel
>
{
t
(
'Enabled'
)
}
</
FormLabel
>
<
FormDescription
className=
'text-xs'
>
...
...
@@ -1299,9 +1585,14 @@ export function ChannelMutateDrawer({
name=
'openai_organization'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'OpenAI Organization'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'OpenAI Organization'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
'org-...'
)
}
{
...
field
}
/>
<
Input
placeholder=
{
t
(
'org-...'
)
}
{
...
field
}
/>
</
FormControl
>
<
FormDescription
>
{
sensitiveLocked
...
...
@@ -1317,8 +1608,13 @@ export function ChannelMutateDrawer({
</
fieldset
>
)
}
</
ChannelBasicSection
>
</
div
>
{
/* ── API Access ── */
}
<
div
id=
'channel-section-credentials'
className=
'scroll-mt-4'
>
<
ChannelApiAccessSection
>
{
CHANNEL_TYPE_WARNINGS
[
currentType
]
&&
(
<
Alert
>
...
...
@@ -1331,13 +1627,12 @@ export function ChannelMutateDrawer({
{
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'
>
<
AlertDescription
>
{
t
(
'No permission to perform this action'
)
}
{
t
(
'No permission to perform this action'
)
}
</
AlertDescription
>
</
Alert
>
)
}
<
div
className=
'border-border/60 bg-muted/10 rounded-lg border p-4'
>
<
fieldset
disabled=
{
sensitiveLocked
}
className=
'space-y-4 disabled:opacity-60'
...
...
@@ -1378,12 +1673,16 @@ export function ChannelMutateDrawer({
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
'e.g., 2025-04-01-preview'
)
}
placeholder=
{
t
(
'e.g., 2025-04-01-preview'
)
}
{
...
field
}
/>
</
FormControl
>
<
FormDescription
>
{
t
(
'Default API version for this channel'
)
}
{
t
(
'Default API version for this channel'
)
}
</
FormDescription
>
<
FormMessage
/>
</
FormItem
>
...
...
@@ -1436,7 +1735,8 @@ export function ChannelMutateDrawer({
/>
</
FormControl
>
<
FormDescription
>
{
t
(
'Enter the complete URL, supports'
)
}
{
'
{
'
}
{
t
(
'Enter the complete URL, supports'
)
}{
' '
}
{
'
{
'
}
{
t
(
'model'
)}
{
'
}
'
}
{
t
(
'variable'
)
}
</
FormDescription
>
...
...
@@ -1453,9 +1753,14 @@ export function ChannelMutateDrawer({
name=
'other'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Model Version *'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Model Version *'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
'e.g., v2.1'
)
}
{
...
field
}
/>
<
Input
placeholder=
{
t
(
'e.g., v2.1'
)
}
{
...
field
}
/>
</
FormControl
>
<
FormDescription
>
{
t
(
...
...
@@ -1476,7 +1781,9 @@ export function ChannelMutateDrawer({
render=
{
({
field
})
=>
(
<
FormItem
className=
'flex items-center justify-between'
>
<
div
className=
'space-y-0.5'
>
<
FormLabel
>
{
t
(
'Enterprise Account'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Enterprise Account'
)
}
</
FormLabel
>
<
FormDescription
>
{
t
(
'Enable if this is an OpenRouter enterprise account with special response format'
...
...
@@ -1506,9 +1813,14 @@ export function ChannelMutateDrawer({
items=
{
[
{
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
}
value=
{
field
.
value
}
...
...
@@ -1520,7 +1832,9 @@ export function ChannelMutateDrawer({
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectItem
value=
'ak_sk'
>
{
t
(
'AccessKey / SecretAccessKey'
)
}
...
...
@@ -1551,7 +1865,9 @@ export function ChannelMutateDrawer({
name=
'other'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Knowledge Base ID *'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Knowledge Base ID *'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
'e.g., 123456'
)
}
...
...
@@ -1574,7 +1890,9 @@ export function ChannelMutateDrawer({
name=
'base_url'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Private Deployment URL'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Private Deployment URL'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
...
...
@@ -1602,7 +1920,9 @@ export function ChannelMutateDrawer({
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'API Base URL (Important: Not Chat API) *'
)
}
{
t
(
'API Base URL (Important: Not Chat API) *'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
...
...
@@ -1659,7 +1979,9 @@ export function ChannelMutateDrawer({
rel=
'noopener noreferrer'
className=
'text-primary underline'
>
{
t
(
'https://cloud.siliconflow.cn/i/hij0YNTZ'
)
}
{
t
(
'https://cloud.siliconflow.cn/i/hij0YNTZ'
)
}
</
a
>
</
AlertDescription
>
</
Alert
>
...
...
@@ -1673,11 +1995,16 @@ export function ChannelMutateDrawer({
name=
'vertex_key_type'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Vertex AI Key Format'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Vertex AI Key Format'
)
}
</
FormLabel
>
<
Select
items=
{
[
{
value
:
'json'
,
label
:
t
(
'JSON'
)
},
{
value
:
'api_key'
,
label
:
t
(
'API Key'
)
},
{
value
:
'api_key'
,
label
:
t
(
'API Key'
),
},
]
}
onValueChange=
{
field
.
onChange
}
value=
{
field
.
value
}
...
...
@@ -1687,7 +2014,9 @@ export function ChannelMutateDrawer({
<
SelectValue
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectItem
value=
'json'
>
{
t
(
'JSON'
)
}
...
...
@@ -1724,13 +2053,15 @@ export function ChannelMutateDrawer({
onChange=
{
async
(
e
)
=>
{
const
fileList
=
e
.
target
.
files
const
files
=
fileList
?
Array
.
from
(
fileList
)
?
[...
fileList
]
:
[]
// allow re-selecting the same file
e
.
target
.
value
=
''
if
(
files
.
length
===
0
)
{
toast
.
info
(
t
(
'Please upload key file(s)'
))
toast
.
info
(
t
(
'Please upload key file(s)'
)
)
return
}
...
...
@@ -1753,7 +2084,9 @@ export function ChannelMutateDrawer({
}
if
(
keys
.
length
===
0
)
{
toast
.
info
(
t
(
'Please upload key file(s)'
))
toast
.
info
(
t
(
'Please upload key file(s)'
)
)
return
}
...
...
@@ -1779,7 +2112,9 @@ export function ChannelMutateDrawer({
</
FormControl
>
<
FormDescription
>
{
isBatchMode
?
t
(
'Upload multiple JSON files in batch modes'
)
?
t
(
'Upload multiple JSON files in batch modes'
)
:
t
(
'Upload a single service account JSON file'
)
}
...
...
@@ -1792,7 +2127,9 @@ export function ChannelMutateDrawer({
name=
'other'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Deployment Region *'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Deployment Region *'
)
}
</
FormLabel
>
<
FormControl
>
<
Textarea
placeholder=
{
t
(
...
...
@@ -1803,7 +2140,9 @@ export function ChannelMutateDrawer({
/>
</
FormControl
>
<
FormDescription
>
{
t
(
'Enter deployment region or JSON mapping:'
)
}{
' '
}
{
t
(
'Enter deployment region or JSON mapping:'
)
}{
' '
}
{
'
{
'
}
{
t
(
'"default": "us-central1", "claude-3-5-sonnet-20240620": "europe-west1"'
...
...
@@ -1833,8 +2172,11 @@ export function ChannelMutateDrawer({
<
Select
items=
{
[
{
value
:
'https://ark.cn-beijing.volces.com'
,
label
:
t
(
'https://ark.cn-beijing.volces.com'
),
value
:
'https://ark.cn-beijing.volces.com'
,
label
:
t
(
'https://ark.cn-beijing.volces.com'
),
},
{
value
:
...
...
@@ -1857,10 +2199,14 @@ export function ChannelMutateDrawer({
<
SelectValue
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectItem
value=
'https://ark.cn-beijing.volces.com'
>
{
t
(
'https://ark.cn-beijing.volces.com'
)
}
{
t
(
'https://ark.cn-beijing.volces.com'
)
}
</
SelectItem
>
<
SelectItem
value=
'https://ark.ap-southeast.bytepluses.com'
>
{
t
(
...
...
@@ -1937,7 +2283,9 @@ export function ChannelMutateDrawer({
<
FormLabel
>
{
t
(
'Base URL'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
BASE_URL
)
}
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
BASE_URL
)
}
{
...
field
}
/>
</
FormControl
>
...
...
@@ -2002,24 +2350,29 @@ export function ChannelMutateDrawer({
control=
{
form
.
control
}
name=
'multi_key_mode'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Add Mode'
)
}
</
FormLabel
>
<
FormItem
className=
'flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'
>
<
FormLabel
className=
'text-muted-foreground text-xs font-medium'
>
{
t
(
'Add Mode'
)
}
</
FormLabel
>
<
Select
items=
{
[
...
addModeOptions
.
map
((
option
)
=>
({
items=
{
addModeOptions
.
map
((
option
)
=>
({
value
:
option
.
value
,
label
:
t
(
option
.
label
),
})),
]
}
}))
}
onValueChange=
{
field
.
onChange
}
value=
{
field
.
value
}
>
<
FormControl
>
<
SelectTrigger
>
<
SelectTrigger
size=
'sm'
className=
'w-full sm:w-56'
>
<
SelectValue
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
{
addModeOptions
.
map
((
option
)
=>
(
<
SelectItem
...
...
@@ -2032,13 +2385,6 @@ export function ChannelMutateDrawer({
</
SelectGroup
>
</
SelectContent
>
</
Select
>
<
FormDescription
>
{
t
(
supportsMultiKeyAddMode
?
FIELD_DESCRIPTIONS
.
BATCH_ADD
:
FIELD_DESCRIPTIONS
.
KEY
)
}
</
FormDescription
>
<
FormMessage
/>
</
FormItem
>
)
}
...
...
@@ -2049,33 +2395,74 @@ export function ChannelMutateDrawer({
control=
{
form
.
control
}
name=
'key'
render=
{
({
field
})
=>
{
const
keyPlaceholder
=
(()
=>
{
let
keyPlaceholder
=
t
(
getKeyPromptForType
(
currentType
)
)
if
(
isEditing
)
{
return
t
(
'Leave empty to keep existing key'
)
}
if
(
currentType
===
33
)
{
if
(
awsKeyType
===
'api_key'
)
{
return
isBatchMode
?
t
(
keyPlaceholder
=
t
(
'Leave empty to keep existing key'
)
}
else
if
(
currentType
===
33
&&
awsKeyType
===
'api_key'
&&
isBatchMode
)
{
keyPlaceholder
=
t
(
'Enter API Key, one per line, format: APIKey|Region'
)
:
t
(
'Enter API Key, format: APIKey|Region'
)
}
return
isBatchMode
?
t
(
}
else
if
(
currentType
===
33
&&
awsKeyType
===
'api_key'
)
{
keyPlaceholder
=
t
(
'Enter API Key, format: APIKey|Region'
)
}
else
if
(
currentType
===
33
&&
isBatchMode
)
{
keyPlaceholder
=
t
(
'Enter key, one per line, format: AccessKey|SecretAccessKey|Region'
)
:
t
(
}
else
if
(
currentType
===
33
)
{
keyPlaceholder
=
t
(
'Enter key, format: AccessKey|SecretAccessKey|Region'
)
}
if
(
isBatchMode
)
{
return
t
(
}
else
if
(
isBatchMode
)
{
keyPlaceholder
=
t
(
'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
(
<
FormItem
>
<
FormLabel
>
{
t
(
'API Key *'
)
}
</
FormLabel
>
...
...
@@ -2088,31 +2475,7 @@ export function ChannelMutateDrawer({
</
FormControl
>
<
FormDescription
>
<
div
className=
'flex flex-col gap-2'
>
<
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
>
<
span
>
{
keyDescription
}
</
span
>
{
isBatchMode
&&
(
<
Button
type=
'button'
...
...
@@ -2165,7 +2528,9 @@ export function ChannelMutateDrawer({
size=
'sm'
onClick=
{
async
()
=>
{
if
(
channelKey
)
{
await
copyToClipboard
(
channelKey
)
await
copyToClipboard
(
channelKey
)
}
}
}
disabled=
{
!
channelKey
}
...
...
@@ -2178,7 +2543,9 @@ export function ChannelMutateDrawer({
<
Input
readOnly
value=
{
channelKey
??
''
}
placeholder=
{
t
(
'Hidden — verify to reveal'
)
}
placeholder=
{
t
(
'Hidden — verify to reveal'
)
}
className=
'font-mono'
/>
</
div
>
...
...
@@ -2237,7 +2604,9 @@ export function ChannelMutateDrawer({
name=
'key_mode'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Key Update Mode'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Key Update Mode'
)
}
</
FormLabel
>
<
Select
items=
{
[
{
...
...
@@ -2246,7 +2615,9 @@ export function ChannelMutateDrawer({
},
{
value
:
'replace'
,
label
:
t
(
'Replace all existing keys'
),
label
:
t
(
'Replace all existing keys'
),
},
]
}
onValueChange=
{
field
.
onChange
}
...
...
@@ -2257,7 +2628,9 @@ export function ChannelMutateDrawer({
<
SelectValue
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectItem
value=
'append'
>
{
t
(
'Append to existing keys'
)
}
...
...
@@ -2283,17 +2656,26 @@ export function ChannelMutateDrawer({
/>
)
}
{
!
isEditing
&&
multiKeyMode
===
'multi_to_single'
&&
(
{
!
isEditing
&&
multiKeyMode
===
'multi_to_single'
&&
(
<
FormField
control=
{
form
.
control
}
name=
'multi_key_type'
render=
{
({
field
})
=>
(
<
FormItem
>
<
FormLabel
>
{
t
(
'Multi-Key Strategy'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Multi-Key Strategy'
)
}
</
FormLabel
>
<
Select
items=
{
[
{
value
:
'random'
,
label
:
t
(
'Random'
)
},
{
value
:
'polling'
,
label
:
t
(
'Polling'
)
},
{
value
:
'random'
,
label
:
t
(
'Random'
),
},
{
value
:
'polling'
,
label
:
t
(
'Polling'
),
},
]
}
onValueChange=
{
field
.
onChange
}
value=
{
field
.
value
}
...
...
@@ -2303,7 +2685,9 @@ export function ChannelMutateDrawer({
<
SelectValue
/>
</
SelectTrigger
>
</
FormControl
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectItem
value=
'random'
>
{
t
(
'Random'
)
}
...
...
@@ -2334,9 +2718,12 @@ export function ChannelMutateDrawer({
)
}
</
ChannelAuthSection
>
</
fieldset
>
</
div
>
</
ChannelApiAccessSection
>
</
div
>
{
/* ── Models & Groups ── */
}
<
div
id=
'channel-section-models'
className=
'scroll-mt-4'
>
<
ChannelModelsSection
>
<
div
className=
'space-y-5'
>
<
div
className=
'border-border/60 bg-muted/10 rounded-lg border p-4'
>
...
...
@@ -2395,7 +2782,8 @@ export function ChannelMutateDrawer({
)
updateModels
(
currentModelsArray
.
filter
(
(
model
)
=>
!
hiddenTargets
.
has
(
model
)
(
model
)
=>
!
hiddenTargets
.
has
(
model
)
)
)
}
}
...
...
@@ -2512,7 +2900,9 @@ export function ChannelMutateDrawer({
type=
'button'
variant=
'secondary'
size=
'sm'
onClick=
{
()
=>
handleAddPrefillGroup
(
group
)
}
onClick=
{
()
=>
handleAddPrefillGroup
(
group
)
}
>
{
group
.
name
}
</
Button
>
...
...
@@ -2562,7 +2952,8 @@ export function ChannelMutateDrawer({
{
t
(
'Request flow'
)
}
</
p
>
<
div
className=
'space-y-1 font-mono text-xs'
>
{
mappingPreviewPairs
.
map
((
pair
)
=>
(
{
mappingPreviewPairs
.
map
(
(
pair
)
=>
(
<
div
key=
{
`${pair.source}-${pair.target}`
}
className=
'flex items-center gap-1'
...
...
@@ -2574,7 +2965,8 @@ export function ChannelMutateDrawer({
/>
<
span
>
{
pair
.
target
}
</
span
>
</
div
>
))
}
)
)
}
{
remainingMappingCount
>
0
&&
(
<
div
className=
'text-[11px] opacity-70'
>
+
{
remainingMappingCount
}{
' '
}
...
...
@@ -2618,7 +3010,9 @@ export function ChannelMutateDrawer({
<
code
className=
'font-mono'
>
{
'
{
"gpt-4"
:
"Azure-GPT4"
}
'
}
</
code
>
{
t
(
'. Please fix the JSON before saving.'
)
}
{
t
(
'. Please fix the JSON before saving.'
)
}
</
AlertDescription
>
</
Alert
>
)
}
...
...
@@ -2677,7 +3071,9 @@ export function ChannelMutateDrawer({
options=
{
groupOptions
}
selected=
{
field
.
value
}
onChange=
{
field
.
onChange
}
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
GROUP
)
}
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
GROUP
)
}
/>
)
}
</
FormControl
>
...
...
@@ -2688,10 +3084,13 @@ export function ChannelMutateDrawer({
</
div
>
</
div
>
</
ChannelModelsSection
>
</
div
>
<
div
id=
'channel-section-advanced'
className=
'scroll-mt-4'
>
<
ChannelAdvancedSection
open=
{
advancedSettingsOpen
}
onOpenChange=
{
handleAdvancedSettingsOpenChange
}
summary=
{
advancedSummary
}
>
{
/* ── Routing & Overrides ── */
}
<
div
className=
{
sideDrawerSectionClassName
()
}
>
...
...
@@ -2762,7 +3161,9 @@ export function ChannelMutateDrawer({
<
FormLabel
>
{
t
(
'Test Model'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
TEST_MODEL
)
}
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
TEST_MODEL
)
}
{
...
field
}
/>
</
FormControl
>
...
...
@@ -2832,7 +3233,9 @@ export function ChannelMutateDrawer({
<
FormLabel
>
{
t
(
'Remark'
)
}
</
FormLabel
>
<
FormControl
>
<
Textarea
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
REMARK
)
}
placeholder=
{
t
(
FIELD_PLACEHOLDERS
.
REMARK
)
}
rows=
{
2
}
{
...
field
}
/>
...
...
@@ -2973,7 +3376,9 @@ export function ChannelMutateDrawer({
<
Textarea
value=
{
field
.
value
||
''
}
onChange=
{
field
.
onChange
}
disabled=
{
sensitiveLocked
||
isSubmitting
}
disabled=
{
sensitiveLocked
||
isSubmitting
}
rows=
{
8
}
placeholder=
{
t
(
'Override request parameters. Cannot override stream parameter.'
...
...
@@ -3011,8 +3416,10 @@ export function ChannelMutateDrawer({
{
'*'
:
true
,
're:^X-Trace-.*$'
:
true
,
'X-Foo'
:
'
{
client_header
:
X
-
Foo
}
',
Authorization: '
Bearer
{
api_key
}
'
,
'X-Foo'
:
'
{
client_header
:
X
-
Foo
}
',
Authorization:
'
Bearer
{
api_key
}
'
,
},
null
,
2
...
...
@@ -3028,7 +3435,11 @@ export function ChannelMutateDrawer({
size=
'sm'
onClick=
{
()
=>
field
.
onChange
(
JSON
.
stringify
({
'*'
:
true
},
null
,
2
)
JSON
.
stringify
(
{
'*'
:
true
},
null
,
2
)
)
}
>
...
...
@@ -3046,7 +3457,7 @@ export function ChannelMutateDrawer({
field
.
onChange
(
JSON
.
stringify
(
parsed
,
null
,
2
)
)
}
catch
(
_e
)
{
}
catch
{
/* ignore invalid JSON */
}
}
}
...
...
@@ -3069,7 +3480,9 @@ export function ChannelMutateDrawer({
rows=
{
6
}
value=
{
field
.
value
||
''
}
onChange=
{
field
.
onChange
}
disabled=
{
sensitiveLocked
||
isSubmitting
}
disabled=
{
sensitiveLocked
||
isSubmitting
}
placeholder=
{
t
(
'Enter JSON to override request headers'
)
}
...
...
@@ -3103,9 +3516,7 @@ export function ChannelMutateDrawer({
{
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'
>
<
AlertDescription
>
{
t
(
'No permission to perform this action'
)
}
{
t
(
'No permission to perform this action'
)
}
</
AlertDescription
>
</
Alert
>
)
}
...
...
@@ -3117,7 +3528,9 @@ export function ChannelMutateDrawer({
<
div
className=
'border-border/60 flex flex-col gap-3 border-y py-4'
>
<
SubHeading
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'
>
...
...
@@ -3128,10 +3541,14 @@ export function ChannelMutateDrawer({
<
FormItem
className=
'flex items-center justify-between gap-3 px-4 py-3'
>
<
div
className=
'space-y-0.5'
>
<
FormLabel
className=
'text-sm'
>
{
t
(
'Allow service_tier passthrough'
)
}
{
t
(
'Allow service_tier passthrough'
)
}
</
FormLabel
>
<
FormDescription
>
{
t
(
'Pass through the service_tier field'
)
}
{
t
(
'Pass through the service_tier field'
)
}
</
FormDescription
>
</
div
>
<
FormControl
>
...
...
@@ -3263,7 +3680,9 @@ export function ChannelMutateDrawer({
<
FormItem
className=
'flex items-center justify-between gap-3 px-4 py-3'
>
<
div
className=
'space-y-0.5'
>
<
FormLabel
className=
'text-sm'
>
{
t
(
'Allow inference_geo passthrough'
)
}
{
t
(
'Allow inference_geo passthrough'
)
}
</
FormLabel
>
<
FormDescription
>
{
t
(
...
...
@@ -3346,7 +3765,9 @@ export function ChannelMutateDrawer({
render=
{
({
field
})
=>
(
<
FormItem
className=
'flex items-center justify-between px-4 py-3'
>
<
div
className=
'space-y-0.5'
>
<
FormLabel
>
{
t
(
'Force Format'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Force Format'
)
}
</
FormLabel
>
<
FormDescription
>
{
t
(
'Force format response to OpenAI standard (OpenAI channel only)'
...
...
@@ -3395,9 +3816,13 @@ export function ChannelMutateDrawer({
render=
{
({
field
})
=>
(
<
FormItem
className=
'flex items-center justify-between px-4 py-3'
>
<
div
className=
'space-y-0.5'
>
<
FormLabel
>
{
t
(
'Pass Through Body'
)
}
</
FormLabel
>
<
FormLabel
>
{
t
(
'Pass Through Body'
)
}
</
FormLabel
>
<
FormDescription
>
{
t
(
'Pass request body directly to upstream'
)
}
{
t
(
'Pass request body directly to upstream'
)
}
</
FormDescription
>
</
div
>
<
FormControl
>
...
...
@@ -3444,7 +3869,9 @@ export function ChannelMutateDrawer({
<
FormLabel
>
{
t
(
'Proxy Address'
)
}
</
FormLabel
>
<
FormControl
>
<
Input
placeholder=
{
t
(
'socks5://user:pass@host:port'
)
}
placeholder=
{
t
(
'socks5://user:pass@host:port'
)
}
{
...
field
}
/>
</
FormControl
>
...
...
@@ -3474,7 +3901,9 @@ export function ChannelMutateDrawer({
/>
</
FormControl
>
<
FormDescription
>
{
t
(
'Default system prompt for this channel'
)
}
{
t
(
'Default system prompt for this channel'
)
}
</
FormDescription
>
<
FormMessage
/>
</
FormItem
>
...
...
@@ -3595,28 +4024,33 @@ export function ChannelMutateDrawer({
<
span
className=
'text-foreground font-medium'
>
{
t
(
'Last check time'
)
}
:
</
span
>
{
' '
}
{
formatUnixTime
(
upstreamUpdateMeta
.
lastCheckTime
)
}
{
formatUnixTime
(
upstreamUpdateMeta
.
lastCheckTime
)
}
</
div
>
<
div
>
<
span
className=
'text-foreground font-medium'
>
{
t
(
'Last detected addable models'
)
}
:
</
span
>
{
' '
}
{
upstreamUpdateMeta
.
detectedModels
.
length
===
0
?
(
{
upstreamUpdateMeta
.
detectedModels
.
length
===
0
?
(
t
(
'None'
)
)
:
(
<>
<
span
className=
'break-all'
>
{
upstreamDetectedModelsPreview
.
join
(
', '
)
}
{
upstreamDetectedModelsPreview
.
join
(
', '
)
}
</
span
>
{
upstreamDetectedModelsOmittedCount
>
0
&&
(
{
upstreamDetectedModelsOmittedCount
>
0
&&
(
<
span
className=
'ml-1'
>
{
t
(
'
({{
total
}}
total
,
{{
omit
}}
omitted
)
'
,
{
total
:
upstreamUpdateMeta
.
detectedModels
.
length
,
upstreamUpdateMeta
.
detectedModels
.
length
,
omit
:
upstreamDetectedModelsOmittedCount
,
}
)
}
...
...
@@ -3631,7 +4065,9 @@ export function ChannelMutateDrawer({
</
fieldset
>
</
div
>
</
ChannelAdvancedSection
>
</>
</
div
>
</
div
>
</
div
>
)
}
</
form
>
</
Form
>
...
...
web/default/src/features/channels/components/drawers/sections/channel-advanced-section.tsx
View file @
1d166532
import
{
ChevronDown
,
Settings
}
from
'lucide-react'
/*
Copyright (C) 2023-2026 QuantumNous
...
...
@@ -17,19 +18,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
type
{
ReactNode
}
from
'react'
import
{
ChevronDown
,
Settings
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
Collapsible
,
CollapsibleContent
,
CollapsibleTrigger
,
}
from
'@/components/ui/collapsible'
import
{
cn
}
from
'@/lib/utils'
type
ChannelAdvancedSectionProps
=
{
children
:
ReactNode
open
:
boolean
onOpenChange
:
(
open
:
boolean
)
=>
void
summary
?:
ReactNode
}
export
function
ChannelAdvancedSection
(
props
:
ChannelAdvancedSectionProps
)
{
...
...
@@ -55,7 +57,8 @@ export function ChannelAdvancedSection(props: ChannelAdvancedSectionProps) {
{
t
(
'Advanced Settings'
)
}
</
div
>
<
div
className=
'text-muted-foreground text-xs'
>
{
t
(
{
props
.
summary
??
t
(
'Request overrides, routing behavior, and upstream model automation'
)
}
</
div
>
...
...
web/default/src/features/channels/components/drawers/sections/channel-api-access-section.tsx
View file @
1d166532
...
...
@@ -17,8 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
type
{
ReactNode
}
from
'react'
import
{
Link2
}
from
'lucide-react'
import
{
KeyRound
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
SideDrawerSection
,
SideDrawerSectionHeader
,
...
...
@@ -34,11 +35,9 @@ export function ChannelApiAccessSection(props: ChannelApiAccessSectionProps) {
return
(
<
SideDrawerSection
>
<
SideDrawerSectionHeader
title=
{
t
(
'API Access'
)
}
description=
{
t
(
'Endpoint, provider-specific settings, and credentials.'
)
}
icon=
{
<
Link2
className=
'h-4 w-4'
aria
-
hidden=
'true'
/>
}
title=
{
t
(
'Credentials'
)
}
description=
{
t
(
'Authentication'
)
}
icon=
{
<
KeyRound
className=
'h-4 w-4'
aria
-
hidden=
'true'
/>
}
/>
{
props
.
children
}
</
SideDrawerSection
>
...
...
web/default/src/features/channels/components/drawers/sections/channel-auth-section.tsx
View file @
1d166532
...
...
@@ -28,7 +28,7 @@ export function ChannelAuthSection(props: ChannelAuthSectionProps) {
const
{
t
}
=
useTranslation
()
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'
>
<
KeyRound
className=
'text-muted-foreground h-3.5 w-3.5'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment