Другое

Как создать форму ввода как в ChatGPT в Next.js 16 + Tailwind v4

Полное руководство по реализации формы ввода как в ChatGPT в Next.js 16 + Tailwind v4. Узнайте как избежать бесконечного переключения состояний и создать плавную анимацию.

Как реализовать форму ввода как в веб-версии ChatGPT в Next.js 16 + Tailwind v4?

Я разрабатываю проект на Next.js 16 + Tailwind v4, который является оберткой над LLM. Мне нужно реализовать форму ввода, аналогичную той, что используется в веб-версии ChatGPT.

Моя текущая реализация использует подход с CSS Grid и двумя состояниями формы:

  1. Сжатое состояние (isExpanded === false):

    • 1 ряд, 3 колонки
    • Структура: кнопка | textarea | контейнер с двумя кнопками
  2. Расширенное состояние (isExpanded === true):

    • 2 ряда, 3 колонки
    • Структура: textarea | textarea | textarea
    • Второй ряд: кнопка | пусто | контейнер с двумя кнопками

Проблема: при автоматическом переключении состояния isExpanded на основе высоты textarea возникает бесконечное переключение между состояниями, так как изменение сетки влияет на ширину textarea.

Я пробовал:

  1. Фиксировать isExpanded в true при первом изменении высоты
  2. Анимировать изменения с помощью чистого CSS (неудачно - элементы теряли форму)
  3. Использовать Framer Motion для анимации лейаута (лучше, но все равно неказисто)

Вопросы:

  • Как правильно реализовать такую форму ввода с плавной анимацией?
  • Стоит ли использовать textarea или contenteditable div для поля ввода?
  • Как избежать проблемы с бесконечным переключением состояний?
  • Какие есть лучшие практики для реализации таких UI-компонентов?

Для реализации формы ввода как в веб-версии ChatGPT в Next.js 16 + Tailwind v4 используйте комбинацию CSS Grid с auto-growing textarea и правильным управлением состоянием. Ключ к успеху — разделение логики высоты от логики ширины, чтобы избежать бесконечного переключения состояний, и использование современных техник анимации.

Содержание

Основные подходы к реализации

Существует несколько подходов к созданию формы ввода типа ChatGPT, каждый со своими преимуществами и недостатками:

  1. CSS Grid с двумя состояниями — ваш текущий подход, который требует тщательного управления состоянием
  2. Flexbox с динамической высотой — более простой подход с автоматическим изменением размера
  3. Contenteditable div — позволяет richer функциональность, но сложнее в реализации
  4. Комбинированный подход — использование библиотек или готовых компонентов

CSS-Tricks предлагает элегантное решение с использованием CSS Grid, где textarea автоматически расширяется в зависимости от содержимого, без необходимости переключения состояний.

Автоматически расширяющийся textarea

Наиболее эффективным решением является использование техники с CSS Grid, где textarea расширяется автоматически без переключения состояний. Вот пример реализации:

html
<div class="grid text-sm after:px-3.5 after:py-2.5 
             [&>textarea]:text-inherit after:text-inherit 
             [&>textarea]:resize-none [&>textarea]:overflow-hidden 
             [&>textarea]:[grid-area:1/1/2/2] 
             after:[grid-area:1/1/2/2] 
             after:whitespace-pre-wrap after:invisible 
             after:content-[attr(data-cloned-val)' '] 
             after:border">
  <textarea 
    class="w-full text-slate-600 bg-slate-100 border border-transparent 
            hover:border-slate-200 appearance-none rounded px-3.5 py-2.5 
            outline-none focus:bg-white focus:border-indigo-400 
            focus:ring-2 focus:ring-indigo-100"
    name="message" 
    id="message" 
    rows="2" 
    onInput="this.parentNode.dataset.clonedVal = this.value"
    placeholder="Your request..." 
    required>
  </textarea>
</div>

Этот подход из Cruip использует псевдоэлемент after для измерения высоты контента без необходимости переключения состояний.

Сравнение textarea и contenteditable div

Преимущества textarea:

  • Безопасность: Автоматически экранирует HTML теги
  • Простота: Легче в управлении состоянием
  • Семантика: Правильно обрабатывается формами
  • Доступность: Лучше поддержка скринридерами

Преимущества contenteditable div:

  • Форматирование: Поддержка богатого текста
  • Гибкость: Можно встраивать изображения и другие элементы
  • Стилизация: Больше возможностей для кастомизации

Как отмечает Stack Overflow, “используйте contenteditable div только тогда, когда вам нужна возможность форматировать текст”. Для простого текстового ввода textarea предпочтительнее.

В Reddit также подчеркивают, что contenteditable — это плохая практика для простых текстовых полей, так как он принимает HTML от пользователя, что небезопасно.

Решение проблемы бесконечного переключения состояний

Основная проблема вашего подхода — взаимозависимость высоты и ширины при переключении состояний. Вот несколько решений:

1. Разделение логики высоты и ширины

jsx
const [isExpanded, setIsExpanded] = useState(false);
const [text, setText] = useState('');
const textareaRef = useRef(null);

const handleInputChange = (e) => {
  const newText = e.target.value;
  setText(newText);
  
  // Проверяем высоту только после обновления значения
  if (textareaRef.current) {
    const scrollHeight = textareaRef.current.scrollHeight;
    const clientHeight = textareaRef.current.clientHeight;
    
    if (scrollHeight > clientHeight && !isExpanded) {
      setIsExpanded(true);
    }
  }
};

// Сбрасываем состояние при очистке поля
useEffect(() => {
  if (text.trim() === '' && isExpanded) {
    setIsExpanded(false);
  }
}, [text, isExpanded]);

2. Использование CSS transitions вместо анимации состояний

css
.chat-input-container {
  transition: all 0.3s ease;
}

.chat-input {
  transition: height 0.3s ease;
}

3. Фиксированная ширина в обоих состояниях

Вместо изменения структуры сетки, изменяйте только высоту textarea, сохраняя фиксированную ширину:

jsx
<div className="flex items-end space-x-2">
  <button className="p-2">📎</button>
  <div className="flex-1">
    <textarea
      ref={textareaRef}
      className={`w-full resize-none overflow-hidden transition-all duration-300 ${
        isExpanded ? 'min-h-[200px]' : 'min-h-[60px]'
      }`}
      value={text}
      onChange={handleInputChange}
      placeholder="Ask anything..."
      rows={1}
    />
  </div>
  <div className="flex items-end space-x-1">
    <button className="p-2">🎲</button>
    <button className="p-2">➡️</button>
  </div>
</div>

Практическая реализация в Next.js + Tailwind

Вот полная реализация компонента формы ввода:

jsx
'use client';

import { useState, useRef, useEffect } from 'react';

export default function ChatInput() {
  const [isExpanded, setIsExpanded] = useState(false);
  const [text, setText] = useState('');
  const textareaRef = useRef(null);

  const handleInputChange = (e) => {
    const newText = e.target.value;
    setText(newText);
    
    if (textareaRef.current) {
      const scrollHeight = textareaRef.current.scrollHeight;
      const clientHeight = textareaRef.current.clientHeight;
      
      if (scrollHeight > clientHeight && !isExpanded) {
        setIsExpanded(true);
      }
    }
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      // Обработка отправки сообщения
      console.log('Sending:', text);
      setText('');
      setIsExpanded(false);
    }
  };

  useEffect(() => {
    if (textareaRef.current) {
      textareaRef.current.style.height = 'auto';
      textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
    }
  }, [text]);

  return (
    <div className="border-t border-gray-200 p-4 bg-white">
      <div className="flex items-end space-x-2 max-w-4xl mx-auto">
        {/* Кнопка добавления файла */}
        <button className="p-2 text-gray-500 hover:text-gray-700 transition-colors">
          <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
          </svg>
        </button>

        {/* Контейнер для textarea */}
        <div className="flex-1">
          <textarea
            ref={textareaRef}
            className={`w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg 
                       focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
                       transition-all duration-200 resize-none overflow-hidden
                       ${isExpanded ? 'min-h-[200px] max-h-[400px]' : 'min-h-[60px]'}`}
            value={text}
            onChange={handleInputChange}
            onKeyDown={handleKeyDown}
            placeholder="Ask anything..."
            rows={1}
          />
        </div>

        {/* Кнопки действий */}
        <div className="flex items-end space-x-1">
          <button className="p-2 text-gray-500 hover:text-gray-700 transition-colors">
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
            </svg>
          </button>
          <button 
            className={`p-2 rounded-lg transition-colors ${
              text.trim() 
                ? 'bg-blue-500 hover:bg-blue-600 text-white' 
                : 'bg-gray-100 text-gray-400 cursor-not-allowed'
            }`}
            disabled={!text.trim()}
          >
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
            </svg>
          </button>
        </div>
      </div>
    </div>
  );
}

Анимация и переходы

Для плавных анимаций используйте CSS transitions и Tailwind utilities:

css
/* В вашем globals.css или Tailwind config */
.chat-input-textarea {
  transition: height 0.2s ease-out, padding 0.2s ease-out;
}

.chat-input-button {
  transition: all 0.2s ease;
}

Или с помощью Tailwind:

jsx
<textarea
  className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg 
             focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
             transition-all duration-200 ease-out resize-none overflow-hidden"
  // ... другие props
/>

Для более сложных анимаций рассмотрите Framer Motion:

jsx
import { motion, AnimatePresence } from 'framer-motion';

<motion.div
  initial={{ opacity: 0, y: 10 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.2 }}
  className="flex items-end space-x-2"
>
  {/* ваш контент */}
</motion.div>

Лучшие практики и рекомендации

  1. Используйте useRef для прямого доступа к DOM элементам — это обеспечивает лучшую производительность, чем поиск элементов через querySelector.

  2. Оптимизируйте обработчики событий — используйте useCallback для обработчиков, которые передаются как пропсы.

  3. Обрабатывайте edge cases — пустые строки, очень длинные тексты, специальные символы.

  4. Добавьте валидацию — проверяйте минимальную и максимальную длину текста.

  5. Реализуйте доступность — добавьте соответствующие aria-атрибуты и обработку клавиатурных сокращений.

  6. Тестируйте на разных устройствах — убедитесь, что форма работает корректно на мобильных устройствах.

  7. Используйте TypeScript — для лучшей типизации и IntelliSense.

  8. Оптимизируйте производительность — используйте React.memo и useMemo где это уместно.

Как отмечено в GeeksforGeeks, правильная структура компонента и управление состоянием — ключ к созданию отзывчивых и эффективных форм в Next.js.

Источники

  1. Auto-Growing Textarea with Tailwind CSS - Cruip
  2. Auto-Growing Inputs & Textareas | CSS-Tricks
  3. What are the cons of using a contentEditable div rather than a textarea? - Stack Overflow
  4. What should I use instead of contenteditable? - Reddit
  5. Create Form Layouts UI using Next.JS and Tailwind CSS - GeeksforGeeks
  6. GitHub - Next.js TailwindCSS ChatGPT Clone
  7. ChatGPT Chatbot Using Next.js + Tailwind CSS - Medium

Заключение

Для реализации формы ввода как в ChatGPT в Next.js + Tailwind v4:

  1. Предпочитайте textarea над contenteditable для простых текстовых полей — это безопаснее и проще в управлении.

  2. Используйте CSS Grid + псевдоэлементы для автоматического изменения размера textarea без переключения состояний.

  3. Разделяйте логику высоты и ширины — изменяйте только высоту textarea, сохраняя фиксированную ширину контейнера.

  4. Добавьте плавные переходы с помощью CSS transitions или Framer Motion для лучшего пользовательского опыта.

  5. Обрабатывайте edge cases — пустые строки, очень длинные тексты, и специальные символы.

  6. Оптимизируйте производительность — используйте useRef и useCallback для эффективной работы с DOM и событиями.

Следуя этим практикам, вы создадите надежный и красивый компонент формы ввода, который будет работать безупречно в вашем Next.js приложении.

Авторы
Проверено модерацией
Модерация