klg-asutk-app/components/AIAgentModal.tsx
Yuriy 0150aba4f5 Consolidation: KLG ASUTK + PAPA integration
- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites)
- Remove localhost refs: openapi, inbox page
- Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI
- Add stub routes: knowledge/insights, recommendations, search, log-error
- Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests
- Fix inbox-server: ORDER BY created_at, package.json
- Remove redundant app/api/inbox/files route (rewrites handle it)
- knowledge/ in gitignore (large PDFs)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:18:31 +03:00

427 lines
14 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';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
files?: File[];
}
interface AIAgentModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function AIAgentModal({ isOpen, onClose }: AIAgentModalProps) {
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
role: 'assistant',
content: 'Здравствуйте! Я ИИ агент системы контроля лётной годности. Чем могу помочь? Я могу помочь с анализом документов, внесением данных в базу, поиском информации и другими задачами.',
timestamp: new Date(),
},
]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
if (!isOpen) {
return null;
}
const handleSend = async () => {
if (!inputValue.trim() && attachedFiles.length === 0) {
return;
}
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
files: attachedFiles.length > 0 ? [...attachedFiles] : undefined,
};
setMessages(prev => [...prev, userMessage]);
const currentInput = inputValue;
const currentFiles = attachedFiles;
setInputValue('');
setAttachedFiles([]);
setIsLoading(true);
try {
// Если есть файлы, отправляем их через FormData
let response: Response;
if (currentFiles.length > 0) {
const formData = new FormData();
formData.append('message', currentInput);
formData.append('history', JSON.stringify(messages.map(m => ({
role: m.role,
content: m.content,
}))));
// Добавляем файлы
currentFiles.forEach((file, index) => {
formData.append(`file_${index}`, file);
});
formData.append('fileCount', currentFiles.length.toString());
response = await fetch('/api/ai-chat', {
method: 'POST',
body: formData,
});
} else {
// Обычный запрос без файлов
response = await fetch('/api/ai-chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: currentInput,
history: messages.map(m => ({
role: m.role,
content: m.content,
})),
}),
});
}
const data = await response.json();
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: data.response || 'Извините, произошла ошибка при обработке запроса.',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Ошибка при запросе к AI:', error);
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Извините, произошла ошибка при подключении к ИИ агенту. Попробуйте позже.',
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const newFiles = Array.from(event.target.files);
// Фильтруем только разрешенные типы файлов
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
const allowedExtensions = ['.pdf', '.jpeg', '.jpg', '.png', '.xls', '.xlsx', '.csv', '.txt', '.doc', '.docx'];
const validFiles = newFiles.filter(file => {
const extension = '.' + file.name.split('.').pop()?.toLowerCase();
return allowedTypes.includes(file.type) || allowedExtensions.includes(extension);
});
if (validFiles.length !== newFiles.length) {
alert('Некоторые файлы не поддерживаются. Разрешены: PDF, JPEG, PNG, XLS, XLSX, CSV, TXT, DOC, DOCX');
}
setAttachedFiles(prev => [...prev, ...validFiles]);
}
};
const handleRemoveFile = (index: number) => {
setAttachedFiles(prev => prev.filter((_, i) => i !== index));
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
width: '90%',
maxWidth: '900px',
height: '90vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div style={{
padding: '20px',
borderBottom: '1px solid #e0e0e0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<div>
<h2 style={{ fontSize: '20px', fontWeight: 'bold', margin: 0 }}>
ИИ Агент
</h2>
<p style={{ fontSize: '12px', color: '#666', margin: '4px 0 0 0' }}>
Помощник по управлению системой контроля лётной годности
</p>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
{/* Messages */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '20px',
backgroundColor: '#f9f9f9',
}}>
{messages.map((message) => (
<div
key={message.id}
style={{
marginBottom: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: message.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
<div style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: '12px',
backgroundColor: message.role === 'user' ? '#1e3a5f' : 'white',
color: message.role === 'user' ? 'white' : '#333',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}>
{message.files && message.files.length > 0 && (
<div style={{ marginBottom: '8px', paddingBottom: '8px', borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
{message.files.map((file, idx) => (
<div key={idx} style={{ fontSize: '12px', opacity: 0.9 }}>
📎 {file.name} ({(file.size / 1024).toFixed(1)} KB)
</div>
))}
</div>
)}
<div style={{ whiteSpace: 'pre-wrap', fontSize: '14px', lineHeight: '1.6' }}>
{message.content}
</div>
<div style={{
fontSize: '10px',
opacity: 0.7,
marginTop: '8px',
}}>
{message.timestamp.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
))}
{isLoading && (
<div style={{
display: 'flex',
alignItems: 'flex-start',
marginBottom: '20px',
}}>
<div style={{
padding: '12px 16px',
borderRadius: '12px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}>
<div style={{ display: 'flex', gap: '4px' }}>
<span style={{ animation: 'blink 1s infinite' }}></span>
<span style={{ animation: 'blink 1s infinite 0.2s' }}></span>
<span style={{ animation: 'blink 1s infinite 0.4s' }}></span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Attached Files */}
{attachedFiles.length > 0 && (
<div style={{
padding: '12px 20px',
borderTop: '1px solid #e0e0e0',
backgroundColor: '#f5f5f5',
display: 'flex',
gap: '8px',
flexWrap: 'wrap',
}}>
{attachedFiles.map((file, index) => (
<div
key={index}
style={{
padding: '6px 12px',
backgroundColor: 'white',
borderRadius: '4px',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '8px',
border: '1px solid #e0e0e0',
}}
>
<span>📎</span>
<span>{file.name}</span>
<button
onClick={() => handleRemoveFile(index)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
color: '#f44336',
fontSize: '16px',
padding: '0',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
))}
</div>
)}
{/* Input */}
<div style={{
padding: '20px',
borderTop: '1px solid #e0e0e0',
backgroundColor: 'white',
}}>
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-end' }}>
<button
onClick={() => fileInputRef.current?.click()}
style={{
padding: '10px',
backgroundColor: '#f5f5f5',
border: '1px solid #e0e0e0',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '18px',
minWidth: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title="Прикрепить файл (PDF, JPEG, PNG, XLS, CSV, TXT)"
>
📎
</button>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
multiple
accept=".pdf,.jpeg,.jpg,.png,.xls,.xlsx,.csv,.txt,.doc,.docx"
style={{ display: 'none' }}
/>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Введите сообщение... (Enter для отправки, Shift+Enter для новой строки)"
style={{
flex: 1,
padding: '12px',
border: '1px solid #e0e0e0',
borderRadius: '4px',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'none',
minHeight: '40px',
maxHeight: '120px',
}}
rows={1}
/>
<button
onClick={handleSend}
disabled={isLoading || (!inputValue.trim() && attachedFiles.length === 0)}
style={{
padding: '10px 20px',
backgroundColor: isLoading || (!inputValue.trim() && attachedFiles.length === 0) ? '#ccc' : '#1e3a5f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading || (!inputValue.trim() && attachedFiles.length === 0) ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '500',
minWidth: '100px',
height: '40px',
}}
>
{isLoading ? 'Отправка...' : 'Отправить'}
</button>
</div>
</div>
</div>
</div>
);
}