deerflow-code/frontend-web/src/open-canvas/components/ai-writing/WritingSkeletonCard.tsx
2026-09-07 18:24:55 +08:00

243 lines
9.6 KiB
TypeScript
Raw Permalink 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.

import { cn } from '@/lib/utils';
import type { WritingStatus } from '../../types/ai-writing';
interface WritingSkeletonCardProps {
status: WritingStatus;
/** 素材收集阶段的实时进度文案(仅 researching 阶段使用) */
researchProgress?: string;
/** 用户自带大纲时为 true:大纲阶段文案显示「整理你的大纲」而非「规划大纲」。 */
organizing?: boolean;
}
// ── 基础骨架元素 ──────────────────────────────────────────────────────────────
function SkeletonLine({ className }: { className?: string }) {
return (
<div className={cn('h-3 rounded-md bg-muted animate-pulse', className)} />
);
}
function ThinkingDots({ color = 'bg-blue-400' }: { color?: string }) {
return (
<span className="inline-flex gap-1 items-center">
{[0, 1, 2].map((i) => (
<span
key={i}
className={cn('size-1.5 rounded-full animate-bounce', color)}
style={{ animationDelay: `${i * 150}ms` }}
/>
))}
</span>
);
}
// ── 素材收集骨架屏 ────────────────────────────────────────────────────────────
function MaterialsSkeleton({ researchProgress }: { researchProgress?: string }) {
return (
<div className="space-y-3 p-4">
{/* 标题行:展示素材收集专家的实时进度 */}
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
<ThinkingDots color="bg-blue-400" />
<span>{researchProgress || '素材收集专家搜索中…'}</span>
</div>
{/* 关键词标签占位 */}
<div className="flex gap-1.5 flex-wrap">
{[60, 80, 50].map((w, i) => (
<div
key={i}
className="h-5 rounded-full bg-blue-100/60 dark:bg-blue-900/30 animate-pulse"
style={{ width: `${w}px` }}
/>
))}
</div>
{/* 素材卡片占位 */}
{[0, 1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border bg-muted/30 p-3 space-y-2">
{/* 标题 */}
<div className="flex items-start gap-2">
<SkeletonLine className={cn('flex-1 h-3.5', i % 3 === 0 ? 'w-4/5' : i % 3 === 1 ? 'w-3/4' : 'w-2/3')} />
</div>
{/* 来源 + 时间 + 相关度 */}
<div className="flex items-center gap-2">
<SkeletonLine className="w-20 h-2" />
<SkeletonLine className="w-16 h-2" />
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-blue-200/60 dark:bg-blue-700/40 animate-pulse"
style={{ width: `${55 + i * 8}%` }}
/>
</div>
</div>
{/* 摘要行 */}
<SkeletonLine className="w-full h-2" />
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-5/6' : 'w-4/5')} />
</div>
))}
</div>
);
}
// ── 大纲规划骨架屏 ────────────────────────────────────────────────────────────
function OutlineSkeleton({ organizing }: { organizing?: boolean } = {}) {
return (
<div className="space-y-3 p-4">
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
<ThinkingDots color="bg-purple-400" />
<span>{organizing ? '作家整理你的大纲中…' : '作家规划大纲中…'}</span>
</div>
{/* 文章标题占位 */}
<SkeletonLine className="w-3/5 h-4 mx-auto" />
{/* 章节列表 */}
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex items-start gap-2 pl-1">
<div className="mt-1 size-2 shrink-0 rounded-sm bg-purple-200/70 dark:bg-purple-800/40 animate-pulse" />
<div className="flex-1 space-y-1.5">
<SkeletonLine className={cn('h-3', i % 2 === 0 ? 'w-2/5' : 'w-1/2')} />
<SkeletonLine className="w-3/5 h-2" />
{i % 2 === 0 && <SkeletonLine className="w-2/5 h-2" />}
</div>
</div>
))}
</div>
);
}
// ── 正文写作骨架屏 ────────────────────────────────────────────────────────────
function DraftSkeleton() {
return (
<div className="space-y-4 p-4">
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
<ThinkingDots color="bg-purple-400" />
<span>作家写作中…</span>
</div>
{/* 标题占位 */}
<SkeletonLine className="w-2/5 h-5 mx-auto" />
{/* 章节内容 */}
{[0, 1, 2].map((i) => (
<div key={i} className="space-y-1.5">
<SkeletonLine className="w-1/4 h-3.5" />
<SkeletonLine className="w-full h-2" />
<SkeletonLine className="w-full h-2" />
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-4/5' : 'w-11/12')} />
<SkeletonLine className="w-full h-2" />
<SkeletonLine className="w-3/4 h-2" />
</div>
))}
</div>
);
}
// ── 编辑审核骨架屏 ────────────────────────────────────────────────────────────
function ReviewSkeleton() {
return (
<div className="space-y-3 p-4">
<div className="flex items-center gap-2 text-xs text-orange-500 font-medium">
<ThinkingDots color="bg-orange-400" />
<span>编辑审核中…</span>
</div>
{/* 综合评分占位 */}
<div className="flex items-center justify-between">
<SkeletonLine className="w-24 h-3" />
<SkeletonLine className="w-12 h-4" />
</div>
{/* 分项评分条 */}
{['事实准确度', '逻辑结构', '语言质量'].map((label, i) => (
<div key={label} className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">{label}</span>
<SkeletonLine className="w-8 h-3" />
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-orange-200/60 dark:bg-orange-700/40 animate-pulse"
style={{ width: `${55 + i * 10}%` }}
/>
</div>
</div>
))}
{/* 意见条目占位 */}
<div className="space-y-2 pt-1 border-t">
<SkeletonLine className="w-full h-2" />
<SkeletonLine className="w-5/6 h-2" />
<SkeletonLine className="w-full h-2" />
<SkeletonLine className="w-4/5 h-2" />
</div>
</div>
);
}
// ── 按意见修改骨架屏 ──────────────────────────────────────────────────────────
function RevisingSkeleton() {
return (
<div className="space-y-4 p-4">
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
<ThinkingDots color="bg-purple-400" />
<span>作家按意见修改中…</span>
</div>
<SkeletonLine className="w-2/5 h-5 mx-auto" />
{[0, 1, 2].map((i) => (
<div key={i} className="space-y-1.5">
<SkeletonLine className="w-1/4 h-3.5" />
<SkeletonLine className="w-full h-2" />
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-4/5' : 'w-full')} />
</div>
))}
</div>
);
}
// ── 状态映射 ──────────────────────────────────────────────────────────────────
// researching / awaiting_material_confirm 由 WritingSkeletonCard 单独处理(需透传进度文案)
const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
// 实际运行状态
writing_outline: OutlineSkeleton,
writing_draft: DraftSkeleton,
reviewing: ReviewSkeleton,
revising: RevisingSkeleton,
// 过渡等待状态(用户提交后、后端事件到达前的空白期)
awaiting_outline_confirm: OutlineSkeleton, // 过渡到写作
awaiting_draft_confirm: DraftSkeleton, // 提交修改意见或编辑审核后
awaiting_review_confirm: RevisingSkeleton, // 采纳编辑意见后
};
export function WritingSkeletonCard({ status, researchProgress, organizing }: WritingSkeletonCardProps) {
// 素材收集阶段单独渲染,以便透传实时进度文案
if (status === 'analyzing_sample' || status === 'researching' || status === 'awaiting_material_confirm') {
return (
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
<MaterialsSkeleton researchProgress={status === 'analyzing_sample' ? '正在分析样文风格…' : researchProgress} />
</div>
);
}
// 大纲阶段单独渲染,以便透传「整理 / 规划」文案(用户自带大纲走「整理」)
if (status === 'writing_outline' || status === 'awaiting_outline_confirm') {
return (
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
<OutlineSkeleton organizing={organizing} />
</div>
);
}
const Skeleton = SKELETON_MAP[status];
if (!Skeleton) return null;
return (
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
<Skeleton />
</div>
);
}