Spaces:
Running
Running
| <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> | |