/* ── SPEAKING MODE: timed topic practice with beep ── */ const SPEAK_TOPICS = [ /* daily */ { cat:'daily', q:'Describe your typical morning routine on a workday.', hints:['What time do you wake up?','What do you eat or drink?','How do you get to work or university?'] }, { cat:'daily', q:'Talk about your favorite place in your city.', hints:['Where is it and what does it look like?','How often do you go there?','Why do you like it?'] }, { cat:'daily', q:'Describe a meal you know how to cook.', hints:['What ingredients do you need?','Explain the steps in order.','Who taught you this recipe?'] }, { cat:'daily', q:'Talk about how you spend your weekends.', hints:['Do you rest or stay busy?','Who do you spend time with?','What would a perfect weekend look like?'] }, { cat:'daily', q:'Describe a habit you want to build or break.', hints:['Why is it important to you?','What have you tried so far?','What makes it difficult?'] }, { cat:'daily', q:'Talk about a trip or journey you remember well.', hints:['Where did you go and with whom?','What happened there?','Would you go back? Why?'] }, /* work */ { cat:'work', q:'Explain your current project to someone who is not an engineer.', hints:['What problem does it solve?','Who will use it?','Avoid technical jargon.'] }, { cat:'work', q:'Describe a difficult problem you solved at work or in your studies.', hints:['What was the problem?','What steps did you take?','What did you learn from it?'] }, { cat:'work', q:'Talk about a colleague or teacher who influenced you.', hints:['How did you meet them?','What did they teach you?','How are you different because of them?'] }, { cat:'work', q:'Describe your ideal work environment.', hints:['Office, remote, or hybrid?','Quiet or collaborative?','What tools do you need?'] }, { cat:'work', q:'Explain how you organize your tasks and deadlines.', hints:['What tools or methods do you use?','How do you handle urgent requests?','What happens when you fall behind?'] }, { cat:'work', q:'Talk about a meeting that went badly and why.', hints:['What was the goal of the meeting?','What went wrong?','How could it have been better?'] }, /* engineering */ { cat:'engineering', q:'Explain a technical concept you know well, in simple English.', hints:['Start with why it matters.','Use an everyday comparison or analogy.','Give one concrete example.'] }, { cat:'engineering', q:'Describe how you would test a new design or system before release.', hints:['What could go wrong?','What would you measure?','When is it safe to ship?'] }, { cat:'engineering', q:'Talk about a technology that changed your field.', hints:['What did people do before it?','What does it make possible now?','Any downsides?'] }, { cat:'engineering', q:'Walk through how you debug a problem you have never seen before.', hints:['Where do you start looking?','How do you narrow it down?','When do you ask for help?'] }, { cat:'engineering', q:'Describe a tool you use every day and how it works.', hints:['What is it for?','Explain the main steps of using it.','What would you improve about it?'] }, /* opinions */ { cat:'opinion', q:'Should engineers learn skills outside engineering? Why or why not?', hints:['Give your opinion clearly first.','Support it with one or two reasons.','Mention the opposite view briefly.'] }, { cat:'opinion', q:'Is working from home better than working in an office?', hints:['State your position.','Compare focus, teamwork, and life balance.','What works best for you personally?'] }, { cat:'opinion', q:'Will AI change your job in the next five years? How?', hints:['What parts of your work could change?','What will stay human?','Are you optimistic or worried?'] }, { cat:'opinion', q:'Is it better to be a specialist or a generalist?', hints:['Define both in your own words.','Which one are you?','Which does the job market reward?'] }, { cat:'opinion', q:'Should university education be more practical and less theoretical?', hints:['What was your experience?','Give one example of each.','What balance would you choose?'] }, /* interview */ { cat:'interview', q:'Tell me about yourself. (classic interview opener)', hints:['Present: what you do now.','Past: one relevant achievement.','Future: why this role interests you.'] }, { cat:'interview', q:'What is your greatest strength, with an example?', hints:['Name one strength clearly.','Tell a short story that proves it.','Connect it to the job.'] }, { cat:'interview', q:'Describe a time you worked in a team under pressure.', hints:['Situation: what was happening?','Action: what did YOU do?','Result: how did it end?'] }, { cat:'interview', q:'Where do you see yourself in five years?', hints:['Be ambitious but realistic.','Mention skills you want to grow.','Link it to the company or field.'] }, { cat:'interview', q:'Why should we hire you instead of someone else?', hints:['Pick your 2 strongest points.','Give evidence, not adjectives.','End with enthusiasm.'] }, /* easy (A1–A2 friendly) */ { cat:'daily', q:'Describe your family.', hints:['How many people are in your family?','What do they do?','Who are you closest to?'] }, { cat:'daily', q:'Talk about your favorite food and drink.', hints:['What is it?','When do you eat or drink it?','Can you make it yourself?'] }, { cat:'daily', q:'Describe the weather today and your favorite season.', hints:['What is the weather like now?','Which season do you like best?','What do you do in that season?'] }, { cat:'daily', q:'Talk about your home or your room.', hints:['How many rooms are there?','What is in your favorite room?','What would you change?'] }, { cat:'work', q:'Describe your office, workshop, or classroom.', hints:['Where is it?','What tools or things are in it?','What do you do there every day?'] }, { cat:'work', q:'Talk about your first day at a job or university.', hints:['How did you feel?','Who did you meet?','What surprised you?'] }, /* B1 stretch */ { cat:'opinion', q:'Is it better to live in a big city or a small town?', hints:['State your choice first.','Compare cost, jobs, and quality of life.','Where do you want to live in ten years?'] }, { cat:'opinion', q:'Should young engineers work abroad for a few years?', hints:['What can you learn abroad?','What is difficult about it?','Would you do it — or have you?'] }, { cat:'work', q:'Explain a mistake you made and what you learned from it.', hints:['What happened?','How did you fix it?','What do you do differently now?'] }, { cat:'engineering', q:'Describe a project you are proud of, step by step.', hints:['What was the goal?','What was the hardest part?','What was the final result?'] }, { cat:'interview', q:'Describe a time you had to learn something quickly.', hints:['What did you need to learn, and why?','How did you learn it?','What was the result?'] }, ]; const SPEAK_CATS = [ { id:'all', emoji:'🎲', en:'Surprise me', ar:'فاجئني', tr:'Şaşırt beni' }, { id:'daily', emoji:'☀️', en:'Daily Life', ar:'الحياة اليومية', tr:'Günlük Hayat' }, { id:'work', emoji:'💼', en:'Workplace', ar:'العمل', tr:'İş Yeri' }, { id:'engineering', emoji:'🔧', en:'Engineering', ar:'الهندسة', tr:'Mühendislik' }, { id:'opinion', emoji:'💬', en:'Opinions', ar:'آراء', tr:'Görüşler' }, { id:'interview', emoji:'🤝', en:'Interview', ar:'مقابلة عمل', tr:'Mülakat' }, ]; const SPEAK_TIMES = [30, 60, 90, 120, 180]; const SPEAK_ROUNDS = [1, 3, 5]; const PREP_SECONDS = 15; const FREE_SPEAK_SESSIONS = 2; /* Web Audio beeps — no audio files needed */ function speakBeep(kind) { try { const Ctx = window.AudioContext || window.webkitAudioContext; const ctx = speakBeep._ctx || (speakBeep._ctx = new Ctx()); if (ctx.state === 'suspended') ctx.resume(); const tone = (freq, start, dur, vol) => { const o = ctx.createOscillator(), g = ctx.createGain(); o.type = 'sine'; o.frequency.value = freq; g.gain.setValueAtTime(0, ctx.currentTime + start); g.gain.linearRampToValueAtTime(vol, ctx.currentTime + start + 0.02); g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur); o.connect(g); g.connect(ctx.destination); o.start(ctx.currentTime + start); o.stop(ctx.currentTime + start + dur + 0.05); }; if (kind === 'tick') tone(880, 0, 0.12, 0.18); // last-3-seconds tick else if (kind === 'start') tone(660, 0, 0.18, 0.3); // go! else if (kind === 'end') { tone(880, 0, 0.3, 0.4); tone(660, 0.32, 0.45, 0.4); } // time's up else if (kind === 'done') { tone(660,0,0.15,0.3); tone(880,0.16,0.15,0.3); tone(1100,0.32,0.35,0.3); } } catch {} } function fmtTime(s) { const m = Math.floor(s / 60), r = s % 60; return m + ':' + String(r).padStart(2, '0'); } function pickSpeakTopics(cat, n, excludeIds) { let pool = SPEAK_TOPICS.map((t, i) => ({ ...t, id: i })) .filter(t => (cat === 'all' || t.cat === cat) && !excludeIds.includes(t.id)); if (pool.length < n) pool = SPEAK_TOPICS.map((t, i) => ({ ...t, id: i })).filter(t => cat === 'all' || t.cat === cat); const a = [...pool]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a.slice(0, n); } function SpeakingMode({ lang, state, setState, t, vocab, member, onMember }) { const [screen, setScreen] = React.useState('setup'); // setup | run | done const [cat, setCat] = React.useState('all'); const [secs, setSecs] = React.useState(60); const [rounds, setRounds] = React.useState(3); const [prep, setPrep] = React.useState(true); const [sound, setSound] = React.useState(true); const [rec, setRec] = React.useState(false); const [micError, setMicError] = React.useState(false); const [recordings, setRecordings] = React.useState([]); const streamRef = React.useRef(null); const recorderRef = React.useRef(null); const [topics, setTopics] = React.useState([]); const [round, setRound] = React.useState(0); const [phase, setPhase] = React.useState('prep'); // prep | speak const [left, setLeft] = React.useState(0); const [paused, setPaused] = React.useState(false); const total = phase === 'prep' ? PREP_SECONDS : secs; const isAr = lang === 'ar'; const catName = (c) => isAr ? c.ar : lang === 'tr' ? c.tr : c.en; const start = async () => { recordings.forEach(r => { try { URL.revokeObjectURL(r.url); } catch {} }); setRecordings([]); setMicError(false); if (rec) { try { streamRef.current = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch { streamRef.current = null; setMicError(true); } } const recent = (state.speaking && state.speaking.recentTopics) || []; setTopics(pickSpeakTopics(cat, rounds, recent)); setRound(0); setPhase(prep ? 'prep' : 'speak'); setLeft(prep ? PREP_SECONDS : secs); setPaused(false); setScreen('run'); if (!prep && sound) speakBeep('start'); }; const finishSession = (completedRounds) => { if (sound) speakBeep('done'); setState(s => { const sp = s.speaking || { sessions: 0, seconds: 0, recentTopics: [] }; const next = { ...s, speaking: { sessions: sp.sessions + 1, seconds: sp.seconds + completedRounds * secs, recentTopics: [...(sp.recentTopics || []), ...topics.map(x => x.id)].slice(-12), }}; return window.bumpHistory ? window.bumpHistory(next, 'speaking') : next; }); setScreen('done'); }; // recorder: one clip per speak phase React.useEffect(() => { if (screen !== 'run' || phase !== 'speak' || !rec || !streamRef.current) return; let mr; try { mr = new MediaRecorder(streamRef.current); } catch { return; } const chunks = []; const topicQ = (topics[round] || {}).q; mr.ondataavailable = e => { if (e.data && e.data.size) chunks.push(e.data); }; mr.onstop = () => { const blob = new Blob(chunks, { type: mr.mimeType || 'audio/webm' }); if (blob.size > 0) setRecordings(rs => [...rs, { q: topicQ, url: URL.createObjectURL(blob) }]); }; mr.start(); recorderRef.current = mr; return () => { try { if (mr.state !== 'inactive') mr.stop(); } catch {} recorderRef.current = null; }; }, [screen, phase, round, topics, rec]); // recorder follows pause React.useEffect(() => { const mr = recorderRef.current; if (!mr) return; try { if (paused && mr.state === 'recording') mr.pause(); else if (!paused && mr.state === 'paused') mr.resume(); } catch {} }, [paused]); // release the microphone when leaving the run screen React.useEffect(() => { if (screen === 'run') return; if (streamRef.current) { streamRef.current.getTracks().forEach(tr => tr.stop()); streamRef.current = null; } }, [screen]); // countdown React.useEffect(() => { if (screen !== 'run' || paused) return; const iv = setInterval(() => { setLeft(l => { if (l > 1) { if (sound && phase === 'speak' && l <= 4) speakBeep('tick'); return l - 1; } // reached 0 if (phase === 'prep') { if (sound) speakBeep('start'); setPhase('speak'); return secs; } // speak phase ended if (sound) speakBeep('end'); if (round < topics.length - 1) { setRound(r => r + 1); setPhase(prep ? 'prep' : 'speak'); return prep ? PREP_SECONDS : secs; } clearInterval(iv); setTimeout(() => finishSession(topics.length), 600); return 0; }); }, 1000); return () => clearInterval(iv); }, [screen, paused, phase, round, topics, sound, prep, secs]); const skipTopic = () => { const recent = topics.map(x => x.id); const [next] = pickSpeakTopics(cat, 1, recent); setTopics(ts => ts.map((x, i) => i === round ? next : x)); setPhase(prep ? 'prep' : 'speak'); setLeft(prep ? PREP_SECONDS : secs); }; const sp = state.speaking || { sessions: 0, seconds: 0 }; // words to weave into the answer — prefer words the user studied (learned/saved) const targetWords = React.useMemo(() => { const all = vocab || []; if (!all.length) return []; const byId = {}; all.forEach(w => { byId[w.id] = w; }); const studiedIds = [...new Set([...(state.learned || []), ...(state.saved || [])])]; const studied = studiedIds.map(id => byId[id]).filter(Boolean); const src = studied.length >= 3 ? studied : all; const a = [...src]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return { words: a.slice(0, 4), fromStudied: studied.length >= 3 }; }, [round, topics]); const pct = total > 0 ? (left / total) : 0; const R = 84, CIRC = 2 * Math.PI * R; /* ── SETUP ── */ if (screen === 'setup' && !member && sp.sessions >= FREE_SPEAK_SESSIONS && window.SignupCard) return ( { if (onMember) onMember(m); }} /> ); if (screen === 'setup') return (
{!member && (
🎁 {t('Free preview','جولة مجانية','Ücretsiz önizleme')} — {Math.max(0, FREE_SPEAK_SESSIONS - sp.sessions)} {t('speaking sessions left. Join free for unlimited.','جلسات متبقية. انضم مجاناً لجلسات بلا حدود.','oturum kaldı. Sınırsız için ücretsiz katıl.')}
)}
🎤 {t('Speaking Practice','تمرين المحادثة','Konuşma Alıştırması')}
{t('Talk about a topic — beat the clock','تحدث عن موضوع — قبل انتهاء الوقت','Bir konu hakkında konuş — süreye karşı')}
{sp.sessions > 0 && (
{sp.sessions}
{t('Sessions','جلسات','Oturum')}
{Math.round(sp.seconds / 60)}
{t('Minutes spoken','دقائق التحدث','Konuşma dk')}
)}
{t('Topic area','مجال الموضوع','Konu alanı')}
{SPEAK_CATS.map(c => ( ))}
{t('Time per topic','الوقت لكل موضوع','Konu başına süre')}
{SPEAK_TIMES.map(s => ( ))}
{t('Topics in a row','عدد المواضيع المتتالية','Arka arkaya konu')}
{SPEAK_ROUNDS.map(r => ( ))}
{rec &&
{t('Recordings stay in your browser only — nothing is uploaded. They are cleared when you refresh the page.','التسجيلات تبقى في متصفحك فقط — لا يُرفع شيء. تُحذف عند تحديث الصفحة.','Kayıtlar yalnızca tarayıcınızda kalır — hiçbir şey yüklenmez. Sayfayı yenileyince silinir.')}
} {micError &&
⚠️ {t('Microphone not available — the session will run without recording.','الميكروفون غير متاح — ستعمل الجلسة بدون تسجيل.','Mikrofon kullanılamıyor — oturum kayıtsız devam edecek.')}
}
{t('Tip: speak out loud, alone or with a friend. Don\u2019t stop until the beep — hesitation is normal!','نصيحة: تحدث بصوت عالٍ، وحدك أو مع صديق. لا تتوقف حتى الصافرة — التردد أمر طبيعي!','İpucu: yüksek sesle konuş. Bip sesine kadar durma!')}
); /* ── DONE ── */ if (screen === 'done') return (
🏁
{t('Session complete!','اكتملت الجلسة!','Oturum tamamlandı!')}
{topics.length} {t('topics','مواضيع','konu')} · {fmtTime(topics.length * secs)} {t('of speaking. Fluency is built exactly like this.','من التحدث. هكذا تُبنى الطلاقة.','konuşma. Akıcılık tam olarak böyle kazanılır.')}
{recordings.length > 0 && (
🎙 {t('Listen to yourself','استمع إلى نفسك','Kendini dinle')} 🤖 {t('AI feedback — coming soon','ملاحظات الذكاء الاصطناعي — قريباً','Yapay zekâ geri bildirimi — çok yakında')}
{recordings.map((r, i) => (
{i + 1}. {r.q}
))}
)}
); /* ── RUN ── */ const topic = topics[round]; const isPrep = phase === 'prep'; const danger = !isPrep && left <= 5; return (
{isPrep ? '🧠 ' + t('Think…','فكّر…','Düşün…') : '🎤 ' + t('Speak now!','تحدث الآن!','Şimdi konuş!')}
{t('Topic','الموضوع','Konu')} {round + 1} / {topics.length}
{fmtTime(left)}
{rec && streamRef.current && !isPrep && !paused &&
REC
}
{(SPEAK_CATS.find(c => c.id === topic.cat) || {}).emoji} {catName(SPEAK_CATS.find(c => c.id === topic.cat) || SPEAK_CATS[0])}
{topic.q}
{t('If you get stuck, talk about:','إذا توقفت، تحدث عن:','Takılırsan şunlardan bahset:')}
{topic.hints.map((h, i) =>
{i + 1}{h}
)}
{targetWords.words && targetWords.words.length > 0 && (
💡 {targetWords.fromStudied ? t('Bonus challenge — use these words you studied:','تحدٍ إضافي — استخدم هذه الكلمات التي درستها:','Bonus görev — çalıştığın bu kelimeleri kullan:') : t('Bonus challenge — try to use these words:','تحدٍ إضافي — حاول استخدام هذه الكلمات:','Bonus görev — bu kelimeleri kullanmayı dene:')}
{targetWords.words.map(w => { const sv = (state.saved || []).includes(w.id); return ( ); })}
{t('Tap a word to save it to your words','اضغط على كلمة لحفظها في كلماتك','Kaydetmek için kelimeye dokun')}
)}
{!isPrep && ( )}
); } window.SpeakingMode = SpeakingMode;