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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user