152 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|