/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import type { QueryClient } from '@tanstack/react-query'
import i18next from 'i18next'
import { toast } from 'sonner'
import { formatCurrencyFromUSD } from '@/lib/currency'
import {
copyChannel,
deleteChannel,
testChannel,
updateChannel,
updateChannelStatus,
batchUpdateChannelStatus,
batchDeleteChannels,
batchSetChannelTag,
enableTagChannels,
disableTagChannels,
deleteDisabledChannels,
fixChannelAbilities,
editTagChannels,
testAllChannels,
updateAllChannelsBalance,
updateChannelBalance,
} from '../api'
import { CHANNEL_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
import type { ChannelTestResponse, CopyChannelParams } from '../types'
// ============================================================================
// Query Keys
// ============================================================================
export const channelsQueryKeys = {
all: ['channels'] as const,
lists: () => [...channelsQueryKeys.all, 'list'] as const,
list: (params: Record) =>
[...channelsQueryKeys.lists(), params] as const,
details: () => [...channelsQueryKeys.all, 'detail'] as const,
detail: (id: number) => [...channelsQueryKeys.details(), id] as const,
}
function getChannelTestResponseTime(
response: ChannelTestResponse
): number | undefined {
const responseTime = response.data?.response_time
if (typeof responseTime === 'number' && Number.isFinite(responseTime)) {
return responseTime
}
if (
typeof response.time === 'number' &&
Number.isFinite(response.time) &&
response.time > 0
) {
return Math.round(response.time * 1000)
}
return undefined
}
function formatChannelTestDuration(responseTime?: number): string | undefined {
if (responseTime === undefined) return undefined
if (responseTime >= 1000) {
return `${(responseTime / 1000).toFixed(2)} s`
}
return `${Math.max(1, Math.round(responseTime))} ms`
}
function getChannelTestLabel(options?: {
channelName?: string
testModel?: string
}): string {
const channelName = options?.channelName?.trim()
const testModel = options?.testModel?.trim()
if (channelName && testModel) {
return i18next.t('Channel {{name}} model {{model}}', {
name: channelName,
model: testModel,
})
}
if (channelName) {
return i18next.t('Channel {{name}}', { name: channelName })
}
if (testModel) {
return i18next.t('Model {{model}}', { model: testModel })
}
return i18next.t('Channel')
}
// ============================================================================
// Single Channel Actions
// ============================================================================
/**
* Enable a channel
*/
export async function handleEnableChannel(
id: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await updateChannelStatus(id, CHANNEL_STATUS.ENABLED)
if (response.success) {
toast.success(i18next.t(SUCCESS_MESSAGES.ENABLED))
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
}
/**
* Disable a channel
*/
export async function handleDisableChannel(
id: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await updateChannelStatus(
id,
CHANNEL_STATUS.MANUAL_DISABLED
)
if (response.success) {
toast.success(i18next.t(SUCCESS_MESSAGES.DISABLED))
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
}
/**
* Toggle channel status (enable/disable)
*/
export async function handleToggleChannelStatus(
id: number,
currentStatus: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
if (currentStatus === CHANNEL_STATUS.ENABLED) {
await handleDisableChannel(id, queryClient, onSuccess)
} else {
await handleEnableChannel(id, queryClient, onSuccess)
}
}
/**
* Delete a channel
*/
export async function handleDeleteChannel(
id: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await deleteChannel(id)
if (response.success) {
toast.success(i18next.t(SUCCESS_MESSAGES.DELETED))
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.DELETE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
}
}
/**
* Update a specific channel field (e.g., priority, weight)
*/
export async function handleUpdateChannelField(
id: number,
fieldName: string,
value: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await updateChannel(id, { [fieldName]: value })
if (response.success) {
// Show success toast with field name
const fieldLabel =
fieldName.charAt(0).toUpperCase() + fieldName.slice(1).toLowerCase()
toast.success(
i18next.t('{{field}} updated to {{value}}', {
field: fieldLabel,
value,
})
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
}
/**
* Update a specific field for all channels with a tag
*/
export async function handleUpdateTagField(
tag: string,
fieldName: 'priority' | 'weight',
value: number,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const params = { tag, [fieldName]: value }
const response = await editTagChannels(params)
if (response.success) {
// Show success toast with field name
const fieldLabel =
fieldName.charAt(0).toUpperCase() + fieldName.slice(1).toLowerCase()
toast.success(
i18next.t('{{field}} updated to {{value}} for tag: {{tag}}', {
field: fieldLabel,
value,
tag,
})
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
}
}
/**
* Test channel connectivity
*/
export async function handleTestChannel(
id: number,
options?: {
channelName?: string
testModel?: string
endpointType?: string
stream?: boolean
silent?: boolean
},
onTestComplete?: (
success: boolean,
responseTime?: number,
error?: string,
errorCode?: string
) => void
): Promise {
const payload =
options && (options.testModel || options.endpointType || options.stream)
? {
...(options.testModel ? { model: options.testModel } : {}),
...(options.endpointType
? { endpoint_type: options.endpointType }
: {}),
...(options.stream ? { stream: true } : {}),
}
: undefined
try {
const response = await testChannel(id, payload)
const responseTime = getChannelTestResponseTime(response)
const duration = formatChannelTestDuration(responseTime)
const target = getChannelTestLabel(options)
if (response.success) {
if (!options?.silent) {
toast.success(
i18next.t('{{target}} test succeeded', { target }),
duration
? {
description: i18next.t('Response time: {{duration}}', {
duration,
}),
}
: undefined
)
}
onTestComplete?.(true, responseTime)
} else {
const errorMsg = response.message || i18next.t(ERROR_MESSAGES.TEST_FAILED)
if (!options?.silent) {
toast.error(i18next.t('{{target}} test failed', { target }), {
description: response.error_code
? `${errorMsg} (${response.error_code})`
: errorMsg,
})
}
onTestComplete?.(false, responseTime, errorMsg, response.error_code)
}
} catch (_error: unknown) {
const err = _error as { response?: { data?: { message?: string } } }
const errorMsg =
err?.response?.data?.message || i18next.t(ERROR_MESSAGES.TEST_FAILED)
const target = getChannelTestLabel(options)
if (!options?.silent) {
toast.error(i18next.t('{{target}} test failed', { target }), {
description: errorMsg,
})
}
onTestComplete?.(false, undefined, errorMsg)
}
}
/**
* Copy a channel
*/
export async function handleCopyChannel(
id: number,
params: CopyChannelParams,
queryClient?: QueryClient,
onSuccess?: (newId: number) => void
): Promise {
try {
const response = await copyChannel(id, params)
if (response.success) {
toast.success(i18next.t(SUCCESS_MESSAGES.COPIED))
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.(response.data?.id ?? 0)
} else {
toast.error(response.message || i18next.t('Failed to copy channel'))
}
} catch {
toast.error(i18next.t('Failed to copy channel'))
}
}
/**
* Update channel balance
*/
export async function handleUpdateChannelBalance(
id: number,
queryClient?: QueryClient,
onSuccess?: (balance: number) => void
): Promise {
try {
const response = await updateChannelBalance(id)
if (response.success && response.balance !== undefined) {
const balance = response.balance
toast.success(
i18next.t('Balance updated: {{balance}}', {
balance: formatCurrencyFromUSD(balance, {
digitsLarge: 2,
digitsSmall: 4,
abbreviate: false,
}),
})
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.(balance)
} else {
toast.error(response.message || i18next.t('Failed to update balance'))
}
} catch (_error: unknown) {
toast.error(
_error instanceof Error
? _error.message
: i18next.t('Failed to update balance')
)
}
}
// ============================================================================
// Batch Actions
// ============================================================================
/**
* Batch delete channels
*/
export async function handleBatchDelete(
ids: number[],
queryClient?: QueryClient,
onSuccess?: (deletedCount: number) => void
): Promise {
if (ids.length === 0) {
toast.error(i18next.t('No channels selected'))
return
}
try {
const response = await batchDeleteChannels({ ids })
if (response.success) {
toast.success(
i18next.t('{{count}} channel(s) deleted', {
count: response.data || ids.length,
})
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.(response.data || ids.length)
} else {
toast.error(response.message || i18next.t(ERROR_MESSAGES.DELETE_FAILED))
}
} catch {
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
}
}
/**
* Batch enable channels
*/
export async function handleBatchEnable(
ids: number[],
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
if (ids.length === 0) {
toast.error(i18next.t('No channels selected'))
return
}
try {
const response = await batchUpdateChannelStatus(ids, CHANNEL_STATUS.ENABLED)
const successCount = response.success ? response.data || 0 : 0
const failCount = ids.length - successCount
if (successCount > 0) {
toast.success(
i18next.t('{{count}} channel(s) enabled', { count: successCount })
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
}
if (!response.success) {
toast.error(response.message || i18next.t('Failed to enable channels'))
} else if (failCount > 0) {
toast.error(
i18next.t('{{count}} channel(s) failed to enable', { count: failCount })
)
}
} catch {
toast.error(i18next.t('Failed to enable channels'))
}
}
/**
* Batch disable channels
*/
export async function handleBatchDisable(
ids: number[],
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
if (ids.length === 0) {
toast.error(i18next.t('No channels selected'))
return
}
try {
const response = await batchUpdateChannelStatus(
ids,
CHANNEL_STATUS.MANUAL_DISABLED
)
const successCount = response.success ? response.data || 0 : 0
const failCount = ids.length - successCount
if (successCount > 0) {
toast.success(
i18next.t('{{count}} channel(s) disabled', { count: successCount })
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
}
if (!response.success) {
toast.error(response.message || i18next.t('Failed to disable channels'))
} else if (failCount > 0) {
toast.error(
i18next.t('{{count}} channel(s) failed to disable', {
count: failCount,
})
)
}
} catch {
toast.error(i18next.t('Failed to disable channels'))
}
}
/**
* Batch set tag
*/
export async function handleBatchSetTag(
ids: number[],
tag: string | null,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
if (ids.length === 0) {
toast.error(i18next.t('No channels selected'))
return
}
try {
const response = await batchSetChannelTag({ ids, tag })
if (response.success) {
toast.success(i18next.t(SUCCESS_MESSAGES.TAG_SET))
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(response.message || i18next.t('Failed to set tag'))
}
} catch {
toast.error(i18next.t('Failed to set tag'))
}
}
// ============================================================================
// Tag-Based Actions
// ============================================================================
/**
* Enable all channels with a tag
*/
export async function handleEnableTagChannels(
tag: string,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await enableTagChannels(tag)
if (response.success) {
toast.success(
i18next.t('Enabled all channels with tag: {{tag}}', { tag })
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(
response.message || i18next.t('Failed to enable tag channels')
)
}
} catch {
toast.error(i18next.t('Failed to enable tag channels'))
}
}
/**
* Disable all channels with a tag
*/
export async function handleDisableTagChannels(
tag: string,
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await disableTagChannels(tag)
if (response.success) {
toast.success(
i18next.t('Disabled all channels with tag: {{tag}}', { tag })
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(
response.message || i18next.t('Failed to disable tag channels')
)
}
} catch {
toast.error(i18next.t('Failed to disable tag channels'))
}
}
// ============================================================================
// System Actions
// ============================================================================
/**
* Delete all disabled channels
*/
export async function handleDeleteAllDisabled(
queryClient?: QueryClient,
onSuccess?: (deletedCount: number) => void
): Promise {
try {
const response = await deleteDisabledChannels()
if (response.success) {
toast.success(
i18next.t('{{count}} disabled channel(s) deleted', {
count: response.data || 0,
})
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.(response.data || 0)
} else {
toast.error(
response.message || i18next.t('Failed to delete disabled channels')
)
}
} catch {
toast.error(i18next.t('Failed to delete disabled channels'))
}
}
/**
* Repair channel consistency
*/
export async function handleFixAbilities(
queryClient?: QueryClient,
onSuccess?: (result: { success: number; fails: number }) => void
): Promise {
try {
const response = await fixChannelAbilities()
if (response.success && response.data) {
toast.success(
i18next.t(
'Channel consistency repaired: {{success}} succeeded, {{fails}} failed',
{
success: response.data.success,
fails: response.data.fails,
}
)
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.(response.data)
} else {
toast.error(
response.message || i18next.t('Failed to repair channel consistency')
)
}
} catch {
toast.error(i18next.t('Failed to repair channel consistency'))
}
}
/**
* Test all enabled channels
*/
export async function handleTestAllChannels(
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await testAllChannels()
if (response.success) {
toast.success(
i18next.t(
'Testing all enabled channels started. Please refresh to see results.'
)
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(
response.message || i18next.t('Failed to start testing all channels')
)
}
} catch {
toast.error(i18next.t('Failed to test all channels'))
}
}
/**
* Update balance for all enabled channels
*/
export async function handleUpdateAllBalances(
queryClient?: QueryClient,
onSuccess?: () => void
): Promise {
try {
const response = await updateAllChannelsBalance()
if (response.success) {
toast.success(
i18next.t(
'Updating all channel balances. This may take a while. Please refresh to see results.'
)
)
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
onSuccess?.()
} else {
toast.error(
response.message || i18next.t('Failed to update all balances')
)
}
} catch {
toast.error(i18next.t('Failed to update all balances'))
}
}