/* 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 { useQueryClient } from '@tanstack/react-query' import { Loader2, RefreshCw } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Dialog } from '@/components/dialog' import { StatusBadge } from '@/components/status-badge' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { useIsMobile } from '@/hooks/use-mobile' import { cn } from '@/lib/utils' import { syncUpstream, previewUpstreamDiff } from '../../api' import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants' import { modelsQueryKeys, vendorsQueryKeys } from '../../lib' import type { SyncLocale, SyncSource } from '../../types' import { useModels } from '../models-provider' type SyncWizardDialogProps = { open: boolean onOpenChange: (open: boolean) => void } export function SyncWizardDialog({ open, onOpenChange, }: SyncWizardDialogProps) { const { t } = useTranslation() const queryClient = useQueryClient() const { setOpen, setUpstreamConflicts, setSyncWizardOptions, syncWizardOptions, } = useModels() const isMobile = useIsMobile() const [locale, setLocale] = useState('zh') const [source, setSource] = useState('official') const [isSyncing, setIsSyncing] = useState(false) // Get translated options const SYNC_SOURCE_OPTIONS = getSyncSourceOptions(t) const SYNC_LOCALE_OPTIONS = getSyncLocaleOptions(t) useEffect(() => { if (open) { setLocale(syncWizardOptions.locale || 'zh') const preferredSource = SYNC_SOURCE_OPTIONS.find( (option) => option.value === syncWizardOptions.source ) setSource( preferredSource && !preferredSource.disabled ? (preferredSource.value as SyncSource) : 'official' ) } }, [open, syncWizardOptions, SYNC_SOURCE_OPTIONS]) const handleSync = async () => { setIsSyncing(true) try { setSyncWizardOptions({ locale, source }) const previewRes = await previewUpstreamDiff({ locale, source }) if (!previewRes.success) { throw new Error(previewRes.message || 'Failed to preview upstream diff') } const conflicts = previewRes.data?.conflicts || [] if (conflicts.length > 0) { toast.warning( `Found ${conflicts.length} conflict${conflicts.length > 1 ? 's' : ''}. Please resolve them first.` ) setUpstreamConflicts(conflicts) setOpen('upstream-conflict') return } // No conflicts, proceed with sync const response = await syncUpstream({ locale, source }) if (response.success) { const { created_models, created_vendors, updated_models } = response.data || {} toast.success( `Sync completed! Created ${created_models || 0} models, updated ${updated_models || 0}, and added ${created_vendors || 0} vendors.` ) queryClient.invalidateQueries({ queryKey: modelsQueryKeys.lists() }) queryClient.invalidateQueries({ queryKey: vendorsQueryKeys.lists() }) onOpenChange(false) } else { toast.error(response.message || 'Sync failed') } } catch (error: unknown) { toast.error((error as Error)?.message || 'Sync failed') } finally { setIsSyncing(false) } } return ( } >

{t('Choose where to fetch upstream metadata.')}

{ const selected = SYNC_SOURCE_OPTIONS.find( (option) => option.value === value ) if (!selected || selected.disabled) return setSource(selected.value) }} className='grid gap-3 md:grid-cols-2' > {SYNC_SOURCE_OPTIONS.map((option) => { const isActive = source === option.value const isDisabled = option.disabled return ( ) })}
setLocale(v as SyncLocale)} className='grid gap-3 sm:grid-cols-3' > {SYNC_LOCALE_OPTIONS.map((option) => (
))}

{t( 'The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.' )}

) }