edtech / apps /admin /src /pages /BillingPage.tsx
CognxSafeTrack
fix: restore Limite Quotidienne and complete admin i18n
4f90920
import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Brain, MessageCircle, TrendingUp, Send, Loader2, AlertCircle, Zap, X, Flame, HelpCircle, Download } from 'lucide-react';
import { api } from '@/lib/api';
import { useAuth } from '@/lib/auth';
import { useTenant } from '@/lib/tenant';
const SUPPORT_WA_NUMBER = import.meta.env.VITE_SUPPORT_WA_NUMBER as string | undefined;
const SUPPORT_WA_URL = SUPPORT_WA_NUMBER
? `https://wa.me/${SUPPORT_WA_NUMBER}?text=Bonjour%2C%20je%20souhaite%20recharger%20mes%20cr%C3%A9dits%20Xaml%C3%A9.`
: null;
function RechargeModal({ onClose }: { onClose: () => void }) {
const { t } = useTranslation();
const CREDIT_PACKS = [
{ labelKey: 'billing.pack_500_label', priceKey: 'billing.pack_500_price', credits: 500, popular: false },
{ labelKey: 'billing.pack_2000_label', priceKey: 'billing.pack_2000_price', credits: 2000, popular: true },
{ labelKey: 'billing.pack_5000_label', priceKey: 'billing.pack_5000_price', credits: 5000, popular: false },
];
useEffect(() => {
document.body.style.overflow = 'hidden';
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
window.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', onKey);
};
}, [onClose]);
return (
<div
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-end sm:items-center justify-center sm:p-4"
onClick={onClose}
>
<div
className="bg-white w-full sm:max-w-md rounded-t-3xl sm:rounded-2xl shadow-2xl flex flex-col max-h-[90vh]"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between px-6 py-5 border-b border-slate-100 shrink-0">
<div>
<h2 className="text-lg font-bold text-slate-900">{t('billing.modal_title')}</h2>
<p className="text-xs text-slate-400 mt-0.5">{t('billing.modal_rate')}</p>
</div>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="overflow-y-auto flex-1 px-6 py-5 space-y-3">
{CREDIT_PACKS.map(pack => (
<a
key={pack.credits}
href={SUPPORT_WA_URL ? `${SUPPORT_WA_URL}%20-%20Pack%20${t(pack.labelKey)}` : '#'}
target={SUPPORT_WA_URL ? '_blank' : undefined}
rel="noopener noreferrer"
className={`relative flex items-center justify-between p-4 border rounded-xl transition-all group ${pack.popular ? 'border-indigo-400 bg-indigo-50' : 'border-slate-200 hover:border-indigo-300 hover:bg-indigo-50'}`}
>
{pack.popular && (
<span className="absolute -top-2.5 left-4 px-2 py-0.5 bg-indigo-600 text-white text-xs font-bold rounded-full">
{t('billing.modal_popular')}
</span>
)}
<div>
<p className="font-semibold text-slate-800 group-hover:text-indigo-700">{t(pack.labelKey)}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('billing.modal_ai_messages', { count: pack.credits })}</p>
</div>
<span className="text-indigo-600 font-bold shrink-0 ml-4">{t(pack.priceKey)}</span>
</a>
))}
</div>
<div className="px-6 py-4 border-t border-slate-100 shrink-0">
<p className="text-xs text-slate-400 text-center">
{t('billing.modal_footer')}
</p>
</div>
</div>
</div>
);
}
interface WalletData {
walletBalance: number;
isHardStopped: boolean;
transactions: Array<{
id: string;
amount: number;
balanceAfter: number;
type: string;
description: string | null;
byok: boolean;
createdAt: string;
}>;
}
interface BillingSummary {
plan: string;
planLabel: string;
period: { start: string; end: string };
ai: {
creditsUsed: number;
creditsLimit: number;
percentUsed: number;
totalCalls: number;
costFcfa: number;
};
whatsapp: { messagesSent: number };
}
interface HistoryDay {
date: string;
aiCalls: number;
whatsappMessages: number;
costFcfa: number;
}
interface BreakdownItem {
feature: string;
calls: number;
}
interface ChatMessage {
role: 'user' | 'assistant';
text: string;
}
function DaysRemainingBadge({ walletBalance, transactions }: { walletBalance: number; transactions: WalletData['transactions'] }) {
const { t } = useTranslation();
const sevenDaysAgo = Date.now() - 7 * 86_400_000;
const weeklyDebit = transactions
.filter(tx => tx.amount < 0 && new Date(tx.createdAt).getTime() > sevenDaysAgo)
.reduce((sum, tx) => sum + Math.abs(tx.amount), 0);
if (weeklyDebit === 0) return null;
const dailyBurn = weeklyDebit / 7;
const days = Math.floor(walletBalance / dailyBurn);
const color = days <= 3 ? 'text-red-600 bg-red-50' : days <= 10 ? 'text-amber-600 bg-amber-50' : 'text-emerald-600 bg-emerald-50';
const icon = days <= 3 ? '🔴' : days <= 10 ? '🟡' : '🟢';
const label = days > 1
? t('billing.days_remaining_plural', { days })
: t('billing.days_remaining', { days });
return (
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold ${color}`}>
{icon} <strong>{label}</strong>
</span>
);
}
export default function BillingPage() {
const { t } = useTranslation();
const { token, user } = useAuth();
const { selectedOrgId } = useTenant();
const orgId = selectedOrgId;
const [summary, setSummary] = useState<BillingSummary | null>(null);
const [wallet, setWallet] = useState<WalletData | null>(null);
const [history, setHistory] = useState<HistoryDay[]>([]);
const [breakdown, setBreakdown] = useState<BreakdownItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showRecharge, setShowRecharge] = useState(false);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [chatInput, setChatInput] = useState('');
const [chatLoading, setChatLoading] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
const QUICK_QUESTIONS = [
t('billing.quick_q1'),
t('billing.quick_q2'),
t('billing.quick_q3'),
t('billing.quick_q4'),
];
const TX_LABELS: Record<string, string> = {
TOP_UP_MANUAL: t('billing.tx_TOP_UP_MANUAL'),
TOP_UP_PAYMENT: t('billing.tx_TOP_UP_PAYMENT'),
ADJUSTMENT: t('billing.tx_ADJUSTMENT'),
DEBIT_AI: t('billing.tx_DEBIT_AI'),
DEBIT_WHATSAPP: t('billing.tx_DEBIT_WHATSAPP'),
DEBIT_BROADCAST: t('billing.tx_DEBIT_BROADCAST'),
};
useEffect(() => {
if (!orgId || !token) return;
setLoading(true);
Promise.all([
api.get('/v1/billing/summary', token, orgId),
api.get('/v1/billing/history?days=30', token, orgId),
api.get('/v1/billing/breakdown', token, orgId),
api.get('/v1/billing/wallet', token, orgId),
]).then(([s, h, b, w]) => {
setSummary(s);
setHistory(h.history ?? []);
setBreakdown(b.breakdown ?? []);
setWallet(w);
}).catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [orgId, token]);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatMessages]);
const sendChat = async (question?: string) => {
const q = (question ?? chatInput).trim();
if (!q || chatLoading || !orgId || !token) return;
setChatInput('');
setChatMessages(prev => [...prev, { role: 'user', text: q }]);
setChatLoading(true);
try {
const res = await api.post('/v1/billing/chat', { question: q, language: user?.language ?? 'FR' }, token, orgId);
setChatMessages(prev => [...prev, { role: 'assistant', text: res.answer }]);
} catch {
setChatMessages(prev => [...prev, { role: 'assistant', text: t('billing.chat_error') }]);
} finally {
setChatLoading(false);
}
};
function downloadTransactionsCSV() {
if (!wallet?.transactions.length) return;
const rows = [
[
t('billing.csv_col_date'),
t('billing.csv_col_type'),
t('billing.csv_col_desc'),
t('billing.csv_col_amount'),
t('billing.csv_col_balance'),
],
...wallet.transactions.map(tx => [
new Date(tx.createdAt).toLocaleString(),
TX_LABELS[tx.type] ?? tx.type,
tx.description ?? '',
tx.amount.toString(),
tx.balanceAfter >= 0 ? tx.balanceAfter.toString() : '',
]),
];
const csv = rows.map(r => r.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n');
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `transactions-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
const maxCalls = Math.max(...history.map(d => d.aiCalls + d.whatsappMessages), 1);
if (!orgId) {
return (
<div className="flex items-center justify-center h-64 text-slate-400">
<p className="text-lg font-medium">{t('billing.select_org')}</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-indigo-500" />
</div>
);
}
if (error) {
return (
<div className="flex items-center gap-3 p-6 text-red-600 bg-red-50 rounded-xl m-6">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span>{t('billing.error_prefix')} {error}</span>
</div>
);
}
if (!summary) return null;
const periodLabel = new Date(summary.period.start).toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
const walletExhausted = wallet ? (wallet.isHardStopped || wallet.walletBalance <= 0) : false;
const walletLow = wallet ? (wallet.walletBalance > 0 && wallet.walletBalance <= 200) : false;
return (
<div className="p-6 max-w-5xl mx-auto space-y-6">
{showRecharge && <RechargeModal onClose={() => setShowRecharge(false)} />}
{/* Suspended service alert */}
{walletExhausted && (
<div className="flex items-center justify-between gap-4 px-5 py-4 bg-red-50 border border-red-200 rounded-2xl">
<div className="flex items-center gap-3 text-red-700">
<AlertCircle className="w-5 h-5 shrink-0" />
<div>
<p className="font-bold text-sm">{t('billing.alert_exhausted_title')}</p>
<p className="text-xs text-red-500 mt-0.5">{t('billing.alert_exhausted_desc')}</p>
</div>
</div>
<button
onClick={() => setShowRecharge(true)}
className="shrink-0 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-bold rounded-xl transition-colors"
>
{t('billing.alert_recharge_btn')}
</button>
</div>
)}
{/* Low balance alert */}
{!walletExhausted && walletLow && (
<div className="flex items-center justify-between gap-4 px-5 py-4 bg-amber-50 border border-amber-200 rounded-2xl">
<div className="flex items-center gap-3 text-amber-700">
<AlertCircle className="w-5 h-5 shrink-0" />
<div>
<p className="font-bold text-sm">{t('billing.alert_low_title', { count: wallet!.walletBalance })}</p>
<p className="text-xs text-amber-600 mt-0.5">{t('billing.alert_low_desc')}</p>
</div>
</div>
<button
onClick={() => setShowRecharge(true)}
className="shrink-0 px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white text-sm font-bold rounded-xl transition-colors"
>
{t('billing.alert_recharge_btn')}
</button>
</div>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">{t('billing.page_title')}</h1>
<p className="text-slate-500 text-sm mt-1">{t('billing.period_label')} {periodLabel}</p>
</div>
<button
onClick={() => setShowRecharge(true)}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-xl transition-colors shadow-sm"
>
<Zap className="w-4 h-4" />
{t('billing.recharge_btn')}
</button>
</div>
{/* Wallet card */}
{wallet && (
<div className={`rounded-2xl border p-6 shadow-sm ${walletExhausted ? 'border-red-200 bg-red-50' : walletLow ? 'border-amber-200 bg-amber-50' : 'bg-white border-slate-200'}`}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 ${walletExhausted ? 'bg-red-100' : walletLow ? 'bg-amber-100' : 'bg-emerald-100'}`}>
💳
</div>
<div>
<p className="text-sm font-medium text-slate-500">{t('billing.wallet_label')}</p>
<p className={`text-4xl font-black leading-tight ${walletExhausted ? 'text-red-600' : walletLow ? 'text-amber-600' : 'text-slate-900'}`}>
{wallet.walletBalance.toLocaleString()}
<span className="text-lg font-normal text-slate-400 ml-1.5">{t('billing.wallet_unit')}</span>
</p>
<p className="text-sm text-slate-400 mt-1">
{t('billing.wallet_fcfa', { amount: (wallet.walletBalance * 10).toLocaleString() })}
</p>
<div className="mt-2">
<DaysRemainingBadge walletBalance={wallet.walletBalance} transactions={wallet.transactions} />
</div>
</div>
</div>
<div className="flex flex-col items-start sm:items-end gap-2 shrink-0">
<button
onClick={() => setShowRecharge(true)}
className="px-5 py-2.5 bg-slate-900 hover:bg-slate-700 text-white rounded-xl font-bold text-sm transition-colors"
>
{t('billing.wallet_recharge_btn')}
</button>
<p className="text-xs text-slate-400">{t('billing.wallet_rate')}</p>
</div>
</div>
</div>
)}
{/* Metric cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* AI messages this month */}
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center">
<Brain className="w-5 h-5 text-indigo-600" />
</div>
<p className="text-sm font-semibold text-slate-600">{t('billing.ai_messages_title')}</p>
</div>
<p className="text-3xl font-black text-slate-900">
{summary.ai.totalCalls.toLocaleString()}
</p>
<p className="text-sm text-slate-400 mt-1">
{t('billing.ai_cost_label', { cost: summary.ai.costFcfa.toLocaleString() })}
</p>
{summary.ai.percentUsed > 85 && (
<div className="mt-3">
<div className="w-full bg-slate-100 rounded-full h-2 mb-1">
<div className="h-2 rounded-full bg-red-500" style={{ width: `${Math.min(summary.ai.percentUsed, 100)}%` }} />
</div>
<button
onClick={() => setShowRecharge(true)}
className="flex items-center gap-1.5 text-xs text-red-600 hover:underline mt-1"
>
<AlertCircle className="w-3.5 h-3.5" />
{t('billing.ai_quota_warning', { pct: summary.ai.percentUsed })}
</button>
</div>
)}
</div>
{/* WhatsApp messages this month */}
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center">
<MessageCircle className="w-5 h-5 text-emerald-600" />
</div>
<p className="text-sm font-semibold text-slate-600">{t('billing.wa_messages_title')}</p>
</div>
<p className="text-3xl font-black text-slate-900">
{summary.whatsapp.messagesSent.toLocaleString()}
</p>
<div className="mt-3 bg-emerald-50 rounded-xl px-3 py-2.5">
<p className="text-xs text-emerald-700">
{t('billing.wa_free_note')}
</p>
</div>
</div>
</div>
{/* 30-day activity chart */}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-slate-400" />
<h2 className="text-sm font-semibold text-slate-700">{t('billing.activity_title')}</h2>
</div>
<p className="text-xs text-slate-400 mb-4">{t('billing.activity_subtitle')}</p>
{history.length === 0 ? (
<p className="text-slate-400 text-sm text-center py-8">{t('billing.activity_empty')}</p>
) : (
<div className="flex items-end gap-1 h-28 overflow-x-auto pb-2">
{history.map(day => {
const total = day.aiCalls + day.whatsappMessages;
const heightPct = Math.round((total / maxCalls) * 100);
const dateLabel = new Date(day.date).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' });
return (
<div key={day.date} className="flex flex-col items-center gap-1 flex-1 min-w-[24px] group relative">
<div
className="w-full rounded-t-sm bg-indigo-300 group-hover:bg-indigo-500 transition-colors cursor-pointer"
style={{ height: `${Math.max(heightPct, 2)}%` }}
title={`${dateLabel} — IA: ${day.aiCalls} msg | WA: ${day.whatsappMessages} msg`}
/>
</div>
);
})}
</div>
)}
</div>
{/* Feature breakdown */}
{breakdown.length > 0 && (
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h2 className="text-sm font-semibold text-slate-700 mb-1">{t('billing.breakdown_title')}</h2>
<p className="text-xs text-slate-400 mb-4">{t('billing.breakdown_subtitle')}</p>
<div className="space-y-3">
{breakdown.slice(0, 5).map(item => {
const totalCalls = breakdown.reduce((s, i) => s + i.calls, 0);
const pct = totalCalls > 0 ? Math.round((item.calls / totalCalls) * 100) : 0;
const featureKey = `billing.feature_${item.feature}` as const;
return (
<div key={item.feature} className="flex items-center gap-3">
<span className="text-sm text-slate-600 w-48 flex-shrink-0 truncate">
{t(featureKey, { defaultValue: item.feature })}
</span>
<div className="flex-1 bg-slate-100 rounded-full h-2.5">
<div className="h-2.5 rounded-full bg-indigo-400" style={{ width: `${pct}%` }} />
</div>
<span className="text-sm font-semibold text-slate-700 w-10 text-right">{pct}%</span>
</div>
);
})}
</div>
</div>
)}
{/* Transaction history */}
{wallet && wallet.transactions.length > 0 && (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
<div>
<h2 className="text-sm font-semibold text-slate-700">{t('billing.transactions_title')}</h2>
<p className="text-xs text-slate-400 mt-0.5">{t('billing.transactions_subtitle')}</p>
</div>
<button
onClick={downloadTransactionsCSV}
className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 px-3 py-1.5 rounded-lg transition-colors border border-slate-200 hover:border-indigo-200"
>
<Download className="w-3.5 h-3.5" />
{t('billing.csv_btn')}
</button>
</div>
<div className="divide-y divide-slate-50">
{wallet.transactions.filter(tx => !tx.byok).map(tx => {
const isCredit = tx.amount > 0;
return (
<div key={tx.id} className="flex items-center justify-between px-6 py-3 hover:bg-slate-50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0 ${isCredit ? 'bg-emerald-100' : 'bg-slate-100'}`}>
{isCredit ? '➕' : '▪️'}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-slate-700 truncate">
{TX_LABELS[tx.type] ?? tx.type}
</p>
<p className="text-xs text-slate-400">
{new Date(tx.createdAt).toLocaleDateString(undefined, {
day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'
})}
</p>
</div>
</div>
<div className="text-right ml-4 flex-shrink-0">
<p className={`text-sm font-bold ${isCredit ? 'text-emerald-600' : 'text-slate-700'}`}>
{isCredit ? '+' : ''}{tx.amount.toLocaleString()} {t('billing.wallet_unit')}
</p>
<p className="text-xs text-slate-400">
{t('billing.balance_label')} {tx.balanceAfter >= 0 ? tx.balanceAfter.toLocaleString() : '—'}
</p>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Billing AI assistant */}
<div className="bg-white rounded-2xl border border-indigo-100 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 bg-gradient-to-r from-indigo-50 to-white">
<div className="flex items-center gap-2">
<HelpCircle className="w-4 h-4 text-indigo-500" />
<h2 className="text-sm font-bold text-slate-800">{t('billing.chat_title')}</h2>
</div>
<p className="text-xs text-slate-400 mt-0.5">{t('billing.chat_subtitle')}</p>
</div>
{/* Suggested questions */}
{chatMessages.length === 0 && (
<div className="px-4 pt-4 flex flex-wrap gap-2">
{QUICK_QUESTIONS.map(q => (
<button
key={q}
onClick={() => sendChat(q)}
disabled={chatLoading}
className="text-xs px-3 py-1.5 bg-slate-100 hover:bg-indigo-100 hover:text-indigo-700 text-slate-600 rounded-full transition-colors border border-transparent hover:border-indigo-200"
>
{q}
</button>
))}
</div>
)}
{/* Messages */}
<div className="p-4 space-y-3 max-h-72 overflow-y-auto">
{chatMessages.length === 0 && (
<p className="text-slate-300 text-sm text-center py-6">
<Flame className="w-8 h-8 mx-auto mb-2 text-slate-200" />
{t('billing.chat_empty')}
</p>
)}
{chatMessages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-xs lg:max-w-md px-4 py-2.5 rounded-2xl text-sm leading-relaxed whitespace-pre-wrap ${
msg.role === 'user'
? 'bg-indigo-600 text-white rounded-br-sm'
: 'bg-slate-100 text-slate-800 rounded-bl-sm'
}`}>
{msg.text}
</div>
</div>
))}
{chatLoading && (
<div className="flex justify-start">
<div className="bg-slate-100 px-4 py-3 rounded-2xl rounded-bl-sm flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
{/* Input */}
<div className="px-4 py-3 border-t border-slate-100 flex gap-2">
<input
type="text"
value={chatInput}
onChange={e => setChatInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && sendChat()}
placeholder={t('billing.chat_placeholder')}
className="flex-1 text-sm border border-slate-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-indigo-300 focus:border-indigo-300"
disabled={chatLoading}
/>
<button
onClick={() => sendChat()}
disabled={!chatInput.trim() || chatLoading}
className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-xl px-4 py-2.5 flex items-center gap-1 text-sm font-medium transition-colors"
>
<Send className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
}