Files
ihaleist/src/components/tools/AiModelSettings.jsx
T

204 lines
9.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { Cpu, Check, ArrowUp, ArrowDown, X, Plus, Sparkles, Save, Shield, Zap, AlertTriangle } from 'lucide-react';
import { getAiConfig } from '../../services/ai';
import confetti from 'canvas-confetti';
const RENK = {
blue: 'bg-blue-50 text-blue-700 border-blue-200',
indigo: 'bg-indigo-50 text-indigo-700 border-indigo-200',
amber: 'bg-amber-50 text-amber-700 border-amber-200',
slate: 'bg-slate-100 text-slate-700 border-slate-200',
emerald: 'bg-emerald-50 text-emerald-700 border-emerald-200',
orange: 'bg-orange-50 text-orange-700 border-orange-200'
};
const SIRA_ETIKET = ['Birincil (Primary)', '2. Yedek (Fallback)', '3. Yedek (Fallback)', '4. Yedek', '5. Yedek', '6. Yedek'];
export const AiModelSettings = () => {
const { AI_MODEL_CATALOG, aiModelConfig, setAiModelConfig, getModelById } = useApp();
const [saved, setSaved] = useState(false);
const [cfg, setCfg] = useState(null);
useEffect(() => { getAiConfig().then(setCfg); }, []);
const aktifYol = cfg?.providers?.[0]?.yol;
const chain = aiModelConfig.chain;
const available = AI_MODEL_CATALOG.filter(m => !chain.includes(m.id));
const update = (newChain) => {
setAiModelConfig({ ...aiModelConfig, chain: newChain });
setSaved(false);
};
const move = (idx, dir) => {
const j = idx + dir;
if (j < 0 || j >= chain.length) return;
const next = [...chain];
[next[idx], next[j]] = [next[j], next[idx]];
update(next);
};
const setPrimary = (idx) => {
if (idx === 0) return;
const next = [...chain];
const [item] = next.splice(idx, 1);
next.unshift(item);
update(next);
};
const remove = (idx) => {
if (chain.length <= 1) return; // en az bir model kalmalı
update(chain.filter((_, i) => i !== idx));
};
const add = (id) => update([...chain, id]);
const handleSave = () => {
// aiModelConfig zaten localStorage'a otomatik yazılıyor; buton kullanıcı geri bildirimi için
setSaved(true);
confetti({ particleCount: 60, spread: 70 });
setTimeout(() => setSaved(false), 3500);
};
return (
<div className="space-y-6 animate-fade-in">
{/* Canlı AI durumu */}
{cfg && (
cfg.configured ? (
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 flex gap-3">
<Zap className="w-4 h-4 text-emerald-700 shrink-0 mt-0.5" />
<div className="text-xs text-emerald-900 leading-relaxed">
<strong>AI aktif</strong> — {cfg.providers.length} model kullanılabilir
{aktifYol === 'openrouter' ? ' (OpenRouter üzerinden tek anahtarla)' : ' (doğrudan sağlayıcı anahtarlarıyla)'}.
<div className="font-mono text-[11px] mt-1 text-emerald-800">
{cfg.providers.map((p) => `${p.label}: ${p.model}`).join(' · ')}
</div>
</div>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<p className="text-xs text-amber-900 leading-relaxed">
<strong>AI anahtarı yok.</strong> Sunucuya tek bir <code className="bg-amber-100 px-1 rounded">OPENROUTER_API_KEY</code> ekleyin —
aşağıdaki tüm modeller bu tek anahtarla çalışır. Aşağıdaki sıralama yalnızca tercih kaydeder;
çağrılar anahtar eklendiğinde başlar.
</p>
</div>
)
)}
{/* Header */}
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-blue-900 text-white p-6 rounded-2xl shadow-lg border border-indigo-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center font-black shadow-md">
<Cpu className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Yapay Zeka Model Yönetimi</h2>
<p className="text-xs text-indigo-200 mt-0.5">Şartname analizi ve AI özelliklerinde kullanılacak modeli ve yedek (fallback) sırasını belirleyin.</p>
</div>
</div>
<button type="button"
onClick={handleSave}
className={`px-4 py-2.5 rounded-xl font-bold text-sm flex items-center gap-2 transition ${saved ? 'bg-emerald-500 text-white' : 'bg-white text-indigo-900 hover:bg-indigo-50'}`}
>
{saved ? <><Check className="w-4 h-4" /> Kaydedildi</> : <><Save className="w-4 h-4" /> Kaydet</>}
</button>
</div>
{/* Aktif zincir */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-indigo-600" /> Aktif Model Sırası (Fallback Zinciri)
</h3>
<p className="text-xs text-slate-500">Birincil model yanıt vermezse sıradaki yedeğe otomatik geçilir. Zincirde olmasa bile <strong>Gemini Flash</strong> ücretli yedek olarak her zaman en sonda denenir; yalnız üsttekilerin hepsi başarısız olursa devreye girer.</p>
</div>
</div>
<div className="space-y-3">
{chain.map((id, idx) => {
const m = getModelById(id);
if (!m) return null;
return (
<div
key={id}
className={`flex items-center gap-4 p-4 rounded-xl border ${idx === 0 ? 'border-indigo-300 bg-indigo-50/40 ring-1 ring-indigo-200' : 'border-slate-200 bg-slate-50/60'}`}
>
<div className="flex flex-col items-center gap-1 w-8">
<button type="button" onClick={() => move(idx, -1)} disabled={idx === 0} className="text-slate-400 hover:text-indigo-600 disabled:opacity-30">
<ArrowUp className="w-4 h-4" />
</button>
<span className="text-xs font-black text-slate-400">{idx + 1}</span>
<button type="button" onClick={() => move(idx, 1)} disabled={idx === chain.length - 1} className="text-slate-400 hover:text-indigo-600 disabled:opacity-30">
<ArrowDown className="w-4 h-4" />
</button>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-extrabold text-sm text-slate-900">{m.label}</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${RENK[m.renk] || RENK.slate}`}>{m.provider}</span>
{idx === 0 && (
<span className="text-[10px] font-black px-2 py-0.5 rounded-md bg-indigo-600 text-white flex items-center gap-1">
<Shield className="w-3 h-3" /> BİRİNCİL
</span>
)}
</div>
<div className="text-[11px] text-slate-500 font-mono mt-1">
<span className="text-slate-700 font-semibold">{m.or}</span>
<span className="text-slate-400"> · doğrudan: </span>{m.model}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">{SIRA_ETIKET[idx] || `${idx + 1}. sırada`}</div>
</div>
<div className="flex items-center gap-2">
{idx !== 0 && (
<button type="button" onClick={() => setPrimary(idx)} className="text-[11px] font-bold text-indigo-600 hover:text-indigo-800 border border-indigo-200 rounded-lg px-2.5 py-1.5">
Birincil yap
</button>
)}
<button type="button" onClick={() => remove(idx)} disabled={chain.length <= 1} className="text-slate-400 hover:text-red-500 disabled:opacity-30 p-1.5">
<X className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
{/* Eklenebilir modeller */}
{available.length > 0 && (
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900">Kullanılabilir Diğer Modeller</h3>
<p className="text-xs text-slate-500">Zincire eklemek için bir modele tıklayın.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{available.map(m => (
<button type="button"
key={m.id}
onClick={() => add(m.id)}
className="text-left p-4 rounded-xl border border-slate-200 hover:border-indigo-300 hover:bg-indigo-50/40 transition group"
>
<div className="flex items-center justify-between">
<span className="font-extrabold text-sm text-slate-900">{m.label}</span>
<Plus className="w-4 h-4 text-slate-300 group-hover:text-indigo-600" />
</div>
<span className={`inline-block mt-1 text-[10px] font-bold px-2 py-0.5 rounded-md border ${RENK[m.renk] || RENK.slate}`}>{m.provider}</span>
<div className="text-[11px] text-slate-500 font-mono mt-2">{m.model}</div>
</button>
))}
</div>
</div>
)}
<p className="text-[11px] text-slate-400 text-center">
Seçimler tarayıcınızda saklanır ve tüm AI özelliklerinde (şartname analizi, fiyat/risk değerlendirmesi) kullanılır.
</p>
</div>
);
};