import { AccountClient, openPhoneConnection } from './transport.mjs' import { VoiceSession } from './voice.mjs' import { taskState } from './behavior.mjs' const $ = id => document.getElementById(id) if (globalThis.Capacitor?.isNativePlatform()) globalThis.Capacitor.registerPlugin('MobileSecrets') let credentials = {}, account, connection, selected, approval, voice, pendingTask, usageTimer, voiceGeneration = 0 let connectedDesktopId = null let connectionEpoch = 0, selectionEpoch = 0 const status = text => { $('status').textContent = text } const native = () => window.Capacitor?.Plugins?.MobileSecrets async function saveCredentials() { if (native()) await native().save({ value: JSON.stringify(credentials) }) } function addText(container, role, text) { const p = document.createElement('p'); p.textContent = `${role}: ${text}`; $(container).append(p); while ($(container).children.length > 150) $(container).firstChild.remove() } function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } } function rpc(action, params) { if (!connection) throw new Error('Önce bilgisayara bağlanın.'); return connection.requests.call(action, params) } function requireSession() { if (!selected) throw new Error('Önce IDE konuşmasını seçin.'); return selected } function setApproval(value) { approval = value; $('approval').hidden = !value; $('approval-detail').textContent = value ? `${value.command}\n\n${value.reason}` : ''; $('allow').disabled = !value?.choices.includes('once'); $('deny').disabled = !value?.choices.includes('deny') } function event(frame) { if (frame.event === 'disconnected') { stopVoice(); status('IDE bağlantısı kesildi. Yeniden bağlanın.'); setApproval(null) } const next = taskState(frame); if (next) $('work-status').textContent = next if (frame.event === 'approval.request') setApproval(frame.approval) if (frame.event === 'message.complete') { setApproval(null); addText('result', 'IDE konuşmasındaki yanıt', frame.text || 'Geçmişi yenileyin.') } if (['task.start', 'task.complete'].includes(frame.event) && credentials.outbox?.taskId === frame.taskId && credentials.outbox.sessionId === frame.sessionId && credentials.outbox.desktopId === connectedDesktopId) { $('work-status').textContent = frame.event === 'task.start' ? 'Gönderdiğiniz görev çalışıyor.' : `Göreviniz sona erdi: ${frame.status}.` if (frame.event === 'task.complete') { addText('result', `Görev ${frame.taskId}`, frame.text || frame.status) voice?.result(frame) delete credentials.outbox; pendingTask = null; $('retry').hidden = true void saveCredentials().catch(() => status('Görev tamamlandı ancak yerel kayıt güncellenemedi.')) } } } action('login-start', async () => { if (credentials.accountToken || connection) throw new Error('Kayıtlı hesabınız var; önce mevcut cihaz erişimini iptal edin.') const result = await new AccountClient({}).call('/v1/aiturk/mobile-auth/start', {}) if (!/^https:\/\/turkservis\.online\/account\/aiturk\/mobile-login\/[0-9a-f-]{36}$/.test(result.verificationUri)) throw new Error('Geçersiz giriş sayfası.') credentials.pendingLogin = result; await saveCredentials() $('login-code').textContent = `Yalnız kendi hesabınızda bu kodu onaylayın: ${result.userCode}` $('login-link').href = result.verificationUri; $('login-link').hidden = false if (native()) await native().openLogin({ url: result.verificationUri }) status('Tarayıcıda giriş yaptıktan sonra buraya dönüp girişi kontrol edin.') }) action('login-check', async () => { if (!credentials.pendingLogin) throw new Error('Önce giriş başlatın.') const result = await new AccountClient({ accountToken: credentials.pendingLogin.deviceCode }).call('/v1/aiturk/mobile-auth/poll', {}) if (result.state !== 'approved') { status('Tarayıcıdaki kod onayı bekleniyor.'); return } credentials.accountToken = result.accountToken; delete credentials.pendingLogin; await saveCredentials() account = new AccountClient(credentials); $('login-code').textContent = 'Hesaba giriş yapıldı.'; $('login-link').hidden = true status('Hesap bağlı. Şimdi telefonun eşleştirme kodunu oluşturun.') }) action('logout', async () => { const token = credentials.accountToken || credentials.pendingLogin?.deviceCode // Revoke the app-specific credential before dropping the local copy. A // network failure leaves it available for an explicit retry. if (token?.startsWith('tsml_')) await new AccountClient({ accountToken: token }).call('/v1/aiturk/mobile-auth/logout', {}) connectionEpoch++; stopVoice(); connection?.close(); connection = null; selected = null; account = null credentials = {}; pendingTask = null; await saveCredentials() $('work').hidden = true; $('login-link').hidden = true; $('login-code').textContent = ''; $('pair-code').textContent = '' status('Bu uygulamanın oturumu kapatıldı. IDE’de başlamış iş kendi çalışma kurallarıyla devam edebilir.') }) action('pair', async () => { if (credentials.deviceId) throw new Error('Bu telefon zaten eşleşme başlatmış. Bilgisayarları bulun veya mevcut erişimi iptal edin.') const manual = $('account-token').value.trim(); $('account-token').value = '' if (manual && credentials.accountToken && manual !== credentials.accountToken) throw new Error('Kayıtlı hesap sessizce değiştirilemez.') if (manual) credentials.accountToken = manual if (!credentials.accountToken) throw new Error('Önce AITURK hesabınıza giriş yapın.') account = new AccountClient(credentials) const result = await account.call('/v1/aiturk/pairings', { display_name: $('device-name').value.trim(), platform: 'android' }) Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id }) await saveCredentials() $('pair-code').textContent = `Eşleştirme kimliği:\n${result.pairing_id}\n\nKod: ${result.user_code}\nSon geçerlilik: ${result.expires_at}` status('Bilgisayardaki açık onay bekleniyor.') }) action('devices', async () => { if (!account) throw new Error('Önce hesabınızı eşleştirin.') const data = await account.call('/v1/aiturk/mobile/devices'); $('desktop').replaceChildren() for (const device of data.devices.filter(d => d.platform.startsWith('windows'))) { const option = document.createElement('option'); option.value = device.id; option.textContent = device.name; $('desktop').append(option) } status('Hesabınızdaki onaylı bilgisayarlar bulundu.') }) action('connect', async () => { if (!account || !$('desktop').value) throw new Error('Önce bir bilgisayar seçin.') const epoch = ++connectionEpoch stopVoice(); connection?.close(); connection = null; selected = null; setApproval(null) status('Bilgisayara bağlanılıyor…') connectedDesktopId = $('desktop').value const opened = await openPhoneConnection(account, connectedDesktopId, frame => { if (epoch === connectionEpoch) event(frame) }) if (epoch !== connectionEpoch) { opened.close(); return } connection = opened $('work').hidden = false; status('Bilgisayar bağlı. IDE konuşmasını seçin.') }) action('sessions', async () => { const data = await rpc('sessions.list'); $('session').replaceChildren() for (const item of data.sessions) { const option = document.createElement('option'); option.value = item.id; option.textContent = `${item.title} · ${item.model || 'Model bilinmiyor'}`; $('session').append(option) } }) action('select-session', async () => { const id = $('session').value; if (!id) throw new Error('Bir konuşma seçin.') const epoch = ++selectionEpoch, targetConnection = connection stopVoice(); selected = null; setApproval(null) const result = await rpc('session.open', { sessionId: id }) if (epoch !== selectionEpoch || connection !== targetConnection) return selected = { id, model: result.model } pendingTask = credentials.outbox?.sessionId === id && credentials.outbox?.desktopId === connectedDesktopId ? credentials.outbox : null $('retry').hidden = !pendingTask if (pendingTask) { $('task').value = pendingTask.text; $('intent').value = pendingTask.intent; $('preview').textContent = pendingTask.text } $('target').textContent = `Bilgisayar: ${$('desktop').selectedOptions[0]?.textContent} · Konuşma: ${$('session').selectedOptions[0]?.textContent} · İş modeli: ${result.model || 'Bilinmiyor'}` $('result').replaceChildren(); for (const message of result.messages) addText('result', message.role, message.text) setApproval(result.approval); $('work-status').textContent = result.running ? 'IDE çalışıyor.' : 'IDE konuşması açıldı.' }) for (const [id, choice] of [['allow', 'once'], ['deny', 'deny']]) action(id, async () => { const target = requireSession(); await rpc('approval.respond', { sessionId: target.id, requestId: approval?.requestId, choice }); setApproval(null); status('Yanıt gerçek IDE onayına iletildi.') }) action('save-google', async () => { const key = $('google-key').value.trim(); $('google-key').value = ''; await rpc('google.key.save', { key }); status('Google anahtarı bilgisayarın güvenli kasasına kaydedildi.') }) action('delete-google', async () => { stopVoice(); await rpc('google.key.delete'); status('Google anahtarı bilgisayar kasasından silindi.') }) function stopVoice() { voiceGeneration++; voice?.stop(); voice = null; clearInterval(usageTimer); $('voice-status').textContent = 'Mikrofon kapalı. IDE işi devam edebilir.' } async function startVoice(microphone) { if (!$('consent').checked) throw new Error('Önce Google veri ve ücret bilgisini okuyup kabul edin.') const target = { ...requireSession() }, targetConnection = connection, desktopId = connectedDesktopId stopVoice(); const generation = voiceGeneration; const token = await rpc('voice.token') if (generation !== voiceGeneration || document.hidden) return const started = Date.now() voice = new VoiceSession({ ...token, onText: (role, text) => addText('discussion', role, text), onDraft: (text, intent) => { $('task').value = text; $('intent').value = intent; updatePreview(); status('Asistan görev taslağını hazırladı. Henüz IDE’ye gönderilmedi.') }, onSubmit: async (text, intent) => { if (generation !== voiceGeneration || connection !== targetConnection || selected?.id !== target.id || selected?.model !== target.model) throw new Error('IDE hedefi veya ses görüşmesi değişti; gönderim yapılmadı.') if (credentials.outbox) throw new Error('Önceki gönderimi kontrol edin; ikinci bir iş otomatik başlatılmadı.') $('task').value = text; $('intent').value = intent; $('preview').textContent = text pendingTask = { taskId: crypto.randomUUID(), sessionId: target.id, model: target.model, desktopId, intent, text } return deliver() }, onError: status, onState: text => { $('voice-status').textContent = text; if (voice?.stopped) clearInterval(usageTimer) } }) usageTimer = setInterval(() => { $('usage').textContent = `Bu uygulamanın ölçtüğü görüşme süresi: ${Math.floor((Date.now() - started) / 1000)} sn. Google kotası/faturası değildir.` }, 1000) await voice.start(microphone) } action('voice-start', () => startVoice(true)); action('voice-text', () => startVoice(false)) action('voice-stop', async () => stopVoice()); action('silence', async () => voice?.interruptPlayback()) action('discuss', async () => { if (!voice?.ready) throw new Error('Önce asistanla yazış veya sesle konuş bağlantısını açın.'); const text = $('idea').value.trim(); if (!text) return; voice.text(text); addText('discussion', 'Siz', text); $('idea').value = '' }) function visibleTask() { const text = $('task').value.trim(); return text ? ($('intent').value === 'research' ? 'Araştırma / inceleme isteği. Değişiklik yapma.\n\n' : '') + text : '' } function updatePreview() { $('preview').textContent = visibleTask(); pendingTask = null; $('retry').hidden = !credentials.outbox } $('task').oninput = updatePreview; $('intent').onchange = updatePreview async function deliver() { if (!pendingTask) throw new Error('Kontrol edilecek gönderim yok.') if (pendingTask.desktopId !== connectedDesktopId || pendingTask.sessionId !== selected?.id || pendingTask.model !== selected?.model) throw new Error('Bekleyen görevin bilgisayarı veya konuşması farklı. Önce doğru hedefi seçin.') credentials.outbox = pendingTask; await saveCredentials() const result = await rpc('task.submit', pendingTask) if (['complete', 'error', 'interrupted'].includes(result.state)) { $('work-status').textContent = `Görev daha önce sona ermiş: ${result.state}. Sonuç için IDE geçmişini açın.` delete credentials.outbox; pendingTask = null; $('retry').hidden = true; await saveCredentials() return { taskId: result.taskId, completed: result.state === 'complete', state: result.state } } if (!credentials.outbox) return { taskId: result.taskId, accepted: true, completed: false, state: 'terminal_event_received' } $('work-status').textContent = result.state === 'accepted' ? 'IDE görevi kabul etti. Tamamlanma sonucu bekleniyor.' : 'Gönderimin sonucu belirsiz. IDE geçmişini kontrol edin; yeniden çalıştırma yapılmadı.' $('retry').hidden = false return { taskId: result.taskId, accepted: result.state === 'accepted', completed: false, state: result.state } } action('submit', async () => { const target = requireSession(), text = visibleTask(); if (!text) throw new Error('Görev metni boş olamaz.') if (credentials.outbox && credentials.outbox.taskId !== pendingTask?.taskId) throw new Error('Önce önceki gönderimi kontrol edin; bekleyen görev kaydı silinmedi.') if (!pendingTask) pendingTask = { taskId: crypto.randomUUID(), sessionId: target.id, model: target.model, desktopId: connectedDesktopId, intent: $('intent').value, text } $('retry').hidden = false; await deliver() }) action('retry', async () => { pendingTask = credentials.outbox || pendingTask; return deliver() }) action('cancel-draft', async () => { $('task').value = ''; updatePreview(); status('Gönderilmemiş görev taslağı iptal edildi. Başlamış IDE işi devam eder.') }) action('interrupt', async () => { const target = requireSession(); await rpc('message.stop', { sessionId: target.id }); $('work-status').textContent = 'IDE’ye durdurma isteği iletildi; gerçek sonucu bekleyin.' }) action('history', async () => { const result = await rpc('session.history', { sessionId: requireSession().id }); $('result').replaceChildren(); for (const message of result.messages) addText('result', message.role, message.text) }) action('clear-history', async () => { $('discussion').replaceChildren(); $('result').replaceChildren(); status('Telefondaki görünür metin temizlendi. Google bağlantı bağlamını da temizlemek için görüşmeyi kapatın.') }) action('disconnect', async () => { connectionEpoch++; stopVoice(); connection?.close(); connection = null; selected = null; $('work').hidden = true; status('Telefon bağlantısı kapalı. IDE işi devam edebilir.') }) action('revoke', async () => { const data = await account.call('/v1/aiturk/mobile/devices'); const self = data.devices.find(d => d.self); await account.call(`/v1/aiturk/devices/${self.id}/revoke`, { expected_version: self.version }); stopVoice(); connection?.close(); connection = null; credentials = {}; await saveCredentials(); account = null; $('work').hidden = true; status('Bu telefonun erişimi iptal edildi.') }) document.addEventListener('visibilitychange', () => { if (document.hidden) stopVoice() }) window.addEventListener('pagehide', () => { stopVoice(); connection?.close() }) try { const saved = await native()?.load(); if (saved?.value) { credentials = JSON.parse(saved.value); if (credentials.accountToken) { account = new AccountClient(credentials); status('Kayıtlı cihaz bulundu. Bilgisayarları yenileyerek bağlanın.') } } } catch { status('Android güvenli kasası açılamadı. Hesabınızı yeniden eşleştirin.') }