Bind offer assistant requests and private context to their session
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { MessageSquareText, ClipboardCheck, PenLine, Loader2, Copy, Upload } from 'lucide-react';
|
||||
import { getToken } from '../services/auth';
|
||||
import { listDocuments } from '../services/account';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { requestOfferAssistant } from '../services/offerAssistant';
|
||||
|
||||
/**
|
||||
* Teklif Asistanı sekmesi — üç araç (İhaleAB'den taşındı):
|
||||
@@ -10,17 +12,24 @@ import { listDocuments } from '../services/account';
|
||||
* 3) Taslak Üret: kapak / teknik / ticari / tam dosya (rakam uydurmaz, [DOLDURUN])
|
||||
* Doküman metni sunucuda EKAP'tan alınır; yoksa buradan .txt/.pdf metni yapıştırılır.
|
||||
*/
|
||||
const post = async (url, body) => {
|
||||
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }, body: JSON.stringify(body) });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!r.ok || d.success === false) throw new Error(d.error || `HTTP ${r.status}`);
|
||||
return d;
|
||||
};
|
||||
|
||||
const ORNEKLER = ['Geçici teminat oranı ve süresi nedir?', 'Teslim süresi kaç gün?', 'Gecikme cezası ne kadar?', 'Hangi belgeler istenmiş?', 'Numune isteniyor mu?'];
|
||||
const DURUM = { karsilaniyor: ['Karşılanıyor', 'bg-emerald-100 text-emerald-800 border-emerald-200'], eksik: ['Eksik', 'bg-red-100 text-red-800 border-red-200'], belirsiz: ['Belirsiz', 'bg-amber-100 text-amber-800 border-amber-200'] };
|
||||
|
||||
export const TeklifAsistani = ({ tender }) => {
|
||||
const {authUser} = useApp();
|
||||
const token = getToken();
|
||||
const identity = useRef({token, revision:0});
|
||||
if (identity.current.token !== token) identity.current = {token, revision:identity.current.revision + 1};
|
||||
return <OfferAssistantSession key={`${authUser?.email || ''}:${tender.ikn || tender.id}:${identity.current.revision}`} tender={tender} />;
|
||||
};
|
||||
|
||||
const OfferAssistantSession = ({ tender }) => {
|
||||
const live = useRef(true);
|
||||
const session = useRef(getToken());
|
||||
const busy = useRef(false);
|
||||
useEffect(() => { live.current = true; return () => { live.current = false; }; }, []);
|
||||
const current = () => live.current && Boolean(session.current) && getToken() === session.current;
|
||||
const post = (url, body, documents) => requestOfferAssistant({url, body, readToken:getToken, isCurrent:current, listDocuments:documents});
|
||||
const [arac, setArac] = useState('sor');
|
||||
const [metin, setMetin] = useState(''); // isteğe bağlı: yapıştırılan doküman metni
|
||||
const [soru, setSoru] = useState('');
|
||||
@@ -32,21 +41,34 @@ export const TeklifAsistani = ({ tender }) => {
|
||||
const [mesgul, setMesgul] = useState(false);
|
||||
const [hata, setHata] = useState(null);
|
||||
|
||||
const calistir = async (fn) => { setMesgul(true); setHata(null); try { await fn(); } catch (e) { setHata(e.message); } finally { setMesgul(false); } };
|
||||
const sor = (q) => calistir(async () => { setCevap(await post('/api/ai/belge-sor', { ikn: tender.ikn, soru: q, text: metin || undefined, tender })); });
|
||||
const denetle = () => calistir(async () => { setDenetim(await post('/api/ai/teklif-denetim', { ikn: tender.ikn, taslak, tender, text: metin || undefined })); });
|
||||
const uret = () => calistir(async () => {
|
||||
const belgeler = await listDocuments().then((d) => (d.belgeler || []).map((b) => ({ ad: b.ad, kategori: b.kategori }))).catch(() => []);
|
||||
setBolumler((await post('/api/ai/teklif-taslak', { ikn: tender.ikn, tur, tender, belgeler, text: metin || undefined })).bolumler || []);
|
||||
const calistir = async (fn) => {
|
||||
if (busy.current || !current()) return;
|
||||
busy.current = true; setMesgul(true); setHata(null);
|
||||
try { await fn(); }
|
||||
catch (e) { if (current()) setHata(e.message); }
|
||||
finally { busy.current = false; if (current()) setMesgul(false); }
|
||||
};
|
||||
const sor = (q) => calistir(async () => {
|
||||
const result = await post('/api/ai/belge-sor', {ikn:tender.ikn, soru:q, text:metin || undefined, tender});
|
||||
if (current()) setCevap(result);
|
||||
});
|
||||
const kopyala = (t) => navigator.clipboard?.writeText(t).catch(() => {});
|
||||
const dosyaOku = async (f) => {
|
||||
const denetle = () => calistir(async () => {
|
||||
const result = await post('/api/ai/teklif-denetim', {ikn:tender.ikn, taslak, tender, text:metin || undefined});
|
||||
if (current()) setDenetim(result);
|
||||
});
|
||||
const uret = () => calistir(async () => {
|
||||
const result = await post('/api/ai/teklif-taslak', {ikn:tender.ikn, tur, tender, text:metin || undefined}, listDocuments);
|
||||
if (current()) setBolumler(result.bolumler || []);
|
||||
});
|
||||
const kopyala = (t) => { if (current()) navigator.clipboard?.writeText(t).catch(() => {}); };
|
||||
const dosyaOku = (f) => {
|
||||
if (!f) return;
|
||||
try {
|
||||
return calistir(async () => {
|
||||
const { extractText } = await import('../services/pdfText');
|
||||
if (!current()) return;
|
||||
const r = await extractText(f);
|
||||
setMetin(r.text || '');
|
||||
} catch (e) { setHata('Dosya okunamadı: ' + e.message); }
|
||||
if (current()) { setMetin(r.text || ''); setCevap(null); setDenetim(null); }
|
||||
});
|
||||
};
|
||||
|
||||
const Sekme = ({ id, icon: Icon, children }) => (
|
||||
@@ -64,7 +86,7 @@ export const TeklifAsistani = ({ tender }) => {
|
||||
<Sekme id="uret" icon={PenLine}>Taslak Üret</Sekme>
|
||||
<label className="ml-auto inline-flex items-center gap-1.5 text-[11px] text-slate-600 cursor-pointer border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">
|
||||
<Upload className="w-3.5 h-3.5" /> {metin ? `Doküman metni yüklendi (${metin.length.toLocaleString('tr-TR')} kr.)` : 'Doküman yükle (.pdf/.txt) — isteğe bağlı'}
|
||||
<input type="file" accept=".pdf,.txt,.md" className="hidden" onChange={(e) => dosyaOku(e.target.files?.[0])} />
|
||||
<input type="file" disabled={mesgul} accept=".pdf,.txt,.md" className="hidden" onChange={(e) => dosyaOku(e.target.files?.[0])} />
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500">Metin sağlanmazsa sunucu EKAP dokümanını / ilan metnini kullanır. Yanıtlar yalnızca belgeden; rakamlar aynen aktarılır, model uydurmaz.</p>
|
||||
@@ -76,7 +98,7 @@ export const TeklifAsistani = ({ tender }) => {
|
||||
<input value={soru} onChange={(e) => setSoru(e.target.value)} placeholder="Ör. Geçici teminat oranı nedir?" className="flex-1 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm" />
|
||||
<button type="submit" disabled={mesgul} className="bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white font-bold text-xs px-4 rounded-lg">{mesgul ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Sor'}</button>
|
||||
</form>
|
||||
{!cevap && <div className="flex flex-wrap gap-1.5">{ORNEKLER.map((o) => <button key={o} type="button" onClick={() => { setSoru(o); sor(o); }} className="rounded-full border border-slate-200 px-2 py-0.5 text-[11px] text-slate-600 hover:bg-slate-50">{o}</button>)}</div>}
|
||||
{!cevap && <div className="flex flex-wrap gap-1.5">{ORNEKLER.map((o) => <button key={o} type="button" disabled={mesgul} onClick={() => { setSoru(o); sor(o); }} className="rounded-full border border-slate-200 px-2 py-0.5 text-[11px] text-slate-600 hover:bg-slate-50">{o}</button>)}</div>}
|
||||
{cevap && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-800 whitespace-pre-wrap leading-relaxed">{cevap.cevap}</p>
|
||||
|
||||
Reference in New Issue
Block a user