- Add .gitignore for Python/data/models - Add matplotlib>=3.8.0 for eval plots - Add PretrainConfig, FinetuneConfig, BalabitAdapterConfig, EvalConfig dataclasses
348 lines
16 KiB
JavaScript
348 lines
16 KiB
JavaScript
/**
|
||
* Verify tab component — unified verification with [鼠标 | 滚轮] sub-tabs.
|
||
*/
|
||
|
||
import { api } from './api.js'
|
||
import { TAB10_COLORS, coolwarmColor, createChart, disposeChart } from './charts.js'
|
||
|
||
const MODE_LABELS = { target: '定点', fast: '快速', precise: '精确' }
|
||
|
||
const template = `
|
||
<div class="view">
|
||
<div class="sub-tabs">
|
||
<button class="sub-tab" :class="{active: subTab==='mouse'}" @click="subTab='mouse'">鼠标轨迹</button>
|
||
<button class="sub-tab" :class="{active: subTab==='scroll'}" @click="subTab='scroll'">滚轮事件</button>
|
||
</div>
|
||
|
||
<!-- ════════ 鼠标验证 ════════ -->
|
||
<div v-show="subTab==='mouse'">
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label>起点 x,y</label>
|
||
<input v-model="verify.startStr" :disabled="verify.loading" placeholder="100,200" />
|
||
</div>
|
||
<div class="field">
|
||
<label>终点 x,y</label>
|
||
<input v-model="verify.endStr" :disabled="verify.loading" placeholder="800,450" />
|
||
</div>
|
||
<div class="field">
|
||
<label>生成条数(1-12)</label>
|
||
<input type="number" v-model.number="verify.nPaths" :disabled="verify.loading" min="1" max="12" />
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" @click="startVerify" :disabled="verify.loading">
|
||
{{ verify.loading ? '生成中…' : '生成' }}
|
||
</button>
|
||
|
||
<div v-if="verify.error" class="msg msg-error">{{ verify.error }}</div>
|
||
|
||
<div v-show="verify.paths.length > 0">
|
||
<div class="verify-stats">
|
||
<div class="stat-pill">距离 <span>{{ verify.stats.dist }} px</span></div>
|
||
<div class="stat-pill">角度 <span>{{ verify.stats.angle }}°</span></div>
|
||
<div class="stat-pill">平均时长 <span>{{ verify.stats.avgDur }} ms</span></div>
|
||
<div class="stat-pill">均值 Δt <span>{{ verify.stats.meanDt }} ms</span></div>
|
||
</div>
|
||
<div class="verify-charts">
|
||
<div class="chart-box">
|
||
<div class="chart-title">轨迹可视化(颜色=速度)</div>
|
||
<div id="trajChart" class="echarts-box"></div>
|
||
</div>
|
||
<div class="chart-box">
|
||
<div class="chart-title">时间间隔 Δt</div>
|
||
<div id="dtChart" class="echarts-box"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════ 滚轮验证 ════════ -->
|
||
<div v-show="subTab==='scroll'">
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label>起始 scrollY</label>
|
||
<input type="number" v-model.number="scrollVerify.startScrollY" :disabled="scrollVerify.loading" />
|
||
</div>
|
||
<div class="field">
|
||
<label>目标 scrollY</label>
|
||
<input type="number" v-model.number="scrollVerify.targetScrollY" :disabled="scrollVerify.loading" />
|
||
</div>
|
||
<div class="field">
|
||
<label>模式</label>
|
||
<select v-model="scrollVerify.mode" :disabled="scrollVerify.loading" class="scroll-select">
|
||
<option value="target">定点</option>
|
||
<option value="fast">快速</option>
|
||
<option value="precise">精确</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>生成条数(1-12)</label>
|
||
<input type="number" v-model.number="scrollVerify.nPaths" :disabled="scrollVerify.loading" min="1" max="12" />
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" @click="startScrollVerify" :disabled="scrollVerify.loading">
|
||
{{ scrollVerify.loading ? '生成中…' : '生成' }}
|
||
</button>
|
||
|
||
<div v-if="scrollVerify.error" class="msg msg-error">{{ scrollVerify.error }}</div>
|
||
|
||
<div v-show="scrollVerify.paths.length > 0">
|
||
<div class="verify-stats">
|
||
<div class="stat-pill">距离 <span>{{ scrollVerify.stats.distance }} px</span></div>
|
||
<div class="stat-pill">模式 <span>{{ scrollVerify.stats.mode }}</span></div>
|
||
<div class="stat-pill">平均时长 <span>{{ scrollVerify.stats.avgDur }} ms</span></div>
|
||
<div class="stat-pill">事件数 <span>{{ scrollVerify.stats.avgEvents }}</span></div>
|
||
<div class="stat-pill">均值 |δY| <span>{{ scrollVerify.stats.meanAbsDy }}</span></div>
|
||
</div>
|
||
<div class="verify-charts">
|
||
<div class="chart-box">
|
||
<div class="chart-title">累计滚动位置</div>
|
||
<div id="scrollPosChart" class="echarts-box"></div>
|
||
</div>
|
||
<div class="chart-box">
|
||
<div class="chart-title">deltaY 事件</div>
|
||
<div id="scrollDyChart" class="echarts-box"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
|
||
export const VerifyView = {
|
||
template,
|
||
setup() {
|
||
const { ref, reactive, nextTick } = Vue
|
||
|
||
const subTab = ref('mouse')
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// 鼠标验证
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const verify = reactive({
|
||
startStr: '100,200', endStr: '700,400', nPaths: 5,
|
||
loading: false, error: '', paths: [],
|
||
stats: { dist: 0, angle: 0, avgDur: 0, meanDt: 0 },
|
||
})
|
||
|
||
let trajChartInst = null, dtChartInst = null
|
||
|
||
async function startVerify() {
|
||
verify.error = ''; verify.loading = true; verify.paths = []
|
||
const start = verify.startStr.split(',').map(Number)
|
||
const end = verify.endStr.split(',').map(Number)
|
||
if (start.length !== 2 || end.length !== 2 || start.some(isNaN) || end.some(isNaN)) {
|
||
verify.error = '坐标格式错误,请输入 x,y'; verify.loading = false; return
|
||
}
|
||
try {
|
||
const r = await api.post('/verify', { start, end, n_paths: Math.max(1, Math.min(12, verify.nPaths)) })
|
||
verify.paths = r.data.paths
|
||
|
||
// Stats
|
||
const dx = end[0] - start[0], dy = end[1] - start[1]
|
||
verify.stats.dist = Math.round(Math.sqrt(dx*dx + dy*dy))
|
||
verify.stats.angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI)
|
||
let totalDur = 0, totalDt = 0, dtCount = 0
|
||
for (const path of r.data.paths) {
|
||
if (path.length > 2) totalDur += path[path.length - 3][2] // last move point
|
||
for (let i = 1; i < path.length - 2; i++) {
|
||
totalDt += path[i][2] - path[i-1][2]; dtCount++
|
||
}
|
||
}
|
||
const n = r.data.paths.length || 1
|
||
verify.stats.avgDur = Math.round(totalDur / n)
|
||
verify.stats.meanDt = dtCount > 0 ? Math.round(totalDt / dtCount) : 0
|
||
|
||
await nextTick()
|
||
drawMouseCharts(r.data.paths, start, end)
|
||
} catch (e) { verify.error = e.response?.data?.detail || e.message }
|
||
finally { verify.loading = false }
|
||
}
|
||
|
||
function drawMouseCharts(paths, start, end) {
|
||
const trajDom = document.getElementById('trajChart')
|
||
const dtDom = document.getElementById('dtChart')
|
||
|
||
trajChartInst = disposeChart(trajChartInst)
|
||
dtChartInst = disposeChart(dtChartInst)
|
||
trajChartInst = createChart(trajDom)
|
||
dtChartInst = createChart(dtDom)
|
||
|
||
// ── Trajectory chart with speed coloring ──
|
||
const allSpeeds = []
|
||
for (const path of paths) {
|
||
for (let i = 1; i < path.length - 2; i++) {
|
||
const dx = path[i][0] - path[i-1][0], dy = path[i][1] - path[i-1][1]
|
||
const dt = (path[i][2] - path[i-1][2]) || 1
|
||
allSpeeds.push(Math.sqrt(dx*dx + dy*dy) / dt)
|
||
}
|
||
}
|
||
const maxSpd = Math.max(...allSpeeds, 0.01)
|
||
|
||
const segSeries = []
|
||
for (const path of paths) {
|
||
for (let i = 1; i < path.length - 2; i++) {
|
||
const dx = path[i][0] - path[i-1][0], dy = path[i][1] - path[i-1][1]
|
||
const dt = (path[i][2] - path[i-1][2]) || 1
|
||
const spd = Math.sqrt(dx*dx + dy*dy) / dt
|
||
const col = coolwarmColor(1 - spd / maxSpd)
|
||
segSeries.push({
|
||
type: 'line', data: [[path[i-1][0], path[i-1][1]], [path[i][0], path[i][1]]],
|
||
lineStyle: { color: col, width: 2, opacity: 0.8 }, symbol: 'none', silent: true, animation: false,
|
||
})
|
||
}
|
||
}
|
||
// Start/end markers
|
||
segSeries.push({
|
||
type: 'scatter', data: [[start[0], start[1]]], symbolSize: 14,
|
||
itemStyle: { color: '#22c55e' }, label: { show: true, formatter: 'S', color: '#fff', fontSize: 10 },
|
||
})
|
||
segSeries.push({
|
||
type: 'scatter', data: [[end[0], end[1]]], symbolSize: 14,
|
||
itemStyle: { color: '#fbbf24' }, label: { show: true, formatter: 'E', color: '#fff', fontSize: 10 },
|
||
})
|
||
|
||
trajChartInst.setOption({
|
||
backgroundColor: '#0a0e1a',
|
||
grid: { left: 48, right: 20, top: 16, bottom: 32 },
|
||
xAxis: { type: 'value', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
yAxis: { type: 'value', inverse: true, axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
series: segSeries,
|
||
}, true)
|
||
|
||
// ── Δt chart ──
|
||
const dtSeries = paths.map((path, idx) => ({
|
||
type: 'line', name: `路径 ${idx+1}`,
|
||
data: path.slice(1, -2).map((p, i) => path[i+1][2] - path[i][2]),
|
||
lineStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length], width: 1.5 },
|
||
itemStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length] },
|
||
symbol: 'none', smooth: false,
|
||
}))
|
||
|
||
// Mean line
|
||
const allDt = []; paths.forEach(p => { for (let i=1; i<p.length-2; i++) allDt.push(p[i][2]-p[i-1][2]) })
|
||
const meanDt = allDt.length > 0 ? allDt.reduce((a,b)=>a+b,0)/allDt.length : 0
|
||
|
||
dtChartInst.setOption({
|
||
backgroundColor: '#0a0e1a',
|
||
tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } },
|
||
legend: { show: paths.length > 1, top: 6, right: 8, textStyle: { color: '#64748b', fontSize: 11 }, itemWidth: 18, itemHeight: 3, icon: 'rect' },
|
||
grid: { left: 48, right: 20, top: paths.length > 1 ? 36 : 16, bottom: 32 },
|
||
xAxis: { type: 'category', name: '点序号', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { show: false } },
|
||
yAxis: { type: 'value', name: 'ms', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
dataZoom: [{ type: 'inside', xAxisIndex: 0, start: 0, end: 100 }],
|
||
series: [...dtSeries, {
|
||
type: 'line', name: '均值', data: [], markLine: {
|
||
silent: true, data: [{ yAxis: meanDt, label: { formatter: `均值 ${meanDt.toFixed(1)}ms`, color: '#64748b', fontSize: 10 } }],
|
||
lineStyle: { color: '#64748b', type: 'dashed' },
|
||
}, itemStyle: { color: 'transparent' },
|
||
}],
|
||
}, true)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// 滚轮验证
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const scrollVerify = reactive({
|
||
startScrollY: 0, targetScrollY: 2000, mode: 'target', nPaths: 5,
|
||
loading: false, error: '', paths: [],
|
||
stats: { distance: 0, mode: '', avgDur: 0, avgEvents: 0, meanAbsDy: 0 },
|
||
})
|
||
|
||
let scrollPosChartInst = null, scrollDyChartInst = null
|
||
|
||
async function startScrollVerify() {
|
||
scrollVerify.error = ''; scrollVerify.loading = true; scrollVerify.paths = []
|
||
try {
|
||
const r = await api.post('/scroll/verify', {
|
||
start_scrollY: scrollVerify.startScrollY, target_scrollY: scrollVerify.targetScrollY,
|
||
mode: scrollVerify.mode, n_paths: Math.max(1, Math.min(12, scrollVerify.nPaths)),
|
||
})
|
||
scrollVerify.paths = r.data.paths
|
||
|
||
const distance = Math.abs(scrollVerify.targetScrollY - scrollVerify.startScrollY)
|
||
let totalDur = 0, totalEvents = 0, totalAbsDy = 0, dyCount = 0
|
||
for (const path of r.data.paths) {
|
||
if (path.length > 0) { totalDur += path[path.length-1].t; totalEvents += path.length }
|
||
for (const ev of path) { totalAbsDy += Math.abs(ev.deltaY); dyCount++ }
|
||
}
|
||
const n = r.data.paths.length || 1
|
||
scrollVerify.stats = {
|
||
distance, mode: MODE_LABELS[scrollVerify.mode] || scrollVerify.mode,
|
||
avgDur: Math.round(totalDur / n), avgEvents: Math.round(totalEvents / n),
|
||
meanAbsDy: dyCount > 0 ? Math.round(totalAbsDy / dyCount) : 0,
|
||
}
|
||
|
||
await nextTick()
|
||
drawScrollCharts(r.data.paths)
|
||
} catch (e) { scrollVerify.error = e.response?.data?.detail || e.message }
|
||
finally { scrollVerify.loading = false }
|
||
}
|
||
|
||
function drawScrollCharts(paths) {
|
||
const posDom = document.getElementById('scrollPosChart')
|
||
const dyDom = document.getElementById('scrollDyChart')
|
||
scrollPosChartInst = disposeChart(scrollPosChartInst)
|
||
scrollDyChartInst = disposeChart(scrollDyChartInst)
|
||
scrollPosChartInst = createChart(posDom)
|
||
scrollDyChartInst = createChart(dyDom)
|
||
|
||
// Cumulative position chart with speed coloring
|
||
const allSpeeds = []
|
||
for (const path of paths) {
|
||
for (let i = 1; i < path.length; i++) {
|
||
const dy = Math.abs(path[i].deltaY), dt = (path[i].t - path[i-1].t) || 1
|
||
allSpeeds.push(dy / dt)
|
||
}
|
||
}
|
||
const sMax = Math.max(...allSpeeds, 0.01)
|
||
|
||
const segSeries = []
|
||
for (const path of paths) {
|
||
let cumY = 0; const pts = [{ t: 0, y: 0 }]
|
||
for (const ev of path) { cumY += ev.deltaY; pts.push({ t: ev.t, y: cumY }) }
|
||
for (let i = 1; i < pts.length; i++) {
|
||
const dy = Math.abs(pts[i].y - pts[i-1].y), dt = (pts[i].t - pts[i-1].t) || 1
|
||
const col = coolwarmColor(1 - (dy/dt) / sMax)
|
||
segSeries.push({
|
||
type: 'line', data: [[pts[i-1].t, pts[i-1].y], [pts[i].t, pts[i].y]],
|
||
lineStyle: { color: col, width: 2, opacity: 0.85 }, symbol: 'none', silent: true, animation: false,
|
||
})
|
||
}
|
||
}
|
||
|
||
scrollPosChartInst.setOption({
|
||
backgroundColor: '#0a0e1a',
|
||
tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } },
|
||
grid: { left: 56, right: 20, top: 16, bottom: 32 },
|
||
xAxis: { type: 'value', name: 'ms', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
yAxis: { type: 'value', name: 'scrollY', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
series: segSeries,
|
||
}, true)
|
||
|
||
// DeltaY chart
|
||
const dySeries = paths.map((path, idx) => ({
|
||
type: 'line', name: `路径 ${idx+1}`, data: path.map(ev => ev.deltaY),
|
||
lineStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length], width: 1.8 },
|
||
itemStyle: { color: TAB10_COLORS[idx % TAB10_COLORS.length] }, symbol: 'none',
|
||
}))
|
||
|
||
scrollDyChartInst.setOption({
|
||
backgroundColor: '#0a0e1a',
|
||
tooltip: { trigger: 'axis', backgroundColor: '#1e293b', borderColor: '#334155', textStyle: { color: '#f8fafc', fontSize: 12 } },
|
||
legend: { show: paths.length > 1, top: 6, right: 8, textStyle: { color: '#64748b', fontSize: 11 }, itemWidth: 18, itemHeight: 3, icon: 'rect' },
|
||
grid: { left: 52, right: 20, top: paths.length > 1 ? 36 : 16, bottom: 32 },
|
||
xAxis: { type: 'category', name: '事件序号', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { show: false } },
|
||
yAxis: { type: 'value', name: 'deltaY', axisLine: { lineStyle: { color: '#334155' } }, axisLabel: { color: '#475569', fontSize: 10 }, splitLine: { lineStyle: { color: '#1e293b' } } },
|
||
dataZoom: [{ type: 'inside', xAxisIndex: 0, start: 0, end: 100 }],
|
||
series: dySeries,
|
||
}, true)
|
||
}
|
||
|
||
return {
|
||
subTab, verify, scrollVerify,
|
||
startVerify, startScrollVerify,
|
||
}
|
||
}
|
||
}
|