🛠️ fix: v1 interface feedback regressions
Resolve verified V1 frontend feedback by improving channel workflows, auth behavior, API key interactions, user filtering, layout persistence, subscription quota handling, i18n text, pricing metadata, and stale frontend cache recovery. - Add a global frontend cache version cleanup to prevent old frontend localStorage from causing page errors after upgrades. - Fix channel copy refresh, model mapping input focus loss, create-channel fetch-model title state, upstream model update confirmation, and batch test toast behavior. - Respect password login settings and improve Turnstile, forgot-password, registration, and invite-link flows. - Make user role/status filtering server-side and preserve table page size in URLs. - Improve API key edit validation feedback and prefetch real keys for reliable copy actions. - Fix rankings access fail-open behavior, double scrollbars, subscription received amount conversion/display, token i18n wording, model deletion confirmation grammar, and Claude pricing context inference. - Add clearer Playground model/group loading errors. Validation: - bun run typecheck - bun run i18n:sync - gofmt on modified Go files - go test ./controller ./model -run '^$'
This commit is contained in:
+40
-8
@@ -28,6 +28,7 @@ import {
|
||||
} from '@tanstack/react-table'
|
||||
import { Check, Copy, Info, Loader2, Settings } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -302,11 +303,12 @@ export function ChannelTestDialog({
|
||||
}, [])
|
||||
|
||||
const testSingleModel = useCallback(
|
||||
async (model: string) => {
|
||||
async (model: string, silent = false): Promise<TestResult | undefined> => {
|
||||
if (!currentRow) return
|
||||
|
||||
markModelTesting(model, true)
|
||||
updateTestResult(model, { status: 'testing' })
|
||||
let finalResult: TestResult | undefined
|
||||
|
||||
try {
|
||||
await handleTestChannel(
|
||||
@@ -315,24 +317,28 @@ export function ChannelTestDialog({
|
||||
testModel: model,
|
||||
endpointType: endpointType === 'auto' ? undefined : endpointType,
|
||||
stream: isStreamTest || undefined,
|
||||
silent,
|
||||
},
|
||||
(success, responseTime, error, errorCode) => {
|
||||
updateTestResult(model, {
|
||||
finalResult = {
|
||||
status: success ? 'success' : 'error',
|
||||
responseTime,
|
||||
error,
|
||||
errorCode,
|
||||
})
|
||||
}
|
||||
updateTestResult(model, finalResult)
|
||||
}
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
updateTestResult(model, {
|
||||
finalResult = {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : t('Test failed'),
|
||||
})
|
||||
}
|
||||
updateTestResult(model, finalResult)
|
||||
} finally {
|
||||
markModelTesting(model, false)
|
||||
}
|
||||
return finalResult
|
||||
},
|
||||
[
|
||||
currentRow,
|
||||
@@ -350,15 +356,41 @@ export function ChannelTestDialog({
|
||||
|
||||
setIsBatchTesting(true)
|
||||
try {
|
||||
await Promise.allSettled(
|
||||
modelsToTest.map((modelName) => testSingleModel(modelName))
|
||||
const settled = await Promise.allSettled(
|
||||
modelsToTest.map((modelName) => testSingleModel(modelName, true))
|
||||
)
|
||||
const results = settled
|
||||
.map((result) =>
|
||||
result.status === 'fulfilled' ? result.value : undefined
|
||||
)
|
||||
.filter((result): result is TestResult => Boolean(result))
|
||||
const successCount = results.filter(
|
||||
(result) => result.status === 'success'
|
||||
).length
|
||||
const failedCount = modelsToTest.length - successCount
|
||||
if (failedCount > 0) {
|
||||
toast.error(
|
||||
t(
|
||||
'Batch test completed: {{success}} succeeded, {{failed}} failed',
|
||||
{
|
||||
success: successCount,
|
||||
failed: failedCount,
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
toast.success(
|
||||
t('Batch test completed: {{count}} succeeded', {
|
||||
count: successCount,
|
||||
})
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
setIsBatchTesting(false)
|
||||
setRowSelection({})
|
||||
}
|
||||
},
|
||||
[testSingleModel]
|
||||
[t, testSingleModel]
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
+21
-12
@@ -67,6 +67,7 @@ type FetchModelsDialogProps = {
|
||||
redirectSourceModels?: string[]
|
||||
customFetcher?: () => Promise<string[]>
|
||||
existingModelsOverride?: string[]
|
||||
channelName?: string | null
|
||||
}
|
||||
|
||||
export function FetchModelsDialog({
|
||||
@@ -77,9 +78,11 @@ export function FetchModelsDialog({
|
||||
redirectSourceModels = [],
|
||||
customFetcher,
|
||||
existingModelsOverride,
|
||||
channelName,
|
||||
}: FetchModelsDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const { currentRow } = useChannels()
|
||||
const activeChannel = customFetcher ? null : currentRow
|
||||
const queryClient = useQueryClient()
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@@ -89,8 +92,9 @@ export function FetchModelsDialog({
|
||||
|
||||
// Parse existing models
|
||||
const existingModels = useMemo(
|
||||
() => existingModelsOverride ?? parseModelsString(currentRow?.models || ''),
|
||||
[existingModelsOverride, currentRow?.models]
|
||||
() =>
|
||||
existingModelsOverride ?? parseModelsString(activeChannel?.models || ''),
|
||||
[existingModelsOverride, activeChannel?.models]
|
||||
)
|
||||
|
||||
// Categorize models with redirect models
|
||||
@@ -125,14 +129,14 @@ export function FetchModelsDialog({
|
||||
}, [fetchedModelSet, redirectSourceKeysSet, searchKeyword, selectedModels])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && (currentRow || customFetcher)) {
|
||||
if (open && (activeChannel || customFetcher)) {
|
||||
handleFetchModels()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, currentRow?.id, customFetcher])
|
||||
}, [open, activeChannel?.id, customFetcher])
|
||||
|
||||
const handleFetchModels = async () => {
|
||||
if (!currentRow && !customFetcher) return
|
||||
if (!activeChannel && !customFetcher) return
|
||||
|
||||
setIsFetching(true)
|
||||
try {
|
||||
@@ -142,7 +146,7 @@ export function FetchModelsDialog({
|
||||
setSelectedModels(existingModels)
|
||||
toast.success(t('Fetched {{count}} models', { count: list.length }))
|
||||
} else {
|
||||
const response = await fetchUpstreamModels(currentRow!.id)
|
||||
const response = await fetchUpstreamModels(activeChannel!.id)
|
||||
if (response.success) {
|
||||
const list = Array.isArray(response.data) ? response.data : []
|
||||
setFetchedModels(list)
|
||||
@@ -173,11 +177,11 @@ export function FetchModelsDialog({
|
||||
}
|
||||
|
||||
// Otherwise, directly save to API (standalone mode)
|
||||
if (!currentRow) return
|
||||
if (!activeChannel) return
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const modelsString = selectedModels.join(',')
|
||||
const response = await updateChannel(currentRow.id, {
|
||||
const response = await updateChannel(activeChannel.id, {
|
||||
models: modelsString,
|
||||
})
|
||||
if (response.success) {
|
||||
@@ -367,10 +371,15 @@ export function FetchModelsDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Fetch Models')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{currentRow ? (
|
||||
{activeChannel ? (
|
||||
<>
|
||||
{t('Fetch available models for:')}{' '}
|
||||
<strong>{currentRow.name}</strong>
|
||||
<strong>{activeChannel.name}</strong>
|
||||
</>
|
||||
) : channelName ? (
|
||||
<>
|
||||
{t('Fetch available models for:')}{' '}
|
||||
<strong>{channelName}</strong>
|
||||
</>
|
||||
) : (
|
||||
t('Fetch available models from upstream')
|
||||
@@ -378,7 +387,7 @@ export function FetchModelsDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!currentRow && !customFetcher ? (
|
||||
{!activeChannel && !customFetcher ? (
|
||||
<div className='text-muted-foreground py-8 text-center'>
|
||||
{t('No channel selected')}
|
||||
</div>
|
||||
@@ -413,7 +422,7 @@ export function FetchModelsDialog({
|
||||
|
||||
{/* Tabs for New vs Existing vs Removed */}
|
||||
<Tabs
|
||||
key={`${currentRow?.id}-${fetchedModels.length}-${removedModels.length}`}
|
||||
key={`${activeChannel?.id ?? 'custom'}-${fetchedModels.length}-${removedModels.length}`}
|
||||
defaultValue={
|
||||
newModels.length > 0
|
||||
? 'new'
|
||||
|
||||
+3
-2
@@ -107,7 +107,7 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
||||
const anyAdd = selectedAddArr.length > 0
|
||||
const anyRemove = selectedRemoveArr.length > 0
|
||||
|
||||
if (hasAdd && hasRemove && (!anyAdd || !anyRemove)) {
|
||||
if (hasAdd && hasRemove && anyAdd !== anyRemove) {
|
||||
setPartialConfirmOpen(true)
|
||||
return
|
||||
}
|
||||
@@ -278,7 +278,8 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
||||
onClick={handleConfirm}
|
||||
disabled={
|
||||
props.confirmLoading ||
|
||||
(selectedAdd.size === 0 && selectedRemove.size === 0)
|
||||
(props.addModels.length === 0 &&
|
||||
props.removeModels.length === 0)
|
||||
}
|
||||
>
|
||||
{t('Confirm')}
|
||||
|
||||
Reference in New Issue
Block a user