geo/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx

388 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Loader2,
Rocket,
Target,
TrendingUp,
Plus,
Users,
BarChart3,
ArrowLeft,
CheckCircle2,
LayoutDashboard,
AlertTriangle,
Crown,
Zap,
} from "lucide-react";
import { api } from "@/lib/api";
import {
UpgradePrompt,
PaidActionOverlay,
} from "@/components/subscription/UpgradePrompt";
interface ActionSuggestionItem {
id?: string;
title: string;
description: string;
priority: "high" | "medium" | "low";
action_type: string;
is_paid_action?: boolean;
action_button_text?: string;
}
interface Step5ActionSuggestionsProps {
brandId: string;
brandName: string;
onComplete: () => void;
onBack: () => void;
onSkip: () => void;
}
const ACTION_ICONS: Record<string, React.ElementType> = {
improve_platform: BarChart3,
add_competitor: Users,
optimize_content: TrendingUp,
increase_frequency: Rocket,
coverage: Target,
keyword: Zap,
platform: BarChart3,
dimension: TrendingUp,
competitive: Users,
upgrade: Crown,
monitor: LayoutDashboard,
brand_info: Target,
sentiment: TrendingUp,
};
const PRIORITY_COLORS: Record<
string,
{ bg: string; text: string; border: string }
> = {
high: { bg: "bg-red-50", text: "text-red-600", border: "border-red-200" },
medium: {
bg: "bg-amber-50",
text: "text-amber-600",
border: "border-amber-200",
},
low: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
};
export function Step5ActionSuggestions({
brandId,
brandName,
onComplete,
onBack,
onSkip,
}: Step5ActionSuggestionsProps) {
const { data: session } = useSession();
const router = useRouter();
const [suggestions, setSuggestions] = useState<ActionSuggestionItem[]>([]);
const [loading, setLoading] = useState(true);
const [completing, setCompleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchSuggestions = async () => {
if (!session?.accessToken) return;
try {
setLoading(true);
setError(null);
const data = (await api.onboarding.getActionSuggestions(
session.accessToken,
brandId,
)) as { suggestions: ActionSuggestionItem[] } | ActionSuggestionItem[];
const items = Array.isArray(data)
? data
: (data as { suggestions: ActionSuggestionItem[] }).suggestions || [];
setSuggestions(items);
} catch (err) {
setError("获取行动建议失败,请重试");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSuggestions();
}, [session?.accessToken, brandId]);
const handleComplete = async () => {
if (!session?.accessToken) return;
try {
setCompleting(true);
setError(null);
await api.onboarding.completeOnboarding(session.accessToken, brandId);
onComplete();
} catch (err) {
setError("完成引导失败,请重试");
} finally {
setCompleting(false);
}
};
const handleActionClick = (suggestion: ActionSuggestionItem) => {
if (suggestion.is_paid_action) {
return;
}
switch (suggestion.action_type) {
case "keyword":
case "coverage":
router.push("/dashboard");
break;
default:
router.push("/dashboard");
}
};
if (loading) {
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-6 text-center">
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
<Target className="h-8 w-8 text-primary animate-pulse" />
</div>
<h2 className="mb-2 text-2xl font-bold">...</h2>
<p className="text-muted-foreground">
</p>
</div>
<Card className="w-full max-w-2xl">
<CardContent className="pt-6 space-y-4">
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</CardContent>
</Card>
</div>
);
}
if (!loading && error) {
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-6 text-center">
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
<AlertTriangle className="h-8 w-8 text-red-600" />
</div>
<h2 className="mb-2 text-2xl font-bold"></h2>
<p className="text-muted-foreground">{error}</p>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={fetchSuggestions}>
</Button>
<Button variant="ghost" onClick={onSkip}>
</Button>
</div>
</div>
);
}
const highPriority = suggestions.filter((s) => s.priority === "high");
const mediumPriority = suggestions.filter((s) => s.priority === "medium");
const lowPriority = suggestions.filter((s) => s.priority === "low");
const renderSuggestionCard = (
suggestion: ActionSuggestionItem,
index: number,
) => {
const Icon = ACTION_ICONS[suggestion.action_type] || Target;
const colors = PRIORITY_COLORS[suggestion.priority];
const isPaid = suggestion.is_paid_action || false;
const actionButton = suggestion.action_button_text ? (
<Button
variant={isPaid ? "outline" : "default"}
size="sm"
className={`gap-1 ${isPaid ? "border-amber-300 text-amber-700 hover:bg-amber-50" : ""}`}
onClick={() => handleActionClick(suggestion)}
>
{isPaid && <Crown className="h-3 w-3" />}
{suggestion.action_button_text}
</Button>
) : null;
return (
<div
key={suggestion.title + index}
className={`flex gap-4 rounded-lg border p-4 ${colors.border} ${colors.bg}`}
>
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full ${colors.bg} ${colors.text}`}
>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold">{suggestion.title}</h4>
<Badge
variant="outline"
className={`${colors.bg} ${colors.text} border-0 text-xs`}
>
{suggestion.priority === "high"
? "高优"
: suggestion.priority === "medium"
? "中优"
: "低优"}
</Badge>
{isPaid && (
<Badge
variant="outline"
className="border-amber-300 bg-amber-50 text-amber-700 text-xs gap-0.5"
>
<Crown className="h-3 w-3" />
Pro
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{suggestion.description}
</p>
{actionButton && <div className="mt-2">{actionButton}</div>}
</div>
<div className="flex items-center">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-medium text-primary">
{index + 1}
</span>
</div>
</div>
</div>
);
};
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-6 text-center">
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-emerald-50">
<Rocket className="h-8 w-8 text-emerald-600" />
</div>
<h2 className="mb-2 text-2xl font-bold"></h2>
<p className="text-muted-foreground">
&ldquo;{brandName}&rdquo;
</p>
</div>
{highPriority.length > 0 && (
<Card className="w-full max-w-2xl mb-4 border-red-200">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg text-red-600">
<Target className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{highPriority.map((suggestion, index) =>
renderSuggestionCard(suggestion, index),
)}
</CardContent>
</Card>
)}
{mediumPriority.length > 0 && (
<Card className="w-full max-w-2xl mb-4 border-amber-200">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg text-amber-600">
<TrendingUp className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{mediumPriority.map((suggestion, index) =>
renderSuggestionCard(suggestion, index + highPriority.length),
)}
</CardContent>
</Card>
)}
{lowPriority.length > 0 && (
<Card className="w-full max-w-2xl mb-4 border-blue-200">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg text-blue-600">
<Plus className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{lowPriority.map((suggestion, index) =>
renderSuggestionCard(
suggestion,
index + highPriority.length + mediumPriority.length,
),
)}
</CardContent>
</Card>
)}
<div className="mt-4 w-full max-w-2xl">
<UpgradePrompt
variant="inline"
feature="完整优化方案"
description="免费版仅展示P0级别建议。升级Pro可获取完整优化方案、AI自动执行和持续监控。"
/>
</div>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Button
type="button"
variant="outline"
onClick={onBack}
className="flex-1"
>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button
type="button"
variant="outline"
onClick={onSkip}
className="flex-1"
>
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</Button>
</div>
<Button
type="button"
onClick={handleComplete}
disabled={completing}
size="lg"
className="mt-3 w-full max-w-2xl"
>
{completing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<CheckCircle2 className="mr-2 h-4 w-4" />
Dashboard
</>
)}
</Button>
<p className="mt-4 text-xs text-muted-foreground">
</p>
</div>
);
}