Download frontend/src/components/Workspace.tsx from SevdanurGenc/LLM-Automated-Test-Generation-Agent: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/Workspace.tsx
- Command line
-
hf download hf://SevdanurGenc/LLM-Automated-Test-Generation-Agent/frontend/src/components/Workspace.tsx
-
curl -L -o Workspace.tsx https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/frontend/src/components/Workspace.tsx
18.5 kB
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |
| import Editor from '@monaco-editor/react'; | |
| import { api, subscribe } from '../api'; | |
| import ModelSelect, { allModels, providerOf, PROVIDER_LABEL } from './ModelSelect'; | |
| import { isNum, usePref } from '../settings'; | |
| import type { Device } from '../types'; | |
| import type { AgentEvent, Analysis, Health, JobState, Language, Mutant, RunResult, Sample, TestKind } from '../types'; | |
| import AgentFlow from './AgentFlow'; | |
| import Results from './Results'; | |
| import { useI18n, type MsgKey } from '../i18n'; | |
| import { useTheme } from '../theme'; | |
| import { ACCEPT, MAX_TOTAL, baseName, helpersFor, langOf, pickTarget, readInputs, type InFile } from '../projectFiles'; | |
| const LANGS: { id: Language; label: string }[] = [ | |
| { id: 'python', label: 'Python' }, | |
| { id: 'javascript', label: 'JavaScript' }, | |
| { id: 'typescript', label: 'TypeScript' }, | |
| { id: 'java', label: 'Java' }, | |
| ]; | |
| const KINDS: TestKind[] = ['unit', 'interface', 'ui']; | |
| const MONACO_LANG: Record<string, string> = { python: 'python', javascript: 'javascript', typescript: 'typescript', java: 'java' }; | |
| interface Project { files: InFile[]; target: string } | |
| const STARTER = `def fizzbuzz(n: int) -> str: | |
| """1..n için FizzBuzz; n < 1 ise ValueError.""" | |
| if n < 1: | |
| raise ValueError("n en az 1 olmalı") | |
| out = [] | |
| for i in range(1, n + 1): | |
| if i % 15 == 0: | |
| out.append("FizzBuzz") | |
| elif i % 3 == 0: | |
| out.append("Fizz") | |
| elif i % 5 == 0: | |
| out.append("Buzz") | |
| else: | |
| out.append(str(i)) | |
| return " ".join(out) | |
| `; | |
| const STARTER_EN = STARTER.replace('1..n için FizzBuzz; n < 1 ise ValueError.', 'FizzBuzz for 1..n; ValueError if n < 1.') | |
| .replace('n en az 1 olmalı', 'n must be at least 1'); | |
| export interface LiveView { | |
| analysis: Analysis | null; | |
| testCode: string; | |
| testFile: string; | |
| run: RunResult | null; | |
| mutants: Mutant[]; | |
| mutantTotal: number; | |
| } | |
| export default function Workspace({ health, jobId }: { health: Health | null; jobId?: string }) { | |
| const { dark } = useTheme(); | |
| const { t, lang } = useI18n(); | |
| const [language, setLanguage] = useState<Language>('python'); | |
| const [kind, setKind] = useState<TestKind>('unit'); | |
| const [code, setCode] = useState(() => (lang === 'en' ? STARTER_EN : STARTER)); | |
| const [html, setHtml] = useState(''); | |
| const [samples, setSamples] = useState<Sample[]>([]); | |
| // Ayarlar taşınabilir ayar dosyasında saklanır (ayarlar/testajani-ayarlar.json) | |
| const [prefModel, setPrefModel] = usePref<string>('ws.model', ''); | |
| const [jobModel, setJobModel] = useState<string | null>(null); // Geçmiş'ten açılan koşunun modeli (varsayılanı değiştirmez) | |
| const model = jobModel ?? (prefModel && (!health || allModels(health).some((m) => m.id === prefModel)) ? prefModel : (health?.default_model || prefModel)); | |
| const setModel = (m: string) => { setJobModel(null); setPrefModel(m); }; | |
| const [device, setDevice] = usePref<Device | null>('ws.device', null, (v) => v === 'auto' || v === 'gpu' || v === 'cpu'); | |
| const [maxIter, setMaxIter] = usePref('ws.maxIter', health?.max_iterations ?? 4, isNum); | |
| const [target, setTarget] = usePref('ws.target', health?.coverage_target ?? 85, isNum); | |
| const [mutation, setMutation] = usePref('ws.mutation', true); | |
| const [maxMutants, setMaxMutants] = usePref('ws.maxMutants', 25, isNum); | |
| const [mutRound, setMutRound] = usePref('ws.mutRound', true); | |
| const [project, setProject] = useState<Project | null>(null); | |
| const [job, setJob] = useState<JobState | null>(null); | |
| const [events, setEvents] = useState<AgentEvent[]>([]); | |
| const [running, setRunning] = useState(false); | |
| const [err, setErr] = useState<string | null>(null); | |
| const [notice, setNotice] = useState<string | null>(null); | |
| const [dragging, setDragging] = useState(false); | |
| const unsub = useRef<null | (() => void)>(null); | |
| const fileInput = useRef<HTMLInputElement>(null); | |
| useEffect(() => { api.samples().then(setSamples).catch(() => {}); }, []); | |
| // Başlangıç örneği değiştirilmediyse dil değişince onun da dilini değiştir | |
| useEffect(() => { setCode((c) => (c === STARTER || c === STARTER_EN ? (lang === 'en' ? STARTER_EN : STARTER) : c)); }, [lang]); | |
| const attach = useCallback((id: string) => { | |
| unsub.current?.(); | |
| setEvents([]); | |
| setRunning(true); | |
| unsub.current = subscribe(id, (e) => setEvents((prev) => [...prev, e]), async () => { | |
| try { setJob(await api.job(id)); } catch { /* yoksay */ } | |
| setRunning(false); | |
| }); | |
| }, []); | |
| // URL'de iş kimliği varsa (Geçmiş'ten açıldıysa) yükle | |
| useEffect(() => { | |
| if (!jobId) return; | |
| api.job(jobId).then((j) => { | |
| setJob(j); | |
| setLanguage(j.request.language); | |
| setKind(j.request.test_kind); | |
| setCode(j.request.code); | |
| if (j.request.model) setJobModel(j.request.model); | |
| setHtml(j.request.html || ''); | |
| const ex = j.request.extra_files || {}; | |
| const tgt = j.request.source_name || 'solution'; | |
| setProject(Object.keys(ex).length ? { target: tgt, files: [{ path: tgt, text: j.request.code }, | |
| ...Object.entries(ex).map(([path, text]) => ({ path, text }))] } : null); | |
| if (j.status === 'running' || j.status === 'queued') attach(j.id); | |
| else setEvents(j.events); | |
| }).catch((e) => setErr(String(e.message || e))); | |
| }, [jobId, attach]); | |
| useEffect(() => () => unsub.current?.(), []); | |
| const start = async () => { | |
| setErr(null); | |
| setJob(null); | |
| try { | |
| const { id } = await api.createJob({ | |
| language, code, test_kind: kind, html: kind === 'ui' && html.trim() ? html : null, | |
| model: model || null, device: cloud ? null : device, max_iterations: maxIter, coverage_target: target, | |
| run_mutation: mutation, max_mutants: maxMutants, lang, mutation_rounds: mutation && mutRound ? 1 : 0, | |
| ...(project ? { extra_files: helpersFor(project.files, project.target).extra, source_name: baseName(project.target) } : {}), | |
| }); | |
| window.history.replaceState(null, '', `#/job/${id}`); | |
| const j = await api.job(id); | |
| setJob(j); | |
| attach(id); | |
| } catch (e: any) { | |
| setErr(String(e.message || e)); | |
| } | |
| }; | |
| const stop = async () => { if (job) await api.cancel(job.id); }; | |
| const loadSample = (id: string) => { | |
| const s = samples.find((x) => x.id === id); | |
| if (!s) return; | |
| setLanguage(s.language); | |
| setKind(s.kind); | |
| setCode(s.code); | |
| setHtml(s.html || ''); | |
| setProject(null); | |
| }; | |
| // Yerel dosya(lar)ı ya da zip'i yükler. Tek dosya: uzantıdan dil seçilir. HTML + JS: Web UI testi. | |
| // Birden çok kaynak dosyası: proje kipi — biri test edilir, diğerleri yanına yardımcı olarak konur. | |
| const loadFiles = async (list: FileList | File[]) => { | |
| setErr(null); | |
| setNotice(null); | |
| let res; | |
| try { res = await readInputs(list); } catch { setErr(t('ws.err.zip')); return; } | |
| const { files, skipped, tooBig } = res; | |
| if (tooBig.length && !files.some((f) => langOf(f.path))) { setErr(t('ws.err.big', { name: tooBig[0], kb: '>200' })); return; } | |
| if (!files.some((f) => langOf(f.path))) { | |
| setErr(t('ws.err.type', { list: ACCEPT.replace(/,/g, ', ') })); | |
| return; | |
| } | |
| const pages = files.filter((f) => langOf(f.path) === 'html'); | |
| const scripts = files.filter((f) => langOf(f.path) === 'javascript'); | |
| const sources = files.filter((f) => { const l = langOf(f.path); return l && l !== 'html'; }); | |
| const skipNote = skipped.length + tooBig.length ? ' ' + t('ws.skipped', { n: skipped.length + tooBig.length }) : ''; | |
| if (pages.length === 1 && scripts.length <= 1 && sources.length === scripts.length) { // Web UI | |
| setLanguage('javascript'); setKind('ui'); setProject(null); | |
| if (scripts[0]) { setCode(scripts[0].text); setHtml(pages[0].text); } | |
| else { setCode(pages[0].text); setHtml(''); } | |
| setNotice((scripts[0] ? t('ws.loaded.ui2', { js: baseName(scripts[0].path), html: baseName(pages[0].path) }) | |
| : t('ws.loaded.ui1', { html: baseName(pages[0].path) })) + skipNote); | |
| return; | |
| } | |
| const target = pickTarget(files); | |
| if (!target) { setErr(t('ws.err.type', { list: ACCEPT.replace(/,/g, ', ') })); return; } | |
| if (files.length === 1) { | |
| setProject(null); | |
| selectTarget({ files, target }, false); | |
| setNotice(t('ws.loaded', { name: baseName(target) }) + skipNote); | |
| return; | |
| } | |
| const total = files.reduce((a, f) => a + f.text.length, 0); | |
| if (total > MAX_TOTAL) { setErr(t('ws.err.total')); return; } | |
| selectTarget({ files, target }, true); | |
| setNotice(t('ws.project.loaded', { n: files.length, name: baseName(target) }) + skipNote); | |
| }; | |
| const selectTarget = (p: Project, keepProject: boolean) => { | |
| const f = p.files.find((x) => x.path === p.target)!; | |
| const l = langOf(f.path) as Language; | |
| setLanguage(l); | |
| if (kind === 'ui' && l !== 'javascript') setKind('unit'); | |
| setCode(f.text); | |
| setHtml(''); | |
| setProject(keepProject ? p : null); | |
| }; | |
| const helperInfo = project ? helpersFor(project.files, project.target) : null; | |
| // Akan olaylardan canlı görünüm; iş bitince sunucudaki kesin sonuç (en iyi sürüm) kullanılır. | |
| const live: LiveView = useMemo(() => { | |
| const final = job?.result && !running ? job.result : null; | |
| let analysis: Analysis | null = null, testCode = '', run: RunResult | null = null; | |
| const muts = new Map<number, Mutant>(); | |
| let mutantTotal = 0; | |
| for (const e of events) { | |
| if (e.type === 'result' && e.data.analysis) analysis = e.data.analysis; | |
| if (e.type === 'tool_call' && e.data.tool === 'write_tests') testCode = e.data.code; | |
| if (e.type === 'tool_result' && e.data.run) run = e.data.run; | |
| if (e.type === 'mutant') { muts.set(e.data.mutant.id, e.data.mutant); mutantTotal = e.data.total; } | |
| } | |
| if (final) { | |
| return { | |
| analysis: final.analysis, testCode: final.test_code, testFile: final.test_filename, | |
| run: final.final_run, mutants: final.mutation?.mutants || [...muts.values()], | |
| mutantTotal: final.mutation?.total ?? mutantTotal, | |
| }; | |
| } | |
| return { analysis, testCode, testFile: job?.result?.test_filename || '', run, mutants: [...muts.values()], mutantTotal }; | |
| }, [events, job, running]); | |
| const kindDisabled = (k: TestKind) => k === 'ui' && language !== 'javascript'; | |
| const codeLang = kind === 'ui' && /^\s*(<!doctype|<html)/i.test(code) ? 'html' : MONACO_LANG[language]; | |
| const prov = providerOf(model); | |
| const cloud = prov !== 'ollama'; | |
| const effDevice: Device = device || health?.providers?.device || 'auto'; | |
| return ( | |
| <main className="workspace"> | |
| <section className="panel" aria-labelledby="code-h"> | |
| <div className="panel-head"> | |
| <h2 id="code-h">{t('ws.code')}</h2> | |
| <div className="seg" role="group" aria-label={t('ws.language')}> | |
| {LANGS.map((l) => ( | |
| <button key={l.id} aria-pressed={language === l.id} | |
| onClick={() => { setLanguage(l.id); setProject(null); if (l.id !== 'javascript' && kind === 'ui') setKind('unit'); }}> | |
| {l.label} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="spacer" /> | |
| <button className="btn btn-sm" onClick={() => fileInput.current?.click()} title={t('ws.openFile.title', { accept: ACCEPT })}> | |
| {t('ws.openFile')} | |
| </button> | |
| <input ref={fileInput} type="file" accept={ACCEPT} multiple hidden | |
| onChange={(e) => { if (e.target.files) loadFiles(e.target.files); e.target.value = ''; }} /> | |
| <select aria-label={t('ws.loadSample.aria')} value="" onChange={(e) => loadSample(e.target.value)} style={{ maxWidth: 170 }}> | |
| <option value="">{t('ws.loadSample')}</option> | |
| {(['dev', 'test', 'bagimsiz'] as const).map((sp) => { | |
| const group = samples.filter((s) => (s.split || 'dev') === sp); | |
| return group.length ? ( | |
| <optgroup key={sp} label={t(`ws.split.${sp}` as MsgKey)}> | |
| {group.map((s) => <option key={s.id} value={s.id}>{lang === 'en' ? s.title_en || s.title : s.title}</option>)} | |
| </optgroup> | |
| ) : null; | |
| })} | |
| </select> | |
| </div> | |
| {project && helperInfo && ( | |
| <div className="project-bar"> | |
| <span className="small muted">{t('ws.project')}</span> | |
| <select value={project.target} aria-label={t('ws.project.target')} title={t('ws.project.target')} | |
| onChange={(e) => selectTarget({ ...project, target: e.target.value }, true)}> | |
| {project.files.filter((f) => { const l = langOf(f.path); return l && l !== 'html'; }).map((f) => ( | |
| <option key={f.path} value={f.path}>{f.path}</option>))} | |
| </select> | |
| <span className="small muted" title={Object.keys(helperInfo.extra).join('\n')}> | |
| {t('ws.project.helpers', { n: Object.keys(helperInfo.extra).length })} | |
| {helperInfo.ignored.length > 0 && ` · ${t('ws.project.ignored', { n: helperInfo.ignored.length })}`} | |
| </span> | |
| <div className="spacer" /> | |
| <button className="btn btn-sm" onClick={() => setProject(null)} title={t('ws.project.clear')}>×</button> | |
| </div> | |
| )} | |
| <div className="panel-head" style={{ minHeight: 0, paddingTop: 8, paddingBottom: 8 }}> | |
| <span className="small muted">{t('ws.testKind')}</span> | |
| <div className="seg" role="group" aria-label={t('ws.testKind')}> | |
| {KINDS.map((k) => ( | |
| <button key={k} aria-pressed={kind === k} disabled={kindDisabled(k)} title={t(`kind.${k}.hint` as MsgKey)} | |
| onClick={() => setKind(k)}>{t(`kind.${k}` as MsgKey)}</button> | |
| ))} | |
| </div> | |
| <span className="small muted">{t(`kind.${kind}.hint` as MsgKey)}</span> | |
| </div> | |
| <div className={`editor-wrap${dragging ? ' dragging' : ''}`} | |
| onDragOver={(e) => { if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); setDragging(true); } }} | |
| onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragging(false); }} | |
| onDrop={(e) => { e.preventDefault(); setDragging(false); if (e.dataTransfer.files.length) loadFiles(e.dataTransfer.files); }}> | |
| {dragging && <div className="drop-hint">{t('ws.drop')}</div>} | |
| <Editor | |
| height="100%" language={codeLang} value={code} onChange={(v) => setCode(v || '')} | |
| theme={dark ? 'vs-dark' : 'vs'} | |
| options={{ minimap: { enabled: false }, fontSize: 13, fontFamily: 'JetBrains Mono, monospace', scrollBeyondLastLine: false, tabSize: 4, automaticLayout: true, | |
| unicodeHighlight: { ambiguousCharacters: false, invisibleCharacters: false } }} | |
| /> | |
| </div> | |
| {kind === 'ui' && codeLang !== 'html' && ( | |
| <details className="html-toggle"> | |
| <summary>{t('ws.html', { file: 'app.js' })}</summary> | |
| <textarea value={html} onChange={(e) => setHtml(e.target.value)} rows={8} className="mono" | |
| placeholder={'…<script src="app.js"></script>'} style={{ width: '100%', marginTop: 8 }} /> | |
| </details> | |
| )} | |
| <div className="run-bar"> | |
| <label className="field">{t('ws.model')} | |
| <ModelSelect health={health} value={model} onChange={setModel} style={{ maxWidth: 220 }} /> | |
| </label> | |
| <div className="field device-seg" title={t('ws.device.title')}>{t('ws.device')} | |
| <div className="seg" role="radiogroup" aria-label={t('ws.device')}> | |
| {cloud ? <button disabled aria-pressed>{t('ws.device.cloud')}</button> | |
| : (['auto', 'gpu', 'cpu'] as const).map((d) => ( | |
| <button key={d} role="radio" aria-checked={effDevice === d} aria-pressed={effDevice === d} | |
| onClick={() => setDevice(d)}>{t(`conn.device.${d}`)}</button>))} | |
| </div> | |
| </div> | |
| <label className="field">{t('ws.maxIter')} | |
| <input type="number" min={1} max={10} value={maxIter} onChange={(e) => setMaxIter(+e.target.value)} style={{ width: 64 }} /> | |
| </label> | |
| <label className="field">{t('ws.target')} | |
| <input type="number" min={0} max={100} value={target} onChange={(e) => setTarget(+e.target.value)} style={{ width: 72 }} /> | |
| </label> | |
| <label className="field">{t('ws.mutants')} | |
| <span className="row" style={{ gap: 6 }}> | |
| <input type="checkbox" checked={mutation} onChange={(e) => setMutation(e.target.checked)} aria-label={t('ws.mutants.aria')} /> | |
| <input type="number" min={0} max={200} value={maxMutants} disabled={!mutation} | |
| onChange={(e) => setMaxMutants(+e.target.value)} style={{ width: 64 }} aria-label={t('ws.maxMutants.aria')} /> | |
| </span> | |
| </label> | |
| <label className="field" title={t('ws.mutRound.hint')}>{t('ws.mutRound')} | |
| <span className="row" style={{ gap: 6, minHeight: 34, alignItems: 'center' }}> | |
| <input type="checkbox" checked={mutation && mutRound} disabled={!mutation} | |
| onChange={(e) => setMutRound(e.target.checked)} aria-label={t('ws.mutRound')} /> | |
| </span> | |
| </label> | |
| {running | |
| ? <button className="btn danger" onClick={stop}>{t('ws.stop')}</button> | |
| : <button className="btn primary" onClick={start} disabled={!code.trim()}>{t('ws.generate')}</button>} | |
| </div> | |
| {cloud && <div className="info-box small cloud-note">{t('ws.cloudNote', { p: PROVIDER_LABEL[prov] })}</div>} | |
| {err && <div className="error-box" style={{ margin: '0 14px 14px' }}>{err}</div>} | |
| {notice && !err && <div className="small muted" style={{ margin: '0 14px 10px' }}>{notice}</div>} | |
| {health && !health.ollama.ok && !cloud && ( | |
| <div className="warn-box" style={{ margin: '0 14px 14px' }}> | |
| {t('ws.ollamaDown', { url: health.ollama.url })} <code>ollama serve</code> · <code>ollama pull {health.default_model}</code> | |
| </div> | |
| )} | |
| </section> | |
| <AgentFlow events={events} job={job} running={running} /> | |
| <Results live={live} job={job} running={running} code={code} language={language} kind={kind} onJob={setJob} /> | |
| </main> | |
| ); | |
| } | |