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 // A page sent from the context menu before any conversation was chosen // waits for the first choice; switching conversations still clears it. if (selected) { attachment = null; $('attachment').hidden = true } selected = id; opening = Boolean(id); stream = null bufferedEvents = [] $('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) } }) function setAttachment(context) { 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(selected ? '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.' : 'Ek hazır. Göndermek için yukarıdan bir IDE konuşması seçin.') } async function capture(tabId) { const results = await chrome.scripting.executeScript({ target: { tabId }, func: capturePageContext }) return results[0]?.result } $('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.') const context = await capture(tab.id) if (intent !== generation) return setAttachment(context) } catch (error) { notice(error.message) } }) // Text or a page sent from the right-click menu while the panel opens. async function takePendingContext() { const saved = await chrome.storage.session.get(['pendingContext', 'pendingNotice']) if (saved.pendingNotice) { notice(saved.pendingNotice); await chrome.storage.session.remove('pendingNotice') } const pending = saved.pendingContext if (!pending) return await chrome.storage.session.remove('pendingContext') if (Date.now() - pending.at > 120000) return try { if (pending.kind === 'selection' && pending.text) { const tab = await chrome.tabs.get(pending.tabId).catch(() => null) let address = '' try { const url = new URL(tab?.url || ''); address = url.origin + url.pathname } catch {} setAttachment({ title: (tab?.title || '').slice(0, 200), url: address, text: pending.text.slice(0, 24000), selection: true }) } else setAttachment(await capture(pending.tabId)) } catch (error) { notice(error.message) } } // ── The agent using this Chrome ─────────────────────────────────────── const STATUS_MARK = { ok: '✓', failed: '✕', cancelled: '■', running: '…' } function renderControl(state) { if (!state || typeof state !== 'object') return $('control-enabled').checked = Boolean(state.enabled) $('control-stop').hidden = !(state.enabled && state.running) $('control-release').hidden = !(state.tabs?.length || state.shared) let text if (!state.enabled) text = 'Kapalı. Açarsan AITURK ajanı tarayıcı işlerini bu Chrome’da, kendi “AITURK” sekmelerinde yapar.' else if (!state.registered) text = `IDE bekleniyor… AITURK IDE açık olmalı.${state.error ? ' (' + state.error + ')' : ''}` else if (state.running) text = 'Ajan şu an bu Chrome’da çalışıyor. Durdurmak için “Durdur”a bas.' else text = 'Hazır: ajan tarayıcı işlerini bu Chrome’da, “AITURK” sekme grubunda yapar. Şifre ve kart alanlarına yazmaz.' if (state.shared) text += ` Paylaşılan sekme: “${state.shared.title || 'sekme'}”; ajan bir sonraki tarayıcı işinde bunu kullanır.` if (state.tabs?.length) text += ` Ajanın sekmeleri: ${state.tabs.map(tab => tab.url || tab.title).filter(Boolean).join(', ')}.` $('control-status').textContent = text const items = (state.log || []).map(entry => { const item = document.createElement('li') item.className = entry.status item.textContent = `${STATUS_MARK[entry.status] || ''} ${entry.text}${entry.note ? ' — ' + entry.note : ''}` return item }) $('control-log').replaceChildren(...items) $('control-details').hidden = !items.length } async function control(type, extra = {}) { try { const reply = await chrome.runtime.sendMessage({ type, ...extra }) if (reply?.failure) notice(reply.failure) else renderControl(reply) } catch (error) { notice(`Eklenti arka planına ulaşılamadı: ${error.message}`) } } chrome.runtime.onMessage.addListener(message => { if (message?.type === 'control.state') renderControl(message.state) }) chrome.storage.onChanged.addListener((changes, area) => { if (area === 'session' && (changes.pendingContext?.newValue || changes.pendingNotice?.newValue)) void takePendingContext() }) $('control-enabled').addEventListener('change', () => void control('control.set', { enabled: $('control-enabled').checked })) $('control-stop').addEventListener('click', () => void control('control.stop')) $('control-share').addEventListener('click', () => void control('control.share', { sessionId: selected || null })) $('control-release').addEventListener('click', () => void control('control.release')) $('remove-attachment').addEventListener('click', () => { attachment = null; $('attachment').hidden = true; notice('') }) void connect() void control('control.get') void takePendingContext()