feat: async task pipeline for media and llm workflows

这个提交包含在:
cryptocommuniums-afk
2026-03-15 00:12:26 +08:00
父节点 1cc863e60e
当前提交 20e183d2da
修改 36 个文件,包含 1961 行新增339 行删除

查看文件

@@ -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>
</>