geo/frontend/app/(dashboard)/dashboard/lifecycle/new/page.tsx

288 lines
9.5 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 } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertCard } from "@/components/business/alert-card";
import { fetchWithAuth } from "@/lib/api/client";
import { Check } from "lucide-react";
interface ProjectResponse {
id: string;
brand_name: string;
brand_url: string;
description?: string;
status: string;
current_stage: string;
stages: string[];
}
type StepStatus = "waiting" | "active" | "completed";
interface ProgressStep {
id: string;
label: string;
status: StepStatus;
}
export default function NewProjectPage() {
const router = useRouter();
const { data: session } = useSession();
const [brandName, setBrandName] = useState("");
const [brandUrl, setBrandUrl] = useState("");
const [description, setDescription] = useState("");
const [touched, setTouched] = useState({ brandName: false, brandUrl: false });
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [steps, setSteps] = useState<ProgressStep[]>([
{ id: "create", label: "正在创建项目...", status: "waiting" },
{ id: "init", label: "正在初始化分析阶段...", status: "waiting" },
{ id: "agent", label: "正在配置AI Agent...", status: "waiting" },
{ id: "ready", label: "准备就绪!正在跳转...", status: "waiting" },
]);
const brandNameError = touched.brandName && !brandName.trim();
const brandUrlError = touched.brandUrl && !brandUrl.trim();
const canSubmit =
brandName.trim().length > 0 && brandUrl.trim().length > 0 && !submitting;
async function handleSubmit() {
if (!canSubmit) return;
setSubmitting(true);
setError(null);
const token = session?.accessToken as string | undefined;
try {
const data = await fetchWithAuth(
"/api/v1/lifecycle/projects/quick-start",
{
method: "POST",
body: JSON.stringify({
brand_name: brandName.trim(),
brand_url: brandUrl.trim(),
description: description.trim() || undefined,
}),
},
token
) as { project: ProjectResponse; message: string };
void data;
animateSteps();
} catch (err) {
setSubmitting(false);
setError(
err instanceof Error ? err.message : "网络错误,请检查连接后重试"
);
}
}
function animateSteps() {
const stepOrder = ["create", "init", "agent", "ready"] as const;
setSteps((prev) =>
prev.map((step, i) =>
i === 0 ? { ...step, status: "active" as StepStatus } : step
)
);
stepOrder.forEach((stepId, index) => {
setTimeout(() => {
setSteps((prev) =>
prev.map((step, i) => {
if (step.id === stepId) return { ...step, status: "completed" };
if (i === index + 1) return { ...step, status: "active" };
return step;
})
);
if (index === stepOrder.length - 1) {
setTimeout(() => {
router.push("/dashboard/lifecycle");
}, 800);
}
}, (index + 1) * 800);
});
}
function handleRetry() {
setError(null);
void handleSubmit();
}
if (submitting) {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center bg-geo-bg px-4">
<div className="w-full max-w-lg space-y-8">
<div className="text-center">
<h2 className="text-2xl font-bold text-geo-text-primary">
</h2>
<p className="mt-2 text-geo-text-secondary">
AI正在自动初始化您的项目
</p>
</div>
<div className="space-y-4">
{steps.map((step) => (
<div key={step.id} className="flex items-center gap-4">
<div
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-bold transition-colors duration-300 ${
step.status === "completed"
? "bg-geo-green text-white"
: step.status === "active"
? "bg-geo-green/10 text-geo-green animate-pulse"
: "bg-gray-100 text-gray-400"
}`}
>
{step.status === "completed" ? (
<Check className="h-4 w-4" />
) : (
<span className="h-2 w-2 rounded-full bg-current" />
)}
</div>
<span
className={`text-sm font-medium transition-colors duration-300 ${
step.status === "completed"
? "text-geo-green"
: step.status === "active"
? "text-geo-text-primary"
: "text-gray-400"
}`}
>
{step.label}
</span>
</div>
))}
</div>
</div>
</div>
);
}
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center bg-geo-bg px-4 py-12">
<div className="w-full max-w-lg space-y-8">
{/* Header */}
<div className="text-center">
<h1 className="text-3xl font-bold text-geo-text-primary">
AI可见性
</h1>
<p className="mt-3 text-geo-text-secondary">
AI将自动分析并制定优化方案
</p>
</div>
{/* Error Alert */}
{error && (
<AlertCard
alerts={[
{
id: "submit-error",
title: "提交失败",
description: error,
severity: "critical",
actions: (
<Button
variant="outline"
size="sm"
onClick={handleRetry}
className="text-xs"
>
</Button>
),
},
]}
maxVisible={1}
/>
)}
{/* Form Card */}
<div className="bg-white rounded-xl shadow-geo-card p-8 space-y-6">
<div className="space-y-2">
<Label
htmlFor="brandName"
className="text-sm font-medium text-geo-text-primary"
>
<span className="text-destructive">*</span>
</Label>
<Input
id="brandName"
placeholder="例如:您的品牌名"
value={brandName}
onChange={(e) => setBrandName(e.target.value)}
onBlur={() =>
setTouched((prev) => ({ ...prev, brandName: true }))
}
className={
brandNameError
? "border-destructive focus-visible:ring-destructive"
: ""
}
/>
{brandNameError && (
<p className="text-xs text-destructive"></p>
)}
</div>
<div className="space-y-2">
<Label
htmlFor="brandUrl"
className="text-sm font-medium text-geo-text-primary"
>
<span className="text-destructive">*</span>
</Label>
<Input
id="brandUrl"
type="url"
placeholder="https://example.com"
value={brandUrl}
onChange={(e) => setBrandUrl(e.target.value)}
onBlur={() =>
setTouched((prev) => ({ ...prev, brandUrl: true }))
}
className={
brandUrlError
? "border-destructive focus-visible:ring-destructive"
: ""
}
/>
{brandUrlError && (
<p className="text-xs text-destructive"></p>
)}
</div>
<div className="space-y-2">
<Label
htmlFor="description"
className="text-sm font-medium text-geo-text-primary"
>
</Label>
<textarea
id="description"
placeholder="简单描述您的品牌帮助AI更好理解可选"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background transition-colors duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:border-primary hover:border-primary/40 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted resize-none"
/>
</div>
<Button
onClick={handleSubmit}
disabled={!canSubmit}
className="w-full h-12 bg-geo-green text-white hover:bg-geo-green/90 hover:shadow-md hover:scale-[1.02] active:scale-[0.98] text-base font-semibold transition-all duration-200 disabled:opacity-50 disabled:hover:scale-100 disabled:hover:shadow-none"
>
</Button>
</div>
</div>
</div>
);
}