import { capturePageContext, composePrompt } from './page-context.js' const $ = id => document.getElementById(id) let port, connected = false, sessions = [], selected = '', generation = 0, nextId = 0 let stream, attachment = null, sending = false, opening = false, connectionGeneration = 0 let bufferedEvents = [], turnGeneration = 0 const pending = new Map() const notice = text => { $('notice').textContent = text; $('notice').hidden = !text } const activity = text => { $('activity').textContent = text } function controls() { $('search').disabled = $('refresh').disabled = !connected $('sessions').disabled = !connected || sending || opening $('message').disabled = $('add-page').disabled = !connected || !selected || opening $('send').disabled = !connected || !selected || sending || opening || !$('message').value.trim() } function request(action, params = {}) { if (!port) return Promise.reject(new Error('Önce IDE’ye bağlanın.')) const id = String(++nextId) return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error('IDE yanıt vermedi. Tekrar göndermeden önce geçmişi kontrol edin.')) }, 75000) pending.set(id, { resolve, reject, timer }) try { port.postMessage({ id, action, params }) } catch (error) { clearTimeout(timer); pending.delete(id); reject(error) } }) } function disconnect(message) { connected = false generation++ $('dot').classList.remove('online') $('connection').textContent = 'IDE bağlantısı yok' $('stop').hidden = true for (const item of pending.values()) { clearTimeout(item.timer); item.reject(new Error(message)) } pending.clear() notice(message) controls() } async function connect() { const epoch = ++connectionGeneration if (port) { const previous = port; port = null; previous.disconnect() } disconnect('') $('connection').textContent = 'IDE’ye bağlanıyor…' const current = chrome.runtime.connectNative('online.turkservis.aiturk') port = current current.onMessage.addListener(frame => { if (epoch !== connectionGeneration) return if (frame.id !== undefined) { const item = pending.get(frame.id) if (!item) return pending.delete(frame.id); clearTimeout(item.timer) frame.error ? item.reject(new Error(frame.error)) : item.resolve(frame.result) } else handleEvent(frame) }) current.onDisconnect.addListener(() => { const details = chrome.runtime.lastError?.message || '' if (epoch !== connectionGeneration) return port = null disconnect(`Bağlantı kurulamadı veya kapandı. Uyumlu AITURK IDE’yi açın ve ZIP içindeki bağlantı kurulumunu çalıştırın.${details ? ' (' + details + ')' : ''}`) }) try { const result = await request('status') if (epoch !== connectionGeneration) return connected = true $('connection').textContent = `Yerel IDE bağlı · ${result.profile}` $('dot').classList.add('online') notice(''); controls() await refresh() const saved = await chrome.storage.local.get('selectedSession') if (epoch !== connectionGeneration) return const target = selected || saved.selectedSession if (sessions.some(row => row.id === target)) { $('sessions').value = target; await openSession(target) } } catch (error) { if (epoch === connectionGeneration) { notice(error.message); controls() } } } function renderOptions() { const query = $('search').value.toLocaleLowerCase('tr') const rows = sessions.filter(row => row.id === selected || row.title.toLocaleLowerCase('tr').includes(query)) $('sessions').replaceChildren(new Option(rows.length ? 'Bir konuşma seçin' : 'Konuşma bulunamadı', '')) rows.forEach(row => $('sessions').add(new Option(row.title, row.id))) $('sessions').value = selected } async function refresh() { const epoch = connectionGeneration try { const data = await request('sessions.list') if (epoch !== connectionGeneration) return sessions = data.sessions renderOptions() $('scope').textContent = `${data.profile} profili · Son 200 kayıttaki IDE konuşmaları · ${sessions.length} konuşma` } catch (error) { notice(error.message) } } function bubble(role, text, streaming = false) { const item = document.createElement('article') item.className = `message ${role}${streaming ? ' streaming' : ''}` const label = document.createElement('div'); label.className = 'role'; label.textContent = role === 'user' ? 'SEN' : 'AITURK' const body = document.createElement('div'); body.className = 'text'; body.textContent = text item.append(label, body); $('transcript').append(item) return body } function scrollEnd() { $('transcript').scrollTop = $('transcript').scrollHeight } function paintHistory(messages) { $('transcript').replaceChildren(); stream = null messages.forEach(row => bubble(row.role, row.text)) scrollEnd() } async function openSession(id) { const intent = ++generation selected = id; opening = Boolean(id); stream = null bufferedEvents = [] attachment = null; $('attachment').hidden = true $('message').value = ''; $('stop').hidden = true notice(''); controls() if (!id) { opening = false; $('transcript').replaceChildren(); activity('Bir konuşma seçin.'); return } activity('Konuşma açılıyor…') try { const data = await request('session.open', { sessionId: id }) if (intent !== generation) return paintHistory(data.messages) if (data.inflight) { stream = bubble('assistant', data.inflight, true); scrollEnd() } $('stop').hidden = !data.running activity(data.running ? 'IDE çalışıyor…' : `${data.model || 'IDE modeli'} · Son 40 kayıt gösteriliyor`) if (data.attention) notice('Bu konuşma IDE’de onay veya ek bilgi bekliyor.') await chrome.storage.local.set({ selectedSession: id }) } catch (error) { if (intent === generation) { selected = ''; notice(error.message) } } finally { if (intent === generation) { opening = false; controls() const events = bufferedEvents; bufferedEvents = [] events.forEach(handleEvent) } } } async function reconcile() { const intent = generation, sessionId = selected, turn = turnGeneration try { const data = await request('session.history', { sessionId }) if (intent === generation && sessionId === selected && !opening && !stream && turn === turnGeneration) paintHistory(data.messages) } catch (error) { if (intent === generation) notice(error.message) } } function handleEvent(frame) { if (frame.event === 'disconnected') { disconnect(frame.message); return } if (frame.event === 'error') { notice(frame.message); return } if (frame.sessionId !== selected) return if (opening) { if (bufferedEvents.length < 2000) bufferedEvents.push(frame); return } const nearBottom = $('transcript').scrollHeight - $('transcript').scrollTop - $('transcript').clientHeight < 120 switch (frame.event) { case 'message.start': turnGeneration++; stream = null; $('stop').hidden = false; activity('AITURK düşünüyor…'); break case 'message.delta': stream ||= bubble('assistant', '', true) stream.textContent = (stream.textContent + frame.text).slice(-100000) activity('Yanıt yazılıyor…'); break case 'message.complete': if (frame.text) { stream ||= bubble('assistant', '', true); stream.textContent = frame.text } stream?.parentElement.classList.remove('streaming'); stream = null $('stop').hidden = true; activity(frame.status === 'error' ? 'IDE yanıtı hata ile tamamlandı.' : 'IDE ile aynı konuşma · Güncel') void reconcile(); break case 'attention': notice(frame.message); activity('IDE’de yanıtınız bekleniyor.'); break case 'activity': activity(frame.message); break case 'error': case 'session.error': notice(frame.text || 'IDE hata bildirdi.'); break } if (nearBottom) scrollEnd() } $('composer').addEventListener('submit', async event => { event.preventDefault() if (sending || opening || !connected || !selected) return const draft = $('message').value.trim() if (!draft) return const text = composePrompt(draft, attachment) const intent = generation, sessionId = selected sending = true; controls(); notice(''); activity('IDE’ye gönderiliyor…') const optimistic = bubble('user', text); scrollEnd() try { await request('message.send', { sessionId, text }) if (intent !== generation) return // Keep server history authoritative; optimistic text is replaced on finish. $('message').value = ''; attachment = null; $('attachment').hidden = true $('stop').hidden = false; activity('Mesaj IDE’ye iletildi; iş sürüyorsa sıraya alınır.') } catch (error) { optimistic.parentElement?.remove() notice(error.message + ' Taslağınız korundu. Otomatik tekrar gönderilmedi.') } finally { sending = false; controls() } }) $('message').addEventListener('input', controls) $('message').addEventListener('keydown', event => { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); $('composer').requestSubmit() } }) $('sessions').addEventListener('change', () => void openSession($('sessions').value)) $('search').addEventListener('input', renderOptions) $('refresh').addEventListener('click', () => { void refresh(); if (selected) void reconcile() }) $('reconnect').addEventListener('click', () => void connect()) $('stop').addEventListener('click', async () => { try { await request('message.stop', { sessionId: selected }); activity('Durdurma isteği IDE’ye iletildi.') } catch (error) { notice(error.message) } }) $('add-page').addEventListener('click', async () => { const intent = generation try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }) if (!tab?.id || !/^https?:\/\//.test(tab.url || '')) throw new Error('Yalnızca normal HTTP/HTTPS sayfaları eklenebilir. Önce AITURK araç çubuğu simgesine tıklayarak bu sekmeye erişim verin.') const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: capturePageContext }) if (intent !== generation) return const context = results[0]?.result if (!context?.text) throw new Error('Bu sayfadan okunabilir metin alınamadı.') attachment = context $('attachment-label').textContent = `${context.selection ? 'Seçim' : 'Sayfa'}: ${context.title} · ${context.text.length.toLocaleString('tr')} karakter` $('attachment').hidden = false notice('Sayfa içeriği mesajınızla birlikte IDE’ye ve seçili model sağlayıcısına gönderilecek. Hassas bilgi içermediğinden emin olun.') } catch (error) { notice(error.message) } }) $('remove-attachment').addEventListener('click', () => { attachment = null; $('attachment').hidden = true; notice('') }) void connect()