新增发布表现校准界面
This commit is contained in:
+18
-1
@@ -295,9 +295,26 @@ h3 {
|
|||||||
padding: 0.25rem 0.55rem;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.workflow-grid,
|
.workflow-grid,
|
||||||
.two-col {
|
.two-col,
|
||||||
|
.calibration-metrics {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
toConfirmedFactCard,
|
toConfirmedFactCard,
|
||||||
} from "../components/fact-card-editor";
|
} from "../components/fact-card-editor";
|
||||||
import { OptimizedPreview } from "../components/optimized-preview";
|
import { OptimizedPreview } from "../components/optimized-preview";
|
||||||
|
import { PerformanceCalibrationPanel } from "../components/performance-calibration-panel";
|
||||||
import { ProgressPanel } from "../components/progress-panel";
|
import { ProgressPanel } from "../components/progress-panel";
|
||||||
import { QaReportPanel } from "../components/qa-report-panel";
|
import { QaReportPanel } from "../components/qa-report-panel";
|
||||||
import type {
|
import type {
|
||||||
@@ -270,6 +271,12 @@ export default function Home() {
|
|||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
/>
|
/>
|
||||||
<QaReportPanel report={qaReport} />
|
<QaReportPanel report={qaReport} />
|
||||||
|
<PerformanceCalibrationPanel
|
||||||
|
apiAccessKey={apiAccessKey}
|
||||||
|
jobId={jobId}
|
||||||
|
key={`${jobId ?? "no-job"}-${optimizedArticle?.revision ?? "no-revision"}`}
|
||||||
|
optimizedRevision={optimizedArticle?.revision ?? null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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