改造一键流式优化首页
This commit is contained in:
@@ -1,81 +1,151 @@
|
||||
"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) {
|
||||
if (!article) {
|
||||
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">
|
||||
<section className="panel empty-panel optimized-panel">
|
||||
<div className="panel-heading">优化结果</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel stack">
|
||||
<div className="panel-heading">优化结果</div>
|
||||
<article className="preview">
|
||||
<h2>{article.title}</h2>
|
||||
<p>{article.summary}</p>
|
||||
<pre>{article.body_markdown}</pre>
|
||||
</article>
|
||||
<div>
|
||||
<h3>图片建议</h3>
|
||||
<ul>
|
||||
{article.image_suggestions.map((item) => (
|
||||
<li key={item.source}>
|
||||
<strong>{item.source}</strong>: {item.suggestion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<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>
|
||||
<div>
|
||||
<h3>修改位置</h3>
|
||||
<div className="tag-row">
|
||||
{article.changed_sections.map((section) => (
|
||||
<span className="tag" key={section}>
|
||||
{section}
|
||||
|
||||
{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>
|
||||
</div>
|
||||
{article.requires_user_confirmation.length > 0 && (
|
||||
<div>
|
||||
<h3>需要人工确认</h3>
|
||||
<ul>
|
||||
{article.requires_user_confirmation.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user