Files
ai_mouse/static/js/verify.js
Huang Qi 4d414fd180 chore: initialize git repo, add matplotlib dep, extend config
- Add .gitignore for Python/data/models
- Add matplotlib>=3.8.0 for eval plots
- Add PretrainConfig, FinetuneConfig, BalabitAdapterConfig, EvalConfig dataclasses
2026-05-10 12:24:07 +08:00

348 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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,
}
}
}