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
This commit is contained in:
@@ -25,6 +25,7 @@ import { DocumentVault } from './components/tools/DocumentVault';
|
|||||||
import { AnalyticsDashboard } from './components/tools/AnalyticsDashboard';
|
import { AnalyticsDashboard } from './components/tools/AnalyticsDashboard';
|
||||||
import { ApiWebhooks } from './components/tools/ApiWebhooks';
|
import { ApiWebhooks } from './components/tools/ApiWebhooks';
|
||||||
import { PwaSettings } from './components/tools/PwaSettings';
|
import { PwaSettings } from './components/tools/PwaSettings';
|
||||||
|
import { AiModelSettings } from './components/tools/AiModelSettings';
|
||||||
|
|
||||||
const MainContent = () => {
|
const MainContent = () => {
|
||||||
const { activeTab } = useApp();
|
const { activeTab } = useApp();
|
||||||
@@ -57,6 +58,7 @@ const MainContent = () => {
|
|||||||
{activeTab === 'analitik_dashboard' && <AnalyticsDashboard />}
|
{activeTab === 'analitik_dashboard' && <AnalyticsDashboard />}
|
||||||
{activeTab === 'api_webhooks' && <ApiWebhooks />}
|
{activeTab === 'api_webhooks' && <ApiWebhooks />}
|
||||||
{activeTab === 'pwa_settings' && <PwaSettings />}
|
{activeTab === 'pwa_settings' && <PwaSettings />}
|
||||||
|
{activeTab === 'ai_ayarlari' && <AiModelSettings />}
|
||||||
|
|
||||||
{/* Global Modals */}
|
{/* Global Modals */}
|
||||||
<DetailedSearchModal />
|
<DetailedSearchModal />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone } from 'lucide-react';
|
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone, Cpu } from 'lucide-react';
|
||||||
|
|
||||||
export const TopNavigation = () => {
|
export const TopNavigation = () => {
|
||||||
const { activeTab, setActiveTab } = useApp();
|
const { activeTab, setActiveTab } = useApp();
|
||||||
@@ -18,6 +18,7 @@ export const TopNavigation = () => {
|
|||||||
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
|
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
|
||||||
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
|
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
|
||||||
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
|
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
|
||||||
|
{ id: 'ai_ayarlari', label: 'AI Model Ayarları', icon: Cpu, badge: 'Admin' },
|
||||||
{ id: 'pwa_settings', label: 'Mobil PWA', icon: Smartphone }
|
{ id: 'pwa_settings', label: 'Mobil PWA', icon: Smartphone }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -128,12 +128,42 @@ export const AppProvider = ({ children }) => {
|
|||||||
{ id: 'key-1', name: 'ERP Entegrasyon Anahtarı', key: 'ih_live_8f92a19c3b88412e', createdAt: '2026-04-10', lastUsed: 'Bugün 12:45' }
|
{ id: 'key-1', name: 'ERP Entegrasyon Anahtarı', key: 'ih_live_8f92a19c3b88412e', createdAt: '2026-04-10', lastUsed: 'Bugün 12:45' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const AI_MODEL_CATALOG = [
|
||||||
|
{ id: 'glm', label: 'GLM 5.3', provider: 'Z.ai', alias: '~z-ai/glm-latest', model: 'glm-5.3', renk: 'blue' },
|
||||||
|
{ id: 'deepseek', label: 'DeepSeek V4 Flash', provider: 'DeepSeek', alias: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-v4-flash-0731', renk: 'indigo' },
|
||||||
|
{ id: 'gemini', label: 'Gemini 3.7 Flash', provider: 'Google', alias: '~google/gemini-flash-latest', model: 'gemini-3.7-flash', renk: 'amber' },
|
||||||
|
{ id: 'grok', label: 'Grok 4.6', provider: 'xAI', alias: '~x-ai/grok-latest', model: 'grok-4.6', renk: 'slate' },
|
||||||
|
{ id: 'gpt', label: 'GPT-5.6 Sol', provider: 'OpenAI', alias: '~openai/gpt-latest', model: 'gpt-5.6-sol', renk: 'emerald' },
|
||||||
|
{ id: 'claude', label: 'Claude Opus 5', provider: 'Anthropic', alias: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Varsayılan fallback zinciri: GLM (birincil) → DeepSeek → Gemini
|
||||||
|
const [aiModelConfig, setAiModelConfig] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('ihaleist_ai_model_config');
|
||||||
|
if (saved) {
|
||||||
|
try { return JSON.parse(saved); } catch { /* yoksay */ }
|
||||||
|
}
|
||||||
|
return { chain: ['glm', 'deepseek', 'gemini'] };
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('ihaleist_ai_model_config', JSON.stringify(aiModelConfig));
|
||||||
|
}, [aiModelConfig]);
|
||||||
|
|
||||||
|
const getModelById = (id) => AI_MODEL_CATALOG.find(m => m.id === id);
|
||||||
|
const activeAiModel = getModelById(aiModelConfig.chain[0]) || AI_MODEL_CATALOG[0];
|
||||||
|
|
||||||
const analyzeSpecPdfWithGemini = (fileName = 'Teknik_Sartname.pdf') => {
|
const analyzeSpecPdfWithGemini = (fileName = 'Teknik_Sartname.pdf') => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const result = {
|
const result = {
|
||||||
fileName,
|
fileName,
|
||||||
model: 'Gemini 3.6 Flash',
|
model: `${activeAiModel.label} (${activeAiModel.provider})`,
|
||||||
|
modelId: activeAiModel.model,
|
||||||
|
fallbackChain: aiModelConfig.chain.map(id => getModelById(id)?.model).filter(Boolean),
|
||||||
analyzedAt: new Date().toLocaleString('tr-TR'),
|
analyzedAt: new Date().toLocaleString('tr-TR'),
|
||||||
summary: 'İşbu teknik şartname, idarenin ihtiyacı olan medikal sarf malzemeleri ve cihaz parçalarının teknik özellikleri, garanti şartları, teslimat vadeleri ve muayene kabul kriterlerini düzenlemektedir.',
|
summary: 'İşbu teknik şartname, idarenin ihtiyacı olan medikal sarf malzemeleri ve cihaz parçalarının teknik özellikleri, garanti şartları, teslimat vadeleri ve muayene kabul kriterlerini düzenlemektedir.',
|
||||||
qualificationCriteria: [
|
qualificationCriteria: [
|
||||||
@@ -348,6 +378,11 @@ export const AppProvider = ({ children }) => {
|
|||||||
setWebhooksConfig,
|
setWebhooksConfig,
|
||||||
apiKeys,
|
apiKeys,
|
||||||
setApiKeys,
|
setApiKeys,
|
||||||
|
AI_MODEL_CATALOG,
|
||||||
|
aiModelConfig,
|
||||||
|
setAiModelConfig,
|
||||||
|
getModelById,
|
||||||
|
activeAiModel,
|
||||||
userProfile,
|
userProfile,
|
||||||
notifications,
|
notifications,
|
||||||
toggleTrackTender,
|
toggleTrackTender,
|
||||||
|
|||||||
Reference in New Issue
Block a user