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:
@@ -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 & 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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user