jamiepine/voicebox · warning · Error

No profile selected

Error message

No profile selected

What it means

Thrown by the compose mutation in FloatingGenerateBox if selectedProfileId is falsy when composeWithPersonality is called. It is a precondition guard: the API call requires a profile id, so composing without one is a programming/state error. The onError handler shows a destructive toast with the message (or a localized fallback).

Source

Thrown at app/src/components/Generation/FloatingGenerateBox.tsx:60

  const setSelectedEngine = useUIStore((state) => state.setSelectedEngine);
  const { data: selectedProfile } = useProfile(selectedProfileId || '');
  const { data: profiles } = useProfiles();
  const [isExpanded, setIsExpanded] = useState(false);
  const [isInstructExpanded, setIsInstructExpanded] = useState(false);
  const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const matchRoute = useMatchRoute();
  const isStoriesRoute = matchRoute({ to: '/stories' });
  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);
  const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight);
  const { data: currentStory } = useStory(selectedStoryId);
  const addPendingStoryAdd = useGenerationStore((s) => s.addPendingStoryAdd);
  const { toast } = useToast();

  const composeMutation = useMutation({
    mutationFn: async () => {
      if (!selectedProfileId) throw new Error('No profile selected');
      return apiClient.composeWithPersonality(selectedProfileId);
    },
    onError: (err: Error) => {
      toast({
        title: t('generation.compose.failedTitle'),
        description: err.message || t('generation.compose.failedDescription'),
        variant: 'destructive',
      });
    },
  });

  // Fetch effect presets for the dropdown
  const { data: effectPresets } = useQuery({
    queryKey: ['effectPresets'],
    queryFn: () => apiClient.listEffectPresets(),
  });

  // Calculate if track editor is visible (on stories route with items)

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Disable the Compose button until selectedProfileId is truthy.
  2. Auto-select a sensible default profile once the profile list resolves.
  3. If triggered by a shortcut/hotkey, no-op when no profile is selected instead of calling the mutation.

Example fix

// before
<button onClick={() => composeMutation.mutate()}>Compose</button>
// after
<button disabled={!selectedProfileId} onClick={() => composeMutation.mutate()}>Compose</button>;
Defensive patterns

Strategy: validation

Validate before calling

const canCompose = Boolean(selectedProfileId);
// Disable Compose until a profile is chosen:
<button disabled={!canCompose} onClick={() => composeMutation.mutate()} />

Try / catch

if (!selectedProfileId) {
  toast({ title: t('generation.compose.selectProfileFirst'), variant: 'destructive' });
  return;
}
try {
  await apiClient.composeWithPersonality(selectedProfileId);
} catch (e) {
  toast({ title: t('generation.compose.failedTitle'), description: (e as Error).message, variant: 'destructive' });
}

Prevention

When it happens

Trigger: The Compose button is clicked before any voice profile is selected — e.g. profile list still loading, profile was deselected/deleted, or selectedProfileId state was reset. Can also fire from a keyboard shortcut that triggers compose regardless of selection.

Common situations: Race between profile fetch and user action on first paint. Profile deleted elsewhere while the generate box stayed mounted. Story route auto-compose triggered without a default profile.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/45a675b1b240d410. Report an issue: GitHub.