223 lines
11 KiB
JavaScript
223 lines
11 KiB
JavaScript
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()
|