145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
'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>
|
||
)}
|
||
</>
|
||
);
|
||
}
|