|
|
@@ -1,235 +0,0 @@
|
|
|
-import React, { useEffect, useState } from 'react'
|
|
|
-
|
|
|
-// 创作Query正交 Demo:仿"制作query"的多列 UI——左侧若干「轴列」,右侧「Query 词」。
|
|
|
-// 关键:左侧轴列只显示【该家族 query 实际用到的去重值】,与右侧 query 一一对应(不是整池)。
|
|
|
-// 阶段列只显示 灵感/选题/脚本 三个裸阶段;组合方式按尾缀分组展示。
|
|
|
-const PARTS_KEY = { // 家族 axes 里的轴名 → 每条 query 的 parts 里的键
|
|
|
- '实质': '实质', '形式': '形式', '作用/感受/意图': '目的',
|
|
|
- '业务阶段': '业务阶段', '模态': '模态', '知识类型': '知识类型',
|
|
|
- '阶段': '阶段', '动作': '动作', '作用': '作用', // 第六家族(老正交)的轴
|
|
|
-}
|
|
|
-const AXIS_LABEL = { // 列头显示用的标注(取值层级)
|
|
|
- '实质': '实质(3-4层)', '形式': '形式(3-4层)',
|
|
|
-}
|
|
|
-const USED_ONLY = new Set(['实质', '形式']) // 仅这两轴只显示这批 query 用到的去重值;其余轴显示完整池
|
|
|
-const POOL_KEY = { // 完整池轴名 → axis_values 键
|
|
|
- '作用/感受/意图': '目的池', '模态': '模态', '知识类型': '知识类型', '业务阶段': '业务阶段',
|
|
|
- '阶段': '阶段', '动作': '动作', '作用': '作用', // 第六家族(老正交)的轴,显示完整池
|
|
|
-}
|
|
|
-
|
|
|
-// 该家族 query 在某轴上实际用过的去重值(按首次出现顺序)
|
|
|
-function usedValues(items, pk) {
|
|
|
- const set = new Set(), out = []
|
|
|
- for (const it of items) {
|
|
|
- const v = it.parts && it.parts[pk]
|
|
|
- if (v && !set.has(v)) { set.add(v); out.push(v) }
|
|
|
- }
|
|
|
- return out
|
|
|
-}
|
|
|
-
|
|
|
-function AxisCol({ ax, fam, axisValues }) {
|
|
|
- let body, count
|
|
|
- if (USED_ONLY.has(ax)) {
|
|
|
- // 实质/形式:只显示这批 query 用到的去重值
|
|
|
- const used = usedValues(fam.items, PARTS_KEY[ax])
|
|
|
- count = used.length
|
|
|
- body = used.map((v, i) => <div className="axv" key={i}>{v}</div>)
|
|
|
- } else {
|
|
|
- // 目的池/业务阶段/模态/知识类型/(老轴):完整池平铺
|
|
|
- const pool = axisValues[POOL_KEY[ax]] || []
|
|
|
- count = pool.length
|
|
|
- body = pool.map((v, i) => <div className="axv" key={i}>{v}</div>)
|
|
|
- }
|
|
|
- return (
|
|
|
- <div className="axcol">
|
|
|
- <div className="axhd">{AXIS_LABEL[ax] || ax}<span className="gn">{count}</span></div>
|
|
|
- <div className="axlist">{body}</div>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-// 一栏家族(竖排按钮):按尾缀分组。按钮内前缀左对齐、尾缀(后 sufLen 个轴)右对齐。
|
|
|
-function FamCol({ title, fams, sufLen, fi, setFi }) {
|
|
|
- return (
|
|
|
- <div className="famcol">
|
|
|
- <div className="famcol-hd">{title}</div>
|
|
|
- {fams.map(f => {
|
|
|
- const pre = f.axes.slice(0, f.axes.length - sufLen).join(' × ')
|
|
|
- const suf = f.axes.slice(f.axes.length - sufLen).join(' × ')
|
|
|
- return (
|
|
|
- <button key={f.key} className={`fbtn ${f.i === fi ? 'on' : ''}`} onClick={() => setFi(f.i)}>
|
|
|
- <span className="fbtn-pre">{pre}</span>
|
|
|
- <span className="fbtn-suf">{suf}</span>
|
|
|
- </button>
|
|
|
- )
|
|
|
- })}
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function rowRank(f) {
|
|
|
- const hasShi = f.axes.includes('实质')
|
|
|
- const hasXing = f.axes.includes('形式')
|
|
|
- const hasPurpose = f.axes.includes('作用/感受/意图')
|
|
|
- if (hasShi && hasXing) return 3
|
|
|
- if (hasShi) return 1
|
|
|
- if (hasXing) return 2
|
|
|
- if (hasPurpose) return 4
|
|
|
- return 5
|
|
|
-}
|
|
|
-
|
|
|
-function byRowRank(a, b) {
|
|
|
- return rowRank(a) - rowRank(b)
|
|
|
-}
|
|
|
-
|
|
|
-function averageCreationStats(items, summary) {
|
|
|
- const rows = items
|
|
|
- .map(it => summary[it.query])
|
|
|
- .filter(Boolean)
|
|
|
- if (!rows.length) return null
|
|
|
- const creation = rows.reduce((s, info) => s + (info.imgtext_creation_count || 0), 0)
|
|
|
- const target = rows[0]?.imgtext_target_count || 10
|
|
|
- const complete = rows.filter(info => (info.imgtext_classified_count || 0) >= (info.imgtext_target_count || 10)).length
|
|
|
- const avg = creation / rows.length
|
|
|
- return {
|
|
|
- label: Number.isInteger(avg) ? String(avg) : avg.toFixed(1),
|
|
|
- target,
|
|
|
- count: rows.length,
|
|
|
- complete,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export default function CreationDemo() {
|
|
|
- const [data, setData] = useState(null)
|
|
|
- const [summary, setSummary] = useState({})
|
|
|
- const [fi, setFi] = useState(0)
|
|
|
- const [showFilter, setShowFilter] = useState(false)
|
|
|
- useEffect(() => {
|
|
|
- fetch('/data/queries/creation_demo.json?v=' + Date.now())
|
|
|
- .then(r => r.ok ? r.json() : null).then(setData).catch(() => setData(null))
|
|
|
- fetch('/api/creation-search/summary?v=' + Date.now())
|
|
|
- .then(r => r.ok ? r.json() : null)
|
|
|
- .then(d => setSummary((d && d.queries) || {}))
|
|
|
- .catch(() => setSummary({}))
|
|
|
- }, [])
|
|
|
-
|
|
|
- if (!data) {
|
|
|
- return <div className="empty">暂无 demo 数据。先跑 <code>scripts/build_creation_demo.py</code></div>
|
|
|
- }
|
|
|
- const fam = data.families[fi]
|
|
|
- const kept = fam.items.filter(it => it.keep).length
|
|
|
- const avgCreation = averageCreationStats(fam.items, summary)
|
|
|
- const allFams = data.families.map((f, i) => ({ ...f, i }))
|
|
|
- const modalFams = allFams
|
|
|
- .filter(f => f.axes.includes('业务阶段'))
|
|
|
- .sort(byRowRank)
|
|
|
- const oldWithEffectFams = allFams
|
|
|
- .filter(f => f.axes.includes('动作') && f.axes.includes('作用'))
|
|
|
- .sort(byRowRank)
|
|
|
- const oldNoEffectFams = allFams
|
|
|
- .filter(f => f.axes.includes('动作') && !f.axes.includes('作用'))
|
|
|
- .sort(byRowRank)
|
|
|
-
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <h1>创作知识 · Query 正交 Demo</h1>
|
|
|
- <div className="sub">{data.families.length} 种组合方式机械正交(取分类树创作支)→ LLM 按提示词评 valid/relevant(query_filter)→ 原串直接可搜(本 demo 不真搜)· 每种组合方式最多 30 条全显示,✕ 删除线=被筛掉
|
|
|
- <button className="btn ghost" style={{ marginLeft: 10 }} onClick={() => setShowFilter(true)}>📋 查看筛选提示词</button>
|
|
|
- </div>
|
|
|
-
|
|
|
- {showFilter && <FilterPromptModal onClose={() => setShowFilter(false)} />}
|
|
|
-
|
|
|
- <div className="famcols">
|
|
|
- <FamCol title="尾缀:模态 × 业务阶段 × 知识类型" sufLen={3}
|
|
|
- fams={modalFams} fi={fi} setFi={setFi} />
|
|
|
- <FamCol title="尾缀:阶段 × 动作 × 作用 × 知识类型" sufLen={4}
|
|
|
- fams={oldWithEffectFams} fi={fi} setFi={setFi} />
|
|
|
- <FamCol title="尾缀:阶段 × 动作 × 知识类型" sufLen={3}
|
|
|
- fams={oldNoEffectFams} fi={fi} setFi={setFi} />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div className="cdemo">
|
|
|
- {fam.axes.map(ax => (
|
|
|
- <AxisCol key={ax} ax={ax} fam={fam} axisValues={data.axis_values} />
|
|
|
- ))}
|
|
|
- <div className="axcol qcol">
|
|
|
- <div className="axhd qhd">
|
|
|
- <span>Query 词</span>
|
|
|
- <span className="qhd-metrics">
|
|
|
- {avgCreation && (
|
|
|
- <span
|
|
|
- className={`avgcreation ${avgCreation.complete >= avgCreation.count ? 'done' : 'pending'}`}
|
|
|
- title={`按当前组 ${avgCreation.count} 个已有结果 query 计算,完整判断 ${avgCreation.complete}/${avgCreation.count}`}
|
|
|
- >
|
|
|
- 平均 {avgCreation.label}/{avgCreation.target}创作知识
|
|
|
- </span>
|
|
|
- )}
|
|
|
- <span className="gn">留 {kept}/{fam.items.length}</span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="axlist">
|
|
|
- {fam.items.map((it, i) => (
|
|
|
- <div className={`qrow ${it.keep ? 'keep' : 'drop'}`} key={i} title={it.reason || ''}>
|
|
|
- <span className="qmark">{it.keep ? '✓' : '✕'}</span>
|
|
|
- {typeof it.valid === 'number' && (
|
|
|
- <span className={`qvalid ${it.valid >= 6 ? 'ok' : 'low'}`}>语义{it.valid}</span>
|
|
|
- )}
|
|
|
- <span className="qtext">{it.query}</span>
|
|
|
- <QueryResultButton query={it.query} info={summary[it.query]} />
|
|
|
- {!it.keep && <span className="qreason">{it.reason}</span>}
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function QueryResultButton({ query, info }) {
|
|
|
- const platforms = info?.platforms || {}
|
|
|
- const counts = ['xiaohongshu', 'weixin', 'douyin']
|
|
|
- .map(p => platforms[p]?.display_count || 0)
|
|
|
- const label = info
|
|
|
- ? `${counts.join('/')} 条`
|
|
|
- : '未跑'
|
|
|
- const cls = info ? 'qdetail on' : 'qdetail'
|
|
|
- const judged = info?.imgtext_classified_count || 0
|
|
|
- const target = info?.imgtext_target_count || 10
|
|
|
- const creation = info?.imgtext_creation_count || 0
|
|
|
- return (
|
|
|
- <>
|
|
|
- {info && (
|
|
|
- <span
|
|
|
- className={`qcreation ${judged >= target ? 'done' : 'pending'}`}
|
|
|
- title={`小红书 + 微信公众号已判断 ${judged}/${target} 条`}
|
|
|
- >
|
|
|
- {creation}/{target}创作知识
|
|
|
- </span>
|
|
|
- )}
|
|
|
- <a className={cls} href={`#/query/${encodeURIComponent(query)}`}>
|
|
|
- 查看结果<span>{label}</span>
|
|
|
- </a>
|
|
|
- </>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-// 弹窗:展示机械正交后做 keep/排除 的创作筛选提示词(acquisition/query_filter.txt)
|
|
|
-function FilterPromptModal({ onClose }) {
|
|
|
- const [p, setP] = useState(null)
|
|
|
- useEffect(() => {
|
|
|
- fetch('/api/filter-prompt').then(r => r.ok ? r.json() : null).then(setP).catch(() => setP(null))
|
|
|
- }, [])
|
|
|
- return (
|
|
|
- <div className="modal-mask" onClick={onClose}>
|
|
|
- <div className="modal wide" onClick={(e) => e.stopPropagation()}>
|
|
|
- <div className="modal-hd"><b>创作 query 筛选提示词</b><span className="x" onClick={onClose}>✕</span></div>
|
|
|
- <div className="modal-bd">
|
|
|
- <p className="note">机械正交拼出的每条 query 逐条喂大模型,返回语义合法性 valid 与创作相关性 relevant;代码按 valid ≥ 6 且 relevant=true 保留。
|
|
|
- 判断口径全靠这条提示词。{p?.file && <code> · {p.file}</code>}</p>
|
|
|
- <pre className="prompt">{p ? p.text : '加载中…'}</pre>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|