feat: deepen douyin commercial workbench
This commit is contained in:
@@ -640,6 +640,97 @@ function renderPage() {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.analysis-grid,
|
||||
.video-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.summary-callout {
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, rgba(31, 110, 95, 0.1), rgba(185, 117, 36, 0.08));
|
||||
border: 1px solid rgba(31, 110, 95, 0.12);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.bullet-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--ink);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.analysis-block {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.video-card {
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
.video-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
.cover-thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4;
|
||||
border-radius: 16px;
|
||||
object-fit: cover;
|
||||
background: rgba(22, 49, 61, 0.08);
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
}
|
||||
.toolbar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.score-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.score-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(31, 110, 95, 0.08);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
}
|
||||
.score-badge strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
.link-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.link-row a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.link-row a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
details {
|
||||
border-top: 1px solid rgba(22, 49, 61, 0.1);
|
||||
padding-top: 10px;
|
||||
}
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
}
|
||||
.empty-state {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
@@ -651,7 +742,7 @@ function renderPage() {
|
||||
margin: 4px 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.grid, .row, .checks, .workbench-layout, .metric-grid, .two-col { grid-template-columns: 1fr; }
|
||||
.grid, .row, .checks, .workbench-layout, .metric-grid, .two-col, .analysis-grid, .video-grid, .video-layout, .toolbar-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -816,6 +907,50 @@ function renderPage() {
|
||||
<div id="analysis-reports" class="stack"></div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel stack">
|
||||
<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;">
|
||||
<div>
|
||||
<h3>作品工作台</h3>
|
||||
<p class="hint" style="margin:6px 0 0;">这里会把高分作品和最新作品拆开看,并给高分作品自动补运营分析。</p>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button class="secondary" id="analyze-top-videos-button" type="button">自动分析高分作品</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-grid">
|
||||
<label>
|
||||
作品列表
|
||||
<select id="videos-scope-select">
|
||||
<option value="all">全部作品</option>
|
||||
<option value="top">高分作品</option>
|
||||
<option value="latest">最新作品</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
排序方式
|
||||
<select id="videos-sort-select">
|
||||
<option value="score">综合高分</option>
|
||||
<option value="commercial">商业价值</option>
|
||||
<option value="latest">最新发布时间</option>
|
||||
<option value="play">播放量</option>
|
||||
<option value="like">点赞量</option>
|
||||
<option value="share">分享量</option>
|
||||
<option value="comment">评论量</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
标签筛选
|
||||
<input id="videos-tag-filter" placeholder="例如:创业 / 文案" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
关键词搜索
|
||||
<input id="videos-query-filter" placeholder="按标题或描述搜索" autocomplete="off" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="videos-summary"></div>
|
||||
<div id="videos-list" class="stack"></div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel stack">
|
||||
<h3>快照与原始采集</h3>
|
||||
<div id="snapshot-summary"></div>
|
||||
@@ -877,6 +1012,13 @@ function renderPage() {
|
||||
const analysisFocusEl = document.getElementById("analysis-focus");
|
||||
const analysisModelSelectEl = document.getElementById("analysis-model-select");
|
||||
const analysisMaxVideosEl = document.getElementById("analysis-max-videos");
|
||||
const analyzeTopVideosButton = document.getElementById("analyze-top-videos-button");
|
||||
const videosScopeEl = document.getElementById("videos-scope-select");
|
||||
const videosSortEl = document.getElementById("videos-sort-select");
|
||||
const videosTagFilterEl = document.getElementById("videos-tag-filter");
|
||||
const videosQueryFilterEl = document.getElementById("videos-query-filter");
|
||||
const videosSummaryEl = document.getElementById("videos-summary");
|
||||
const videosListEl = document.getElementById("videos-list");
|
||||
const snapshotSummaryEl = document.getElementById("snapshot-summary");
|
||||
const snapshotListEl = document.getElementById("snapshot-list");
|
||||
const snapshotDetailEl = document.getElementById("snapshot-detail");
|
||||
@@ -894,6 +1036,11 @@ function renderPage() {
|
||||
accounts: [],
|
||||
selectedAccountId: "",
|
||||
selectedWorkspace: null,
|
||||
videoItems: [],
|
||||
videoMeta: null,
|
||||
topScoredVideoIds: [],
|
||||
latestVideoIds: [],
|
||||
highScoreThreshold: 60,
|
||||
snapshots: [],
|
||||
selectedSnapshotId: "",
|
||||
selectedSnapshotDetail: null,
|
||||
@@ -939,6 +1086,18 @@ function renderPage() {
|
||||
return date.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
const num = Number(value || 0);
|
||||
if (!Number.isFinite(num)) {
|
||||
return "-";
|
||||
}
|
||||
let text = (num * 100).toFixed(2);
|
||||
if (text.endsWith(".00")) {
|
||||
text = text.slice(0, -3);
|
||||
}
|
||||
return text + "%";
|
||||
}
|
||||
|
||||
function normalizeBackendUrl(value) {
|
||||
let normalized = String(value || "").trim();
|
||||
while (normalized.endsWith("/")) {
|
||||
@@ -971,6 +1130,11 @@ function renderPage() {
|
||||
workbenchState.accounts = [];
|
||||
workbenchState.selectedAccountId = "";
|
||||
workbenchState.selectedWorkspace = null;
|
||||
workbenchState.videoItems = [];
|
||||
workbenchState.videoMeta = null;
|
||||
workbenchState.topScoredVideoIds = [];
|
||||
workbenchState.latestVideoIds = [];
|
||||
workbenchState.highScoreThreshold = 60;
|
||||
workbenchState.snapshots = [];
|
||||
workbenchState.selectedSnapshotId = "";
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
@@ -1026,6 +1190,158 @@ function renderPage() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderBulletList(items, emptyText) {
|
||||
const safeItems = safeArray(items).filter(Boolean);
|
||||
if (!safeItems.length) {
|
||||
return '<p class="empty-state">' + escapeHtml(emptyText || "暂无内容。") + '</p>';
|
||||
}
|
||||
return '<ul class="bullet-list">' + safeItems.map((item) => '<li>' + escapeHtml(typeof item === "string" ? item : JSON.stringify(item)) + '</li>').join("") + '</ul>';
|
||||
}
|
||||
|
||||
function renderObjectBulletList(items, renderFn, emptyText) {
|
||||
const safeItems = safeArray(items);
|
||||
if (!safeItems.length) {
|
||||
return '<p class="empty-state">' + escapeHtml(emptyText || "暂无内容。") + '</p>';
|
||||
}
|
||||
return '<div class="stack">' + safeItems.map(renderFn).join("") + '</div>';
|
||||
}
|
||||
|
||||
function getSortedVideos() {
|
||||
const scope = videosScopeEl.value || "all";
|
||||
const sortBy = videosSortEl.value || "score";
|
||||
const query = videosQueryFilterEl.value.trim().toLowerCase();
|
||||
const tag = videosTagFilterEl.value.trim().toLowerCase();
|
||||
const topSet = new Set(safeArray(workbenchState.topScoredVideoIds));
|
||||
const latestSet = new Set(safeArray(workbenchState.latestVideoIds));
|
||||
let items = safeArray(workbenchState.videoItems).filter((video) => {
|
||||
if (scope === "top" && !topSet.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
if (scope === "latest" && !latestSet.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
if (query) {
|
||||
const haystack = [video.title, video.description, video.aweme_id, ...safeArray(video.tags)].join(" ").toLowerCase();
|
||||
if (!haystack.includes(query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (tag) {
|
||||
const tags = safeArray(video.tags).map((item) => String(item).toLowerCase());
|
||||
if (!tags.some((item) => item.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const getValue = (video) => {
|
||||
if (sortBy === "commercial") return Number(video.score?.commercial_score || 0);
|
||||
if (sortBy === "latest") return new Date(video.published_at || 0).getTime();
|
||||
if (sortBy === "play") return Number(video.stats?.play || 0);
|
||||
if (sortBy === "like") return Number(video.stats?.like || 0);
|
||||
if (sortBy === "share") return Number(video.stats?.share || 0);
|
||||
if (sortBy === "comment") return Number(video.stats?.comment || 0);
|
||||
return Number(video.score?.performance_score || 0);
|
||||
};
|
||||
items.sort((left, right) => getValue(right) - getValue(left));
|
||||
return items;
|
||||
}
|
||||
|
||||
function renderAccountSuggestion(suggestion) {
|
||||
const parsed = suggestion.parsed_json || {};
|
||||
if (!parsed.executive_summary) {
|
||||
return [
|
||||
'<div class="report-item">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap;">',
|
||||
'<strong>' + escapeHtml(suggestion.model_label || "模型") + '</strong>',
|
||||
'<span class="pill">' + escapeHtml(suggestion.status || "-") + '</span>',
|
||||
'</div>',
|
||||
'<div class="report-suggestion">' + escapeHtmlWithBreaks(suggestion.suggestion_text || "暂无结论") + '</div>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
const positioning = parsed.commercial_positioning || {};
|
||||
const engine = parsed.content_engine || {};
|
||||
return [
|
||||
'<div class="report-item">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap;">',
|
||||
'<strong>' + escapeHtml(suggestion.model_label || "模型") + '</strong>',
|
||||
'<span class="pill">' + escapeHtml(suggestion.status || "-") + '</span>',
|
||||
'</div>',
|
||||
'<div class="summary-callout" style="margin-top:12px;">' + escapeHtml(parsed.executive_summary || "暂无总结") + '</div>',
|
||||
'<div class="analysis-grid" style="margin-top:12px;">',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">商业定位</h4><div class="meta">受众:' + escapeHtml(positioning.audience || "-") + '</div><div class="meta" style="margin-top:8px;">核心承诺:' + escapeHtml(positioning.core_promise || "-") + '</div><div class="meta" style="margin-top:8px;">商业化准备度:' + escapeHtml(formatNumber(positioning.monetization_readiness_score)) + '</div><div style="margin-top:12px;">' + renderBulletList(positioning.offer_directions, "暂无产品方向建议") + '</div></div>',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">内容引擎</h4><div class="meta">内容支柱</div>' + renderBulletList(engine.pillars, "暂无内容支柱") + '<div class="meta" style="margin-top:10px;">开头模式</div>' + renderBulletList(engine.hook_patterns, "暂无开头模式") + '</div>',
|
||||
'</div>',
|
||||
'<div class="analysis-grid" style="margin-top:12px;">',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">结构与 CTA</h4><div class="meta">结构模式</div>' + renderBulletList(engine.structure_patterns, "暂无结构结论") + '<div class="meta" style="margin-top:10px;">CTA 模式</div>' + renderBulletList(engine.cta_patterns, "暂无 CTA 建议") + '</div>',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">商业化与运营动作</h4><div class="meta">承接路径</div>' + renderBulletList(parsed.monetization_plan, "暂无商业化承接建议") + '<div class="meta" style="margin-top:10px;">30 天动作</div>' + renderBulletList(parsed.next_30_day_actions, "暂无 30 天动作建议") + '</div>',
|
||||
'</div>',
|
||||
'<div class="analysis-grid" style="margin-top:12px;">',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">高分作品规律</h4>' + renderObjectBulletList(parsed.winning_patterns, (item) => '<div class="link-item"><strong>' + escapeHtml(item.video_title || "高分作品") + '</strong><div class="meta" style="margin-top:6px;">' + escapeHtml(item.why || "-") + '</div><div class="meta" style="margin-top:6px;">复刻建议:' + escapeHtml(item.replication_angle || "-") + '</div></div>', "暂无高分作品分析") + '</div>',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">最近内容信号</h4>' + renderObjectBulletList(parsed.latest_signal, (item) => '<div class="link-item"><strong>' + escapeHtml(item.video_title || "最近作品") + '</strong><div class="meta" style="margin-top:6px;">' + escapeHtml(item.signal || "-") + '</div><div class="meta" style="margin-top:6px;">动作:' + escapeHtml(item.action || "-") + '</div></div>', "暂无最新作品信号") + '</div>',
|
||||
'</div>',
|
||||
'<div class="analysis-grid" style="margin-top:12px;">',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">对标与风险</h4><div class="meta">对标洞察</div>' + renderBulletList(parsed.benchmark_insights, "暂无对标洞察") + '<div class="meta" style="margin-top:10px;">风险观察</div>' + renderBulletList(parsed.risk_watchlist, "暂无风险提示") + '</div>',
|
||||
'<div class="detail-box"><h4 style="margin:0 0 10px;">当前缺口</h4>' + renderBulletList(parsed.operational_gaps, "暂无明显缺口") + '</div>',
|
||||
'</div>',
|
||||
suggestion.suggestion_text ? '<details style="margin-top:12px;"><summary>查看模型原始输出</summary><div class="report-suggestion">' + escapeHtmlWithBreaks(suggestion.suggestion_text) + '</div></details>' : '',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderVideoAnalysisCard(video) {
|
||||
const analysis = video.latest_analysis || {};
|
||||
const parsed = analysis.parsed_json || {};
|
||||
const score = video.score || {};
|
||||
const stats = video.stats || {};
|
||||
return [
|
||||
'<article class="video-card">',
|
||||
'<div class="video-layout">',
|
||||
video.cover_url ? '<img class="cover-thumb" src="' + escapeHtml(video.cover_url) + '" alt="cover" />' : '<div class="cover-thumb"></div>',
|
||||
'<div class="stack">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap;">',
|
||||
'<div><strong>' + escapeHtml(video.title || video.aweme_id || "未命名作品") + '</strong><div class="meta" style="margin-top:8px;">发布时间:' + escapeHtml(formatDateTime(video.published_at)) + '</div></div>',
|
||||
'<span class="pill">' + escapeHtml(video.aweme_id || "-") + '</span>',
|
||||
'</div>',
|
||||
'<div class="score-badges">',
|
||||
'<span class="score-badge"><strong>综合</strong>' + escapeHtml(formatNumber(score.performance_score)) + '</span>',
|
||||
'<span class="score-badge"><strong>商业</strong>' + escapeHtml(formatNumber(score.commercial_score)) + '</span>',
|
||||
'<span class="score-badge"><strong>播放</strong>' + escapeHtml(formatNumber(stats.play)) + '</span>',
|
||||
'<span class="score-badge"><strong>点赞</strong>' + escapeHtml(formatNumber(stats.like)) + '</span>',
|
||||
'<span class="score-badge"><strong>分享</strong>' + escapeHtml(formatNumber(stats.share)) + '</span>',
|
||||
'<span class="score-badge"><strong>收藏率</strong>' + escapeHtml(formatPercent(score.collect_rate)) + '</span>',
|
||||
'</div>',
|
||||
safeArray(video.tags).length ? '<div class="chips">' + safeArray(video.tags).map((tag) => '<span class="chip">' + escapeHtml(tag) + '</span>').join("") + '</div>' : '',
|
||||
'<div class="meta">互动率:' + escapeHtml(formatPercent(score.engagement_rate)) + ',评论率:' + escapeHtml(formatPercent(score.comment_rate)) + ',发布时间距今:' + escapeHtml(score.age_days == null ? "-" : score.age_days + " 天") + '</div>',
|
||||
'<div class="chips">' + safeArray(score.signals).map((item) => '<span class="chip">' + escapeHtml(item) + '</span>').join("") + '</div>',
|
||||
'<div class="link-row">' + (video.share_url ? '<a href="' + escapeHtml(video.share_url) + '" target="_blank" rel="noreferrer">打开作品</a>' : '') + '</div>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
parsed.headline_summary ? '<div class="analysis-block"><div class="summary-callout">' + escapeHtml(parsed.headline_summary || analysis.summary_text || "暂无分析结论") + '</div><div class="analysis-grid"><div class="detail-box"><h4 style="margin:0 0 10px;">为什么值得做</h4><div class="meta">商业判断:' + escapeHtml((parsed.commercial_angle || {}).judgement || "-") + '</div><div class="meta" style="margin-top:8px;">可承接方向</div>' + renderBulletList((parsed.commercial_angle || {}).suitable_for, "暂无承接方向") + '<div class="meta" style="margin-top:10px;">分项评分</div>' + renderBulletList(['钩子 ' + formatNumber((parsed.scores || {}).hook), '留存 ' + formatNumber((parsed.scores || {}).retention), '转化 ' + formatNumber((parsed.scores || {}).conversion), '商业 ' + formatNumber((parsed.scores || {}).commercial)], "暂无分项评分") + '</div><div class="detail-box"><h4 style="margin:0 0 10px;">复刻与运营动作</h4><div class="meta">复刻计划</div>' + renderBulletList(parsed.replication_plan, "暂无复刻计划") + '<div class="meta" style="margin-top:10px;">运营动作</div>' + renderBulletList(parsed.operator_actions, "暂无运营动作") + '</div></div><div class="analysis-grid"><div class="detail-box"><h4 style="margin:0 0 10px;">钩子与结构</h4><div class="meta">钩子拆解</div>' + renderBulletList(parsed.hook_breakdown, "暂无钩子拆解") + '<div class="meta" style="margin-top:10px;">结构拆解</div>' + renderBulletList(parsed.structure_breakdown, "暂无结构拆解") + '</div><div class="detail-box"><h4 style="margin:0 0 10px;">风险提醒</h4>' + renderBulletList(parsed.risk_notes, "暂无风险提醒") + '</div></div></div>' : '<div class="detail-box"><p class="empty-state">这条作品还没有自动分析。点击上面的“自动分析高分作品”即可补齐。</p></div>',
|
||||
'</article>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderVideos() {
|
||||
const items = getSortedVideos();
|
||||
const meta = workbenchState.videoMeta || {};
|
||||
videosSummaryEl.innerHTML = [
|
||||
'<div class="metric-grid">',
|
||||
'<div class="metric-card"><div class="metric-label">作品总数</div><div class="metric-value">' + escapeHtml(formatNumber(meta.total_count)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">已分析作品</div><div class="metric-value">' + escapeHtml(formatNumber(meta.analyzed_count)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">高分作品数</div><div class="metric-value">' + escapeHtml(formatNumber(meta.high_score_count)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">当前显示</div><div class="metric-value">' + escapeHtml(formatNumber(items.length)) + '</div></div>',
|
||||
'</div>',
|
||||
'<p class="hint" style="margin-top:12px;">高分阈值:' + escapeHtml(formatNumber(workbenchState.highScoreThreshold)) + '。高分榜更适合找商业化样板,最新榜更适合看近期题材窗口。</p>'
|
||||
].join("");
|
||||
if (!items.length) {
|
||||
videosListEl.innerHTML = '<p class="empty-state">当前筛选条件下没有作品。</p>';
|
||||
return;
|
||||
}
|
||||
videosListEl.innerHTML = '<div class="video-grid">' + items.map(renderVideoAnalysisCard).join("") + '</div>';
|
||||
}
|
||||
|
||||
function renderWorkbenchSession() {
|
||||
const session = workbenchState.session;
|
||||
if (!session) {
|
||||
@@ -1132,6 +1448,8 @@ function renderPage() {
|
||||
snapshotListEl.innerHTML = "";
|
||||
linkedAccountsEl.innerHTML = "";
|
||||
similarSearchListEl.innerHTML = "";
|
||||
videosSummaryEl.innerHTML = "";
|
||||
videosListEl.innerHTML = "";
|
||||
renderSnapshotDetail();
|
||||
renderSimilarSearchDetail();
|
||||
return;
|
||||
@@ -1187,7 +1505,7 @@ function renderPage() {
|
||||
'<strong>' + escapeHtml(report.focus_text || "默认分析") + '</strong>',
|
||||
'<span class="pill">' + escapeHtml(formatDateTime(report.created_at)) + '</span>',
|
||||
'</div>',
|
||||
safeArray(report.suggestions).length ? safeArray(report.suggestions).map((suggestion) => '<div class="report-suggestion"><div class="meta">' + escapeHtml(suggestion.model_label || "模型") + ' / ' + escapeHtml(suggestion.status || "-") + '</div><div style="margin-top:8px;">' + escapeHtml(suggestion.suggestion_text || "暂无结论") + '</div></div>').join("") : '<p class="empty-state" style="margin-top:10px;">这份报告还没有 suggestion。</p>',
|
||||
safeArray(report.suggestions).length ? safeArray(report.suggestions).map(renderAccountSuggestion).join("") : '<p class="empty-state" style="margin-top:10px;">这份报告还没有 suggestion。</p>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}).join("") : '<p class="empty-state">这个账号还没有分析报告。你可以直接点上面的“运行分析”。</p>';
|
||||
@@ -1214,6 +1532,7 @@ function renderPage() {
|
||||
return '<button type="button" class="similar-item ' + (selected ? "active" : "") + '" data-search-id="' + escapeHtml(search.id) + '"><strong>' + escapeHtml(safeArray(search.keywords).slice(0, 4).join(" / ") || search.id) + '</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(formatDateTime(search.created_at)) + '</div></button>';
|
||||
}).join("") : '<p class="empty-state">这个账号还没有相似搜索记录。</p>';
|
||||
renderSimilarSearchDetail();
|
||||
renderVideos();
|
||||
}
|
||||
|
||||
async function loadSnapshotDetail(snapshotId) {
|
||||
@@ -1274,10 +1593,16 @@ function renderPage() {
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/workspace"),
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/snapshots").catch(() => [])
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/snapshots").catch(() => []),
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/videos?limit=80").catch(() => ({ items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }))
|
||||
]);
|
||||
workbenchState.selectedWorkspace = results[0];
|
||||
workbenchState.snapshots = safeArray(results[1]);
|
||||
workbenchState.videoItems = safeArray(results[2]?.items);
|
||||
workbenchState.videoMeta = results[2]?.meta || {};
|
||||
workbenchState.topScoredVideoIds = safeArray(results[2]?.top_scored_video_ids);
|
||||
workbenchState.latestVideoIds = safeArray(results[2]?.latest_video_ids);
|
||||
workbenchState.highScoreThreshold = Number(results[2]?.high_score_threshold || 60);
|
||||
workbenchState.selectedSnapshotId = options.snapshotId || workbenchState.snapshots[0]?.id || "";
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
workbenchState.similarSearchDetail = null;
|
||||
@@ -1545,7 +1870,38 @@ function renderPage() {
|
||||
}
|
||||
});
|
||||
|
||||
analyzeTopVideosButton.addEventListener("click", async () => {
|
||||
if (!workbenchState.selectedAccountId) {
|
||||
alert("请先选择一个账号。");
|
||||
return;
|
||||
}
|
||||
analyzeTopVideosButton.disabled = true;
|
||||
analysisFeedbackEl.textContent = "正在自动分析高分作品...";
|
||||
try {
|
||||
await storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(workbenchState.selectedAccountId) + "/videos/analyze-top", {
|
||||
method: "POST",
|
||||
body: {
|
||||
model_profile_id: analysisModelSelectEl.value || null,
|
||||
top_video_count: Math.max(2, Math.min(8, Number.parseInt(analysisMaxVideosEl.value || "6", 10) || 6)),
|
||||
min_score: Number(workbenchState.highScoreThreshold || 45),
|
||||
temperature: 0.2
|
||||
}
|
||||
});
|
||||
workbenchState.lastAnalysisMessage = "高分作品自动分析已更新。";
|
||||
analysisFeedbackEl.textContent = workbenchState.lastAnalysisMessage;
|
||||
await selectAccount(workbenchState.selectedAccountId, { preserveFeedback: true });
|
||||
} catch (error) {
|
||||
analysisFeedbackEl.textContent = "高分作品自动分析失败: " + error.message;
|
||||
} finally {
|
||||
analyzeTopVideosButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
accountsFilterEl.addEventListener("input", renderAccountList);
|
||||
videosScopeEl.addEventListener("change", renderVideos);
|
||||
videosSortEl.addEventListener("change", renderVideos);
|
||||
videosTagFilterEl.addEventListener("input", renderVideos);
|
||||
videosQueryFilterEl.addEventListener("input", renderVideos);
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const accountButton = event.target.closest("[data-account-id]");
|
||||
|
||||
Reference in New Issue
Block a user