Kullanıcı testi hataları: sabit tarihler, kişisel varsayılanlar, kılavuz düğmesi, mobil başlık, AI taslak çökmesi

- Günlük Bülten ve İhale Takvimi sabit "10.08.2026" yerine bugünden hesaplar;
  takvim 7 günü gerçek ihale tarihleriyle sayar
- Ekip/üye/firma varsayılanları oturumdaki kullanıcıdan; sahte "Ekip Notları"
  ve sabit "%94 Uyum" rozeti kaldırıldı (gerçek uyum skoru varsa gösterilir)
- Kılavuz düğmesi document.body'ye portal (ekran dışına düşüyordu)
- Detay modalı havuzdaki güncel kaydı okur: Takip Et / Görev aç etiketleri güncellenir
- Mobil: başlıkta ikincil düğmeler md altında gizli, arama formu küçülebilir
- PWA sayfası: sahte önbellek sayıları yerine Cache API ve bellek sayımı
- Sözleşme sorgula pazar payı bir ondalık (hep %0 görünüyordu)
- Rakip Radar "SMS" vaadi düzeltildi; sektör=CPV etiketi tekrarı gizlendi;
  Stripe kurulum uyarısı yalnız yöneticiye
- Açılışta 7 gereksiz PUT /api/me/data: ilk yükleme yazımı atlanır
- server: teklif taslağı çökmesi (kalemOku nesne döndürür), denetim yanıtından
  model adı kaldırıldı, gereklilikler satır bazlı; derin analiz 12k token +
  kesik JSON onarımı

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 09:43:41 +03:00
co-authored by Claude Fable 5.1
parent 6d7d938711
commit 08c0a0af7e
14 changed files with 130 additions and 60 deletions
+14 -5
View File
@@ -71,7 +71,7 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) {
const out = await analyzeSpec({ text: `PARÇALAR:\n\n${baglam}\n\nSORU: ${q}\n\nYalnızca {"cevap":"..."} JSON'u döndür.`, tender, chain, systemOverride: sistem });
const cevap = String(out?.result?.cevap ?? '').trim() || 'Yanıt üretilemedi.';
const kullanilan = new Set([...cevap.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1])));
return { cevap, kaynaklar: (kullanilan.size ? kaynaklar.filter((k) => kullanilan.has(k.n)) : kaynaklar.slice(0, 3)).map((k) => ({ n: k.n, alinti: k.icerik.slice(0, 1400) })), kaynak, model: out.model?.label };
return { cevap, kaynaklar: (kullanilan.size ? kaynaklar.filter((k) => kullanilan.has(k.n)) : kaynaklar.slice(0, 3)).map((k) => ({ n: k.n, alinti: k.icerik.slice(0, 1400) })), kaynak };
}
function gereklilikMetni(tender, gereklilikler, metin) {
@@ -79,8 +79,17 @@ function gereklilikMetni(tender, gereklilikler, metin) {
for (const x of Array.isArray(gereklilikler) ? gereklilikler : []) if (x) g.push(typeof x === 'string' ? x : x.belge || x.item || x.ad || JSON.stringify(x));
for (const y of Array.isArray(tender?.yeterlikKriterleri) ? tender.yeterlikKriterleri : []) if (y) g.push(String(y));
for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim());
if (tender?.ikn && kalemStore.available()) for (const k of (kalemStore.kalemOku(tender.ikn) || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim());
if (metin) for (const s of ['teminat', 'teslim süresi', 'geçerlilik', 'ceza', 'numune', 'belge']) { const p = getir(parcala(metin), s, 1)[0]; if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`); }
if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim());
if (metin) {
// Önce anahtar sözcüğü geçen SATIR (madde) alınır; bütün metni yapıştırmak okunmaz oluyordu.
const satirlar = String(metin).split(/\r?\n+/).map((x) => x.trim()).filter((x) => x.length > 15);
for (const s of ['teminat', 'teslim süresi', 'geçerlilik', 'ceza', 'numune', 'belge']) {
const satir = satirlar.find((x) => x.toLocaleLowerCase('tr-TR').includes(s));
if (satir) { g.push(`${s}: ${satir.slice(0, 300).replace(/\s+/g, ' ')}`); continue; }
const p = getir(parcala(metin), s, 1)[0];
if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`);
}
}
return [...new Set(g)].slice(0, 60);
}
@@ -100,7 +109,7 @@ export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain,
});
const ok = maddeler.filter((m) => m.durum === 'karsilaniyor').length;
const yarim = maddeler.filter((m) => m.durum === 'belirsiz').length;
return { ozet: String(out?.result?.ozet ?? '').slice(0, 800), maddeler, uyum_yuzde: Math.round(((ok + yarim * 0.5) / maddeler.length) * 100), eksikler: maddeler.filter((m) => m.durum === 'eksik').map((m) => m.gereklilik), model: out.model?.label };
return { ozet: String(out?.result?.ozet ?? '').slice(0, 800), maddeler, uyum_yuzde: Math.round(((ok + yarim * 0.5) / maddeler.length) * 100), eksikler: maddeler.filter((m) => m.durum === 'eksik').map((m) => m.gereklilik) };
}
const BOLUMLER = {
@@ -127,5 +136,5 @@ export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, g
.map((b) => ({ baslik: String(b?.baslik ?? '').slice(0, 200), icerik: String(b?.icerik ?? '').slice(0, 12000) }))
.filter((b) => b.baslik && b.icerik);
if (!bolumler.length) throw new Error('Model bölüm üretmedi; tekrar deneyin.');
return { bolumler, model: out.model?.label };
return { bolumler };
}
+34 -2
View File
@@ -276,7 +276,39 @@ function extractJson(raw) {
const start = s.indexOf('{');
const end = s.lastIndexOf('}');
if (start === -1 || end === -1) throw new Error('Yanıtta JSON bulunamadı');
return JSON.parse(s.slice(start, end + 1));
const govde = s.slice(start, end + 1);
try { return JSON.parse(govde); } catch (e) {
const onarilmis = kesikJsonOnar(s.slice(start));
if (onarilmis) return onarilmis;
throw e;
}
}
/**
* Model çıktısı max_tokens'ta kesildiğinde JSON yarım kalır. Sondan geriye
* doğru güvenli kesme noktaları denenir; açık kalan dizi/nesneler kapatılır.
*/
function kesikJsonOnar(s) {
for (let cut = s.length; cut > 50; ) {
cut = Math.max(s.lastIndexOf('}', cut - 1), s.lastIndexOf(']', cut - 1), s.lastIndexOf('"', cut - 1));
if (cut <= 0) return null;
let parca = s.slice(0, cut + 1).replace(/,\s*$/, '');
// yığın: string dışındaki açık parantezler
const yigin = []; let strIcinde = false, kacis = false;
for (const ch of parca) {
if (strIcinde) { if (kacis) kacis = false; else if (ch === '\\') kacis = true; else if (ch === '"') strIcinde = false; continue; }
if (ch === '"') strIcinde = true;
else if (ch === '{' || ch === '[') yigin.push(ch);
else if (ch === '}' || ch === ']') yigin.pop();
}
if (strIcinde) { cut -= 1; continue; }
parca = parca.replace(/,\s*$/, '');
// "anahtar": ile biten yarım çift -> anahtarı at
parca = parca.replace(/,?\s*"[^"]*"\s*:\s*$/, '');
while (yigin.length) parca += yigin.pop() === '{' ? '}' : ']';
try { return JSON.parse(parca); } catch { cut -= 1; }
}
return null;
}
/** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */
@@ -288,7 +320,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
// kesilirse JSON parse edilemez ve zincir boşuna tüketilir.
const userPrompt = buildUserPrompt({ text, fileName, tender, limit: derin ? 90000 : 60000 });
const sistem = systemOverride || (derin ? DERIN_SYSTEM_PROMPT : SYSTEM_PROMPT);
const maxTokens = derin ? 8000 : 4000;
const maxTokens = derin ? 12000 : 4000;
const timeoutMs = derin ? 150000 : 90000;
const attempts = [];