新增发布表现校准界面
This commit is contained in:
+18
-1
@@ -295,9 +295,26 @@ h3 {
|
||||
padding: 0.25rem 0.55rem;
|
||||
}
|
||||
|
||||
.calibration-metrics {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.calibration-observations {
|
||||
border: 1px solid #e5e9f0;
|
||||
border-radius: 8px;
|
||||
color: #586174;
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
padding: 0.75rem 0.75rem 0.75rem 1.4rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.workflow-grid,
|
||||
.two-col {
|
||||
.two-col,
|
||||
.calibration-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
toConfirmedFactCard,
|
||||
} from "../components/fact-card-editor";
|
||||
import { OptimizedPreview } from "../components/optimized-preview";
|
||||
import { PerformanceCalibrationPanel } from "../components/performance-calibration-panel";
|
||||
import { ProgressPanel } from "../components/progress-panel";
|
||||
import { QaReportPanel } from "../components/qa-report-panel";
|
||||
import type {
|
||||
@@ -270,6 +271,12 @@ export default function Home() {
|
||||
jobId={jobId}
|
||||
/>
|
||||
<QaReportPanel report={qaReport} />
|
||||
<PerformanceCalibrationPanel
|
||||
apiAccessKey={apiAccessKey}
|
||||
jobId={jobId}
|
||||
key={`${jobId ?? "no-job"}-${optimizedArticle?.revision ?? "no-revision"}`}
|
||||
optimizedRevision={optimizedArticle?.revision ?? null}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { PublishPlatform } from "../lib/domain/types";
|
||||
|
||||
interface PerformanceCalibrationPanelProps {
|
||||
apiAccessKey: string;
|
||||
jobId: string | null;
|
||||
optimizedRevision: number | null;
|
||||
}
|
||||
|
||||
interface ScoreResponse {
|
||||
scoringRun?: { id: string; composite_score: number; rationale: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface PublicationResponse {
|
||||
publication?: { id: string; url: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface PerformanceResponse {
|
||||
snapshot?: { metrics: Record<string, number> };
|
||||
calibrationEvent?: { observations: string[]; recommended_action: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function PerformanceCalibrationPanel({
|
||||
apiAccessKey,
|
||||
jobId,
|
||||
optimizedRevision,
|
||||
}: PerformanceCalibrationPanelProps) {
|
||||
const [platform, setPlatform] = useState<PublishPlatform>("official_site");
|
||||
const [url, setUrl] = useState("");
|
||||
const [publishedAt, setPublishedAt] = useState(() =>
|
||||
new Date().toISOString().slice(0, 16),
|
||||
);
|
||||
const [publicationId, setPublicationId] = useState<string | null>(null);
|
||||
const [views, setViews] = useState("");
|
||||
const [clicks, setClicks] = useState("");
|
||||
const [inquiries, setInquiries] = useState("");
|
||||
const [feedbackSummary, setFeedbackSummary] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [observations, setObservations] = useState<string[]>([]);
|
||||
|
||||
const disabled = !jobId || !optimizedRevision;
|
||||
|
||||
async function scoreRevision() {
|
||||
if (!jobId) return;
|
||||
setMessage("");
|
||||
const response = await fetch(`/api/jobs/${jobId}/calibration/score`, {
|
||||
method: "POST",
|
||||
headers: apiHeaders(apiAccessKey),
|
||||
});
|
||||
const body = (await response.json()) as ScoreResponse;
|
||||
if (!response.ok || !body.scoringRun) {
|
||||
setMessage(body.error ?? "评分失败");
|
||||
return;
|
||||
}
|
||||
setMessage(
|
||||
`校准评分 ${body.scoringRun.composite_score}/10:${body.scoringRun.rationale}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function registerPublication() {
|
||||
if (!jobId || !publishedAt) return;
|
||||
setMessage("");
|
||||
const response = await fetch(`/api/jobs/${jobId}/publications`, {
|
||||
method: "POST",
|
||||
headers: apiHeaders(apiAccessKey),
|
||||
body: JSON.stringify({
|
||||
platform,
|
||||
url,
|
||||
published_at: new Date(publishedAt).toISOString(),
|
||||
notes: "",
|
||||
}),
|
||||
});
|
||||
const body = (await response.json()) as PublicationResponse;
|
||||
if (!response.ok || !body.publication) {
|
||||
setMessage(body.error ?? "发布记录保存失败");
|
||||
return;
|
||||
}
|
||||
setPublicationId(body.publication.id);
|
||||
setMessage("发布记录已保存。");
|
||||
}
|
||||
|
||||
async function recordPerformance() {
|
||||
if (!publicationId) return;
|
||||
setMessage("");
|
||||
const response = await fetch(`/api/publications/${publicationId}/performance`, {
|
||||
method: "POST",
|
||||
headers: apiHeaders(apiAccessKey),
|
||||
body: JSON.stringify({
|
||||
window_label: "T+7d",
|
||||
views,
|
||||
clicks,
|
||||
inquiries,
|
||||
feedback_summary: feedbackSummary,
|
||||
}),
|
||||
});
|
||||
const body = (await response.json()) as PerformanceResponse;
|
||||
if (!response.ok || !body.calibrationEvent) {
|
||||
setMessage(body.error ?? "表现数据保存失败");
|
||||
return;
|
||||
}
|
||||
setObservations(body.calibrationEvent.observations);
|
||||
setMessage(body.calibrationEvent.recommended_action);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel stack">
|
||||
<div className="panel-heading">
|
||||
<span>发布表现校准</span>
|
||||
<button disabled={disabled} onClick={scoreRevision} type="button">
|
||||
生成评分
|
||||
</button>
|
||||
</div>
|
||||
<label>
|
||||
<span>发布平台</span>
|
||||
<select
|
||||
disabled={disabled}
|
||||
value={platform}
|
||||
onChange={(event) => setPlatform(event.target.value as PublishPlatform)}
|
||||
>
|
||||
<option value="official_site">官网文章</option>
|
||||
<option value="media_article">媒体稿</option>
|
||||
<option value="comparison_review">对比评测</option>
|
||||
<option value="recommendation_list">推荐榜单</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>发布链接</span>
|
||||
<input
|
||||
disabled={disabled}
|
||||
placeholder="https://example.com/article"
|
||||
value={url}
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>发布时间</span>
|
||||
<input
|
||||
disabled={disabled}
|
||||
type="datetime-local"
|
||||
value={publishedAt}
|
||||
onChange={(event) => setPublishedAt(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
disabled={disabled || !url || !publishedAt}
|
||||
onClick={registerPublication}
|
||||
type="button"
|
||||
>
|
||||
保存发布记录
|
||||
</button>
|
||||
<div className="calibration-metrics">
|
||||
<label>
|
||||
<span>阅读/浏览</span>
|
||||
<input
|
||||
min="0"
|
||||
type="number"
|
||||
value={views}
|
||||
onChange={(event) => setViews(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>点击</span>
|
||||
<input
|
||||
min="0"
|
||||
type="number"
|
||||
value={clicks}
|
||||
onChange={(event) => setClicks(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>询盘</span>
|
||||
<input
|
||||
min="0"
|
||||
type="number"
|
||||
value={inquiries}
|
||||
onChange={(event) => setInquiries(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>反馈摘要</span>
|
||||
<textarea
|
||||
value={feedbackSummary}
|
||||
onChange={(event) => setFeedbackSummary(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button disabled={!publicationId} onClick={recordPerformance} type="button">
|
||||
记录表现并生成复盘
|
||||
</button>
|
||||
{message ? <p className="status-text">{message}</p> : null}
|
||||
{observations.length > 0 ? (
|
||||
<ul className="calibration-observations">
|
||||
{observations.map((observation) => (
|
||||
<li key={observation}>{observation}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function apiHeaders(apiAccessKey: string) {
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
if (apiAccessKey) {
|
||||
headers["x-api-key"] = apiAccessKey;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
Reference in New Issue
Block a user