改造一键流式优化首页

This commit is contained in:
czj
2026-07-01 13:25:07 +08:00
parent 8a1ceb3b14
commit 016b653b76
5 changed files with 621 additions and 351 deletions
+120 -50
View File
@@ -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>
);
}