feat: enhance audio processing and error handling in TTS backend; refactor user dialog form validation
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
@@ -16,7 +16,6 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const previousAudioUrlRef = useRef<string>('')
|
||||
const playerRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioUrl || audioUrl === previousAudioUrlRef.current) return
|
||||
|
||||
@@ -20,7 +20,6 @@ import { JobDetailDialog } from '@/components/JobDetailDialog'
|
||||
interface HistoryItemProps {
|
||||
job: Job
|
||||
onDelete: (id: number) => void
|
||||
onLoadParams: (job: Job) => void
|
||||
}
|
||||
|
||||
const jobTypeBadgeVariant = {
|
||||
@@ -35,7 +34,7 @@ const jobTypeLabel = {
|
||||
voice_clone: '声音克隆',
|
||||
}
|
||||
|
||||
const HistoryItem = memo(({ job, onDelete, onLoadParams }: HistoryItemProps) => {
|
||||
const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false)
|
||||
|
||||
const getLanguageDisplay = (lang: string | undefined) => {
|
||||
|
||||
@@ -5,16 +5,13 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2, FileAudio, RefreshCw } from 'lucide-react'
|
||||
import type { JobType } from '@/types/job'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface HistorySidebarProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onLoadParams: (jobId: number, jobType: JobType) => Promise<void>
|
||||
}
|
||||
|
||||
function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLoadParams'>) {
|
||||
function HistorySidebarContent() {
|
||||
const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistoryContext()
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -35,14 +32,6 @@ function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLo
|
||||
return () => observer.disconnect()
|
||||
}, [hasMore, loadingMore, loadMore])
|
||||
|
||||
const handleLoadParams = async (jobId: number, jobType: JobType) => {
|
||||
try {
|
||||
await onLoadParams(jobId, jobType)
|
||||
} catch (error) {
|
||||
toast.error('加载参数失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b">
|
||||
@@ -79,7 +68,6 @@ function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLo
|
||||
key={job.id}
|
||||
job={job}
|
||||
onDelete={deleteJob}
|
||||
onLoadParams={(job) => handleLoadParams(job.id, job.type)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -96,16 +84,16 @@ function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLo
|
||||
)
|
||||
}
|
||||
|
||||
export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) {
|
||||
export function HistorySidebar({ open, onOpenChange }: HistorySidebarProps) {
|
||||
return (
|
||||
<>
|
||||
<aside className="hidden lg:block w-[320px] border-r h-full">
|
||||
<HistorySidebarContent onLoadParams={onLoadParams} />
|
||||
<HistorySidebarContent />
|
||||
</aside>
|
||||
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="left" className="w-full sm:max-w-md p-0">
|
||||
<HistorySidebarContent onLoadParams={onLoadParams} />
|
||||
<HistorySidebarContent />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
|
||||
@@ -15,11 +15,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
||||
import { ttsApi, jobApi } from '@/lib/api'
|
||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { PresetSelector } from '@/components/PresetSelector'
|
||||
import { ParamInput } from '@/components/ParamInput'
|
||||
import { PRESET_INSTRUCTS, ADVANCED_PARAMS_INFO } from '@/lib/constants'
|
||||
import type { Language, Speaker } from '@/types/tts'
|
||||
|
||||
@@ -56,7 +54,6 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
|
||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||
const { refresh } = useHistoryContext()
|
||||
const { preferences } = useUserPreferences()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -90,7 +87,6 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
setValue('top_k', params.top_k || 20)
|
||||
setValue('top_p', params.top_p || 0.7)
|
||||
setValue('repetition_penalty', params.repetition_penalty || 1.05)
|
||||
setValue('backend', params.backend || 'local')
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -214,11 +210,11 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
<Dialog open={advancedOpen} onOpenChange={(open) => {
|
||||
if (open) {
|
||||
setTempAdvancedParams({
|
||||
max_new_tokens: watch('max_new_tokens'),
|
||||
temperature: watch('temperature'),
|
||||
top_k: watch('top_k'),
|
||||
top_p: watch('top_p'),
|
||||
repetition_penalty: watch('repetition_penalty')
|
||||
max_new_tokens: watch('max_new_tokens') || 2048,
|
||||
temperature: watch('temperature') || 0.3,
|
||||
top_k: watch('top_k') || 20,
|
||||
top_p: watch('top_p') || 0.7,
|
||||
repetition_penalty: watch('repetition_penalty') || 1.05
|
||||
})
|
||||
}
|
||||
setAdvancedOpen(open)
|
||||
@@ -339,11 +335,11 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setTempAdvancedParams({
|
||||
max_new_tokens: watch('max_new_tokens'),
|
||||
temperature: watch('temperature'),
|
||||
top_k: watch('top_k'),
|
||||
top_p: watch('top_p'),
|
||||
repetition_penalty: watch('repetition_penalty')
|
||||
max_new_tokens: watch('max_new_tokens') || 2048,
|
||||
temperature: watch('temperature') || 0.3,
|
||||
top_k: watch('top_k') || 20,
|
||||
top_p: watch('top_p') || 0.7,
|
||||
repetition_penalty: watch('repetition_penalty') || 1.05
|
||||
})
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
|
||||
@@ -9,14 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Settings, Globe2, Type, Play, FileText, Mic, Zap, Database, ArrowRight, ArrowLeft } from 'lucide-react'
|
||||
import { Settings, Globe2, Type, Play, FileText, Mic, ArrowRight, ArrowLeft } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { IconLabel } from '@/components/IconLabel'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { ttsApi, jobApi } from '@/lib/api'
|
||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { FileUploader } from '@/components/FileUploader'
|
||||
@@ -54,7 +53,6 @@ function VoiceCloneForm() {
|
||||
|
||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||
const { refresh } = useHistoryContext()
|
||||
const { preferences } = useUserPreferences()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -92,6 +90,14 @@ function VoiceCloneForm() {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputTab === 'record' && PRESET_REF_TEXTS.length > 0) {
|
||||
setValue('ref_text', PRESET_REF_TEXTS[0].text)
|
||||
} else if (inputTab === 'upload') {
|
||||
setValue('ref_text', '')
|
||||
}
|
||||
}, [inputTab])
|
||||
|
||||
const handleNextStep = async () => {
|
||||
// Validate step 1 fields
|
||||
const valid = await trigger(['ref_audio', 'ref_text'])
|
||||
@@ -180,22 +186,31 @@ function VoiceCloneForm() {
|
||||
onSelect={(preset) => setValue('ref_text', preset.text)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="button" className="w-full mt-6" onClick={handleNextStep}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="record" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">请朗读以下任一段落:</Label>
|
||||
<div className="grid gap-2">
|
||||
{PRESET_REF_TEXTS.map((preset, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-3 border rounded-lg hover:bg-accent cursor-pointer transition-colors text-sm"
|
||||
onClick={() => setValue('ref_text', preset.text)}
|
||||
>
|
||||
<div className="font-medium mb-1">{preset.label}</div>
|
||||
<div className="text-muted-foreground line-clamp-2">{preset.text}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{PRESET_REF_TEXTS.map((preset, i) => {
|
||||
const isSelected = watch('ref_text') === preset.text
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`p-3 border rounded-lg hover:bg-accent cursor-pointer transition-colors text-sm text-center ${
|
||||
isSelected ? 'border-primary bg-primary/10' : ''
|
||||
}`}
|
||||
onClick={() => setValue('ref_text', preset.text)}
|
||||
>
|
||||
<div className="font-medium">{preset.label}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-0.5 pt-2">
|
||||
<Label>当前参考文本</Label>
|
||||
@@ -209,28 +224,31 @@ function VoiceCloneForm() {
|
||||
|
||||
{/* Mobile-friendly Bottom Recorder Area */}
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-background border-t z-50 md:relative md:border-t-0 md:bg-transparent md:p-0 md:z-0">
|
||||
<Controller
|
||||
name="ref_audio"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AudioRecorder
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
{watch('ref_audio') && (
|
||||
<Button type="button" className="w-full" onClick={handleNextStep}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
{errors.ref_audio && (
|
||||
<p className="text-sm text-destructive mt-2 text-center md:text-left">{errors.ref_audio.message}</p>
|
||||
)}
|
||||
<Controller
|
||||
name="ref_audio"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AudioRecorder
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.ref_audio && (
|
||||
<p className="text-sm text-destructive mt-2 text-center md:text-left">{errors.ref_audio.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Spacer for mobile to prevent content being hidden behind fixed footer */}
|
||||
<div className="h-24 md:hidden" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Button type="button" className="w-full mt-6" onClick={handleNextStep}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={step === 2 ? 'block space-y-4' : 'hidden'}>
|
||||
|
||||
@@ -15,11 +15,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
||||
import { ttsApi, jobApi } from '@/lib/api'
|
||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||
import { PresetSelector } from '@/components/PresetSelector'
|
||||
import { ParamInput } from '@/components/ParamInput'
|
||||
import { PRESET_VOICE_DESIGNS, ADVANCED_PARAMS_INFO } from '@/lib/constants'
|
||||
import type { Language } from '@/types/tts'
|
||||
|
||||
@@ -54,7 +52,6 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
|
||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||
const { refresh } = useHistoryContext()
|
||||
const { preferences } = useUserPreferences()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -182,11 +179,11 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
<Dialog open={advancedOpen} onOpenChange={(open) => {
|
||||
if (open) {
|
||||
setTempAdvancedParams({
|
||||
max_new_tokens: watch('max_new_tokens'),
|
||||
temperature: watch('temperature'),
|
||||
top_k: watch('top_k'),
|
||||
top_p: watch('top_p'),
|
||||
repetition_penalty: watch('repetition_penalty')
|
||||
max_new_tokens: watch('max_new_tokens') || 2048,
|
||||
temperature: watch('temperature') || 0.3,
|
||||
top_k: watch('top_k') || 20,
|
||||
top_p: watch('top_p') || 0.7,
|
||||
repetition_penalty: watch('repetition_penalty') || 1.05
|
||||
})
|
||||
}
|
||||
setAdvancedOpen(open)
|
||||
@@ -307,11 +304,11 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setTempAdvancedParams({
|
||||
max_new_tokens: watch('max_new_tokens'),
|
||||
temperature: watch('temperature'),
|
||||
top_k: watch('top_k'),
|
||||
top_p: watch('top_p'),
|
||||
repetition_penalty: watch('repetition_penalty')
|
||||
max_new_tokens: watch('max_new_tokens') || 2048,
|
||||
temperature: watch('temperature') || 0.3,
|
||||
top_k: watch('top_k') || 20,
|
||||
top_p: watch('top_p') || 0.7,
|
||||
repetition_penalty: watch('repetition_penalty') || 1.05
|
||||
})
|
||||
setAdvancedOpen(false)
|
||||
}}
|
||||
|
||||
@@ -22,23 +22,15 @@ import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { User } from '@/types/auth'
|
||||
|
||||
const editUserFormSchema = z.object({
|
||||
const userFormSchema = z.object({
|
||||
username: z.string().min(3, '用户名至少3个字符').max(20, '用户名最多20个字符'),
|
||||
email: z.string().email('请输入有效的邮箱地址'),
|
||||
password: z.string().optional(),
|
||||
is_active: z.boolean().default(true),
|
||||
is_superuser: z.boolean().default(false),
|
||||
is_active: z.boolean(),
|
||||
is_superuser: z.boolean(),
|
||||
})
|
||||
|
||||
const createUserFormSchema = z.object({
|
||||
username: z.string().min(3, '用户名至少3个字符').max(20, '用户名最多20个字符'),
|
||||
email: z.string().email('请输入有效的邮箱地址'),
|
||||
password: z.string().min(8, '密码至少8个字符'),
|
||||
is_active: z.boolean().default(true),
|
||||
is_superuser: z.boolean().default(false),
|
||||
})
|
||||
|
||||
type UserFormValues = z.infer<typeof editUserFormSchema>
|
||||
type UserFormValues = z.infer<typeof userFormSchema>
|
||||
|
||||
interface UserDialogProps {
|
||||
open: boolean
|
||||
@@ -58,7 +50,7 @@ export function UserDialog({
|
||||
const isEditing = !!user
|
||||
|
||||
const form = useForm<UserFormValues>({
|
||||
resolver: zodResolver(isEditing ? editUserFormSchema : createUserFormSchema),
|
||||
resolver: zodResolver(userFormSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
email: '',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: 'light' | 'dark'
|
||||
|
||||
@@ -52,7 +52,7 @@ export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
return function(...args: Parameters<T>) {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
|
||||
@@ -8,10 +8,6 @@ import type { VoiceDesignFormHandle } from '@/components/tts/VoiceDesignForm'
|
||||
import { HistorySidebar } from '@/components/HistorySidebar'
|
||||
import { OnboardingDialog } from '@/components/OnboardingDialog'
|
||||
import FormSkeleton from '@/components/FormSkeleton'
|
||||
import type { JobType } from '@/types/job'
|
||||
import { jobApi } from '@/lib/api'
|
||||
import { toast } from 'sonner'
|
||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
|
||||
|
||||
const CustomVoiceForm = lazy(() => import('@/components/tts/CustomVoiceForm'))
|
||||
@@ -22,7 +18,6 @@ function Home() {
|
||||
const [currentTab, setCurrentTab] = useState('custom-voice')
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
const { loadCompletedJob } = useJobPolling()
|
||||
const { preferences } = useUserPreferences()
|
||||
|
||||
const customVoiceFormRef = useRef<CustomVoiceFormHandle>(null)
|
||||
@@ -34,30 +29,6 @@ function Home() {
|
||||
}
|
||||
}, [preferences])
|
||||
|
||||
const handleLoadParams = async (jobId: number, jobType: JobType) => {
|
||||
try {
|
||||
const job = await jobApi.getJob(jobId)
|
||||
|
||||
setSidebarOpen(false)
|
||||
|
||||
if (jobType === 'custom_voice') {
|
||||
setCurrentTab('custom-voice')
|
||||
setTimeout(() => {
|
||||
customVoiceFormRef.current?.loadParams(job.parameters)
|
||||
}, 100)
|
||||
} else if (jobType === 'voice_design') {
|
||||
setCurrentTab('voice-design')
|
||||
setTimeout(() => {
|
||||
voiceDesignFormRef.current?.loadParams(job.parameters)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
loadCompletedJob(job)
|
||||
toast.success('参数已加载到表单')
|
||||
} catch (error) {
|
||||
toast.error('加载参数失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-hidden flex flex-col bg-background">
|
||||
@@ -72,7 +43,6 @@ function Home() {
|
||||
<HistorySidebar
|
||||
open={sidebarOpen}
|
||||
onOpenChange={setSidebarOpen}
|
||||
onLoadParams={handleLoadParams}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-y-auto container mx-auto p-3 md:p-6 max-w-[800px] md:max-w-[700px]">
|
||||
|
||||
Reference in New Issue
Block a user