|
@@ -4,14 +4,10 @@ import Link from "next/link";
|
|
|
import { WalkJourney } from "@/features/runs/WalkJourney";
|
|
import { WalkJourney } from "@/features/runs/WalkJourney";
|
|
|
|
|
|
|
|
import {
|
|
import {
|
|
|
- ChevronDown,
|
|
|
|
|
ChevronRight,
|
|
ChevronRight,
|
|
|
FileJson,
|
|
FileJson,
|
|
|
- PlayCircle,
|
|
|
|
|
GitBranch,
|
|
GitBranch,
|
|
|
- ListFilter,
|
|
|
|
|
PanelRightOpen,
|
|
PanelRightOpen,
|
|
|
- ShieldCheck,
|
|
|
|
|
Target
|
|
Target
|
|
|
} from "lucide-react";
|
|
} from "lucide-react";
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -36,8 +32,6 @@ import type {
|
|
|
TimelineResponse,
|
|
TimelineResponse,
|
|
|
} from "@/lib/api/types";
|
|
} from "@/lib/api/types";
|
|
|
import {compactValue, statusLabel } from "@/lib/status/status";
|
|
import {compactValue, statusLabel } from "@/lib/status/status";
|
|
|
-import { contentUrl, embedPlayerUrl, platformLabel } from "@/lib/platform/content";
|
|
|
|
|
-import { isLlmVariantQuery, llmVariantPromptPayload } from "@/features/runs/queryPrompt";
|
|
|
|
|
|
|
|
|
|
type DashboardData = {
|
|
type DashboardData = {
|
|
|
dashboard: DashboardResponse;
|
|
dashboard: DashboardResponse;
|
|
@@ -307,74 +301,6 @@ function stageCount(dashboard: DashboardResponse, stageId: string): number | nul
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function queryStageStatus(query: Record<string, unknown>): { status: string; label: string } {
|
|
|
|
|
- if (query.failure_reason) {
|
|
|
|
|
- return { status: "failed", label: "平台失败" };
|
|
|
|
|
- }
|
|
|
|
|
- return { status: "success", label: "生成成功" };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function queryGenerationMethodLabel(method: unknown): string {
|
|
|
|
|
- const value = String(method || "");
|
|
|
|
|
- const labels: Record<string, string> = {
|
|
|
|
|
- item_single: "原词直搜",
|
|
|
|
|
- llm_variant: "AI 扩写",
|
|
|
|
|
- query_next_page: "翻页搜索",
|
|
|
|
|
- tag_query: "Tag 搜索"
|
|
|
|
|
- };
|
|
|
|
|
- return labels[value] || compactValue(method);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function queryGenerationExplanation(query: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
- const seedRef = query.pattern_seed_ref && typeof query.pattern_seed_ref === "object"
|
|
|
|
|
- ? query.pattern_seed_ref as Record<string, unknown>
|
|
|
|
|
- : {};
|
|
|
|
|
- const llmInput = query.llm_input_evidence && typeof query.llm_input_evidence === "object"
|
|
|
|
|
- ? query.llm_input_evidence as Record<string, unknown>
|
|
|
|
|
- : {};
|
|
|
|
|
- const method = String(query.search_query_generation_method || "");
|
|
|
|
|
- const promptText =
|
|
|
|
|
- method === "llm_variant"
|
|
|
|
|
- ? "AI 扩写:基于 seed_term、分类路径、元素证据和已有 query,生成一条更适合抖音搜索的变体 query。"
|
|
|
|
|
- : method === "item_single"
|
|
|
|
|
- ? "原词直搜:不调用 LLM,直接把 DemandAgent seed_term 作为搜索词。"
|
|
|
|
|
- : method === "query_next_page"
|
|
|
|
|
- ? "翻页搜索:不调用 LLM,沿用上一页 query 和 next_cursor 拉取下一页。"
|
|
|
|
|
- : "按当前生成方式生成搜索词。";
|
|
|
|
|
- return {
|
|
|
|
|
- "生成出的 query": query.search_query,
|
|
|
|
|
- "生成方式": queryGenerationMethodLabel(method),
|
|
|
|
|
- "种子词": seedRef.seed_term || llmInput.seed_term || query.search_query,
|
|
|
|
|
- "父 query": query.llm_variant_of || query.parent_search_query_id || "无",
|
|
|
|
|
- "提示词口径": promptText,
|
|
|
|
|
- "提示词版本": query.llm_prompt_version || "不适用",
|
|
|
|
|
- "LLM 模型": query.llm_generation_model || "不适用",
|
|
|
|
|
- "输入证据摘要": {
|
|
|
|
|
- seed_terms: llmInput.seed_terms || seedRef.seed_terms || seedRef.seed_term,
|
|
|
|
|
- itemset_ids: llmInput.itemset_ids || seedRef.itemset_ids,
|
|
|
|
|
- category_bindings: llmInput.category_bindings,
|
|
|
|
|
- element_bindings: llmInput.element_bindings,
|
|
|
|
|
- source_post_id: llmInput.source_post_id || seedRef.source_post_id,
|
|
|
|
|
- pattern_execution_id: llmInput.pattern_execution_id || seedRef.pattern_execution_id,
|
|
|
|
|
- existing_search_queries: llmInput.existing_search_queries
|
|
|
|
|
- },
|
|
|
|
|
- "原始记录": query
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function primaryQuerySource(item: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
- const sources = Array.isArray(item.query_sources) ? item.query_sources : [];
|
|
|
|
|
- const first = sources[0];
|
|
|
|
|
- if (first && typeof first === "object") {
|
|
|
|
|
- return first as Record<string, unknown>;
|
|
|
|
|
- }
|
|
|
|
|
- return {
|
|
|
|
|
- search_query_id: item.search_query_id,
|
|
|
|
|
- search_query: item.search_query,
|
|
|
|
|
- search_query_generation_method: item.search_query_generation_method
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function runtimeData(file: RuntimeFileResponse | null): Record<string, unknown> {
|
|
function runtimeData(file: RuntimeFileResponse | null): Record<string, unknown> {
|
|
|
if (!file?.data || typeof file.data !== "object") {
|
|
if (!file?.data || typeof file.data !== "object") {
|
|
|
return {};
|
|
return {};
|
|
@@ -482,112 +408,6 @@ function StagePanel({
|
|
|
</BusinessSection>
|
|
</BusinessSection>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
- if (activeStage === "query") {
|
|
|
|
|
- return (
|
|
|
|
|
- <BusinessSection title="Query 效果" icon={<ListFilter size={17} />}>
|
|
|
|
|
- <ConclusionBody stage={data.dashboard.stage_conclusions.find((stage) => stage.stage_id === "query")} />
|
|
|
|
|
- <div className="business-card-list">
|
|
|
|
|
- {data.queries.items.length ? data.queries.items.map((query, index) => (
|
|
|
|
|
- <div className="business-record-card" key={String(query.search_query_id || index)}>
|
|
|
|
|
- <strong>{compactValue(query.search_query)}</strong>
|
|
|
|
|
- <div className="badge-row">
|
|
|
|
|
- <span className="badge">{queryGenerationMethodLabel(query.search_query_generation_method)}</span>
|
|
|
|
|
- <span className={`badge ${queryStageStatus(query).status}`}>
|
|
|
|
|
- {queryStageStatus(query).label}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="query-meta-line">
|
|
|
|
|
- <span>种子词:{compactValue((query.pattern_seed_ref as Record<string, unknown> | undefined)?.seed_term)}</span>
|
|
|
|
|
- <span>Query ID:{compactValue(query.search_query_id)}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <span>{compactValue(query.failure_reason || "已生成,可在判断模块查看内容结果")}</span>
|
|
|
|
|
- <div className="business-action-row">
|
|
|
|
|
- <button
|
|
|
|
|
- className="text-button"
|
|
|
|
|
- onClick={() =>
|
|
|
|
|
- onOpenDrawer({
|
|
|
|
|
- kind: "technical",
|
|
|
|
|
- title: `Query 生成依据:${compactValue(query.search_query)}`,
|
|
|
|
|
- payload: queryGenerationExplanation(query)
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- type="button"
|
|
|
|
|
- >
|
|
|
|
|
- 查看生成依据
|
|
|
|
|
- </button>
|
|
|
|
|
- {isLlmVariantQuery(query) ? (
|
|
|
|
|
- <button
|
|
|
|
|
- className="text-button"
|
|
|
|
|
- onClick={() =>
|
|
|
|
|
- onOpenDrawer({
|
|
|
|
|
- kind: "technical",
|
|
|
|
|
- title: `AI 扩写提示词:${compactValue(query.search_query)}`,
|
|
|
|
|
- payload: llmVariantPromptPayload(query)
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- type="button"
|
|
|
|
|
- >
|
|
|
|
|
- 查看 AI 扩写提示词
|
|
|
|
|
- </button>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )) : <div className="empty-state">没有 query 记录</div>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </BusinessSection>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- if (activeStage === "platform") {
|
|
|
|
|
- return (
|
|
|
|
|
- <BusinessSection title="平台发现内容" icon={<Target size={17} />}>
|
|
|
|
|
- <ConclusionBody stage={data.dashboard.stage_conclusions.find((stage) => stage.stage_id === "platform")} />
|
|
|
|
|
- <div className="business-card-list">
|
|
|
|
|
- {data.contentItems.items.length ? data.contentItems.items.map((item, index) => (
|
|
|
|
|
- <ContentPostCard item={item} index={index} key={String(item.platform_content_id || index)} />
|
|
|
|
|
- )) : <div className="empty-state">还没有发现内容;请先查看 Query 或平台失败原因</div>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </BusinessSection>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- if (activeStage === "judge") {
|
|
|
|
|
- const itemByContentId = new Map<string, Record<string, unknown>>();
|
|
|
|
|
- data.contentItems.items.forEach((it) => {
|
|
|
|
|
- const id = String(it.platform_content_id || "");
|
|
|
|
|
- if (id) itemByContentId.set(id, it);
|
|
|
|
|
- });
|
|
|
|
|
- return (
|
|
|
|
|
- <BusinessSection title="策略包如何施加到内容判断" icon={<ShieldCheck size={17} />}>
|
|
|
|
|
- <ConclusionBody stage={data.dashboard.stage_conclusions.find((stage) => stage.stage_id === "judge")} />
|
|
|
|
|
- <div className="rule-chain">
|
|
|
|
|
- <span>视频</span>
|
|
|
|
|
- <ChevronRight size={16} />
|
|
|
|
|
- <span>Gemini 直读判定</span>
|
|
|
|
|
- <ChevronRight size={16} />
|
|
|
|
|
- <span>硬性筛选门槛</span>
|
|
|
|
|
- <ChevronRight size={16} />
|
|
|
|
|
- <span>Scorecard(相关性 + 平台热度)</span>
|
|
|
|
|
- <ChevronRight size={16} />
|
|
|
|
|
- <span>Decision</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="business-card-list">
|
|
|
|
|
- {data.dashboard.rule_application_summary.length ? data.dashboard.rule_application_summary.map((item, index) => (
|
|
|
|
|
- <RuleApplicationCard
|
|
|
|
|
- item={item}
|
|
|
|
|
- contentItem={itemByContentId.get(String(item.platform_content_id || ""))}
|
|
|
|
|
- key={item.decision_id || index}
|
|
|
|
|
- onOpen={() =>
|
|
|
|
|
- onOpenDrawer({
|
|
|
|
|
- kind: "rule",
|
|
|
|
|
- title: item.content_title || item.platform_content_id || "规则详情",
|
|
|
|
|
- payload: item
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- )) : <div className="empty-state">还没有内容进入规则判断</div>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </BusinessSection>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
if (activeStage === "walk") {
|
|
if (activeStage === "walk") {
|
|
|
return (
|
|
return (
|
|
|
<BusinessSection title="内容发现旅程" icon={<GitBranch size={17} />}>
|
|
<BusinessSection title="内容发现旅程" icon={<GitBranch size={17} />}>
|
|
@@ -597,10 +417,10 @@ function StagePanel({
|
|
|
contentItems={data.contentItems}
|
|
contentItems={data.contentItems}
|
|
|
queries={data.queries.items}
|
|
queries={data.queries.items}
|
|
|
fallbackEdges={data.dashboard.walk_graph.edges}
|
|
fallbackEdges={data.dashboard.walk_graph.edges}
|
|
|
- onOpen={(payload) =>
|
|
|
|
|
|
|
+ onOpen={(payload, kind = "walk") =>
|
|
|
onOpenDrawer({
|
|
onOpenDrawer({
|
|
|
- kind: "walk",
|
|
|
|
|
- title: String(payload.title || payload.id || "视频旅程详情"),
|
|
|
|
|
|
|
+ kind,
|
|
|
|
|
+ title: String(payload["这个提示词在做什么"] ? "AI 扩写提示词" : payload.title || payload.id || "视频旅程详情"),
|
|
|
payload
|
|
payload
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -649,102 +469,6 @@ function StagePanel({
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function ContentPostCard({ item, index }: { item: Record<string, unknown>; index: number }) {
|
|
|
|
|
- const [open, setOpen] = useState(true);
|
|
|
|
|
- const [videoOn, setVideoOn] = useState(false);
|
|
|
|
|
- const querySource = primaryQuerySource(item);
|
|
|
|
|
- const contentId = String(item.platform_content_id || "");
|
|
|
|
|
- const platform = String(item.platform || "");
|
|
|
|
|
- const pLabel = platformLabel(platform);
|
|
|
|
|
- const playerUrl = embedPlayerUrl(item); // 仅抖音有官方嵌入播放器;其余平台 null
|
|
|
|
|
- const origin = contentUrl(item);
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="business-record-card">
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- className="content-card-toggle"
|
|
|
|
|
- aria-expanded={open}
|
|
|
|
|
- onClick={() => setOpen((v) => !v)}
|
|
|
|
|
- >
|
|
|
|
|
- {open ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
|
|
|
- <span className="content-card-heading">
|
|
|
|
|
- <span>{pLabel}内容 #{index + 1}</span>
|
|
|
|
|
- <strong>{compactValue(item.title || item.description || `${pLabel}视频`)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- </button>
|
|
|
|
|
- {open ? (
|
|
|
|
|
- <div className="content-card-body">
|
|
|
|
|
- <div className="content-card-detail">
|
|
|
|
|
- <div className="content-meta-grid">
|
|
|
|
|
- <span>
|
|
|
|
|
- {pLabel}视频 ID
|
|
|
|
|
- <strong>{compactValue(item.platform_content_id)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- 来源 Query
|
|
|
|
|
- <strong>{compactValue(querySource.search_query_id)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- 搜索关键词
|
|
|
|
|
- <strong>{compactValue(querySource.search_query)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- Query 类型
|
|
|
|
|
- <strong>{queryGenerationMethodLabel(querySource.search_query_generation_method)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- {pLabel}作者
|
|
|
|
|
- <strong>{compactValue(item.author_display_name || item.platform_author_id)}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="business-action-row">
|
|
|
|
|
- {origin ? (
|
|
|
|
|
- <a className="text-button" href={origin} rel="noreferrer" target="_blank">
|
|
|
|
|
- 打开原帖
|
|
|
|
|
- </a>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="muted">该平台未提供原帖链接</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="content-card-video">
|
|
|
|
|
- {!contentId ? (
|
|
|
|
|
- <div className="content-video-placeholder muted">无视频 ID</div>
|
|
|
|
|
- ) : !playerUrl ? (
|
|
|
|
|
- <div className="content-video-placeholder muted">
|
|
|
|
|
- {pLabel}无公开嵌入播放器
|
|
|
|
|
- {origin ? (
|
|
|
|
|
- <a className="text-button" href={origin} rel="noreferrer" target="_blank">
|
|
|
|
|
- 打开原帖
|
|
|
|
|
- </a>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : videoOn ? (
|
|
|
|
|
- <iframe
|
|
|
|
|
- className="content-video-frame"
|
|
|
|
|
- src={playerUrl}
|
|
|
|
|
- title={`${pLabel}视频 ${contentId}`}
|
|
|
|
|
- loading="lazy"
|
|
|
|
|
- allow="autoplay; encrypted-media; fullscreen; picture-in-picture"
|
|
|
|
|
- allowFullScreen
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- className="content-video-placeholder content-video-load"
|
|
|
|
|
- onClick={() => setVideoOn(true)}
|
|
|
|
|
- >
|
|
|
|
|
- <PlayCircle size={36} />
|
|
|
|
|
- <span>点击播放视频</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function BusinessSection({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) {
|
|
function BusinessSection({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) {
|
|
|
return (
|
|
return (
|
|
|
<section className="business-section">
|
|
<section className="business-section">
|
|
@@ -769,115 +493,6 @@ function ConclusionBody({ stage }: { stage?: StageConclusion }) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function RuleApplicationCard({
|
|
|
|
|
- item,
|
|
|
|
|
- contentItem,
|
|
|
|
|
- onOpen
|
|
|
|
|
-}: {
|
|
|
|
|
- item: RuleApplicationSummary;
|
|
|
|
|
- contentItem?: Record<string, unknown>;
|
|
|
|
|
- onOpen: () => void;
|
|
|
|
|
-}) {
|
|
|
|
|
- const title = item.content_title && item.content_title !== item.platform_content_id ? item.content_title : "视频内容";
|
|
|
|
|
- const platform = contentItem ? String(contentItem.platform || "") : "";
|
|
|
|
|
- const pmr = (contentItem?.pattern_match_result as Record<string, unknown>) || null;
|
|
|
|
|
- const idLabel = platform ? `${platformLabel(platform)}视频 ID` : "视频 ID";
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="rule-application-card">
|
|
|
|
|
- <div className="rule-card-heading">
|
|
|
|
|
- <span>判断对象</span>
|
|
|
|
|
- <strong>{compactValue(title)}</strong>
|
|
|
|
|
- <small>{idLabel}:{compactValue(item.platform_content_id)}</small>
|
|
|
|
|
- </div>
|
|
|
|
|
- {pmr ? <GeminiJudgeRow pmr={pmr} /> : null}
|
|
|
|
|
- <div className="rule-application-flow">
|
|
|
|
|
- <span>
|
|
|
|
|
- 规则包:{rulePackLabel(item.rule_pack)}
|
|
|
|
|
- <small>规则包 ID:{compactValue(item.rule_pack)}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- 硬性筛选门槛:{hardGateLabel(item)}
|
|
|
|
|
- <small>{reasonLabel(item.decision_reason_code)}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- Score:{scoreLabel(item.score)}
|
|
|
|
|
- <small>{item.score === null || item.score === undefined ? "未进入打分或分数缺失" : "内容综合分"}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span>
|
|
|
|
|
- 判断结果:{decisionActionLabel(item.decision_action)}
|
|
|
|
|
- <small>{effectStatusLabel(item.content_effect_status)}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <ScorecardRow contentItem={contentItem} />
|
|
|
|
|
- <div className="business-alert compact">
|
|
|
|
|
- <ShieldCheck size={14} />
|
|
|
|
|
- <span>主原因:{reasonLabel(item.decision_reason_code || item.primary_reason)}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button className="text-button" onClick={onOpen} type="button">
|
|
|
|
|
- 查看规则包详情
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function GeminiJudgeRow({ pmr }: { pmr: Record<string, unknown> }) {
|
|
|
|
|
- const fit = pmr.fit_senior_50plus;
|
|
|
|
|
- const conf = pmr.fit_confidence;
|
|
|
|
|
- const rel = pmr.relevance_score;
|
|
|
|
|
- const status = String(pmr.judge_status || "");
|
|
|
|
|
- const reason = String(pmr.reason || "");
|
|
|
|
|
- const quota = status === "gemini_quota_exhausted";
|
|
|
|
|
- // 仅 V3 Gemini 判定有这些字段;V2 run 无任何判定信号则整行不渲染。
|
|
|
|
|
- const hasV3Signal = status !== "" || typeof fit === "boolean" || rel != null || conf != null;
|
|
|
|
|
- if (!hasV3Signal) return null;
|
|
|
|
|
- return (
|
|
|
|
|
- <div className={`gemini-judge-row${quota ? " quota" : ""}`}>
|
|
|
|
|
- <span className="gemini-judge-tag">Gemini 判定</span>
|
|
|
|
|
- {quota ? (
|
|
|
|
|
- <span className="gemini-judge-quota">⚠ 配额截断,未判定</span>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <>
|
|
|
|
|
- <span>适合 50+:{fit === true ? "是" : fit === false ? "否" : "—"}</span>
|
|
|
|
|
- <span>置信度:{conf == null ? "—" : compactValue(conf)}</span>
|
|
|
|
|
- <span>相关性:{rel == null ? "—" : compactValue(rel)}</span>
|
|
|
|
|
- <span>状态:{statusLabel(status)}</span>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- {reason ? <small className="gemini-judge-reason">{reason}</small> : null}
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function ScorecardRow({ contentItem }: { contentItem?: Record<string, unknown> }) {
|
|
|
|
|
- const decision = (contentItem?.rule_decision as Record<string, unknown>) || null;
|
|
|
|
|
- const scorecard = (decision?.scorecard as Record<string, unknown>) || null;
|
|
|
|
|
- const dims = (scorecard?.dimensions as Array<Record<string, unknown>>) || [];
|
|
|
|
|
- if (!dims.length) return null;
|
|
|
|
|
- const DEPRECATED = new Set([
|
|
|
|
|
- "content_audience_profile",
|
|
|
|
|
- "interaction_performance",
|
|
|
|
|
- "freshness_available",
|
|
|
|
|
- "douyin_tone",
|
|
|
|
|
- "adaptability"
|
|
|
|
|
- ]);
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="scorecard-row">
|
|
|
|
|
- <span className="scorecard-label">评分维度</span>
|
|
|
|
|
- {dims.map((d, i) => {
|
|
|
|
|
- const key = String(d.key || "");
|
|
|
|
|
- const deprecated = DEPRECATED.has(key);
|
|
|
|
|
- return (
|
|
|
|
|
- <span className={`scorecard-chip${deprecated ? " deprecated" : ""}`} key={key || i}>
|
|
|
|
|
- {key}:{d.score == null ? "—" : compactValue(d.score)}
|
|
|
|
|
- {d.max_score != null ? ` / ${compactValue(d.max_score)}` : ""}
|
|
|
|
|
- {deprecated ? "(弃用)" : ""}
|
|
|
|
|
- </span>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function rulePackLabel(rulePack: unknown): string {
|
|
function rulePackLabel(rulePack: unknown): string {
|
|
|
const value = String(rulePack || "");
|
|
const value = String(rulePack || "");
|
|
|
const labels: Record<string, string> = {
|
|
const labels: Record<string, string> = {
|