Files
ihaleist/src/components/tools/AiModelSettings.jsx
T
hikmettan cc0b6b6017 feat(admin): AI model yönetimi paneli (fallback zinciri seçimi)
- 6 model kataloğu: GLM 5.3, DeepSeek V4 Flash, Gemini 3.7 Flash, Grok 4.6, GPT-5.6 Sol, Claude Opus 5
- Varsayılan fallback zinciri: GLM (birincil) → DeepSeek → Gemini
- Admin panelden sıralama, birincil seçme, ekleme/çıkarma; localStorage'da saklanır
- AI analizi aktif modeli kullanır
2026-08-27 09:44:34 +03:00

172 lines
7.9 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 } from 'react';
import { useApp } from '../../context/AppContext';
import { Cpu, Check, ArrowUp, ArrowDown, X, Plus, Sparkles, Save, Shield } from 'lucide-react';
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 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">
{/* 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
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.</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 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 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">{m.alias} <span className="text-slate-400">→</span> <span className="text-slate-700 font-semibold">{m.model}</span></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 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 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
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>
);
};