- Add singleSelect to status/role filters in API keys, users, and redemptions tables (#4880) - Fix affiliate link 404 by changing /register to /sign-up (#4893) - Open FetchModelsDialog in channel creation mode via customFetcher prop (#4817) - Add TruncatedText component with tooltip for long channel names, token names, and usernames (#4877) - Elevate forgot-password link z-index to prevent label click interception (#4898)
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
} from '@/lib/format'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { cn, truncateText } from '@/lib/utils'
|
||||
import { TruncatedText } from '@/components/truncated-text'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
@@ -556,7 +557,11 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='font-medium'>{truncateText(name, 30)}</span>
|
||||
<TruncatedText
|
||||
text={name}
|
||||
className='font-medium'
|
||||
maxWidth='max-w-[180px]'
|
||||
/>
|
||||
{isPassThrough && (
|
||||
<TooltipProvider delay={100}>
|
||||
<Tooltip>
|
||||
|
||||
+31
-15
@@ -65,6 +65,8 @@ type FetchModelsDialogProps = {
|
||||
onModelsSelected?: (models: string[]) => void
|
||||
redirectModels?: string[]
|
||||
redirectSourceModels?: string[]
|
||||
customFetcher?: () => Promise<string[]>
|
||||
existingModelsOverride?: string[]
|
||||
}
|
||||
|
||||
export function FetchModelsDialog({
|
||||
@@ -73,6 +75,8 @@ export function FetchModelsDialog({
|
||||
onModelsSelected,
|
||||
redirectModels = [],
|
||||
redirectSourceModels = [],
|
||||
customFetcher,
|
||||
existingModelsOverride,
|
||||
}: FetchModelsDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const { currentRow } = useChannels()
|
||||
@@ -85,8 +89,10 @@ export function FetchModelsDialog({
|
||||
|
||||
// Parse existing models
|
||||
const existingModels = useMemo(
|
||||
() => parseModelsString(currentRow?.models || ''),
|
||||
[currentRow?.models]
|
||||
() =>
|
||||
existingModelsOverride ??
|
||||
parseModelsString(currentRow?.models || ''),
|
||||
[existingModelsOverride, currentRow?.models]
|
||||
)
|
||||
|
||||
// Categorize models with redirect models
|
||||
@@ -121,26 +127,33 @@ export function FetchModelsDialog({
|
||||
}, [fetchedModelSet, redirectSourceKeysSet, searchKeyword, selectedModels])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && currentRow) {
|
||||
if (open && (currentRow || customFetcher)) {
|
||||
handleFetchModels()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, currentRow?.id])
|
||||
}, [open, currentRow?.id, customFetcher])
|
||||
|
||||
const handleFetchModels = async () => {
|
||||
if (!currentRow) return
|
||||
if (!currentRow && !customFetcher) return
|
||||
|
||||
setIsFetching(true)
|
||||
try {
|
||||
const response = await fetchUpstreamModels(currentRow.id)
|
||||
if (response.success) {
|
||||
const list = Array.isArray(response.data) ? response.data : []
|
||||
if (customFetcher) {
|
||||
const list = await customFetcher()
|
||||
setFetchedModels(list)
|
||||
setSelectedModels(existingModels)
|
||||
toast.success(t('Fetched {{count}} models', { count: list.length }))
|
||||
} else {
|
||||
toast.error(response.message || t('Failed to fetch models'))
|
||||
setFetchedModels([])
|
||||
const response = await fetchUpstreamModels(currentRow!.id)
|
||||
if (response.success) {
|
||||
const list = Array.isArray(response.data) ? response.data : []
|
||||
setFetchedModels(list)
|
||||
setSelectedModels(existingModels)
|
||||
toast.success(t('Fetched {{count}} models', { count: list.length }))
|
||||
} else {
|
||||
toast.error(response.message || t('Failed to fetch models'))
|
||||
setFetchedModels([])
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
@@ -153,8 +166,6 @@ export function FetchModelsDialog({
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentRow) return
|
||||
|
||||
// If onModelsSelected callback is provided, use it (form filling mode)
|
||||
if (onModelsSelected) {
|
||||
onModelsSelected(selectedModels)
|
||||
@@ -164,6 +175,7 @@ export function FetchModelsDialog({
|
||||
}
|
||||
|
||||
// Otherwise, directly save to API (standalone mode)
|
||||
if (!currentRow) return
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const modelsString = selectedModels.join(',')
|
||||
@@ -357,12 +369,16 @@ export function FetchModelsDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Fetch Models')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('Fetch available models for:')}{' '}
|
||||
<strong>{currentRow?.name}</strong>
|
||||
{currentRow
|
||||
? <>
|
||||
{t('Fetch available models for:')}{' '}
|
||||
<strong>{currentRow.name}</strong>
|
||||
</>
|
||||
: t('Fetch available models from upstream')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!currentRow ? (
|
||||
{!currentRow && !customFetcher ? (
|
||||
<div className='text-muted-foreground py-8 text-center'>
|
||||
{t('No channel selected')}
|
||||
</div>
|
||||
|
||||
+36
-55
@@ -301,7 +301,6 @@ export function ChannelMutateDrawer({
|
||||
const { setOpen } = useChannels()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [customModel, setCustomModel] = useState('')
|
||||
const [isFetchingModels, setIsFetchingModels] = useState(false)
|
||||
const [fetchModelsDialogOpen, setFetchModelsDialogOpen] = useState(false)
|
||||
const [channelKey, setChannelKey] = useState<string | null>(null)
|
||||
const [isChannelKeyLoading, setIsChannelKeyLoading] = useState(false)
|
||||
@@ -767,43 +766,29 @@ export function ChannelMutateDrawer({
|
||||
return
|
||||
}
|
||||
|
||||
// For editing mode, open FetchModelsDialog to let user select
|
||||
if (isEditing && currentRow) {
|
||||
setFetchModelsDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// For creation mode, fetch and fill all models
|
||||
const key = form.getValues('key')
|
||||
if (!key?.trim()) {
|
||||
toast.error(t('Please enter API key first'))
|
||||
return
|
||||
}
|
||||
|
||||
setIsFetchingModels(true)
|
||||
try {
|
||||
const response = await fetchModels({
|
||||
type,
|
||||
key,
|
||||
base_url: form.getValues('base_url') || '',
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
updateModels(response.data, true)
|
||||
toast.success(
|
||||
t('Fetched {{count}} model(s) from upstream', {
|
||||
count: response.data.length,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
toast.error(t('No models fetched from upstream'))
|
||||
// For creation mode, validate key before opening dialog
|
||||
if (!isEditing) {
|
||||
const key = form.getValues('key')
|
||||
if (!key?.trim()) {
|
||||
toast.error(t('Please enter API key first'))
|
||||
return
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(getErrorMessage(error) || t('Failed to fetch models'))
|
||||
} finally {
|
||||
setIsFetchingModels(false)
|
||||
}
|
||||
}, [isEditing, currentRow, form, t, updateModels])
|
||||
|
||||
setFetchModelsDialogOpen(true)
|
||||
}, [isEditing, form, t])
|
||||
|
||||
const createModeFetcher = useCallback(async (): Promise<string[]> => {
|
||||
const response = await fetchModels({
|
||||
type: form.getValues('type'),
|
||||
key: form.getValues('key'),
|
||||
base_url: form.getValues('base_url') || '',
|
||||
})
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.message || 'No models fetched from upstream')
|
||||
}, [form])
|
||||
|
||||
// Handle adding custom models
|
||||
const handleAddCustomModels = useCallback(() => {
|
||||
@@ -2234,13 +2219,8 @@ export function ChannelMutateDrawer({
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<Sparkles className='mr-2 h-4 w-4' />
|
||||
)}
|
||||
<Sparkles className='mr-2 h-4 w-4' />
|
||||
{t('Fetch from Upstream')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -3390,19 +3370,20 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fetch Models Dialog (for editing mode) */}
|
||||
{isEditing && currentRow && (
|
||||
<FetchModelsDialog
|
||||
open={fetchModelsDialogOpen}
|
||||
onOpenChange={setFetchModelsDialogOpen}
|
||||
onModelsSelected={(models) => {
|
||||
// Fill selected models to form
|
||||
form.setValue('models', formatModelsArray(models))
|
||||
}}
|
||||
redirectModels={redirectModelList}
|
||||
redirectSourceModels={redirectModelKeyList}
|
||||
/>
|
||||
)}
|
||||
{/* Fetch Models Dialog */}
|
||||
<FetchModelsDialog
|
||||
open={fetchModelsDialogOpen}
|
||||
onOpenChange={setFetchModelsDialogOpen}
|
||||
onModelsSelected={(models) => {
|
||||
form.setValue('models', formatModelsArray(models))
|
||||
}}
|
||||
redirectModels={redirectModelList}
|
||||
redirectSourceModels={redirectModelKeyList}
|
||||
customFetcher={!isEditing ? createModeFetcher : undefined}
|
||||
existingModelsOverride={
|
||||
!isEditing ? parseModelsString(form.getValues('models') || '') : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<SecureVerificationDialog
|
||||
open={verificationOpen}
|
||||
|
||||
Reference in New Issue
Block a user