Files
TalkBox/app/src/components/ServerTab/CloudSection.tsx
T
Labyricorn b8815e94ea
CI / frontend-quality (push) Canceled after 0s
rebrand: rename VoiceBox to TalkBox throughout codebase
- All 'voicebox'/'Voicebox'/'VOICEBOX' strings replaced with 'talkbox'/'TalkBox'/'TALKBOX'
- Port changed from 17493 to 17494 (avoids conflict with upstream VoiceBox)
- MCP tool namespace: voicebox.* -> talkbox.*
- App bundle ID: sh.voicebox.app -> com.talkbox.app
- Binary names: voicebox-server -> talkbox-server, voicebox-mcp -> talkbox-mcp
- Docker user/group: voicebox -> talkbox
- Database: voicebox.db -> talkbox.db
- Env vars: VOICEBOX_* -> TALKBOX_*
- Asset files renamed: voicebox-logo.* -> talkbox-logo.*, etc.
- External binaries in tauri.conf.json updated to talkbox-server/talkbox-mcp
2026-08-24 19:45:56 -07:00

152 lines
4.8 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Cloud, Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
import { apiClient } from '@/lib/api/client';
import { SettingRow, SettingSection } from './SettingRow';
// "Log in with browser" device pairing. The backend opens the system browser
// 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 { toast } = useToast();
const queryClient = useQueryClient();
const [polling, setPolling] = useState(false);
const { data: status } = useQuery({
queryKey: ['cloud-status'],
queryFn: () => apiClient.getCloudStatus(),
refetchInterval: polling ? 2000 : false,
});
const connected = status?.connected ?? false;
// Once the browser flow completes, stop polling and celebrate.
useEffect(() => {
if (connected && polling) {
setPolling(false);
toast({
title: 'Connected to TalkBox Cloud',
description: `Linked as ${status?.device_name ?? 'this device'}.`,
});
}
}, [connected, polling, status?.device_name, toast]);
// 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
// ten, so the user can simply start again.
useEffect(() => {
if (!polling) return;
const timeoutId = window.setTimeout(() => {
setPolling(false);
toast({
title: 'Sign-in timed out',
description: 'The browser sign-in was not completed. Try again.',
variant: 'destructive',
});
}, 120_000);
return () => window.clearTimeout(timeoutId);
}, [polling, toast]);
const startLogin = useMutation({
mutationFn: () => apiClient.startCloudLogin(),
onSuccess: () => {
setPolling(true);
toast({
title: 'Continue in your browser',
description: 'Authorize this device, then return here.',
});
},
onError: (error: Error) =>
toast({
title: 'Could not start sign-in',
description: error.message,
variant: 'destructive',
}),
});
const disconnect = useMutation({
mutationFn: () => apiClient.disconnectCloud(),
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.',
});
},
onError: (error: Error) =>
toast({ title: 'Could not disconnect', description: error.message, variant: 'destructive' }),
});
const busy = startLogin.isPending || polling;
return (
<SettingSection
title="TalkBox Cloud"
description="End-to-end encrypted backup & sync across your devices."
>
<SettingRow
title={connected ? 'Connected' : '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.'
}
action={
connected ? (
<Button
disabled={disconnect.isPending}
onClick={() => disconnect.mutate()}
size="sm"
variant="outline"
>
{disconnect.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Disconnecting
</>
) : (
'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…'}
</>
) : (
<>
<Cloud className="h-3.5 w-3.5 mr-1.5" />
Log in with browser
</>
)}
</Button>
)
}
/>
{connected && (
<SettingRow
title="Manage"
description="Revoke this device, add API keys, or manage billing from your account."
>
<a
className="text-sm text-accent hover:underline"
href={status?.dashboard_url ?? 'https://talkbox.sh/account'}
rel="noopener noreferrer"
target="_blank"
>
Open account dashboard
</a>
</SettingRow>
)}
</SettingSection>
);
}