Backend is here. Default admin is created if needed.

This commit is contained in:
aodulov
2025-11-19 10:48:37 +02:00
parent 10819cc6f5
commit bb705c8a63
25 changed files with 3662 additions and 944 deletions

View File

@@ -29,14 +29,14 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
useEffect(() => {
try {
const chat = createFitnessChat(history);
if (chat) {
chatSessionRef.current = chat;
} else {
setError(t('ai_error', lang));
}
const chat = createFitnessChat(history);
if (chat) {
chatSessionRef.current = chat;
} else {
setError(t('ai_error', lang));
}
} catch (err) {
setError("Failed to initialize AI");
setError("Failed to initialize AI");
}
}, [history, lang]);
@@ -59,28 +59,38 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
try {
const result: GenerateContentResponse = await chatSessionRef.current.sendMessage({ message: userMsg.text });
const text = result.text;
const aiMsg: Message = {
id: crypto.randomUUID(),
role: 'model',
text: text || "Error generating response."
const aiMsg: Message = {
id: crypto.randomUUID(),
role: 'model',
text: text || "Error generating response."
};
setMessages(prev => [...prev, aiMsg]);
} catch (err) {
console.error(err);
setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'model', text: 'Connection error.' }]);
let errorText = 'Connection error.';
if (err instanceof Error) {
try {
const json = JSON.parse(err.message);
if (json.error) errorText = json.error;
else errorText = err.message;
} catch {
errorText = err.message;
}
}
setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'model', text: errorText }]);
} finally {
setLoading(false);
}
};
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full p-6 text-center text-on-surface-variant">
<AlertTriangle size={48} className="text-error mb-4" />
<p>{error}</p>
</div>
)
return (
<div className="flex flex-col items-center justify-center h-full p-6 text-center text-on-surface-variant">
<AlertTriangle size={48} className="text-error mb-4" />
<p>{error}</p>
</div>
)
}
return (
@@ -88,7 +98,7 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
{/* Header */}
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10">
<div className="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center">
<Bot size={20} className="text-on-secondary-container" />
<Bot size={20} className="text-on-secondary-container" />
</div>
<h2 className="text-xl font-normal text-on-surface">{t('ai_expert', lang)}</h2>
</div>
@@ -97,22 +107,21 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-4">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-4 rounded-[20px] text-sm leading-relaxed shadow-sm ${
msg.role === 'user'
? 'bg-primary text-on-primary rounded-br-none'
<div className={`max-w-[85%] p-4 rounded-[20px] text-sm leading-relaxed shadow-sm ${msg.role === 'user'
? 'bg-primary text-on-primary rounded-br-none'
: 'bg-surface-container-high text-on-surface border border-outline-variant/20 rounded-bl-none'
}`}>
}`}>
{msg.text}
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="bg-surface-container-high px-4 py-3 rounded-[20px] rounded-bl-none flex gap-2 items-center text-on-surface-variant text-sm">
<Loader2 size={16} className="animate-spin" />
{t('ai_typing', lang)}
</div>
<div className="flex justify-start">
<div className="bg-surface-container-high px-4 py-3 rounded-[20px] rounded-bl-none flex gap-2 items-center text-on-surface-variant text-sm">
<Loader2 size={16} className="animate-spin" />
{t('ai_typing', lang)}
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
@@ -120,21 +129,21 @@ const AICoach: React.FC<AICoachProps> = ({ history, lang }) => {
{/* Input */}
<div className="p-4 bg-surface-container mt-auto">
<div className="flex gap-2 items-center bg-surface-container-high rounded-full border border-outline-variant px-2 py-1">
<input
type="text"
className="flex-1 bg-transparent border-none px-4 py-3 text-on-surface focus:outline-none placeholder-on-surface-variant"
placeholder={t('ai_placeholder', lang)}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button
onClick={handleSend}
disabled={loading || !input.trim()}
className="w-10 h-10 bg-primary rounded-full flex items-center justify-center text-on-primary hover:opacity-90 disabled:opacity-50 transition-opacity shrink-0"
>
<Send size={18} />
</button>
<input
type="text"
className="flex-1 bg-transparent border-none px-4 py-3 text-on-surface focus:outline-none placeholder-on-surface-variant"
placeholder={t('ai_placeholder', lang)}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button
onClick={handleSend}
disabled={loading || !input.trim()}
className="w-10 h-10 bg-primary rounded-full flex items-center justify-center text-on-primary hover:opacity-90 disabled:opacity-50 transition-opacity shrink-0"
>
<Send size={18} />
</button>
</div>
</div>
</div>