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">