DUSUNEN-Search-Lab / index.html
GoktugD's picture
Refresh DUSUNEN branding, releases and measured evidence
cf50a73 verified
Raw
History Blame Contribute Delete
12.5 kB
<!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#070a12">
<meta name="description" content="DUSUNEN Rota 270M ile tarayıcı içinde çalışan özel Türkçe anlamsal arama.">
<title>DUSUNEN Search Lab</title>
<style>
:root{--bg:#070a12;--panel:#0f1422;--line:#252c3f;--text:#f7f8ff;--muted:#9aa5bb;--cyan:#54d9e8;--violet:#a78bfa;--green:#91e6a8}
*{box-sizing:border-box}html{color-scheme:dark}body{margin:0;background:radial-gradient(circle at 15% 0,rgba(103,70,220,.22),transparent 34rem),radial-gradient(circle at 100% 35%,rgba(35,211,238,.12),transparent 30rem),var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;min-height:100vh}
body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.16;background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);background-size:48px 48px;mask-image:linear-gradient(#000,transparent 75%)}
.shell{position:relative;width:min(1040px,calc(100% - 32px));margin:auto;padding:34px 0 70px}
nav{display:flex;align-items:center;justify-content:space-between;padding-bottom:26px;border-bottom:1px solid var(--line)}
.brand{font-weight:800;letter-spacing:-.02em}.brand span{display:inline-grid;place-items:center;width:30px;height:30px;margin-right:10px;border:1px solid rgba(167,139,250,.5);border-radius:9px;color:var(--violet);font:700 11px ui-monospace,monospace}
nav a{color:var(--muted);text-decoration:none;font-size:13px;font-weight:650}nav a:hover{color:var(--text)}
header{padding:76px 0 38px}.kicker{color:var(--cyan);font:700 11px ui-monospace,monospace;letter-spacing:.14em;text-transform:uppercase}h1{max-width:820px;margin:18px 0;font-size:clamp(45px,8vw,78px);line-height:.98;letter-spacing:-.06em}.gradient{color:transparent;background:linear-gradient(100deg,#fff,var(--violet),var(--cyan));background-clip:text}.lead{max-width:760px;color:var(--muted);font-size:18px;line-height:1.65}.lead strong{color:var(--text)}
.privacy{display:inline-flex;align-items:center;gap:8px;margin-top:12px;color:var(--green);font:650 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em}.privacy:before{content:"";width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 14px var(--green)}
.search-box{padding:22px;border:1px solid var(--line);border-radius:19px;background:rgba(15,20,34,.9);box-shadow:0 24px 70px rgba(0,0,0,.28)}label{display:block;margin-bottom:9px;color:var(--muted);font:650 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em}.controls{display:grid;grid-template-columns:1fr auto;gap:10px}input{width:100%;height:54px;padding:0 17px;border:1px solid var(--line);border-radius:12px;background:#090d17;color:var(--text);font:500 16px inherit;outline:none}input:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(84,217,232,.1)}button{height:54px;padding:0 22px;border:0;border-radius:12px;background:var(--text);color:#090d17;font:800 14px inherit;cursor:pointer}button:hover{transform:translateY(-1px)}button:disabled{cursor:wait;opacity:.55;transform:none}
.examples{display:flex;flex-wrap:wrap;gap:7px;margin-top:12px}.example{height:auto;padding:7px 10px;border:1px solid var(--line);border-radius:9px;background:transparent;color:var(--muted);font-size:11px;font-weight:600}.example:hover{color:var(--text);border-color:#46516d}
.status-wrap{margin:16px 2px 0}.status-line{display:flex;justify-content:space-between;gap:12px;color:var(--muted);font:600 11px ui-monospace,monospace}.bar{height:3px;margin-top:9px;overflow:hidden;border-radius:2px;background:#1e2535}.bar span{display:block;width:0;height:100%;background:linear-gradient(90deg,var(--violet),var(--cyan));transition:width .25s}.note{margin:10px 0 0;color:#657089;font-size:11px;line-height:1.5}
#results{margin-top:26px}.result{padding:20px 22px;margin:11px 0;border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,rgba(16,22,37,.96),rgba(9,12,21,.96));animation:rise .35s both}.meta{display:flex;align-items:center;gap:9px;color:var(--violet);font:700 10px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em}.rank{color:var(--cyan)}.score{margin-left:auto;color:#6e7a91}.result h2{margin:11px 0 7px;font-size:18px;letter-spacing:-.02em}.result p{margin:0;color:#c6ccda;font-size:14px;line-height:1.65}
.empty{padding:30px;border:1px dashed var(--line);border-radius:15px;color:var(--muted);text-align:center}.facts{display:grid;grid-template-columns:repeat(4,1fr);margin-top:46px;border:1px solid var(--line);border-radius:16px;overflow:hidden}.fact{padding:18px;border-right:1px solid var(--line)}.fact:last-child{border:0}.fact strong{display:block;font-size:19px}.fact span{color:var(--muted);font:600 9px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em}
footer{display:flex;justify-content:space-between;gap:20px;margin-top:46px;padding-top:24px;border-top:1px solid var(--line);color:#667188;font-size:11px}footer a{color:#aeb8cd;text-decoration:none}
@keyframes rise{from{opacity:0;transform:translateY(8px)}}
@media(max-width:680px){.shell{width:min(100% - 22px,1040px);padding-top:22px}header{padding-top:54px}h1{font-size:44px}.lead{font-size:16px}.controls{grid-template-columns:1fr}button{width:100%}.facts{grid-template-columns:repeat(2,1fr)}.fact:nth-child(2){border-right:0}.fact:nth-child(-n+2){border-bottom:1px solid var(--line)}footer{flex-direction:column}}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<div class="shell">
<nav><div class="brand"><span>DSN</span>DUSUNEN Search Lab</div><a href="https://huggingface.co/GoktugD/DUSUNEN-Rota-270M-v1" target="_blank" rel="noreferrer">Model card ↗</a></nav>
<header>
<div class="kicker">270M encoder / ONNX / browser inference</div>
<h1>Türkçe anlamı.<br><span class="gradient">Cihazında ara.</span></h1>
<p class="lead">Anahtar kelime eşleşmesinin ötesinde, sorgunun anlamına göre en ilgili metinleri sıralayan <strong>açık Türkçe dense retrieval modeli.</strong></p>
<div class="privacy">Sorgu cihazından çıkmaz · ücretli API yok</div>
</header>
<main>
<section class="search-box">
<label for="query">Türkçe sorgu</label>
<div class="controls">
<input id="query" value="Bir modeli küçük GPU'da nasıl verimli eğitebilirim?" autocomplete="off" maxlength="300">
<button id="search">Modeli yükle ve ara</button>
</div>
<div class="examples">
<button class="example" data-query="Arama sistemlerinde kalite nasıl ölçülür?">Retrieval kalitesi</button>
<button class="example" data-query="Hard negative örnekleri retrieval modelini nasıl geliştirir?">Hard negatives</button>
<button class="example" data-query="Yapay zekâ sonuçları neden yeniden üretilebilir olmalı?">Reproducibility</button>
</div>
<div class="status-wrap">
<div class="status-line"><span id="status">Hazır · ilk arama modeli tarayıcı önbelleğine alır</span><span id="latency"></span></div>
<div class="bar"><span id="progress"></span></div>
<p class="note">İlk kullanımda doğrulanmış 570 MB karma-hassasiyetli model indirilir; sonraki ziyaretlerde tarayıcı önbelleği kullanılır. Cosine skorları olasılık değildir.</p>
</div>
</section>
<section id="results"><div class="empty">Bir sorgu çalıştırdığında en ilgili beş belge burada sıralanacak.</div></section>
<section class="facts" aria-label="Model facts">
<div class="fact"><strong>270M</strong><span>parameters</span></div>
<div class="fact"><strong>640</strong><span>embedding dim</span></div>
<div class="fact"><strong>0.99996</strong><span>PyTorch agreement</span></div>
<div class="fact"><strong>0 API</strong><span>local query inference</span></div>
</section>
</main>
<footer><span>Demo corpus is separate from training and benchmark data.</span><span><a href="https://huggingface.co/datasets/GoktugD/DUSUNEN-Retrieval-100K-v1">Dataset</a> · <a href="https://huggingface.co/GoktugD">Göktuğ Düşünen</a></span></footer>
</div>
<script type="module">
const queryInput = document.querySelector("#query");
const searchButton = document.querySelector("#search");
const status = document.querySelector("#status");
const latency = document.querySelector("#latency");
const progress = document.querySelector("#progress");
const results = document.querySelector("#results");
const worker = new Worker("./worker.js", { type: "module" });
let documents = [];
let embeddings = [];
let requestId = 0;
let modelReady = false;
const assets = Promise.all([
fetch("./corpus.json").then((r) => r.json()),
fetch("./embeddings.json").then((r) => r.json()),
]).then(([docs, vectors]) => { documents = docs; embeddings = vectors; });
function cosine(a, b) {
let score = 0;
for (let i = 0; i < a.length; i++) score += a[i] * b[i];
return score;
}
function render(vector, inferenceMs) {
const ranked = embeddings
.map((embedding, index) => ({ index, score: cosine(embedding, vector) }))
.sort((a, b) => b.score - a.score)
.slice(0, 5);
results.replaceChildren(...ranked.map(({ index, score }, rank) => {
const item = documents[index];
const card = document.createElement("article");
card.className = "result";
const meta = document.createElement("div"); meta.className = "meta";
const rankEl = document.createElement("span"); rankEl.className = "rank"; rankEl.textContent = `#${rank + 1}`;
const category = document.createElement("span"); category.textContent = item.category;
const scoreEl = document.createElement("span"); scoreEl.className = "score"; scoreEl.textContent = `cosine ${score.toFixed(4)}`;
meta.append(rankEl, category, scoreEl);
const title = document.createElement("h2"); title.textContent = item.title;
const text = document.createElement("p"); text.textContent = item.text;
card.append(meta, title, text);
return card;
}));
latency.textContent = `${inferenceMs.toFixed(0)} ms · 24 belge`;
status.textContent = "Tarayıcı içi ONNX araması tamamlandı";
progress.style.width = "100%";
searchButton.disabled = false;
searchButton.textContent = "Anlamsal arama";
modelReady = true;
}
async function search() {
const query = queryInput.value.trim();
if (query.length < 3) { status.textContent = "En az üç karakter yaz."; return; }
await assets;
searchButton.disabled = true;
searchButton.textContent = modelReady ? "Aranıyor…" : "Model yükleniyor…";
status.textContent = modelReady ? "Sorgu cihazında kodlanıyor" : "ONNX model dosyası hazırlanıyor";
progress.style.width = modelReady ? "65%" : "4%";
worker.postMessage({ type: "search", query, requestId: ++requestId });
}
worker.onmessage = ({ data }) => {
if (data.type === "progress") {
const event = data.event || {};
if (event.progress != null) progress.style.width = `${Math.max(4, Math.round(event.progress))}%`;
if (event.file) status.textContent = `Model yükleniyor · ${event.file.split("/").pop()}`;
} else if (data.type === "result" && data.requestId === requestId) {
render(data.embedding, data.inferenceMs);
} else if (data.type === "error") {
status.textContent = `Yükleme hatası: ${data.message}`;
progress.style.width = "0";
searchButton.disabled = false;
searchButton.textContent = "Yeniden dene";
}
};
searchButton.addEventListener("click", search);
queryInput.addEventListener("keydown", (event) => { if (event.key === "Enter") search(); });
document.querySelectorAll(".example").forEach((button) => button.addEventListener("click", () => {
queryInput.value = button.dataset.query; search();
}));
</script>
</body>
</html>