klg-asutk-app/components/AIAssistant.tsx
Yuriy 48d80137ac feat: demo data, AI assistant, UI fixes for presentation
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 23:33:49 +03:00

145 lines
4.9 KiB
TypeScript
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.

'use client';
import { useState, useRef, useEffect } from 'react';
import { getAuthToken } from '@/lib/api/api-client';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export default function AIAssistant() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content:
'Здравствуйте! Я AI-помощник REFLY. Спросите меня о лётной годности, ТО, директивах или рисках.',
},
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || loading) return;
const userMsg = input.trim();
setInput('');
setMessages((prev) => [...prev, { role: 'user', content: userMsg }]);
setLoading(true);
try {
const token =
typeof window !== 'undefined'
? (getAuthToken() || document.cookie.match(/auth-token=([^;]+)/)?.[1] || 'dev')
: 'dev';
const res = await fetch('/api/v1/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ message: userMsg }),
});
if (!res.ok) throw new Error('AI service unavailable');
const data = await res.json();
setMessages((prev) => [...prev, { role: 'assistant', content: data.reply }]);
} catch {
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: 'Извините, не удалось получить ответ. Попробуйте позже.',
},
]);
} finally {
setLoading(false);
}
};
return (
<>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-blue-600 text-white shadow-lg hover:bg-blue-700 transition-all flex items-center justify-center text-2xl"
title="AI Помощник"
aria-label={isOpen ? 'Закрыть чат' : 'Открыть AI Помощник'}
>
{isOpen ? '✕' : '🤖'}
</button>
{isOpen && (
<div
className="fixed bottom-24 right-6 z-50 w-96 max-w-[calc(100vw-3rem)] h-[500px] bg-white rounded-2xl shadow-2xl border border-gray-200 flex flex-col overflow-hidden"
role="dialog"
aria-label="Чат с AI-помощником"
>
<div className="bg-blue-600 text-white px-4 py-3 flex items-center gap-2">
<span className="text-xl" aria-hidden>🤖</span>
<div>
<div className="font-semibold text-sm">AI Помощник REFLY</div>
<div className="text-xs opacity-80">Контроль лётной годности</div>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-3">
{messages.map((msg, i) => (
<div
key={i}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] px-3 py-2 rounded-xl text-sm whitespace-pre-wrap ${
msg.role === 'user'
? 'bg-blue-600 text-white rounded-br-sm'
: 'bg-gray-100 text-gray-800 rounded-bl-sm'
}`}
>
{msg.content}
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="bg-gray-100 px-3 py-2 rounded-xl text-sm text-gray-500">
Думаю...
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="p-3 border-t flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Задайте вопрос..."
className="flex-1 px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
aria-label="Сообщение"
/>
<button
type="button"
onClick={sendMessage}
disabled={loading || !input.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50"
aria-label="Отправить"
>
</button>
</div>
</div>
)}
</>
);
}