mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
运行对比页加「分析卡」一区(服务端 compare 的 analyses):两次运行的分析卡按节点和分析项对齐,一项一行——分析卡的名字和项的标识、算子的叫法(契约)、两次的判定语照原样(没有的一边写「—」,算不了的照判定语写原因)、变了没有(判定一致 / 判定变了 / 判定一致而判定语里的数变了 / 这次新增 / 这次没有,按服务端比出来的代码挑说法和颜色);只列判定,不拆变化来源;两次都没有分析卡(老服务端没有这一项)时不画。check-compare 加一节:先后照服务端、几种变化、新增与没有、老服务端不画、窄屏不溢出;夹具的形状照服务端真算的(compare.compare 跑虚构周报加分析卡)
This commit is contained in:
@@ -6,6 +6,7 @@ import { RUN_COMPARE_TEXT } from '../../lib/terms'
|
|||||||
import type { RunComparison } from '../../types'
|
import type { RunComparison } from '../../types'
|
||||||
import { useCandidates, useComparison, useDefaultBase, useRunRecord } from './compare/data'
|
import { useCandidates, useComparison, useDefaultBase, useRunRecord } from './compare/data'
|
||||||
import { comparePath, type CompareVia } from './compare/entry'
|
import { comparePath, type CompareVia } from './compare/entry'
|
||||||
|
import { Analyses } from './compare/Analyses'
|
||||||
import { Metrics } from './compare/Metrics'
|
import { Metrics } from './compare/Metrics'
|
||||||
import { compareErrorKind, isGone } from './compare/model'
|
import { compareErrorKind, isGone } from './compare/model'
|
||||||
import { CompareError, type CompareErrorContext } from './compare/parts'
|
import { CompareError, type CompareErrorContext } from './compare/parts'
|
||||||
@@ -124,6 +125,7 @@ function Comparison({ data }: { data: RunComparison }) {
|
|||||||
<Summary summary={data.summary} filter={filter} onFilter={setFilter} />
|
<Summary summary={data.summary} filter={filter} onFilter={setFilter} />
|
||||||
<NotesSection notes={data.notes ?? []} />
|
<NotesSection notes={data.notes ?? []} />
|
||||||
<Metrics metrics={data.metrics ?? []} filter={filter} onClearFilter={() => setFilter(null)} />
|
<Metrics metrics={data.metrics ?? []} filter={filter} onClearFilter={() => setFilter(null)} />
|
||||||
|
<Analyses analyses={data.analyses} />
|
||||||
<Queries queries={data.queries ?? []} />
|
<Queries queries={data.queries ?? []} />
|
||||||
<TemplateSection template={data.template} />
|
<TemplateSection template={data.template} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
import { ANALYSIS_TEXT, analysisMethodLabel } from '../../../lib/termsAnalysis'
|
||||||
|
import type { RunCompareAnalysis, RunCompareAnalysisSide } from '../../../types'
|
||||||
|
import { Block } from './parts'
|
||||||
|
|
||||||
|
const C = ANALYSIS_TEXT.compare
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析卡区(engine/compare._analyses):两次运行的分析卡按节点和分析项对齐,一项一行——算子的叫法、两次的判定语、变了没有。
|
||||||
|
* 只列出两次的判定,不拆变化来源(方案 W1-c 的最小可用)。判定语、判定都是服务端给的原样;变化(same / verdict / phrase /
|
||||||
|
* added / removed)是服务端比出来的,界面只按它挑颜色。两次运行都没有分析卡(老服务端没有这一项)时不画这一区
|
||||||
|
*/
|
||||||
|
export function Analyses({ analyses }: { analyses?: RunCompareAnalysis[] }) {
|
||||||
|
const id = useId()
|
||||||
|
if (!analyses?.length) return null
|
||||||
|
return (
|
||||||
|
<Block id={`${id}-analyses`} title={C.title} count={String(analyses.length)} hint={C.hint} attr={{ 'data-compare-analyses': '' }}>
|
||||||
|
<ul className="divide-y">
|
||||||
|
{analyses.map((a) => <AnalysisItem key={`${a.node_id}:${a.item}`} a={a} />)}
|
||||||
|
</ul>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Side({ label, side }: { label: string; side: RunCompareAnalysisSide | null }) {
|
||||||
|
return (
|
||||||
|
<div className="min-w-0" data-compare-analysis-side={label}>
|
||||||
|
<div className="text-2xs text-faint">{label}</div>
|
||||||
|
<div className="text-xs [overflow-wrap:anywhere]" style={side?.status === 'refused' ? { color: 'var(--st-waiting)' } : undefined}>
|
||||||
|
{side ? (side.phrase || C.none) : C.none}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnalysisItem({ a }: { a: RunCompareAnalysis }) {
|
||||||
|
const changed = a.change !== 'same'
|
||||||
|
const color = a.change === 'verdict' ? 'var(--warn)' : a.change === 'added' ? 'var(--accent)' : 'var(--text-dim)'
|
||||||
|
return (
|
||||||
|
<li className="py-2.5 first:pt-0 last:pb-0" data-compare-analysis={`${a.node_id}:${a.item}`} data-change={a.change}>
|
||||||
|
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
|
<span className="min-w-0 text-xs font-medium [overflow-wrap:anywhere]" title={a.node_id}>{C.item(a.title || a.node_id, a.item)}</span>
|
||||||
|
<span className="text-2xs text-faint">{analysisMethodLabel(a.op, a.mode)}</span>
|
||||||
|
<span className="chip shrink-0" data-compare-analysis-change="" style={{
|
||||||
|
color, borderColor: `color-mix(in srgb, ${color} 40%, transparent)`,
|
||||||
|
background: changed && a.change === 'verdict' ? `color-mix(in srgb, ${color} 12%, transparent)` : undefined,
|
||||||
|
}}>
|
||||||
|
{C.change[a.change] ?? a.change}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 grid grid-cols-2 gap-3 max-sm:grid-cols-1">
|
||||||
|
<Side label={C.base} side={a.base} />
|
||||||
|
<Side label={C.target} side={a.target} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -180,6 +180,28 @@ const NOTE = '基准运行中「退货明细」的查询结果已截断(只保
|
|||||||
/** 以更早的一次(B2)为基准时多一条说明:换基准、过时答复的检查靠它认出页面上是哪一份 */
|
/** 以更早的一次(B2)为基准时多一条说明:换基准、过时答复的检查靠它认出页面上是哪一份 */
|
||||||
const NOTE_B2 = '以更早的一次运行为基准'
|
const NOTE_B2 = '以更早的一次运行为基准'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析卡(分析能力一轮,engine/compare._analyses):两次运行的分析卡按节点和项的标识对齐。形状照服务端真算出来的(用
|
||||||
|
* tests/fixtures/compare_runs 的虚构周报加分析卡的 analysis.end 跑 compare.compare):判定变了两项、判定一样判定语的数变了
|
||||||
|
* 一项、这次新增(算不了)一项、这次没有一项
|
||||||
|
*/
|
||||||
|
const side = (verdict, direction, phrase, status = 'ok', item = '') => ({ status, verdict, direction, phrase, artifact: `art-${item}-${verdict}` })
|
||||||
|
const ANALYSES = [
|
||||||
|
{ node_id: 'insight', title: '异动分析', item: 'wow', op: 'baseline', mode: null, change: 'verdict',
|
||||||
|
base: side('within_band', 'down', '销售额下降 3.1%,在过去 8 周的常规波动范围内(-6.2% 至 5.8%)', 'ok', 'wow'),
|
||||||
|
target: side('outside_band', 'down', '销售额下降 9.8%,超出过去 8 周的常规波动范围(-6.2% 至 5.8%)', 'ok', 'wow') },
|
||||||
|
{ node_id: 'insight', title: '异动分析', item: 'where', op: 'decompose', mode: 'additive', change: 'verdict',
|
||||||
|
base: side('driver', 'down', '销售额下降 340(-12.6%),主要来自华东(-300,占 88.2%)', 'ok', 'where'),
|
||||||
|
target: side('dispersed', 'down', '销售额下降 120(-4.5%),分散在各地区,没有哪一段占到一半', 'ok', 'where') },
|
||||||
|
{ node_id: 'insight', title: '异动分析', item: 'trend', op: 'trend', mode: null, change: 'phrase',
|
||||||
|
base: side('rising', 'up', '过去 8 周线上销售额呈上升趋势(每周约 2.1%)', 'ok', 'trend'),
|
||||||
|
target: side('rising', 'up', '过去 8 周线上销售额呈上升趋势(每周约 2.4%)', 'ok', 'trend') },
|
||||||
|
{ node_id: 'insight', title: '异动分析', item: 'refund', op: 'differ', mode: 'rate', change: 'added', base: null,
|
||||||
|
target: side(null, null, '差异检验没有做成:两组各只有 1 个样本,拿不到 p 值', 'refused', 'refund') },
|
||||||
|
{ node_id: 'insight', title: '异动分析', item: 'gone', op: 'comparable', mode: null, change: 'removed',
|
||||||
|
base: side('comparable', null, '两期可比', 'ok', 'gone'), target: null },
|
||||||
|
]
|
||||||
|
|
||||||
function comparison(target, base) {
|
function comparison(target, base) {
|
||||||
return { target: refOf(target), base: refOf(base), summary: SUMMARY, template: TEMPLATE, metrics: METRICS, queries: QUERIES,
|
return { target: refOf(target), base: refOf(base), summary: SUMMARY, template: TEMPLATE, metrics: METRICS, queries: QUERIES,
|
||||||
notes: base === B2 ? [NOTE, NOTE_B2] : [NOTE] }
|
notes: base === B2 ? [NOTE, NOTE_B2] : [NOTE] }
|
||||||
@@ -1418,6 +1440,39 @@ const overflow = (page) => page.evaluate(() => {
|
|||||||
return { root: root.scrollWidth - root.clientWidth, right: Math.round(r.right - innerWidth), wide: wide.slice(0, 3) }
|
return { root: root.scrollWidth - root.clientWidth, right: Math.round(r.right - innerWidth), wide: wide.slice(0, 3) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await section('分析卡:两次的判定按节点和分析项对齐,变了没有照服务端', async () => {
|
||||||
|
const { page, errors } = await open(comparePath(T, B), { compare: (t, b) => ({ json: { ...comparison(t, b), analyses: ANALYSES } }) })
|
||||||
|
await settled(page)
|
||||||
|
const box = page.locator('[data-compare-analyses]')
|
||||||
|
check('分析卡一区:标题、项数、说明(只列判定,不拆变化来源)', (await box.innerText().catch(() => '')).includes('分析卡')
|
||||||
|
&& (await box.innerText()).includes('不拆变化来源'), (await box.innerText().catch(() => '')).slice(0, 80))
|
||||||
|
const rows = box.locator('[data-compare-analysis]')
|
||||||
|
check('一项一行,先后照服务端', JSON.stringify(await rows.evaluateAll((els) => els.map((e) => e.dataset.compareAnalysis)))
|
||||||
|
=== JSON.stringify(ANALYSES.map((a) => `${a.node_id}:${a.item}`)))
|
||||||
|
const row = (item) => box.locator(`[data-compare-analysis="insight:${item}"]`)
|
||||||
|
const wow = (await row('wow').innerText()).replace(/\s+/g, ' ')
|
||||||
|
check('判定变了:写「判定变了」,两边的判定语照原样', wow.includes('判定变了') && wow.includes('在过去 8 周的常规波动范围内')
|
||||||
|
&& wow.includes('超出过去 8 周的常规波动范围'), wow)
|
||||||
|
check('……标题写分析卡的名字和项的标识、算子的叫法(契约)', wow.includes('异动分析 · wow') && wow.includes('波动基线')
|
||||||
|
&& (await row('where').innerText()).includes('变化拆解(可加)'))
|
||||||
|
check('判定一样、判定语里的数变了', (await row('trend').innerText()).includes('判定一致,判定语里的数变了'))
|
||||||
|
const added = (await row('refund').innerText()).replace(/\s+/g, ' ')
|
||||||
|
check('这次新增的一项:基准一边写「—」,算不了的判定语照原样', added.includes('这次新增') && added.includes('—')
|
||||||
|
&& added.includes('差异检验没有做成'), added)
|
||||||
|
check('这次没有的一项', (await row('gone').innerText()).includes('这次没有'))
|
||||||
|
check('……变化的说法用服务端的代码挑(data-change)', JSON.stringify(await rows.evaluateAll((els) => els.map((e) => e.dataset.change)))
|
||||||
|
=== JSON.stringify(ANALYSES.map((a) => a.change)))
|
||||||
|
check('没有运行时报错', errors.length === 0, errors.join(' | '))
|
||||||
|
// 老服务端、两次都没有分析卡:不画这一区
|
||||||
|
const old = await open(comparePath(T, B))
|
||||||
|
await settled(old.page)
|
||||||
|
check('没有分析卡(老服务端没有这一项):不画这一区', await old.page.locator('[data-compare-analyses]').count() === 0)
|
||||||
|
const narrow = await open(comparePath(T, B), { width: 390, height: 900, compare: (t, b) => ({ json: { ...comparison(t, b), analyses: ANALYSES } }) })
|
||||||
|
await settled(narrow.page)
|
||||||
|
const o = await overflow(narrow.page)
|
||||||
|
check('窄屏:分析卡一区不撑出横向滚动', o.root <= 0 && o.right <= 0, JSON.stringify(o))
|
||||||
|
})
|
||||||
|
|
||||||
await section('窄屏', async () => {
|
await section('窄屏', async () => {
|
||||||
for (const theme of ['dark', 'light']) {
|
for (const theme of ['dark', 'light']) {
|
||||||
const { page, errors } = await open(comparePath(T, B), { width: 390, height: 844, theme })
|
const { page, errors } = await open(comparePath(T, B), { width: 390, height: 844, theme })
|
||||||
|
|||||||
Reference in New Issue
Block a user