| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- "use client";
- import Link from "next/link";
- import { ExternalLink, Hash, Info, ListVideo, Search, UserSearch } from "lucide-react";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import { AppShell } from "@/components/AppShell";
- import { BusinessDrawer, type BusinessDrawerState } from "@/components/BusinessDrawer";
- import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
- import { getFlowLedgerWalk } from "@/lib/api/client";
- import type { FlowLedgerWalkResponse, RawRecord } from "@/lib/api/types";
- import { methodLabel } from "@/lib/flow-ledger/business";
- import { asRecord, text } from "@/lib/flow-ledger/format";
- type WalkGroup = {
- id: string;
- kind: "video" | "query";
- video?: RawRecord;
- title: string;
- author: string;
- decisionLabel: string;
- terminalLabel: string;
- actions: RawRecord[];
- };
- type WalkTree = {
- roots: WalkGroup[];
- childrenByActionId: Map<string, WalkGroup[]>;
- totalGroups: number;
- };
- export function QueryWalkPage({ runId, queryId }: { runId: string; queryId: string }) {
- const [data, setData] = useState<FlowLedgerWalkResponse | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [drawer, setDrawer] = useState<BusinessDrawerState>(null);
- const load = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const debug = new URLSearchParams(window.location.search).get("debug") === "1";
- setData(await getFlowLedgerWalk(runId, queryId, debug));
- } catch (err) {
- setError(err instanceof Error ? err.message : String(err));
- } finally {
- setLoading(false);
- }
- }, [queryId, runId]);
- useEffect(() => {
- void load();
- }, [load]);
- const query = asRecord(data?.query);
- const actions = useMemo(() => [...(data?.actions || [])].sort((a, b) => actionDepth(a) - actionDepth(b)), [data?.actions]);
- const tree = useMemo(() => buildWalkTree(actions), [actions]);
- return (
- <AppShell title="继续扩展过程" subtitle={query.text ? `搜索词:${text(query.text)}` : "查看系统有没有继续向外找内容"} runId={runId} showBack onRefresh={load}>
- {loading ? <LoadingState /> : null}
- {error ? <ErrorState message={error} /> : null}
- {!loading && !error && !actions.length ? <EmptyState label="这个搜索词本轮没有继续扩展" /> : null}
- {data ? (
- <>
- <details className="declarations" open>
- <summary>
- <span className="kw">游走</span> <b>{text(query.text, "搜索词待确认")}</b>
- <span className="decl-purpose">{methodLabel(text(query.method, ""))}</span>
- <span className="tag-mini">{tree.totalGroups ? `${tree.totalGroups} 个起点` : "没有继续扩展"}</span>
- </summary>
- <div className="decl-body">
- <Summary label="怎么看" value="先看每次游走用了什么标签、作者或翻页;再看它带回的视频是否入池、待复看或淘汰。带回的视频如果继续游走,会在视频卡下面继续缩进展开。" />
- <Summary label="本轮概况" value={`已游走 ${Number(data.summary.success_count || 0)} 条路线 · 未游走 ${Number(data.summary.skipped_count || 0)} 条路线 · 新增搜索词 ${Number(data.summary.extension_query_count || 0)} 个`} />
- </div>
- </details>
- <section className="walk-ledger-board">
- <div className="walk-ledger-root">
- <span className="chip dark">当前搜索词</span>
- <strong>{text(query.text, "搜索词待确认")}</strong>
- <small>{methodLabel(text(query.method, ""))}</small>
- </div>
- <div className="walk-ledger-groups">
- {tree.roots.map((group, index) => (
- <WalkGroupCard group={group} index={index} runId={runId} tree={tree} level={0} onOpenScore={setDrawer} key={group.id} />
- ))}
- </div>
- </section>
- <BusinessDrawer drawer={drawer} onClose={() => setDrawer(null)} />
- </>
- ) : null}
- </AppShell>
- );
- }
- function Summary({ label, value }: { label: string; value: string }) {
- return (
- <div className="decl-section">
- <div className="decl-label">{label}</div>
- <div className="decl-row">{value}</div>
- </div>
- );
- }
- function WalkGroupCard({
- group,
- index,
- runId,
- tree,
- level,
- onOpenScore,
- seen = new Set<string>()
- }: {
- group: WalkGroup;
- index: number;
- runId: string;
- tree: WalkTree;
- level: number;
- onOpenScore: (drawer: BusinessDrawerState) => void;
- seen?: Set<string>;
- }) {
- const routeActions = group.actions.filter((action) => !isTerminalEdge(text(action.edge_id, "")));
- const titleParts = splitTitleAndTags(group.title);
- const score = group.video ? scoreSummary(asRecord(group.video.decision).score_items) : "";
- const nextSeen = new Set(seen).add(group.id);
- return (
- <article className={`walk-story-block level-${Math.min(level, 3)}`} id={`source-${encodeURIComponent(group.id)}`}>
- <header className="walk-story-head">
- <span className="walk-source-kicker">{sourceKicker(group, index, level)}</span>
- <div className="walk-story-title">
- <h2>{titleParts.title}</h2>
- {titleParts.tags.length ? (
- <div className="walk-source-tags">
- {titleParts.tags.map((tag) => <span key={tag}>{tag}</span>)}
- </div>
- ) : null}
- </div>
- <VideoLinks video={group.video} runId={runId} />
- </header>
- <div className="walk-source-facts">
- {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={querySourceName(group.title)} />}
- {score ? <Fact label="评分" value={score} /> : null}
- {group.terminalLabel ? <Fact label="处理" value={group.terminalLabel} /> : null}
- </div>
- <section className="walk-story-routes" aria-label={`${group.title} 的游走路线`}>
- <div className="walk-story-routes-label">{group.kind === "video" ? "从这条视频继续往外找" : "从这个搜索词翻下一页"}</div>
- {routeActions.length ? routeActions.map((action, actionIndex) => (
- <WalkRouteCard
- action={action}
- runId={runId}
- sourceId={group.id}
- tree={tree}
- level={level}
- onOpenScore={onOpenScore}
- seen={nextSeen}
- key={`${text(action.id, "walk")}-${actionIndex}`}
- />
- )) : <div className="walk-no-route">{group.kind === "video" ? "这条视频没有继续产生标签、作者或翻页路线。" : "这个搜索词没有继续翻页或产生新路线。"}</div>}
- </section>
- </article>
- );
- }
- function Fact({ label, value, tone = "plain" }: { label: string; value: string; tone?: string }) {
- return (
- <div className={`walk-fact ${tone}`}>
- <span>{label}</span>
- <strong>{value || "待确认"}</strong>
- </div>
- );
- }
- function WalkRouteCard({
- action,
- runId,
- sourceId,
- tree,
- level,
- onOpenScore,
- seen
- }: {
- action: RawRecord;
- runId: string;
- sourceId: string;
- tree: WalkTree;
- level: number;
- onOpenScore: (drawer: BusinessDrawerState) => void;
- seen: Set<string>;
- }) {
- const edgeId = text(action.edge_id, "");
- const targetQuery = asRecord(action.target_query);
- const targetVideos = Array.isArray(action.target_videos) ? action.target_videos.filter(isRecord) : [];
- const status = text(action.status, "");
- const isSuccess = status === "success";
- const routeName = routeLabel(edgeId);
- const gate = gateText(action, isSuccess);
- const childGroups = (tree.childrenByActionId.get(actionKey(action)) || []).filter((group) => !seen.has(group.id));
- const childByVideoId = new Map(childGroups.map((group) => [group.id, group]));
- return (
- <article className={`walk-tree-route ${edgeTone(edgeId)} ${isSuccess ? "success" : "stopped"}`} id={`route-${encodeURIComponent(text(action.id, routeName))}`}>
- <div className="walk-tree-route-dot" />
- <div className="walk-tree-route-body">
- <div className="walk-route-head">
- <span className="walk-edge-icon">{edgeIcon(edgeId)}</span>
- <div>
- <strong>{routeName}</strong>
- <p>{routeSummary(action, targetQuery, targetVideos, isSuccess)}</p>
- </div>
- <span className={`chip ${isSuccess ? "green" : "yellow"}`}>{isSuccess ? "已游走" : "未游走"}</span>
- </div>
- <div className="walk-route-bottom">
- <span className={`walk-gate ${isSuccess ? "pass" : "stop"}`}>{gate}</span>
- <button className="walk-jump-button" type="button" onClick={() => onOpenScore(scoreDrawer(action, routeName, isSuccess))}>
- <Info size={13} />
- 看筛选口径
- </button>
- {targetQuery.id ? (
- <Link className="walk-jump-button" href={`/runs/${encodeURIComponent(runId)}/queries/${encodeURIComponent(text(targetQuery.id, ""))}/videos`}>
- 查看带回视频
- </Link>
- ) : null}
- <a className="walk-jump-button" href={`#source-${encodeURIComponent(sourceId)}`}>回到起点</a>
- </div>
- {targetVideos.length ? (
- <div className="walk-result-videos">
- <div className="walk-result-title">
- <ListVideo size={15} />
- <span>带回视频 {targetVideos.length} 条</span>
- </div>
- <div className="walk-result-video-grid">
- {targetVideos.map((video, videoIndex) => (
- <WalkResultVideoCard
- video={video}
- runId={runId}
- tree={tree}
- level={level}
- seen={seen}
- child={childByVideoId.get(text(video.id, ""))}
- onOpenScore={onOpenScore}
- key={text(video.id, `${text(video.title, "video")}-${videoIndex}`)}
- />
- ))}
- </div>
- </div>
- ) : !isSuccess ? (
- <div className="walk-stop-note">{stoppedSentence(action)}</div>
- ) : null}
- </div>
- </article>
- );
- }
- function WalkResultVideoCard({
- video,
- runId,
- tree,
- level,
- seen,
- child,
- onOpenScore
- }: {
- video: RawRecord;
- runId: string;
- tree: WalkTree;
- level: number;
- seen: Set<string>;
- child?: WalkGroup;
- onOpenScore: (drawer: BusinessDrawerState) => void;
- }) {
- const decision = asRecord(video.decision);
- const label = text(decision.label, "未判断");
- const scores = videoScoreItems(video);
- const titleParts = splitTitleAndTags(text(video.title, "无标题视频"));
- return (
- <article className={`walk-result-video-card ${decisionTone(label)} ${child ? "has-child" : ""}`}>
- <div className="walk-result-video-top">
- <span className={`walk-result-status ${decisionTone(label)}`}>{label}</span>
- {child ? <span className="walk-result-continue">继续游走</span> : <span className="walk-result-stop">{stopLabel(label)}</span>}
- </div>
- <Link className="walk-result-video-title" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(text(video.id, ""))}`}>
- {titleParts.title}
- </Link>
- {titleParts.tags.length ? (
- <div className="walk-source-tags compact-tags">
- {titleParts.tags.slice(0, 4).map((tag) => <span key={tag}>{tag}</span>)}
- </div>
- ) : null}
- <div className="walk-result-video-meta">{text(video.author, "未知作者")}</div>
- {scores.length ? (
- <div className="walk-score-chips">
- {scores.slice(0, 3).map((item) => (
- <span key={item.label}>{shortScoreLabel(item.label)} {item.score}</span>
- ))}
- </div>
- ) : null}
- <VideoLinks video={video} runId={runId} compact />
- {child ? (
- <div className="walk-card-children">
- <WalkGroupCard
- group={child}
- index={0}
- runId={runId}
- tree={tree}
- level={level + 1}
- seen={seen}
- onOpenScore={onOpenScore}
- />
- </div>
- ) : null}
- </article>
- );
- }
- function buildWalkTree(actions: RawRecord[]): WalkTree {
- const groups = buildWalkGroups(actions);
- const byId = new Map(groups.map((group) => [group.id, group]));
- const childrenByActionId = new Map<string, WalkGroup[]>();
- const childIds = new Set<string>();
- groups.forEach((group) => {
- group.actions.filter((action) => !isTerminalEdge(text(action.edge_id, ""))).forEach((action) => {
- const targetVideos = Array.isArray(action.target_videos) ? action.target_videos.filter(isRecord) : [];
- const children = targetVideos
- .map((video) => byId.get(text(video.id, "")))
- .filter((child): child is WalkGroup => child !== undefined && child.id !== group.id);
- if (!children.length) return;
- const uniqueChildren = [...new Map(children.map((child) => [child.id, child])).values()];
- childrenByActionId.set(actionKey(action), uniqueChildren);
- uniqueChildren.forEach((child) => childIds.add(child.id));
- });
- });
- const roots = groups.filter((group) => !childIds.has(group.id));
- return {
- roots: roots.length ? roots : groups,
- childrenByActionId,
- totalGroups: groups.length
- };
- }
- function buildWalkGroups(actions: RawRecord[]): WalkGroup[] {
- const groups = new Map<string, WalkGroup>();
- actions.forEach((action, index) => {
- const video = asRecord(action.source_video);
- const terminal = isTerminalEdge(text(action.edge_id, ""));
- const id = text(video.id) || `search-${text(action.from_label, "root")}-${index}`;
- if (!groups.has(id)) {
- const decision = asRecord(video.decision);
- groups.set(id, {
- id,
- kind: video.id ? "video" : "query",
- video: video.id ? video : undefined,
- title: video.id ? text(video.title, "无标题视频") : queryGroupTitle(action),
- author: video.id ? text(video.author, "未知作者") : "搜索词翻页",
- decisionLabel: text(decision.label, "未判断"),
- terminalLabel: terminal ? terminalResultText(action) : "",
- actions: []
- });
- }
- const group = groups.get(id)!;
- if (terminal && !group.terminalLabel) group.terminalLabel = terminalResultText(action);
- group.actions.push(action);
- });
- return [...groups.values()];
- }
- 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, "")}`);
- }
- 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 querySourceName(value: string): string {
- return value.replace(/^搜索词《/, "").replace(/》$/, "") || "搜索词待确认";
- }
- function VideoLinks({ video, runId, compact = false }: { video?: RawRecord; runId: string; compact?: boolean }) {
- if (!video || !video.id) return null;
- const platformUrl = text(video.platform_url, "");
- return (
- <div className={compact ? "walk-video-links compact" : "walk-video-links"}>
- <Link href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(text(video.id, ""))}`}>
- 看详情
- </Link>
- {platformUrl ? (
- <a href={platformUrl} target="_blank" rel="noreferrer">
- <ExternalLink size={12} />
- 抖音
- </a>
- ) : null}
- </div>
- );
- }
- function actionDepth(action: RawRecord): number {
- const value = Number(action.depth || 0);
- return Number.isFinite(value) ? value : 0;
- }
- function splitTitleAndTags(value: string): { title: string; tags: string[] } {
- const matches = value.match(/#[^\s#]+/g) || [];
- const title = value.replace(/#[^\s#]+/g, " ").replace(/\s+/g, " ").trim();
- return {
- title: title || value,
- tags: matches.slice(0, 6)
- };
- }
- function scoreSummary(value: unknown): string {
- const items = Array.isArray(value) ? value.filter(isRecord) : [];
- const visible = items.filter((item) => text(item.score_label));
- if (!visible.length) return "";
- return visible.slice(0, 3).map((item) => `${shortScoreLabel(text(item.label, "评分"))} ${text(item.score_label)}`).join(" / ");
- }
- function actionScoreItems(action: RawRecord): { label: string; score: string }[] {
- const items = Array.isArray(action.score_items) ? action.score_items.filter(isRecord) : [];
- return items
- .map((item) => ({ label: text(item.label, "评分"), score: text(item.score_label, "") }))
- .filter((item) => item.score);
- }
- function videoScoreItems(video: RawRecord): { label: string; score: string }[] {
- const decision = asRecord(video.decision);
- const items = Array.isArray(decision.score_items) ? decision.score_items.filter(isRecord) : [];
- return items
- .map((item) => ({ label: text(item.label, "评分"), score: text(item.score_label, "") }))
- .filter((item) => item.score);
- }
- function gateText(action: RawRecord, isSuccess: boolean): string {
- const score = firstScore(scoreSummary(action.score_items));
- if (isSuccess) {
- const edgeId = text(action.edge_id, "");
- const label = edgeId === "hashtag_to_query" || edgeId === "tag_query"
- ? "标签评分通过"
- : edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author"
- ? "作者评分通过"
- : edgeId === "query_next_page"
- ? "翻页通过"
- : "筛选通过";
- return score ? `${label}:${score}` : label;
- }
- const reason = cleanBusinessText(text(action.reason_label, ""));
- if (reason.includes("游走次数上限")) return "原因:达到游走次数上限,未继续扩展";
- if (reason.includes("复看")) return "原因:待复看,先不继续扩展";
- if (reason.includes("不适合继续向外扩展")) return `原因:${reason.replace(/^不适合继续向外扩展:?/, "") || "不适合继续向外扩展"},未继续扩展`;
- return `原因:${reason || "没有达到继续扩展要求"},未继续扩展`;
- }
- function scoreDrawer(action: RawRecord, routeName: string, isSuccess: boolean): BusinessDrawerState {
- const items = actionScoreItems(action);
- const total = items.find((item) => item.label === "总分")?.score || "";
- const query = items.find((item) => item.label === "是否契合需求")?.score || "";
- const platform = items.find((item) => item.label === "平台表现")?.score || "";
- const reason = cleanBusinessText(text(action.reason_label, ""));
- return {
- title: `${routeName}筛选口径`,
- sections: [
- {
- label: "这一步为什么能继续",
- value: isSuccess
- ? `${routeName}已通过筛选${total ? `,总分 ${total}` : ""}。系统认为这条路线值得继续带回视频。`
- : `这一步没有继续游走。${reason ? `原因:${reason}` : "原因:没有达到继续扩展要求。"}`,
- tone: isSuccess ? "good" : "warn"
- },
- {
- label: "评分怎么看",
- items: [
- total ? `总分:综合判断这条路线值不值得继续。当前 ${total}。` : "总分:本次没有记录总分。",
- query ? `是否契合需求:看内容方向和本次需求是否贴合。当前 ${query}。` : "是否契合需求:本次没有记录该分。",
- platform ? `平台表现:看点赞、评论、转发等平台反馈。当前 ${platform}。` : "平台表现:本次没有记录该分。"
- ],
- tone: "info"
- },
- {
- label: "对流程的影响",
- value: isSuccess ? "通过后会生成新搜索词或翻页,并把带回的视频继续送去判断。" : "未通过时,这条路线停止,不再继续带回新视频。",
- tone: "neutral"
- }
- ]
- };
- }
- function firstScore(value: string): string {
- return value.split("/")[0]?.trim() || value;
- }
- function stopLabel(label: string): string {
- if (label === "入池") return "本卡未继续";
- if (label === "待复看") return "待复看停止";
- if (label === "淘汰") return "淘汰停止";
- return "未继续";
- }
- function terminalResultText(action: RawRecord): string {
- const edgeId = text(action.edge_id, "");
- if (edgeId === "decision_to_asset") return "入池";
- if (edgeId === "budget_downgrade") return "待复看";
- if (edgeId === "path_stop") return "淘汰并停止";
- return cleanBusinessText(text(action.result, "已记录"));
- }
- function isTerminalEdge(edgeId: string): boolean {
- return edgeId === "decision_to_asset" || edgeId === "budget_downgrade" || edgeId === "path_stop";
- }
- function routeLabel(edgeId: string): string {
- if (edgeId === "hashtag_to_query" || edgeId === "tag_query") return "从视频标签继续游走";
- if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") return "从作者继续游走";
- if (edgeId === "query_next_page") return "翻页继续游走";
- return "继续游走";
- }
- function routeSubject(action: RawRecord): string {
- const edgeId = text(action.edge_id, "");
- const raw = cleanBusinessText(text(action.trigger_label, ""));
- if (edgeId === "hashtag_to_query" || edgeId === "tag_query") {
- const value = raw.replace(/^候选标签:/, "").replace(/^标签搜索词:/, "#").replace(/^标签:/, "");
- return value || "标签待确认";
- }
- if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") {
- return raw.replace(/^作者:/, "") || "作者待确认";
- }
- if (edgeId === "query_next_page") {
- return cleanBusinessText(text(action.from_label, "当前搜索词")).replace(/^从搜索词/, "").replace(/继续$/, "") || "当前搜索词";
- }
- return raw || "当前内容";
- }
- function routeSummary(action: RawRecord, targetQuery: RawRecord, targetVideos: RawRecord[], isSuccess: boolean): string {
- if (!isSuccess) return stoppedSentence(action);
- const edgeId = text(action.edge_id, "");
- const subject = routeSubject(action);
- if (edgeId === "query_next_page") {
- return `从搜索词 ${subject} 翻到下一页,带回 ${targetVideos.length} 条视频。`;
- }
- if (edgeId === "hashtag_to_query" || edgeId === "tag_query") {
- const queryText = text(targetQuery.text, "");
- return queryText ? `标签 ${subject} 通过筛选,生成搜索词《${queryText}》,带回 ${targetVideos.length} 条视频。` : `标签 ${subject} 通过筛选,带回 ${targetVideos.length} 条视频。`;
- }
- if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") {
- return `作者 ${subject} 通过筛选,继续查看作品,带回 ${targetVideos.length} 条视频。`;
- }
- return cleanBusinessText(text(action.result, "已执行"));
- }
- function stoppedSentence(action: RawRecord): string {
- return gateText(action, false);
- }
- function shortScoreLabel(label: string): string {
- if (label === "是否契合需求") return "需求";
- if (label === "平台表现") return "平台";
- return label;
- }
- function cleanBusinessText(value: string): string {
- if (!value || value === "系统原因待补充" || value === "这一步没有记录额外原因") return "";
- return value.replaceAll("“", "《").replaceAll("”", "》");
- }
- function decisionTone(value: string): string {
- if (value === "入池") return "good";
- if (value === "待复看") return "warn";
- if (value === "淘汰") return "bad";
- return "plain";
- }
- function edgeTone(edgeId: string): string {
- if (edgeId === "hashtag_to_query" || edgeId === "tag_query") return "tag";
- if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") return "author";
- if (edgeId === "query_next_page") return "page";
- return "stop";
- }
- function edgeIcon(edgeId: string) {
- if (edgeId === "hashtag_to_query" || edgeId === "tag_query") return <Hash size={15} />;
- if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") return <UserSearch size={15} />;
- return <Search size={15} />;
- }
- function isRecord(value: unknown): value is RawRecord {
- return typeof value === "object" && value !== null;
- }
|