Add HistoryContext and integrate it into relevant components for improved job management
This commit is contained in:
@@ -5,6 +5,7 @@ import { ThemeProvider } from '@/contexts/ThemeContext'
|
|||||||
import { AuthProvider, useAuth } from '@/contexts/AuthContext'
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext'
|
||||||
import { AppProvider } from '@/contexts/AppContext'
|
import { AppProvider } from '@/contexts/AppContext'
|
||||||
import { JobProvider } from '@/contexts/JobContext'
|
import { JobProvider } from '@/contexts/JobContext'
|
||||||
|
import { HistoryProvider } from '@/contexts/HistoryContext'
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary'
|
import ErrorBoundary from '@/components/ErrorBoundary'
|
||||||
import LoadingScreen from '@/components/LoadingScreen'
|
import LoadingScreen from '@/components/LoadingScreen'
|
||||||
import { SuperAdminRoute } from '@/components/SuperAdminRoute'
|
import { SuperAdminRoute } from '@/components/SuperAdminRoute'
|
||||||
@@ -71,9 +72,11 @@ function App() {
|
|||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<JobProvider>
|
<HistoryProvider>
|
||||||
<Home />
|
<JobProvider>
|
||||||
</JobProvider>
|
<Home />
|
||||||
|
</JobProvider>
|
||||||
|
</HistoryProvider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRef, useEffect } from 'react'
|
import { useRef, useEffect } from 'react'
|
||||||
import { useHistory } from '@/hooks/useHistory'
|
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||||
import { HistoryItem } from '@/components/HistoryItem'
|
import { HistoryItem } from '@/components/HistoryItem'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||||
@@ -15,7 +15,7 @@ interface HistorySidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLoadParams'>) {
|
function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLoadParams'>) {
|
||||||
const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistory()
|
const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistoryContext()
|
||||||
const observerTarget = useRef<HTMLDivElement>(null)
|
const observerTarget = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,7 +99,7 @@ function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLo
|
|||||||
export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) {
|
export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside className="hidden lg:block w-[320px] border-r h-[calc(100vh-64px)]">
|
<aside className="hidden lg:block w-[320px] border-r h-full">
|
||||||
<HistorySidebarContent onLoadParams={onLoadParams} />
|
<HistorySidebarContent onLoadParams={onLoadParams} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { IconLabel } from '@/components/IconLabel'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { ttsApi, jobApi } from '@/lib/api'
|
import { ttsApi, jobApi } from '@/lib/api'
|
||||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||||
|
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||||
import { LoadingState } from '@/components/LoadingState'
|
import { LoadingState } from '@/components/LoadingState'
|
||||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||||
import { PresetSelector } from '@/components/PresetSelector'
|
import { PresetSelector } from '@/components/PresetSelector'
|
||||||
@@ -53,6 +54,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||||
|
const { refresh } = useHistoryContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -112,6 +114,9 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
|
|||||||
const result = await ttsApi.createCustomVoiceJob(data)
|
const result = await ttsApi.createCustomVoiceJob(data)
|
||||||
toast.success('任务已创建')
|
toast.success('任务已创建')
|
||||||
startPolling(result.job_id)
|
startPolling(result.job_id)
|
||||||
|
try {
|
||||||
|
await refresh()
|
||||||
|
} catch {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('创建任务失败')
|
toast.error('创建任务失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { IconLabel } from '@/components/IconLabel'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { ttsApi, jobApi } from '@/lib/api'
|
import { ttsApi, jobApi } from '@/lib/api'
|
||||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||||
|
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||||
import { LoadingState } from '@/components/LoadingState'
|
import { LoadingState } from '@/components/LoadingState'
|
||||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||||
import { AudioInputSelector } from '@/components/AudioInputSelector'
|
import { AudioInputSelector } from '@/components/AudioInputSelector'
|
||||||
@@ -48,6 +49,7 @@ function VoiceCloneForm() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||||
|
const { refresh } = useHistoryContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -93,6 +95,9 @@ function VoiceCloneForm() {
|
|||||||
})
|
})
|
||||||
toast.success('任务已创建')
|
toast.success('任务已创建')
|
||||||
startPolling(result.job_id)
|
startPolling(result.job_id)
|
||||||
|
try {
|
||||||
|
await refresh()
|
||||||
|
} catch {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('创建任务失败')
|
toast.error('创建任务失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { IconLabel } from '@/components/IconLabel'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { ttsApi, jobApi } from '@/lib/api'
|
import { ttsApi, jobApi } from '@/lib/api'
|
||||||
import { useJobPolling } from '@/hooks/useJobPolling'
|
import { useJobPolling } from '@/hooks/useJobPolling'
|
||||||
|
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||||
import { LoadingState } from '@/components/LoadingState'
|
import { LoadingState } from '@/components/LoadingState'
|
||||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||||
import { PresetSelector } from '@/components/PresetSelector'
|
import { PresetSelector } from '@/components/PresetSelector'
|
||||||
@@ -51,6 +52,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
|
||||||
|
const { refresh } = useHistoryContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -103,6 +105,9 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
|
|||||||
const result = await ttsApi.createVoiceDesignJob(data)
|
const result = await ttsApi.createVoiceDesignJob(data)
|
||||||
toast.success('任务已创建')
|
toast.success('任务已创建')
|
||||||
startPolling(result.job_id)
|
startPolling(result.job_id)
|
||||||
|
try {
|
||||||
|
await refresh()
|
||||||
|
} catch {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('创建任务失败')
|
toast.error('创建任务失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
126
qwen3-tts-frontend/src/contexts/HistoryContext.tsx
Normal file
126
qwen3-tts-frontend/src/contexts/HistoryContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'
|
||||||
|
import { jobApi } from '@/lib/api'
|
||||||
|
import type { Job } from '@/types/job'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
interface HistoryContextType {
|
||||||
|
jobs: Job[]
|
||||||
|
total: number
|
||||||
|
loading: boolean
|
||||||
|
loadingMore: boolean
|
||||||
|
hasMore: boolean
|
||||||
|
error: string | null
|
||||||
|
loadMore: () => Promise<void>
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
retry: () => Promise<void>
|
||||||
|
deleteJob: (id: number) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryContext = createContext<HistoryContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function HistoryProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [jobs, setJobs] = useState<Job[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [skip, setSkip] = useState(0)
|
||||||
|
const limit = 20
|
||||||
|
|
||||||
|
const hasMore = jobs.length < total
|
||||||
|
|
||||||
|
const loadJobs = useCallback(async (currentSkip: number, isLoadMore = false) => {
|
||||||
|
try {
|
||||||
|
if (isLoadMore) {
|
||||||
|
setLoadingMore(true)
|
||||||
|
} else {
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const response = await jobApi.listJobs(currentSkip, limit)
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
setJobs(prev => [...prev, ...response.jobs])
|
||||||
|
} else {
|
||||||
|
setJobs(response.jobs)
|
||||||
|
}
|
||||||
|
setTotal(response.total)
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.message || '加载历史记录失败'
|
||||||
|
setError(errorMessage)
|
||||||
|
toast.error(errorMessage)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setLoadingMore(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (loadingMore || !hasMore) return
|
||||||
|
const newSkip = skip + limit
|
||||||
|
setSkip(newSkip)
|
||||||
|
await loadJobs(newSkip, true)
|
||||||
|
}, [skip, loadingMore, hasMore, loadJobs])
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setSkip(0)
|
||||||
|
await loadJobs(0, false)
|
||||||
|
}, [loadJobs])
|
||||||
|
|
||||||
|
const retry = useCallback(async () => {
|
||||||
|
setSkip(0)
|
||||||
|
await loadJobs(0, false)
|
||||||
|
}, [loadJobs])
|
||||||
|
|
||||||
|
const deleteJob = useCallback(async (id: number) => {
|
||||||
|
const previousJobs = [...jobs]
|
||||||
|
const previousTotal = total
|
||||||
|
|
||||||
|
setJobs(prev => prev.filter(job => job.id !== id))
|
||||||
|
setTotal(prev => prev - 1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await jobApi.deleteJob(id)
|
||||||
|
toast.success('删除成功')
|
||||||
|
} catch (error) {
|
||||||
|
setJobs(previousJobs)
|
||||||
|
setTotal(previousTotal)
|
||||||
|
toast.error('删除失败')
|
||||||
|
}
|
||||||
|
}, [jobs, total])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadJobs(0, false)
|
||||||
|
}, [loadJobs])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
jobs,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
loadMore,
|
||||||
|
refresh,
|
||||||
|
retry,
|
||||||
|
deleteJob,
|
||||||
|
}),
|
||||||
|
[jobs, total, loading, loadingMore, hasMore, error, loadMore, refresh, retry, deleteJob]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HistoryContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</HistoryContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHistoryContext() {
|
||||||
|
const context = useContext(HistoryContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useHistoryContext must be used within HistoryProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
|
|||||||
import { jobApi } from '@/lib/api'
|
import { jobApi } from '@/lib/api'
|
||||||
import type { Job, JobStatus } from '@/types/job'
|
import type { Job, JobStatus } from '@/types/job'
|
||||||
import { POLL_INTERVAL } from '@/lib/constants'
|
import { POLL_INTERVAL } from '@/lib/constants'
|
||||||
|
import { useHistoryContext } from '@/contexts/HistoryContext'
|
||||||
|
|
||||||
interface JobContextType {
|
interface JobContextType {
|
||||||
currentJob: Job | null
|
currentJob: Job | null
|
||||||
@@ -23,6 +24,8 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [elapsedTime, setElapsedTime] = useState(0)
|
const [elapsedTime, setElapsedTime] = useState(0)
|
||||||
|
|
||||||
|
const { refresh: historyRefresh } = useHistoryContext()
|
||||||
|
|
||||||
const stopJob = useCallback(() => {
|
const stopJob = useCallback(() => {
|
||||||
setCurrentJob(null)
|
setCurrentJob(null)
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
@@ -61,11 +64,17 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
|||||||
if (pollInterval) clearInterval(pollInterval)
|
if (pollInterval) clearInterval(pollInterval)
|
||||||
if (timeInterval) clearInterval(timeInterval)
|
if (timeInterval) clearInterval(timeInterval)
|
||||||
toast.success('任务完成!')
|
toast.success('任务完成!')
|
||||||
|
try {
|
||||||
|
historyRefresh()
|
||||||
|
} catch {}
|
||||||
} else if (job.status === 'failed') {
|
} else if (job.status === 'failed') {
|
||||||
if (pollInterval) clearInterval(pollInterval)
|
if (pollInterval) clearInterval(pollInterval)
|
||||||
if (timeInterval) clearInterval(timeInterval)
|
if (timeInterval) clearInterval(timeInterval)
|
||||||
setError(job.error_message || '任务失败')
|
setError(job.error_message || '任务失败')
|
||||||
toast.error(job.error_message || '任务失败')
|
toast.error(job.error_message || '任务失败')
|
||||||
|
try {
|
||||||
|
historyRefresh()
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (pollInterval) clearInterval(pollInterval)
|
if (pollInterval) clearInterval(pollInterval)
|
||||||
@@ -86,7 +95,7 @@ export function JobProvider({ children }: { children: ReactNode }) {
|
|||||||
if (pollInterval) clearInterval(pollInterval)
|
if (pollInterval) clearInterval(pollInterval)
|
||||||
if (timeInterval) clearInterval(timeInterval)
|
if (timeInterval) clearInterval(timeInterval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [historyRefresh])
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -50,17 +50,17 @@ function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="h-screen overflow-hidden flex flex-col bg-background">
|
||||||
<Navbar onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} />
|
<Navbar onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
<HistorySidebar
|
<HistorySidebar
|
||||||
open={sidebarOpen}
|
open={sidebarOpen}
|
||||||
onOpenChange={setSidebarOpen}
|
onOpenChange={setSidebarOpen}
|
||||||
onLoadParams={handleLoadParams}
|
onLoadParams={handleLoadParams}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="flex-1 container mx-auto p-3 md:p-6 max-w-[800px] md:max-w-[700px]">
|
<main className="flex-1 overflow-y-auto container mx-auto p-3 md:p-6 max-w-[800px] md:max-w-[700px]">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Tabs value={currentTab} onValueChange={setCurrentTab}>
|
<Tabs value={currentTab} onValueChange={setCurrentTab}>
|
||||||
|
|||||||
Reference in New Issue
Block a user