Download frontend/src/components/ExperimentsPage.tsx from SevdanurGenc/LLM-Automated-Test-Generation-Agent: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/ExperimentsPage.tsx
- Command line
-
hf download hf://SevdanurGenc/LLM-Automated-Test-Generation-Agent/frontend/src/components/ExperimentsPage.tsx
-
curl -L -o ExperimentsPage.tsx https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/ExperimentsPage.tsx
13.1 kB
| import { useEffect, useMemo, useState } from 'react'; | |
| import { api } from '../api'; | |
| import { useI18n, type MsgKey } from '../i18n'; | |
| import type { Experiment, ExpPlan, ExpStep, Health } from '../types'; | |
| import { allModels, openConnections, providerOf } from './ModelSelect'; | |
| import { isNum, usePref } from '../settings'; | |
| function useDuration() { | |
| const { t } = useI18n(); | |
| return (s: number) => { | |
| if (!s || s < 60) return t('exp.dur.min', { m: s ? 1 : 0 }); | |
| const h = Math.floor(s / 3600), m = Math.round((s % 3600) / 60); | |
| return h ? t('exp.dur.hm', { h, m }) : t('exp.dur.min', { m }); | |
| }; | |
| } | |
| function stepName(s: ExpStep, t: (k: MsgKey, p?: Record<string, string | number>) => string) { | |
| if (s.kind === 'judge') return t('exp.step.judge', { mode: s.rule_mode }); | |
| if (s.kind === 'xmut') return t('exp.step.xmut'); | |
| if (s.ablate.length) return t('exp.step.abl', { a: s.ablate.join(', ') }); | |
| return t('exp.step.eval', { k: s.max_iterations, r: s.repeats }); | |
| } | |
| const PROFILES = ['deneme', 'hizli', 'tam', 'karsilastirma'] as const; | |
| type Profile = typeof PROFILES[number]; | |
| const MAX_MODELS = 8; | |
| export default function ExperimentsPage({ health }: { health: Health | null }) { | |
| const { t, pct, date, lang } = useI18n(); | |
| const dur = useDuration(); | |
| const [profile, setProfile] = usePref<Profile>('exp.profile', 'deneme', (v) => (PROFILES as readonly string[]).includes(v as string)); | |
| const [savedModels, setModels] = usePref<string[]>('exp.models', [], (v) => Array.isArray(v)); | |
| const [maxMutants, setMaxMutants] = usePref('exp.maxMutants', 30, isNum); | |
| const [plan, setPlan] = useState<ExpPlan | null>(null); | |
| const [list, setList] = useState<Experiment[]>([]); | |
| const [sel, setSel] = useState<string | null>(null); | |
| const [err, setErr] = useState<string | null>(null); | |
| const [busy, setBusy] = useState(false); | |
| const [repLang, setRepLang] = useState<'tr' | 'en'>(lang); | |
| useEffect(() => { setRepLang(lang); }, [lang]); | |
| const options = allModels(health); | |
| const available = options.map((m) => m.id); | |
| // Kayıtlı seçimden bu bilgisayarda bulunmayan modeller çıkarılır (ayar dosyası başka bilgisayardan gelmiş olabilir) | |
| const kept = available.length ? savedModels.filter((m) => available.includes(m)) : savedModels; | |
| const defaults = (() => { | |
| const pref = ['qwen2.5-coder:7b', 'qwen2.5-coder:14b'].filter((m) => available.includes(m)); | |
| return pref.length ? pref : available.length ? [health?.default_model && available.includes(health.default_model) ? health.default_model : available[0]] : []; | |
| })(); | |
| const models = kept.length || savedModels.length && !available.length ? kept : defaults; | |
| useEffect(() => { | |
| if (!models.length) return; | |
| api.expPlan(profile, models).then(setPlan).catch(() => setPlan(null)); | |
| }, [profile, models.join(',')]); // eslint-disable-line react-hooks/exhaustive-deps | |
| const load = () => api.experiments().then((l) => { | |
| setList(l); | |
| setSel((s) => s ?? (l[0]?.id || null)); | |
| }).catch((e) => setErr(String(e.message || e))); | |
| useEffect(() => { | |
| load(); | |
| const h = setInterval(load, 5000); | |
| return () => clearInterval(h); | |
| }, []); // eslint-disable-line react-hooks/exhaustive-deps | |
| const cur = useMemo(() => list.find((e) => e.id === sel) || null, [list, sel]); | |
| const active = list.find((e) => e.status === 'running'); | |
| const toggleModel = (m: string) => setModels(models.includes(m) ? models.filter((x) => x !== m) : [...models, m]); | |
| const start = async () => { | |
| setErr(null); setBusy(true); | |
| try { | |
| const { id } = await api.startExperiment({ profile, models, max_mutants: maxMutants }); | |
| setSel(id); await load(); | |
| } catch (e: any) { setErr(String(e.message) === 'busy' ? t('exp.err.busy') : String(e.message || e)); } | |
| setBusy(false); | |
| }; | |
| const act = async (fn: () => Promise<unknown>) => { | |
| setErr(null); | |
| try { await fn(); await load(); } catch (e: any) { setErr(String(e.message) === 'busy' ? t('exp.err.busy') : String(e.message || e)); } | |
| }; | |
| return ( | |
| <div className="page"> | |
| <h1>{t('exp.title')}</h1> | |
| <p className="lead">{t('exp.lead')}</p> | |
| <div className="grid-2"> | |
| <div className="panel"> | |
| <div className="panel-head"><h2>{t('exp.new')}</h2></div> | |
| <div className="panel-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> | |
| <div className="choice-list" role="radiogroup" aria-label={t('exp.profile')}> | |
| {PROFILES.map((p) => ( | |
| <label key={p} className={`choice${profile === p ? ' on' : ''}`}> | |
| <input type="radio" name="profile" checked={profile === p} onChange={() => setProfile(p)} /> | |
| <span><strong>{t(`exp.profile.${p}` as MsgKey)}</strong><br /> | |
| <span className="small muted">{t(`exp.profile.${p}.hint` as MsgKey)}</span></span> | |
| </label> | |
| ))} | |
| </div> | |
| <div> | |
| <strong className="small">{t('exp.models')}</strong> | |
| <div className="check-list" style={{ maxHeight: 260 }}> | |
| {(['ollama', 'openai', 'anthropic'] as const).map((p) => { | |
| const items = options.filter((m) => m.provider === p); | |
| return items.length ? ( | |
| <div key={p}> | |
| <div className="small muted" style={{ fontWeight: 600, margin: '4px 0 2px' }}>{t(`model.group.${p}` as MsgKey)}</div> | |
| {items.map((m) => ( | |
| <label key={m.id}><input type="checkbox" checked={models.includes(m.id)} disabled={!models.includes(m.id) && models.length >= MAX_MODELS} onChange={() => toggleModel(m.id)} /> | |
| <code>{m.name}</code></label> | |
| ))} | |
| </div>) : null; | |
| })} | |
| {!available.length && <div className="small muted">{t('exp.noModels')}</div>} | |
| </div> | |
| <div className="small muted">{t('exp.modelsHint')} <a href="#" onClick={(e) => { e.preventDefault(); openConnections(); }}>{t('model.add')}</a></div> | |
| {models.some((m) => providerOf(m) !== 'ollama') && <div className="warn-box small" style={{ marginTop: 6 }}>{t('ev.cloudCost')}</div>} | |
| </div> | |
| <label className="field">{t('ev.mutPerItem')} | |
| <input type="number" min={0} max={200} value={maxMutants} onChange={(e) => setMaxMutants(+e.target.value)} style={{ width: 80 }} /> | |
| </label> | |
| {plan && ( | |
| <div className="plan-box small"> | |
| {t('exp.planSummary', { s: plan.steps.length, r: plan.item_runs, n: plan.items })}<br /> | |
| <strong>{t('exp.estimate', { d: dur(plan.eta_s) })}</strong> | |
| <span className="muted"> · {t('exp.estimateHint')}</span> | |
| </div> | |
| )} | |
| <button className="btn primary" onClick={start} disabled={busy || !models.length || !!active}> | |
| {active ? t('exp.running') : t('exp.start')} | |
| </button> | |
| <div className="small muted">{t('exp.resumeHint')}</div> | |
| {err && <div className="error-box">{err}</div>} | |
| {list.length > 0 && ( | |
| <div> | |
| <strong className="small">{t('exp.previous')}</strong> | |
| <table className="data-table"><tbody> | |
| {list.map((e) => ( | |
| <tr key={e.id} className={`clickable${e.id === sel ? ' selected' : ''}`} onClick={() => setSel(e.id)}> | |
| <td>{t(`exp.profile.${e.profile}` as MsgKey)} · <code>{e.models.join(', ')}</code> | |
| <div className="small muted">{date(e.created_at)}</div></td> | |
| <td className="num"><span className={`tag ${e.status}`}>{t(`exp.status.${e.status}` as MsgKey)}</span> | |
| <div className="small muted">{pct(e.percent, 0)}</div></td> | |
| </tr> | |
| ))} | |
| </tbody></table> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="panel"> | |
| <div className="panel-head"> | |
| <h2>{cur ? <>{t(`exp.profile.${cur.profile}` as MsgKey)} · <code>{cur.models.join(', ')}</code></> : t('exp.progress')}</h2> | |
| <div className="spacer" /> | |
| {cur && cur.status === 'running' && <button className="btn" onClick={() => act(() => api.pauseExperiment(cur.id))}>{t('exp.pause')}</button>} | |
| {cur && ['running', 'paused', 'failed'].includes(cur.status) && | |
| <button className="btn danger" onClick={() => { if (window.confirm(t('exp.stopConfirm'))) act(() => api.cancelExperiment(cur.id)); }}>{t('exp.stop')}</button>} | |
| {cur && (cur.status === 'paused' || cur.status === 'failed') && | |
| <button className="btn primary" onClick={() => act(() => api.resumeExperiment(cur.id))}>{t('exp.resume')}</button>} | |
| {cur && (cur.status === 'done' || cur.status === 'failed') && !!cur.retryable && (cur.retryable.items + cur.retryable.xmut) > 0 && | |
| <button className="btn" title={t('exp.retryHint')} onClick={() => act(() => api.retryExperiment(cur.id))}> | |
| {t('exp.retry', { n: cur.retryable.items + cur.retryable.xmut })}</button>} | |
| {cur && (<> | |
| <div className="seg rep-lang" role="group" aria-label={t('exp.reportLang')} title={t('exp.reportLang')}> | |
| {(['tr', 'en'] as const).map((l) => ( | |
| <button key={l} aria-pressed={repLang === l} onClick={() => setRepLang(l)}>{l.toUpperCase()}</button>))} | |
| </div> | |
| <a className="btn" href={`/api/experiments/runs/${cur.id}/report.html?lang=${repLang}`} target="_blank" rel="noreferrer" | |
| title={t('exp.reportHtml.title')}>{t('exp.reportHtml')}</a> | |
| <a className="btn" href={`/api/experiments/runs/${cur.id}/report.html?lang=${repLang}&print=1`} target="_blank" rel="noreferrer" | |
| title={t('hist.reportPdf.title')}>PDF</a> | |
| <a className="btn" href={`/api/experiments/runs/${cur.id}/results.zip?lang=${repLang}`} title={t('exp.zip.title')}>{t('exp.download')}</a> | |
| </>)} | |
| </div> | |
| <div className="panel-body"> | |
| {!cur ? ( | |
| <div className="empty"><div><h3>{t('exp.emptyTitle')}</h3><p>{t('exp.emptyText')}</p></div></div> | |
| ) : (<> | |
| <div className="exp-progress"> | |
| <div className="row" style={{ justifyContent: 'space-between' }}> | |
| <span><span className={`tag ${cur.status}`}>{t(`exp.status.${cur.status}` as MsgKey)}</span>{' '} | |
| <strong>{pct(cur.percent, 1)}</strong> · {t('exp.items', { d: cur.items_done, n: cur.items_total })} | |
| {cur.judge_total > 0 && ` · ${t('exp.judges', { d: cur.judge_done, n: cur.judge_total })}`}</span> | |
| {cur.status !== 'done' && <span className="small muted">{t('exp.eta', { d: dur(cur.eta_s), a: Math.round(cur.avg_item_s) })}</span>} | |
| </div> | |
| <div className="meter neutral" style={{ marginTop: 8, height: 10 }}><span style={{ width: `${cur.percent}%` }} /></div> | |
| {cur.status === 'running' && <div className="small muted" style={{ marginTop: 6 }}>{t('exp.sleepHint')}</div>} | |
| {cur.status === 'done' && <div className="ok-box small" style={{ marginTop: 10 }}>{t('exp.doneHint')}</div>} | |
| {cur.error && <div className="error-box small" style={{ marginTop: 10 }}>{cur.error}</div>} | |
| </div> | |
| <div style={{ overflowX: 'auto' }} className="section-gap"> | |
| <table className="data-table"> | |
| <thead><tr><th>{t('exp.col.step')}</th><th>{t('exp.col.model')}</th><th>{t('exp.col.status')}</th> | |
| <th className="num">{t('exp.col.progress')}</th><th className="num">{t('ev.col.cov')}</th><th className="num">{t('ev.col.mut')}</th></tr></thead> | |
| <tbody> | |
| {cur.steps.map((s) => ( | |
| <tr key={s.label}> | |
| <td>{stepName(s, t)}<div className="small muted"><code>{s.label}</code></div> | |
| {s.error && <div className="small" style={{ color: 'var(--miss)' }}>{s.error.slice(0, 160)}</div>}</td> | |
| <td><code>{s.model}</code></td> | |
| <td><span className={`tag ${s.status}`}>{t(`exp.status.${s.status}` as MsgKey)}</span></td> | |
| <td className="num">{s.kind === 'eval' ? `${s.done}/${s.total}` : s.kind === 'xmut' && s.done ? s.done : '—'}</td> | |
| <td className="num">{s.summary ? `${pct(s.summary.first_coverage)} → ${pct(s.summary.final_coverage)}` : '—'}</td> | |
| <td className="num">{s.summary ? pct(s.summary.mutation_score_pooled) : '—'}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </>)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |