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 = { 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('python'); const [kind, setKind] = useState('unit'); const [code, setCode] = useState(() => (lang === 'en' ? STARTER_EN : STARTER)); const [html, setHtml] = useState(''); const [samples, setSamples] = useState([]); // Ayarlar taşınabilir ayar dosyasında saklanır (ayarlar/testajani-ayarlar.json) const [prefModel, setPrefModel] = usePref('ws.model', ''); const [jobModel, setJobModel] = useState(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('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(null); const [job, setJob] = useState(null); const [events, setEvents] = useState([]); const [running, setRunning] = useState(false); const [err, setErr] = useState(null); const [notice, setNotice] = useState(null); const [dragging, setDragging] = useState(false); const unsub = useRef void)>(null); const fileInput = useRef(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(); 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*(

{t('ws.code')}

{LANGS.map((l) => ( ))}
{ if (e.target.files) loadFiles(e.target.files); e.target.value = ''; }} />
{project && helperInfo && (
{t('ws.project')} {t('ws.project.helpers', { n: Object.keys(helperInfo.extra).length })} {helperInfo.ignored.length > 0 && ` · ${t('ws.project.ignored', { n: helperInfo.ignored.length })}`}
)}
{t('ws.testKind')}
{KINDS.map((k) => ( ))}
{t(`kind.${kind}.hint` as MsgKey)}
{ 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 &&
{t('ws.drop')}
} 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 } }} />
{kind === 'ui' && codeLang !== 'html' && (
{t('ws.html', { file: 'app.js' })}