import React, { useState, useEffect, useRef } from 'react'; import { CurrentView } from '../types'; import { NAVIGABLE_VIEWS } from '../constants'; import { ArrowUpIcon } from './icons'; import { STRAPI_URL } from '../strapiService'; const PLACEHOLDERS = [ "Создайте мне сайт для моего бизнеса", "Какие у вас есть вакансии?", "Расскажите о ваших услугах для бизнеса", "Мне нужно мобильное приложение", "Что такое ваш ИИ-акселератор?", "Я хочу научиться ИИ", "Покажите ваши последние исследования" ]; interface ChatInputProps { setCurrentView: (view: CurrentView) => void; isSticky?: boolean; autoFocusOnMount?: boolean; } const ChatInput: React.FC = ({ setCurrentView, isSticky = false, autoFocusOnMount = false }) => { const [prompt, setPrompt] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentPlaceholderIndex, setCurrentPlaceholderIndex] = useState(0); const textareaRef = useRef(null); useEffect(() => { const interval = setInterval(() => { setCurrentPlaceholderIndex((prevIndex) => (prevIndex + 1) % PLACEHOLDERS.length); }, 5000); // Change placeholder every 5 seconds return () => clearInterval(interval); }, []); useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, [prompt]); useEffect(() => { if (autoFocusOnMount && textareaRef.current) { // Small timeout to ensure the element is fully visible and transitions are complete before focusing const timer = setTimeout(() => { textareaRef.current?.focus(); }, 100); return () => clearTimeout(timer); } }, [autoFocusOnMount]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!prompt.trim() || isLoading) return; setIsLoading(true); setError(null); const validViewsString = NAVIGABLE_VIEWS.join(', '); const systemInstruction = `You are an expert navigation assistant for a website. Your task is to analyze the user's query and determine which section of the website they want to visit. Respond ONLY with a JSON object containing a single key "view". The possible values for "view" are: ${validViewsString}. Here are some examples of user queries and your expected JSON response: - "I want to see your work" -> {"view": "storiesAll"} - "Do you have any jobs?" -> {"view": "careers"} - "What services do you offer for businesses?" -> {"view": "businessServices"} - "I need a website" or "I need a program" -> {"view": "businessServices"} - "Tell me about your company" -> {"view": "about"} - "How can I contact you?" -> {"view": "main"} - "What AI research do you do?" -> {"view": "researchAll"} - "I want to learn AI" -> {"view": "educationStudents"} - "I have an idea" or "I have a project" -> {"view": "acceleratorAbout"} - "Do you have an accelerator?" -> {"view": "acceleratorAbout"} If the user's query is about having an idea or a project they want to develop, direct them to the accelerator. If they explicitly want to order a service like a website or an app, direct them to business services. If the user's query is unclear or doesn't map to a specific view, respond with {"view": "main"}. Your entire response must be only the JSON object, with no other text, explanation, or markdown formatting.`; try { // Запрос через прокси Strapi (/api/ollama/chat) — один origin с фронтом, нет CORS и работает из любой сети const response = await fetch(`${STRAPI_URL}/api/ollama/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gemma3n:e4b', messages: [ { role: 'system', content: systemInstruction }, { role: 'user', content: prompt } ], stream: false, }), }); if (!response.ok) { throw new Error(`Ошибка сети: ${response.status} ${response.statusText}. Убедитесь, что бэкенд и Ollama доступны.`); } const data = await response.json(); if (data.error) { throw new Error(`Сервер ИИ вернул ошибку: ${typeof data.error === 'string' ? data.error : data.error.message || 'неизвестная ошибка'}`); } // Ответ Ollama в /api/chat (без стриминга): // { message: { role: 'assistant', content: '...' }, ... } const responseJsonString = data.message?.content; if (!responseJsonString) { throw new Error("Сервер ИИ вернул пустой ответ."); } // More robust JSON extraction let parsedData = null; const jsonMatch = responseJsonString.match(/\{[\s\S]*\}/); if (jsonMatch) { try { parsedData = JSON.parse(jsonMatch[0]); } catch (e) { // JSON might be invalid, fall through to error console.error("Failed to parse JSON from response:", e); } } if (parsedData && parsedData.view && NAVIGABLE_VIEWS.includes(parsedData.view as any)) { setCurrentView(parsedData.view); } else { setError("К сожалению, я не смог понять ваш запрос. Попробуйте переформулировать."); } } catch (err: any) { console.error("Ошибка при обращении к серверу ИИ (Ollama):", err); setError(err.message || "Произошла ошибка. Убедитесь, что бэкенд и сервер Ollama доступны. Пожалуйста, попробуйте еще раз."); } finally { setIsLoading(false); } }; const wrapperClasses = isSticky ? "" : "relative z-10 mx-auto w-full max-w-3xl"; const baseLabelClasses = "relative flex w-full cursor-text flex-col overflow-hidden rounded-3xl py-3 pl-4 pr-14 transition-all duration-300"; const nonStickyLabelClasses = "border border-slate-200 bg-white shadow-sm focus-within:shadow-md"; const stickyLabelClasses = "border border-slate-300 bg-white/80 shadow-lg backdrop-blur-xl focus-within:shadow-xl"; const labelClasses = `${baseLabelClasses} ${isSticky ? stickyLabelClasses : nonStickyLabelClasses}`; return (