678 lines
25 KiB
TypeScript
678 lines
25 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useCallback, useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import {
|
||
Sparkles,
|
||
Calendar,
|
||
Type,
|
||
Loader2,
|
||
Check,
|
||
Circle,
|
||
Rocket,
|
||
PenTool,
|
||
Wand2,
|
||
Eye,
|
||
AlertCircle,
|
||
RefreshCw,
|
||
} from "lucide-react";
|
||
import {
|
||
contentsApi,
|
||
contentGenerationApi,
|
||
knowledgeApi,
|
||
type Content,
|
||
type KnowledgeBase,
|
||
} from "@/lib/api";
|
||
|
||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||
|
||
type PipelineStepStatus = "completed" | "active" | "pending";
|
||
|
||
interface PipelineStep {
|
||
id: string;
|
||
title: string;
|
||
status: PipelineStepStatus;
|
||
}
|
||
|
||
// ─── Static Options ───────────────────────────────────────────────────────────
|
||
|
||
const platformOptions = [
|
||
{ value: "wechat", label: "微信公众号" },
|
||
{ value: "zhihu", label: "知乎" },
|
||
{ value: "xiaohongshu", label: "小红书" },
|
||
{ value: "baijiahao", label: "百家号" },
|
||
{ value: "douyin", label: "抖音" },
|
||
{ value: "general", label: "通用" },
|
||
];
|
||
|
||
const styleOptions = [
|
||
{ value: "professional", label: "专业严谨" },
|
||
{ value: "casual", label: "轻松活泼" },
|
||
{ value: "academic", label: "学术深度" },
|
||
];
|
||
|
||
const wordCountOptions = [
|
||
{ value: "1000", label: "1000字" },
|
||
{ value: "2000", label: "2000字" },
|
||
{ value: "3000", label: "3000字" },
|
||
{ value: "5000", label: "5000字" },
|
||
];
|
||
|
||
const pipelineStepsTemplate: PipelineStep[] = [
|
||
{ id: "topic", title: "选题分析", status: "pending" },
|
||
{ id: "generate", title: "内容生成", status: "pending" },
|
||
{ id: "deai", title: "去AI化处理", status: "pending" },
|
||
{ id: "geo", title: "GEO优化", status: "pending" },
|
||
{ id: "rule", title: "规则验证", status: "pending" },
|
||
{ id: "media", title: "多媒体内容插入", status: "pending" },
|
||
];
|
||
|
||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
||
function getStatusBadgeConfig(status: Content["status"]) {
|
||
switch (status) {
|
||
case "draft":
|
||
return { label: "草稿", className: "bg-gray-50 text-gray-600 border-gray-200" };
|
||
case "review":
|
||
return { label: "待审核", className: "bg-purple-50 text-purple-600 border-purple-200" };
|
||
case "approved":
|
||
return { label: "已审核", className: "bg-blue-50 text-blue-600 border-blue-200" };
|
||
case "published":
|
||
return { label: "已发布", className: "bg-primary/10 text-primary border-primary/20" };
|
||
case "archived":
|
||
return { label: "已归档", className: "bg-slate-50 text-slate-600 border-slate-200" };
|
||
default:
|
||
return { label: "未知", className: "bg-muted text-muted-foreground" };
|
||
}
|
||
}
|
||
|
||
function getContentTypeBadge(type: Content["content_type"]) {
|
||
const map: Record<string, string> = {
|
||
article: "bg-blue-50 text-blue-700 border-blue-200",
|
||
qa: "bg-green-50 text-green-700 border-green-200",
|
||
knowledge_base: "bg-amber-50 text-amber-700 border-amber-200",
|
||
social_post: "bg-red-50 text-red-700 border-red-200",
|
||
other: "bg-gray-50 text-gray-600 border-gray-200",
|
||
};
|
||
const labelMap: Record<string, string> = {
|
||
article: "文章",
|
||
qa: "问答",
|
||
knowledge_base: "知识库",
|
||
social_post: "社媒",
|
||
other: "其他",
|
||
};
|
||
return { className: map[type] ?? map.other, label: labelMap[type] ?? type };
|
||
}
|
||
|
||
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||
|
||
function ContentCard({ item }: { item: Content }) {
|
||
const statusConfig = getStatusBadgeConfig(item.status);
|
||
const typeConfig = getContentTypeBadge(item.content_type);
|
||
const wordCount = item.body ? item.body.length : 0;
|
||
const dateStr = new Date(item.created_at).toLocaleDateString("zh-CN");
|
||
|
||
return (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-5 hover:border-gray-300 transition-colors group cursor-pointer">
|
||
<div>
|
||
<div className="flex items-start justify-between gap-3 mb-3">
|
||
<h3 className="text-base font-semibold text-gray-900 leading-snug line-clamp-2 group-hover:text-primary transition-colors">
|
||
{item.title}
|
||
</h3>
|
||
</div>
|
||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||
<Badge variant="outline" className={`text-xs font-medium ${typeConfig.className}`}>
|
||
{typeConfig.label}
|
||
</Badge>
|
||
<Badge variant="outline" className={`text-xs font-medium ${statusConfig.className}`}>
|
||
{statusConfig.label}
|
||
</Badge>
|
||
</div>
|
||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||
<div className="flex items-center gap-3">
|
||
<span className="flex items-center gap-1">
|
||
<Type className="h-3.5 w-3.5" />
|
||
{wordCount > 0 ? `${wordCount} 字` : "暂无内容"}
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<Calendar className="h-3.5 w-3.5" />
|
||
{dateStr}
|
||
</span>
|
||
</div>
|
||
{item.tags && item.tags.length > 0 && (
|
||
<span className="text-muted-foreground truncate max-w-[100px]">
|
||
{item.tags.slice(0, 2).join(", ")}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PipelineTimeline({ steps }: { steps: PipelineStep[] }) {
|
||
return (
|
||
<div className="flex flex-col gap-0 py-2">
|
||
{steps.map((step, idx) => {
|
||
const isLast = idx === steps.length - 1;
|
||
const isCompleted = step.status === "completed";
|
||
const isActive = step.status === "active";
|
||
const isPending = step.status === "pending";
|
||
return (
|
||
<div key={step.id} className="flex gap-4">
|
||
<div className="flex flex-col items-center">
|
||
<div
|
||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors duration-300 ${
|
||
isCompleted
|
||
? "bg-primary text-primary-foreground"
|
||
: isActive
|
||
? "bg-blue-50 border-2 border-blue-500 text-blue-500"
|
||
: "bg-white border-2 border-gray-200 text-gray-300"
|
||
}`}
|
||
>
|
||
{isCompleted && <Check className="h-4 w-4" />}
|
||
{isActive && <Loader2 className="h-4 w-4 animate-spin" />}
|
||
{isPending && <Circle className="h-3 w-3" />}
|
||
</div>
|
||
{!isLast && (
|
||
<div
|
||
className={`w-0.5 flex-1 my-1 min-h-[1.5rem] transition-colors duration-300 ${
|
||
isCompleted ? "bg-primary" : "bg-gray-200"
|
||
}`}
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className={`flex-1 pb-5 ${isLast ? "pb-0" : ""}`}>
|
||
<div className="flex items-center gap-2 h-8">
|
||
<span
|
||
className={`text-sm font-medium ${
|
||
isCompleted || isActive ? "text-foreground" : "text-muted-foreground"
|
||
}`}
|
||
>
|
||
{step.title}
|
||
</span>
|
||
{isCompleted && <span className="text-xs text-primary">完成</span>}
|
||
{isActive && <span className="text-xs text-blue-500">进行中</span>}
|
||
{isPending && <span className="text-xs text-muted-foreground">等待中</span>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function EmptyState({ onGenerate }: { onGenerate: () => void }) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center rounded-xl border border-gray-200 bg-white py-20 text-center">
|
||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
|
||
<PenTool className="h-6 w-6 text-gray-400" />
|
||
</div>
|
||
<h3 className="text-base font-semibold text-gray-900">还没有内容</h3>
|
||
<p className="mt-2 mb-6 max-w-sm text-sm text-gray-500">
|
||
让AI帮你创作第一篇内容,开启智能内容生产之旅
|
||
</p>
|
||
<Button
|
||
variant="outline"
|
||
onClick={onGenerate}
|
||
>
|
||
<Sparkles className="mr-2 h-4 w-4" />
|
||
AI生成新内容
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Main Page ───────────────────────────────────────────────────────────────
|
||
|
||
export default function ContentPage() {
|
||
const router = useRouter();
|
||
|
||
// Data states
|
||
const [contents, setContents] = useState<Content[]>([]);
|
||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||
const [pageLoading, setPageLoading] = useState(true);
|
||
const [pageError, setPageError] = useState<string | null>(null);
|
||
|
||
// Dialog states
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
const [dialogMode, setDialogMode] = useState<"form" | "pipeline" | "result">("form");
|
||
|
||
// Form states
|
||
const [keyword, setKeyword] = useState("");
|
||
const [platform, setPlatform] = useState("");
|
||
const [selectedKbs, setSelectedKbs] = useState<string[]>([]);
|
||
const [style, setStyle] = useState("professional");
|
||
const [wordCount, setWordCount] = useState("2000");
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||
const [generatedContent, setGeneratedContent] = useState<string | null>(null);
|
||
|
||
// Pipeline states
|
||
const [pipelineSteps, setPipelineSteps] = useState<PipelineStep[]>(
|
||
pipelineStepsTemplate.map((s) => ({ ...s }))
|
||
);
|
||
|
||
// Fetch content list and knowledge bases on mount
|
||
useEffect(() => {
|
||
async function fetchPageData() {
|
||
try {
|
||
setPageLoading(true);
|
||
setPageError(null);
|
||
const [contentList, kbList] = await Promise.all([
|
||
contentsApi.list(),
|
||
knowledgeApi.listBases(undefined, "enterprise"),
|
||
]);
|
||
setContents(contentList ?? []);
|
||
setKnowledgeBases(kbList ?? []);
|
||
} catch (err) {
|
||
console.error("Content page fetch error:", err);
|
||
setPageError(err instanceof Error ? err.message : "数据加载失败");
|
||
} finally {
|
||
setPageLoading(false);
|
||
}
|
||
}
|
||
fetchPageData();
|
||
}, []);
|
||
|
||
const resetForm = useCallback(() => {
|
||
setKeyword("");
|
||
setPlatform("");
|
||
setSelectedKbs([]);
|
||
setStyle("professional");
|
||
setWordCount("2000");
|
||
setDialogMode("form");
|
||
setPipelineSteps(pipelineStepsTemplate.map((s) => ({ ...s })));
|
||
setIsSubmitting(false);
|
||
setSubmitError(null);
|
||
setGeneratedContent(null);
|
||
}, []);
|
||
|
||
const handleOpenDialog = useCallback(() => {
|
||
resetForm();
|
||
setDialogOpen(true);
|
||
}, [resetForm]);
|
||
|
||
const handleCloseDialog = useCallback(() => {
|
||
setDialogOpen(false);
|
||
setTimeout(() => resetForm(), 300);
|
||
}, [resetForm]);
|
||
|
||
const handleSubmit = useCallback(async () => {
|
||
if (!keyword.trim() || !platform) return;
|
||
|
||
setIsSubmitting(true);
|
||
setSubmitError(null);
|
||
setDialogMode("pipeline");
|
||
|
||
// Start pipeline animation
|
||
const steps = pipelineStepsTemplate.map((s) => ({ ...s }));
|
||
steps[0].status = "active";
|
||
setPipelineSteps([...steps]);
|
||
|
||
try {
|
||
// Call real API
|
||
const result = await contentGenerationApi.generateContent(undefined, {
|
||
target_keyword: keyword.trim(),
|
||
target_platform: platform,
|
||
knowledge_base_ids: selectedKbs.length > 0 ? selectedKbs : undefined,
|
||
content_style: style,
|
||
word_count: parseInt(wordCount, 10),
|
||
run_deai: true,
|
||
run_geo: true,
|
||
});
|
||
|
||
// Animate pipeline steps based on result
|
||
const stageMap: Record<string, number> = {
|
||
topic_analysis: 0,
|
||
content_generation: 1,
|
||
deai_processing: 2,
|
||
geo_optimization: 3,
|
||
rule_validation: 4,
|
||
media_insertion: 5,
|
||
};
|
||
|
||
if (result.pipeline_stages) {
|
||
for (let i = 0; i < result.pipeline_stages.length; i++) {
|
||
const stage = result.pipeline_stages[i];
|
||
const stepIdx = stageMap[stage.stage] ?? i;
|
||
await new Promise<void>((resolve) => {
|
||
setTimeout(() => {
|
||
setPipelineSteps((prev) =>
|
||
prev.map((s, idx) => ({
|
||
...s,
|
||
status:
|
||
idx < stepIdx
|
||
? "completed"
|
||
: idx === stepIdx
|
||
? "active"
|
||
: "pending",
|
||
}))
|
||
);
|
||
resolve();
|
||
}, 600 * i);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Mark all completed
|
||
await new Promise<void>((resolve) => setTimeout(resolve, 800));
|
||
setPipelineSteps(pipelineStepsTemplate.map((s) => ({ ...s, status: "completed" })));
|
||
|
||
setGeneratedContent(result.optimized_content || result.content);
|
||
|
||
// Save content to backend
|
||
try {
|
||
const saved = await contentsApi.create(undefined, {
|
||
title: keyword.trim(),
|
||
body: result.optimized_content || result.content,
|
||
content_type: "article",
|
||
tags: [platform, keyword.trim()],
|
||
});
|
||
setContents((prev) => [saved, ...prev]);
|
||
} catch {
|
||
// Ignore save error, content was still generated
|
||
}
|
||
|
||
setDialogMode("result");
|
||
} catch (err) {
|
||
console.error("Content generation error:", err);
|
||
setSubmitError(err instanceof Error ? err.message : "内容生成失败,请重试");
|
||
setDialogMode("form");
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
}, [keyword, platform, selectedKbs, style, wordCount]);
|
||
|
||
const toggleKb = useCallback((kbId: string) => {
|
||
setSelectedKbs((prev) =>
|
||
prev.includes(kbId) ? prev.filter((id) => id !== kbId) : [...prev, kbId]
|
||
);
|
||
}, []);
|
||
|
||
const isFormValid = keyword.trim() && platform;
|
||
|
||
// Loading skeleton
|
||
if (pageLoading) {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<Skeleton className="h-8 w-40" />
|
||
<Skeleton className="h-10 w-32" />
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-40 rounded-2xl" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* ── Top Area ── */}
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-8">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">内容工坊</h1>
|
||
<p className="mt-1 text-sm text-gray-500">AI驱动的内容生产流水线</p>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleOpenDialog}
|
||
className="shrink-0"
|
||
>
|
||
<Sparkles className="mr-2 h-4 w-4" />
|
||
AI生成新内容
|
||
</Button>
|
||
</div>
|
||
|
||
{/* ── Error Banner ── */}
|
||
{pageError && (
|
||
<div className="flex items-center gap-3 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||
<span>{pageError}</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="ml-auto h-7 text-xs"
|
||
onClick={() => window.location.reload()}
|
||
>
|
||
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||
重试
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Content List ── */}
|
||
{!pageError && contents.length === 0 ? (
|
||
<EmptyState onGenerate={handleOpenDialog} />
|
||
) : (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{contents.map((item) => (
|
||
<ContentCard key={item.id} item={item} />
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* ── AI Generation Dialog ── */}
|
||
<Dialog open={dialogOpen} onOpenChange={(open) => { if (!open) handleCloseDialog(); }}>
|
||
<DialogContent className="max-w-lg rounded-2xl p-0 overflow-hidden gap-0">
|
||
{dialogMode === "form" && (
|
||
<>
|
||
<DialogHeader className="p-6 pb-4">
|
||
<DialogTitle className="text-lg font-semibold flex items-center gap-2">
|
||
<Wand2 className="h-5 w-5 text-primary" />
|
||
AI生成新内容
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
填写以下信息,AI将为您生成符合品牌调性的优化内容
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="px-6 py-2 space-y-5">
|
||
{submitError && (
|
||
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600">
|
||
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
||
{submitError}
|
||
</div>
|
||
)}
|
||
|
||
{/* 目标关键词 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="keyword">
|
||
目标关键词<span className="text-destructive ml-0.5">*</span>
|
||
</Label>
|
||
<Input
|
||
id="keyword"
|
||
placeholder="输入核心关键词,如'AI营销'"
|
||
value={keyword}
|
||
onChange={(e) => setKeyword(e.target.value)}
|
||
className="rounded-xl"
|
||
/>
|
||
</div>
|
||
|
||
{/* 目标平台 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="platform">
|
||
目标平台<span className="text-destructive ml-0.5">*</span>
|
||
</Label>
|
||
<Select value={platform} onValueChange={setPlatform}>
|
||
<SelectTrigger id="platform" className="rounded-xl">
|
||
<SelectValue placeholder="选择发布平台" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{platformOptions.map((p) => (
|
||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 选择知识库 */}
|
||
{knowledgeBases.length > 0 && (
|
||
<div className="space-y-2">
|
||
<Label>选择知识库</Label>
|
||
<div className="flex flex-col gap-2">
|
||
{knowledgeBases.map((kb) => (
|
||
<label
|
||
key={kb.id}
|
||
className="flex items-center gap-2.5 p-2.5 rounded-xl border border-geo-border hover:border-primary/30 hover:bg-primary/[0.02] transition-colors cursor-pointer"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedKbs.includes(kb.id)}
|
||
onChange={() => toggleKb(kb.id)}
|
||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary accent-primary"
|
||
/>
|
||
<span className="text-sm text-geo-text-primary">{kb.name}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 内容风格 */}
|
||
<div className="space-y-2">
|
||
<Label>内容风格</Label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{styleOptions.map((s) => (
|
||
<label
|
||
key={s.value}
|
||
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl border cursor-pointer transition-all duration-200 text-sm ${
|
||
style === s.value
|
||
? "border-primary bg-primary/5 text-primary font-medium"
|
||
: "border-geo-border text-geo-text-secondary hover:border-primary/30"
|
||
}`}
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="style"
|
||
value={s.value}
|
||
checked={style === s.value}
|
||
onChange={() => setStyle(s.value)}
|
||
className="sr-only"
|
||
/>
|
||
{s.label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 期望字数 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="wordCount">期望字数</Label>
|
||
<Select value={wordCount} onValueChange={setWordCount}>
|
||
<SelectTrigger id="wordCount" className="rounded-xl">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{wordCountOptions.map((w) => (
|
||
<SelectItem key={w.value} value={w.value}>{w.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="p-6 pt-4">
|
||
<Button variant="outline" onClick={handleCloseDialog} className="rounded-xl">
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSubmit}
|
||
disabled={!isFormValid || isSubmitting}
|
||
className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-xl"
|
||
>
|
||
{isSubmitting ? (
|
||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />提交中...</>
|
||
) : (
|
||
<><Rocket className="mr-2 h-4 w-4" />开始AI生成</>
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
)}
|
||
|
||
{dialogMode === "pipeline" && (
|
||
<>
|
||
<DialogHeader className="p-6 pb-2">
|
||
<DialogTitle className="text-lg font-semibold flex items-center gap-2">
|
||
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
|
||
内容生成中
|
||
</DialogTitle>
|
||
<DialogDescription>AI正在为您生成优化内容,请稍候...</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="px-6 py-4">
|
||
<PipelineTimeline steps={pipelineSteps} />
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{dialogMode === "result" && (
|
||
<>
|
||
<DialogHeader className="p-6 pb-2">
|
||
<DialogTitle className="text-lg font-semibold flex items-center gap-2">
|
||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary">
|
||
<Check className="h-4 w-4 text-primary-foreground" />
|
||
</div>
|
||
内容生成完成
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
AI已成功生成并优化您的内容,您可以立即查看或前往分发
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
{generatedContent && (
|
||
<div className="px-6 py-2">
|
||
<div className="rounded-xl border border-geo-border bg-geo-bg p-3 text-xs text-geo-text-secondary line-clamp-4 leading-relaxed">
|
||
{generatedContent.slice(0, 300)}{generatedContent.length > 300 ? "..." : ""}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="px-6 py-2">
|
||
<PipelineTimeline
|
||
steps={pipelineSteps.map((s) => ({ ...s, status: "completed" as PipelineStepStatus }))}
|
||
/>
|
||
</div>
|
||
<DialogFooter className="p-6 pt-2">
|
||
<Button variant="outline" onClick={handleCloseDialog} className="rounded-xl">
|
||
关闭
|
||
</Button>
|
||
<Button
|
||
onClick={() => { handleCloseDialog(); router.push("/dashboard/distribution"); }}
|
||
className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-xl"
|
||
>
|
||
<Eye className="mr-2 h-4 w-4" />
|
||
前往分发中心
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|