mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 13:20:39 -07:00
New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs: Captures (the hero — floating gold mic, live mic-meter waveform, expand-row playback), Generate (profile picker + speak + autoplay + recent), and Voices (searchable profile list). Pairing (V0): backend mints a one-time token, mobile scans/pastes the voicebox:// URL, server returns a long-lived bearer it stores only as a SHA-256 hash. Bearer-or-loopback auth applied to every user-data router so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers (the desktop app) keep their friction-free access. Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto- detected via the App-bundle binary path on macOS), QR rendering, 5-minute expiry countdown, copyable URL fallback, paired-device list with revoke. Auto-closes when a new device pairs. just dev now binds the backend to 0.0.0.0 so paired phones can reach it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh Apple Silicon worktrees get a working STT path on first install.
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { apiClient } from '@/lib/api/client';
|
|
|
|
export const PAIRED_DEVICES_KEY = ['paired-devices'] as const;
|
|
export const PAIR_HOST_CANDIDATES_KEY = ['pair-host-candidates'] as const;
|
|
|
|
/**
|
|
* List all paired (and revoked) devices. Pass ``polling: true`` while the
|
|
* pair dialog is open so the device list refreshes when the user finishes
|
|
* scanning on their phone — this is how the desktop UI detects success
|
|
* without needing an SSE stream.
|
|
*/
|
|
export function usePairedDevices({ polling = false }: { polling?: boolean } = {}) {
|
|
return useQuery({
|
|
queryKey: PAIRED_DEVICES_KEY,
|
|
queryFn: () => apiClient.listPairedDevices(),
|
|
refetchInterval: polling ? 2000 : false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The candidate addresses the desktop can embed in the QR (LAN, Tailscale,
|
|
* loopback). Cached for the lifetime of the dialog — interfaces don't
|
|
* change often enough to be worth re-polling.
|
|
*/
|
|
export function usePairHostCandidates(enabled: boolean) {
|
|
return useQuery({
|
|
queryKey: PAIR_HOST_CANDIDATES_KEY,
|
|
queryFn: () => apiClient.getPairHostCandidates(),
|
|
enabled,
|
|
staleTime: Infinity,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mint a fresh pairing token for a chosen host. The result is short-lived
|
|
* (5 min); the dialog should re-mint when expiry is hit.
|
|
*/
|
|
export function useInitPairing() {
|
|
return useMutation({
|
|
mutationFn: (host: string) => apiClient.initPairing(host),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Revoke a paired device. Invalidates the device list so the row disappears
|
|
* on success.
|
|
*/
|
|
export function useRevokePairedDevice() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (deviceId: string) => apiClient.revokePairedDevice(deviceId),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: PAIRED_DEVICES_KEY });
|
|
},
|
|
});
|
|
}
|