diff --git a/web/default/src/components/truncated-text.tsx b/web/default/src/components/truncated-text.tsx new file mode 100644 index 00000000..75cf7d7c --- /dev/null +++ b/web/default/src/components/truncated-text.tsx @@ -0,0 +1,38 @@ +import { cn } from '@/lib/utils' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' + +interface TruncatedTextProps { + text: string + className?: string + maxWidth?: string + side?: 'top' | 'bottom' | 'left' | 'right' +} + +export function TruncatedText({ + text, + className, + maxWidth = 'max-w-[200px]', + side = 'top', +}: TruncatedTextProps) { + return ( + + + + } + > + {text} + + + {text} + + + + ) +} diff --git a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx index c0a7e725..1b4e2348 100644 --- a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx +++ b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx @@ -313,7 +313,7 @@ export function UserAuthForm({ {t('Forgot password?')} diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 1af1cd3e..ee6a5d36 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -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[] {
- {truncateText(name, 30)} + {isPassThrough && ( diff --git a/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx b/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx index ebf8170e..180fdb9a 100644 --- a/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx @@ -65,6 +65,8 @@ type FetchModelsDialogProps = { onModelsSelected?: (models: string[]) => void redirectModels?: string[] redirectSourceModels?: string[] + customFetcher?: () => Promise + 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({ {t('Fetch Models')} - {t('Fetch available models for:')}{' '} - {currentRow?.name} + {currentRow + ? <> + {t('Fetch available models for:')}{' '} + {currentRow.name} + + : t('Fetch available models from upstream')} - {!currentRow ? ( + {!currentRow && !customFetcher ? (
{t('No channel selected')}
diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 899dd42c..6123480a 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -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(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 => { + 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 ? ( - - ) : ( - - )} + {t('Fetch from Upstream')} )} @@ -3390,19 +3370,20 @@ export function ChannelMutateDrawer({ /> )} - {/* Fetch Models Dialog (for editing mode) */} - {isEditing && currentRow && ( - { - // Fill selected models to form - form.setValue('models', formatModelsArray(models)) - }} - redirectModels={redirectModelList} - redirectSourceModels={redirectModelKeyList} - /> - )} + {/* Fetch Models Dialog */} + { + form.setValue('models', formatModelsArray(models)) + }} + redirectModels={redirectModelList} + redirectSourceModels={redirectModelKeyList} + customFetcher={!isEditing ? createModeFetcher : undefined} + existingModelsOverride={ + !isEditing ? parseModelsString(form.getValues('models') || '') : undefined + } + /> [] { {sensitiveVisible ? getUserAvatarFallback(log.username) : '•'} - - {sensitiveVisible ? log.username : '••••'} - + + + + } + > + {sensitiveVisible ? log.username : '••••'} + + {sensitiveVisible && log.username.length > 12 && ( + + {log.username} + + )} + + ) }, @@ -468,15 +481,30 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { if (groupRatioText) metaParts.push(groupRatioText) return ( -
- +
+ + + + } + > + + + {sensitiveVisible && tokenName.length > 16 && ( + + {tokenName} + + )} + + {metaParts.length > 0 && ( {metaParts.join(' · ')} @@ -486,7 +514,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, meta: { label: t('Token') }, - size: 130, + size: 160, }) columns.push( diff --git a/web/default/src/features/users/components/users-table.tsx b/web/default/src/features/users/components/users-table.tsx index 65f61020..310a57f1 100644 --- a/web/default/src/features/users/components/users-table.tsx +++ b/web/default/src/features/users/components/users-table.tsx @@ -187,11 +187,13 @@ export function UsersTable() { columnId: 'status', title: t('Status'), options: getUserStatusOptions(t), + singleSelect: true, }, { columnId: 'role', title: t('Role'), options: getUserRoleOptions(t), + singleSelect: true, }, ], }} diff --git a/web/default/src/features/wallet/lib/affiliate.ts b/web/default/src/features/wallet/lib/affiliate.ts index da6556b2..3e4a12f8 100644 --- a/web/default/src/features/wallet/lib/affiliate.ts +++ b/web/default/src/features/wallet/lib/affiliate.ts @@ -25,5 +25,5 @@ For commercial licensing, please contact support@quantumnous.com */ export function generateAffiliateLink(affCode: string): string { if (typeof window === 'undefined') return '' - return `${window.location.origin}/register?aff=${affCode}` + return `${window.location.origin}/sign-up?aff=${affCode}` }