Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 41 additions & 27 deletions app/src/components/ServerTab/CloudSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Cloud, Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
import { apiClient } from '@/lib/api/client';
Expand All @@ -10,6 +11,7 @@ import { SettingRow, SettingSection } from './SettingRow';
// and completes the code exchange; here we just kick it off and poll status
// until the link goes live. The API key never touches the frontend.
export function CloudSection() {
const { t } = useTranslation();
const { toast } = useToast();
const queryClient = useQueryClient();
const [polling, setPolling] = useState(false);
Expand All @@ -27,11 +29,13 @@ export function CloudSection() {
if (connected && polling) {
setPolling(false);
toast({
title: 'Connected to Voicebox Cloud',
description: `Linked as ${status?.device_name ?? 'this device'}.`,
title: t('settings.general.cloud.connectedToast.title'),
description: t('settings.general.cloud.connectedToast.description', {
name: status?.device_name ?? t('settings.general.cloud.account.thisDevice'),
}),
});
}
}, [connected, polling, status?.device_name, toast]);
}, [connected, polling, status?.device_name, toast, t]);

// Give up after two minutes so an abandoned browser flow doesn't leave the
// button stuck on "Waiting for browser…". The backend state stays valid for
Expand All @@ -41,26 +45,26 @@ export function CloudSection() {
const timeoutId = window.setTimeout(() => {
setPolling(false);
toast({
title: 'Sign-in timed out',
description: 'The browser sign-in was not completed. Try again.',
title: t('settings.general.cloud.signInTimedOut.title'),
description: t('settings.general.cloud.signInTimedOut.description'),
variant: 'destructive',
});
}, 120_000);
return () => window.clearTimeout(timeoutId);
}, [polling, toast]);
}, [polling, toast, t]);

const startLogin = useMutation({
mutationFn: () => apiClient.startCloudLogin(),
onSuccess: () => {
setPolling(true);
toast({
title: 'Continue in your browser',
description: 'Authorize this device, then return here.',
title: t('settings.general.cloud.continueInBrowser.title'),
description: t('settings.general.cloud.continueInBrowser.description'),
});
},
onError: (error: Error) =>
toast({
title: 'Could not start sign-in',
title: t('settings.general.cloud.signInFailedTitle'),
description: error.message,
variant: 'destructive',
}),
Expand All @@ -71,30 +75,38 @@ export function CloudSection() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cloud-status'] });
toast({
title: 'Disconnected',
description:
'This device is no longer linked. The key stays valid until revoked in your account.',
title: t('settings.general.cloud.disconnectedToast.title'),
description: t('settings.general.cloud.disconnectedToast.description'),
});
},
onError: (error: Error) =>
toast({ title: 'Could not disconnect', description: error.message, variant: 'destructive' }),
toast({
title: t('settings.general.cloud.disconnectFailedTitle'),
description: error.message,
variant: 'destructive',
}),
});

const busy = startLogin.isPending || polling;

return (
<SettingSection
title="Voicebox Cloud"
description="End-to-end encrypted backup & sync across your devices."
title={t('settings.general.cloud.title')}
description={t('settings.general.cloud.description')}
>
<SettingRow
title={connected ? 'Connected' : 'Account'}
title={
connected
? t('settings.general.cloud.account.connected')
: t('settings.general.cloud.account.account')
}
description={
connected
? `Linked as ${status?.device_name ?? 'this device'}${
status?.key_prefix ? ` · ${status.key_prefix}…` : ''
}`
: 'Log in to back up and sync your captures and generations.'
? t('settings.general.cloud.account.linkedAs', {
name: status?.device_name ?? t('settings.general.cloud.account.thisDevice'),
keyPrefix: status?.key_prefix ? ` · ${status.key_prefix}…` : '',
})
: t('settings.general.cloud.account.logInHint')
}
action={
connected ? (
Expand All @@ -107,23 +119,25 @@ export function CloudSection() {
{disconnect.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Disconnecting…
{t('settings.general.cloud.actions.disconnecting')}
</>
) : (
'Disconnect'
t('settings.general.cloud.actions.disconnect')
)}
</Button>
) : (
<Button disabled={busy} onClick={() => startLogin.mutate()} size="sm">
{busy ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{polling ? 'Waiting for browser…' : 'Opening…'}
{polling
? t('settings.general.cloud.actions.waitingForBrowser')
: t('settings.general.cloud.actions.opening')}
</>
) : (
<>
<Cloud className="h-3.5 w-3.5 mr-1.5" />
Log in with browser
{t('settings.general.cloud.actions.logInWithBrowser')}
</>
)}
</Button>
Expand All @@ -133,16 +147,16 @@ export function CloudSection() {

{connected && (
<SettingRow
title="Manage"
description="Revoke this device, add API keys, or manage billing from your account."
title={t('settings.general.cloud.manage.title')}
description={t('settings.general.cloud.manage.description')}
>
<a
className="text-sm text-accent hover:underline"
href={status?.dashboard_url ?? 'https://voicebox.sh/account'}
rel="noopener noreferrer"
target="_blank"
>
Open account dashboard ↗
{t('settings.general.cloud.manage.openDashboard')}
</a>
</SettingRow>
)}
Expand Down
41 changes: 41 additions & 0 deletions app/src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,47 @@
"profiles": "List voices",
"history": "Past generations"
}
},
"cloud": {
"title": "Voicebox Cloud",
"description": "End-to-end encrypted backup & sync across your devices.",
"connectedToast": {
"title": "Connected to Voicebox Cloud",
"description": "Linked as {{name}}."
},
"signInTimedOut": {
"title": "Sign-in timed out",
"description": "The browser sign-in was not completed. Try again."
},
"continueInBrowser": {
"title": "Continue in your browser",
"description": "Authorize this device, then return here."
},
"signInFailedTitle": "Could not start sign-in",
"disconnectedToast": {
"title": "Disconnected",
"description": "This device is no longer linked. The key stays valid until revoked in your account."
},
"disconnectFailedTitle": "Could not disconnect",
"account": {
"connected": "Connected",
"account": "Account",
"linkedAs": "Linked as {{name}}{{keyPrefix}}",
"thisDevice": "this device",
"logInHint": "Log in to back up and sync your captures and generations."
},
"actions": {
"disconnecting": "Disconnecting…",
"disconnect": "Disconnect",
"waitingForBrowser": "Waiting for browser…",
"opening": "Opening…",
"logInWithBrowser": "Log in with browser"
},
"manage": {
"title": "Manage",
"description": "Revoke this device, add API keys, or manage billing from your account.",
"openDashboard": "Open account dashboard ↗"
}
}
},
"generation": {
Expand Down
41 changes: 41 additions & 0 deletions app/src/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,47 @@
"profiles": "Listar voces",
"history": "Generaciones anteriores"
}
},
"cloud": {
"title": "Voicebox Cloud",
"description": "Copia de seguridad y sincronización cifradas de extremo a extremo en todos tus dispositivos.",
"connectedToast": {
"title": "Conectado a Voicebox Cloud",
"description": "Vinculado como {{name}}."
},
"signInTimedOut": {
"title": "Tiempo de inicio de sesión agotado",
"description": "No se completó el inicio de sesión en el navegador. Inténtalo de nuevo."
},
"continueInBrowser": {
"title": "Continúa en tu navegador",
"description": "Autoriza este dispositivo y luego vuelve aquí."
},
"signInFailedTitle": "No se pudo iniciar el inicio de sesión",
"disconnectedToast": {
"title": "Desconectado",
"description": "Este dispositivo ya no está vinculado. La clave sigue siendo válida hasta que la revoques en tu cuenta."
},
"disconnectFailedTitle": "No se pudo desconectar",
"account": {
"connected": "Conectado",
"account": "Cuenta",
"linkedAs": "Vinculado como {{name}}{{keyPrefix}}",
"thisDevice": "este dispositivo",
"logInHint": "Inicia sesión para hacer copia de seguridad y sincronizar tus capturas y generaciones."
},
"actions": {
"disconnecting": "Desconectando…",
"disconnect": "Desconectar",
"waitingForBrowser": "Esperando al navegador…",
"opening": "Abriendo…",
"logInWithBrowser": "Iniciar sesión con el navegador"
},
"manage": {
"title": "Gestionar",
"description": "Revoca este dispositivo, añade claves de API o gestiona la facturación desde tu cuenta.",
"openDashboard": "Abrir panel de la cuenta ↗"
}
}
},
"generation": {
Expand Down
73 changes: 71 additions & 2 deletions app/src/i18n/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,47 @@
"profiles": "Lister les voix",
"history": "Générations passées"
}
},
"cloud": {
"title": "Voicebox Cloud",
"description": "Sauvegarde et synchronisation chiffrées de bout en bout sur tous vos appareils.",
"connectedToast": {
"title": "Connecté à Voicebox Cloud",
"description": "Lié en tant que {{name}}."
},
"signInTimedOut": {
"title": "Délai de connexion dépassé",
"description": "La connexion via le navigateur n'a pas abouti. Réessayez."
},
"continueInBrowser": {
"title": "Continuez dans votre navigateur",
"description": "Autorisez cet appareil, puis revenez ici."
},
"signInFailedTitle": "Impossible de démarrer la connexion",
"disconnectedToast": {
"title": "Déconnecté",
"description": "Cet appareil n'est plus lié. La clé reste valide jusqu'à ce qu'elle soit révoquée depuis votre compte."
},
"disconnectFailedTitle": "Impossible de se déconnecter",
"account": {
"connected": "Connecté",
"account": "Compte",
"linkedAs": "Lié en tant que {{name}}{{keyPrefix}}",
"thisDevice": "cet appareil",
"logInHint": "Connectez-vous pour sauvegarder et synchroniser vos captures et générations."
},
"actions": {
"disconnecting": "Déconnexion…",
"disconnect": "Déconnecter",
"waitingForBrowser": "En attente du navigateur…",
"opening": "Ouverture…",
"logInWithBrowser": "Se connecter via le navigateur"
},
"manage": {
"title": "Gérer",
"description": "Révoquez cet appareil, ajoutez des clés API ou gérez la facturation depuis votre compte.",
"openDashboard": "Ouvrir le tableau de bord du compte ↗"
}
}
},
"generation": {
Expand Down Expand Up @@ -1091,11 +1132,15 @@
"active": "Actif",
"cuda": {
"title": "Backend CUDA",
"activeTitle": "Backend CUDA actif",
"description": "Accélération GPU NVIDIA via un backend CUDA téléchargeable.",
"downloading": "Téléchargement du backend CUDA…",
"downloadingShort": "Téléchargement…",
"updating": "Mise à jour…"
},
"activeBackend": {
"description": "L'accélération GPU est actuellement activée."
},
"restart": {
"ready": "Serveur redémarré avec succès",
"waiting": "Redémarrage du serveur…",
Expand Down Expand Up @@ -1126,9 +1171,33 @@
"downloadStart": "Échec du démarrage du téléchargement",
"restartFailed": "Échec du redémarrage",
"switchCpu": "Échec du basculement vers CPU",
"deleteCuda": "Échec de la suppression du backend CUDA"
"deleteCuda": "Échec de la suppression du backend CUDA",
"deleteRocm": "Échec de la suppression du backend ROCm"
},
"footer": "Voicebox détecte et utilise automatiquement le meilleur GPU disponible sur votre système. Sur les Mac Apple Silicon, le backend MLX fonctionne nativement sur le Neural Engine et le GPU via Metal Performance Shaders (MPS), sans configuration supplémentaire. Sur Windows et Linux avec GPU NVIDIA, vous pouvez télécharger un backend CUDA optionnel pour l'inférence accélérée par le matériel. AMD ROCm, Intel XPU et DirectML sont également pris en charge là où disponibles via PyTorch. Quand aucun GPU n'est détecté, Voicebox utilise le CPU — tous les moteurs fonctionnent, mais plus lentement."
"footer": "Voicebox détecte et utilise automatiquement le meilleur GPU disponible sur votre système. Sur les Mac Apple Silicon, le backend MLX fonctionne nativement sur le Neural Engine et le GPU via Metal Performance Shaders (MPS), sans configuration supplémentaire. Sur Windows, vous pouvez télécharger des backends optionnels CUDA (NVIDIA) ou ROCm (AMD) pour l'inférence accélérée par le matériel. Intel XPU et DirectML sont également pris en charge là où disponibles via PyTorch. Quand aucun GPU n'est détecté, Voicebox utilise le CPU — tous les moteurs fonctionnent, mais plus lentement.",
"rocm": {
"title": "Backend AMD ROCm",
"activeTitle": "Backend ROCm actif",
"description": "Accélération GPU AMD via un backend ROCm téléchargeable.",
"downloading": "Téléchargement du backend ROCm…",
"downloadingShort": "Téléchargement…",
"updating": "Mise à jour…"
},
"downloadRocm": {
"title": "Télécharger le backend AMD ROCm",
"description": "~2-3 Go à télécharger. Nécessite un GPU AMD Radeon avec support ROCm.",
"button": "Télécharger"
},
"switchToRocm": {
"title": "Passer au backend ROCm",
"description": "Le backend ROCm est téléchargé et prêt. Redémarrez pour activer.",
"button": "Redémarrer"
},
"removeRocm": {
"title": "Supprimer le backend ROCm",
"description": "Supprimez le binaire ROCm téléchargé pour libérer de l'espace disque.",
"button": "Supprimer"
}
},
"logs": {
"title": "Journaux du serveur",
Expand Down
Loading