|
@@ -12,6 +12,7 @@ import { asRecord, text } from "@/lib/flow-ledger/format";
|
|
|
|
|
|
|
|
type WalkGroup = {
|
|
type WalkGroup = {
|
|
|
id: string;
|
|
id: string;
|
|
|
|
|
+ kind: "video" | "query";
|
|
|
video?: RawRecord;
|
|
video?: RawRecord;
|
|
|
title: string;
|
|
title: string;
|
|
|
author: string;
|
|
author: string;
|
|
@@ -121,7 +122,7 @@ function WalkGroupCard({
|
|
|
return (
|
|
return (
|
|
|
<article className={`walk-story-block level-${Math.min(level, 3)}`} id={`source-${encodeURIComponent(group.id)}`}>
|
|
<article className={`walk-story-block level-${Math.min(level, 3)}`} id={`source-${encodeURIComponent(group.id)}`}>
|
|
|
<header className="walk-story-head">
|
|
<header className="walk-story-head">
|
|
|
- <span className="walk-source-kicker">{level ? `第 ${level + 1} 层起点` : `起点视频 ${index + 1}`}</span>
|
|
|
|
|
|
|
+ <span className="walk-source-kicker">{sourceKicker(group, index, level)}</span>
|
|
|
<div className="walk-story-title">
|
|
<div className="walk-story-title">
|
|
|
<h2>{titleParts.title}</h2>
|
|
<h2>{titleParts.title}</h2>
|
|
|
{titleParts.tags.length ? (
|
|
{titleParts.tags.length ? (
|
|
@@ -134,14 +135,14 @@ function WalkGroupCard({
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
<div className="walk-source-facts">
|
|
<div className="walk-source-facts">
|
|
|
- <Fact label="作者" value={group.author} />
|
|
|
|
|
- <Fact label="判断" value={group.decisionLabel} tone={decisionTone(group.decisionLabel)} />
|
|
|
|
|
|
|
+ {group.kind === "video" ? <Fact label="作者" value={group.author} /> : <Fact label="类型" value="搜索词级扩展" tone="info" />}
|
|
|
|
|
+ {group.kind === "video" ? <Fact label="判断" value={group.decisionLabel} tone={decisionTone(group.decisionLabel)} /> : <Fact label="来源" value="从当前搜索词继续找" />}
|
|
|
{score ? <Fact label="评分" value={score} /> : null}
|
|
{score ? <Fact label="评分" value={score} /> : null}
|
|
|
{group.terminalLabel ? <Fact label="处理" value={group.terminalLabel} /> : null}
|
|
{group.terminalLabel ? <Fact label="处理" value={group.terminalLabel} /> : null}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<section className="walk-story-routes" aria-label={`${group.title} 的游走路线`}>
|
|
<section className="walk-story-routes" aria-label={`${group.title} 的游走路线`}>
|
|
|
- <div className="walk-story-routes-label">从这条视频继续往外找</div>
|
|
|
|
|
|
|
+ <div className="walk-story-routes-label">{group.kind === "video" ? "从这条视频继续往外找" : "从这个搜索词继续往外找"}</div>
|
|
|
{routeActions.length ? routeActions.map((action, actionIndex) => (
|
|
{routeActions.length ? routeActions.map((action, actionIndex) => (
|
|
|
<WalkRouteCard
|
|
<WalkRouteCard
|
|
|
action={action}
|
|
action={action}
|
|
@@ -152,7 +153,7 @@ function WalkGroupCard({
|
|
|
seen={nextSeen}
|
|
seen={nextSeen}
|
|
|
key={`${text(action.id, "walk")}-${actionIndex}`}
|
|
key={`${text(action.id, "walk")}-${actionIndex}`}
|
|
|
/>
|
|
/>
|
|
|
- )) : <div className="walk-no-route">这条视频没有继续产生标签、作者或翻页路线。</div>}
|
|
|
|
|
|
|
+ )) : <div className="walk-no-route">{group.kind === "video" ? "这条视频没有继续产生标签、作者或翻页路线。" : "这个搜索词没有继续翻页或产生新路线。"}</div>}
|
|
|
</section>
|
|
</section>
|
|
|
</article>
|
|
</article>
|
|
|
);
|
|
);
|
|
@@ -207,7 +208,7 @@ function WalkRouteCard({
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="walk-flow" aria-label={`${routeName} 的链路`}>
|
|
<div className="walk-flow" aria-label={`${routeName} 的链路`}>
|
|
|
- <FlowNode label="从" value="这条视频" href={`#source-${encodeURIComponent(sourceId)}`} />
|
|
|
|
|
|
|
+ <FlowNode label="从" value={text(action.from_label, "").includes("搜索词") ? "这个搜索词" : "这条视频"} href={`#source-${encodeURIComponent(sourceId)}`} />
|
|
|
<FlowArrow />
|
|
<FlowArrow />
|
|
|
<FlowNode label={firstNode.label} value={firstNode.value} />
|
|
<FlowNode label={firstNode.label} value={firstNode.value} />
|
|
|
<FlowArrow />
|
|
<FlowArrow />
|
|
@@ -321,8 +322,9 @@ function buildWalkGroups(actions: RawRecord[]): WalkGroup[] {
|
|
|
const decision = asRecord(video.decision);
|
|
const decision = asRecord(video.decision);
|
|
|
groups.set(id, {
|
|
groups.set(id, {
|
|
|
id,
|
|
id,
|
|
|
|
|
+ kind: video.id ? "video" : "query",
|
|
|
video: video.id ? video : undefined,
|
|
video: video.id ? video : undefined,
|
|
|
- title: video.id ? text(video.title, "无标题视频") : cleanBusinessText(text(action.from_label, "从当前搜索词触发")),
|
|
|
|
|
|
|
+ title: video.id ? text(video.title, "无标题视频") : queryGroupTitle(action),
|
|
|
author: video.id ? text(video.author, "未知作者") : "搜索词级扩展",
|
|
author: video.id ? text(video.author, "未知作者") : "搜索词级扩展",
|
|
|
decisionLabel: text(decision.label, "未判断"),
|
|
decisionLabel: text(decision.label, "未判断"),
|
|
|
terminalLabel: terminal ? terminalResultText(action) : "",
|
|
terminalLabel: terminal ? terminalResultText(action) : "",
|
|
@@ -340,6 +342,17 @@ function actionKey(action: RawRecord): string {
|
|
|
return text(action.id || action.walk_action_id || `${text(action.edge_id, "walk")}-${text(action.from_label, "")}-${text(action.result, "")}`);
|
|
return text(action.id || action.walk_action_id || `${text(action.edge_id, "walk")}-${text(action.from_label, "")}-${text(action.result, "")}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function queryGroupTitle(action: RawRecord): string {
|
|
|
|
|
+ const from = cleanBusinessText(text(action.from_label, ""));
|
|
|
|
|
+ if (from) return from.replace(/^从搜索词/, "搜索词").replace(/继续$/, "");
|
|
|
|
|
+ return "当前搜索词";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sourceKicker(group: WalkGroup, index: number, level: number): string {
|
|
|
|
|
+ if (level) return `第 ${level + 1} 层起点`;
|
|
|
|
|
+ return group.kind === "video" ? `起点视频 ${index + 1}` : "起点搜索词";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function VideoLinks({ video, runId, compact = false }: { video?: RawRecord; runId: string; compact?: boolean }) {
|
|
function VideoLinks({ video, runId, compact = false }: { video?: RawRecord; runId: string; compact?: boolean }) {
|
|
|
if (!video || !video.id) return null;
|
|
if (!video || !video.id) return null;
|
|
|
const platformUrl = text(video.platform_url, "");
|
|
const platformUrl = text(video.platform_url, "");
|
|
@@ -447,7 +460,10 @@ function routeResultNode(
|
|
|
|
|
|
|
|
function successSentence(action: RawRecord, targetQuery: RawRecord, targetVideos: RawRecord[]): string {
|
|
function successSentence(action: RawRecord, targetQuery: RawRecord, targetVideos: RawRecord[]): string {
|
|
|
const edgeId = text(action.edge_id, "");
|
|
const edgeId = text(action.edge_id, "");
|
|
|
- if (edgeId === "query_next_page") return `翻到下一页,带回 ${targetVideos.length} 条视频。`;
|
|
|
|
|
|
|
+ if (edgeId === "query_next_page") {
|
|
|
|
|
+ const from = cleanBusinessText(text(action.from_label, "当前搜索词")).replace(/继续$/, "");
|
|
|
|
|
+ return `${from}翻到下一页,带回 ${targetVideos.length} 条视频。`;
|
|
|
|
|
+ }
|
|
|
if (edgeId === "hashtag_to_query" || edgeId === "tag_query") {
|
|
if (edgeId === "hashtag_to_query" || edgeId === "tag_query") {
|
|
|
const queryText = text(targetQuery.text, "");
|
|
const queryText = text(targetQuery.text, "");
|
|
|
return queryText ? `用标签生成搜索词《${queryText}》,带回 ${targetVideos.length} 条视频。` : `从标签继续搜,带回 ${targetVideos.length} 条视频。`;
|
|
return queryText ? `用标签生成搜索词《${queryText}》,带回 ${targetVideos.length} 条视频。` : `从标签继续搜,带回 ${targetVideos.length} 条视频。`;
|