В этой статье мы поэтапно создадим современного помощника на основе искусственного интеллекта, который сможет обрабатывать текстовые запросы и генерировать изображения, используя возможности API OpenAI. Наш помощник будет иметь удобный интерфейс, похожий на современные чат-боты, такие как ChatGPT. Используемые технологии — React и Next.js.
Вы получите в свое распоряжение полноценного ИИ-ассистента, научитесь работать с использованием актуальных JavaScript-инструментов, интегрировать API и создавать адаптивный интерфейс.
gpu
Что понадобится для старта
Чтобы успешно выполнить этот проект, вам понадобятся:
- Node.js версии 18 или выше. Установите с официального сайта, следуя инструкциям для вашей операционной системы.
- Редактор кода, например VS Code.
- Ключ API от OpenAI. Чтобы ключ API работал, необходимо пополнить баланс аккаунта на специальной странице. Обратите внимание, что пополнение российскими или белорусскими картами в данный момент недоступно.
- Аккаунт на GitHub.
Шаг 1: Подготовка рабочей среды
Начнем с создания проекта на Next.js и установки всех необходимых инструментов.
Создание проекта
С помощью команды создайте новый проект на Next.js:
npx create-next-app@latest ai-assistant --typescript --tailwind --app
При создании приложения вам будут заданы вопросы. Выберите:
- Использовать ESLint? — Да.
- Использовать папку src/? — Да.
- Использовать Turbopack для next dev? — Да.
- Настроить псевдоним импорта (@/*)? — Нет.
Эти настройки создадут проект на основе TypeScript и Tailwind CSS с улучшенными условиями разработки благодаря Turbopack.
Установка зависимостей
В рамках данного проекта мы установим SDK OpenAI и необходимые библиотеки для работы с интерфейсом чата. Для начала необходимо открыть папку проекта:
cd ai-assistant
После этого необходимо установить библиотеки.
npm install openai react-markdown remark-gfm
openai
— клиент для работы с API OpenAI.react-markdown
иremark-gfm
— нужны для рендеринга отформатированных текстовых ответов.
Шаг 2: Настройка API для AI-запросов
Разработаем интерфейс для работы с OpenAI — он будет анализировать текстовые сообщения и генерировать изображения.
Создайте директорию и файл для API:
- Внутри папки
app
, которая находится вsrc
, создайте новую папкуapi
. - Внутри папки
api
создайте еще одну папку, под названиемchat
. - Внутри папки
chat
создайте файлroute.ts
.
Добавьте в src/app/api/chat/route.ts
следующий код:
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(request: NextRequest) {
try {
const { messages, mode, model = 'gpt-4o' } = await request.json();
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return NextResponse.json(
{ error: 'Неверный формат сообщений' },
{ status: 400 }
);
}
if (mode === 'chat') {
const completion = await openai.chat.completions.create({
model,
messages,
temperature: 0.7,
max_tokens: 1000,
});
return NextResponse.json({
content: completion.choices[0].message.content,
role: 'assistant',
});
} else if (mode === 'image') {
const lastMessage = messages[messages.length - 1];
const imageResponse = await openai.images.generate({
model: 'dall-e-3',
prompt: lastMessage.content,
n: 1,
size: '1024x1024',
quality: 'hd',
style: 'vivid',
});
if (!imageResponse.data || !Array.isArray(imageResponse.data) || imageResponse.data.length === 0) {
return NextResponse.json({ error: 'Ошибка: изображение не получено' }, { status: 500 });
}
return NextResponse.json({
content: imageResponse.data[0].url,
role: 'assistant',
type: 'image',
});
}
return NextResponse.json({ error: 'Неверный режим' }, { status: 400 });
} catch (error) {
console.error('Ошибка API:', error);
if (error instanceof OpenAI.APIError) {
return NextResponse.json(
{ error: `Ошибка API OpenAI: ${error.message}` },
{ status: error.status || 500 }
);
}
return NextResponse.json(
{ error: 'Не удалось обработать запрос' },
{ status: 500 }
);
}
}
Этот код создает API, который:
- Обрабатывает текстовые запросы через GPT-4o в режиме
chat
. - Генерирует изображения через DALL-E 3 в режиме
image
. - Использует параметр
temperature=0.7
для баланса между креативностью и точностью. Более подробно про этот параметр можно прочитать в официальной документации.
Шаг 3: Разработка интерфейса чата
Разработаем современный дизайн, напоминающий ChatGPT, который включает в себя функции текстового чата и возможность создания изображений. Этот интерфейс будет реализован с использованием React — популярного JavaScript-фреймворка, который позволяет создавать интерактивные пользовательские интерфейсы через компоненты. Мы создадим компонент ChatInterface
, который станет основой нашего чата, и разберем его структуру, чтобы вы могли адаптировать его под свои нужды.
В каталоге src
создайте папку с именем components
. В созданной папке создайте новый файл с названием ChatInterface.tsx
.
Компонент ChatInterface
использует хуки React (например, useState
и useEffect
) для управления состоянием и побочными эффектами, а также стилизацию через Tailwind CSS для современного дизайна.
-
Импорт зависимостей: Мы импортируем необходимые модули, такие как
useState
,useRef
иuseEffect
из React, а такжеReactMarkdown
иremarkGfm
для рендеринга разметки Markdown. -
Интерфейс Message: Определяет структуру сообщений (роль, содержимое, тип и модель), что помогает TypeScript проверять типы данных.
-
Константа AVAILABLE_MODELS: Список доступных моделей ИИ с описаниями, которые отображаются в выпадающем меню для выбора.
-
Состояние и рефы: Компонент использует несколько состояний (
messages
,input
,isLoading
, и т.д.) для управления чатом, а также рефы (messagesEndRef
,textareaRef
) для управления DOM-элементами (например, автоматической прокруткой и динамической высотой текстового поля). -
Функции:
scrollToBottom
обеспечивает плавную прокрутку к последнему сообщению.handleSubmit
отправляет запрос к API и обновляет состояние чата.handleKeyDown
позволяет отправлять сообщение по нажатию Enter.clearConversation
сбрасывает историю чата.
-
Разметка: Интерфейс разбит на три секции — заголовок с выбором модели и кнопкой очистки, область сообщений и поле ввода с переключением режимов (текст/изображение). Tailwind CSS используется для стилизации.
Добавьте в ChatInterface.tsx
следующий код. Комментарии внутри кода помогут вам понять, как каждая часть работает, и дадут подсказки, где можно экспериментировать.
'use client';
// Импортируем хуки React для управления состоянием и DOM
import { useState, useRef, useEffect } from 'react';
// Импортируем ReactMarkdown для отображения текста в формате Markdown
import ReactMarkdown from 'react-markdown';
// Добавляем поддержку таблиц и других элементов GitHub Flavored Markdown
import remarkGfm from 'remark-gfm';
// Определяем интерфейс для сообщений, чтобы TypeScript проверял их структуру
interface Message {
role: 'user' | 'assistant';
content: string;
type?: 'text' | 'image';
model?: string;
}
// Список доступных моделей ИИ с описаниями для выбора пользователем
const AVAILABLE_MODELS = [
{ id: 'gpt-4o', name: 'GPT-4o', description: 'Идеально для большинства задач' },
{ id: 'o3', name: 'o3', description: 'Продвинутое логическое мышление' },
{ id: 'o4-mini', name: 'o4-mini', description: 'Быстрое решение сложных задач' },
{ id: 'o4-mini-high', name: 'o4-mini-high', description: 'Отлично для кода и визуального анализа' },
];
// Основной компонент чата
export default function ChatInterface() {
// Состояния для управления чатом: история сообщений, ввод текста, загрузка и т.д.
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [mode, setMode] = useState<'chat' | 'image'>('chat');
const [selectedModel, setSelectedModel] = useState('gpt-4o');
const [showModelDropdown, setShowModelDropdown] = useState(false);
// Референсы для управления DOM: прокрутка и высота текстового поля
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Функция, которая позволяет перейти к последнему сообщению.
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
// Эффект для автоматической прокрутки при обновлении сообщений
useEffect(() => {
scrollToBottom();
}, [messages]);
// Эффект для динамической высоты текстового поля при вводе
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
}
}, [input]);
// Обработчик отправки формы (отправка сообщения)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const userMessage: Message = {
role: 'user',
content: input,
model: selectedModel,
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [...messages, userMessage],
mode: mode,
model: selectedModel,
}),
});
const data = await response.json();
setMessages((prev) => [...prev, { ...data, model: selectedModel }]);
} catch (error) {
console.error('Ошибка:', error);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: 'Извините, что-то пошло не так.',
model: selectedModel,
},
]);
} finally {
setIsLoading(false);
}
};
// Обработчик нажатия Enter для отправки сообщения
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any); // Приведение типа для совместимости (можно улучшить с типами)
}
};
// Функция для очистки истории
const clearConversation = () => {
setMessages([]);
};
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* Заголовок с выбором модели и кнопкой очистки */}
<div className="bg-white border-b border-gray-200 px-4 py-2">
<div className="max-w-3xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold">Ваш AI-ассистент</h1>
<div className="relative">
<button
onClick={() => setShowModelDropdown(!showModelDropdown)}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800 transition-colors"
>
<span>{AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name}</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showModelDropdown && (
<div className="absolute top-full left-0 mt-1 w-80 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50">
<div className="px-4 py-2 text-sm text-gray-500">Выбор модели</div>
{AVAILABLE_MODELS.map((model) => (
<button
key={model.id}
onClick={() => {
setSelectedModel(model.id);
setShowModelDropdown(false);
}}
className={`w-full text-left px-4 py-3 hover:bg-gray-50 flex items-center justify-between group ${
selectedModel === model.id ? 'bg-gray-50' : ''
}`}
>
<div>
<div className="font-medium text-sm">{model.name}</div>
<div className="text-xs text-gray-500">{model.description}</div>
</div>
{selectedModel === model.id && (
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
))}
</div>
)}
</div>
</div>
<button
onClick={clearConversation}
className="p-2 text-gray-600 hover:text-gray-800 transition-colors"
title="Очистить чат"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
</div>
{/* Область отображения сообщений с поддержкой прокрутки */}
<div className="flex-1 overflow-y-auto p-4">
<div className="max-w-3xl mx-auto">
{messages.length === 0 && (
<div className="text-center py-16">
<h2 className="text-2xl font-semibold text-gray-900">Задайте мне вопрос или опишите изображение!</h2>
</div>
)}
{messages.map((message, index) => (
<div key={index} className="py-4">
<div className="flex gap-3">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center ${
message.role === 'user' ? 'bg-blue-100' : 'bg-gray-800 text-white'
}`}
>
{message.role === 'user' ? (
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
) : (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" />
</svg>
)}
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-700">
{message.role === 'user' ? 'Вы' : 'Ассистент'}
</div>
{message.type === 'image' ? (
<img
src={message.content}
alt="Сгенерированное изображение"
className="max-w-full rounded-lg mt-2"
/>
) : (
<div className="text-gray-800 mt-1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
</div>
)}
</div>
</div>
</div>
))}
{isLoading && (
<div className="py-4">
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center">
<div className="flex space-x-1">
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse" />
<div
className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"
style={{ animationDelay: '0.2s' }}
/>
<div
className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"
style={{ animationDelay: '0.4s' }}
/>
</div>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-700">Ассистент</div>
<div className="text-gray-600 text-sm mt-1">Обрабатываю запрос...</div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Поле ввода с переключением режимов */}
<div className="bg-white border-t p-4">
<div className="max-w-3xl mx-auto">
<div className="flex gap-2 mb-3">
<button
onClick={() => setMode('chat')}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
mode === 'chat' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Текст
</button>
<button
onClick={() => setMode('image')}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
mode === 'image' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Изображение
</button>
</div>
<form onSubmit={handleSubmit} className="relative">
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={mode === 'chat' ? 'Введите ваш вопрос...' : 'Опишите желаемое изображение...'}
className="w-full p-3 bg-gray-100 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={1}
style={{ maxHeight: '150px' }}
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className={`absolute right-2 bottom-2 p-2 rounded-lg ${
isLoading || !input.trim() ? 'text-gray-400 cursor-not-allowed' : 'text-white bg-blue-600 hover:bg-blue-700'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
</form>
<p className="text-xs text-gray-500 mt-2 text-center">
{mode === 'chat' ? 'Ответы могут содержать неточности, проверяйте важные данные.' : 'Изображения могут отличаться от описания.'}
</p>
</div>
</div>
</div>
);
}
В этом интерфейсе можно переключаться между текстовым общением и созданием изображений. Также здесь отображается история сообщений, и при необходимости система автоматически прокручивает к последнему сообщению.
Шаг 4: Интеграция интерфейса в приложение
На следующем этапе потребуется объединить наш интерфейс с приложением и завершить работу.
Откройте src/app/page.tsx
и замените всё его содержимое на это:
import ChatInterface from '@/components/ChatInterface';
export default function Home() {
return (
<main className="h-screen">
<ChatInterface />
</main>
);
}
Откройте файл src/app/layout.tsx
и замените всё его содержимое для добавления заголовка:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Умный AI-ассистент',
description: 'Создайте своего AI-ассистента с JavaScript и OpenAI',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body className={inter.className}>
<header className="bg-white border-b p-4">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">Умный AI-ассистент</h1>
<span className="text-sm text-gray-600">На базе OpenAI</span>
</div>
</header>
{children}
</body>
</html>
);
}
Добавьте обработку ошибок, создав src/app/error.tsx
:
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-xl font-bold text-gray-800 mb-4">Ой, что-то пошло не так!</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Попробовать снова
</button>
</div>
</div>
);
}
Шаг 5: Тестирование ассистента
Запустите сервер разработки:
npm run dev
Запустите браузер и откройте адрес http://localhost:3000
. Перед вами появится окно помощника — в нем можно менять режим с общения в чате на создание изображений. Попробуйте оба варианта, задавая вопросы и предоставляя описания изображений.
Шаг 6: Развертывание AI-ассистента
Теперь, когда AI-ассистент полностью готов, его можно развернуть на сервере, чтобы иметь к нему беспрерывный доступ.
Но перед этим откройте файл next.config.ts
и настройте его следующим образом:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // Генерируем статические файлы в /out
distDir: 'out', // Папка для статических файлов
images: {
unoptimized: true, // Отключаем оптимизацию изображений
},
reactStrictMode: true,
eslint: {
ignoreDuringBuilds: true, // Игнорируем ошибки ESLint
},
typescript: {
ignoreBuildErrors: true, // Игнорируем ошибки TypeScript
},
trailingSlash: true,
};
export default nextConfig;
Также создайте файл .eslintrc.json
в корне проекта и добавьте в него следующий код:
{
"extends": ["next/core-web-vitals"],
"rules": {
"@next/next/no-img-element": "off"
}
}
Загрузка на GitHub
Создайте закрытый репозиторий на GitHub, а затем вернитесь к вашему проекту. Далее нужно проделать следующие действия, чтобы загрузить код на GitHub:
-
Добавьте все изменения к коммиту:
git add .
-
Создайте коммит:
git commit -m "first commit"
-
Измените название текущей ветви и установите ее как основную:
git branch -M main
-
Добавьте удаленный репозиторий и свяжите его с указанным URL. Вместо ссылки на мой репозиторий укажите ссылку на свой.
git remote add origin https://github.com/an-vadim-an/ai-assistant
-
Отправьте изменения на репозитории:
git push -u origin main
Загрузка на сервер
Когда файлы будут загружены на GitHub, можно приступить к запуску приложения. Я буду использовать сервис Apps от Timeweb Cloud.
В первую очередь нужно создать новый проект. Присвойте ему имя, а при желании — дополните его описанием и иллюстрацией.
После того как проект был создан, необходимо создать Apps. Выберите тип Frontend и фреймворк Next.js.
Чтобы загрузить проекты, нужно привязать учетную запись с GitHub. После завершения процедуры привязки введите название репозитория, которое использовали при создании. В разделе «Регион» выберите тот, который находится ближе всего к вам и имеет наименьшую задержку.
Выберите минимальную конфигурацию — ее достаточно для нашего AI-ассистента. В настройках приложения необходимо добавить одну переменную, под названием OPENAI_API_KEY
, а в значение вставить ключ API.
В описании приложения необходимо указать его имя, при необходимости — комментарий. Также не забудьте выбрать проект, к которому будет привязан Apps.
Затем можно активировать процесс развертывания Next.js на сервере, нажав на кнопку «Запустить деплой».
Через некоторое время приложение будет запущено, и в случае успешного развертывания проекта в журнале деплоя появится сообщение «Deployment successfully completed».
После успешного запуска приложения вы можете открыть его, нажав на соответствующую кнопку, которая расположена справа от названия приложения.
Нажмите на нее, и вы увидите новую вкладку в браузере с вашим готовым к работе AI-ассистентом.
Часто задаваемые вопросы
Возможно ли разработать помощника на основе искусственного интеллекта исключительно с использованием JavaScript?
Конечно! Применение JavaScript в сочетании с такими библиотеками, как OpenAI, открывает разнообразные перспективы для разработки полезных помощников для работы с текстом и изображениями.
Какая библиотека лучше для чат-бота?
- OpenAI: отличная библиотека для работы с текстом и изображениями.
- Dialogflow: идеальное решение для реализации разговорных интерфейсов.
- Botpress: универсальный и эффективный инструмент для разработки сложных чат-ботов.
Стоит ли применять методы машинного обучения?
Для базовых чат-ботов достаточно использовать стандартные алгоритмы, но для более сложных задач, таких как анализ текста, может потребоваться применение методов машинного обучения.
Python или JavaScript для AI?
Python — превосходный инструмент для создания сложных моделей с применением TensorFlow и Keras. В то же время JavaScript — оптимальный выбор для разработки веб-приложений и чат-ботов благодаря использованию Node.js и библиотек, таких как TensorFlow.js.
Выгодные тарифы на облако в Timeweb Cloud
Заключение
В этой статье мы разработали и внедрили современного помощника на основе искусственного интеллекта. Он может обрабатывать текстовые запросы и создавать изображения с помощью API OpenAI. Ассистент, вдохновленный дизайном ChatGPT, готов к применению в режиме реального времени и может быть усовершенствован и дополнен новыми возможностями, такими как поддержка новых моделей или интеграция с другими сервисами. Проект демонстрирует мощь JavaScript и современных фреймворков для создания различных приложений.