204 lines
9.8 KiB
React
204 lines
9.8 KiB
React
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>
|
||
);
|
||
};
|