| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- "use client";
- import Link from "next/link";
- import { PanelRightOpen } from "lucide-react";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import { AppShell } from "@/components/AppShell";
- import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
- import { TechnicalRetryDetails } from "@/components/TechnicalRetryDetails";
- import { getFlowLedgerVideos } from "@/lib/api/client";
- import type { FlowLedgerVideosResponse } from "@/lib/api/types";
- import { videoFromApi } from "@/lib/flow-ledger/build";
- import { actionLabel, mediaStatusLabel, metricLabel, methodLabel, platformLabel, scoreItemsText } from "@/lib/flow-ledger/business";
- import { asRecord, text } from "@/lib/flow-ledger/format";
- import type { VideoRef } from "@/lib/flow-ledger/types";
- export function QueryVideosPage({ runId, queryId }: { runId: string; queryId: string }) {
- const [data, setData] = useState<FlowLedgerVideosResponse | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const load = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const debug = new URLSearchParams(window.location.search).get("debug") === "1";
- setData(await getFlowLedgerVideos(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 videos = useMemo(() => (data?.videos || []).map(videoFromApi), [data]);
- return (
- <AppShell title="首轮搜到的视频" subtitle={query.text ? `搜索词:${text(query.text)}` : "查看这个搜索词搜到了哪些内容"} runId={runId} showBack onRefresh={load}>
- {loading ? <LoadingState /> : null}
- {error ? <ErrorState message={error} /> : null}
- {!loading && !error && !videos.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">首轮视频 {Number(data.summary.total || videos.length)}</span>
- </summary>
- <div className="decl-body">
- <Summary label="判断汇总" value={`入池 ${Number(data.summary.pool_count || 0)} · 待复看 ${Number(data.summary.review_count || 0)} · 技术问题 ${Number(data.summary.technical_retry_count || 0)} · 淘汰 ${Number(data.summary.reject_count || 0)}`} />
- <Summary label="视频保存" value={`${mediaStatusLabel("oss_uploaded")} ${Number(data.summary.oss_uploaded_count || 0)} · ${mediaStatusLabel("oss_upload_pending")} ${Number(data.summary.oss_pending_count || 0)} · ${mediaStatusLabel("oss_upload_failed")} ${Number(data.summary.oss_failed_count || 0)}`} />
- <Summary label="数据来源" value={data.data_origin_label} />
- </div>
- </details>
- <section className="detail-table-wrap">
- <table className="detail-table">
- <thead>
- <tr>
- <th>#</th>
- <th>视频</th>
- <th>作者 / 平台</th>
- <th>互动</th>
- <th>判断</th>
- <th>评分详情</th>
- <th>视频保存</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- {videos.map((video, index) => (
- <VideoRow
- key={`${video.id}-${index}`}
- index={index}
- runId={runId}
- video={video}
- />
- ))}
- </tbody>
- </table>
- </section>
- </>
- ) : 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 VideoRow({ index, runId, video }: { index: number; runId: string; video: VideoRef }) {
- return (
- <tr>
- <td>{index + 1}</td>
- <td>
- <Link className="title-link" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.contentDiscoveryId || video.id)}`}>
- {video.title}
- </Link>
- </td>
- <td>
- <strong>{video.author}</strong>
- <div className="muted">{platformLabel(video.platform)}</div>
- </td>
- <td>
- <span className="chip">{metricLabel(video)}</span>
- </td>
- <td>
- <span className="chip blue">{actionLabel(video.decisionAction)}</span>
- {video.decisionAction === "TECHNICAL_RETRY_REQUIRED" ? (
- <div className="muted">{technicalRetryShortText(video)}</div>
- ) : null}
- {video.decisionAction === "TECHNICAL_RETRY_REQUIRED" ? <TechnicalRetryDetails detail={video.technicalRetryDetail} compact /> : null}
- </td>
- <td>
- <div className="score-mini">{scoreItemsText(video.scoreItems)}</div>
- </td>
- <td>
- <span className={`chip ${video.mediaStatus === "oss_uploaded" ? "green" : "yellow"}`}>{video.mediaStatusLabel}</span>
- {video.mediaFailureReason ? <div className="muted">{video.mediaFailureReason}</div> : null}
- </td>
- <td>
- <Link className="mini-button" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.contentDiscoveryId || video.id)}`}>
- <PanelRightOpen size={13} />
- 看详情
- </Link>
- </td>
- </tr>
- );
- }
- function technicalRetryShortText(video: VideoRef): string {
- const detail = video.technicalRetryDetail;
- if (!detail) return "系统需要重试后再判断";
- const stage = detail.stageLabel || "技术问题";
- const reason = detail.briefReason || detail.failureLabel || detail.failureType || "系统需要重试后再判断";
- return `${stage}:${reason}`;
- }
|