|
|
@@ -69,17 +69,88 @@ function AxisColumn({ data, family, axis }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+function singletonMaps(singleton) {
|
|
|
+ const byQueryText = new Map()
|
|
|
+ const decodedByQueryId = new Map()
|
|
|
+ for (const item of singleton?.decoded_items || []) {
|
|
|
+ const key = item.query_id || ''
|
|
|
+ decodedByQueryId.set(key, [...(decodedByQueryId.get(key) || []), item])
|
|
|
+ }
|
|
|
+ for (const row of singleton?.queries || []) {
|
|
|
+ byQueryText.set(row.query_text, {
|
|
|
+ ...row,
|
|
|
+ decoded_items: decodedByQueryId.get(row.query_id) || [],
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return byQueryText
|
|
|
+}
|
|
|
+
|
|
|
+function SingletonLinks({ singleton }) {
|
|
|
+ if (!singleton?.batch || !singleton?.run) return null
|
|
|
+ const firstDecoded = singleton.decoded_items?.[0]
|
|
|
+ const firstQuery = singleton.queries?.find((row) => row.query_id === firstDecoded?.query_id) || singleton.queries?.[0]
|
|
|
+ const searchHref = firstQuery ? `#/query/${encodeURIComponent(singleton.run.id)}/${encodeURIComponent(firstQuery.query_id)}` : ''
|
|
|
+ const decodeHref = firstDecoded ? `#/decode-item/${encodeURIComponent(firstDecoded.item_id)}` : ''
|
|
|
+ const payloadText = firstDecoded?.payload_count ? ` ${firstDecoded.payload_count} payload` : ''
|
|
|
+ return (
|
|
|
+ <div className="singleton-panel">
|
|
|
+ <div>
|
|
|
+ <b>最近真实单例</b>
|
|
|
+ <span>{singleton.batch.name} · {singleton.run.status}</span>
|
|
|
+ </div>
|
|
|
+ <div className="singleton-actions">
|
|
|
+ {searchHref && <a className="btn ghost" href={searchHref}>查看搜索结果</a>}
|
|
|
+ {decodeHref && <a className="btn" href={decodeHref}>查看拆解结果{payloadText}</a>}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function PreviewQueryRow({ item, index, latest, singleton }) {
|
|
|
+ const decoded = latest?.decoded_items?.[0]
|
|
|
+ const searchHref = latest && singleton?.run
|
|
|
+ ? `#/query/${encodeURIComponent(singleton.run.id)}/${encodeURIComponent(latest.query_id)}`
|
|
|
+ : ''
|
|
|
+ const decodeHref = decoded ? `#/decode-item/${encodeURIComponent(decoded.item_id)}` : ''
|
|
|
+ return (
|
|
|
+ <div className={`qrow ${item.keep === false ? 'drop' : 'keep'}`} key={`${item.query}-${index}`} title={item.reason || ''}>
|
|
|
+ <span className="qmark">{item.keep === false ? '✕' : '✓'}</span>
|
|
|
+ {typeof item.valid === 'number' && (
|
|
|
+ <span className={`qvalid ${item.valid >= 6 ? 'ok' : 'low'}`}>语义{item.valid}</span>
|
|
|
+ )}
|
|
|
+ <span className="qtext">{item.query}</span>
|
|
|
+ {latest ? (
|
|
|
+ <>
|
|
|
+ <span className="qcreation done">{latest.creation_hit_count}/{latest.candidate_count} 命中</span>
|
|
|
+ <a className="qdetail on" href={searchHref}>查看搜索结果</a>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <span className="qdetail">未搜索</span>
|
|
|
+ )}
|
|
|
+ {decodeHref && <a className="qdetail decode-on" href={decodeHref}>查看拆解结果<span>{decoded.payload_count} payload</span></a>}
|
|
|
+ {item.reason && <span className="qreason">{item.reason}</span>}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
function QueryGenerationPreview() {
|
|
|
const [data, setData] = useState(null)
|
|
|
+ const [singleton, setSingleton] = useState(null)
|
|
|
const [activeKey, setActiveKey] = useState('f1')
|
|
|
const [err, setErr] = useState('')
|
|
|
|
|
|
useEffect(() => {
|
|
|
setErr('')
|
|
|
- fetch('/api/query-generation/preview?per=30&dry=true')
|
|
|
- .then((r) => (r.ok ? r.json() : Promise.reject(new Error('query 生成预览读取失败'))))
|
|
|
- .then((payload) => {
|
|
|
+ Promise.all([
|
|
|
+ fetch('/api/query-generation/preview?per=30&dry=true')
|
|
|
+ .then((r) => (r.ok ? r.json() : Promise.reject(new Error('query 生成预览读取失败')))),
|
|
|
+ fetch('/api/query-generation/latest-singleton')
|
|
|
+ .then((r) => (r.ok ? r.json() : null))
|
|
|
+ .catch(() => null),
|
|
|
+ ])
|
|
|
+ .then(([payload, latest]) => {
|
|
|
setData(payload)
|
|
|
+ setSingleton(latest)
|
|
|
const firstKey = payload.families?.[0]?.key
|
|
|
if (firstKey) setActiveKey(firstKey)
|
|
|
})
|
|
|
@@ -92,6 +163,7 @@ function QueryGenerationPreview() {
|
|
|
const families = data.families || []
|
|
|
const family = families.find((row) => row.key === activeKey) || families[0]
|
|
|
const kept = (family?.items || []).filter((item) => item.keep !== false).length
|
|
|
+ const latestByQuery = singletonMaps(singleton)
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
@@ -99,6 +171,7 @@ function QueryGenerationPreview() {
|
|
|
<div className="sub">
|
|
|
只显示当前激活的 2 种正交方式:f1 / f2。生成逻辑来自正式 query builder;此页只预览,不写 DB、不发起搜索。
|
|
|
</div>
|
|
|
+ <SingletonLinks singleton={singleton} />
|
|
|
|
|
|
<div className="famcols two">
|
|
|
{families.map((row) => (
|
|
|
@@ -126,15 +199,13 @@ function QueryGenerationPreview() {
|
|
|
</div>
|
|
|
<div className="axlist">
|
|
|
{(family.items || []).map((item, index) => (
|
|
|
- <div className={`qrow ${item.keep === false ? 'drop' : 'keep'}`} key={`${item.query}-${index}`} title={item.reason || ''}>
|
|
|
- <span className="qmark">{item.keep === false ? '✕' : '✓'}</span>
|
|
|
- {typeof item.valid === 'number' && (
|
|
|
- <span className={`qvalid ${item.valid >= 6 ? 'ok' : 'low'}`}>语义{item.valid}</span>
|
|
|
- )}
|
|
|
- <span className="qtext">{item.query}</span>
|
|
|
- <span className="qdetail">未搜索</span>
|
|
|
- {item.reason && <span className="qreason">{item.reason}</span>}
|
|
|
- </div>
|
|
|
+ <PreviewQueryRow
|
|
|
+ item={item}
|
|
|
+ index={index}
|
|
|
+ key={`${item.query}-${index}`}
|
|
|
+ latest={latestByQuery.get(item.query)}
|
|
|
+ singleton={singleton}
|
|
|
+ />
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|