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
24 changes: 12 additions & 12 deletions app/src/components/Generation/FloatingGenerateBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,22 @@ export function FloatingGenerateBox({
}, [watchedEngine, setSelectedEngine]);

// Sync generation form language, engine, and effects with selected profile
type EngineValue = 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada' | 'kokoro' | 'qwen_custom_voice';
useEffect(() => {
if (selectedProfile?.language) {
form.setValue('language', selectedProfile.language as LanguageCode);
}
// Auto-switch engine if profile has a default
if (selectedProfile?.default_engine) {
form.setValue(
'engine',
selectedProfile.default_engine as
| 'qwen'
| 'luxtts'
| 'chatterbox'
| 'chatterbox_turbo'
| 'tada'
| 'kokoro',
);
// Auto-switch engine to match the profile
const engine = selectedProfile?.default_engine ?? selectedProfile?.preset_engine;
if (engine) {
form.setValue('engine', engine as EngineValue);
} else if (selectedProfile && selectedProfile.voice_type !== 'preset') {
Comment on lines +128 to +137
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how engine metadata is typed and consumed
fd -i 'types.ts' --exec rg -n 'default_engine|preset_engine' {}
rg -n --type=ts --type=tsx -C2 '\b(default_engine|preset_engine)\b'

Repository: jamiepine/voicebox

Length of output: 224


🏁 Script executed:

# Get the actual FloatingGenerateBox.tsx file content around lines 128-137
fd -i floatinggeneratebox -type f | head -5

Repository: jamiepine/voicebox

Length of output: 232


🏁 Script executed:

# Find the types files that matched in the initial search
fd -i 'types.ts' -type f

Repository: jamiepine/voicebox

Length of output: 232


🏁 Script executed:

# Search for EngineValue definitions
rg -n 'type EngineValue|EngineValue\s*='

Repository: jamiepine/voicebox

Length of output: 237


🏁 Script executed:

# Understand form setup and validation
rg -n 'form\.setValue.*engine' -C 3 -i

Repository: jamiepine/voicebox

Length of output: 4299


🏁 Script executed:

# Find GenerationFormValues definition
rg -n 'type GenerationFormValues|interface GenerationFormValues' -A 15

Repository: jamiepine/voicebox

Length of output: 1512


🏁 Script executed:

# Look for form initialization and schema validation
rg -n 'const form = useForm|useForm.*GenerationFormValues' -B 2 -A 10

Repository: jamiepine/voicebox

Length of output: 5975


🏁 Script executed:

# Check for zod/validation schema
rg -n 'generationSchema|engine.*z\.' -A 3

Repository: jamiepine/voicebox

Length of output: 1016


🏁 Script executed:

# Get full FloatingGenerateBox.tsx context around engine assignment
sed -n '120,150p' app/src/components/Generation/FloatingGenerateBox.tsx

Repository: jamiepine/voicebox

Length of output: 1346


🏁 Script executed:

# Get the full generationSchema definition
sed -n '15,33p' app/src/lib/hooks/useGenerationForm.ts

Repository: jamiepine/voicebox

Length of output: 541


Add runtime validation for backend engine values before setting form state.

Lines 134–136 cast backend engine strings directly to EngineValue without checking validity. If the backend returns an unexpected value, form.setValue('engine', ...) accepts it and the form silently becomes invalid.

The form schema defines engine as a Zod enum (generationSchema), but form.setValue() doesn't trigger validation—only form submission does. This creates a window where the form state contains an invalid engine without error signaling.

Add a guard to validate against known engines:

Suggested fix
  type EngineValue = 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada' | 'kokoro' | 'qwen_custom_voice';
+ const VALID_ENGINES = new Set<EngineValue>([
+   'qwen',
+   'luxtts',
+   'chatterbox',
+   'chatterbox_turbo',
+   'tada',
+   'kokoro',
+   'qwen_custom_voice',
+ ]);
  useEffect(() => {
    if (selectedProfile?.language) {
      form.setValue('language', selectedProfile.language as LanguageCode);
    }
    // Auto-switch engine to match the profile
    const engine = selectedProfile?.default_engine ?? selectedProfile?.preset_engine;
-   if (engine) {
+   if (engine && VALID_ENGINES.has(engine as EngineValue)) {
      form.setValue('engine', engine as EngineValue);

Also consider extracting the engine enum to a shared constant to avoid duplication across FloatingGenerateBox.tsx, generationSchema, and other components like EngineModelSelector.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/Generation/FloatingGenerateBox.tsx` around lines 128 -
137, The code casts backend engine strings to EngineValue and calls
form.setValue('engine', ...) without validating them, which can produce an
invalid form state; update the logic in the useEffect around selectedProfile to
check that the resolved engine (from selectedProfile.default_engine or
.preset_engine) is one of the allowed EngineValue members before calling
form.setValue('engine', ...), and if it is not valid either skip setting the
form or map/fallback to a safe default and optionally log/debug; additionally
extract the engine enum/allowed values used here into a shared constant (used by
generationSchema and EngineModelSelector) so validation is centralized and you
can perform a simple includes(...) check against that shared list before setting
the form value.

// Cloned/designed profile with no default — ensure a compatible (non-preset) engine
const currentEngine = form.getValues('engine');
const presetEngines = new Set(['kokoro', 'qwen_custom_voice']);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated preset engine set across components

Low Severity

The set of preset engines (['kokoro', 'qwen_custom_voice']) is independently defined in both ProfileList.tsx (as PRESET_ENGINES) and inline inside a useEffect in FloatingGenerateBox.tsx (as presetEngines). A related complement set CLONING_ENGINES also exists in EngineModelSelector.tsx. Adding a new preset engine requires updating all three locations, creating a maintenance risk of inconsistency.

Additional Locations (1)
Fix in Cursor Fix in Web

if (presetEngines.has(currentEngine)) {
form.setValue('engine', 'qwen');
}
}
// Pre-fill effects from profile defaults
if (
Expand Down
14 changes: 11 additions & 3 deletions app/src/components/VoiceProfiles/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ const ENGINE_DISPLAY_NAMES: Record<string, string> = {

interface ProfileCardProps {
profile: VoiceProfileResponse;
disabled?: boolean;
}

export function ProfileCard({ profile }: ProfileCardProps) {
export function ProfileCard({ profile, disabled }: ProfileCardProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);

const deleteProfile = useDeleteProfile();
Expand All @@ -40,6 +41,12 @@ export function ProfileCard({ profile }: ProfileCardProps) {
const isSelected = selectedProfileId === profile.id;

const handleSelect = () => {
// If disabled but already selected, bounce the selection to re-trigger engine auto-switch
if (disabled && isSelected) {
setSelectedProfileId(null);
setTimeout(() => setSelectedProfileId(profile.id), 0);
return;
}
setSelectedProfileId(isSelected ? null : profile.id);
};

Expand Down Expand Up @@ -80,8 +87,9 @@ export function ProfileCard({ profile }: ProfileCardProps) {
<>
<Card
className={cn(
'cursor-pointer hover:shadow-md transition-all flex flex-col h-[162px]',
isSelected && 'ring-2 ring-accent shadow-md',
'cursor-pointer transition-all flex flex-col h-[162px]',
disabled ? 'opacity-40 hover:opacity-60' : 'hover:shadow-md',
isSelected && !disabled && 'ring-2 ring-accent shadow-md',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabled+selected card deselects instead of re-engaging

Medium Severity

When a profile is both disabled and isSelected (e.g., user selects a cloned profile then manually switches the engine to Kokoro), the selection ring is hidden via isSelected && !disabled, making the card look unselected. However, handleSelect still uses the toggle isSelected ? null : profile.id, so clicking it deselects the profile instead of re-selecting it and triggering the engine auto-switch. The FloatingGenerateBox's EngineModelSelector doesn't pass selectedProfile, so users can freely switch to an incompatible engine, hitting this state.

Additional Locations (1)
Fix in Cursor Fix in Web

)}
onClick={handleSelect}
tabIndex={0}
Expand Down
81 changes: 51 additions & 30 deletions app/src/components/VoiceProfiles/ProfileList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Mic, Music, Sparkles } from 'lucide-react';
import { Info, Mic, Sparkles } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useProfiles } from '@/lib/hooks/useProfiles';
Expand All @@ -9,16 +10,31 @@ import { ProfileForm } from './ProfileForm';
/** Engines that use preset (built-in) voices instead of cloned profiles. */
const PRESET_ENGINES = new Set(['kokoro', 'qwen_custom_voice']);

/** Human-readable engine names for empty state messages. */
const ENGINE_NAMES: Record<string, string> = {
kokoro: 'Kokoro',
qwen_custom_voice: 'Qwen CustomVoice',
};

export function ProfileList() {
const { data: profiles, isLoading, error } = useProfiles();
const setDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
const selectedEngine = useUIStore((state) => state.selectedEngine);
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map());

// Scroll to the selected profile after engine/sort changes
useEffect(() => {
if (!selectedProfileId) return;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const rafId = requestAnimationFrame(() => {
const el = cardRefs.current.get(selectedProfileId);
if (!el) return;

// Temporarily apply scroll-margin so it doesn't land flush at the top
el.style.scrollMarginTop = '180px';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
timeoutId = setTimeout(() => { el.style.scrollMarginTop = ''; }, 500);
});
return () => {
cancelAnimationFrame(rafId);
if (timeoutId) clearTimeout(timeoutId);
};
}, [selectedProfileId, selectedEngine]);
Comment on lines +21 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add cleanup for deferred scroll operations.

Lines 24-33 schedule requestAnimationFrame and setTimeout without cancellation. Rapid profile/engine changes can trigger stale scroll writes after unmount or subsequent selections.

Proposed fix
 useEffect(() => {
   if (!selectedProfileId) return;
-  // Wait a frame for the DOM to update after re-sort
-  requestAnimationFrame(() => {
+  let timeoutId: ReturnType<typeof setTimeout> | null = null;
+  const rafId = requestAnimationFrame(() => {
     const el = cardRefs.current.get(selectedProfileId);
     if (!el) return;
@@
-    setTimeout(() => { el.style.scrollMarginTop = ''; }, 500);
+    timeoutId = setTimeout(() => {
+      el.style.scrollMarginTop = '';
+    }, 500);
   });
+  return () => {
+    cancelAnimationFrame(rafId);
+    if (timeoutId) clearTimeout(timeoutId);
+  };
 }, [selectedProfileId, selectedEngine]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!selectedProfileId) return;
// Wait a frame for the DOM to update after re-sort
requestAnimationFrame(() => {
const el = cardRefs.current.get(selectedProfileId);
if (!el) return;
// Temporarily apply scroll-margin so it doesn't land flush at the top
el.style.scrollMarginTop = '180px';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
// Clean up after scroll completes
setTimeout(() => { el.style.scrollMarginTop = ''; }, 500);
});
}, [selectedProfileId, selectedEngine]);
useEffect(() => {
if (!selectedProfileId) return;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const rafId = requestAnimationFrame(() => {
const el = cardRefs.current.get(selectedProfileId);
if (!el) return;
// Temporarily apply scroll-margin so it doesn't land flush at the top
el.style.scrollMarginTop = '180px';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
// Clean up after scroll completes
timeoutId = setTimeout(() => {
el.style.scrollMarginTop = '';
}, 500);
});
return () => {
cancelAnimationFrame(rafId);
if (timeoutId) clearTimeout(timeoutId);
};
}, [selectedProfileId, selectedEngine]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/VoiceProfiles/ProfileList.tsx` around lines 21 - 34, The
effect schedules a requestAnimationFrame and a setTimeout without cancelling
them, which can cause stale DOM writes after unmount or when selections change;
modify the useEffect that references selectedProfileId/selectedEngine and
cardRefs to capture the RAF id and timeout id returned by requestAnimationFrame
and setTimeout, and return a cleanup that calls cancelAnimationFrame(rafId) and
clearTimeout(timeoutId); additionally, before mutating el.style inside the RAF
callback, re-check that cardRefs.current.get(selectedProfileId) still exists and
that the component is still mounted (or that selectedProfileId matches) to avoid
applying scrollMarginTop to stale elements.


if (isLoading) {
return null;
Expand All @@ -35,10 +51,18 @@ export function ProfileList() {
const allProfiles = profiles || [];
const isPresetEngine = PRESET_ENGINES.has(selectedEngine);

// Filter profiles based on selected engine
const filteredProfiles = isPresetEngine
? allProfiles.filter((p) => p.voice_type === 'preset' && p.preset_engine === selectedEngine)
: allProfiles.filter((p) => p.voice_type !== 'preset');
/** Whether a profile is supported by the currently selected engine. */
const isSupported = (p: (typeof allProfiles)[number]) =>
isPresetEngine
? p.voice_type === 'preset' && p.preset_engine === selectedEngine
: p.voice_type !== 'preset';
Comment on lines +55 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isSupported should account for default_engine on non-preset profiles.

Lines 52-55 currently treat every non-preset profile as supported whenever a non-preset engine is selected. Profiles with a specific default_engine can be incorrectly marked as supported.

Proposed fix
 const isSupported = (p: (typeof allProfiles)[number]) =>
-  isPresetEngine
-    ? p.voice_type === 'preset' && p.preset_engine === selectedEngine
-    : p.voice_type !== 'preset';
+  isPresetEngine
+    ? p.voice_type === 'preset' && p.preset_engine === selectedEngine
+    : p.voice_type !== 'preset' &&
+      (!p.default_engine || p.default_engine === selectedEngine);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isSupported = (p: (typeof allProfiles)[number]) =>
isPresetEngine
? p.voice_type === 'preset' && p.preset_engine === selectedEngine
: p.voice_type !== 'preset';
const isSupported = (p: (typeof allProfiles)[number]) =>
isPresetEngine
? p.voice_type === 'preset' && p.preset_engine === selectedEngine
: p.voice_type !== 'preset' &&
(!p.default_engine || p.default_engine === selectedEngine);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/VoiceProfiles/ProfileList.tsx` around lines 52 - 55, The
isSupported predicate currently marks every non-preset profile as supported when
a non-preset engine is selected; update isSupported (used with allProfiles and
selectedEngine) so that for non-preset engines it returns true only for profiles
with voice_type !== 'preset' AND where profile.default_engine is either
unset/empty or equals selectedEngine; preserve the existing preset branch that
checks p.voice_type === 'preset' && p.preset_engine === selectedEngine so both
preset_engine and default_engine constraints are handled correctly.


// Sort so supported profiles come first
const sortedProfiles = [...allProfiles].sort(
(a, b) => (isSupported(a) ? 0 : 1) - (isSupported(b) ? 0 : 1),
);

const hasUnsupported = sortedProfiles.some((p) => !isSupported(p));

return (
<div className="flex flex-col">
Expand All @@ -56,29 +80,26 @@ export function ProfileList() {
</Button>
</CardContent>
</Card>
) : filteredProfiles.length === 0 && isPresetEngine ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Music className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-2">
No {ENGINE_NAMES[selectedEngine] ?? selectedEngine} voices created yet.
</p>
<p className="text-sm text-muted-foreground mb-4">
Create a profile to choose a specific voice before generating.
</p>
<Button onClick={() => setDialogOpen(true)}>
<Sparkles className="mr-2 h-4 w-4" />
Create {ENGINE_NAMES[selectedEngine] ?? selectedEngine} Voice
</Button>
</CardContent>
</Card>
) : (
<div className="flex gap-4 overflow-x-auto p-1 pb-1 lg:grid lg:grid-cols-3 lg:auto-rows-auto lg:overflow-x-visible lg:pb-[150px]">
{filteredProfiles.map((profile) => (
<div key={profile.id} className="shrink-0 w-[200px] lg:w-auto lg:shrink">
<ProfileCard profile={profile} />
{sortedProfiles.map((profile) => (
<div
key={profile.id}
className="shrink-0 w-[200px] lg:w-auto lg:shrink"
ref={(el) => {
if (el) cardRefs.current.set(profile.id, el);
else cardRefs.current.delete(profile.id);
}}
>
<ProfileCard profile={profile} disabled={!isSupported(profile)} />
</div>
))}
{hasUnsupported && (
<div className="col-span-full flex items-center gap-2 text-xs text-muted-foreground py-2">
<Info className="h-3.5 w-3.5 shrink-0" />
<span>Only supported voice profiles can be selected for the current model.</span>
</div>
)}
</div>
)}
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/src/lib/hooks/useGenerationForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useGeneration } from '@/lib/hooks/useGeneration';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { useGenerationStore } from '@/stores/generationStore';
import { useServerStore } from '@/stores/serverStore';
import { useUIStore } from '@/stores/uiStore';

const generationSchema = z.object({
text: z.string().min(1, '').max(50000),
Expand Down Expand Up @@ -45,6 +46,7 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) {
const maxChunkChars = useServerStore((state) => state.maxChunkChars);
const crossfadeMs = useServerStore((state) => state.crossfadeMs);
const normalizeAudio = useServerStore((state) => state.normalizeAudio);
const selectedEngine = useUIStore((state) => state.selectedEngine);
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);

Expand All @@ -62,7 +64,7 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) {
seed: undefined,
modelSize: '1.7B',
instruct: '',
engine: 'qwen',
engine: (selectedEngine as GenerationFormValues['engine']) || 'qwen',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify selectedEngine is not strongly constrained at store boundary
fd -i 'uiStore.ts' --exec sed -n '1,220p' {}
rg -n --type=ts --type=tsx -C2 '\bsetSelectedEngine\s*\('

Repository: jamiepine/voicebox

Length of output: 2481


🏁 Script executed:

# Check useGenerationForm.ts around line 67 to confirm the code
fd -type f -name 'useGenerationForm.ts' -exec sed -n '40,75p' {} +

Repository: jamiepine/voicebox

Length of output: 232


🏁 Script executed:

# Find GenerationFormValues type definition
rg -n 'type GenerationFormValues|interface GenerationFormValues' -A 15 --type ts

Repository: jamiepine/voicebox

Length of output: 1512


🏁 Script executed:

# Find all usages of setSelectedEngine to see what values are being set
rg -n 'setSelectedEngine' --type ts -B 2 -A 2

Repository: jamiepine/voicebox

Length of output: 1912


🏁 Script executed:

# Find generationSchema definition
rg -n 'generationSchema\s*=' -A 20 --type ts

Repository: jamiepine/voicebox

Length of output: 1493


🏁 Script executed:

# Read FloatingGenerateBox around setSelectedEngine usage to see watchedEngine source
sed -n '110,135p' app/src/components/Generation/FloatingGenerateBox.tsx

Repository: jamiepine/voicebox

Length of output: 1060


🏁 Script executed:

# Read useGenerationForm.ts around line 67
sed -n '60,75p' app/src/lib/hooks/useGenerationForm.ts

Repository: jamiepine/voicebox

Length of output: 477


Guard selectedEngine at runtime instead of asserting it.

Line 67 force-casts a plain string from store into GenerationFormValues['engine']. The store accepts any string without validation, and while the current code path (form.watch → setSelectedEngine) maintains valid enum values, the store boundary is unguarded. If the store gets persisted, synced from external sources, or called from new code paths, invalid values can bypass form validation and break downstream logic.

Proposed fix
+const ALLOWED_ENGINES = new Set<NonNullable<GenerationFormValues['engine']>>([
+  'qwen',
+  'qwen_custom_voice',
+  'luxtts',
+  'chatterbox',
+  'chatterbox_turbo',
+  'tada',
+  'kokoro',
+]);
+
+const initialEngine: NonNullable<GenerationFormValues['engine']> = ALLOWED_ENGINES.has(
+  selectedEngine as NonNullable<GenerationFormValues['engine']>,
+)
+  ? (selectedEngine as NonNullable<GenerationFormValues['engine']>)
+  : 'qwen';
+
 const form = useForm<GenerationFormValues>({
   resolver: zodResolver(generationSchema),
   defaultValues: {
@@
-    engine: (selectedEngine as GenerationFormValues['engine']) || 'qwen',
+    engine: initialEngine,
     ...options.defaultValues,
   },
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/lib/hooks/useGenerationForm.ts` at line 67, The code currently
force-casts selectedEngine into GenerationFormValues['engine'] when building the
form default (engine: (selectedEngine as GenerationFormValues['engine']) ||
'qwen'); replace that assertion with a runtime guard: validate selectedEngine
against the allowed engine values (e.g., check it exists in the
GenerationFormValues engine union/enum or in an allowedEngines
array/Object.values) and only use it if valid, otherwise fall back to 'qwen';
add or reuse a helper like isValidEngine(selectedEngine) and reference it where
engine is set to avoid unsafe casts and protect downstream logic.

...options.defaultValues,
},
});
Expand Down
1 change: 1 addition & 0 deletions backend/routes/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json as _json
import logging
import tempfile
from datetime import datetime
from pathlib import Path

from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
Expand Down