{"record":{"id":"45a675b1b240d410","repo":"jamiepine/voicebox","slug":"no-profile-selected","errorCode":null,"errorMessage":"No profile selected","messagePattern":"No profile selected","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"app/src/components/Generation/FloatingGenerateBox.tsx","lineNumber":60,"sourceCode":"  const setSelectedEngine = useUIStore((state) => state.setSelectedEngine);\n  const { data: selectedProfile } = useProfile(selectedProfileId || '');\n  const { data: profiles } = useProfiles();\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [isInstructExpanded, setIsInstructExpanded] = useState(false);\n  const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n  const matchRoute = useMatchRoute();\n  const isStoriesRoute = matchRoute({ to: '/stories' });\n  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);\n  const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight);\n  const { data: currentStory } = useStory(selectedStoryId);\n  const addPendingStoryAdd = useGenerationStore((s) => s.addPendingStoryAdd);\n  const { toast } = useToast();\n\n  const composeMutation = useMutation({\n    mutationFn: async () => {\n      if (!selectedProfileId) throw new Error('No profile selected');\n      return apiClient.composeWithPersonality(selectedProfileId);\n    },\n    onError: (err: Error) => {\n      toast({\n        title: t('generation.compose.failedTitle'),\n        description: err.message || t('generation.compose.failedDescription'),\n        variant: 'destructive',\n      });\n    },\n  });\n\n  // Fetch effect presets for the dropdown\n  const { data: effectPresets } = useQuery({\n    queryKey: ['effectPresets'],\n    queryFn: () => apiClient.listEffectPresets(),\n  });\n\n  // Calculate if track editor is visible (on stories route with items)","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/app/src/components/Generation/FloatingGenerateBox.tsx#L42-L78","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Disable the Compose button until selectedProfileId is truthy.","Auto-select a sensible default profile once the profile list resolves.","If triggered by a shortcut/hotkey, no-op when no profile is selected instead of calling the mutation."],"exampleFix":"// before\n<button onClick={() => composeMutation.mutate()}>Compose</button>\n// after\n<button disabled={!selectedProfileId} onClick={() => composeMutation.mutate()}>Compose</button>;","handlingStrategy":"validation","validationCode":"const canCompose = Boolean(selectedProfileId);\n// Disable Compose until a profile is chosen:\n<button disabled={!canCompose} onClick={() => composeMutation.mutate()} />","typeGuard":null,"tryCatchPattern":"if (!selectedProfileId) {\n  toast({ title: t('generation.compose.selectProfileFirst'), variant: 'destructive' });\n  return;\n}\ntry {\n  await apiClient.composeWithPersonality(selectedProfileId);\n} catch (e) {\n  toast({ title: t('generation.compose.failedTitle'), description: (e as Error).message, variant: 'destructive' });\n}","preventionTips":["Disable Compose unless selectedProfileId is truthy.","Auto-select a default profile once the list resolves.","No-op on hotkey-triggered compose when no profile is selected."],"tags":["ui","validation","state","react","typescript","generation"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}