Bu yazı da Google Chrome da bulunan Client-side yapay zeka modellerini ve bu modelleri nasıl kullanabileceğimizden bahsedeceğim. Önce teorik bir giriş yaptıktan sonra pratik yapacağız. Örneğimiz Prompt API ile e-ticaret ürün detay sayfalarında kullanılabilecek ve ürün hakkında bilgi verecek ve sadece Client-side yapay zeka modeli kullanan bir chatbot yapacağız.

Google Chrome bilgisayarınıza indirmeniz ile birlikte Browser içindeki yapay zeka araçlarını kullanabileceğinizi öğrendiğime göre öğretmenin de vakti geldi. Ne kadar yapay zekadan etkilenip yazmasam da (2yıl olmuş) nasıl olsa AI cevabını veriyor yazmaya gerek yok kimse okumaz desem de türkçe kaynak da olmadığı için yazmak istedim.

Client-side AI Nedir?

Web üzerindeki çoğu yapay zeka özelliği sunuculara bağlıdır, Client-side AI ise kullanıcının tarayıcısında çalışır ve işlemi kullanıcının cihazında gerçekleştirir. Bu işlem daha düşük gecikme süresi, maliyetinin azalması, kullanıcı gizliliğinin artması ve çevrimdışı erişim gibi birçok avantaja sahiptir.

Client-side tarafındaki yapay zeka, performans için optimize edilmiş daha küçük, optimize edilmiş modellere barındırır. Bu tür modellerin belirli görevler için daha büyük sunucu tarafı modellerden daha iyi performans gösterir.

https://developer.chrome.com/docs/ai/built-in/overview

Built-in AI Nedir?

Built-in AI, daha küçük modellerin tarayıcıya entegre edildiği bir Client-side AI biçimidir. Chrome için bu, Gemini Nano ve uzman modelleri içerir. Bu modeller indirildikten sonra, Built-in AI kullanan tüm web siteleri ve web uygulamaları indirme süresini atlayabilir ve doğrudan bu modelleri kullanabilir. Bu sayede web siteniz veya web uygulamanız, modelleri dağıtmanıza, yönetmenize veya kendi sunucunuzda barındırmanıza gerek kalmadan yapay zeka destekli görevleri gerçekleştirebilirsiniz.

Tamamen tarayıcıda çalışan Built-in AI, sunucuda maliyet açısından çok yüksek olacak kişiselleştirilmiş özellikleri kullanıma sunmanıza olanak tanır. Token faturalarını ve diğer engelleri atlayarak tamamen benzersiz bir kullanıcı deneyimi sağlarsınız. En önemlisi, tarayıcı optimize edilmiş modeller sayesinde daha fazla kullanıcının web’de yapay zeka deneyimlerinden yararlanmasını sağlar.

Built-in AI APIleri nelerdir?

Built-in AI içinde kullanabileceğiniz 7 adet API vardır.

  • Translator API: Translator API’si Chrome 138 sürümünden itibaren kullanılabilir. Kullanıcı tarafından oluşturulan ve dinamik içerikleri istek üzerine çevirebilirsiniz.
  • Language Detector API: Language Detector API’si Chrome 138 sürümünden itibaren kullanılabilir. Bu API’yi kullanarak metnin dilini algılayabilirsiniz. Bu, çeviri sürecinin önemli bir parçasıdır, çünkü çeviri için giriş dilini her zaman bilemeyebilirsiniz.
  • Summarizer API: Summarizer API’si Chrome 138 sürümünden itibaren kullanılabilir. Bu API ile uzun içerikleri kısaltabilirsiniz. Daha kısa içerikler kullanıcılar için daha erişilebilir ve kullanışlı olabilir.
  • Writer and Rewriter APIs: Writer API’si, belirtilen bir yazma görevine uygun yeni içerik oluşturmanıza yardımcı olurken, Rewriter API’si metni gözden geçirmenize ve yeniden yapılandırmanıza yardımcı olur.
  • Prompt API: Prompt API sayesinde, Chrome üzerinden Gemini Nano’yu kullanabilirsiniz.
  • Proofreader API: Proofreader API’si, deneme sürümü olarak kullanıma sunulmuştur. Bu API ile web uygulamanızda veya Chrome uzantınızda kullanıcılarınıza etkileşimli yazım denetimi sağlayabilirsiniz.
https://developer.chrome.com/docs/ai/built-in-apis

Gemini Nano Nedir?

Gemini Nano, üretken bir yapay zeka modelidir. Chrome, Gemini Nano dil modellerini kullanır. Gemini Nano mobil cihazlarda kullanılamaz.

Built-in AI API nasıl kullanılır?

Chrome’da API’ler ve modeller yerleşik olarak bulunur. Kullanıcı bu API’lerle ilk kez etkileşim kurduğunda, modelin tarayıcıya indirilmesi gerekir.

Tüm APIler Chome dalocalhostüzerinde kullanılır.

  1. chrome://flags/#optimization-guide-on-device-model. bu adrese gidin
  2. bunu Enabled BypassPerfRequirement yapın.
  3. chrome://flags/#prompt-api-for-gemini-nano bu adrese gidin. 4GBlık bir model.
  4. bunu Enabled yapın.
  5. Chrome yeniden başlatın.
  6. chrome://on-device-internals/ buradan model statusden modelin indirildiğinden emin olun.
  7. DevTools Console açarak await LanguageModel.availability(); bu komutu yapıştırın. available şeklinde response dönüyorsa başarılı.

Prompt API ile AI Chatbot nasıl geliştirilir?

Tüm buraya kadar anlayarak geldiysek örneğe başlayalım. Örneğimiz herhangi bir eticaret sitesindeki bir ürünün detay sayfasında (PDP- product detail page) ürün hakkında bilgi veren AI chatbotu olacak.

Bu örnekte Trendyol gibi bir e-ticaret sitesinin ürün sayfasında DevTools konsoluna tek satır kod yapıştırıyorsun sağ altta yeşil bir “Trendyol’a sor” butonu beliriyor. Tıkladığında sağdan bir sohbet paneli açılıyor, ürünün görselini/fiyatını/puanını gösteriyor ve “Saklama alanı var mı?”, “Kaç kişilik?” gibi soruları yanıtlıyor.

Kritik nokta: cevabı üreten model tarayıcının içinde çalışan Gemini Nano. Ürün bilgisi hiçbir yere gönderilmiyor, hiçbir API anahtarı yok, hiçbir maliyet yok.

Bu kısımda anlatacaklarım;

  • Projeyi nasıl kurdum, nelerden oluşuyor?
  • Ürün verisini sayfadan nasıl kazıdım?
  • Prompt’u nasıl kurguladım? (halüsinasyonu nasıl engelledim
  • Prompt API nasıl kullanılır?
  • Arayüzü nasıl yaptım
  • Kullanıcının bunu nasıl kullanabilir?

1. Mimari: neden eklenti değil de konsol snippet yazdım

Bunu bir Chrome eklentisi olarak da yazabilirdim. Ama amacım hızlı bir kanıt/demo üretmekti: herhangi bir ürün sayfasında, kurulum gerektirmeden, tek bir kod parçasıyla çalışan bir asistan. Bu yüzden proje bir eklenti değil kendi kendine yeten tek bir JavaScript bundle’ından oluşuyor. Kodu bir sayfaya enjekte ediyorsun, o da o anki DOM’u okuyup çalışıyor.

console snippet  →  public/pdp-assistant.js (Vercel'de host'lanıyor)
├─ extractor.js canlı DOM'dan ürün gerçeklerini okur
│ (JSON-LD → __NEXT_DATA__ → DOM/meta)
├─ prompt.js system prompt kurar
├─ nano.js LanguageModel oturumu (cihaz üstünde)
└─ ui.js Shadow-DOM sohbet widget'ı + tüm durumlar

Her modülün tek bir sorumluluğu var:

  • extractor.jsSayfadaki DOM'dan ürün bilgilerini çıkartır.
  • prompt.jsBu gerçeklerden dayanaklı bir system prompt üretir
  • nano.jsChrome Prompt API'sini (Gemini Nano) sarmalar
  • ui.jsShadow DOM içindeki sohbet arayüzünü çizer
  • main.jsHepsini birbirine bağlayan kontrolcü

Framework yok sadece JavaScript. Tek dış bağımlılık, o da sadece build için: esbuild.

2. Projeyi nasıl kurdum

package.json son derece sade. type: "module", tek devDependency esbuild ve üç script:

{
"name": "pdp-nano-assistant",
"type": "module",
"scripts": {
"build": "node build.mjs",
"watch": "node build.mjs --watch",
"serve": "node serve.mjs"
},
"devDependencies": { "esbuild": "^0.21.5" }
}

build.mjs, src/main.js'i tüm modülleriyle birlikte tek bir dosyaya paketliyor. Bunu bir sayfanın konsoluna yapıştıracağımız için çıktının self-contained ve global güvenli olması şart bu yüzden format iife:

IIFE (Immediately Invoked Function Expression), tanımlandığı anda otomatik olarak kendiliğinden çalışan bir JavaScript fonksiyonudur. (MDN Web Docs)
const options = {
entryPoints: ["src/main.js"],
outfile: "public/pdp-assistant.js",
bundle: true,
minify: !watch,
format: "iife", // sayfaya yapıştırılınca global'leri kirletmesin
target: ["chrome120"],
banner: { js: "/* PDP Nano Assistant — local Gemini Nano. No cloud calls for answers. */" },
};

npm run build → public/pdp-assistant.js. npm run serve ise test/pdp.html sahte bir ürün sayfası sunan, CORS'u açık küçük bir test sunucusu başlatıyor böylece canlı siteye ihtiyaç duymadan tüm akışı lokalde deneyebiliyorum.

3. Ürün verisini sayfadan nasıl çıkarttım

Bir dil modeline “bu ürün hakkında soru yanıtla” demek istiyorsan, önce ona ürün hakkında gerçek bilgi vermelisin. Aksi halde model uydurmaya (halüsinasyon) başlar. İşin en kritik ve en özgün kısmı bu: extractor.js.

Bundle sayfaya hydrate olduktan sonra enjekte edildiği için, canlı DOM’da client-side render edilmiş içerik de elimizde. Veriyi tek bir kaynaktan değil, öncelik sırasıyla katman katman topluyorum:

  1. JSON-LD (Product şeması) en yapılandırılmış, en güvenilir kaynak
  2. Framework state (__NEXT_DATA__ içinde ürün nesnesini sezgisel arama)
  3. DOM / meta fallback (h1, breadcrumb, fiyat regex, Open Graph, spec tabloları)
  4. Yorum / Soru-Cevap / politika metni (taksit, iade, kargo)

En temiz kaynak olan JSON-LD çıkarımından bir kesit:

JSON-LD (JavaScript Object Notation for Linked Data), web sayfalarındaki karmaşık verilerin arama motorları ve diğer makineler tarafından kolayca anlaşılmasını sağlayan bir yapılandırılmış veri formatıdır. (Webtures)
function factsFromJsonLd(product, facts) {
push(facts, "Name", product.name, "JSON-LD");
push(facts, "Brand", product.brand && (product.brand.name || product.brand), "JSON-LD");
push(facts, "Color", product.color, "JSON-LD");
push(facts, "Material", product.material, "JSON-LD");
push(facts, "Description", product.description, "JSON-LD");

const offers = Array.isArray(product.offers) ? product.offers[0] : product.offers;
if (offers && typeof offers === "object") {
const price = offers.price || (offers.priceSpecification && offers.priceSpecification.price);
if (price) push(facts, "Price", clean(price) + ..., "JSON-LD");
}
// aggregateRating, review, additionalProperty ... hepsi push ediliyor
}

push() yardımcısı, aynı etiketin (label) tekrar yazılmasını engelliyor yani JSON-LD bir alanı zaten doldurduysa, daha zayıf kaynaklar (DOM) onu ezmiyor. Bu sayede "önce en güvenilir kaynak kazanır" mantığı otomatik işliyor.

Ve çok önemli bir detay Gemini Nano’nun context penceresi sınırlı. Bu yüzden çıkardığım veriyi bir üst sınırla kırpıyorum:

const MAX_FACTS_CHARS = 5000; // Nano'nun context penceresine sığdır

let text = "";
for (const f of facts) {
const line = `${f.label}: ${f.value}\n`;
if (text.length + line.length > MAX_FACTS_CHARS) break;
text += line;
}

Sonuçta elimde şuna benzeyen kompakt bir "field: value" bloğu oluyor:

Name: Serra 3 Kişilik Yataklı Kanepe
Color: Antrasit Gri
Category: Mobilya > Salon ve Oturma Odası > Kanepe
Price: 12.999 TL
Rating: 3.4 (6 reviews)
...

İşte modele “gerçek” olarak vereceğim şey tam olarak bu.

4. Prompt nasıl yazdım

prompt.js iki şey yapıyor: modele davranış kurallarını veren bir system metni, ve gerçekleri bu metnin içine gömen bir sarmalayıcı var.

System prompt’un en kritik özelliği sadece verilen verilerden yanıt ver, spec uydurma, cevap yoksa “sayfa bunu belirtmiyor” de, ve kullanıcının diliyle cevap ver (Türkçe soru → Türkçe cevap):

const SYSTEM = [
"You are a shopping assistant embedded on a single product page of an online store.",
"Answer ONLY using the PRODUCT FACTS provided below.",
"Do not invent specifications, measurements, materials, or features that are not in the facts.",
"If the facts do not contain the answer, say clearly that the page does not provide this information",
"and suggest what the shopper could check (e.g. the seller, Q&A, or product manual).",
"Reply in the SAME language as the user's question (Turkish question -> Turkish answer, English -> English).",
"Be concise and concrete. When useful, you may make a light, clearly-hedged inference from the facts",
"(e.g. a 3-seat sofa-bed in the 'living room' category is suitable for a living room),",
"but never present an inference as a confirmed spec.",
].join(" ");

Sonra çıkardığım veriyi sınırlayıcılar arasına gömüyorum. Bu sınırlayıcılar modele “işte tek doğru kaynağın bu” mesajını net veriyor:

export function buildSystemPrompt(factsText) {
return (
SYSTEM +
"\n\n=== PRODUCT FACTS (the only source of truth) ===\n" +
(factsText && factsText.trim() ? factsText.trim() : "(no product facts could be extracted from this page)") +
"\n=== END PRODUCT FACTS ==="
);
}

5. Prompt API’yi nasıl kullandım

Şimdi asıl konuya geldik. Chrome 138+'da Prompt API, global bir LanguageModel nesnesi olarak geliyor. Ama daha eski deneysel sürümler bunu window.ai.languageModel altında sunuyordu. İkisini de desteklemek için önce bir tespit/fallback katmanı yazdım:

function getModelApi() {
if (typeof LanguageModel !== "undefined") return LanguageModel;
if (typeof window !== "undefined" && window.ai && window.ai.languageModel) return window.ai.languageModel;
return null;
}

export function isSupported() {
return getModelApi() != null;
}

Model cihazda hazır mı, indirilmesi mi gerekiyor, yoksa hiç desteklenmiyor mu? Bunu availability() söylüyor. Ancak Chrome sürümleri arasında dönen string'ler farklı (readily, after-download, no gibi eski değerler vs. yeni değerler). Hepsini tek bir sözlüğe normalize ediyorum:

function normalizeAvailability(raw) {
switch (raw) {
case "available":
case "readily": return "available";
case "downloadable":
case "after-download": return "downloadable";
case "downloading": return "downloading";
default: return "unavailable";
}
}

export async function getAvailability() {
const api = getModelApi();
if (!api) return "unavailable";
try {
const raw = await api.availability();
return normalizeAvailability(raw);
} catch (e) {
console.warn("[pdp-nano] availability() failed", e);
return "unavailable";
}
}

Burada önemli bir kısım var: LanguageModel.create() bir kullanıcı tepkisi gerektiriyor. Bu yüzden oturumu sayfa yüklenirken değil, kullanıcı ilk soruyu sorduğunda kuruyorum. Oturumu system prompt ile tohumluyorum ve modelin tek seferlik indirmesini monitor üzerinden dinleyip yüzde olarak arayüze aktarıyorum:

async ensure({ systemPrompt, onDownloadProgress }) {
if (this._session && this._systemPrompt === systemPrompt) return this._session;
// grounding context değiştiyse oturumu yeniden kur
if (this._session) { try { this._session.destroy(); } catch {} this._session = null; }

const api = getModelApi();
if (!api) throw new Error("LanguageModel API is not available in this browser.");

this._systemPrompt = systemPrompt;
this._session = await api.create({
initialPrompts: [{ role: "system", content: systemPrompt }],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
// e.loaded güncel Chrome'da 0..1 aralığında
if (onDownloadProgress) onDownloadProgress(Math.round((e.loaded || 0) * 100));
});
},
});
return this._session;
}

Cevabı tek seferde beklemek yerine promptStreaming ile parça parça alıyorum ki kullanıcı yazı yazılıyormuş gibi anında görsün. Ama farklı Chrome sürümleri bu parçaları iki farklı biçimde yolluyor: Delta biçimi (sadece yeni eklenen kısmı yollar):

chunk 1: "Bu"
chunk 2: " kanepe"
chunk 3: " 3 kişilik"

Bunları ekranda birleştirmek için hepsini arka arkaya eklersin (append) → "Bu kanepe 3 kişilik" ✅

Kümülatif biçim (her seferinde baştan tüm metni yollar):

chunk 1: "Bu"
chunk 2: "Bu kanepe"
chunk 3: "Bu kanepe 3 kişilik"

Burada aynı append’i yaparsan → "Bu" + "Bu kanepe" + "Bu kanepe 3 kişilik" = "BuBu kanepeBu kanepe 3 kişilik" Metin tekrar tekrar birikir, bozulur. UI’ın hep basitçe “gelen parçayı sona ekle” diyebilmesini istiyorum. O yüzden ask() içinde her iki biçimi de delta'ya çeviriyorum.

async *ask(question, opts = {}) {
if (!this._session) throw new Error("Session not initialised; call ensure() first.");
const stream = this._session.promptStreaming(question, { signal: opts.signal });
let prev = "";
for await (const chunk of stream) {
// Bazı sürümler kümülatif metin, bazıları delta yollar. Delta'ya normalize et.
if (chunk.startsWith(prev)) {
const delta = chunk.slice(prev.length);
prev = chunk;
if (delta) yield delta;
} else {
prev += chunk;
yield chunk;
}
}
}

6. Arayüz nasıl oluşturdum

ui.js tüm arayüzü tek bir AssistantUI sınıfında çiziyor. En kritik tasarım kararı Shadow DOM kullanmak oldu. Çünkü bu widget rastgele bir e-ticaret sayfasının içine giriyor o sayfanın CSS'i benim stillerimi bozmamalı, benim stillerim de sayfayı bozmamalı. Shadow DOM bu izolasyonu tam olarak sağlıyor:

Shadow DOM, HTML ve CSS yapılarını izole ederek bağımsız bileşenler oluşturmasını sağlayan bir web standartıdır.
this.host = document.createElement("div");
this.host.id = HOST_ID;
const root = this.host.attachShadow({ mode: "open" });

Görsel olarak sağ altta yeşil bir “pill” launcher, tıklayınca sağdan açılan 400px'lik tam boy bir panel. Panelin içinde header, bir ürün kartı (görsel + başlık + kırmızı fiyat + yıldız puanı), sohbet gövdesi, başlangıç soru kalıpları, input ve footer ekledim

<div class="pcard">
<img class="img" src="..." />
<div class="info">
<div class="ptitle">${p.title}</div>
<div class="pmeta">
<span class="price">${p.price}</span>
<span class="rating">★ ${shortRating(p.rating)}</span>
</div>
<a class="plink" href="${p.url}">Ürün sayfası ↗</a>
</div>
</div>

Widget’ın yönettiği tüm görsel durumlar:

  • Yükleniyor — “Ürün sayfası okunuyor”
  • Boş — “Merhaba! 👋” + başlangıç soru kalıpları
  • İndiriliyor — Nano’nun tek seferlik indirmesi için progress bar
  • Desteklenmiyor — flag’leri açma rehberi (chrome://flags/...)
  • Yanıtlıyor — yanıp sönen bir ▋ imleçle streaming; bitince Kaynak: satırı

Streaming sırasındaki o “yazıyor” imleci saf CSS:

.typing::after { content: "▋"; animation: blink 1s steps(2) infinite; }

7. Tüm parçaları birleştirelim

virtuöz

main.js orkestra şefi, virtuöz boot() fonksiyonu tüm parçaları birleştiriyor ve modüllerin buluştuğu o iki satır aslında tüm mimariyi özetliyor:

const product = extractProduct();                    // extractor
const systemPrompt = buildSystemPrompt(product.text); // prompt
const session = new NanoSession(); // nano

Kullanıcı bir soru sorduğunda çalışan handleAsk akışı şöyle: mesajı ekle → oturumu hazırla (gerekiyorsa indirme yüzdesini göster) → cevabı stream'le → bitince kaynak ekle:

async function handleAsk(question) {
ui.addUserMessage(question);
const out = ui.startAssistantMessage();

await session.ensure({
systemPrompt,
onDownloadProgress: (pct) => out.status(`Downloading Gemini Nano… ${pct}%`),
});

for await (const delta of session.ask(question)) {
out.append(delta);
}
out.done(matchSources(product.facts, out._text));
}

O matchSources fonksiyonu güzel bir detay. Modelin içini göremediğimiz için hangi gerçeği kullandığını kesin bilemeyiz; bu yüzden best effort bir atıf yapıyorum cevaptaki kelimeleri, çıkardığım gerçeklerin değerleriyle eşleştirip "muhtemelen bu bilgilerden yararlandı" diyorum. Cevabın altında görünen Kaynak: Color, Category satırı işte buradan geliyor gerçekten çalıştığının görünür kanıtı.

main.js ayrıca hostname'den launcher etiketini türetiyor (trendyol.com→ "Trednyol'a sor"), favicon'u buluyor ve ekliyorum.

8. Hadi Test Edelim

Bundle Vercel’de host’ladım denemeniz için üç farklı kullanma yöntemi var. Hangisini kullanacağınız, hedef sayfanın Content-Security-Policy (CSP) ayarına bağlı. Ben 3. yöntemle direkt dev console script yapıştırarak deneyeceğiz sizde istediğiniz ürün için deneyebilirsiniz.

  1. <script> tag'i enjekte edebeilirsiniz
(()=>{const s=document.createElement('script');s.src='https://pdp-nano-assistant.vercel.app/pdp-assistant.js?v='+Date.now();s.onerror=()=>console.warn('[pdp-nano] script blocked — try the fetch() snippet');document.head.appendChild(s);})();

2) fetch() + eval olarak ekleyebilirsiniz

fetch('https://pdp-nano-assistant.vercel.app/pdp-assistant.js').then(r=>r.text()).then(c=>(0,eval)(c)).catch(e=>console.warn('[pdp-nano] fetch blocked',e));

3) https://pdp-nano-assistant.vercel.app/pdp-assistant.js script buradan kopyalayıp direkt konsola yapıştırabilirsiniz.

Sonuç:

trendyol pdp AI chatbot
trendyol pdp aı chatbot

Yazının sonuna geldik arkadaşlar Repo’yu inceleyebilirsiniz, kendi favori e-ticaret sitenizde deneyebilirsiniz.

Okuduğunuz için teşekkürler takip edip abone olursanız sevinirim. claps yapmayı unutmayınız. 🥳🤩👏

Kaynaklar;