SevdanurGenc's picture
Upload 300 files
9438a57 verified
Raw History Blame Contribute Delete
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>
);
}