import { useEffect, useState } from 'react' import { getAllConfigCodes } from './recall' /** * 召回维度配置 (configCode -> 中文标签) * * 数据源: video-vector 后端 GET /videoSearch/getAllConfigCodes * 启动时拉一次, 进程内缓存, 全部消费方共享. * * 在请求未返回前先吐出 SAFE_FALLBACK 避免 UI 闪烁; 拉到后切换到真实数据. * 命名约定: * VIDEO_* 视频解构维度 (选题/灵感点/关键点/目的点) * RESULT_LOG_* AI 识别维度 (选题/主题/关键词/口播) */ const SAFE_FALLBACK: Record = { VIDEO_TOPIC: '选题', VIDEO_INSPIRATION: '灵感点', } let cache: Record | null = null let pending: Promise> | null = null function fetchOnce(): Promise> { if (cache) return Promise.resolve(cache) if (!pending) { pending = getAllConfigCodes() .then((d) => { cache = d return d }) .catch((e) => { pending = null throw e }) } return pending } export function useConfigCodes(): Record { const [data, setData] = useState>(cache ?? SAFE_FALLBACK) useEffect(() => { if (cache) { setData(cache) return } fetchOnce().then(setData).catch(() => { // 失败保持 fallback 不阻塞 UI }) }, []) return data } /** * 真实字典是否已经从后端拉到 (而非 SAFE_FALLBACK). * 用于"URL 自动召回"等场景需要等真实全量字典再触发, 避免只跑 fallback 的 2 个维度. */ export function useConfigCodesReady(): boolean { const [ready, setReady] = useState(cache != null) useEffect(() => { if (cache != null) { setReady(true) return } fetchOnce() .then(() => setReady(true)) .catch(() => { // 失败保持 false, 由调用方决定是否兜底 }) }, []) return ready } /** "全部" 维度的特殊 value, 提交时拆开成所有 configCode 并发调用 */ export const ALL_CONFIG_CODE = '__ALL__' /** * "内容理解-旧" 组(原 AI识别维度)中文标签覆写 * 后端字典里这组的标签是 "选题"/"主题",前端要展示成 "内容选题"/"视频主题" * 其他标签(关键词/口播等)直通 */ const RESULT_LOG_LABEL_OVERRIDE: Record = { 选题: '内容选题', 主题: '视频主题', } /** * 给定 configCode + 字典, 返回前端实际展示的中文标签 * dropdown / 表格 / Tag 全部走这里, 保持一致 */ export function getConfigDisplayLabel( code: string, codes: Record, ): string { const raw = codes[code] ?? code if (code.startsWith('RESULT_LOG_')) return RESULT_LOG_LABEL_OVERRIDE[raw] ?? raw return raw } /** Tab2 文本召回 dropdown: 按前缀分组, 顶部加"全部"快捷项 */ export function buildGroupedConfigOptions(codes: Record) { const video: { label: string; value: string }[] = [] const result: { label: string; value: string }[] = [] const other: { label: string; value: string }[] = [] for (const [code, label] of Object.entries(codes)) { if (code.startsWith('VIDEO_')) { video.push({ label, value: code }) } else if (code.startsWith('RESULT_LOG_')) { result.push({ label: RESULT_LOG_LABEL_OVERRIDE[label] ?? label, value: code }) } else { other.push({ label, value: code }) } } type Item = { label: string; value: string } type Group = { label: string; options: Item[] } const items: (Item | Group)[] = [] // 顶部独立"全部"项 — 无 group items.push({ label: '全部', value: ALL_CONFIG_CODE }) if (video.length) items.push({ label: '视频解构维度', options: video }) if (result.length) items.push({ label: '内容理解-旧', options: result }) if (other.length) items.push({ label: '其他', options: other }) return items } /** * 给定字典,返回所有"可召回"的 configCode 列表(排除 ALL) * 用于"全部"模式下并发调用 */ export function listAllConfigCodes(codes: Record): string[] { return Object.keys(codes) } /** * DeconstructTree 节点中文类型 → configCode 映射 * 用于判断"以此召回"按钮该传哪个 configCode, 以及该按钮是否生效 * (生效条件: 映射到的 configCode 在后端字典里存在) */ export const TREE_NODE_TYPE_TO_CONFIG_CODE: Record = { 选题: 'VIDEO_TOPIC', 灵感点: 'VIDEO_INSPIRATION', 关键点: 'VIDEO_KEYPOINT', 目的点: 'VIDEO_PURPOSE', }