Download frontend/src/components/TrackingPage.tsx from SevdanurGenc/LLM-Automated-Test-Generation-Agent: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/TrackingPage.tsx
- Command line
-
hf download hf://SevdanurGenc/LLM-Automated-Test-Generation-Agent/frontend/src/components/TrackingPage.tsx
-
curl -L -o TrackingPage.tsx https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/TrackingPage.tsx
15.2 kB
| import { useEffect, useMemo, useState } from 'react'; | |
| import { api } from '../api'; | |
| import { useI18n, type MsgKey } from '../i18n'; | |
| import type { Health, TrackDetail, TrackRun, TrackSummary } from '../types'; | |
| import { modelName } from './ModelSelect'; | |
| const LANG_NAME: Record<string, string> = { python: 'Python', javascript: 'JavaScript', typescript: 'TypeScript', java: 'Java' }; | |
| /** Listedeki küçük eğilim çizgisi (mutasyon, yoksa kapsama). */ | |
| function Spark({ data }: { data: (number | null)[] }) { | |
| const v = data.filter((x): x is number => x != null); | |
| const w = 72, h = 22; | |
| if (v.length < 2) return <svg className="trk-spark" width={w} height={h} aria-hidden />; | |
| const pts = v.map((y, i) => `${(i * (w - 4)) / (v.length - 1) + 2},${h - 2 - (y / 100) * (h - 4)}`).join(' '); | |
| const last = pts.split(' ').pop()!.split(','); | |
| return ( | |
| <svg className="trk-spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden> | |
| <polyline points={pts} /> | |
| <circle cx={last[0]} cy={last[1]} r={2.5} /> | |
| </svg> | |
| ); | |
| } | |
| /** Zaman içinde kapsama ve mutasyon: tek eksen (0–100 %), iki seri, üzerine gelince ipucu. */ | |
| function TrendChart({ runs, health }: { runs: TrackRun[]; health: Health | null }) { | |
| const { t, pct, date } = useI18n(); | |
| const [hover, setHover] = useState<number | null>(null); | |
| const done = runs.filter((r) => r.status === 'done' && r.total); | |
| if (done.length < 2) return <p className="small muted">{t('trk.chart.one')}</p>; | |
| const W = 720, H = 240, L = 40, R = 96, T = 14, B = 30; | |
| const x = (i: number) => L + (i * (W - L - R)) / (done.length - 1); | |
| const y = (v: number) => T + (1 - v / 100) * (H - T - B); | |
| const series = [ | |
| { key: 'coverage' as const, cls: 's1', label: t('trk.chart.cov') }, | |
| { key: 'mutation' as const, cls: 's2', label: t('trk.chart.mut') }, | |
| ]; | |
| const path = (k: 'coverage' | 'mutation') => { | |
| let d = ''; | |
| done.forEach((r, i) => { | |
| const v = r[k]; | |
| if (v == null) return; | |
| d += `${d && done[i - 1]?.[k] != null ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`; | |
| }); | |
| return d; | |
| }; | |
| const onMove = (e: React.MouseEvent) => { | |
| const rect = (e.currentTarget as SVGElement).getBoundingClientRect(); | |
| const px = ((e.clientX - rect.left) / rect.width) * W; | |
| let best = 0; | |
| done.forEach((_, i) => { if (Math.abs(x(i) - px) < Math.abs(x(best) - px)) best = i; }); | |
| setHover(best); | |
| }; | |
| const h = hover != null ? done[hover] : null; | |
| const lastIdx = done.length - 1; | |
| return ( | |
| <div className="trk-chart"> | |
| <div className="legend"> | |
| {series.map((s) => <span key={s.key}><i className={`sw ${s.cls}`} />{s.label}</span>)} | |
| </div> | |
| <svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label={t('trk.chart')} | |
| onMouseMove={onMove} onMouseLeave={() => setHover(null)}> | |
| {[0, 25, 50, 75, 100].map((v) => ( | |
| <g key={v}> | |
| <line x1={L} x2={W - R} y1={y(v)} y2={y(v)} className="grid" /> | |
| <text x={L - 6} y={y(v) + 4} className="tick" textAnchor="end">{v}</text> | |
| </g> | |
| ))} | |
| {done.map((r, i) => (i === 0 || i === lastIdx || done.length <= 8 || i % Math.ceil(done.length / 8) === 0) && ( | |
| <text key={r.job_id} x={x(i)} y={H - 10} className="tick" textAnchor="middle">{i + 1}</text> | |
| ))} | |
| {hover != null && <line x1={x(hover)} x2={x(hover)} y1={T} y2={H - B} className="cross" />} | |
| {series.map((s) => ( | |
| <g key={s.key} className={s.cls}> | |
| <path d={path(s.key)} className="line" /> | |
| {done.map((r, i) => r[s.key] != null && ( | |
| <circle key={r.job_id} cx={x(i)} cy={y(r[s.key] as number)} r={hover === i ? 5 : 4} className="dot" /> | |
| ))} | |
| {done[lastIdx][s.key] != null && ( | |
| <text x={x(lastIdx) + 10} y={y(done[lastIdx][s.key] as number) + 4} className="dlabel"> | |
| {pct(done[lastIdx][s.key], 0)}</text> | |
| )} | |
| </g> | |
| ))} | |
| </svg> | |
| {h && ( | |
| <div className="trk-tip" style={{ left: `${Math.min(80, Math.max(20, (x(hover!) / W) * 100))}%` }}> | |
| <strong>{t('trk.tip.run', { n: hover! + 1 })}</strong> · {date(h.created_at)}<br /> | |
| <code>{h.model ? modelName(h.model, health) : '—'}</code> · {t(`trk.origin.${h.origin}` as MsgKey)}<br /> | |
| {t('hist.tests')}: {h.passed}/{h.total} · {t('trk.chart.cov')}: {pct(h.coverage)} · {t('trk.chart.mut')}: {pct(h.mutation)} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default function TrackingPage({ health, initial }: { health: Health | null; initial?: string }) { | |
| const { t, pct, date, lang } = useI18n(); | |
| const [list, setList] = useState<TrackSummary[]>([]); | |
| const [loaded, setLoaded] = useState(false); | |
| const [sel, setSel] = useState<string | null>(initial || null); | |
| const [detail, setDetail] = useState<TrackDetail | null>(null); | |
| const [q, setQ] = useState(''); | |
| const [fLang, setFLang] = useState(''); | |
| const [fSrc, setFSrc] = useState(''); | |
| const [sort, setSort] = useState<'recent' | 'runs' | 'name' | 'best'>('recent'); | |
| useEffect(() => { | |
| api.tracking().then((l) => { setList(l); setLoaded(true); setSel((s) => s || l[0]?.key || null); }).catch(() => setLoaded(true)); | |
| }, []); | |
| useEffect(() => { | |
| if (!sel) return; | |
| window.history.replaceState(null, '', `#/tracking/${sel}`); | |
| api.trackingDetail(sel).then(setDetail).catch(() => setDetail(null)); | |
| }, [sel]); | |
| const title = (g: { title: string; title_en: string }) => (lang === 'en' ? g.title_en || g.title : g.title); | |
| const shown = useMemo(() => { | |
| const f = list.filter((g) => (!fLang || g.language === fLang) && (!fSrc || g.source === fSrc) | |
| && (!q || `${title(g)} ${g.sample_id || ''}`.toLowerCase().includes(q.toLowerCase()))); | |
| const by: Record<string, (a: TrackSummary, b: TrackSummary) => number> = { | |
| recent: (a, b) => b.last_at - a.last_at, | |
| runs: (a, b) => b.runs - a.runs || b.last_at - a.last_at, | |
| name: (a, b) => title(a).localeCompare(title(b), lang), | |
| best: (a, b) => (b.best?.mutation ?? -1) - (a.best?.mutation ?? -1), | |
| }; | |
| return [...f].sort(by[sort]); | |
| }, [list, q, fLang, fSrc, sort, lang]); // eslint-disable-line react-hooks/exhaustive-deps | |
| const dur = (s: number | null) => (s == null ? '—' : s < 120 ? `${Math.round(s)} ${lang === 'tr' ? 'sn' : 's'}` : `${(s / 60).toFixed(1)} ${lang === 'tr' ? 'dk' : 'min'}`); | |
| const d = detail && detail.key === sel ? detail : null; | |
| return ( | |
| <div className="page"> | |
| <h1>{t('trk.title')}</h1> | |
| <p className="lead">{t('trk.lead')}</p> | |
| {loaded && !list.length ? ( | |
| <div className="panel"><div className="panel-body"><div className="empty"><div><h3>{t('trk.emptyTitle')}</h3><p>{t('trk.emptyText')}</p></div></div></div></div> | |
| ) : ( | |
| <div className="grid-2 trk-grid"> | |
| <div className="panel"> | |
| <div className="panel-head"><h2>{t('trk.codes')}</h2><div className="spacer" /><span className="small muted">{shown.length}</span></div> | |
| <div className="panel-body" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | |
| <input type="search" placeholder={t('trk.search')} value={q} onChange={(e) => setQ(e.target.value)} /> | |
| <div className="row" style={{ gap: 6 }}> | |
| <select value={fLang} onChange={(e) => setFLang(e.target.value)} aria-label={t('auto.allLangs')}> | |
| <option value="">{t('auto.allLangs')}</option> | |
| {Object.entries(LANG_NAME).map(([k, v]) => <option key={k} value={k}>{v}</option>)} | |
| </select> | |
| <select value={fSrc} onChange={(e) => setFSrc(e.target.value)} aria-label={t('trk.src.all')}> | |
| <option value="">{t('trk.src.all')}</option> | |
| {(['sample', 'library', 'custom'] as const).map((s) => <option key={s} value={s}>{t(`trk.src.${s}`)}</option>)} | |
| </select> | |
| <select value={sort} onChange={(e) => setSort(e.target.value as typeof sort)} aria-label="sort"> | |
| {(['recent', 'runs', 'best', 'name'] as const).map((s) => <option key={s} value={s}>{t(`trk.sort.${s}`)}</option>)} | |
| </select> | |
| </div> | |
| <div className="trk-list"> | |
| {shown.map((g) => ( | |
| <button key={g.key} className={`trk-row${g.key === sel ? ' on' : ''}`} onClick={() => setSel(g.key)}> | |
| <span className="trk-row-main"> | |
| <span className="trk-row-title">{title(g)}</span> | |
| <span className="small muted">{LANG_NAME[g.language]} · {t('trk.runs', { n: g.runs })} · {date(g.last_at)}</span> | |
| </span> | |
| <Spark data={g.trend} /> | |
| <span className="trk-row-best small">{g.best?.mutation != null ? pct(g.best.mutation, 0) : g.best?.coverage != null ? pct(g.best.coverage, 0) : '—'} | |
| <span className="muted"> {t('trk.best')}</span></span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="panel"> | |
| {!d ? ( | |
| <div className="panel-body"><div className="empty"><div><p>{t('trk.pick')}</p></div></div></div> | |
| ) : (<> | |
| <div className="panel-head"> | |
| <h2>{title(d)}</h2> | |
| <div className="spacer" /> | |
| {d.best && <a className="btn" href={`#/job/${d.best.job_id}`}>{t('trk.openBest')}</a>} | |
| <a className="btn" href={`#/job/${d.runs[d.runs.length - 1].job_id}`}>{t('trk.openLast')}</a> | |
| <a className="btn" href={`/api/tracking/${d.key}/runs.csv`}>{t('trk.csv')}</a> | |
| </div> | |
| <div className="panel-body" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> | |
| <div className="small muted"> | |
| {LANG_NAME[d.language]} · {t(`kind.${d.kind}` as MsgKey)} · {t('trk.lines', { n: d.lines })} · {t(`trk.src.${d.source}`)} | |
| {d.sample_id && <> · <code>{d.sample_id}</code></>} | |
| </div> | |
| <div className="trk-kpis"> | |
| {[ | |
| [String(d.stats.runs), t('trk.k.runs')], | |
| [`${d.stats.green}/${d.stats.runs}`, t('trk.k.green')], | |
| [pct(d.stats.best_coverage), t('trk.k.bestCov')], | |
| [pct(d.stats.best_mutation), t('trk.k.bestMut')], | |
| [dur(d.stats.avg_duration_s), t('trk.k.avgDur')], | |
| ].map(([v, k]) => <div key={k} className="trk-kpi"><b>{v}</b><span>{k}</span></div>)} | |
| </div> | |
| <div> | |
| <h3 className="trk-h">{t('trk.chart')}</h3> | |
| <TrendChart runs={d.runs} health={health} /> | |
| </div> | |
| <div> | |
| <h3 className="trk-h">{t('trk.byModel')}</h3> | |
| <div style={{ overflowX: 'auto' }}> | |
| <table className="data-table"> | |
| <thead><tr><th>{t('trk.col.model')}</th><th className="num">{t('trk.col.runs')}</th><th className="num">{t('trk.col.green')}</th> | |
| <th className="num">{t('trk.col.avgCov')}</th><th className="num">{t('trk.col.avgMut')}</th><th className="num">{t('trk.col.bestMut')}</th> | |
| <th className="num">{t('trk.col.avgDur')}</th></tr></thead> | |
| <tbody> | |
| {d.models.map((m) => ( | |
| <tr key={m.model}><td><code>{m.model === '—' ? '—' : modelName(m.model, health)}</code></td> | |
| <td className="num">{m.runs}</td><td className="num">{m.green}</td><td className="num">{pct(m.avg_coverage)}</td> | |
| <td className="num">{pct(m.avg_mutation)}</td><td className="num">{pct(m.best_mutation)}</td> | |
| <td className="num nowrap">{dur(m.avg_duration_s)}</td></tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 className="trk-h">{t('trk.allRuns')}</h3> | |
| <div style={{ overflowX: 'auto' }}> | |
| <table className="data-table"> | |
| <thead><tr><th>#</th><th>{t('trk.col.date')}</th><th>{t('trk.col.origin')}</th><th>{t('trk.col.model')}</th> | |
| <th>{t('auto.col.status')}</th><th className="num">{t('hist.tests')}</th><th className="num">{t('ev.col.cov')}</th> | |
| <th className="num">{t('ev.col.mut')}</th><th className="num">{t('trk.col.rounds')}</th><th className="num">{t('trk.col.time')}</th> | |
| <th>{t('auto.col.reports')}</th></tr></thead> | |
| <tbody> | |
| {[...d.runs].reverse().map((r, i) => { | |
| const best = d.best?.job_id === r.job_id; | |
| return ( | |
| <tr key={r.job_id} className={best ? 'selected' : ''}> | |
| <td className="num">{d.runs.length - i}{best && <span title={t('trk.bestRun')}> ★</span>}</td> | |
| <td className="nowrap"><a href={`#/job/${r.job_id}`}>{date(r.created_at)}</a></td> | |
| <td><span className="tag queued">{t(`trk.origin.${r.origin}` as MsgKey)}</span></td> | |
| <td className="nowrap"><code>{r.model ? modelName(r.model, health) : '—'}</code></td> | |
| <td><span className={`tag ${r.status === 'done' && (!r.total || r.passed !== r.total) ? 'paused' : r.status}`}> | |
| {t(`exp.status.${r.status === 'queued' ? 'pending' : r.status}` as MsgKey)}</span></td> | |
| <td className="num">{r.total != null ? `${r.passed}/${r.total}` : '—'}</td> | |
| <td className="num">{pct(r.coverage)}</td> | |
| <td className="num nowrap">{r.mutation_before != null && r.mutation != null && r.mutation_before !== r.mutation | |
| ? `${pct(r.mutation_before, 0)} → ${pct(r.mutation, 0)}` : pct(r.mutation)}</td> | |
| <td className="num">{r.iterations}</td> | |
| <td className="num nowrap">{dur(r.duration_s)}</td> | |
| <td className="nowrap"> | |
| <a className="rep-link html" target="_blank" rel="noreferrer" href={`/api/jobs/${r.job_id}/report.html?lang=${lang}`}>HTML</a>{' '} | |
| <a className="rep-link pdf" target="_blank" rel="noreferrer" href={`/api/jobs/${r.job_id}/report.html?lang=${lang}&print=1`}>PDF</a> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </>)} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |