github-asistani / static /index.html
kadiirgok
Switch to Docker SDK: FastAPI + custom UI
939f7a9
Raw History Blame Contribute Delete
25.5 kB
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Asistanı</title>
<!-- Inter fontu (Google Fonts CDN) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Markdown render + XSS koruması (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<style>
:root {
--bg: #F2F2F7; /* sayfa arka planı (açık iOS gri) */
--surface: #FFFFFF; /* sohbet kutusu yüzeyi */
--hairline: #E5E5EA; /* ince ayraç çizgileri */
--text: #1C1C1E;
--muted: #8E8E93;
--blue: #007AFF; /* Apple mavisi */
--blue-press: #0069E0;
--blue-disabled: #B3D4FF;
--user-bubble: #007AFF;
--asst-bubble: #E9E9EB; /* açık gri balon */
--input-bg: #F2F2F7;
--ok-bg: #E6F7EE;
--ok-fg: #157347;
--warn-bg: #FFF1E0;
--warn-fg: #B45309;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.app {
width: 100%;
max-width: 1200px;
height: 100vh;
height: 100dvh;
margin: 0 auto;
background: var(--surface);
display: flex;
flex-direction: row;
box-shadow: 0 0 0 1px rgba(0,0,0,.03), 0 8px 40px rgba(0,0,0,.06);
}
@media (max-width: 900px) {
.app { box-shadow: none; }
}
/* --- Sol panel: profil + repo listesi --- */
.panel {
width: 300px;
flex-shrink: 0;
border-right: 1px solid var(--hairline);
background: #FAFAFC;
overflow-y: auto;
padding: 16px 14px;
display: flex;
flex-direction: column;
gap: 14px;
}
.panel::-webkit-scrollbar { width: 6px; }
.panel::-webkit-scrollbar-thumb { background: rgba(0,0,0,.15); border-radius: 3px; }
.panel-title { font-size: 16px; font-weight: 700; word-break: break-word; }
.panel-stat { font-size: 13px; color: var(--muted); margin-top: 3px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chip {
background: #EAF2FF; color: var(--blue);
border-radius: 999px; padding: 2px 9px; font-size: 11.5px; font-weight: 500;
}
.panel-section-title {
font-size: 11px; font-weight: 600; letter-spacing: .05em;
text-transform: uppercase; color: var(--muted);
}
.repo-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.repo-item { padding: 8px 10px; background: #fff; border: 1px solid var(--hairline); border-radius: 10px; }
.repo-name { font-size: 14px; font-weight: 600; color: var(--text); text-decoration: none; word-break: break-word; }
.repo-name:hover { color: var(--blue); }
.repo-meta { display: flex; gap: 8px; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
.repo-lang { color: var(--blue); }
.repo-desc { font-size: 12px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
.refresh-btn {
border: 1px solid var(--hairline);
background: #fff;
color: var(--text);
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background .15s ease, border-color .15s ease;
}
.refresh-btn:hover { background: var(--input-bg); border-color: #c0c0c6; }
.refresh-btn:disabled { opacity: .5; cursor: default; }
.refresh-status { font-size: 12px; color: var(--muted); margin-top: 6px; min-height: 15px; }
/* --- Ana kolon --- */
.main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
/* --- Üst çubuk --- */
.topbar {
padding: 14px 16px 12px;
border-bottom: 1px solid var(--hairline);
flex-shrink: 0;
}
.topbar-row { display: flex; align-items: center; gap: 10px; }
.topbar h1 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }
.topbar .subtitle { margin: 2px 0 0; font-size: 13px; color: var(--muted); }
.menu-btn {
display: none;
width: 36px; height: 36px;
flex-shrink: 0;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text);
cursor: pointer;
border-radius: 8px;
}
.menu-btn:hover { background: rgba(0,0,0,.05); }
/* --- GitHub hedefi giriş çubuğu --- */
.targetbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--hairline);
flex-shrink: 0;
background: var(--surface);
}
.targetbar input {
flex: 1;
background: var(--input-bg);
border: 1px solid #E0E0E5;
border-radius: 10px;
padding: 10px 14px;
font-size: 14px;
font-family: inherit;
color: var(--text);
outline: none;
transition: border-color .15s ease, background .15s ease;
}
.targetbar input::placeholder { color: var(--muted); }
.targetbar input:focus { border-color: var(--blue); background: #fff; }
.targetbar input:disabled { opacity: .6; }
.load-btn {
flex-shrink: 0;
border: none;
border-radius: 10px;
background: var(--blue);
color: #fff;
padding: 10px 16px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background .15s ease, transform .1s ease;
}
.load-btn:hover { background: var(--blue-press); }
.load-btn:active { transform: scale(.97); }
.load-btn:disabled { background: var(--blue-disabled); cursor: default; transform: none; }
/* --- Mesaj alanı --- */
.chat {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
overscroll-behavior: contain;
}
.chat::-webkit-scrollbar { width: 6px; }
.chat::-webkit-scrollbar-thumb { background: rgba(0,0,0,.15); border-radius: 3px; }
.chat::-webkit-scrollbar-track { background: transparent; }
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 32px;
gap: 6px;
}
.empty-icon {
width: 92px; height: 92px;
border-radius: 26px;
background: #EAF2FF;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
.empty-state h2 { margin: 0; font-size: 20px; font-weight: 600; color: var(--text); }
.empty-state p { margin: 0; max-width: 360px; font-size: 14px; line-height: 1.55; color: var(--muted); }
.msg-row {
display: flex;
flex-direction: column;
max-width: 78%;
animation: msg-in .28s cubic-bezier(.2,.8,.3,1);
}
.msg-row.user { align-self: flex-end; align-items: flex-end; }
.msg-row.assistant { align-self: flex-start; align-items: flex-start; }
.bubble {
padding: 10px 14px;
border-radius: 18px;
line-height: 1.45;
font-size: 15px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.msg-row.user .bubble {
background: var(--user-bubble);
color: #fff;
border-bottom-right-radius: 5px;
}
.msg-row.assistant .bubble {
background: var(--asst-bubble);
color: var(--text);
border-bottom-left-radius: 5px;
white-space: normal; /* Markdown içerik düzgün aksın */
}
/* Markdown içeriği stilleri */
.msg-row.assistant .bubble p { margin: 0 0 8px; }
.msg-row.assistant .bubble p:last-child { margin-bottom: 0; }
.msg-row.assistant .bubble ul, .msg-row.assistant .bubble ol { margin: 0 0 8px; padding-left: 20px; }
.msg-row.assistant .bubble h1, .msg-row.assistant .bubble h2,
.msg-row.assistant .bubble h3, .msg-row.assistant .bubble h4 { margin: 10px 0 6px; }
.msg-row.assistant .bubble a { color: var(--blue); }
.msg-row.assistant .bubble blockquote {
margin: 8px 0; padding-left: 12px;
border-left: 3px solid var(--hairline); color: var(--muted);
}
.msg-row.assistant .bubble code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
}
.msg-row.assistant .bubble :not(pre) > code {
background: rgba(0,0,0,.08); padding: 1px 5px; border-radius: 4px;
}
.msg-row.assistant .bubble pre {
background: #1C1C1E; color: #fff;
padding: 10px 12px; border-radius: 8px;
overflow-x: auto; font-size: 13px; line-height: 1.4;
}
.msg-row.assistant .bubble pre code { background: none; padding: 0; color: inherit; }
.msg-row.assistant .bubble table { border-collapse: collapse; margin: 8px 0; font-size: 13px; }
.msg-row.assistant .bubble th, .msg-row.assistant .bubble td { border: 1px solid var(--hairline); padding: 5px 8px; }
.msg-row.assistant .bubble th { background: rgba(0,0,0,.04); }
.copy-btn {
align-self: flex-start;
margin-top: 5px;
border: 1px solid var(--hairline);
background: #fff;
color: var(--muted);
border-radius: 999px;
padding: 2px 10px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
transition: color .15s ease, border-color .15s ease;
}
.copy-btn:hover { color: var(--text); border-color: #c0c0c6; }
@keyframes msg-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- Kaynak / doğrulama rozeti --- */
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 500;
line-height: 1.4;
background: var(--ok-bg);
color: var(--ok-fg);
}
.badge.warn { background: var(--warn-bg); color: var(--warn-fg); }
.badge-icon { display: inline-flex; }
.badge-icon svg { display: block; }
.typing-dots { display: inline-flex; align-items: center; gap: 5px; padding: 3px 2px; }
.typing-dots .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #A6A6AC;
animation: bounce 1.2s infinite ease-in-out;
}
.typing-dots .dot:nth-child(2) { animation-delay: .15s; }
.typing-dots .dot:nth-child(3) { animation-delay: .3s; }
@keyframes bounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
/* --- Alt yazma çubuğu --- */
.composer {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid var(--hairline);
flex-shrink: 0;
background: var(--surface);
}
.composer input {
flex: 1;
background: var(--input-bg);
border: 1px solid #E0E0E5;
border-radius: 22px;
padding: 11px 16px;
font-size: 15px;
font-family: inherit;
color: var(--text);
outline: none;
transition: border-color .15s ease, background .15s ease;
}
.composer input::placeholder { color: var(--muted); }
.composer input:focus { border-color: var(--blue); background: #fff; }
.composer input:disabled { opacity: .6; }
.send-btn {
width: 40px; height: 40px;
flex-shrink: 0;
border: none;
border-radius: 50%;
background: var(--blue);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background .15s ease, transform .1s ease;
}
.send-btn:hover { background: var(--blue-press); }
.send-btn:active { transform: scale(.92); }
.send-btn:disabled { background: var(--blue-disabled); cursor: default; transform: none; }
/* --- Dar ekran: panel çekmeceye dönüşür --- */
.backdrop {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,.3);
z-index: 15;
}
@media (max-width: 900px) {
.menu-btn { display: inline-flex; }
.panel {
position: fixed;
left: 0; top: 0; bottom: 0;
width: 280px;
z-index: 20;
transform: translateX(-100%);
transition: transform .25s ease;
box-shadow: 0 0 40px rgba(0,0,0,.2);
}
.panel.open { transform: translateX(0); }
.backdrop.show { display: block; }
}
</style>
</head>
<body>
<div class="app">
<aside class="panel" id="panel">
<div id="profileSummary"></div>
<div>
<button id="refreshBtn" class="refresh-btn">Güncelle</button>
<div id="refreshStatus" class="refresh-status"></div>
</div>
<div class="panel-section-title">Repolar</div>
<div id="repoList"></div>
</aside>
<div class="main">
<header class="topbar">
<div class="topbar-row">
<button class="menu-btn" id="menuBtn" aria-label="Repo panelini aç/kapat">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" aria-hidden="true">
<path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<div>
<h1>GitHub Asistanı</h1>
<p class="subtitle" id="subtitle">GitHub'daki projeler hakkında sor</p>
</div>
</div>
</header>
<div class="targetbar">
<input id="target" type="text" placeholder="GitHub kullanıcı adı veya link (örn. kadiirgok)"
autocomplete="off" aria-label="GitHub hedefi" />
<button id="load" class="load-btn">Yükle</button>
</div>
<main class="chat" id="chat">
<div class="empty-state" id="emptyState">
<div class="empty-icon">
<svg viewBox="0 0 24 24" width="46" height="46" fill="none" stroke="#007AFF"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
</div>
<h2>Merhaba 👋</h2>
<p>Yukarıya bir GitHub kullanıcı adı veya linki yazıp "Yükle" de. Ardından o kişinin projeleri hakkında soru sorabilirsin.</p>
</div>
</main>
<footer class="composer">
<input id="input" type="text" placeholder="Bir proje hakkında soru sor..."
autocomplete="off" aria-label="Soru" />
<button id="send" class="send-btn" aria-label="Gönder" title="Gönder">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M12 19V5m0 0-7 7m7-7 7 7" stroke="#fff" stroke-width="2.4"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</footer>
</div>
</div>
<div class="backdrop" id="backdrop"></div>
<script>
const chat = document.getElementById('chat');
const emptyState = document.getElementById('emptyState');
const subtitle = document.getElementById('subtitle');
const targetInput = document.getElementById('target');
const loadBtn = document.getElementById('load');
const input = document.getElementById('input');
const sendBtn = document.getElementById('send');
const panel = document.getElementById('panel');
const profileSummary = document.getElementById('profileSummary');
const repoList = document.getElementById('repoList');
const refreshBtn = document.getElementById('refreshBtn');
const refreshStatus = document.getElementById('refreshStatus');
const menuBtn = document.getElementById('menuBtn');
const backdrop = document.getElementById('backdrop');
let loaded = false;
const CHECK_ICON =
'<svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden="true">' +
'<path d="M3 8.5l3 3 7-7" stroke="currentColor" stroke-width="2" ' +
'stroke-linecap="round" stroke-linejoin="round"/></svg>';
const WARN_ICON =
'<svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden="true">' +
'<path d="M8 2 1.5 13h13L8 2Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>' +
'<path d="M8 6.5v3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>' +
'<circle cx="8" cy="11" r="0.9" fill="currentColor"/></svg>';
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
{ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
));
}
function hideEmpty() {
if (emptyState) emptyState.style.display = 'none';
}
function scrollToBottom() {
chat.scrollTop = chat.scrollHeight;
}
function addMessage(role, text, meta) {
hideEmpty();
const row = document.createElement('div');
row.className = 'msg-row ' + role;
const bubble = document.createElement('div');
bubble.className = 'bubble';
if (role === 'assistant' && typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
bubble.innerHTML = DOMPurify.sanitize(marked.parse(text, { breaks: true }));
} else {
bubble.textContent = text;
}
row.appendChild(bubble);
if (role === 'assistant' && meta && meta.kaynak !== 'none') {
row.appendChild(makeBadge(meta));
}
if (role === 'assistant' && meta) {
row.appendChild(makeCopyBtn(text));
}
chat.appendChild(row);
scrollToBottom();
}
function makeBadge(meta) {
const verified = !!meta.dogrulandi;
const badge = document.createElement('div');
badge.className = 'badge' + (verified ? '' : ' warn');
const icon = document.createElement('span');
icon.className = 'badge-icon';
icon.innerHTML = verified ? CHECK_ICON : WARN_ICON;
const kaynak = ({ local: 'local', web: 'web', none: 'kaynak yok' })[meta.kaynak]
|| meta.kaynak || '?';
const sure = (meta.sure_saniye != null)
? Number(meta.sure_saniye).toFixed(1).replace('.0', '') + 'sn'
: '';
const parts = [verified ? 'Doğrulandı' : 'Doğrulanamadı', kaynak, sure].filter(Boolean);
const text = document.createElement('span');
text.className = 'badge-text';
text.textContent = parts.join(' · ');
badge.appendChild(icon);
badge.appendChild(text);
return badge;
}
function makeCopyBtn(text) {
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'Kopyala';
btn.addEventListener('click', () => {
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Kopyalandı';
setTimeout(() => { btn.textContent = 'Kopyala'; }, 1200);
}).catch(() => {});
});
return btn;
}
function showTyping() {
const row = document.createElement('div');
row.className = 'msg-row assistant';
row.id = 'typing';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.innerHTML =
'<span class="typing-dots" aria-label="yazıyor">' +
'<span class="dot"></span><span class="dot"></span><span class="dot"></span>' +
'</span>';
row.appendChild(bubble);
chat.appendChild(row);
scrollToBottom();
}
function hideTyping() {
const el = document.getElementById('typing');
if (el) el.remove();
}
function setBusy(busy) {
sendBtn.disabled = busy;
input.disabled = busy;
loadBtn.disabled = busy;
targetInput.disabled = busy;
}
function renderProfile(d) {
const hedef = d.hedef || '';
const diller = d.diller || {};
const chips = Object.entries(diller).map(([lang, n]) =>
'<span class="chip">' + esc(lang) + ' (' + n + ')</span>').join('');
profileSummary.innerHTML =
(hedef ? '<div class="panel-title">' + esc(hedef) + '</div>' : '') +
'<div class="panel-stat">' + (d.repo_sayisi || 0) + ' repo · ★ ' +
(d.toplam_yildiz || 0) + '</div>' +
(chips ? '<div class="chips">' + chips + '</div>' : '');
const items = (d.repolar || []).map((r) => {
const title = r.html_url
? '<a class="repo-name" href="' + esc(r.html_url) + '" target="_blank" rel="noopener">' + esc(r.name) + '</a>'
: '<span class="repo-name">' + esc(r.name) + '</span>';
const lang = r.language ? '<span class="repo-lang">' + esc(r.language) + '</span>' : '';
const stars = r.stars ? '<span class="repo-stars">★ ' + esc(r.stars) + '</span>' : '';
const desc = r.description ? '<div class="repo-desc">' + esc(r.description) + '</div>' : '';
return '<li class="repo-item">' + title +
'<div class="repo-meta">' + lang + stars + '</div>' + desc + '</li>';
}).join('');
repoList.innerHTML = items
? '<ul class="repo-list">' + items + '</ul>'
: '<p style="font-size:13px;color:var(--muted);">Repo yok</p>';
}
async function loadProfile() {
try {
const res = await fetch('/profile');
if (!res.ok) return;
const d = await res.json();
loaded = true;
if (d.hedef) {
targetInput.value = d.hedef;
subtitle.textContent = d.hedef + ' · ' + d.repo_sayisi + ' repo yüklendi';
}
renderProfile(d);
hideEmpty();
} catch (err) { /* sunucu kapalıysa sessizce geç */ }
}
async function loadTarget() {
const hedef = targetInput.value.trim();
if (!hedef || loadBtn.disabled) return;
const eskiMetin = loadBtn.textContent;
loadBtn.disabled = true;
targetInput.disabled = true;
loadBtn.textContent = 'Yükleniyor…';
sendBtn.disabled = true;
input.disabled = true;
addMessage('assistant', '⏳ GitHub\'dan repolar çekiliyor, bu biraz sürebilir...');
try {
const res = await fetch('/ingest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hedef })
});
const data = await res.json();
if (!res.ok) {
const mesaj = data.mesaj || data.detail || ('HTTP ' + res.status);
subtitle.textContent = 'Hata: ' + mesaj;
addMessage('assistant', 'Yüklenemedi: ' + mesaj);
} else {
loaded = true;
subtitle.textContent = hedef + ' · ' + data.repo_sayisi + ' repo yüklendi';
addMessage('assistant', '✅ ' + data.repo_sayisi + ' repo yüklendi.');
await loadProfile();
}
} catch (err) {
subtitle.textContent = 'Bağlantı hatası';
addMessage('assistant', 'Bağlantı hatası: ' + err.message);
} finally {
loadBtn.disabled = false;
targetInput.disabled = false;
loadBtn.textContent = eskiMetin;
sendBtn.disabled = false;
input.disabled = false;
input.focus();
}
}
async function refreshTarget() {
if (!loaded) return;
refreshBtn.disabled = true;
refreshStatus.textContent = 'Kontrol ediliyor…';
try {
const res = await fetch('/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const d = await res.json();
if (d.guncel) {
refreshStatus.textContent = 'Zaten güncel';
} else {
refreshStatus.textContent = 'Eklendi: ' + (d.eklenen || []).length +
' · Kaldırıldı: ' + (d.kaldirilan || []).length +
' · Değişti: ' + (d.degisen || []).length;
subtitle.textContent = (d.hedef || '') + ' · ' + d.repo_sayisi + ' repo yüklendi';
await loadProfile();
}
} catch (err) {
refreshStatus.textContent = 'Hata: ' + err.message;
} finally {
refreshBtn.disabled = false;
}
}
async function send(question) {
question = (question || input.value).trim();
if (!question || sendBtn.disabled) return;
if (!loaded) {
addMessage('assistant', 'Önce yukarıya bir GitHub kullanıcı adı veya linki yazıp "Yükle" de.');
return;
}
addMessage('user', question);
input.value = '';
setBusy(true);
showTyping();
try {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ soru: question })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
hideTyping();
addMessage('assistant', data.cevap || '(boş cevap)', data);
} catch (err) {
hideTyping();
addMessage('assistant', 'Bir hata oluştu: ' + err.message);
} finally {
setBusy(false);
input.focus();
}
}
function setPanelOpen(open) {
panel.classList.toggle('open', open);
backdrop.classList.toggle('show', open);
}
loadBtn.addEventListener('click', loadTarget);
targetInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
loadTarget();
}
});
sendBtn.addEventListener('click', () => send());
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
});
refreshBtn.addEventListener('click', refreshTarget);
menuBtn.addEventListener('click', () => setPanelOpen(!panel.classList.contains('open')));
backdrop.addEventListener('click', () => setPanelOpen(false));
// Sayfa açılınca (sunucu yeniden başlamış olsa bile) yüklü hedefi geri getir.
loadProfile();
</script>
</body>
</html>