Analyze server-resolved document reviews with revision checks

This commit is contained in:
2026-09-06 09:49:30 +03:00
parent e49593be46
commit 742e05933e
11 changed files with 87 additions and 18 deletions
+3
View File
@@ -245,6 +245,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{kaynakGecerlilik.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı süreler var. Güncel resmi dokümanı belirleyip analizi yenileyin; çelişki çözülmeden çıktı alınamaz.</p>}
{[...new Set(kaynakGecerlilik.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
</div>}
{effectiveResults.sourceReview?.humanReview && <p className="text-xs text-slate-600">
Metin incelemesi: sürüm {effectiveResults.sourceReview.humanReview.revision} · {effectiveResults.sourceReview.humanReview.reviewedBy} · {effectiveResults.sourceReview.humanReview.reviewedAt}. Kullanıcı karşılaştırmasıdır; resmi onay değildir.
</p>}
{analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.</p>}
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
+19 -6
View File
@@ -2,7 +2,7 @@ import React,{useEffect,useRef,useState} from 'react';
import {getDocumentTextReview,saveDocumentTextReview} from '../services/account';
import {getToken} from '../services/auth';
export function DocumentTextReview({documentId,extractedText}) {
export function DocumentTextReview({documentId,extractedText,onAnalyze}) {
const [source,setSource]=useState(null);
const [text,setText]=useState(extractedText || '');
const [note,setNote]=useState('');
@@ -10,6 +10,7 @@ export function DocumentTextReview({documentId,extractedText}) {
const [busy,setBusy]=useState(false);
const [error,setError]=useState(null);
const [saved,setSaved]=useState(null);
const [analyzing,setAnalyzing]=useState(false);
const live=useRef(true);
const sequence=useRef(0);
const load=async()=>{
@@ -38,27 +39,39 @@ export function DocumentTextReview({documentId,extractedText}) {
} catch(e) {if(live.current && token===getToken())setError(e.message);}
finally {if(live.current && token===getToken())setBusy(false);}
};
const analyze=async()=>{
if(!source?.review || source.stale || busy || analyzing)return;
const token=getToken();setAnalyzing(true);setError(null);
try {await onAnalyze({documentId,revision:source.review.revision});}
catch(e) {if(live.current && token===getToken())setError(e.message);}
finally {if(live.current && token===getToken())setAnalyzing(false);}
};
return <section aria-label="Belge metni incelemesi" className="border-t pt-3 space-y-2">
<h5 className="font-bold text-sm">Metni karşılaştır ve düzelt</h5>
<p className="text-xs text-slate-600">Belgenin tüm sayfalarını açıp metin, miktar ve birimleri karşılaştırın. Bu kayıt kullanıcı incelemesidir; resmî onay veya e-imza değildir. Kaydetmek tek başına teklif dosyasını hazır yapmaz.</p>
{source?.stale && <p role="alert" className="text-xs text-amber-800">Kaynak dosya değişmiş. Önceki inceleme geçerli değil; tüm sayfaları yeniden karşılaştırın.</p>}
{source?.review && <p className="text-xs text-slate-500">Son kayıt: sürüm {source.review.revision} · {new Date(source.review.reviewed_at).toLocaleString('tr-TR')}</p>}
<label className="block text-xs font-semibold">Kontrol edilen metin
<textarea value={text} onChange={e=>{setText(e.target.value);setCompared(false);setSaved(null);}} disabled={busy}
<textarea value={text} onChange={e=>{setText(e.target.value);setCompared(false);setSaved(null);}} disabled={busy || analyzing}
className="block w-full h-64 border rounded p-2 font-mono font-normal mt-1" />
</label>
<label className="block text-xs font-semibold">Karşılaştırılan sayfalar ve düzeltmeler
<textarea value={note} onChange={e=>{setNote(e.target.value);setCompared(false);setSaved(null);}} disabled={busy}
<textarea value={note} onChange={e=>{setNote(e.target.value);setCompared(false);setSaved(null);}} disabled={busy || analyzing}
placeholder="Örnek: Sayfa 1–3 karşılaştırıldı; 2. sayfadaki miktar 100 olarak düzeltildi."
className="block w-full border rounded p-2 font-normal mt-1" />
</label>
<label className="flex gap-2 text-xs"><input type="checkbox" checked={compared} onChange={e=>setCompared(e.target.checked)} disabled={busy}/>
<label className="flex gap-2 text-xs"><input type="checkbox" checked={compared} onChange={e=>setCompared(e.target.checked)} disabled={busy || analyzing}/>
Bu metni kaynak dosyanın tüm sayfalarıyla karşılaştırdım.</label>
<button onClick={save} disabled={busy || !source || !compared || text.trim().length<40 || note.trim().length<20}
<button onClick={save} disabled={busy || analyzing || !source || !compared || text.trim().length<40 || note.trim().length<20}
className="text-xs bg-blue-700 text-white px-3 py-2 rounded disabled:opacity-40">{busy ? 'İşleniyor…' : 'İncelemeyi kaydet'}</button>
{onAnalyze && source?.review && !source.stale && <button onClick={analyze}
disabled={busy || analyzing || text.trim()!==source.review.corrected_text || note.trim()!==source.review.review_note}
className="ml-2 text-xs bg-amber-500 px-3 py-2 rounded disabled:opacity-40">
{analyzing ? 'İncelenmiş metin analiz ediliyor…' : 'Kaydedilmiş incelemeyi analiz et'}
</button>}
{error && <div role="alert" className="text-xs text-red-700">{error}
<p>Metin taslağınız bu ekranda korunuyor. Sürüm değiştiyse taslağı kopyalayıp güncel kaydı yükleyin.</p>
<button onClick={load} disabled={busy} className="underline">Güncel kaydı yükle (taslağı değiştirir)</button>
<button onClick={load} disabled={busy || analyzing} className="underline">Güncel kaydı yükle (taslağı değiştirir)</button>
</div>}
{saved && <p role="status" className="text-xs text-green-800">İnceleme sürüm {saved.revision} sunucuda kaydedildi. Analiz ve teklif kontrolleri ayrıca tamamlanmalıdır.</p>}
</section>;
+4 -1
View File
@@ -420,7 +420,10 @@ export const TenderDocuments = ({ tender }) => {
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} />}
{metinOnizleme.type === 'ihale' && <DocumentTextReview key={metinOnizleme.documentId} documentId={metinOnizleme.documentId} extractedText={metinOnizleme.text} onAnalyze={async(reviewedSource)=>{
await analyzeSpecPdfWithGemini({reviewedSource,tender,fileName:metinOnizleme.name});
setIsAiSpecAnalyzerOpen(true);
}} />}
<button onClick={()=>setMetinOnizleme(null)} className="text-xs underline">Metin önizlemesini kapat</button>
</section>}
+2 -2
View File
@@ -428,13 +428,13 @@ export const AppProvider = ({ children }) => {
// Gerçek AI analizi — backend /api/ai/analyze üzerinden, admin panelde
// seçilen model zinciri (fallback) ile.
// mode='derin' → ince detaylar, belge kontrol listesi ve yol haritası da üretilir
const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode, extraction } = {}) => {
const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode, extraction, reviewedSource } = {}) => {
const session = getToken();
setAiAnalyzing(true);
setAiAnalysisResults(null);
try {
const { analyzeSpecText } = await import('../services/ai');
const data = await analyzeSpecText({ text, fileName, tender, chain: aiModelConfig.chain, mode, extraction });
const data = await analyzeSpecText({ text, fileName, tender, chain: aiModelConfig.chain, mode, extraction, reviewedSource });
const result = {
...data.result,
tenderKey: tender?.ikn || tender?.id || null,
+2 -2
View File
@@ -13,11 +13,11 @@ export async function getAiConfig() {
}
}
export async function analyzeSpecText({ text, fileName, tender, chain, mode, extraction }) {
export async function analyzeSpecText({ text, fileName, tender, chain, mode, extraction, reviewedSource }) {
const res = await fetch('/api/ai/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ text, fileName, tender, chain, mode, extraction }),
body: JSON.stringify({ text, fileName, tender, chain, mode, extraction, reviewedSource }),
});
const data = await res.json().catch(() => ({}));
if (!data.success) {
+1
View File
@@ -33,6 +33,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
deadline: [tender?.tarih || '', tender?.saat || ''],
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
procurementTerms: results?.procurementTerms || null,
humanReview: results?.sourceReview?.humanReview || null,
analyzedItems: results?.itemsAndSpecs || [],
rows: rows.map(r => r.priceKey).sort(),
documents: results?.documentChecklist || [],