feat: async task pipeline for media and llm workflows
这个提交包含在:
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/_core/hooks/useAuth";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
@@ -6,10 +6,12 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { useBackgroundTask } from "@/hooks/useBackgroundTask";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Target, Loader2, CheckCircle2, Circle, Clock, Dumbbell,
|
||||
RefreshCw, Footprints, Hand, ArrowRight, Sparkles
|
||||
RefreshCw, Footprints, Hand, ArrowRight, Sparkles, ListTodo
|
||||
} from "lucide-react";
|
||||
|
||||
const categoryIcons: Record<string, React.ReactNode> = {
|
||||
@@ -42,24 +44,26 @@ export default function Training() {
|
||||
const [skillLevel, setSkillLevel] = useState<"beginner" | "intermediate" | "advanced">("beginner");
|
||||
const [durationDays, setDurationDays] = useState(7);
|
||||
const [selectedDay, setSelectedDay] = useState(1);
|
||||
const [generateTaskId, setGenerateTaskId] = useState<string | null>(null);
|
||||
const [adjustTaskId, setAdjustTaskId] = useState<string | null>(null);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: activePlan, isLoading: planLoading } = trpc.plan.active.useQuery();
|
||||
const generateTaskQuery = useBackgroundTask(generateTaskId);
|
||||
const adjustTaskQuery = useBackgroundTask(adjustTaskId);
|
||||
|
||||
const generateMutation = trpc.plan.generate.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("训练计划已生成!");
|
||||
utils.plan.active.invalidate();
|
||||
utils.plan.list.invalidate();
|
||||
onSuccess: (data) => {
|
||||
setGenerateTaskId(data.taskId);
|
||||
toast.success("训练计划任务已提交");
|
||||
},
|
||||
onError: (err) => toast.error("生成失败: " + err.message),
|
||||
});
|
||||
|
||||
const adjustMutation = trpc.plan.adjust.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success("训练计划已调整!");
|
||||
utils.plan.active.invalidate();
|
||||
if (data.adjustmentNotes) toast.info("调整说明: " + data.adjustmentNotes);
|
||||
setAdjustTaskId(data.taskId);
|
||||
toast.success("训练计划调整任务已提交");
|
||||
},
|
||||
onError: (err) => toast.error("调整失败: " + err.message),
|
||||
});
|
||||
@@ -81,6 +85,36 @@ export default function Training() {
|
||||
}, [activePlan, selectedDay]);
|
||||
|
||||
const totalDays = activePlan?.durationDays || 7;
|
||||
const generating = generateMutation.isPending || generateTaskQuery.data?.status === "queued" || generateTaskQuery.data?.status === "running";
|
||||
const adjusting = adjustMutation.isPending || adjustTaskQuery.data?.status === "queued" || adjustTaskQuery.data?.status === "running";
|
||||
|
||||
useEffect(() => {
|
||||
if (generateTaskQuery.data?.status === "succeeded") {
|
||||
toast.success("训练计划已生成");
|
||||
utils.plan.active.invalidate();
|
||||
utils.plan.list.invalidate();
|
||||
setGenerateTaskId(null);
|
||||
} else if (generateTaskQuery.data?.status === "failed") {
|
||||
toast.error(`训练计划生成失败: ${generateTaskQuery.data.error || "未知错误"}`);
|
||||
setGenerateTaskId(null);
|
||||
}
|
||||
}, [generateTaskQuery.data, utils.plan.active, utils.plan.list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (adjustTaskQuery.data?.status === "succeeded") {
|
||||
toast.success("训练计划已调整");
|
||||
utils.plan.active.invalidate();
|
||||
utils.plan.list.invalidate();
|
||||
const adjustmentNotes = (adjustTaskQuery.data.result as { adjustmentNotes?: string } | null)?.adjustmentNotes;
|
||||
if (adjustmentNotes) {
|
||||
toast.info(`调整说明: ${adjustmentNotes}`);
|
||||
}
|
||||
setAdjustTaskId(null);
|
||||
} else if (adjustTaskQuery.data?.status === "failed") {
|
||||
toast.error(`训练计划调整失败: ${adjustTaskQuery.data.error || "未知错误"}`);
|
||||
setAdjustTaskId(null);
|
||||
}
|
||||
}, [adjustTaskQuery.data, utils.plan.active, utils.plan.list]);
|
||||
|
||||
if (planLoading) {
|
||||
return (
|
||||
@@ -100,6 +134,17 @@ export default function Training() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{generating || adjusting ? (
|
||||
<Alert>
|
||||
<ListTodo className="h-4 w-4" />
|
||||
<AlertTitle>后台任务执行中</AlertTitle>
|
||||
<AlertDescription>
|
||||
{generating ? "训练计划正在后台生成。" : "训练计划正在根据最近分析结果调整。"}
|
||||
你可以切换到其他页面,完成后会在任务中心显示结果。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!activePlan ? (
|
||||
/* Generate new plan */
|
||||
<Card className="border-0 shadow-sm">
|
||||
@@ -145,11 +190,11 @@ export default function Training() {
|
||||
<Button
|
||||
data-testid="training-generate-button"
|
||||
onClick={() => generateMutation.mutate({ skillLevel, durationDays })}
|
||||
disabled={generateMutation.isPending}
|
||||
disabled={generating}
|
||||
className="w-full sm:w-auto gap-2"
|
||||
>
|
||||
{generateMutation.isPending ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" />生成中...</>
|
||||
{generating ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" />后台生成中...</>
|
||||
) : (
|
||||
<><Sparkles className="h-4 w-4" />生成训练计划</>
|
||||
)}
|
||||
@@ -178,10 +223,10 @@ export default function Training() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => adjustMutation.mutate({ planId: activePlan.id })}
|
||||
disabled={adjustMutation.isPending}
|
||||
disabled={adjusting}
|
||||
className="gap-1"
|
||||
>
|
||||
{adjustMutation.isPending ? (
|
||||
{adjusting ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
@@ -274,11 +319,11 @@ export default function Training() {
|
||||
onClick={() => {
|
||||
generateMutation.mutate({ skillLevel, durationDays });
|
||||
}}
|
||||
disabled={generateMutation.isPending}
|
||||
disabled={generating}
|
||||
className="gap-2"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
重新生成计划
|
||||
{generating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
|
||||
{generating ? "后台生成中..." : "重新生成计划"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
在新工单中引用
屏蔽一个用户