feat: derin analiz & yol haritasi + ihale dosyasi hazirlama; docx/xlsx/zip okunuyor

Doküman panelindeki "Tum Evraki Derin Analiz Et" dugmesi EKAP ilan metnini ve
ihaleye yuklenmis butun belgeleri TEK metinde birlestirip gonderiyor; model
sartnameyi ilanla ve eklerle capraz okuyabiliyor.

Derin analiz (mode='derin', ayri sistem istemi + 8000 token):
- Teklifi gecersiz kilabilecek noktalar (kirmizi uyari, en ustte)
- Gozden kacan ince detaylar (Kritik/Yuksek/Orta + madde no + neden onemli)
- Alinacak kalemler: aranan evsaflar, miktar, sartnamede geciyorsa fiyat
  (modele "TAHMIN URETME" talimati verildi, yoksa null)
- Belge kontrol listesi, kritik tarihler, maliyet kalemleri
- Teklife kadar yol haritasi (faz / kim / ne zaman / cikti)
- Idareye sorulacak belirsizlikler
Standart hizli ozet modu aynen duruyor.

Ihale Dosyasi Hazirla (rapor sonunda, acilir bolum):
- Eksik firma bilgileri kullaniciya sorulur, profilden gelenler sorulmaz,
  girilenler tarayicida saklanir
- Kalem fiyatlandirma tablosu, canli toplam + yaziyla karsiligi
- Teklif Mektubu, Birim Fiyat Teklif Cetveli, Evrak Kontrol Listesi
- Belgeler yazdirilabilir HTML: jsPDF'in gomulu fontlari WinAnsi, s/g/i/I
  tasimiyor (bkz. services/pdf.js ASCII basliklari) — resmi teklif mektubunda
  bozuk Turkce kabul edilemez

docx/xlsx/zip metin cikarimi (server/office-text.mjs):
- ihaleab projesindeki officeText.ts'ten tasindi, harici bagimlilik yok
- Sunucuda: tarayicida cozucu yok, baytlar zaten BELGE_DIR'de
- Tablolar " | " ile korunuyor, ic ice zip aciliyor, ~$ gecici dosyalari atlanir
- Yeni uc: GET /api/me/documents/text
- Yukleme kabul listesi .doc/.rar cikarildi (karsiligi yok), .xlsx eklendi

fix: yuklenen belgelerdeki "AI analiz" dugmesi bozuktu — extractText nesne
donduruyor ({text,pageCount,usedPages}), kod onu duz metin sanip gonderiyordu;
sunucuya "[object Object]" gidiyor ve analiz reddediliyordu. Uzunluk kontrolu
de nesne uzerinde undefined verdigi icin hatayi yutuyordu.

fix: analiz dokuman panelinden baslatildiginda modal kendi isAnalyzing'ini
bilmiyordu, kullanici 1-2 dakika bos modal goruyordu — ortak aiAnalyzing sinyali
This commit is contained in:
2026-08-31 14:53:46 +03:00
parent be79f9366a
commit 3eb0f91823
10 changed files with 1584 additions and 51 deletions
+53 -11
View File
@@ -3,13 +3,14 @@ import { useApp } from '../context/AppContext';
import { Sparkles, Upload, FileText, CheckCircle2, AlertTriangle, X, Loader2, TrendingUp, Sliders, Info } from 'lucide-react';
import confetti from 'canvas-confetti';
import { getAiConfig } from '../services/ai';
import { DeepAnalysisReport } from './DeepAnalysisReport';
const tl = (n) => (typeof n === 'number' && isFinite(n) ? '₺' + n.toLocaleString('tr-TR') : '—');
export const AiSpecAnalyzerModal = () => {
const {
isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen,
analyzeSpecPdfWithGemini, aiAnalysisResults, activeAiModel, activeTenderDetail,
analyzeSpecPdfWithGemini, aiAnalysisResults, aiAnalyzing, activeAiModel, activeTenderDetail,
} = useApp();
const [isAnalyzing, setIsAnalyzing] = useState(false);
@@ -18,6 +19,9 @@ export const AiSpecAnalyzerModal = () => {
const [progress, setProgress] = useState('');
const [customBid, setCustomBid] = useState(null);
const [aiCfg, setAiCfg] = useState(null);
// Derin mod: ince detaylar, belge kontrol listesi ve yol haritası da üretilir.
// Çıktısı standart şemanın üst kümesi olduğu için varsayılan açık.
const [derin, setDerin] = useState(true);
useEffect(() => {
if (isAiSpecAnalyzerOpen && aiCfg === null) getAiConfig().then(setAiCfg);
@@ -25,6 +29,9 @@ export const AiSpecAnalyzerModal = () => {
if (!isAiSpecAnalyzerOpen) return null;
// Analiz doküman panelinden de başlatılabiliyor — o durumda isAnalyzing false
// kalır, bu yüzden context'teki ortak sinyalle birleştiriyoruz.
const mesgul = isAnalyzing || aiAnalyzing;
const results = aiAnalysisResults;
const bid = results?.bidAnalysis || {};
@@ -33,8 +40,8 @@ export const AiSpecAnalyzerModal = () => {
setIsAnalyzing(true);
setSelectedFile(fileName);
try {
setProgress('Model analiz ediyor…');
const r = await analyzeSpecPdfWithGemini({ text, fileName, tender: activeTenderDetail });
setProgress(derin ? 'Derin analiz üretiliyor (1-2 dk sürebilir)…' : 'Model analiz ediyor…');
const r = await analyzeSpecPdfWithGemini({ text, fileName, tender: activeTenderDetail, mode: derin ? 'derin' : undefined });
if (typeof r?.bidAnalysis?.recommendedBid === 'number') setCustomBid(r.bidAnalysis.recommendedBid);
confetti({ particleCount: 80, spread: 70 });
} catch (e) {
@@ -56,8 +63,8 @@ export const AiSpecAnalyzerModal = () => {
if (!text || text.length < 40) {
throw new Error('Belgeden metin çıkarılamadı. Taranmış (görüntü) PDF olabilir — metin tabanlı bir dosya deneyin.');
}
setProgress(`${usedPages}/${pageCount} sayfa okundu, model analiz ediyor…`);
const r = await analyzeSpecPdfWithGemini({ text, fileName: file.name, tender: activeTenderDetail });
setProgress(`${usedPages}/${pageCount} sayfa okundu, ${derin ? 'derin analiz üretiliyor (1-2 dk sürebilir)…' : 'model analiz ediyor…'}`);
const r = await analyzeSpecPdfWithGemini({ text, fileName: file.name, tender: activeTenderDetail, mode: derin ? 'derin' : undefined });
if (typeof r?.bidAnalysis?.recommendedBid === 'number') setCustomBid(r.bidAnalysis.recommendedBid);
confetti({ particleCount: 80, spread: 70 });
} catch (e) {
@@ -133,9 +140,37 @@ export const AiSpecAnalyzerModal = () => {
<p className="text-xs text-slate-500">Metin tabanlı <code>.pdf</code> veya <code>.txt</code> dosyası — belge tarayıcınızda okunur, yalnızca metni analiz için gönderilir.</p>
</div>
<div className="inline-flex items-center gap-1 bg-white border border-slate-300 rounded-xl p-1">
<button
type="button"
disabled={mesgul}
onClick={() => setDerin(true)}
className={`text-[11px] font-extrabold px-3 py-1.5 rounded-lg transition-colors disabled:opacity-50 ${
derin ? 'bg-amber-400 text-slate-950' : 'text-slate-600 hover:bg-slate-100'
}`}
>
Derin Analiz + Yol Haritası
</button>
<button
type="button"
disabled={mesgul}
onClick={() => setDerin(false)}
className={`text-[11px] font-extrabold px-3 py-1.5 rounded-lg transition-colors disabled:opacity-50 ${
!derin ? 'bg-slate-800 text-white' : 'text-slate-600 hover:bg-slate-100'
}`}
>
Hızlı Özet
</button>
</div>
<p className="text-[11px] text-slate-500 leading-relaxed max-w-lg mx-auto">
{derin
? 'Elenme sebepleri, gözden kaçan ince detaylar, belge kontrol listesi, kritik tarihler ve teklife kadar adım adım yol haritası üretilir. Daha uzun sürer.'
: 'Yalnızca özet, yeterlik kriterleri, riskler ve teklif aralığı üretilir. Daha hızlıdır.'}
</p>
<div className="flex justify-center gap-3 pt-2 flex-wrap">
<label className={`font-extrabold text-xs px-5 py-2.5 rounded-xl shadow-md transition-colors flex items-center gap-2 ${
isAnalyzing ? 'bg-slate-300 text-slate-500 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-500 text-white cursor-pointer'
mesgul ? 'bg-slate-300 text-slate-500 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-500 text-white cursor-pointer'
}`}>
<Upload className="w-4 h-4" />
<span>Dosya Seç (PDF / TXT)</span>
@@ -143,7 +178,7 @@ export const AiSpecAnalyzerModal = () => {
type="file"
accept=".pdf,.txt,.md"
className="hidden"
disabled={isAnalyzing}
disabled={mesgul}
onChange={(e) => { if (e.target.files?.[0]) handleFile(e.target.files[0]); }}
/>
</label>
@@ -151,7 +186,7 @@ export const AiSpecAnalyzerModal = () => {
{activeTenderDetail && (
<button
type="button"
disabled={isAnalyzing}
disabled={mesgul}
onClick={analyzeCurrentTender}
className="bg-white hover:bg-slate-100 disabled:opacity-50 text-slate-700 font-bold text-xs px-4 py-2.5 rounded-xl border border-slate-300 transition-colors flex items-center gap-2"
>
@@ -174,18 +209,18 @@ export const AiSpecAnalyzerModal = () => {
)}
{/* Yükleniyor */}
{isAnalyzing && (
{mesgul && (
<div className="bg-linear-to-r from-blue-50 to-indigo-50 border border-blue-200 p-6 rounded-2xl space-y-2 animate-fade-in shadow-xs">
<div className="flex items-center gap-2 font-extrabold text-xs text-blue-900">
<Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
<span>{progress || 'İşleniyor…'}</span>
<span>{progress || 'Evraklar analiz ediliyor…'}</span>
</div>
<p className="text-xs text-slate-600">{selectedFile}</p>
</div>
)}
{/* Sonuç */}
{!isAnalyzing && results && (
{!mesgul && results && (
<div className="space-y-6 animate-fade-in">
<div className="bg-slate-900 text-white p-5 rounded-2xl space-y-3 shadow-md">
@@ -193,6 +228,11 @@ export const AiSpecAnalyzerModal = () => {
<span className="text-xs font-extrabold text-amber-400 uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-4 h-4" /> Şartname Özeti
</span>
{results.mode === 'derin' && (
<span className="bg-amber-400 text-slate-950 text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-wider">
Derin Analiz
</span>
)}
<span className="text-[10px] text-slate-400">
{results.model} · {results.analyzedAt}
</span>
@@ -201,6 +241,8 @@ export const AiSpecAnalyzerModal = () => {
{results.fileName && <p className="text-[10px] text-slate-500">Kaynak: {results.fileName}</p>}
</div>
<DeepAnalysisReport results={results} tender={activeTenderDetail} />
{/* Teklif analizi — yalnızca model sayı ürettiyse */}
{(bid.estimatedCost != null || bid.recommendedBid != null) && (
<div className="bg-linear-to-br from-emerald-950 via-slate-900 to-blue-950 text-white p-6 rounded-2xl space-y-4 shadow-xl border border-emerald-800/40">
+329
View File
@@ -0,0 +1,329 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '../context/AppContext';
import {
FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks,
AlertTriangle, Printer, Info,
} from 'lucide-react';
import {
yaziylaTL, paraTR, miktarAyristir, belgeYazdir,
teklifMektubuHtml, teklifCetveliHtml, evrakListesiHtml,
} from '../services/bidDocs';
/**
* İhale Dosyası Hazırla — derin analiz bittikten sonra teklif zarfına girecek
* belgeleri üretir. Analizden gelen kalemler fiyatlandırılır, eksik firma
* bilgileri kullanıcıya sorulur; belgeler tam Türkçe olarak yazdırılır.
*
* Firma bilgileri localStorage'da tutulur — her ihalede yeniden sorulmasın.
*/
const ALANLAR = [
{ k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true },
{ k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true },
{ k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true },
{ k: 'vergiNo', l: 'Vergi numarası', zorunlu: true },
{ k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false },
{ k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true },
{ k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' },
{ k: 'telefon', l: 'Telefon', zorunlu: true },
{ k: 'eposta', l: 'E-posta', zorunlu: true },
];
const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
export const BidPackageBuilder = ({ results, tender }) => {
const { userProfile } = useApp();
const [acik, setAcik] = useState(false);
const [hata, setHata] = useState(null);
const [firma, setFirma] = useState(() => {
let kayitli = {};
try { kayitli = JSON.parse(localStorage.getItem(FIRMA_ANAHTAR) || '{}'); } catch { /* yoksay */ }
return {
...bosFirma(),
firmaUnvan: userProfile?.company || '',
...kayitli,
};
});
const [teklifTuru, setTeklifTuru] = useState('birim');
const [gecerlilikGun, setGecerlilikGun] = useState('60');
const [kisim, setKisim] = useState('');
const [goturuBedel, setGoturuBedel] = useState('');
const kalemler = Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : [];
const [fiyatlar, setFiyatlar] = useState({});
const alanYaz = (k, v) => {
const yeni = { ...firma, [k]: v };
setFirma(yeni);
try { localStorage.setItem(FIRMA_ANAHTAR, JSON.stringify(yeni)); } catch { /* kota */ }
};
const satirlar = useMemo(
() => kalemler.map((k, i) => ({ ...k, birimFiyat: fiyatlar[i] })),
[kalemler, fiyatlar],
);
const kalemToplami = useMemo(
() => satirlar.reduce((t, r) => {
const { miktar } = miktarAyristir(r.quantity);
return t + (miktar ?? 0) * (Number(r.birimFiyat) || 0);
}, 0),
[satirlar],
);
const toplam = teklifTuru === 'goturu' ? (Number(goturuBedel) || 0) : kalemToplami;
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
const hazir = eksikler.length === 0 && toplam > 0;
const formVerisi = () => ({
...firma,
teklifTuru,
gecerlilikGun,
kisim,
idare: tender?.kurum || '',
isinAdi: tender?.baslik || '',
ikn: tender?.ikn || '',
ihaleTarihi: tender?.ihaleTarihi || tender?.tarih || '',
});
const uret = (tip) => {
setHata(null);
try {
const f = formVerisi();
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam));
if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, results?.documentChecklist || []));
} catch (e) {
setHata(e.message);
}
};
return (
<div className="bg-white rounded-2xl border-2 border-emerald-300 shadow-xs overflow-hidden">
<button
type="button"
onClick={() => setAcik((o) => !o)}
className="w-full flex items-center gap-3 p-5 text-left hover:bg-emerald-50/50 transition-colors"
>
<div className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shrink-0">
<FolderCheck className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-black text-sm text-slate-900">İhale Dosyası Hazırla</h4>
<p className="text-[11px] text-slate-500 leading-relaxed">
Analizdeki kalemleri fiyatlandırın; teklif mektubu, birim fiyat teklif cetveli ve
evrak kontrol listesi hazır olsun.
</p>
</div>
{acik ? <ChevronUp className="w-5 h-5 text-slate-400 shrink-0" />
: <ChevronDown className="w-5 h-5 text-slate-400 shrink-0" />}
</button>
{acik && (
<div className="border-t border-emerald-200 p-5 space-y-5">
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-3.5 flex gap-2.5">
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<div className="min-w-0">
<p className="text-xs font-bold text-amber-900">Sizden beklenen bilgiler</p>
<p className="text-[11px] text-amber-800 leading-relaxed mt-0.5">
Belgeleri hazırlayabilmek için şunlar gerekli:{' '}
<strong>{eksikler.map((a) => a.l).join(', ')}</strong>.
Bir kez girdiğinizde bu tarayıcıda saklanır, sonraki ihalelerde tekrar sorulmaz.
</p>
</div>
</div>
)}
{/* Firma bilgileri */}
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">İstekli (firma) bilgileri</h5>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{ALANLAR.map((a) => {
const eksik = a.zorunlu && !String(firma[a.k] || '').trim();
return (
<label key={a.k} className={a.genis ? 'sm:col-span-2' : ''}>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
{a.l}{a.zorunlu && <span className="text-red-600"> *</span>}
</span>
<input
value={firma[a.k] || ''}
onChange={(e) => alanYaz(a.k, e.target.value)}
placeholder={a.ph || ''}
className={`w-full text-xs px-3 py-2 rounded-lg border bg-white focus:ring-2 focus:ring-emerald-500 outline-none ${
eksik ? 'border-amber-400 bg-amber-50/40' : 'border-slate-300'
}`}
/>
</label>
);
})}
</div>
</div>
{/* Teklif ayarları */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif türü</span>
<select
value={teklifTuru}
onChange={(e) => setTeklifTuru(e.target.value)}
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white"
>
<option value="birim">Birim fiyat</option>
<option value="goturu">Anahtar teslim götürü bedel</option>
</select>
</label>
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif geçerlilik (gün)</span>
<input
value={gecerlilikGun}
onChange={(e) => setGecerlilikGun(e.target.value.replace(/\D/g, ''))}
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300"
/>
</label>
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Kısım (varsa)</span>
<input
value={kisim}
onChange={(e) => setKisim(e.target.value)}
placeholder="Örn. 2. kısım"
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300"
/>
</label>
</div>
{/* Fiyatlandırma */}
{teklifTuru === 'goturu' ? (
<label className="block">
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
Teklif edilen toplam bedel (KDV hariç, TL)
</span>
<input
value={goturuBedel}
onChange={(e) => setGoturuBedel(e.target.value.replace(/[^\d.]/g, ''))}
placeholder="0"
className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300"
/>
</label>
) : kalemler.length > 0 ? (
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5>
<div className="overflow-x-auto">
<table className="w-full text-xs min-w-[520px]">
<thead>
<tr className="text-[11px] text-slate-500 border-b border-slate-200">
<th className="text-left py-1.5 font-semibold">Kalem</th>
<th className="text-right py-1.5 font-semibold w-24">Miktar</th>
<th className="text-right py-1.5 font-semibold w-32">Birim fiyat</th>
<th className="text-right py-1.5 font-semibold w-28">Tutar</th>
</tr>
</thead>
<tbody>
{satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const tutar = (miktar ?? 0) * (Number(r.birimFiyat) || 0);
return (
<tr key={i} className="border-b border-slate-100">
<td className="py-1.5 pr-2 text-slate-800">{r.name || '—'}</td>
<td className="py-1.5 text-right text-slate-600 whitespace-nowrap">
{miktar ?? '—'} {birim}
</td>
<td className="py-1.5 text-right">
<input
value={fiyatlar[i] ?? ''}
onChange={(e) =>
setFiyatlar((o) => ({ ...o, [i]: e.target.value.replace(/[^\d.]/g, '') }))}
placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'}
className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300"
/>
</td>
<td className="py-1.5 text-right font-semibold text-slate-900 whitespace-nowrap">
{tutar ? paraTR(tutar) : '—'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{kalemler.some((k) => k.estimatedUnitPrice != null) && (
<p className="text-[10px] text-slate-400 flex items-start gap-1.5">
<Info className="w-3 h-3 shrink-0 mt-0.5" />
Gri yazılı değerler şartnamede geçen tahmini birim fiyatlardır — teklifiniz değildir.
</p>
)}
</div>
) : (
<p className="text-xs text-slate-500 bg-slate-50 border border-slate-200 rounded-lg p-3">
Analizde kalem dökümü çıkmadı. Götürü bedel seçip toplam teklifinizi doğrudan girebilirsiniz.
</p>
)}
{/* Toplam */}
<div className="bg-slate-900 text-white rounded-xl p-4 space-y-1">
<div className="flex items-baseline justify-between gap-3 flex-wrap">
<span className="text-xs font-semibold text-slate-300">Teklif toplamı (KDV hariç)</span>
<span className="text-lg font-black text-amber-300">{paraTR(toplam)} TL</span>
</div>
{toplam > 0 && (
<p className="text-[11px] text-slate-400 leading-relaxed">Yazı ile: {yaziylaTL(toplam)}</p>
)}
</div>
{hata && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 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">{hata}</p>
</div>
)}
{/* Belgeler */}
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Hazırlanacak belgeler</h5>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
{[
{ t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' },
{ t: 'cetvel', ad: 'Birim Fiyat Teklif Cetveli', ikon: Table2, aciklama: `${kalemler.length} kalem` },
{ t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${(results?.documentChecklist || []).length} belge` },
].map((b) => (
<button
key={b.t}
type="button"
disabled={!hazir}
onClick={() => uret(b.t)}
className="text-left bg-white border border-slate-300 hover:border-emerald-500 hover:bg-emerald-50/50 disabled:opacity-40 disabled:hover:border-slate-300 disabled:hover:bg-white rounded-xl p-3 transition-colors"
>
<b.ikon className="w-4 h-4 text-emerald-600 mb-1.5" />
<span className="block text-xs font-bold text-slate-900 leading-snug">{b.ad}</span>
<span className="block text-[10px] text-slate-500 mt-0.5">{b.aciklama}</span>
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-700 mt-1.5">
<Printer className="w-3 h-3" /> Yazdır / PDF
</span>
</button>
))}
</div>
{!hazir && (
<p className="text-[11px] text-slate-500">
{eksikler.length > 0
? 'Yukarıdaki zorunlu alanları doldurun.'
: 'Belgeleri hazırlamak için teklif tutarını girin.'}
</p>
)}
</div>
<p className="text-[10px] text-slate-400 leading-relaxed">
Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce
ihale dokümanındaki form metinleriyle karşılaştırın.
</p>
</div>
)}
</div>
);
};
+328
View File
@@ -0,0 +1,328 @@
import React, { useState } from 'react';
import {
AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route,
HelpCircle, Coins, Target, CheckSquare, Square, Package,
} from 'lucide-react';
import { BidPackageBuilder } from './BidPackageBuilder';
/**
* Derin analiz raporu — /api/ai/analyze mode='derin' çıktısını gösterir.
* Standart analizden farkı: elenme sebepleri, gözden kaçan ince detaylar,
* belge kontrol listesi, süreler ve teklife kadarki yol haritası.
*
* Model alanları kısmi doldurabilir; her bölüm kendi verisi varsa çizilir.
*/
const SEVIYE_RENK = {
Kritik: 'bg-red-100 text-red-800 border-red-200',
Yüksek: 'bg-orange-100 text-orange-800 border-orange-200',
Orta: 'bg-amber-100 text-amber-800 border-amber-200',
Düşük: 'bg-slate-200 text-slate-700 border-slate-300',
};
const FAZ_RENK = {
Hazırlık: 'bg-slate-700',
'Belge Toplama': 'bg-blue-600',
Teknik: 'bg-indigo-600',
Fiyatlandırma: 'bg-emerald-600',
Teklif: 'bg-amber-600',
Sözleşme: 'bg-purple-600',
};
const dolu = (x) => Array.isArray(x) && x.length > 0;
const tl = (n) =>
typeof n === 'number' && isFinite(n)
? n.toLocaleString('tr-TR', { maximumFractionDigits: 2 }) + ' ₺'
: null;
const Baslik = ({ icon: Icon, renk, children, not }) => (
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
<Icon className={`w-4 h-4 ${renk}`} />
<span>{children}</span>
{not && <span className="ml-auto text-[10px] font-semibold text-slate-400">{not}</span>}
</h4>
);
export const DeepAnalysisReport = ({ results, tender }) => {
const [isaretli, setIsaretli] = useState({});
if (!results) return null;
const {
scopeOfWork, disqualificationRisks, criticalDetails, documentChecklist,
deadlines, costDrivers, roadmap, questionsForAdministration, itemsAndSpecs,
} = results;
const varMi = scopeOfWork || dolu(disqualificationRisks) || dolu(criticalDetails) ||
dolu(documentChecklist) || dolu(deadlines) || dolu(costDrivers) ||
dolu(roadmap) || dolu(questionsForAdministration) || dolu(itemsAndSpecs);
if (!varMi) return null;
const degistir = (k) => setIsaretli((o) => ({ ...o, [k]: !o[k] }));
return (
<div className="space-y-6">
{/* İşin kapsamı */}
{scopeOfWork && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<Baslik icon={Target} renk="text-indigo-600">İşin Kapsamı</Baslik>
<p className="text-xs text-slate-700 leading-relaxed">{scopeOfWork}</p>
</div>
)}
{/* UYARI — teklifi geçersiz kılan noktalar */}
{dolu(disqualificationRisks) && (
<div className="bg-red-50 border-2 border-red-300 rounded-2xl p-5 space-y-3 shadow-xs">
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-red-600 text-white flex items-center justify-center shrink-0">
<AlertOctagon className="w-5 h-5" />
</div>
<div>
<h4 className="font-black text-sm text-red-900">Teklifinizi Geçersiz Kılabilecek Noktalar</h4>
<p className="text-[11px] text-red-700">Bu maddelerden biri eksikse teklif değerlendirme dışı bırakılabilir.</p>
</div>
</div>
<ul className="space-y-2">
{disqualificationRisks.map((r, i) => (
<li key={i} className="flex items-start gap-2 bg-white border border-red-200 rounded-lg p-2.5">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<span className="text-xs text-red-900 font-medium leading-relaxed">{r}</span>
</li>
))}
</ul>
</div>
)}
{/* Gözden kaçan ince detaylar */}
{dolu(criticalDetails) && (
<div className="bg-white p-5 rounded-2xl border border-amber-300 shadow-xs space-y-3">
<Baslik icon={Eye} renk="text-amber-600" not={`${criticalDetails.length} madde`}>
Gözden Kaçmaması Gereken İnce Detaylar
</Baslik>
<div className="space-y-2.5">
{criticalDetails.map((d, i) => (
<div key={i} className="bg-amber-50/60 border border-amber-200 rounded-xl p-3 space-y-1.5">
<div className="flex items-start justify-between gap-2 flex-wrap">
<span className="font-bold text-xs text-slate-900">{d.title || 'Detay'}</span>
<div className="flex items-center gap-1.5 shrink-0">
{d.where && (
<span className="text-[10px] font-semibold text-slate-500 bg-white border border-slate-200 px-1.5 py-0.5 rounded">
{d.where}
</span>
)}
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded border ${SEVIYE_RENK[d.level] || SEVIYE_RENK.Orta}`}>
{d.level || 'Orta'}
</span>
</div>
</div>
{d.detail && <p className="text-xs text-slate-700 leading-relaxed">{d.detail}</p>}
{d.whyItMatters && (
<p className="text-[11px] text-amber-900 bg-amber-100/70 rounded-lg px-2.5 py-1.5 leading-relaxed">
<strong>Neden önemli:</strong> {d.whyItMatters}
</p>
)}
</div>
))}
</div>
</div>
)}
{/* Alınacak ürün/hizmet: evsaf, miktar, tahmini fiyat */}
{dolu(itemsAndSpecs) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={Package} renk="text-emerald-600" not={`${itemsAndSpecs.length} kalem`}>
Alınacak Kalemler &amp; Aranan Evsaflar
</Baslik>
<div className="space-y-2.5">
{itemsAndSpecs.map((k, i) => (
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 space-y-2">
<div className="flex items-start justify-between gap-3 flex-wrap">
<span className="font-bold text-xs text-slate-900 flex-1 min-w-0">{k.name || '—'}</span>
<div className="flex items-center gap-1.5 shrink-0">
{k.quantity && (
<span className="text-[11px] font-black text-slate-900 bg-white border border-slate-300 px-2 py-0.5 rounded">
{k.quantity}
</span>
)}
{tl(k.estimatedUnitPrice) && (
<span className="text-[11px] font-bold text-emerald-800 bg-emerald-100 border border-emerald-200 px-2 py-0.5 rounded"
title="Şartnamede geçen tahmini birim fiyat">
birim {tl(k.estimatedUnitPrice)}
</span>
)}
{tl(k.estimatedTotal) && (
<span className="text-[11px] font-black text-emerald-900 bg-emerald-200 px-2 py-0.5 rounded"
title="Şartnamede geçen tahmini toplam">
toplam {tl(k.estimatedTotal)}
</span>
)}
</div>
</div>
{dolu(k.keySpecs) && (
<ul className="space-y-1">
{k.keySpecs.map((e, j) => (
<li key={j} className="flex items-start gap-2 text-[11px] text-slate-700 leading-relaxed">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-600 mt-1.5 shrink-0" />
<span>{e}</span>
</li>
))}
</ul>
)}
{k.note && <p className="text-[11px] text-slate-500 leading-snug italic">{k.note}</p>}
</div>
))}
</div>
<p className="text-[10px] text-slate-400 leading-relaxed">
Fiyatlar yalnızca şartnamede/ilanda geçiyorsa gösterilir; model tahmin üretmez.
</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Belge kontrol listesi */}
{dolu(documentChecklist) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={ClipboardList} renk="text-blue-600" not="işaretleyerek ilerleyin">
Belge Kontrol Listesi
</Baslik>
<ul className="space-y-1.5">
{documentChecklist.map((b, i) => {
const k = `b${i}`;
const secili = !!isaretli[k];
return (
<li key={i}>
<button
type="button"
onClick={() => degistir(k)}
className="w-full text-left flex items-start gap-2 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-lg p-2.5 transition-colors"
>
{secili
? <CheckSquare className="w-4 h-4 text-emerald-600 shrink-0 mt-0.5" />
: <Square className="w-4 h-4 text-slate-300 shrink-0 mt-0.5" />}
<span className="flex-1 min-w-0">
<span className={`text-xs font-semibold block ${secili ? 'line-through text-slate-400' : 'text-slate-800'}`}>
{b.document || '—'}
</span>
{b.note && <span className="text-[11px] text-slate-500 leading-snug block mt-0.5">{b.note}</span>}
</span>
{b.mandatory === false
? <span className="text-[10px] font-bold text-slate-500 bg-slate-200 px-1.5 py-0.5 rounded shrink-0">İsteğe bağlı</span>
: <span className="text-[10px] font-bold text-red-700 bg-red-100 px-1.5 py-0.5 rounded shrink-0">Zorunlu</span>}
</button>
</li>
);
})}
</ul>
</div>
)}
{/* Süreler + maliyet sürücüleri */}
<div className="space-y-6">
{dolu(deadlines) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={CalendarClock} renk="text-rose-600">Kritik Tarihler & Süreler</Baslik>
<div className="space-y-2">
{deadlines.map((d, i) => (
<div key={i} className="bg-slate-50 border border-slate-200 rounded-lg p-2.5">
<div className="flex items-baseline justify-between gap-2 flex-wrap">
<span className="text-xs font-semibold text-slate-800">{d.label || '—'}</span>
<span className="text-xs font-black text-rose-700">{d.value || '—'}</span>
</div>
{d.note && <p className="text-[11px] text-slate-500 leading-snug mt-0.5">{d.note}</p>}
</div>
))}
</div>
</div>
)}
{dolu(costDrivers) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={Coins} renk="text-emerald-600">Maliyeti Belirleyen Kalemler</Baslik>
<ul className="space-y-1.5 text-xs text-slate-700">
{costDrivers.map((c, i) => (
<li key={i} className="flex items-start gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-600 mt-1.5 shrink-0" />
<span className="leading-relaxed">{c}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
{/* Yol haritası */}
{dolu(roadmap) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={Route} renk="text-purple-600" not={`${roadmap.length} adım`}>
Teklife Kadar Yol Haritası
</Baslik>
<ol className="space-y-2">
{roadmap.map((a, i) => {
const k = `y${i}`;
const secili = !!isaretli[k];
return (
<li key={i} className="flex gap-3">
<div className="flex flex-col items-center shrink-0">
<button
type="button"
onClick={() => degistir(k)}
aria-label={`${i + 1}. adımı tamamlandı işaretle`}
className={`w-7 h-7 rounded-full text-white text-[11px] font-black flex items-center justify-center transition-colors ${
secili ? 'bg-emerald-600' : (FAZ_RENK[a.phase] || 'bg-slate-700')
}`}
>
{secili ? '✓' : (a.step ?? i + 1)}
</button>
{i < roadmap.length - 1 && <div className="w-px flex-1 bg-slate-200 my-1" />}
</div>
<div className={`flex-1 min-w-0 bg-slate-50 border border-slate-200 rounded-xl p-3 space-y-1 mb-1 ${secili ? 'opacity-60' : ''}`}>
<div className="flex items-start justify-between gap-2 flex-wrap">
<span className={`text-xs font-bold ${secili ? 'line-through text-slate-400' : 'text-slate-900'}`}>
{a.action || '—'}
</span>
{a.phase && (
<span className="text-[10px] font-extrabold text-white px-2 py-0.5 rounded shrink-0 bg-slate-700">
{a.phase}
</span>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-[11px] text-slate-500">
{a.owner && <span><strong className="text-slate-600">Kim:</strong> {a.owner}</span>}
{a.timing && <span><strong className="text-slate-600">Ne zaman:</strong> {a.timing}</span>}
{a.output && <span><strong className="text-slate-600">Çıktı:</strong> {a.output}</span>}
</div>
</div>
</li>
);
})}
</ol>
</div>
)}
{/* İdareye sorulacaklar */}
{dolu(questionsForAdministration) && (
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-5 space-y-3">
<Baslik icon={HelpCircle} renk="text-blue-700">İdareye Sorulması Gereken Belirsizlikler</Baslik>
<ul className="space-y-1.5">
{questionsForAdministration.map((q, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-blue-900 leading-relaxed">
<span className="font-black text-blue-500 shrink-0">{i + 1}.</span>
<span>{q}</span>
</li>
))}
</ul>
<p className="text-[10px] text-blue-700 leading-relaxed">
Bu sorular için şartnamede belirtilen süre içinde idareye açıklama talebi / zeyilname
başvurusu yapabilirsiniz.
</p>
</div>
)}
{/* Analiz ve yol haritası okunduktan sonraki adım: teklif zarfını hazırla */}
<BidPackageBuilder results={results} tender={tender} />
</div>
);
};
+159 -18
View File
@@ -1,7 +1,7 @@
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 } from 'lucide-react';
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download, Upload, Trash2, Paperclip, Route } from 'lucide-react';
import { uploadDocument, listTenderDocuments, deleteDocument } from '../services/account';
import { extractText } from '../services/pdfText';
@@ -15,6 +15,7 @@ export const TenderDocuments = ({ tender }) => {
const [busy, setBusy] = useState(null);
const [error, setError] = useState(null);
const [bilgi, setBilgi] = useState(null);
const [ilerleme, setIlerleme] = useState('');
const ikn = tender?.ikn;
const [belgeler, setBelgeler] = useState([]);
@@ -47,19 +48,41 @@ export const TenderDocuments = ({ tender }) => {
try { await deleteDocument(id); belgeleriYukle(); } catch (err) { setError(err.message); }
};
// Yüklenen PDF'i AI'ya ver
const belgeyiAnalizEt = async (b) => {
setBusy('belge'); setError(null);
try {
const r = await fetch(`/api/me/documents/download?id=${encodeURIComponent(b.id)}`, {
// PDF/metin tarayıcıda (pdfjs) okunur; docx/xlsx/zip için tarayıcıda çözücü
// yok, sunucudaki çıkarıcıya sorulur. .doc ve .rar'ın karşılığı yok.
const tarayicidaOkunur = (ad) => /\.(pdf|txt|md)$/i.test(String(ad || ''));
const sunucudaOkunur = (ad) => /\.(docx|xlsx|zip)$/i.test(String(ad || ''));
const okunabilirMi = (ad) => tarayicidaOkunur(ad) || sunucudaOkunur(ad);
const belgeMetni = async (b) => {
if (sunucudaOkunur(b.ad)) {
const r = await fetch(`/api/me/documents/text?id=${encodeURIComponent(b.id)}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!r.ok) throw new Error('Belge indirilemedi');
const blob = await r.blob();
const metin = await extractText(new File([blob], b.ad, { type: b.mime || 'application/pdf' }));
const d = await r.json().catch(() => ({}));
if (!d.success) throw new Error(d.error || 'metin çıkarılamadı');
return d.metin;
}
const r = await fetch(`/api/me/documents/download?id=${encodeURIComponent(b.id)}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!r.ok) throw new Error('belge indirilemedi');
const blob = await r.blob();
// extractText { text, pageCount, usedPages } döndürür — nesneyi metin sanıp
// göndermek analizi sessizce bozuyordu.
const { text } = await extractText(new File([blob], b.ad, { type: b.mime || 'application/pdf' }));
return text;
};
// 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 metin = await belgeMetni(b);
if (!metin || metin.length < 100) throw new Error('Belgeden analiz için yeterli metin çıkarılamadı (taranmış PDF olabilir).');
setIsAiSpecAnalyzerOpen(true);
await analyzeSpecPdfWithGemini({ text: metin, fileName: b.ad, tender });
await analyzeSpecPdfWithGemini({ text: metin, fileName: b.ad, tender, mode: mod });
} catch (e) {
setError(e.message);
} finally { setBusy(null); }
@@ -79,14 +102,19 @@ export const TenderDocuments = ({ tender }) => {
} finally { setBusy(null); }
};
const ilanMetniGetir = async () => {
const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
const d = await r.json();
if (!d.success) throw new Error(d.error);
return d;
};
const ilaniAnalizEt = async () => {
setBusy('analiz'); setError(null);
try {
const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
const d = await r.json();
if (!d.success) throw new Error(d.error);
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 });
setIsAiSpecAnalyzerOpen(true);
@@ -96,6 +124,63 @@ export const TenderDocuments = ({ tender }) => {
} 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 = [];
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 {
atlanan.push('EKAP ilan metni (alınamadı)');
}
}
for (const b of belgeler) {
if (!okunabilirMi(b.ad)) { atlanan.push(`${b.ad} (metin çıkarılamıyor)`); continue; }
setIlerleme(`${b.ad} okunuyor…`);
try {
const t = await belgeMetni(b);
if (t && t.length >= 200) parcalar.push(`===== BELGE: ${b.ad} =====\n${t}`);
else atlanan.push(`${b.ad} (yeterli metin yok — taranmış olabilir)`);
} catch (e) {
atlanan.push(`${b.ad} (${e.message})`);
}
}
return { metin: parcalar.join('\n\n'), parcaSayisi: parcalar.length, atlanan };
};
const derinAnalizEt = async () => {
setBusy('derin'); setError(null); setBilgi(null);
try {
const { metin, parcaSayisi, atlanan } = 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('');
setIsAiSpecAnalyzerOpen(true);
await analyzeSpecPdfWithGemini({
text: metin,
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">
@@ -140,7 +225,7 @@ export const TenderDocuments = ({ tender }) => {
</button>
</div>
{bilgi && (
{bilgi && !bilgi.derin && (
<p className="text-[11px] text-slate-500">
{bilgi.dokumanSayisi != null && <>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. </>}
@@ -173,7 +258,7 @@ export const TenderDocuments = ({ tender }) => {
ref={dosyaRef}
type="file"
onChange={dosyaSec}
accept=".pdf,.doc,.docx,.zip,.rar,.txt"
accept=".pdf,.docx,.xlsx,.zip,.txt"
className="hidden"
/>
<button
@@ -184,7 +269,7 @@ export const TenderDocuments = ({ tender }) => {
{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, Word, ZIP · en fazla 10 MB</span>
<span className="text-[10px] text-slate-400">PDF, DOCX, XLSX, ZIP · en fazla 10 MB</span>
</div>
</div>
@@ -207,6 +292,14 @@ export const TenderDocuments = ({ tender }) => {
>
{busy === 'belge' ? '…' : 'AI analiz'}
</button>
<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>
<button onClick={() => belgeSil(b.id)} className="text-slate-300 hover:text-red-600" aria-label="Sil">
<Trash2 className="w-3.5 h-3.5" />
</button>
@@ -215,6 +308,54 @@ export const TenderDocuments = ({ tender }) => {
</div>
)}
{(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
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>
);
};