/* 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 { Bell, Loader2, Mail, Server, Webhook } from 'lucide-react' import { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { PasswordInput } from '@/components/password-input' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { ROLE } from '@/lib/roles' import { updateUserSettings } from '../../api' import { DEFAULT_QUOTA_WARNING_THRESHOLD, NOTIFICATION_METHODS, } from '../../constants' import { parseUserSettings } from '../../lib' import type { UserProfile, UserSettings, NotifyType } from '../../types' const NOTIFICATION_ICONS: Record = { email: Mail, webhook: Webhook, bark: Bell, gotify: Server, } const NOTIFICATION_VALUES = new Set( NOTIFICATION_METHODS.map((method) => method.value) ) function normalizeNotifyType(value: unknown): NotifyType { return typeof value === 'string' && NOTIFICATION_VALUES.has(value as NotifyType) ? (value as NotifyType) : 'email' } // ============================================================================ // Settings Tab Component // ============================================================================ interface NotificationTabProps { profile: UserProfile | null onUpdate: () => void } export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { const { t } = useTranslation() const isAdmin = (profile?.role ?? 0) >= ROLE.ADMIN const [loading, setLoading] = useState(false) const [settings, setSettings] = useState({ notify_type: 'email', quota_warning_threshold: DEFAULT_QUOTA_WARNING_THRESHOLD, notification_email: '', webhook_url: '', webhook_secret: '', bark_url: '', gotify_url: '', gotify_token: '', gotify_priority: 5, accept_unset_model_ratio_model: false, record_ip_log: false, upstream_model_update_notify_enabled: false, }) // Update form field helper const updateField = useCallback( (field: K, value: UserSettings[K]) => { setSettings((prev) => ({ ...prev, [field]: value })) }, [] ) useEffect(() => { if (profile?.setting) { const parsed = parseUserSettings(profile.setting) setSettings({ notify_type: normalizeNotifyType(parsed.notify_type), quota_warning_threshold: parsed.quota_warning_threshold ?? DEFAULT_QUOTA_WARNING_THRESHOLD, notification_email: parsed.notification_email ?? '', webhook_url: parsed.webhook_url ?? '', webhook_secret: parsed.webhook_secret ?? '', bark_url: parsed.bark_url ?? '', gotify_url: parsed.gotify_url ?? '', gotify_token: parsed.gotify_token ?? '', gotify_priority: parsed.gotify_priority ?? 5, accept_unset_model_ratio_model: parsed.accept_unset_model_ratio_model || false, record_ip_log: parsed.record_ip_log || false, upstream_model_update_notify_enabled: parsed.upstream_model_update_notify_enabled || false, }) } }, [profile]) const handleSave = async () => { try { setLoading(true) const response = await updateUserSettings(settings) if (response.success) { toast.success(t('Settings updated successfully')) onUpdate() } else { toast.error(response.message || t('Failed to update settings')) } } catch (_error) { toast.error(t('Failed to update settings')) } finally { setLoading(false) } } const notifyType = normalizeNotifyType(settings.notify_type) return (
{/* Notification Type */}
{ const nextValue = value.find((item) => item !== notifyType) if (nextValue) updateField('notify_type', normalizeNotifyType(nextValue)) }} aria-label={t('Notification Method')} variant='outline' size='lg' spacing={2} className='grid w-full grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3' > {NOTIFICATION_METHODS.map((method) => { const Icon = NOTIFICATION_ICONS[method.value] return ( {t(method.label)} ) })}
{/* Warning Threshold */}
updateField('quota_warning_threshold', Number(e.target.value)) } placeholder={t('Enter threshold')} />

{t('Get notified when balance falls below this value')}

{/* Email Settings */} {notifyType === 'email' && (
updateField('notification_email', e.target.value)} placeholder={t('Leave empty to use account email')} />
)} {/* Webhook Settings */} {notifyType === 'webhook' && ( <>
updateField('webhook_url', e.target.value)} placeholder={t('https://example.com/webhook')} />
updateField('webhook_secret', e.target.value)} placeholder={t('Enter secret key')} />
)} {/* Bark Settings */} {notifyType === 'bark' && (
updateField('bark_url', e.target.value)} placeholder={t('https://api.day.app/yourkey/{{title}}/{{content}}')} />

{t('Template variables:')} {'{{title}}'}, {'{{content}}'}

)} {/* Gotify Settings */} {notifyType === 'gotify' && ( <>
updateField('gotify_url', e.target.value)} placeholder={t('https://gotify.example.com')} />

{t('Enter the full URL of your Gotify server')}

updateField('gotify_token', e.target.value)} placeholder={t('Enter application token')} />

{t('Token obtained from your Gotify application')}

updateField('gotify_priority', Number(e.target.value)) } placeholder='5' />

{t( 'Priority level from 0 (lowest) to 10 (highest), default is 5' )}

{t('Setup Instructions')}
  1. {t('1. Create an application in your Gotify server')}
  2. {t('2. Copy the application token')}
  3. {t('3. Enter your Gotify server URL and token above')}

{t('Learn more:')}{' '} {t('Gotify Documentation')}

)} {/* Divider */}
{/* Preferences Section */}

{t('Preferences')}

{t('Configure your account behavior preferences')}

{/* Receive Upstream Model Update Notifications (admin only) */} {isAdmin && (

{t( 'Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.' )}

updateField('upstream_model_update_notify_enabled', checked) } />
)} {/* Accept Unset Model Price */}

{t('Allow using models without price configuration')}

updateField('accept_unset_model_ratio_model', checked) } />
{/* Record IP Log */}

{t('Log IP address for usage and error logs')}

updateField('record_ip_log', checked)} />
{/* Save Button */}
) }