/* 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 { useCallback, useState } from 'react' import { type Row } from '@tanstack/react-table' import { Trash2, Edit, Power, PowerOff, ExternalLink, ArrowRightLeft, Copy, Link, Loader2, MoreHorizontal as DotsHorizontalIcon, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { copyToClipboard } from '@/lib/copy-to-clipboard' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { useChatPresets } from '@/features/chat/hooks/use-chat-presets' import { resolveChatUrl, type ChatPreset } from '@/features/chat/lib/chat-links' import { sendToFluent } from '@/features/chat/lib/send-to-fluent' import { updateApiKeyStatus } from '../api' import { API_KEY_STATUS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants' import { apiKeySchema } from '../types' import { useApiKeys } from './api-keys-provider' function getServerAddress(): string { try { const raw = localStorage.getItem('status') if (raw) { const status = JSON.parse(raw) if (status.server_address) return status.server_address as string } } catch { /* empty */ } return window.location.origin } function encodeConnectionString(key: string, url: string): string { return JSON.stringify({ _type: 'newapi_channel_conn', key, url, }) } type DataTableRowActionsProps = { row: Row } export function DataTableRowActions({ row, }: DataTableRowActionsProps) { const { t } = useTranslation() const apiKey = apiKeySchema.parse(row.original) const { setOpen, setCurrentRow, triggerRefresh, setResolvedKey, resolveRealKey, resolvedKeys, loadingKeys, } = useApiKeys() const isEnabled = apiKey.status === API_KEY_STATUS.ENABLED const { chatPresets, serverAddress } = useChatPresets() const [isTogglingStatus, setIsTogglingStatus] = useState(false) const resolvedRealKey = resolvedKeys[apiKey.id] const isRealKeyLoading = Boolean(loadingKeys[apiKey.id]) const hasChatPresets = chatPresets.length > 0 const handleMenuOpenChange = useCallback( (open: boolean) => { if (open && !resolvedRealKey && !isRealKeyLoading) { void resolveRealKey(apiKey.id) } }, [apiKey.id, isRealKeyLoading, resolvedRealKey, resolveRealKey] ) const getCachedRealKey = useCallback(() => { if (resolvedRealKey) return resolvedRealKey void resolveRealKey(apiKey.id) toast.info(t('API key is loading, please try again in a moment')) return null }, [apiKey.id, resolvedRealKey, resolveRealKey, t]) const handleOpenChatPreset = useCallback( async (preset: ChatPreset) => { const realKey = await resolveRealKey(apiKey.id) if (!realKey) return if (preset.type === 'fluent') { const success = sendToFluent(realKey, serverAddress) if (success) { toast.success(t('Sent the API key to FluentRead.')) } else { toast.info( t( 'FluentRead extension not detected. Please ensure it is installed and active.' ) ) } return } const resolvedUrl = resolveChatUrl({ template: preset.url, apiKey: realKey, serverAddress, }) if (!resolvedUrl) { toast.error(t('Invalid chat link. Please contact your administrator.')) return } if (typeof window === 'undefined') return try { window.open(resolvedUrl, '_blank', 'noopener') } catch { window.location.href = resolvedUrl } }, [resolveRealKey, apiKey.id, serverAddress, t] ) const handleToggleStatus = async ( e?: React.MouseEvent ) => { e?.stopPropagation() const newStatus = isEnabled ? API_KEY_STATUS.DISABLED : API_KEY_STATUS.ENABLED setIsTogglingStatus(true) try { const result = await updateApiKeyStatus(apiKey.id, newStatus) if (result.success) { const message = isEnabled ? t(SUCCESS_MESSAGES.API_KEY_DISABLED) : t(SUCCESS_MESSAGES.API_KEY_ENABLED) toast.success(message) triggerRefresh() } else { toast.error(result.message || t(ERROR_MESSAGES.STATUS_UPDATE_FAILED)) } } catch { toast.error(t(ERROR_MESSAGES.UNEXPECTED)) } finally { setIsTogglingStatus(false) } } return (
} > {isTogglingStatus ? ( ) : isEnabled ? ( ) : ( )} {isEnabled ? t('Disable') : t('Enable')} } > {t('Open menu')} { const realKey = getCachedRealKey() if (!realKey) return const ok = await copyToClipboard(realKey) if (ok) toast.success(t('Copied')) }} > {t('Copy Key')} { const realKey = getCachedRealKey() if (!realKey) return const connStr = encodeConnectionString( realKey, getServerAddress() ) const ok = await copyToClipboard(connStr) if (ok) toast.success(t('Copied')) }} > {t('Copy Connection Info')} { setCurrentRow(apiKey) setOpen('update') }} > {t('Edit')} { const realKey = await resolveRealKey(apiKey.id) if (!realKey) return setResolvedKey(realKey) setCurrentRow(apiKey) setOpen('cc-switch') }} > {t('CC Switch')} {hasChatPresets && ( {t('Chat')} {chatPresets.map((preset) => ( handleOpenChatPreset(preset)} > {preset.name} {preset.type !== 'web' && ( )} ))} )} { setCurrentRow(apiKey) setOpen('delete') }} className='text-destructive focus:text-destructive' > {t('Delete')}
) }