Files
ihaleist/src/components/TenderDocuments.jsx
T

516 lines
25 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { DocumentTextReview } from './DocumentTextReview';
import { readableDocument, inspectStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
import { documentProcessingLabel } from '../services/documentProcessing';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useApp } from '../context/AppContext';
import { getToken } from '../services/auth';
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download, Upload, Trash2, Paperclip, Route } from 'lucide-react';
import { uploadDocument, listTenderDocuments, deleteDocument, reportDocumentReadError } from '../services/account';
import { extractText } from '../services/pdfText';
/**
* İhale dokümanları paneli.
* - EKAP doküman bağlantısını üretip kullanıcının tarayıcısında açar
* - İlan metnini (ilan + OKAS kalemleri) çeker ve doğrudan AI analizine verir
*/
export const TenderDocuments = ({tender}) => {
const {authUser} = useApp();
return <TenderDocumentSession key={`${authUser?.email || ''}:${tender.ikn || tender.id}`} tender={tender} />;
};
const TenderDocumentSession = ({ tender }) => {
const { analyzeSpecPdfWithGemini, setIsAiSpecAnalyzerOpen } = useApp();
const readLifetime = useRef(new AbortController());
const sessionToken = useRef(getToken());
const current = () => !readLifetime.current.signal.aborted && Boolean(sessionToken.current) && sessionToken.current === getToken();
const assertCurrent = () => {
readLifetime.current.signal.throwIfAborted();
if (!current()) throw new Error('Oturum değişti; belgeyi yeniden açın.');
};
useEffect(() => {
readLifetime.current = new AbortController();
return () => readLifetime.current.abort();
}, []);
const useOwnedState = initial => {
const [value,setValue] = useState(initial);
const setOwned = useCallback(next => {if(current())setValue(next);},[]);
return [value,setOwned];
};
const [busy, setBusy] = useOwnedState(null);
const [error, setError] = useOwnedState(null);
const [bilgi, setBilgi] = useOwnedState(null);
const [ilerleme, setIlerleme] = useOwnedState('');
const [metinOnizleme, setMetinOnizleme] = useOwnedState(null);
// Son üretilen EKAP doküman adresi — yeni sekme engellenirse kullanıcı
// gerçek bir bağlantıya tıklayarak açabilsin (bağlantı tıklaması engellenmez).
const [ekapUrl, setEkapUrl] = useOwnedState(null);
// Eski kayıtlarda İKN boşluklu gelebiliyor (" 2026/1622878"); EKAP tespiti
// ve sunucu çağrıları boşluksuz değerle yapılır.
const ikn = String(tender?.ikn || '').replace(/[\s\u00a0]+/g, '') || undefined;
const [belgeler, setBelgeler] = useOwnedState([]);
const [yukleniyor, setYukleniyor] = useOwnedState(false);
const [listeHata, setListeHata] = useOwnedState(null);
const [listeBekliyor, setListeBekliyor] = useOwnedState(false);
const listeIstegi = useRef(0);
const onizlemeIstegi = useRef(0);
const dosyaRef = useRef(null);
const belgeleriYukle = useCallback(async () => {
if (!ikn || !current()) return;
const request = ++listeIstegi.current;
setListeBekliyor(true); setListeHata(null);
try {
const rows = await listTenderDocuments(ikn);
if (request === listeIstegi.current) setBelgeler(rows);
} catch (e) {
if (request === listeIstegi.current) setListeHata(e.message);
} finally {
if (request === listeIstegi.current) setListeBekliyor(false);
}
}, [ikn]);
useEffect(() => { setBelgeler([]); setMetinOnizleme(null); belgeleriYukle(); return () => { listeIstegi.current++; onizlemeIstegi.current++; }; }, [belgeleriYukle]);
// EKAP dokümanı kullanıcının tarayıcısına iniyor; indirdiği dosyayı buraya
// yükleyince ihaleye bağlanır ve metni AI analizine verilebilir.
const dosyaSec = async (e) => {
const f = e.target.files?.[0];
e.target.value = '';
if (!f) return;
if (f.size > 10 * 1024 * 1024) {
setError(`Yüklenemedi: ${f.name} ${(f.size / 1048576).toFixed(1)} MB — sınır 10 MB.`);
return;
}
setYukleniyor(true); setError(null);
try {
// Sunucu docx/xlsx/zip için teklif cetveli kalemlerini yüklemede
// otomatik çıkarır. PDF tarayıcıda çözüldüğü için metni biz göndeririz.
assertCurrent();
const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, {documentType:'ihale'});
assertCurrent();
let kalem = cevap?.kalem?.sayi || null;
let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null;
if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) {
try {
let text;
try { ({text} = await extractText(f,{signal:readLifetime.current.signal})); }
catch (readError) {
assertCurrent();
if(readError.name === 'AbortError')throw readError;
try { await reportDocumentReadError(cevap.id,cevap.belgeler.find(b=>b.id===cevap.id)?.isleme); }
catch { throw new Error(`${readError.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
throw readError;
}
const { kalemCikar } = await import('../services/kalem');
assertCurrent();
const d = await kalemCikar({ ikn, metin: text, dosya: f.name, documentId:cevap.id });
kalem = d.sayi || null;
} catch (error) { kalemHata = error.message; }
}
belgeleriYukle();
setBilgi({ yuklendi: f.name, kalem, kalemHata });
} catch (err) {
setError(err.kod === 'KOTA_ASILDI' ? err.message : `Yüklenemedi: ${err.message}`);
} finally { setYukleniyor(false); }
};
const belgeSil = async (id) => {
try { await deleteDocument(id); belgeleriYukle(); } catch (err) { setError(err.message); }
};
const okunabilirMi = readableDocument;
const metniIncele = async (b) => {
setBusy('metin'); setError(null); setMetinOnizleme(null);
const token = getToken();
const request = ++onizlemeIstegi.current;
try {
const result = await inspectStoredDocument(b,{signal:readLifetime.current.signal});
if (token !== getToken() || request !== onizlemeIstegi.current) return;
setMetinOnizleme({...result,name:b.ad,documentId:b.id,type:b.tur});
} catch (e) { if (token === getToken() && request === onizlemeIstegi.current) setError(e.message); }
finally { if (token === getToken() && request === onizlemeIstegi.current) setBusy(null); }
};
const kalemleriYenidenCikar = async (b) => {
setBusy('kalem'); setError(null); setBilgi(null);
try {
const result = await retryDocumentExtraction(b,{signal:readLifetime.current.signal});
setBilgi({ isleme: `${b.ad}: ${result.sayi || 0} kalem çıkarıldı.` });
} catch (e) { setError(`Kalem çıkarımı tamamlanamadı: ${e.message}. Dosya kaydı korunmuştur.`); }
finally { await belgeleriYukle(); setBusy(null); }
};
// Yüklenen PDF'i AI'ya ver
const belgeyiAnalizEt = async (b, mod) => {
setBusy('belge'); setError(null);
try {
if (!okunabilirMi(b.ad)) throw new Error('Bu dosya türünden metin çıkarılamıyor (PDF, DOCX, XLSX, ZIP ve TXT destekleniyor).');
const reading = await inspectStoredDocument(b,{signal:readLifetime.current.signal});
const metin = reading.text;
if (!metin || metin.length < 100) throw new Error('Belgeden analiz için yeterli metin çıkarılamadı (taranmış PDF olabilir).');
assertCurrent();
setIsAiSpecAnalyzerOpen(true);
await analyzeSpecPdfWithGemini({ text: metin, fileName: b.ad, extraction:{ocr:reading.ocr,pageCount:reading.pageCount,usedPages:reading.usedPages,complete:reading.complete,fileName:b.ad}, tender, mode: mod });
} catch (e) {
setError(e.message);
} finally { setBusy(null); }
};
const ekapIhalesi = /^\d{4}\/\d+$/.test(String(ikn || ''));
const dokumaniAc = async () => {
setBusy('url'); setError(null);
// Açılır pencere engelleyici: yeni sekme, tıklamanın HEMEN içinde
// (await'ten önce) açılmalı. Sunucu yanıtından sonra çağrılan window.open
// Safari, PWA ve sıkı ayarlı Chrome'da sessizce engelleniyor — kullanıcı
// "düğme çalışmıyor / dokümanlar açılmıyor" görüyordu. Sekme boş açılır,
// adres gelince yönlendirilir; engellenirse aşağıda gerçek bağlantı çıkar.
let sekme = null;
try { sekme = window.open('', '_blank'); } catch { sekme = null; }
if (sekme) {
try {
sekme.opener = null;
sekme.document.write('<p style="font-family:sans-serif;padding:24px;color:#334155">EKAP doküman sayfasına yönlendiriliyor…</p>');
} catch { /* yazılamazsa da yönlendirme çalışır */ }
}
try {
const r = await fetch(`/api/documents/url?ikn=${encodeURIComponent(ikn)}`);
const d = await r.json();
if (!d.success) throw new Error(d.error);
setEkapUrl(d.url);
setBilgi({ dokumanSayisi: d.dokumanSayisi, ekapSekme: sekme ? 'acildi' : 'engellendi' });
if (sekme) sekme.location.href = d.url;
} catch (e) {
if (sekme) { try { sekme.close(); } catch { /* yoksay */ } }
setError(e.message);
} finally { setBusy(null); }
};
const ilanMetniGetir = async () => {
assertCurrent();
const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, {
signal:readLifetime.current.signal,
headers: { Authorization: `Bearer ${sessionToken.current}` },
});
const d = await r.json();
assertCurrent();
if (!d.success) throw new Error(d.error);
return d;
};
const ilaniAnalizEt = async () => {
setBusy('analiz'); setError(null);
try {
const d = await ilanMetniGetir();
if (!d.metin || d.metin.length < 100) throw new Error('İlan metni analiz için yeterli değil.');
setBilgi({ karakter: d.metin.length, ilanSayisi: d.ilanSayisi, okasSayisi: d.okasSayisi });
assertCurrent();
setIsAiSpecAnalyzerOpen(true);
await analyzeSpecPdfWithGemini({ text: d.metin, fileName: `${d.ikn} — EKAP ilan metni`, tender });
} catch (e) {
setError(e.message);
} finally { setBusy(null); }
};
/**
* Derin analiz — teknik şartname ve diğer evrakın tamamını TEK metinde
* birleştirip gönderir: EKAP ilan metni + bu ihaleye yüklenmiş her belge.
* Böylece model şartnameyi ilanla ve eklerle çapraz okuyabiliyor.
*/
const tumEvrakiTopla = async () => {
const parcalar = [];
const atlanan = [];
const extraction = [];
if (ekapIhalesi) {
setIlerleme('EKAP ilan metni alınıyor…');
try {
const d = await ilanMetniGetir();
if (d.metin && d.metin.length >= 100) {
parcalar.push(`===== EKAP İLAN METNİ (${d.ikn}) =====\n${d.metin}`);
}
} catch {
assertCurrent();
atlanan.push('EKAP ilan metni (alınamadı)');
extraction.push({fileName:'EKAP ilan metni',complete:false});
}
}
for (const b of belgeler) {
assertCurrent();
if (!okunabilirMi(b.ad)) { atlanan.push(`${b.ad} (metin çıkarılamıyor)`); extraction.push({fileName:b.ad,complete:false}); continue; }
setIlerleme(`${b.ad} okunuyor…`);
try {
const reading = await inspectStoredDocument(b,{signal:readLifetime.current.signal});
const t = reading.text;
extraction.push({ocr:reading.ocr,pageCount:reading.pageCount,usedPages:reading.usedPages,complete:reading.complete,fileName:b.ad});
if (t && t.length >= 200) parcalar.push(`===== BELGE: ${b.ad} =====\n${t}`);
else { atlanan.push(`${b.ad} (yeterli metin yok — taranmış olabilir)`); extraction.push({fileName:b.ad,complete:false}); }
} catch (e) {
assertCurrent();
atlanan.push(`${b.ad} (${e.message})`);
extraction.push({fileName:b.ad,complete:false});
}
}
return { metin: parcalar.join('\n\n'), parcaSayisi: parcalar.length, atlanan, extraction };
};
const derinAnalizEt = async () => {
setBusy('derin'); setError(null); setBilgi(null);
try {
const { metin, parcaSayisi, atlanan, extraction } = await tumEvrakiTopla();
if (!metin || metin.length < 200) {
throw new Error('Derin analiz için yeterli metin toplanamadı. EKAP dokümanını indirip PDF olarak yükleyin.');
}
setBilgi({ derin: { parcaSayisi, karakter: metin.length, atlanan } });
setIlerleme('');
assertCurrent();
setIsAiSpecAnalyzerOpen(true);
await analyzeSpecPdfWithGemini({
text: metin,
extraction,
fileName: `${ikn || 'ihale'} — ${parcaSayisi} evrak birlikte`,
tender,
mode: 'derin',
});
} catch (e) {
setError(e.message);
} finally { setBusy(null); setIlerleme(''); }
};
return (
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-3">
<h4 className="font-bold text-sm text-slate-900 flex items-center gap-2">
<FileText className="w-4 h-4 text-blue-600" /> İhale Dokümanları
</h4>
{!ekapIhalesi ? (
<p className="text-xs text-slate-500 leading-relaxed">
Bu kayıt EKAP İKN formatında değil; doküman bağlantısı yalnızca EKAP ihaleleri için üretilebilir.
{tender?.kaynakUrl && (
<> İlanın kaynağını{' '}
<a href={tender.kaynakUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-semibold hover:underline">
buradan açabilirsiniz
</a>.
</>
)}
</p>
) : (
<>
<p className="text-xs text-slate-500 leading-relaxed">
İhale dokümanları EKAP üzerinde tutulur. Aşağıdaki bağlantı sizi doğrudan bu ihalenin
EKAP doküman sayfasına götürür; EKAP önce kısa bir <strong>güvenlik doğrulaması</strong> (resim seçme)
ister, ardından şartname ve ekleri listelenir. İlan metnini ise analiz için buradan çekebilirsiniz.
</p>
<div className="flex gap-2 flex-wrap">
<button type="button"
onClick={dokumaniAc}
disabled={busy !== null}
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center gap-2"
>
{busy === 'url' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <ExternalLink className="w-3.5 h-3.5" />}
EKAP Dokümanlarını Aç
</button>
<button type="button"
onClick={ilaniAnalizEt}
disabled={busy !== null}
className="bg-amber-400 hover:bg-amber-300 disabled:opacity-40 text-slate-950 font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2"
>
{busy === 'analiz' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5" />}
İlan Metnini AI ile Analiz Et
</button>
</div>
{ekapUrl && (
<div className={`rounded-lg p-2.5 flex gap-2 border ${bilgi?.ekapSekme === 'engellendi' ? 'bg-amber-50 border-amber-200' : 'bg-slate-50 border-slate-200'}`}>
<ExternalLink className="w-3.5 h-3.5 text-blue-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-slate-700 leading-relaxed">
{bilgi?.ekapSekme === 'engellendi'
? <>Tarayıcınız yeni sekmeyi engelledi. </>
: <>EKAP doküman sayfası yeni sekmede açıldı. Açılmadıysa: </>}
<a href={ekapUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-bold hover:underline">
EKAP doküman sayfasını aç
</a>
{' '}· Bağlantı bir süre sonra geçerliliğini yitirebilir; açılmazsa düğmeye yeniden basın.
</p>
</div>
)}
{bilgi && !bilgi.derin && (
<p className="text-[11px] text-slate-500">
{bilgi.dokumanSayisi > 0 && <>EKAP'ta {bilgi.dokumanSayisi} doküman kayıtlı. </>}
{bilgi.karakter != null && <>{bilgi.ilanSayisi} ilan · {bilgi.okasSayisi} kalem · {bilgi.karakter.toLocaleString('tr-TR')} karakter metin analize gönderildi. </>}
{bilgi.yuklendi && (
<>
<strong>{bilgi.yuklendi}</strong> yüklendi ve bu ihaleye bağlandı.
{bilgi.kalemHata && <span role="alert"> Kalem aktarımı tamamlanamadı: {bilgi.kalemHata}. Dosya kaydı korunmuştur.</span>}
{bilgi.kalem
? <> Teklif cetvelinden <strong>{bilgi.kalem} kalem</strong> otomatik çıkarıldı; bu ihale artık kalemleriyle listeleniyor.</>
: <> Bu dosyadan teklif cetveli kalemi çıkarılamadı.</>}
</>
)}
</p>
)}
</>
)}
{/* EKAP dokümanı kullanıcının tarayıcısına iniyor; sunucu tarafından
alınamıyor. İndirilen dosya buraya yüklenip ihaleye bağlanır. */}
<div className="border border-dashed border-slate-300 rounded-xl p-3 bg-slate-50/60">
<p className="text-[11px] text-slate-600 leading-relaxed flex items-start gap-1.5">
<Download className="w-3.5 h-3.5 shrink-0 mt-0.5 text-slate-400" />
<span>
EKAP dokümanı kendi oturumunuz üzerinden <strong>bilgisayarınıza iner</strong> — biz sunucudan
alamıyoruz. İnen dosyayı buraya yükleyin; bu ihaleye bağlanır ve AI ile analiz edebilirsiniz.
</span>
</p>
<div className="flex items-center gap-2 mt-2">
<input
ref={dosyaRef}
type="file"
onChange={dosyaSec}
accept=".pdf,.docx,.xlsx,.zip,.txt"
className="hidden"
/>
<button type="button"
onClick={() => dosyaRef.current?.click()}
disabled={yukleniyor}
className="bg-slate-900 hover:bg-slate-800 disabled:opacity-40 text-white font-bold text-xs px-3.5 py-2 rounded-lg flex items-center gap-2"
>
{yukleniyor ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
{yukleniyor ? 'Yükleniyor…' : 'İndirdiğim dosyayı yükle'}
</button>
<span className="text-[10px] text-slate-400">PDF, DOCX, XLSX, ZIP · en fazla 10 MB</span>
</div>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800 leading-relaxed">{error}</p>
</div>
)}
{bilgi?.isleme && <p role="status" className="text-xs text-slate-700">{bilgi.isleme}</p>}
{listeBekliyor && <p role="status" className="text-xs text-slate-500">Belge listesi yükleniyor…</p>}
{listeHata && <div role="alert" className="text-xs text-red-700">
Belge listesi alınamadı: {listeHata}
<button type="button" onClick={belgeleriYukle} className="ml-2 underline">Listeyi yeniden yükle</button>
</div>}
{belgeler.length > 0 && (
<div className="space-y-1.5">
<p className="text-[11px] font-bold text-slate-700 flex items-center gap-1.5">
<Paperclip className="w-3.5 h-3.5 text-slate-400" /> Bu ihaleye yüklenen belgeler ({belgeler.length})
</p>
{belgeler.map((b) => (
<div key={b.id} className="flex flex-wrap items-center gap-2 bg-white border border-slate-200 rounded-lg px-2.5 py-2">
<FileText className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<div className="flex-1 min-w-32"><span className="block text-xs text-slate-800 break-all">{b.ad}</span><span className="block text-[10px] text-slate-500">{documentProcessingLabel(b.isleme)}</span></div>
<span className="text-[10px] text-slate-400 whitespace-nowrap">
{(b.boyut / 1024).toLocaleString('tr-TR', { maximumFractionDigits: 0 })} KB
</span>
<button type="button"
onClick={() => belgeyiAnalizEt(b)}
disabled={busy !== null}
className="text-[10px] font-bold text-amber-700 hover:text-amber-800 disabled:opacity-40 whitespace-nowrap"
>
{busy === 'belge' ? '…' : 'AI analiz'}
</button>
<button type="button"
onClick={() => belgeyiAnalizEt(b, 'derin')}
disabled={busy !== null}
className="text-[10px] font-bold text-indigo-700 hover:text-indigo-800 disabled:opacity-40 whitespace-nowrap"
title="Yalnızca bu belge için ince detaylar + yol haritası"
>
Derin
</button>
{okunabilirMi(b.ad) && <button type="button" onClick={() => metniIncele(b)} disabled={busy !== null || !b.erisilebilir}
aria-label={`${b.ad} metnini incele`} className="text-[10px] text-blue-700 disabled:opacity-40">Metni incele</button>}
{b.tur === 'ihale' && okunabilirMi(b.ad) && (
<button type="button" onClick={() => kalemleriYenidenCikar(b)} disabled={busy !== null || yukleniyor || !b.erisilebilir}
aria-label={`${b.ad} için kalem çıkarımını yeniden dene`}
className="text-[10px] font-bold text-blue-700 disabled:opacity-40">
{busy === 'kalem' ? 'İşleniyor…' : 'Kalemleri yeniden çıkar'}
</button>
)}
<button type="button" disabled={busy !== null || yukleniyor} onClick={() => belgeSil(b.id)} className="text-slate-300 hover:text-red-600" aria-label="Sil">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
{metinOnizleme && <section aria-label="Okunan belge metni" className="border rounded-xl p-3 space-y-2">
<h5 className="font-bold text-xs">{metinOnizleme.name} — okunan metin</h5>
<p className="text-xs text-slate-600">
{metinOnizleme.pageCount != null ? `${metinOnizleme.usedPages}/${metinOnizleme.pageCount} sayfa okundu. ` : 'Sunucuda okunan belge metni. '}
{metinOnizleme.ocr ? `OCR kullanılan sayfalar: ${metinOnizleme.pages.filter(p=>p.method === 'ocr').map(p=>p.page).join(', ')}. ` : ''}
Metni resmi dokümanla karşılaştırın; okunmuş olması doğruluğunun onaylandığı anlamına gelmez.
</p>
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words text-xs">{metinOnizleme.text || 'Okunabilir metin bulunamadı.'}</pre>
{metinOnizleme.type === 'ihale' && <DocumentTextReview key={metinOnizleme.documentId} documentId={metinOnizleme.documentId} extractedText={metinOnizleme.text} onAnalyze={async(reviewedSource)=>{
assertCurrent();
await analyzeSpecPdfWithGemini({reviewedSource,tender,fileName:metinOnizleme.name});
assertCurrent();
setIsAiSpecAnalyzerOpen(true);
}} />}
<button type="button" onClick={()=>setMetinOnizleme(null)} className="text-xs underline">Metin önizlemesini kapat</button>
</section>}
{(ekapIhalesi || belgeler.length > 0) && (
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-xl p-4 space-y-2.5">
<div className="flex items-start gap-2.5">
<div className="w-9 h-9 rounded-xl bg-amber-400 text-slate-950 flex items-center justify-center shrink-0">
<Route className="w-5 h-5" />
</div>
<div className="min-w-0">
<h5 className="font-black text-sm text-white">Derin Analiz &amp; Yol Haritası</h5>
<p className="text-[11px] text-slate-300 leading-relaxed">
EKAP ilan metni ve bu ihaleye yüklediğiniz tüm evrak <strong className="text-white">birlikte</strong> okunur;
elenme sebepleri, gözden kaçan ince detaylar, belge kontrol listesi, kritik tarihler ve
teklife kadar adım adım yol haritası çıkarılır.
</p>
</div>
</div>
<button type="button"
onClick={derinAnalizEt}
disabled={busy !== null}
className="w-full bg-amber-400 hover:bg-amber-300 disabled:opacity-40 text-slate-950 font-black text-xs px-4 py-2.5 rounded-xl flex items-center justify-center gap-2"
>
{busy === 'derin'
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <Sparkles className="w-3.5 h-3.5" />}
{busy === 'derin' ? (ilerleme || 'Analiz ediliyor…') : 'Tüm Evrakı Derin Analiz Et'}
</button>
{bilgi?.derin ? (
<div className="bg-black/30 rounded-lg p-2.5 space-y-1">
<p className="text-[10px] text-slate-300">
{bilgi.derin.parcaSayisi} kaynak · {bilgi.derin.karakter.toLocaleString('tr-TR')} karakter analize gönderildi.
</p>
{bilgi.derin.atlanan.length > 0 && (
<p className="text-[10px] text-amber-300 leading-relaxed">
Atlanan: {bilgi.derin.atlanan.join(', ')}
</p>
)}
</div>
) : (
<p className="text-[10px] text-slate-400 text-center">
{belgeler.length > 0
? `${ekapIhalesi ? 'İlan metni + ' : ''}${belgeler.length} yüklü belge okunacak`
: 'Henüz belge yüklemediniz — yalnızca ilan metni okunacak'}
</p>
)}
</div>
)}
</div>
);
};