<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#FEF9C3"><meta name="color-scheme" content="light"><title>Soru Hazırlama Asistanı</title>
<style>
:root{--yellow:#fef9c3;--ink:#1f2937;--muted:#64748b;--line:#e2e8f0;--green:#dcfce7;--greenb:#86efac;--red:#ffe4e6;--redb:#fda4af}*{box-sizing:border-box}body{margin:0;background:#f8fafc;color:var(--ink);font:15px "Segoe UI",Arial,sans-serif}.head{background:var(--yellow);padding:16px;display:flex;align-items:center;gap:12px;box-shadow:0 2px 8px #0001}.appicon{width:62px;height:62px;background:var(--yellow);border:0;border-radius:14px;display:grid;place-items:center;font-size:19px;font-weight:900;letter-spacing:-.8px;white-space:nowrap;color:#111827;flex-shrink:0}.head h1{font-size:20px;margin:0}.head[role=button]{cursor:pointer}.head[role=button]:focus-visible{outline:3px solid #0284c7;outline-offset:-3px}.quick-card{border-color:#fde68a}.quick-saved-row{display:flex;gap:7px;align-items:center;border-top:1px solid #e5e7eb;padding:8px 0}.quick-saved-row:first-child{border-top:0}.quick-saved-row .open-saved{flex:1;text-align:left;justify-content:flex-start;display:flex;flex-direction:column;align-items:flex-start;min-width:0;overflow-wrap:anywhere}.quick-saved-row .quick-delete{flex:0 0 auto}.quick-saved-row .open-saved.active{background:#dcfce7;border-color:#86efac}.quick-card h2{margin-bottom:7px}.head p{margin:4px 0 0;font-size:12px}.wrap{width:100%;max-width:none;margin:0;padding:14px}.card{background:white;border:1px solid var(--line);border-radius:12px;padding:15px;margin-bottom:13px;box-shadow:0 3px 12px #0f172a08}.card h2{font-size:17px;margin:0 0 12px}.row{display:flex;gap:9px;flex-wrap:wrap;align-items:center}.field{display:flex;flex-direction:column;gap:5px;flex:1;min-width:180px}.field label{font-size:12px;font-weight:700;color:#475569}input[type=text],select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:11px;font:inherit;background:white}.btn,.pick{border:1px solid #cbd5e1;background:white;color:#1f2937;border-radius:8px;padding:10px 13px;font:600 14px inherit;display:inline-flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;min-height:42px}.btn.primary{background:#1f2937;color:white;border-color:#1f2937}.btn.yellow{background:#fef08a;border-color:#eab308}.btn.start-test{background:#dcfce7;border-color:#86efac;color:#14532d}.btn.finish-test{background:#fef9c3;border-color:#fde047;color:#713f12}.btn.answers-test{background:#dbeafe;border-color:#93c5fd;color:#1e3a8a}.letter-input{width:68px!important;min-width:68px;text-align:center;font-size:19px!important;font-weight:800!important;text-transform:uppercase}.matching-row{padding:11px 0;border-bottom:1px solid #e2e8f0}.matching-row:last-child{border-bottom:0}.matching-fields{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:8px}.matching-fields .btn{min-width:94px}.status.success{background:#f0fdf4;border:1px solid #86efac}.missing-choice{padding:9px;margin-top:8px;border:1px dashed #f59e0b;border-radius:8px;background:#fffbeb}.btn:disabled{opacity:.45;cursor:not-allowed}.hint{font-size:12px;color:var(--muted);line-height:1.5}.hidden{display:none!important}.pick-label{position:relative;overflow:hidden}.native-picker{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;z-index:2}.status{padding:10px;border-radius:8px;background:#f1f5f9;font-size:13px;margin-top:10px;white-space:pre-wrap}.saved{display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--line);padding:10px 0}.saved:last-child{border-bottom:0}.saved .name{flex:1;min-width:0;overflow-wrap:anywhere}.q{border:1px solid var(--line);border-radius:10px;padding:13px;margin:12px 0;background:white}.qtitle{display:flex;gap:9px;align-items:flex-start;font-weight:700;line-height:1.5}.num{background:#fef08a;border-radius:50%;min-width:29px;height:29px;display:grid;place-items:center}.opts{display:grid;gap:7px;margin:12px 0 0 38px}.opt{display:flex;gap:8px;align-items:flex-start;padding:10px;border:1px solid #dbe3ea;border-radius:8px;line-height:1.4}.opt input{margin-top:3px}.opt.chosen{background:#eff6ff;border-color:#60a5fa}.clear-answer-btn{background:#fff7ed;border-color:#fdba74;color:#9a3412;margin:8px 0 0 38px;padding:7px 11px;min-height:36px;font-size:12px}.opt.correct{background:var(--green);border-color:var(--greenb)}.opt.wrong{background:var(--red);border-color:var(--redb)}.answer{margin:10px 0 0 38px;background:var(--green);border:1px solid var(--greenb);padding:10px;border-radius:8px;line-height:1.5}.answer.wrong{background:var(--red);border-color:var(--redb)}.answer.unmatched{background:#fff7ed;border-color:#fdba74}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.metric{background:#f8fafc;border:1px solid var(--line);border-radius:8px;text-align:center;padding:10px}.metric b{display:block;font-size:20px}.metric span{font-size:11px;color:var(--muted)}progress{width:100%;height:12px;accent-color:#eab308}canvas{max-width:100%;height:150px}.filelist{font-size:12px;color:#475569;margin-top:7px;overflow-wrap:anywhere}@media(max-width:540px){.wrap{padding:9px}.head{padding:12px}.head h1{font-size:18px}.metrics{grid-template-columns:repeat(2,1fr)}.field{min-width:100%}.btn,.pick{flex:1}.q{padding:10px}.opts{margin-left:0}.answer{margin-left:0}}
/* EKRAN GENİŞLİĞİNE UYARLANAN TAM GENİŞLİK DÜZENİ
Önceki maksimum 980px sınırı kaldırıldı. Masaüstü, tablet ve telefonlar. */
html { width: 100%; min-width: 0; }
body { width: 100%; min-width: 0; margin: 0; }
.head { width: 100%; min-width: 0; padding: clamp(10px, 1.3vw, 20px); }
.head > div:last-child { flex: 1 1 auto; min-width: 0; }
.head h1, .head p { overflow-wrap: anywhere; }
.wrap { width: 100%; max-width: none; min-width: 0; margin: 0; padding: clamp(8px, 1.2vw, 20px); }
.wrap > .card { width: 100%; max-width: none; min-width: 0; }
.card { min-width: 0; padding: clamp(10px, 1.15vw, 20px); }
.row, .matching-fields { max-width: 100%; min-width: 0; }
.row > *, .matching-fields > * { max-width: 100%; }
.field { min-width: min(180px, 100%); }
.field input, .field select, .matching-fields input { min-width: 0; }
.btn, .pick { white-space: normal; text-align: center; overflow-wrap: anywhere; }
.pick-label { flex: 1 1 210px; }
.test-actions > .btn { flex: 1 1 180px; min-width: 0; }
.q, .qtitle, .opt, .opt span, .status, .hint, .matching-row,
.mapping-roster, .mapping-line, .quick-card { min-width: 0; overflow-wrap: anywhere; }
.opts { grid-template-columns: minmax(0, 1fr); }
.quick-saved-row { min-width: 0; }
.quick-saved-row .open-saved { min-width: 0; }
.quick-saved-row .open-saved small { overflow-wrap: anywhere; }
img, video, canvas, svg, table { max-width: 100%; }
canvas { display: block; }
@media (max-width: 768px) {
.wrap { padding: 9px; }
.head { padding: 11px; }
.card { padding: 11px; }
.field { flex: 1 1 100%; min-width: 0; }
.row .pick-label { flex: 1 1 calc(50% - 9px); min-width: 0; }
.test-actions > .btn { flex: 1 1 160px; }
.matching-fields > input[type="text"]:not(.letter-input) { flex: 1 1 150px; }
.clear-answer-btn, .answer, .opts { margin-left: 0; }
}
@media (max-width: 440px) {
.head h1 { font-size: 17px; }
.appicon { width: 55px; height: 55px; font-size: 17px; }
.quick-saved-row { flex-wrap: wrap; }
.quick-saved-row .open-saved { flex: 1 1 calc(100% - 66px); }
.quick-saved-row .quick-delete { flex: 0 0 auto; }
.row .pick-label { flex: 1 1 100%; }
.test-actions > .btn { flex: 1 1 100%; }
.row > button:not(.quick-delete) { max-width: 100%; }
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.q { padding: 10px; }
.opt { padding: 10px 8px; }
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.8.0/mammoth.browser.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
</head><body>
<header class="head" id="homeHeader" role="button" tabindex="0" aria-label="Yeni test için başlangıç ekranına dön"><div class="appicon">Soru</div><div><h1>Soru Hazırlama Asistanı</h1><p>Dosya ve fotoğraflardan test oluşturma</p></div></header>
<main class="wrap">
<section class="card" id="recordCard"><h2>1. Test kaydı</h2><div class="row hidden" id="recordEditRow"><div class="field"><label>Test adı</label><input id="testName" type="text" placeholder="Örn. İş Güvenliği Deneme 1"></div><button class="btn yellow" id="saveBtn">Kaydet</button></div><div class="row hidden" id="legacySavedControls" style="margin-top:10px"><div class="field"><label>Kayıtlı testler</label><select id="savedSelect"><option value="">Kayıt seçin</option></select></div><button class="btn" id="loadBtn">Aç</button><button class="btn" id="deleteBtn">Sil</button></div><p class="hint hidden" id="legacySavedHint">Kayıtlar bu cihazın uygulama verilerinde saklanır; uygulama verilerini temizlemek kayıtları da silebilir.</p><div id="savedList" class="hidden"></div></section>
<section class="card" id="uploadCard"><h2>2. Soruları ve cevapları yükle</h2><div class="row"><label class="btn yellow pick-label">📄 Soru dosyası<input class="native-picker" id="filesInput" type="file" multiple accept=".pdf,.txt,.docx,.xlsx"></label><label class="btn yellow pick-label">📄 Cevap dosyası<input class="native-picker" id="answerFilesInput" type="file" multiple accept=".pdf,.txt,.docx,.xlsx"></label><label class="btn yellow pick-label">🖼️ Soru fotoğrafları<input class="native-picker" id="questionPhotos" type="file" multiple accept="image/*"></label><label class="btn yellow pick-label">🖼️ Cevap fotoğrafları<input class="native-picker" id="answerPhotos" type="file" multiple accept="image/*"></label></div><div class="filelist" id="fileList">Henüz dosya veya fotoğraf eklenmedi.</div><div class="row" style="margin-top:12px"><button class="btn primary" id="extractBtn">Metinleri oku ve eşleştir</button><button class="btn" id="clearUploadBtn">Yüklemeleri temizle</button></div><div id="manualAnswers" class="status hidden"></div><div id="matchingSummary" class="status success hidden"><b id="matchingCount"></b> Soruların doğru şıkları tamamlandı. Testi kaydedebilir veya başlatabilirsiniz. <button type="button" class="btn" id="editMatchingBtn">Şık Harflerini Düzenle</button></div><div class="status" id="importStatus">Dosyaları yükleyin. Fotoğraflar OCR ile okunur; cevap anahtarı ayrı dosya veya fotoğraf olarak eklenebilir.</div><p class="hint">Desteklenen dosyalar: PDF, TXT, DOCX, XLSX. Birden fazla fotoğraf seçilebilir. OCR sonucu ve cevap eşleşmeleri doğruluk açısından kontrol edilmelidir.</p></section>
<section class="card"><h2>3. Test</h2><div class="row test-actions"><button class="btn start-test" id="startBtn" disabled>Test Başla</button><button class="btn finish-test" id="finishBtn" disabled>Test Bitir</button><button class="btn answers-test" id="toggleAnswersBtn" disabled>Cevapları Göster</button></div><p class="hint" id="choiceHint">Test sırasında bir şık seçin. İşaretlenen şıkka tekrar dokunursanız seçim kaldırılır; farklı bir şık seçerseniz cevap değiştirilir. İsterseniz “Seçimi Kaldır” düğmesini de kullanabilirsiniz.</p><div style="margin:12px 0"><div class="row" style="justify-content:space-between"><b id="progressLabel">İlerleme: 0 / 0</b><b id="timer">Süre: 00:00</b></div><progress id="progress" value="0" max="100"></progress></div><div id="metrics" class="metrics hidden"><div class="metric"><b id="correctCount">0</b><span>Doğru</span></div><div class="metric"><b id="wrongCount">0</b><span>Yanlış</span></div><div class="metric"><b id="rate">0%</b><span>Başarı oranı</span></div><div class="metric"><b id="unanswered">0</b><span>Cevapsız</span></div></div><canvas id="chart" class="hidden" width="640" height="180"></canvas><div id="questions"><p class="hint">Önce dosya veya fotoğraf ekleyip metinleri okuyun.</p></div></section>
</main>
<script>
'use strict';
const $=id=>document.getElementById(id);
const data={questionFiles:[],answerFiles:[],questions:[],answersVisible:false,running:false,finished:false,startAt:0,timerId:null,sourceNames:[],currentSavedName:'',editingAnswers:false,manualOptionFor:null,pendingLetters:{}};
const DB='soruAsistaniRecordsV2';
function notify(m){$('importStatus').textContent=m}
// Keep the app's light corporate palette even when Android uses a dark system theme.
try{document.documentElement.style.colorScheme='light'}catch(e){}
function readRecords(){try{return JSON.parse(localStorage.getItem(DB)||'[]')}catch{return []}}
function writeRecords(a){localStorage.setItem(DB,JSON.stringify(a));renderSaved()}
function renderSaved(){const records=readRecords();$('savedSelect').innerHTML='<option value="">Kayıt seçin</option>'+records.map((r,i)=>'<option value="'+i+'">'+esc(r.name)+'</option>').join('');$('savedList').innerHTML=records.map((r,i)=>'<div class="saved"><span class="name">'+esc(r.name)+'</span><span class="hint">'+(r.questions?.length||0)+' soru</span><button class="btn" data-open="'+i+'">Aç</button><button class="btn" data-del="'+i+'">Sil</button></div>').join('');$('savedList').querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>loadRecord(+b.dataset.open));$('savedList').querySelectorAll('[data-del]').forEach(b=>b.onclick=()=>deleteRecord(+b.dataset.del))}
function esc(s=''){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
function filesText(){const all=[...data.questionFiles,...data.answerFiles];$('fileList').textContent=all.length?all.map(f=>(data.answerFiles.includes(f)?'Cevap: ':'Soru: ')+f.name).join('\n'):'Henüz dosya veya fotoğraf eklenmedi.'}
$('filesInput').onchange=e=>{data.questionFiles.push(...e.target.files);data.sourceNames.push(...[...e.target.files].map(f=>f.name));filesText();e.target.value=''};
$('answerFilesInput').onchange=e=>{data.answerFiles.push(...e.target.files);data.sourceNames.push(...[...e.target.files].map(f=>f.name));filesText();e.target.value=''};
$('questionPhotos').onchange=e=>{data.questionFiles.push(...e.target.files);data.sourceNames.push(...[...e.target.files].map(f=>f.name));filesText();e.target.value=''};
$('answerPhotos').onchange=e=>{data.answerFiles.push(...e.target.files);data.sourceNames.push(...[...e.target.files].map(f=>f.name));filesText();e.target.value=''};
$('clearUploadBtn').onclick=()=>{data.questionFiles=[];data.answerFiles=[];data.sourceNames=[];filesText();notify('Yükleme listesi temizlendi.')};
async function extractFile(file){const ext=file.name.split('.').pop().toLowerCase();if(ext==='txt')return await file.text();if(ext==='pdf'){if(!window.pdfjsLib)throw Error('PDF okuyucu yüklenemedi.');pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';const pdf=await pdfjsLib.getDocument({data:await file.arrayBuffer()}).promise;let out='';for(let i=1;i<=pdf.numPages;i++){const p=await pdf.getPage(i);const c=await p.getTextContent();let line='',prevY=null;for(const item of c.items){const y=Math.round((item.transform&&item.transform[5])||0);if(prevY!==null&&Math.abs(y-prevY)>3){out+=line.trim()+'\n';line=''}line+=item.str+' ';prevY=y}out+=line.trim()+'\n'}return out}if(ext==='docx'){if(!window.mammoth)throw Error('Word okuyucu yüklenemedi.');return (await mammoth.extractRawText({arrayBuffer:await file.arrayBuffer()})).value}if(ext==='xlsx'){if(!window.XLSX)throw Error('Excel okuyucu yüklenemedi.');const w=XLSX.read(await file.arrayBuffer(),{type:'array'});return w.SheetNames.map(n=>XLSX.utils.sheet_to_csv(w.Sheets[n])).join('\n')}throw Error('Desteklenmeyen dosya: '+file.name)}
async function ocrFile(file){if(!window.Tesseract)throw Error('OCR bileşeni yüklenemedi. İnternet bağlantısını kontrol edin.');const result=await Tesseract.recognize(file,'tur+eng',{logger:m=>{if(m.status==='recognizing text')notify('Fotoğraf okunuyor: '+file.name+' · %'+Math.round((m.progress||0)*100))}});return result.data.text}
$('extractBtn').onclick=async()=>{
if(!data.questionFiles.length){notify('Önce soru dosyası veya fotoğrafı yükleyin.');return}
const qtexts=[],atexts=[];
try{
let n=0,total=data.questionFiles.length+data.answerFiles.length;
for(const f of data.questionFiles){
n++;notify('Soru kaynağı okunuyor ('+n+'/'+total+'): '+f.name);
qtexts.push(await (/^image\//.test(f.type)?ocrFile(f):extractFile(f)))
}
for(const f of data.answerFiles){
n++;notify('Cevap kaynağı okunuyor ('+n+'/'+total+'): '+f.name);
atexts.push(await (/^image\//.test(f.type)?ocrFile(f):extractFile(f)))
}
data.questions=parseQuestions(qtexts.join('\n'),atexts.join('\n'));
if(!data.questions.length){notify('Soru yapısı tanınamadı. Soruların 1. ..., şıkların A) ... biçiminde olduğunu kontrol edin.');return}
clearInterval(data.timerId);data.running=false;data.finished=false;data.answersVisible=false;
data.editingAnswers=false;data.manualOptionFor=null;data.pendingLetters={};data.currentSavedName='';
$('testName').value='';$('timer').textContent='Süre: 00:00';
$('recordCard').classList.remove('hidden');$('uploadCard').classList.remove('hidden');
$('metrics').classList.add('hidden');$('chart').classList.add('hidden');
$('toggleAnswersBtn').textContent='Cevapları Göster';
renderQuestions();renderManualAnswers();
notify(allAnswersMatched()?'Tüm cevaplar eşleştirildi. Test adını girip kaydedebilir veya testi başlatabilirsiniz.':data.questions.length+' soru okundu. Eksik doğru cevapların harflerini A–E olarak yazıp Eşleştir düğmesine basın.');
}catch(e){notify('İçe aktarma hatası: '+e.message)}
};
function normalizeQuestionChoices(q){
const options=Array.isArray(q.options)?q.options:[];
const letters=Array.isArray(q.letters)?q.letters:[];
const oldCorrect=Number.isInteger(q.correct)?q.correct:null;
const wantedLetter=String(q.correctLetter||((oldCorrect!==null?letters[oldCorrect]:null)||'')).trim().toUpperCase();
const seenLetters=new Set(),seenTexts=new Set(),unique=[];
for(let i=0;i<options.length;i++){
const letter=String(letters[i]||String.fromCharCode(65+i)).trim().toUpperCase();
const value=String(options[i]||'').replace(/\s+/g,' ').trim();
const textKey=value.toLocaleLowerCase('tr-TR');
if(!/^[A-E]$/.test(letter)||!value||seenLetters.has(letter)||seenTexts.has(textKey))continue;
seenLetters.add(letter);seenTexts.add(textKey);unique.push({letter,text:value})
}
const correct=unique.findIndex(x=>x.letter===wantedLetter);
return {...q,letters:unique.map(x=>x.letter),options:unique.map(x=>x.text),correct:correct>=0?correct:null,correctLetter:correct>=0?wantedLetter:null,selected:null}
}
function parseQuestions(text,answerText){
const answerMap={},sequential=[];
const answerLines=(String(answerText||'')+'\n'+String(text||'')).replace(/\r/g,'').split('\n');
for(const line of answerLines){
let m;
const pairRe=/(?:^|[\s,;|])(?:Soru\s*)?(\d{1,4})\s*[.)\-::]\s*([A-E])(?=\s|$|[,;|])/gi;
while((m=pairRe.exec(line)))answerMap[+m[1]]=m[2].toUpperCase();
const spacedRe=/(?:^|[\s,;|])(\d{1,4})\s+([A-E])(?=\s|$|[,;|])/gi;
while((m=spacedRe.exec(line)))answerMap[+m[1]]=m[2].toUpperCase();
const standalone=line.match(/^\s*([A-E])\s*[.)]?\s*$/i);
}
sequential.push(...String(answerText||'').replace(/\r/g,'').split('\n').map(x=>x.trim()).filter(x=>/^[A-E][.)]?$/i.test(x)).map(x=>x[0].toUpperCase()));
const lines=String(text||'').replace(/\r/g,'').split('\n').map(s=>s.trim()).filter(Boolean);
const blocks=[];let cur=null;
for(const line of lines){
if(/^(cevap\s*anahtar[ıi]|answer\s*key)\s*:?/i.test(line)) {if(cur)blocks.push(cur);cur=null;break}
if(/^\d{1,4}\s*[-:]\s*[A-E]\s*$/i.test(line))continue;
const m=line.match(/^\s*(?:(?:Soru)\s*)?(\d{1,4})\s*[.)\-:]\s*(.*)$/i);
if(m){if(cur)blocks.push(cur);cur={n:+m[1],lines:[m[2]]};continue}
if(cur)cur.lines.push(line)
}
if(cur)blocks.push(cur);
const parsed=[];
for(const b of blocks){
const opts=[],stem=[];
for(const line of b.lines){
const marks=[...line.matchAll(/(?:^|\s)([A-E])\s*[).:-]\s*/gi)];
if(/^\s*A\s*[).:-]/i.test(line)&&marks.length>1){
for(let k=0;k<marks.length;k++){const mark=marks[k],start=mark.index+mark[0].length,end=k+1<marks.length?marks[k+1].index:line.length;opts.push({letter:mark[1].toUpperCase(),text:line.slice(start,end).trim()})}
}else{
const m=line.match(/^\s*([A-E])\s*[).:-]\s*(.*)$/i);
if(m)opts.push({letter:m[1].toUpperCase(),text:m[2]});else stem.push(line)
}
}
const q=stem.join(' ').trim();if(q.length<2)continue;
parsed.push({n:b.n,text:q,options:opts.map(o=>o.text),letters:opts.map(o=>o.letter),correctLetter:answerMap[b.n]||null,selected:null})
}
if(sequential.length===parsed.length)parsed.forEach((q,i)=>{if(!q.correctLetter)q.correctLetter=sequential[i]});
return parsed.map(q=>normalizeQuestionChoices(q))
}
function isMatched(q){
return !!q && Array.isArray(q.options) && Array.isArray(q.letters) && q.options.length>=2 &&
Number.isInteger(q.correct) && q.correct>=0 && q.correct<q.options.length && !!String(q.options[q.correct]||'').trim() &&
String(q.letters[q.correct]||'').toUpperCase()===String(q.correctLetter||'').toUpperCase();
}
function allAnswersMatched(){return data.questions.length>0&&data.questions.every(isMatched)}
function updateTestControls(){
const ready=allAnswersMatched();
$('startBtn').disabled=!ready||data.running;
$('finishBtn').disabled=!data.running;
$('toggleAnswersBtn').disabled=!ready||data.running;
$('recordEditRow').classList.toggle('hidden',!ready||data.running||data.editingAnswers);
}
function renderManualAnswers(){
const box=$('manualAnswers'),summary=$('matchingSummary');
const total=data.questions.length,missing=data.questions.map((q,i)=>({q,i})).filter(x=>!isMatched(x.q));
if(!total||data.running){box.classList.add('hidden');summary.classList.add('hidden');updateTestControls();return}
if(!missing.length&&!data.editingAnswers){
box.classList.add('hidden');summary.classList.remove('hidden');
$('matchingCount').textContent=total+' / '+total+' eşleşme tamam.';
updateTestControls();return
}
summary.classList.add('hidden');box.classList.remove('hidden');
const rows=(data.editingAnswers?data.questions.map((q,i)=>({q,i})):missing);
box.innerHTML='<b>Doğru şık harflerini elle girin</b><p>Menüden cevap seçmeniz gerekmez. Eksik her sorunun kutusuna A, B, C, D veya E yazın ve <b>Eşleştir</b> düğmesine basın.</p>'+
rows.map(({q,i})=>{
const letters=q.letters.map(x=>String(x).toUpperCase()).join(', ')||'Şık okunamadı';
const needsChoice=q.options.length<2||data.manualOptionFor===i;
const prefill=esc(data.pendingLetters[i]||q.correctLetter||'');
return '<div class="matching-row"><b>'+esc(String(q.n||i+1))+'. '+esc(q.text.slice(0,150))+'</b><div class="hint">Okunan şıklar: '+esc(letters)+'</div>'+
'<div class="matching-fields"><label for="letter-'+i+'">Doğru harf:</label><input id="letter-'+i+'" class="letter-input" type="text" maxlength="1" autocapitalize="characters" autocomplete="off" inputmode="text" data-letter="'+i+'" value="'+prefill+'" placeholder="A-E"><button type="button" class="btn yellow" data-apply-letter="'+i+'">Eşleştir</button><span class="hint">'+(isMatched(q)?'Eşleşti':'Eşleşmedi')+'</span></div>'+
(needsChoice?'<div class="missing-choice"><b>Eksik soru şıkkını elle tamamla</b><p class="hint">Kaynakta seçenek okunmadıysa harfini ve metnini aşağıya girin.</p><div class="matching-fields"><input class="letter-input" type="text" maxlength="1" autocomplete="off" data-extra-letter="'+i+'" value="'+esc(data.pendingLetters[i]||'')+'" placeholder="A-E" aria-label="Eksik şık harfi"><input type="text" data-extra-text="'+i+'" placeholder="Şık metnini girin" style="flex:1;min-width:150px"><button type="button" class="btn" data-add-extra="'+i+'">Şıkkı Ekle</button></div></div>':'')+
'</div>';
}).join('')+
'<p><b>'+(total-missing.length)+' / '+total+'</b> cevap eşleştirildi.'+(missing.length?' Kalan '+missing.length+' soru için şık harfi girin.':'')+'</p>'+
(data.editingAnswers?'<button type="button" class="btn start-test" id="confirmMatchingBtn">Eşleştirmeyi Tamamla</button>':'');
box.querySelectorAll('[data-apply-letter]').forEach(btn=>btn.onclick=()=>{
const i=+btn.dataset.applyLetter,q=data.questions[i];
const letter=String(box.querySelector('[data-letter="'+i+'"]').value||'').trim().toUpperCase();
if(!/^[A-E]$/.test(letter)){notify('Sadece A, B, C, D veya E harflerinden birini girin.');return}
const index=q.letters.findIndex(x=>String(x).toUpperCase()===letter);
if(index<0){
data.manualOptionFor=i;data.pendingLetters[i]=letter;
notify('Girilen '+letter+' şıkkı soru seçeneklerinde bulunamadı. Aşağıdaki eksik şık alanına şık metnini ekleyin.');
renderManualAnswers();return
}
q.correct=index;q.correctLetter=letter;delete data.pendingLetters[i];data.manualOptionFor=null;
if(allAnswersMatched()&&!data.editingAnswers)notify('Tüm cevaplar eşleştirildi. Kaydetme alanı açıldı.');
renderManualAnswers();renderQuestions()
});
box.querySelectorAll('[data-letter]').forEach(inp=>inp.onkeydown=e=>{
if(e.key==='Enter'){e.preventDefault();box.querySelector('[data-apply-letter="'+inp.dataset.letter+'"]').click()}
});
box.querySelectorAll('[data-add-extra]').forEach(btn=>btn.onclick=()=>{
const i=+btn.dataset.addExtra,q=data.questions[i];
const letter=String(box.querySelector('[data-extra-letter="'+i+'"]').value||'').trim().toUpperCase();
const optionText=String(box.querySelector('[data-extra-text="'+i+'"]').value||'').trim();
if(!/^[A-E]$/.test(letter)||!optionText){notify('Eksik şıkkın A–E harfini ve metnini girin.');return}
if(q.options.length>=5){notify('Bir soruda en fazla 5 şık bulunabilir.');return}
let idx=q.letters.findIndex(x=>String(x).toUpperCase()===letter);
if(idx>=0){q.options[idx]=optionText}else{q.letters.push(letter);q.options.push(optionText);idx=q.options.length-1}
q.correct=idx;q.correctLetter=letter;data.manualOptionFor=null;delete data.pendingLetters[i];
if(allAnswersMatched()&&!data.editingAnswers)notify('Tüm cevaplar eşleştirildi. Kaydetme alanı açıldı.');
renderManualAnswers();renderQuestions()
});
const done=$('confirmMatchingBtn');
if(done)done.onclick=()=>{
if(!allAnswersMatched()){notify('Önce eksik soruların cevaplarını tamamlayın.');return}
data.editingAnswers=false;renderManualAnswers();notify('Eşleştirmeler tamamlandı. Kaydetme alanı açıldı.')
};
updateTestControls();
}
$('editMatchingBtn').onclick=()=>{if(data.running)return;data.editingAnswers=true;renderManualAnswers()};
function renderQuestions(){
const list=$('questions');
if(!data.questions.length){list.innerHTML='<p class="hint">Henüz soru yok.</p>';return}
list.innerHTML=data.questions.map((q,i)=>{
const opts=q.options.length?q.options.map((o,j)=>{
let cls='opt';
if(data.running&&q.selected===j)cls+=' chosen';
if(data.answersVisible&&q.correct===j)cls+=' correct';
if(data.answersVisible&&q.selected===j&&q.correct!==j)cls+=' wrong';
return '<label class="'+cls+'"><input type="radio" name="answer_question_'+i+'" data-question="'+i+'" value="'+j+'" '+(q.selected===j?'checked':'')+' '+(!data.running?'disabled':'')+'><span><b>'+esc(q.letters[j]||String.fromCharCode(65+j))+')</b> '+esc(o)+'</span></label>'
}).join(''):'<p class="hint">Bu soruda seçenekler tanınmadı. Kaynak metni kontrol edin.</p>';
let answerRow='';
if(data.answersVisible){
if(q.correct!==null){
const userLetter=q.selected===null?'Cevaplanmadı':(q.letters[q.selected]||String.fromCharCode(65+q.selected));
const isCorrect=q.selected===q.correct;
answerRow='<div class="answer '+(q.selected===null?'':isCorrect?'correct':'wrong')+'"><b>Doğru cevap:</b> '+esc(q.correctLetter||q.letters[q.correct]||String.fromCharCode(65+q.correct))+' <span> · </span><b>Senin cevabın:</b> '+esc(userLetter)+(q.selected===null?'':' — '+(isCorrect?'Doğru':'Yanlış'))+'</div>';
}else answerRow='<div class="answer unmatched">Cevap anahtarı bu soruyla eşleşmedi. Kaynak/cevap anahtarını kontrol edin.</div>';
}
const clearControl=data.running?'<button type="button" class="btn clear-answer-btn '+(q.selected===null?'hidden':'')+'" data-clear-answer="'+i+'" aria-label="'+(i+1)+'. sorunun seçimini kaldır">Seçimi Kaldır</button>':'';
return '<article class="q"><div class="qtitle"><span class="num">'+(i+1)+'</span><span>'+esc(q.text)+'</span></div><div class="opts">'+opts+'</div>'+clearControl+answerRow+'</article>'
}).join('');
function changeAnswer(i,choice){
if(!data.running)return;
const q=data.questions[i];
if(!q||!(choice===null||(Number.isInteger(choice)&&choice>=0&&choice<q.options.length)))return;
q.selected=choice;
list.querySelectorAll('input[name="answer_question_'+i+'"]').forEach(input=>{
const chosen=choice!==null&&Number(input.value)===choice;
input.checked=chosen;
input.closest('.opt')?.classList.toggle('chosen',chosen);
});
const clearButton=list.querySelector('[data-clear-answer="'+i+'"]');
if(clearButton)clearButton.classList.toggle('hidden',choice===null);
updateProgress();
}
list.querySelectorAll('input[type=radio]').forEach(inp=>inp.onclick=()=>{
if(!data.running)return;
const i=Number(inp.dataset.question),j=Number(inp.value);
const q=data.questions[i];
if(!q||!Number.isInteger(j)||j<0||j>=q.options.length)return;
// Clicking the selected option again cancels it; another option replaces it.
changeAnswer(i,q.selected===j?null:j)
});
list.querySelectorAll('[data-clear-answer]').forEach(btn=>btn.onclick=()=>changeAnswer(Number(btn.dataset.clearAnswer),null));
updateProgress()
}
function updateProgress(){const total=data.questions.length,answered=data.questions.filter(q=>q.selected!==null).length;$('progressLabel').textContent='İlerleme: '+answered+' / '+total;$('progress').value=total?answered/total*100:0}
$('startBtn').onclick=()=>{
if(data.running||!allAnswersMatched()){notify('Önce tüm soruların doğru şıklarını eşleştirin.');return}
clearInterval(data.timerId);data.questions.forEach(q=>q.selected=null);
data.running=true;data.finished=false;data.answersVisible=false;data.startAt=Date.now();
$('recordCard').classList.add('hidden');$('uploadCard').classList.add('hidden');
$('metrics').classList.add('hidden');$('chart').classList.add('hidden');
$('toggleAnswersBtn').textContent='Cevapları Göster';$('timer').textContent='Süre: 00:00';
data.timerId=setInterval(()=>{const t=Math.floor((Date.now()-data.startAt)/1000);$('timer').textContent='Süre: '+String(Math.floor(t/60)).padStart(2,'0')+':'+String(t%60).padStart(2,'0')},1000);
renderQuestions();updateTestControls();window.scrollTo({top:$('questions').offsetTop,behavior:'smooth'})
};
$('finishBtn').onclick=()=>{
if(!data.running)return;
data.running=false;data.finished=true;clearInterval(data.timerId);data.answersVisible=false;
$('recordCard').classList.remove('hidden');$('uploadCard').classList.remove('hidden');
$('toggleAnswersBtn').textContent='Cevapları Göster';
const correct=data.questions.filter(q=>q.selected===q.correct).length;
const wrong=data.questions.filter(q=>q.selected!==null&&q.selected!==q.correct).length;
const unanswered=data.questions.filter(q=>q.selected===null).length;
const rate=data.questions.length?Math.round(correct/data.questions.length*100):0;
$('correctCount').textContent=correct;$('wrongCount').textContent=wrong;$('rate').textContent=rate+'%';$('unanswered').textContent=unanswered;
$('metrics').classList.remove('hidden');drawChart(correct,wrong,unanswered);$('chart').classList.remove('hidden');
renderManualAnswers();renderQuestions();updateTestControls();
notify('Test bitti. Sonuçlar hazır. Doğru ve yanlış şıkları görmek için Cevapları Göster düğmesine basın.')
};
$('toggleAnswersBtn').onclick=()=>{
if(data.running||!allAnswersMatched()){notify('Cevapları görmek için eşleştirmeyi tamamlayın ve varsa testi bitirin.');return}
data.answersVisible=!data.answersVisible;
$('toggleAnswersBtn').textContent=data.answersVisible?'Cevapları Gizle':'Cevapları Göster';
renderQuestions()
};
function drawChart(c,w,u){const cv=$('chart'),ctx=cv.getContext('2d');ctx.clearRect(0,0,cv.width,cv.height);const values=[c,w,u],labels=['Doğru','Yanlış','Cevapsız'],colors=['#86efac','#fda4af','#cbd5e1'],max=Math.max(1,...values);values.forEach((v,i)=>{const x=60+i*190,h=v/max*110;ctx.fillStyle=colors[i];ctx.fillRect(x,130-h,90,h);ctx.fillStyle='#1f2937';ctx.font='bold 18px Arial';ctx.fillText(String(v),x+36,118-h);ctx.font='14px Arial';ctx.fillText(labels[i],x+8,155)});}
function currentRecord(){return {name:$('testName').value.trim(),questions:data.questions.map(normalizeQuestionChoices),sourceNames:[...data.sourceNames],updatedAt:new Date().toISOString()}}
function saveCurrent(show=true){if(data.running||data.editingAnswers){notify('Test veya eşleştirme düzenlemesi sırasında kayıt yapılamaz.');return}if(!allAnswersMatched()){notify('Kaydetmeden önce bütün soruları cevaplarıyla eşleştirin.');return}const name=$('testName').value.trim();if(!name){if(show)notify('Kaydetmek için test adı girin.');return}try{const records=readRecords(),r=currentRecord(),i=records.findIndex(x=>x.name.toLocaleLowerCase('tr-TR')===name.toLocaleLowerCase('tr-TR'));if(i>=0)records[i]=r;else records.unshift(r);data.currentSavedName=name;writeRecords(records);$('savedSelect').value=String(Math.max(0,records.findIndex(x=>x.name===name)));if(show)notify('“'+name+'” kaydedildi.')}catch(e){notify('Kayıt başarısız. Tarayıcı depolama sınırı dolmuş olabilir: '+e.message)}}
$('saveBtn').onclick=()=>saveCurrent(true);
function loadRecord(i){
if(data.running){notify('Testi bitirmeden başka kayıt açılamaz.');return}
const r=readRecords()[i];if(!r)return;
data.currentSavedName=r.name;$('testName').value=r.name;
data.questions=(r.questions||[]).map(normalizeQuestionChoices);
data.sourceNames=r.sourceNames||[];data.answersVisible=false;data.running=false;data.finished=false;
data.editingAnswers=false;data.manualOptionFor=null;data.pendingLetters={};
clearInterval(data.timerId);$('timer').textContent='Süre: 00:00';
$('recordCard').classList.remove('hidden');$('uploadCard').classList.remove('hidden');
$('metrics').classList.add('hidden');$('chart').classList.add('hidden');
$('toggleAnswersBtn').textContent='Cevapları Göster';
renderQuestions();renderManualAnswers();notify('“'+r.name+'” açıldı. Test Başla ile istediğiniz kadar yeniden çözebilirsiniz.')
}
$('loadBtn').onclick=()=>{if($('savedSelect').value===''){notify('Açmak için kayıt seçin.');return}loadRecord(+$('savedSelect').value)};
function deleteRecord(i){const records=readRecords();if(!records[i])return;const name=records[i].name;if(!confirm('“'+name+'” kaydı kalıcı olarak silinsin mi?'))return;records.splice(i,1);writeRecords(records);notify('Kayıt silindi.')}
$('deleteBtn').onclick=()=>{if($('savedSelect').value===''){notify('Silmek için kayıt seçin.');return}deleteRecord(+$('savedSelect').value)};
renderSaved();updateProgress();renderManualAnswers();
</script><script>
/* Android question/answer reliability layer - loaded AFTER original app script.
OCR matching coverage is not proof of semantic correctness: confirm against the source. */
(() => {
'use strict';
const el=id=>document.getElementById(id);
const safe=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const collapse=s=>String(s??'').normalize('NFKC').replace(/\u00a0/g,' ').replace(/[ \t]+/g,' ').trim();
const expectedLetters=n=>n===5?['A','B','C','D','E']:['A','B','C','D'];
function strictQuestion(raw) {
const q={...raw, options:[],letters:[],selected:null,correct:null,correctLetter:null,matchSource:null};
const rawOptions=Array.isArray(raw.options)?raw.options:[];
const rawLetters=Array.isArray(raw.letters)?raw.letters:[];
const count=rawOptions.length;
if(![4,5].includes(count))return {ok:false,reason:'Şık sayısı eksik veya fazla ('+count+'). Dört ya da beş şık gerekli.'};
const letters=rawLetters.map(x=>collapse(x).toUpperCase());
const values=rawOptions.map(collapse);
const expected=expectedLetters(count);
if(letters.join(',')!==expected.join(','))return {ok:false,reason:'Şık harfleri eksik, yinelenmiş veya sırasız.'};
if(values.some(x=>!x))return {ok:false,reason:'Boş şık bulundu.'};
if(new Set(values.map(x=>x.toLocaleLowerCase('tr-TR'))).size!==count)return {ok:false,reason:'Aynı şık metni birden fazla okunmuş.'};
if(values.some(x=>/(?:^|\s)[A-E]\s*[\)\].:]\s+\S/.test(x)))return {ok:false,reason:'İç içe geçmiş şık bulundu.'};
if(collapse(raw.text).length<4)return {ok:false,reason:'Soru metni okunamadı.'};
q.options=values; q.letters=letters; q.text=collapse(raw.text);
const preferred=collapse(raw.correctLetter||(
Number.isInteger(raw.correct)?letters[raw.correct]:''
)).toUpperCase();
const idx=letters.indexOf(preferred);
if(idx>=0){
q.correct=idx;q.correctLetter=preferred;
q.matchSource=raw.matchSource==='manual'?'manual':'auto';
}
return {ok:true,question:q};
}
function keyData(source) {
const raw=String(source||'').normalize('NFKC').replace(/\r/g,'\n');
const map=new Map(),conflicts=new Set();
const pair=/(?:^|[\s,;|])(?:Soru\s*)?(\d{1,4})\s*(?:[.)\-:]|\s)\s*([A-E])(?=$|[\s,;|.)])/gi;
let m;
while((m=pair.exec(raw))!==null){
const n=Number(m[1]),letter=m[2].toUpperCase();
if(map.has(n)&&map.get(n)!==letter)conflicts.add(n);
else map.set(n,letter);
}
for(const n of conflicts)map.delete(n);
const sequential=raw.split(/\n/).map(collapse).filter(x=>/^[A-E][.)]?$/.test(x)).map(x=>x[0]);
return {map,conflicts,sequential};
}
function parseReport(questionText,answerText) {
const raw=String(questionText||'').normalize('NFKC').replace(/\r/g,'\n').replace(/\u00a0/g,' ');
const keyMarker=/(?:^|\n)\s*(?:cevap\s*anahtar[ıi]|answer\s*key)\s*[:\-\n]/i;
const found=keyMarker.exec(raw);
const questionPart=found?raw.slice(0,found.index):raw;
const inlineKey=found?raw.slice(found.index):'';
const answerKeys=keyData(String(answerText||'')+'\n'+inlineKey);
const lines=questionPart.split(/\n/).map(collapse).filter(Boolean);
const blocks=[],skipped=[];
let current=null,unreadableFragments=0;
const push=()=>{if(current){blocks.push(current);current=null}};
for(const line of lines){
if(/^\d{1,4}\s*[.:\-]\s*[A-E]\s*$/i.test(line))continue;
const qm=/^(?:(?:Soru|S)\s*[:#]?\s*)?(\d{1,4})\s*[.)\-:]\s*(.*)$/i.exec(line);
if(qm){
push();
current={n:Number(qm[1]),lines:[qm[2]]};
}else if(current){current.lines.push(line)}
else if(/^(?:Soru|Question)\s*[:#]?\s*[^\d\s]/i.test(line))unreadableFragments++;
}
push();
if(!blocks.length && questionPart.trim())unreadableFragments=Math.max(1,unreadableFragments);
const questions=[],used=new Set(),usedNums=new Set();
for(const block of blocks){
const opts=[],stem=[];
let sawOptions=false;
for(const line of block.lines){
// A) ... B) ... on one OCR row, or one choice per row.
const markers=[...line.matchAll(/(?:^|[\s;|])([A-E])\s*[\)\].:]\s*/gi)];
if(!markers.length){
if(sawOptions && opts.length)opts[opts.length-1].text+=' '+line;
else stem.push(line);
continue;
}
const leading=line.slice(0,markers[0].index).trim();
if(leading){
if(sawOptions && opts.length)opts[opts.length-1].text+=' '+leading;
else stem.push(leading);
}
for(let i=0;i<markers.length;i++){
const marker=markers[i],end=i+1<markers.length?markers[i+1].index:line.length;
opts.push({letter:marker[1].toUpperCase(),text:line.slice(marker.index+marker[0].length,end).trim()});
}
sawOptions=true;
}
const source={n:block.n,text:stem.join(' ').trim(),letters:opts.map(x=>x.letter),options:opts.map(x=>x.text)};
if(usedNums.has(block.n)){
skipped.push({n:block.n,reason:'Aynı soru numarası birden çok kez okundu.'});
const previous=questions.findIndex(q=>q.n===block.n);
if(previous>=0)questions.splice(previous,1);
continue;
}
usedNums.add(block.n);
const auto=answerKeys.map.get(block.n) || (
answerKeys.sequential.length>=block.n ? answerKeys.sequential[block.n-1] : ''
);
source.correctLetter=answerKeys.conflicts.has(block.n)?null:(auto||null);
const validated=strictQuestion(source);
if(!validated.ok){skipped.push({n:block.n,reason:validated.reason});continue}
const q=validated.question;
if(used.has(block.n)){skipped.push({n:block.n,reason:'Çift soru numarası.'});continue}
questions.push(q);used.add(block.n);
}
// In case a duplicate number appears after a previously accepted question.
const blocked=new Set(skipped.filter(x=>x.reason.startsWith('Aynı soru numarası')).map(x=>x.n));
const valid=questions.filter(q=>!blocked.has(q.n));
if(unreadableFragments)skipped.push({n:null,reason:'Numarası okunamayan '+unreadableFragments+' soru/bölüm'});
return {questions:valid,skipped,detected:blocks.length+unreadableFragments,auto:valid.filter(q=>q.matchSource==='auto').length};
}
normalizeQuestionChoices=function(q){
const v=strictQuestion(q);
return v.ok?v.question:{...q,options:[],letters:[],correct:null,correctLetter:null,selected:null,invalidReason:v.reason}
};
isMatched=function(q){return strictQuestion(q).ok && Number.isInteger(q.correct) && q.letters[q.correct]===q.correctLetter};
allAnswersMatched=function(){return data.questions.length>0 && data.questions.every(isMatched)};
parseQuestions=function(qText,aText){
const result=parseReport(qText,aText);
data.skippedQuestions=result.skipped;
data.totalDetected=result.detected;
return result.questions;
};
if(!Array.isArray(data.skippedQuestions))data.skippedQuestions=[];
data.totalDetected=data.totalDetected||0;
data.editingAnswers=false;
// Guide the user through source upload, matching report, then the save panel.
const uploadCard=el('uploadCard');
const recordCard=el('recordCard');
uploadCard.insertAdjacentElement('afterend',recordCard);
uploadCard.querySelector('h2').textContent='1. Soruları ve cevapları yükle';
recordCard.querySelector('h2').textContent='2. Test kaydı';
const summary=el('matchingSummary');
const report=document.createElement('div');report.id='matchingReport';report.className='status hidden';
summary.insertAdjacentElement('afterend',report);
const skippedBox=document.createElement('div');skippedBox.id='skippedReport';skippedBox.className='status hidden';
report.insertAdjacentElement('afterend',skippedBox);
summary.innerHTML='<b id="matchingCount"></b> <span>Şık eşleştirmeleri tamamlandı. Test kayıt ekranı açıldı.</span>';
function renderReports(){
const total=data.questions.length;
const auto=data.questions.filter(q=>isMatched(q)&&q.matchSource!=='manual').length;
const manual=data.questions.filter(q=>isMatched(q)&&q.matchSource==='manual').length;
const missing=data.questions.filter(q=>!isMatched(q)).length;
const skipped=data.skippedQuestions||[];
const ready=total>0 && !missing;
summary.classList.toggle('hidden',!ready||data.running);
if(ready){
el('matchingCount').textContent='Eşleşme tamamlandı: '+auto+' otomatik + '+manual+' manuel = '+total+' / '+total+'.';
}
report.classList.toggle('hidden',!total||data.running);
report.innerHTML='<b>Eşleşme özeti:</b> Otomatik '+auto+' | Manuel '+manual+
' | Tamamlanan '+(auto+manual)+' / '+total+' | Bekleyen '+missing+
' | Pas geçilen '+skipped.length+
(ready?'<p><b>Tüm kabul edilen soruların cevap harfleri belirlendi.</b> Cevapların anlam bakımından doğruluğunu kaynak anahtarla karşılaştırın.</p>':'<p>Eksik cevaplar aşağıdan harf girilerek tamamlanmalıdır.</p>')+
'<details '+(ready?'open':'')+'><summary>Geçerli sorular ve cevapların tamamı ('+total+')</summary>'+
'<div class="mapping-roster">'+data.questions.map(q=>
'<div class="mapping-line">Soru '+safe(q.n)+': <b>'+safe(q.correctLetter||'Bekliyor')+
'</b> — '+safe(q.matchSource==='manual'?'Manuel':isMatched(q)?'Otomatik':'Eşleşmedi')+'</div>'
).join('')+'</div></details>';
skippedBox.classList.toggle('hidden',!skipped.length||data.running);
skippedBox.innerHTML=skipped.length?'<b>'+skipped.length+' soru/bölüm pas geçildi. Teste dahil edilmedi.</b>'+
'<div class="mapping-roster">'+skipped.map(x=>'<div class="mapping-line">'+
(x.n==null?'Numarası bilinmeyen bölüm':'Soru '+safe(x.n))+': '+safe(x.reason)+'</div>').join('')+'</div>':'';
el('recordCard').classList.toggle('hidden',!ready||data.running||!!data.fromSaved);
}
updateTestControls=function(){
const ready=allAnswersMatched();
el('startBtn').disabled=!ready||data.running;
el('finishBtn').disabled=!data.running;
el('toggleAnswersBtn').disabled=!data.finished||data.running;
el('recordEditRow').classList.toggle('hidden',!ready||data.running);
};
renderManualAnswers=function(){
const box=el('manualAnswers');
if(!data.questions.length||data.running){box.classList.add('hidden');renderReports();updateTestControls();return}
const missing=data.questions.map((q,i)=>({q,i})).filter(x=>!isMatched(x.q));
if(!missing.length){
box.classList.add('hidden');
renderReports();updateTestControls();
return;
}
box.classList.remove('hidden');
box.innerHTML='<b>Sadece eşleşmeyen cevaplar</b><p>Doğru şıkkın mevcut harfini yazın. Yalnızca o soruda bulunan A–D veya A–E harfleri kabul edilir; başka şık ya da metin eklenemez.</p>'+
missing.map(({q,i})=>{
const allowed=q.letters.join('');
return '<div class="matching-row"><b>Soru '+safe(q.n)+': '+safe(q.text.slice(0,140))+'</b>'+
'<p class="hint">Geçerli şık harfleri: '+safe(q.letters.join(', '))+'</p>'+
'<div class="matching-fields"><input type="text" class="letter-input" inputmode="text" autocapitalize="characters" autocomplete="off" maxlength="1" pattern="['+safe(allowed)+']" data-letter="'+i+'" aria-label="Soru '+safe(q.n)+' doğru şık harfi" placeholder="'+safe(allowed)+'">'+
'<button class="btn yellow" type="button" data-apply-letter="'+i+'">Eşleştir</button></div></div>'
}).join('')+'<p>Eksik kalan: <b>'+missing.length+'</b></p>';
box.querySelectorAll('[data-letter]').forEach(input=>{
const q=data.questions[Number(input.dataset.letter)];
input.addEventListener('input',()=>{
const valid=new Set(q.letters);
input.value=Array.from(input.value.toLocaleUpperCase('tr-TR')).find(c=>valid.has(c))||'';
});
input.addEventListener('keydown',e=>{
if(e.key==='Enter'){e.preventDefault();box.querySelector('[data-apply-letter="'+input.dataset.letter+'"]').click()}
});
});
box.querySelectorAll('[data-apply-letter]').forEach(button=>{
button.onclick=()=>{
const i=Number(button.dataset.applyLetter),q=data.questions[i];
const letter=collapse(box.querySelector('[data-letter="'+i+'"]').value).toUpperCase();
if(!/^[A-E]$/.test(letter)||!q.letters.includes(letter)){
notify('Geçersiz harf. Bu soru için yalnızca '+q.letters.join(', ')+' kullanılabilir.');
return;
}
q.correct=q.letters.indexOf(letter);
q.correctLetter=letter;q.matchSource='manual';
renderManualAnswers();renderQuestions();
if(allAnswersMatched()){
notify('Eşleşme tamamlandı: Otomatik ve manuel eşleşmeler birlikte listelendi. Test kayıt ekranı açıldı.');
el('recordCard').scrollIntoView?.({behavior:'smooth',block:'start'});
}
}
});
renderReports();updateTestControls();
};
el('extractBtn').onclick=async()=>{
if(!data.questionFiles.length){notify('Önce soru dosyası veya soru fotoğrafı ekleyin.');return}
const qt=[],at=[];
const btn=el('extractBtn');
btn.disabled=true;
try{
const files=[...data.questionFiles.map(f=>[f,qt,'Soru']),...data.answerFiles.map(f=>[f,at,'Cevap'])];
for(let i=0;i<files.length;i++){
const [file,output,label]=files[i];
notify((i+1)+'/'+files.length+' '+label+' kaynağı okunuyor: '+file.name);
output.push(await (/^image\//.test(file.type)?ocrFile(file):extractFile(file)));
}
const result=parseReport(qt.join('\n'),at.join('\n'));
clearInterval(data.timerId);
data.questions=result.questions;
data.skippedQuestions=result.skipped;data.totalDetected=result.detected;
data.answersVisible=false;data.running=false;data.finished=false;
data.currentSavedName='';data.editingAnswers=false;
el('testName').value='';el('timer').textContent='Süre: 00:00';
el('recordCard').classList.add('hidden');
el('metrics').classList.add('hidden');el('chart').classList.add('hidden');
el('toggleAnswersBtn').textContent='Cevapları Göster';
renderQuestions();renderManualAnswers();
const auto=result.auto;
notify(result.questions.length?
'Okuma tamamlandı. '+auto+' cevap otomatik eşleşti. '+(result.questions.length-auto)+' cevap manuel bekliyor. '+result.skipped.length+' soru/bölüm pas geçildi.':
'Geçerli soru bulunamadı. Eksik/okunamayan soru ve şıklar aşağıda raporlandı.');
}catch(error){notify('Okuma işlemi tamamlanamadı: '+error.message)}
finally{btn.disabled=false}
};
const previousLoad=loadRecord;
loadRecord=function(i){
if(data.running)return;
const record=readRecords()[i];if(!record)return;
const questions=[],skipped=Array.isArray(record.skippedQuestions)?record.skippedQuestions.map(item=>({n:item.n??null,reason:String(item.reason||'Okunamayan soru')})):[];
for(const item of (record.questions||[])){
const v=strictQuestion(item);
if(v.ok)questions.push(v.question);
else skipped.push({n:item.n||null,reason:'Kayıttaki eksik soru: '+v.reason});
}
data.questions=questions;data.skippedQuestions=skipped;
data.totalDetected=questions.length+skipped.length;
data.currentSavedName=record.name||'';
el('testName').value=data.currentSavedName;
data.sourceNames=record.sourceNames||[];
data.answersVisible=false;data.running=false;data.finished=false;data.editingAnswers=false;
clearInterval(data.timerId);
el('timer').textContent='Süre: 00:00';
el('recordCard').classList.remove('hidden');el('uploadCard').classList.remove('hidden');
el('metrics').classList.add('hidden');el('chart').classList.add('hidden');
el('toggleAnswersBtn').textContent='Cevapları Göster';
renderQuestions();renderManualAnswers();
notify(questions.length?'Kayıt açıldı. Test Başla ile istediğiniz kadar yeniden çözebilirsiniz.':'Bu kayıtta geçerli soru bulunamadı. Lütfen kaynağı yeniden okuyun.');
};
const previousCurrent=currentRecord;
currentRecord=function(){
const value=previousCurrent();
return {...value,questions:data.questions.map(q=>({...q,selected:null})),skippedQuestions:[...data.skippedQuestions],totalDetected:data.totalDetected};
};
el('startBtn').onclick=()=>{
if(data.running||!allAnswersMatched())return;
clearInterval(data.timerId);data.questions.forEach(q=>q.selected=null);
data.answersVisible=false;data.running=true;data.finished=false;data.startAt=Date.now();
el('recordCard').classList.add('hidden');el('uploadCard').classList.add('hidden');
el('metrics').classList.add('hidden');el('chart').classList.add('hidden');
el('timer').textContent='Süre: 00:00';el('toggleAnswersBtn').textContent='Cevapları Göster';
data.timerId=setInterval(()=>{
const seconds=Math.floor((Date.now()-data.startAt)/1000);
el('timer').textContent='Süre: '+String(Math.floor(seconds/60)).padStart(2,'0')+':'+String(seconds%60).padStart(2,'0');
},1000);
renderQuestions();updateTestControls();
window.scrollTo?.({top:el('questions').offsetTop,behavior:'smooth'});
};
el('finishBtn').onclick=()=>{
if(!data.running)return;
data.running=false;data.finished=true;clearInterval(data.timerId);data.answersVisible=false;
el('recordCard').classList.remove('hidden');el('uploadCard').classList.remove('hidden');
el('toggleAnswersBtn').textContent='Cevapları Göster';
const correct=data.questions.filter(q=>q.selected===q.correct).length;
const wrong=data.questions.filter(q=>q.selected!==null&&q.selected!==q.correct).length;
const unanswered=data.questions.filter(q=>q.selected===null).length;
el('correctCount').textContent=correct;el('wrongCount').textContent=wrong;el('unanswered').textContent=unanswered;
el('rate').textContent=Math.round(correct/data.questions.length*100)+'%';
el('metrics').classList.remove('hidden');drawChart(correct,wrong,unanswered);el('chart').classList.remove('hidden');
renderQuestions();renderManualAnswers();updateTestControls();
notify('Test tamamlandı. Cevapları Göster ile doğruları ve yanlışları inceleyebilir, Test Başla ile yeniden çözebilirsiniz.');
};
el('toggleAnswersBtn').onclick=()=>{
if(data.running||!data.finished)return;
data.answersVisible=!data.answersVisible;
el('toggleAnswersBtn').textContent=data.answersVisible?'Cevapları Gizle':'Cevapları Göster';
renderQuestions();
};
// Fresh-start and saved-test navigation. Keep the persistent storage when the title is tapped.
const quickCard=document.createElement('section');
quickCard.id='quickSavedCard';quickCard.className='card quick-card hidden';
quickCard.innerHTML='<h2>Kayıtlı Testler</h2><p class="hint">Bir testi seçtiğinizde sorular ve doğru cevap eşleşmeleri hazır açılır. Ardından Test Başla düğmesine dokunun.</p><div id="quickSavedList"></div>';
el('uploadCard').insertAdjacentElement('beforebegin',quickCard);
const oldRenderSaved=renderSaved;
function renderQuickSaved(){
const records=readRecords();
const visible=records.length>0&&!data.running;
quickCard.classList.toggle('hidden',!visible);
const list=el('quickSavedList');
list.innerHTML=records.map((record,i)=>{
const selected=data.fromSaved&&data.currentSavedName===record.name;
const count=Array.isArray(record.questions)?record.questions.length:0;
return '<div class="quick-saved-row"><button type="button" class="btn open-saved'+(selected?' active':'')+'" data-quick-open="'+i+'" aria-label="'+safe(record.name)+' testini aç"><b>'+safe(record.name)+'</b><small>'+count+' soru · '+(selected?'Test hazır':'Aç ve çöz')+'</small></button>'+
'<button type="button" class="btn quick-delete" data-quick-delete="'+i+'" aria-label="'+safe(record.name)+' kaydını sil">Sil</button></div>';
}).join('');
list.querySelectorAll('[data-quick-open]').forEach(button=>button.onclick=()=>{
loadRecord(Number(button.dataset.quickOpen));
if(allAnswersMatched())el('startBtn').scrollIntoView?.({block:'center',behavior:'smooth'});
});
list.querySelectorAll('[data-quick-delete]').forEach(button=>button.onclick=deleteRecord.bind(null,Number(button.dataset.quickDelete)));
}
renderSaved=function(){
oldRenderSaved();
renderQuickSaved();
};
function resetToHome(){
clearInterval(data.timerId);data.timerId=null;
data.questionFiles=[];data.answerFiles=[];data.sourceNames=[];
data.questions=[];data.skippedQuestions=[];data.totalDetected=0;
data.answersVisible=false;data.running=false;data.finished=false;
data.editingAnswers=false;data.manualOptionFor=null;data.pendingLetters={};
data.startAt=0;data.fromSaved=false;data.currentSavedName='';
['filesInput','answerFilesInput','questionPhotos','answerPhotos'].forEach(id=>{el(id).value=''});
el('testName').value='';el('savedSelect').value='';
filesText();
el('timer').textContent='Süre: 00:00';
el('progress').value=0;el('progressLabel').textContent='İlerleme: 0 / 0';
['correctCount','wrongCount','unanswered'].forEach(id=>{el(id).textContent='0'});
el('rate').textContent='0%';
el('metrics').classList.add('hidden');el('chart').classList.add('hidden');
el('uploadCard').classList.remove('hidden');
el('recordCard').classList.add('hidden');
el('toggleAnswersBtn').textContent='Cevapları Göster';
renderQuestions();
el('questions').innerHTML='<p class="hint">Önce soru ve cevap dosyası veya fotoğrafı ekleyin; ya da yukarıdaki kayıtlı testlerden birini seçin.</p>';
el('importStatus').textContent='Dosyaları yükleyin. Fotoğraflar OCR ile okunur; cevap anahtarı ayrı dosya veya fotoğraf olarak eklenebilir.';
renderManualAnswers();
renderQuickSaved();
window.scrollTo?.({top:0,behavior:'instant'});
}
const header=el('homeHeader');
header.onclick=()=>resetToHome();
header.onkeydown=event=>{
if(event.key==='Enter'||event.key===' '){event.preventDefault();resetToHome()}
};
const savedLoadRecord=loadRecord;
loadRecord=function(index){
if(data.running)return;
if(!readRecords()[index])return;
data.fromSaved=true;
data.questionFiles=[];data.answerFiles=[];
['filesInput','answerFilesInput','questionPhotos','answerPhotos'].forEach(id=>{el(id).value=''});
savedLoadRecord(index);
el('uploadCard').classList.add('hidden');
el('recordCard').classList.add('hidden');
renderQuickSaved();
if(!allAnswersMatched()){
// Older or corrupted records do not bypass validation.
el('uploadCard').classList.remove('hidden');
notify('Bu kayıt eşleştirme açısından eksik görünüyor. Teste başlamadan önce kaynağı yeniden yükleyin.');
}
};
deleteRecord=function(index){
const records=readRecords(),record=records[index];
if(!record)return;
if(!confirm('“'+record.name+'” kaydını kalıcı olarak silmek istediğinize emin misiniz?'))return;
const wasSelected=Boolean(data.currentSavedName)&&data.currentSavedName===record.name;
records.splice(index,1);
writeRecords(records);
if(wasSelected||records.length===0)resetToHome();
else renderQuickSaved();
if(records.length===0)notify('Son kayıt silindi. Yeni test oluşturmak için dosya veya fotoğraf yükleyin.');
else notify('“'+record.name+'” silindi. Diğer kayıtlı testler başlığın altında duruyor.');
};
const extractOriginal=el('extractBtn').onclick;
el('extractBtn').onclick=async()=>{
data.fromSaved=false;
el('uploadCard').classList.remove('hidden');
await extractOriginal();
renderQuickSaved();
};
const originalStart=el('startBtn').onclick;
el('startBtn').onclick=()=>{
originalStart();
renderQuickSaved();
};
const originalFinish=el('finishBtn').onclick;
el('finishBtn').onclick=()=>{
originalFinish();
if(data.fromSaved){
el('recordCard').classList.add('hidden');
el('uploadCard').classList.add('hidden');
}
renderQuickSaved();
};
window.__quizDiagnostics={parseReport,strictQuestion,keyData,resetToHome};
resetToHome();
})();
</script></body></html>