Initial commit
This commit is contained in:
56
hsbg_ai/frontend/src/App.jsx
Normal file
56
hsbg_ai/frontend/src/App.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Routes, Route, NavLink } from 'react-router-dom'
|
||||
import { Activity, Sword, Brain, Database, Settings } from 'lucide-react'
|
||||
import DashboardPage from './pages/DashboardPage'
|
||||
import GamePage from './pages/GamePage'
|
||||
import LearningPage from './pages/LearningPage'
|
||||
import DatabasePage from './pages/DatabasePage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
|
||||
const NAV = [
|
||||
{ to: '/', icon: Activity, label: 'Dashboard' },
|
||||
{ to: '/game', icon: Sword, label: 'Partie' },
|
||||
{ to: '/learning', icon: Brain, label: 'Apprentissage' },
|
||||
{ to: '/database', icon: Database, label: 'Base HSBG' },
|
||||
{ to: '/settings', icon: Settings, label: 'Paramètres' },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<aside className="w-14 md:w-52 bg-gray-900/95 border-r border-yellow-900/20 flex flex-col pt-4 shrink-0">
|
||||
<div className="px-3 mb-6 hidden md:block">
|
||||
<p className="text-yellow-400 font-bold text-lg tracking-wide">HSBG AI</p>
|
||||
<p className="text-gray-500 text-xs mt-0.5">Assistant Battlegrounds</p>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-1 px-2 flex-1">
|
||||
{NAV.map(({ to, icon: Icon, label }) => (
|
||||
<NavLink
|
||||
key={to} to={to} end={to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-all
|
||||
${isActive
|
||||
? 'bg-yellow-900/40 text-yellow-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/80'}`
|
||||
}
|
||||
>
|
||||
<Icon size={17} className="shrink-0" />
|
||||
<span className="hidden md:inline">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="px-3 py-3 border-t border-gray-800 hidden md:block">
|
||||
<p className="text-gray-600 text-xs">v1.0.0 · Local AI</p>
|
||||
</div>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/game" element={<GamePage />} />
|
||||
<Route path="/learning" element={<LearningPage />} />
|
||||
<Route path="/database" element={<DatabasePage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
hsbg_ai/frontend/src/index.css
Normal file
12
hsbg_ai/frontend/src/index.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-gray-950 text-gray-100;
|
||||
background: radial-gradient(ellipse at top, #1c1010 0%, #080808 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-glow { box-shadow: 0 0 20px rgba(245,179,10,0.3); }
|
||||
.confidence-bar { @apply h-1.5 rounded-full transition-all; }
|
||||
13
hsbg_ai/frontend/src/main.jsx
Normal file
13
hsbg_ai/frontend/src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
105
hsbg_ai/frontend/src/pages/DashboardPage.jsx
Normal file
105
hsbg_ai/frontend/src/pages/DashboardPage.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Activity, Brain, Zap, Target, CheckCircle, XCircle } from 'lucide-react'
|
||||
|
||||
function StatCard({ icon: Icon, label, value, sub, color }) {
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 flex items-center gap-4 hover:border-gray-700 transition-colors">
|
||||
<div className={`p-2.5 rounded-xl ${color}`}><Icon size={20} /></div>
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs mb-0.5">{label}</p>
|
||||
<p className="text-white font-bold text-xl leading-none">{value}</p>
|
||||
{sub && <p className="text-gray-500 text-xs mt-1">{sub}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [health, setHealth] = useState(null)
|
||||
const [stats, setStats] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
axios.get('/health').then(r => setHealth(r.data)).catch(() => {}),
|
||||
axios.get('/api/learning/stats').then(r => setStats(r.data)).catch(() => {}),
|
||||
]).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-yellow-400 mb-1">HSBG AI Assistant</h1>
|
||||
<p className="text-gray-400">Intelligence artificielle locale pour Hearthstone Battlegrounds</p>
|
||||
</div>
|
||||
|
||||
{/* Status serveur */}
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
{health ? (
|
||||
<span className="flex items-center gap-2 bg-green-900/30 border border-green-700/50 rounded-full px-4 py-1.5 text-green-400 text-sm">
|
||||
<CheckCircle size={14} />
|
||||
Serveur actif · v{health.version} · Modèle: {health.llm_model}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2 bg-red-900/30 border border-red-700/50 rounded-full px-4 py-1.5 text-red-400 text-sm">
|
||||
<XCircle size={14} />
|
||||
Serveur hors ligne
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{!loading && stats && (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard icon={Brain} label="Feedbacks total" value={stats.total}
|
||||
color="bg-purple-900/50 text-purple-400" />
|
||||
<StatCard icon={Activity} label="Taux de réussite" value={`${stats.good_rate}%`}
|
||||
sub={`${stats.good} bons / ${stats.bad} mauvais`} color="bg-green-900/50 text-green-400" />
|
||||
<StatCard icon={Zap} label="Entraînements" value={stats.trained}
|
||||
color="bg-blue-900/50 text-blue-400" />
|
||||
<StatCard icon={Target} label="En attente" value={stats.buffer_pending}
|
||||
sub="dans le buffer" color="bg-yellow-900/50 text-yellow-400" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Guide rapide */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-white mb-4">🚀 Démarrage rapide</h3>
|
||||
<ol className="space-y-2.5 text-sm text-gray-300">
|
||||
{[
|
||||
['Partie', 'Démarrer une session et saisir l\'état du jeu'],
|
||||
['Conseil', 'L\'IA génère des recommandations en temps réel'],
|
||||
['Feedback', 'Évaluer les conseils pour entraîner l\'IA'],
|
||||
['Base HSBG', 'Consulter et enrichir les données de cartes'],
|
||||
['Paramètres', 'Configurer le LLM et la vision'],
|
||||
].map(([page, desc], i) => (
|
||||
<li key={i} className="flex gap-2">
|
||||
<span className="text-yellow-400 font-bold shrink-0">{i + 1}.</span>
|
||||
<span><strong className="text-yellow-300">{page}</strong> — {desc}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-white mb-4">🧠 Configuration LLM</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="bg-gray-800 rounded-lg p-3">
|
||||
<p className="text-gray-300 font-mono text-xs mb-1"># Installer Ollama</p>
|
||||
<p className="text-yellow-300 font-mono text-xs">curl -fsSL https://ollama.ai/install.sh | sh</p>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-lg p-3">
|
||||
<p className="text-gray-300 font-mono text-xs mb-1"># Télécharger un modèle</p>
|
||||
<p className="text-yellow-300 font-mono text-xs">ollama pull llama3.2</p>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs">
|
||||
Sans LLM, l'IA fonctionne en mode heuristique (rapide, pas de raisonnement naturel).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
hsbg_ai/frontend/src/pages/DatabasePage.jsx
Normal file
142
hsbg_ai/frontend/src/pages/DatabasePage.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Database, Shield, Sword, Search } from 'lucide-react'
|
||||
|
||||
const TIER_COLORS = {
|
||||
'1': 'bg-gray-600', '2': 'bg-green-800', '3': 'bg-blue-800',
|
||||
'4': 'bg-purple-800', '5': 'bg-orange-800', '6': 'bg-red-800',
|
||||
}
|
||||
|
||||
export default function DatabasePage() {
|
||||
const [tab, setTab] = useState('heroes')
|
||||
const [data, setData] = useState([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [tier, setTier] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const params = new URLSearchParams()
|
||||
if (search) params.append('search', search)
|
||||
if (tier && tab === 'minions') params.append('tier', tier)
|
||||
const url = `/api/database/${tab === 'heroes' ? 'heroes' : 'minions'}?${params}`
|
||||
axios.get(url).then(r => setData(r.data)).catch(() => setData([])).finally(() => setLoading(false))
|
||||
}, [tab, search, tier])
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Database size={24} className="text-blue-400" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Base de données HSBG</h2>
|
||||
<p className="text-gray-400 text-sm">Héros, serviteurs et sorts du patch {import.meta.env?.VITE_PATCH || 'actuel'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
{[{ id: 'heroes', icon: Shield, label: 'Héros' }, { id: 'minions', icon: Sword, label: 'Serviteurs' }].map(({ id, icon: Icon, label }) => (
|
||||
<button key={id} onClick={() => { setTab(id); setSearch(''); setTier('') }}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors
|
||||
${tab === id ? 'bg-blue-700 text-white' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'}`}>
|
||||
<Icon size={15} />{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mb-5">
|
||||
<div className="relative flex-1">
|
||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input value={search} onChange={e => setSearch(e.target.value)}
|
||||
placeholder={`Rechercher ${tab === 'heroes' ? 'un héros' : 'un serviteur'}...`}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-xl pl-9 pr-3 py-2.5 text-sm text-white focus:outline-none focus:border-blue-600" />
|
||||
</div>
|
||||
{tab === 'minions' && (
|
||||
<select value={tier} onChange={e => setTier(e.target.value)}
|
||||
className="bg-gray-900 border border-gray-700 rounded-xl px-3 py-2.5 text-sm text-white focus:outline-none focus:border-blue-600">
|
||||
<option value="">Tous tiers</option>
|
||||
{[1,2,3,4,5,6].map(t => <option key={t} value={t}>Tier {t}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-400 text-center py-10">Chargement...</p>
|
||||
) : tab === 'heroes' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{data.map(h => (
|
||||
<div key={h.id} className="bg-gray-900 border border-gray-800 rounded-xl p-4 hover:border-yellow-800/40 transition-colors">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-bold text-yellow-400">{h.name}</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-yellow-500 text-sm font-bold">{h.tier_rating}</span>
|
||||
<span className="text-gray-600 text-xs">/10</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-300 text-xs mb-2 leading-relaxed">{h.hero_power}</p>
|
||||
{h.description && <p className="text-gray-500 text-xs mb-3">{h.description}</p>}
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{(h.strengths || []).map(s => (
|
||||
<span key={s} className="text-xs bg-green-900/40 text-green-400 border border-green-800/30 px-1.5 py-0.5 rounded">{s}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(h.weaknesses || []).map(w => (
|
||||
<span key={w} className="text-xs bg-red-900/30 text-red-400 border border-red-800/30 px-1.5 py-0.5 rounded">{w}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-800 text-xs text-gray-400 font-medium">
|
||||
{['Nom', 'Tier', 'Race', 'ATT', 'HP', 'Capacités', 'Description'].map(h => (
|
||||
<th key={h} className="px-3 py-2.5 text-left whitespace-nowrap">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-800">
|
||||
{data.map(m => (
|
||||
<tr key={m.id} className="hover:bg-gray-800/30 transition-colors">
|
||||
<td className="px-3 py-2.5 font-semibold text-white whitespace-nowrap">{m.name}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<span className={`text-xs px-2 py-0.5 rounded font-medium text-white ${TIER_COLORS[m.tier] || 'bg-gray-600'}`}>
|
||||
T{m.tier}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-blue-400 text-xs whitespace-nowrap">
|
||||
{Array.isArray(m.race) ? m.race.join(', ') : m.race}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-green-400 font-bold text-center">{m.attack}</td>
|
||||
<td className="px-3 py-2.5 text-red-400 font-bold text-center">{m.health}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{m.has_divine && <span className="text-xs bg-yellow-900/40 text-yellow-300 border border-yellow-800/30 px-1.5 py-0.5 rounded">Divine</span>}
|
||||
{m.has_taunt && <span className="text-xs bg-gray-700 text-gray-300 px-1.5 py-0.5 rounded">Taunt</span>}
|
||||
{m.has_windfury && <span className="text-xs bg-purple-900/40 text-purple-300 px-1.5 py-0.5 rounded">Windfury</span>}
|
||||
{m.has_poisonous && <span className="text-xs bg-green-900/40 text-green-300 px-1.5 py-0.5 rounded">Venin</span>}
|
||||
{m.has_reborn && <span className="text-xs bg-blue-900/40 text-blue-300 px-1.5 py-0.5 rounded">Reborn</span>}
|
||||
{m.battlecry && <span className="text-xs bg-purple-900/30 text-purple-400 px-1.5 py-0.5 rounded">Cri</span>}
|
||||
{m.deathrattle && <span className="text-xs bg-red-900/30 text-red-400 px-1.5 py-0.5 rounded">Mort</span>}
|
||||
{m.passive && <span className="text-xs bg-gray-700 text-gray-400 px-1.5 py-0.5 rounded">Passif</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-gray-400 text-xs max-w-[200px]">
|
||||
{m.battlecry || m.deathrattle || m.passive || '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{data.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-8">Aucun résultat — vérifiez la base de données</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
406
hsbg_ai/frontend/src/pages/GamePage.jsx
Normal file
406
hsbg_ai/frontend/src/pages/GamePage.jsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Play, Square, RefreshCw, Monitor, Swords, Plus, X, ThumbsUp, ThumbsDown, Minus } from 'lucide-react'
|
||||
|
||||
const ACTION_MAP = {
|
||||
buy: { label: 'Acheter', color: 'text-green-400' },
|
||||
sell: { label: 'Vendre', color: 'text-red-400' },
|
||||
freeze: { label: 'Geler', color: 'text-blue-400' },
|
||||
upgrade: { label: 'Monter tier', color: 'text-yellow-400' },
|
||||
reposition: { label: 'Repositionner', color: 'text-purple-400' },
|
||||
hero_power: { label: 'Pouvoir héros', color: 'text-orange-400' },
|
||||
wait: { label: 'Attendre', color: 'text-gray-400' },
|
||||
}
|
||||
|
||||
function ConfBar({ value }) {
|
||||
const pct = Math.round((value || 0) * 100)
|
||||
const col = pct >= 70 ? 'bg-green-500' : pct >= 40 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>Confiance</span><span>{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${col} transition-all duration-500`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MinionChip({ minion, onRemove }) {
|
||||
return (
|
||||
<div className="relative bg-gray-800 border border-gray-600 rounded-lg p-2 text-xs group">
|
||||
{onRemove && (
|
||||
<button onClick={onRemove}
|
||||
className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-red-600 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<X size={10} />
|
||||
</button>
|
||||
)}
|
||||
<p className="font-semibold text-yellow-300 truncate max-w-[90px]">{minion.name || '?'}</p>
|
||||
<p className="text-gray-300">{minion.attack || 0}/{minion.health || 0}</p>
|
||||
{minion.race && (
|
||||
<p className="text-blue-400 text-[10px]">
|
||||
{Array.isArray(minion.race) ? minion.race.join(', ') : minion.race}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AddMinionButton({ onClick }) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
className="border border-dashed border-gray-600 rounded-lg p-2 text-xs text-gray-500 hover:border-yellow-600 hover:text-yellow-500 transition-colors flex flex-col items-center justify-center min-h-[64px]">
|
||||
<Plus size={14} className="mb-1" />
|
||||
Ajouter
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedbackBar({ decisionId, onDone }) {
|
||||
const [sent, setSent] = useState(false)
|
||||
const [better, setBetter] = useState('')
|
||||
const [comment, setComment] = useState('')
|
||||
|
||||
const send = async (rating) => {
|
||||
try {
|
||||
await axios.post('/api/learning/feedback', {
|
||||
decision_id: decisionId, rating,
|
||||
better_action: better ? { action: better } : null,
|
||||
comment: comment || null,
|
||||
})
|
||||
setSent(true)
|
||||
onDone && onDone()
|
||||
} catch { alert('Erreur envoi feedback') }
|
||||
}
|
||||
|
||||
if (sent) return (
|
||||
<div className="bg-green-900/20 border border-green-700/40 rounded-xl p-3 text-green-400 text-sm text-center">
|
||||
✅ Feedback envoyé — merci!
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-4">
|
||||
<p className="text-xs text-gray-400 mb-3">Ce conseil était-il bon?</p>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<button onClick={() => send('good')} className="flex-1 flex items-center justify-center gap-1.5 bg-green-700 hover:bg-green-600 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||
<ThumbsUp size={13} /> Bon
|
||||
</button>
|
||||
<button onClick={() => send('neutral')} className="flex-1 flex items-center justify-center gap-1.5 bg-gray-700 hover:bg-gray-600 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||
<Minus size={13} /> Neutre
|
||||
</button>
|
||||
<button onClick={() => send('bad')} className="flex-1 flex items-center justify-center gap-1.5 bg-red-700 hover:bg-red-600 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||
<ThumbsDown size={13} /> Mauvais
|
||||
</button>
|
||||
</div>
|
||||
<input value={better} onChange={e => setBetter(e.target.value)}
|
||||
placeholder="Meilleure action? (buy/sell/freeze/upgrade/wait...)"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-3 py-1.5 text-xs text-white mb-2 focus:outline-none focus:border-yellow-600" />
|
||||
<input value={comment} onChange={e => setComment(e.target.value)}
|
||||
placeholder="Commentaire (optionnel)"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none focus:border-yellow-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GamePage() {
|
||||
const [session, setSession] = useState(null)
|
||||
const [advice, setAdvice] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [screenshot, setScreenshot] = useState(null)
|
||||
const [state, setState] = useState({
|
||||
turn: 1, tavern_tier: 1, gold: 3, hero_hp: 40,
|
||||
board_minions: [], tavern_minions: [],
|
||||
freeze: false, can_upgrade: true, upgrade_cost: 5,
|
||||
current_placement: 5, player_count: 8, phase: 'recruit',
|
||||
})
|
||||
|
||||
const startSession = async () => {
|
||||
try {
|
||||
const r = await axios.post('/api/game/start')
|
||||
setSession(r.data)
|
||||
setAdvice(null)
|
||||
} catch { alert('Erreur démarrage session') }
|
||||
}
|
||||
|
||||
const endSession = async () => {
|
||||
const place = parseInt(prompt('Place finale (1-8)?', '4') || '4')
|
||||
if (!place) return
|
||||
try {
|
||||
await axios.post(`/api/game/${session.session_id}/end?final_place=${place}`)
|
||||
setSession(null); setAdvice(null)
|
||||
} catch { alert('Erreur fin de session') }
|
||||
}
|
||||
|
||||
const getAdvice = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const r = await axios.post('/api/advice/', { ...state, session_id: session?.session_id })
|
||||
setAdvice(r.data)
|
||||
} catch { alert('Service IA non disponible — vérifiez que le backend tourne sur :8000') }
|
||||
finally { setLoading(false) }
|
||||
}, [state, session])
|
||||
|
||||
const captureScreen = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const r = await axios.get('/api/advice/from-screen')
|
||||
setAdvice(r.data.advice)
|
||||
if (r.data.screenshot) setScreenshot(r.data.screenshot)
|
||||
if (r.data.extracted_state?.gold) setState(s => ({ ...s, ...r.data.extracted_state }))
|
||||
} catch { alert('Capture non disponible — activez la vision dans les paramètres') }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const addMinion = (field) => {
|
||||
const name = prompt('Nom du serviteur?')
|
||||
if (!name) return
|
||||
const attack = parseInt(prompt('Attaque?', '2') || '2')
|
||||
const health = parseInt(prompt('Points de vie?', '2') || '2')
|
||||
const race = prompt('Race? (mech/beast/murloc/demon/dragon/none)', 'none') || 'none'
|
||||
setState(s => ({
|
||||
...s,
|
||||
[field]: [...s[field], { name, attack, health, race: [race] }]
|
||||
}))
|
||||
}
|
||||
|
||||
const removeMinion = (field, idx) => {
|
||||
setState(s => ({ ...s, [field]: s[field].filter((_, i) => i !== idx) }))
|
||||
}
|
||||
|
||||
const setField = (key, val) => setState(s => ({ ...s, [key]: val }))
|
||||
|
||||
const mainAction = advice?.main_decision
|
||||
const actionInfo = ACTION_MAP[mainAction?.action] || { label: mainAction?.action, color: 'text-white' }
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-yellow-400">Partie en cours</h2>
|
||||
<p className="text-gray-500 text-sm">
|
||||
{session ? `Session #${session.session_id}` : 'Aucune session active'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{!session ? (
|
||||
<button onClick={startSession}
|
||||
className="flex items-center gap-2 bg-green-600 hover:bg-green-500 px-4 py-2 rounded-xl text-sm font-semibold transition-colors">
|
||||
<Play size={15} /> Démarrer
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={endSession}
|
||||
className="flex items-center gap-2 bg-red-600 hover:bg-red-500 px-4 py-2 rounded-xl text-sm font-semibold transition-colors">
|
||||
<Square size={15} /> Terminer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-5">
|
||||
{/* Colonne gauche: état du jeu */}
|
||||
<div className="xl:col-span-2 space-y-4">
|
||||
{/* Stats numériques */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-4">État du tour</h3>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
|
||||
{[
|
||||
['Tour', 'turn', 1, 50],
|
||||
['Tier', 'tavern_tier', 1, 6],
|
||||
['Or (g)', 'gold', 0, 20],
|
||||
['HP Héros', 'hero_hp', 1, 40],
|
||||
['Position', 'current_placement', 1, 8],
|
||||
['Coût up.', 'upgrade_cost', 0, 10],
|
||||
].map(([label, key, min, max]) => (
|
||||
<div key={key}>
|
||||
<label className="text-xs text-gray-500 block mb-1">{label}</label>
|
||||
<input
|
||||
type="number" min={min} max={max}
|
||||
value={state[key]}
|
||||
onChange={e => setField(key, parseInt(e.target.value) || 0)}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-2 py-1.5 text-sm text-white text-center focus:outline-none focus:border-yellow-600"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-6 mt-3 pt-3 border-t border-gray-800">
|
||||
{[['freeze', '❄️ Gel actif'], ['can_upgrade', '⬆️ Peut monter']].map(([key, label]) => (
|
||||
<label key={key} className="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" checked={state[key]}
|
||||
onChange={e => setField(key, e.target.checked)}
|
||||
className="w-4 h-4 accent-yellow-500" />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Board */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-300">
|
||||
Board <span className="text-gray-500">({state.board_minions.length}/7)</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-7 gap-2">
|
||||
{state.board_minions.map((m, i) => (
|
||||
<MinionChip key={i} minion={m} onRemove={() => removeMinion('board_minions', i)} />
|
||||
))}
|
||||
{state.board_minions.length < 7 && (
|
||||
<AddMinionButton onClick={() => addMinion('board_minions')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Taverne */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-300">
|
||||
Taverne <span className="text-gray-500">({state.tavern_minions.length})</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-7 gap-2">
|
||||
{state.tavern_minions.map((m, i) => (
|
||||
<MinionChip key={i} minion={m} onRemove={() => removeMinion('tavern_minions', i)} />
|
||||
))}
|
||||
{state.tavern_minions.length < 7 && (
|
||||
<AddMinionButton onClick={() => addMinion('tavern_minions')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<button onClick={getAdvice} disabled={loading}
|
||||
className="flex-1 flex items-center justify-center gap-2 bg-yellow-600 hover:bg-yellow-500 disabled:opacity-50 disabled:cursor-not-allowed px-4 py-3 rounded-xl font-semibold transition-colors">
|
||||
{loading
|
||||
? <><RefreshCw size={16} className="animate-spin" /> Analyse...</>
|
||||
: <><Swords size={16} /> Obtenir conseil</>
|
||||
}
|
||||
</button>
|
||||
<button onClick={captureScreen} disabled={loading}
|
||||
title="Capturer l'écran automatiquement"
|
||||
className="flex items-center gap-2 bg-blue-700 hover:bg-blue-600 disabled:opacity-50 px-4 py-3 rounded-xl text-sm font-medium transition-colors">
|
||||
<Monitor size={16} /> Capturer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colonne droite: conseils */}
|
||||
<div className="space-y-4">
|
||||
{advice ? (
|
||||
<>
|
||||
{/* Conseil principal */}
|
||||
<div className="bg-gray-900 border border-yellow-800/40 rounded-xl p-5 card-glow">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs text-gray-400 uppercase tracking-wider">Conseil principal</span>
|
||||
<span className="text-xs text-gray-600 bg-gray-800 px-2 py-0.5 rounded">{advice.model_used}</span>
|
||||
</div>
|
||||
<p className={`text-3xl font-bold mt-2 ${actionInfo.color}`}>
|
||||
{actionInfo.label}
|
||||
</p>
|
||||
{mainAction?.target && (
|
||||
<p className="text-yellow-300 text-sm mt-1">
|
||||
→ {mainAction.target?.name || JSON.stringify(mainAction.target)}
|
||||
</p>
|
||||
)}
|
||||
<ConfBar value={mainAction?.confidence} />
|
||||
<p className="text-gray-200 text-sm mt-3 leading-relaxed">{mainAction?.reasoning}</p>
|
||||
{mainAction?.warnings?.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{mainAction.warnings.map((w, i) => (
|
||||
<p key={i} className="text-yellow-400 text-xs">⚠️ {w}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{mainAction?.synergies?.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{mainAction.synergies.map(s => (
|
||||
<span key={s} className="text-xs bg-blue-900/40 text-blue-400 border border-blue-800/30 px-2 py-0.5 rounded">
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Analyse du board */}
|
||||
{advice.board_analysis && (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider mb-2">Analyse</p>
|
||||
<p className="text-gray-200 text-sm leading-relaxed">{advice.board_analysis}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stratégie LLM */}
|
||||
{advice.strategy_long_term && (
|
||||
<div className="bg-gray-900 border border-blue-800/30 rounded-xl p-4">
|
||||
<p className="text-xs text-blue-400 uppercase tracking-wider mb-2">Stratégie</p>
|
||||
<p className="text-gray-200 text-sm">{advice.strategy_long_term}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Menaces */}
|
||||
{advice.threat_assessment && (
|
||||
<div className="bg-gray-900 border border-red-900/30 rounded-xl p-4">
|
||||
<p className="text-xs text-red-400 uppercase tracking-wider mb-2">Menaces</p>
|
||||
<p className="text-gray-200 text-sm">{advice.threat_assessment}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alternatives */}
|
||||
{advice.secondary_decisions?.length > 0 && (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider mb-2">Alternatives</p>
|
||||
<div className="space-y-1">
|
||||
{advice.secondary_decisions.slice(0, 4).map((d, i) => {
|
||||
const info = ACTION_MAP[d.action] || { label: d.action, color: 'text-gray-400' }
|
||||
return (
|
||||
<div key={i} className="flex items-center justify-between py-1.5 border-b border-gray-800 last:border-0">
|
||||
<span className={`text-sm font-medium ${info.color}`}>{info.label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-500 text-xs truncate max-w-[120px]">{d.reasoning?.slice(0, 40)}...</span>
|
||||
<span className="text-gray-600 text-xs">{Math.round(d.confidence * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Feedback */}
|
||||
{advice.decision_id && (
|
||||
<FeedbackBar decisionId={advice.decision_id} />
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<p className="text-gray-700 text-xs text-center">{advice.processing_ms}ms de traitement</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-8 text-center">
|
||||
<Swords size={40} className="mx-auto mb-4 text-gray-700" />
|
||||
<p className="text-gray-400 font-medium mb-2">Prêt à analyser</p>
|
||||
<p className="text-gray-600 text-sm">Renseignez l'état du jeu puis cliquez sur "Obtenir conseil"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screenshot preview */}
|
||||
{screenshot && (
|
||||
<div className="mt-4 bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<p className="text-sm text-gray-400">Capture d'écran</p>
|
||||
<button onClick={() => setScreenshot(null)} className="text-gray-600 hover:text-gray-400">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<img src={`data:image/png;base64,${screenshot}`} className="max-h-48 rounded-lg" alt="capture" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
163
hsbg_ai/frontend/src/pages/LearningPage.jsx
Normal file
163
hsbg_ai/frontend/src/pages/LearningPage.jsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Brain, ThumbsUp, ThumbsDown, Minus, RefreshCw, BarChart2 } from 'lucide-react'
|
||||
|
||||
const RATING_ICONS = {
|
||||
1: <ThumbsUp size={13} className="text-green-400" />,
|
||||
0: <Minus size={13} className="text-gray-400" />,
|
||||
'-1': <ThumbsDown size={13} className="text-red-400" />,
|
||||
}
|
||||
const ACTION_COLORS = {
|
||||
buy: 'text-green-400', sell: 'text-red-400', freeze: 'text-blue-400',
|
||||
upgrade: 'text-yellow-400', reposition: 'text-purple-400', wait: 'text-gray-400',
|
||||
}
|
||||
|
||||
export default function LearningPage() {
|
||||
const [stats, setStats] = useState(null)
|
||||
const [decisions, setDecisions] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [s, d] = await Promise.all([
|
||||
axios.get('/api/learning/stats'),
|
||||
axios.get('/api/learning/decisions?limit=30'),
|
||||
])
|
||||
setStats(s.data)
|
||||
setDecisions(d.data)
|
||||
} catch {}
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const sendFeedback = async (id, rating) => {
|
||||
try {
|
||||
await axios.post('/api/learning/feedback', { decision_id: id, rating })
|
||||
load()
|
||||
} catch { alert('Erreur envoi feedback') }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Brain size={24} className="text-purple-400" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Mode Apprentissage</h2>
|
||||
<p className="text-gray-400 text-sm">Évaluez les décisions IA pour améliorer le modèle</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={load} className="flex items-center gap-1.5 text-gray-400 hover:text-white text-sm transition-colors">
|
||||
<RefreshCw size={14} /> Actualiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{[
|
||||
['Feedbacks total', stats.total, 'text-white'],
|
||||
[`Bons (${stats.good_rate}%)`, stats.good, 'text-green-400'],
|
||||
['Mauvais', stats.bad, 'text-red-400'],
|
||||
['Entraînements', stats.trained, 'text-blue-400'],
|
||||
].map(([label, val, color]) => (
|
||||
<div key={label} className="bg-gray-900 border border-gray-800 rounded-xl p-4 text-center">
|
||||
<p className={`text-2xl font-bold ${color}`}>{val}</p>
|
||||
<p className="text-gray-500 text-xs mt-1">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Barre de progression good rate */}
|
||||
{stats && stats.total > 0 && (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 mb-6">
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-gray-300 flex items-center gap-1.5"><BarChart2 size={14} /> Qualité des conseils</span>
|
||||
<span className="text-gray-400">{stats.good_rate}% de réussite</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-green-600 to-green-400 rounded-full transition-all"
|
||||
style={{ width: `${stats.good_rate}%` }} />
|
||||
</div>
|
||||
{stats.buffer_pending > 0 && (
|
||||
<p className="text-yellow-400 text-xs mt-2">⚡ {stats.buffer_pending} feedbacks en attente d'export</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Historique */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-800">
|
||||
<h3 className="font-semibold text-white">Historique des décisions</h3>
|
||||
<p className="text-gray-500 text-xs mt-0.5">Cliquez 👍/👎 pour noter les conseils non évalués</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-400 text-center py-8">Chargement...</p>
|
||||
) : decisions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Brain size={32} className="mx-auto mb-3 text-gray-700" />
|
||||
<p className="text-gray-500">Aucune décision enregistrée</p>
|
||||
<p className="text-gray-600 text-sm mt-1">Lancez une partie et demandez des conseils!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-800">
|
||||
{decisions.map(d => {
|
||||
const action = d.recommendation?.main_decision?.action
|
||||
return (
|
||||
<div key={d.id} className="px-4 py-3 hover:bg-gray-800/30 transition-colors">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-semibold text-sm ${ACTION_COLORS[action] || 'text-white'}`}>
|
||||
{action?.toUpperCase() || '?'}
|
||||
</span>
|
||||
<span className="text-gray-600 text-xs">Tour {d.turn}</span>
|
||||
<span className="text-gray-700 text-xs">{d.phase}</span>
|
||||
<span className="text-gray-700 text-xs bg-gray-800 px-1.5 py-0.5 rounded">{d.model_used}</span>
|
||||
<span className="text-gray-700 text-xs">{d.processing_ms}ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-2 shrink-0">
|
||||
{d.outcome_rating !== null && d.outcome_rating !== undefined
|
||||
? RATING_ICONS[String(d.outcome_rating)]
|
||||
: (
|
||||
<>
|
||||
<button onClick={() => sendFeedback(d.id, 'good')}
|
||||
className="p-1 hover:text-green-400 text-gray-600 transition-colors" title="Bon conseil">
|
||||
<ThumbsUp size={13} />
|
||||
</button>
|
||||
<button onClick={() => sendFeedback(d.id, 'neutral')}
|
||||
className="p-1 hover:text-gray-300 text-gray-600 transition-colors" title="Neutre">
|
||||
<Minus size={13} />
|
||||
</button>
|
||||
<button onClick={() => sendFeedback(d.id, 'bad')}
|
||||
className="p-1 hover:text-red-400 text-gray-600 transition-colors" title="Mauvais conseil">
|
||||
<ThumbsDown size={13} />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-300 text-xs leading-relaxed">{d.reasoning}</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<div className="flex-1 h-1 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-600 rounded-full"
|
||||
style={{ width: `${Math.round(d.confidence * 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-gray-600 text-xs">{Math.round(d.confidence * 100)}%</span>
|
||||
</div>
|
||||
{d.user_feedback && (
|
||||
<p className="text-gray-500 text-xs mt-1 italic">💬 {d.user_feedback}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
114
hsbg_ai/frontend/src/pages/SettingsPage.jsx
Normal file
114
hsbg_ai/frontend/src/pages/SettingsPage.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Settings, Cpu, Eye, Brain, AlertCircle } from 'lucide-react'
|
||||
|
||||
function Section({ title, icon: Icon, children }) {
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b border-gray-800">
|
||||
<Icon size={16} className="text-gray-400" />
|
||||
<h3 className="font-semibold text-white">{title}</h3>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value, mono = false, highlight = false }) {
|
||||
return (
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-800 last:border-0">
|
||||
<span className="text-gray-400 text-sm">{label}</span>
|
||||
<span className={`text-sm font-medium max-w-[60%] text-right truncate
|
||||
${mono ? 'font-mono text-yellow-300' : highlight ? 'text-green-400' : 'text-white'}`}>
|
||||
{String(value)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [cfg, setCfg] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
axios.get('/api/settings/').then(r => setCfg(r.data)).catch(() => {})
|
||||
}, [])
|
||||
|
||||
if (!cfg) return <div className="p-6 text-gray-400">Chargement...</div>
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-3xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings size={24} className="text-gray-400" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Paramètres</h2>
|
||||
<p className="text-gray-400 text-sm">Configuration de l'IA et des services</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-900/20 border border-yellow-700/40 rounded-xl p-4 mb-6 flex gap-3">
|
||||
<AlertCircle size={16} className="text-yellow-400 shrink-0 mt-0.5" />
|
||||
<p className="text-yellow-300 text-sm">
|
||||
Pour modifier ces paramètres, éditez <code className="bg-gray-800 px-1 rounded">.env</code> puis redémarrez le backend avec <code className="bg-gray-800 px-1 rounded">bash start_backend.sh</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Section title="LLM Local (Ollama)" icon={Cpu}>
|
||||
<Row label="Fournisseur" value={cfg.llm_provider} mono />
|
||||
<Row label="Modèle actif" value={cfg.llm_model} mono />
|
||||
<Row label="URL Ollama" value={cfg.llm_base_url} mono />
|
||||
<Row label="Température" value={cfg.llm_temperature} />
|
||||
<Row label="Tokens max" value={cfg.llm_max_tokens} />
|
||||
</Section>
|
||||
|
||||
<Section title="Vision & OCR" icon={Eye}>
|
||||
<Row label="Activée" value={cfg.vision_enabled ? '✅ Oui' : '❌ Non'}
|
||||
highlight={cfg.vision_enabled} />
|
||||
<Row label="Intervalle de capture" value={`${cfg.screenshot_interval}s`} />
|
||||
</Section>
|
||||
|
||||
<Section title="Apprentissage" icon={Brain}>
|
||||
<Row label="Activé" value={cfg.learning_enabled ? '✅ Oui' : '❌ Non'}
|
||||
highlight={cfg.learning_enabled} />
|
||||
<Row label="Taux d'apprentissage" value={cfg.learning_rate} />
|
||||
<Row label="Taille du batch" value={cfg.learning_batch_size} />
|
||||
</Section>
|
||||
|
||||
<Section title="Système" icon={Settings}>
|
||||
<Row label="Patch HSBG" value={cfg.current_patch} />
|
||||
<Row label="Mode debug" value={cfg.debug ? 'Oui' : 'Non'} />
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
{/* Guides d'installation */}
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="bg-blue-900/20 border border-blue-800/40 rounded-xl p-5">
|
||||
<h4 className="font-semibold text-blue-300 mb-3">🧠 Installer Ollama (LLM local)</h4>
|
||||
<div className="space-y-2 font-mono text-xs">
|
||||
{[
|
||||
['# 1. Installer Ollama', ''],
|
||||
['curl -fsSL https://ollama.ai/install.sh | sh', 'text-yellow-300'],
|
||||
['# 2. Télécharger llama3.2 (4.7GB)', ''],
|
||||
['ollama pull llama3.2', 'text-yellow-300'],
|
||||
['# 3. Ou un modèle plus léger', ''],
|
||||
['ollama pull mistral:7b', 'text-yellow-300'],
|
||||
].map(([cmd, color], i) => (
|
||||
<p key={i} className={`${color || 'text-gray-500'}`}>{cmd}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-900/20 border border-green-800/40 rounded-xl p-5">
|
||||
<h4 className="font-semibold text-green-300 mb-3">👁️ Activer la vision OCR</h4>
|
||||
<div className="space-y-2 font-mono text-xs">
|
||||
<p className="text-gray-500"># Installer Tesseract OCR</p>
|
||||
<p className="text-yellow-300">sudo apt-get install tesseract-ocr tesseract-ocr-fra</p>
|
||||
<p className="text-gray-500"># Puis dans .env:</p>
|
||||
<p className="text-yellow-300">VISION_ENABLED=true</p>
|
||||
<p className="text-yellow-300">TESSERACT_PATH=/usr/bin/tesseract</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user