feat(mobile): correlate voice tasks and recover results on reconnect
This commit is contained in:
1 parent
9f9f3226a1
commit
b66daed07a
17 files changed
+308
-48
No files matched your search
+46
-8
@@ -4,6 +4,8 @@ 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) }) }
|
||||
@@ -16,7 +18,17 @@ 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 sonucu', frame.text || 'Tamamlanma olayı alındı; geçmişi yenileyin.') }
|
||||
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('pair', async () => {
|
||||
credentials = { accountToken: $('account-token').value.trim() }; $('account-token').value = ''
|
||||
@@ -35,9 +47,13 @@ action('devices', async () => {
|
||||
})
|
||||
action('connect', async () => {
|
||||
if (!account || !$('desktop').value) throw new Error('Önce bir bilgisayar seçin.')
|
||||
stopVoice(); connection?.close(); selected = null; setApproval(null)
|
||||
const epoch = ++connectionEpoch
|
||||
stopVoice(); connection?.close(); connection = null; selected = null; setApproval(null)
|
||||
status('Bilgisayara bağlanılıyor…')
|
||||
connection = await openPhoneConnection(account, $('desktop').value, event)
|
||||
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 () => {
|
||||
@@ -46,9 +62,12 @@ action('sessions', async () => {
|
||||
})
|
||||
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 : null
|
||||
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'}`
|
||||
@@ -61,12 +80,21 @@ action('delete-google', async () => { stopVoice(); await rpc('google.key.delete'
|
||||
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)
|
||||
@@ -75,26 +103,36 @@ action('voice-start', () => startVoice(true)); action('voice-text', () => startV
|
||||
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 = true }
|
||||
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 (!pendingTask) pendingTask = { taskId: crypto.randomUUID(), sessionId: target.id, model: target.model, intent: $('intent').value, text }
|
||||
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', 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 () => { stopVoice(); connection?.close(); connection = null; selected = null; $('work').hidden = true; status('Telefon bağlantısı kapalı. IDE işi devam edebilir.') })
|
||||
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() })
|
||||
|
||||
Reference in new issue
Block a user