Files
GEOAgentArticleOptimizer/src/components/optimized-preview.tsx
T
2026-07-01 13:25:07 +08:00

152 lines
4.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import type { OptimizedArticle } from "../lib/domain/types";
interface OptimizedPreviewProps {
article: OptimizedArticle | null;
draftArticle?: OptimizedArticle | null;
jobId: string | null;
isStreaming?: boolean;
activityText?: string;
}
const exportFiles = ["optimized.md", "optimized.docx", "qa_report.json"];
const streamWords = ["事实", "标题", "结构", "语气", "检索", "改写", "终稿"];
export function OptimizedPreview({
article,
draftArticle = null,
jobId,
isStreaming = false,
activityText = "",
}: OptimizedPreviewProps) {
const [streamTick, setStreamTick] = useState(0);
const visibleArticle = article ?? draftArticle;
const visibleWordCount = isStreaming ? Math.min(streamTick, 6) : 0;
const visibleWords = Array.from({ length: visibleWordCount }, (_, offset) => {
const index = streamTick - visibleWordCount + offset;
return streamWords[index % streamWords.length];
});
useEffect(() => {
if (!isStreaming) return;
const timer = window.setInterval(() => {
setStreamTick((tick) => tick + 1);
}, 850);
return () => window.clearInterval(timer);
}, [isStreaming]);
if (!visibleArticle && !isStreaming) {
return (
<section className="panel empty-panel optimized-panel">
<div className="panel-heading">优化结果</div>
</section>
);
}
return (
<section
aria-live="polite"
className={`panel stack optimized-panel ${isStreaming ? "is-streaming" : ""}`}
>
<div className="panel-heading">
<span>优化结果</span>
{isStreaming && (
<span className="stream-status">
<span className="stream-cursor" aria-hidden="true" />
{activityText || "正在优化"}
</span>
)}
</div>
{visibleArticle ? (
<article className={`preview ${isStreaming ? "streaming-preview" : ""}`}>
<h2>{visibleArticle.title}</h2>
<p>{visibleArticle.summary}</p>
<pre>{visibleArticle.body_markdown}</pre>
</article>
) : (
<div className="preview stream-placeholder">
<div className="preview-pulse-line title-line" />
<div className="preview-pulse-line" />
<div className="preview-pulse-line short-line" />
<div className="preview-pulse-block" />
</div>
)}
{isStreaming && visibleWords.length > 0 && (
<div className="stream-word-row" aria-hidden="true">
{visibleWords.map((word, index) => (
<span className="stream-word" key={`${word}-${streamTick}-${index}`}>
{word}
</span>
))}
</div>
)}
{visibleArticle && (
<>
<div>
<h3>图片建议</h3>
{visibleArticle.image_suggestions.length > 0 ? (
<ul>
{visibleArticle.image_suggestions.map((item) => (
<li key={item.source}>
<strong>{item.source}</strong>: {item.suggestion}
</li>
))}
</ul>
) : (
<p className="empty-panel">暂无图片建议。</p>
)}
</div>
<div>
<h3>修改位置</h3>
<div className="tag-row">
{visibleArticle.changed_sections.map((section) => (
<span className="tag" key={section}>
{section}
</span>
))}
</div>
</div>
{visibleArticle.requires_user_confirmation.length > 0 && (
<div>
<h3>需要人工确认</h3>
<ul>
{visibleArticle.requires_user_confirmation.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
)}
</>
)}
{article && (
<div className="export-row">
{exportFiles.map((fileName) =>
jobId ? (
<a
className="download-link"
href={`/api/jobs/${jobId}/exports/${fileName}`}
key={fileName}
>
{fileName}
</a>
) : (
<button disabled key={fileName} type="button">
{fileName}
</button>
),
)}
</div>
)}
</section>
);
}