Привет, друзья!


В праздники я провел небольшое исследование представленных сегодня на рынке бесплатных инструментов с участием "искусственного интеллекта" для помощи в веб-разработке, результатами которого хочу с вами поделиться, авось кому-нибудь пригодится.


Обратите внимание, что я акцентировал внимание на бесплатных решениях для написания кода клиентской части веб-приложений.


Список протестированных инструментов:



Далее я подробнее расскажу о первых трех (Codeium, Devv, V0) и немного о двух следующих за ними (ChatGPT, GPT4All) решениях из представленных в списке, как наиболее "выдающихся" с моей точки зрения. Остальные сервисы (начиная с cursor и ниже) в той или иной степени похожи на codeium и `devv`, но показывают более плохие результаты генерации кода по запросу, исправления ошибок и документирования кода, а также менее удобны в использовании и иногда требуют дополнительных настроек, например, указания ключа OpenAI, установки лишних инструментов (тулкитов), являются условно бесплатными и т.п.


❯ Сodeium





Сodeium — это набор инструментов для помощи в написании нового и улучшении существующего кода, документации к нему и т.д. Работает в контексте кодовой базы, т.е. предложения по дополнению/модификации основаны на коде проекта, его стиле (используемых подходах и принципах), форматировании и т.п. Поставляется в виде расширения для VSCode, плагина для JetBrains и других редакторов. Поддерживает 70+ языков программирования. Полностью бесплатный.


В настоящее время команда codeium разрабатывает собственный редактор кода — Windsurf, где разработчик и ИИ занимаются парным программированием (затея сомнительная, но окэй :))


Преимущества использования


Контекстуальные предложения и дополнения кода


  • codeium анализирует кодовую базу целиком, включая стиль, форматирование и используемые подходы. Это обеспечивает согласованность и единообразие во всех компонентах и функциях
  • автоматически предлагает код, который соответствует существующим стандартам, что упрощает внедрение новых компонентов и сокращает время на ревью

Поддержка документации


  • Хорошая кодовая база должна включать не только код, но и качественную документацию: примеры использования компонентов, их ограничения и API
  • codeium помогает как в генерации новой, так и в улучшении существующей документации

Масштабируемость для больших проектов


  • Код проекта обычно разрастается со временем, включает десятки и сотни компонентов
  • codeium поддерживает работу с крупными проектами и эффективно обрабатывает большие кодовые базы

Поддержка 70+ языков программирования


  • При разработке приложений используются различные технологии:
    • Javascript/Typescript для логики компонентов
    • CSS/TailwindCSS/CSS-in-JS для стилизации
    • Markdown/MDX для документации
  • codeium поддерживает все эти "языки" и позволяет гибко переключаться между разными частями проекта

Интеграция с популярными IDE


  • Плагины для VSCode, JetBrains и других редакторов делают codeium удобным инструментом для команд, использующих разные инструменты разработки

Резюме


  • codeium упрощает повторное использование кода, что критично при создании универсальных компонентов
  • улучшает согласованность кода за счет анализа стиля и предоставления адаптированных рекомендаций
  • поддержка документации позволяет облегчить онбординг разработчиков, которые будут поддерживать или расширять проект



Попросим codeium написать TS-функцию для извлечения свойств объекта по ключам:





Я бы вместо T extends object использовал T extends Record<string, unknown>, в остальном к сгенерированному коду вопросов нет, что просили, то и получили.


Специально для адептов функционального программирования ;):


const extractProps = <
  T extends Record<string, unknown>,
  K extends keyof T,
>(
  props: T,
  ...keys: K[]
) =>
  Object.fromEntries(
    keys.filter((key) => key in props).map((key) => [key, props[key]]),
  ) as Pick<T, K>

❯ Devv_





Devv_ — это "поисковик следующего поколения для разработчиков". Показывает отличные результаты в генерации кода по запросу, в частности на JavaScript/TypeScript/React. Дает развернутые и понятные комментарии к сгенерированному коду. Полностью бесплатный.


Преимущества использования


Высокое качество генерируемого кода


  • devv_ очень эффективен при работе с JavaScript/TypeScript/React, которые являются основой многих веб-приложений
  • может генерировать качественные, чистые и современные компоненты, соответствующие актуальным практикам и технологиям

Развернутые комментарии


  • При разработке проекта важно не только писать код, но и объяснять сложную логику для последующей поддержки и развития другими разработчиками
  • devv_ генерирует понятные и развернутые комментарии, что улучшает читаемость кода и упрощает развитие проекта

Решение узких задач и разработка небольших компонентов


  • Для создания отдельных компонентов (например, кнопок, карточек или модальных окон) devv_ позволяет быстро генерировать код по конкретному запросу
  • это сокращает время на разработку и позволяет сосредоточиться на более высокоуровневых задачах (например, архитектуре системы)

Доступность


  • devv_ работает бесплатно в браузере, не требует дополнительных настроек и может быть использован на любом этапе разработки

Резюме


  • devv_ хорошо подходит для быстрого прототипирования компонентов и генерации примеров использования
  • объяснение кода помогает создавать самодокументирующуюся кодовую базу, что облегчает работу других разработчиков
  • удобный веб-интерфейс делает devv_ отличным инструментом для небольших и средних команд, которые могут использовать его для проверки идей и разработки MVP



Попросим devv_ реализовать алгоритм Кнута-Морриса-Пратта (здесь про алгоритм можно почитать подробнее и найти пример его реализации):





Сгенерированный код:
/**
 * The Knuth-Morris-Pratt (KMP) algorithm is a string-searching algorithm that searches for occurrences of a "word" W within a main "text string" S.
 * It utilizes a pre-computed table (LPS array - Longest Proper Prefix Suffix) to optimize the search process.
 *
 * @param text The main text string to search within.
 * @param pattern The word (pattern) to search for in the text.
 * @returns An array of indices representing the starting positions of all occurrences of the pattern within the text.
 */
function knuthMorrisPratt(text: string, pattern: string): number[] {
    const textLength = text.length;
    const patternLength = pattern.length;

    // 1. Build LPS (Longest Proper Prefix Suffix) array
    const lps = buildLPS(pattern);

    // 2. Search for pattern in text
    const occurrences: number[] = [];
    let textIndex = 0;
    let patternIndex = 0;

    while (textIndex < textLength) {
        if (text[textIndex] === pattern[patternIndex]) {
            textIndex++;
            patternIndex++;

            if (patternIndex === patternLength) {
                // Pattern found!
                occurrences.push(textIndex - patternLength);
                patternIndex = lps[patternIndex - 1]; // Reset patternIndex using LPS
            }
        } else {
            if (patternIndex !== 0) {
                patternIndex = lps[patternIndex - 1]; // Shift pattern based on LPS
            } else {
                textIndex++; // Move to the next character in the text
            }
        }
    }

    return occurrences;
}

/**
 * Builds the LPS (Longest Proper Prefix Suffix) array for a given pattern.
 *
 * @param pattern The pattern string.
 * @returns The LPS array.
 */
function buildLPS(pattern: string): number[] {
    const patternLength = pattern.length;
    const lps: number[] = new Array(patternLength).fill(0);
    let length = 0; // Length of the previous longest prefix suffix

    lps[0] = 0; // lps[0] is always 0
    let i = 1;

    while (i < patternLength) {
        if (pattern[i] === pattern[length]) {
            length++;
            lps[i] = length;
            i++;
        } else {
            if (length !== 0) {
                length = lps[length - 1]; // Fallback to previous LPS value
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }

    return lps;
}

// Example usage:
const text = "ABC ABCDAB ABCDABCDABDE";
const pattern = "ABCDABD";
const indices = knuthMorrisPratt(text, pattern);
console.log(`Pattern found at indices: ${indices}`); // Output: Pattern found at indices: 15

const text2 = "ABABDABACDABABCABAB";
const pattern2 = "ABABCABAB";
const indices2 = knuthMorrisPratt(text2, pattern2);
console.log(`Pattern found at indices: ${indices2}`); // Output: Pattern found at indices: 10

const text3 = "AAAAABAAABA";
const pattern3 = "AAAA";
const indices3 = knuthMorrisPratt(text3, pattern3);
console.log(`Pattern found at indices: ${indices3}`); // Output: Pattern found at indices: 0,1

const text4 = "THIS IS A TEST TEXT";
const pattern4 = "TEST";
const indices4 = knuthMorrisPratt(text4, pattern4);
console.log(`Pattern found at indices: ${indices4}`); // Output: Pattern found at indices: 10

Сгенерированный код работает примерно так, как и должен.




Попросим devv_ реализовать хук React/TS для извлечения ширины и высоты элемента при изменении его размеров:





Код хука:
import { useState, useRef, useLayoutEffect, useCallback } from 'react';
import ResizeObserver from 'resize-observer-polyfill';

interface Size {
  width: number | undefined;
  height: number | undefined;
}

type UseResizeObserverOptions = {
  box?: 'border-box' | 'content-box' | 'device-pixel-content-box';
};

function useResizeObserver<T extends HTMLElement = HTMLElement>(
  options: UseResizeObserverOptions = {}
): [React.RefObject<T>, Size] {
  const { box = 'content-box' } = options;
  const ref = useRef<T>(null);
  const [size, setSize] = useState<Size>({ width: undefined, height: undefined });

  const handleResize = useCallback(
    (entries: ResizeObserverEntry[]) => {
      if (!Array.isArray(entries)) {
        return;
      }

      const entry = entries[0];
      const newWidth = entry.contentRect.width;
      const newHeight = entry.contentRect.height;

      setSize({ width: newWidth, height: newHeight });
    },
    []
  );

  useLayoutEffect(() => {
    if (!ref.current) return;

    const resizeObserver = new ResizeObserver(handleResize);
    resizeObserver.observe(ref.current, { box });

    return () => {
      resizeObserver.disconnect();
    };
  }, [ref.current, box, handleResize]);

  return [ref, size];
}

export default useResizeObserver;

Пример использования:
import React from 'react';
import useResizeObserver from './useResizeObserver';

const MyComponent: React.FC = () => {
  const [ref, size] = useResizeObserver();

  return (
    <div ref={ref} style={{ width: '50%', height: '200px', border: '1px solid black' }}>
      {size.width && size.height && (
        <p>Width: {size.width}px, Height: {size.height}px</p>
      )}
    </div>
  );
};

export default MyComponent;

К этому коду есть несколько вопросов, например:


  • на сегодняшний день нам не нужен полифил
  • ref.current в зависимостях хука useLayoutEffect() не нужен
  • вместо useLayoutEffect() я бы использовал useEffect() и т.д.

Но, в целом, код довольно качественный и с ним можно работать.


❯ V0





v0 — AI-помощник, специализирующийся на веб-разработке. Показывает отличные результаты в написании кода на JavaScript/TypeScript по запросу. Генерирует компоненты React с использованием современных подходов и технологий. Дает развернутые и понятные комментарии к написанному коду. Полностью бесплатный.


Преимущества использования


Фокус на веб-разработке


  • v0 специально заточен под React и TypeScript, которые сегодня фактически являются стандартом разработки веб-приложений
  • генерирует компоненты с использованием современных подходов (например, хуков React, TailwindCSS или стилизованных компонентов (Styled Components)).

Автоматизация создания компонентов


  • Позволяет быстро и эффективно генерировать модульные и переиспользуемые компоненты на основе запроса
  • подходит для создания как автономных компонентов (например, кнопок, иконок, выпадающих списков), так и составных компонентов (например, форм или карточек)

Развернутые комментарии и объяснения


  • v0 генерирует развернутые пояснения к сгенерированному коду. Это помогает документировать кодовую базу и обеспечивает прозрачность логики компонентов

Быстрая интеграция в проекты


  • Созданные компоненты легко интегрируются в существующие проекты
  • позволяет следовать принципам DRY (Don't Repeat Yourself) и атомарному подходу к разработке компонентов

Простота использования


  • v0 работает бесплатно в браузере, не требует установки дополнительных инструментов и предоставляет простой, понятный и удобный интерфейс

Резюме


  • v0 отлично подходит для генерации и оптимизации компонентов, что ускоряет разработку веб-приложений
  • фокус на React и современных технологиях делает его особенно полезным при разработке библиотек компонентов (дизайн-систем)
  • объяснения и комментарии помогают команде быстро разбираться в коде и легко расширять проект



Попросим v0 сгенерировать код компонента React "Модальное окно" с помощью элемента HTML dialog:






Код модального окна:
// Предполагается использование серверных компонентов Next.js
'use client'

import React, { useRef, useEffect, ReactNode } from 'react'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'

interface ModalProps {
  isOpen: boolean
  onClose: () => void
  children: ReactNode
  title: string
}

export function Modal({ isOpen, onClose, children, title }: ModalProps) {
  const dialogRef = useRef<HTMLDialogElement>(null)

  useEffect(() => {
    const dialogElement = dialogRef.current
    if (!dialogElement) return

    if (isOpen) {
      dialogElement.showModal()
    } else {
      dialogElement.close()
    }

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        onClose()
      }
    }

    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown)
    }

    return () => {
      document.removeEventListener('keydown', handleKeyDown)
    }
  }, [isOpen, onClose])

  const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
    if (e.target === dialogRef.current) {
      onClose()
    }
  }

  return (
    {/* dialog, как мы и просили */}
    <dialog
      ref={dialogRef}
      // Предполагается использование tailwind
      className="p-0 bg-background rounded-lg shadow-lg backdrop:bg-black/50"
      onClick={handleBackdropClick}
    >
      <div className="min-w-[300px] max-w-md">
        <div className="flex justify-between items-center p-4 border-b">
          <h2 className="text-lg font-semibold">{title}</h2>
          <Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">
            <X className="h-4 w-4" />
          </Button>
        </div>
        <div className="p-4">{children}</div>
      </div>
    </dialog>
  )
}

Код примера использования:
'use client'

import { useState } from 'react'
import { Modal } from '@/components/Modal'
import { Button } from '@/components/ui/button'

export default function Home() {
  const [isModalOpen, setIsModalOpen] = useState(false)

  return (
    <div className="flex flex-col items-center justify-center min-h-screen p-4">
      <h1 className="text-2xl font-bold mb-4">Modal Demo</h1>
      <Button onClick={() => setIsModalOpen(true)}>Open Modal</Button>

      <Modal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        title="Example Modal"
      >
        <p>This is the content of the modal. You can put any React components or HTML here.</p>
        <div className="mt-4 flex justify-end">
          <Button onClick={() => setIsModalOpen(false)}>Close</Button>
        </div>
      </Modal>
    </div>
  )
}

Комментарии:

I'll create a React modal component using the HTML <dialog> element. This approach provides a native, accessible modal solution with minimal JavaScript. Let's build this component step by step.


This implementation creates a reusable Modal component that uses the HTML <dialog> element. Here's a breakdown of the key features:


  1. The Modal component accepts isOpen, onClose, children, and title as props.
  2. It uses the useRef hook to get a reference to the <dialog> element.
  3. The useEffect hook is used to control the opening and closing of the modal based on the isOpen prop.
  4. It implements a click handler on the backdrop to close the modal when clicking outside of it.
  5. The component is styled using Tailwind CSS classes for a clean, modern look.
  6. It uses the Button component from the UI library for consistency.
  7. The modal is accessible, supporting closing via the Escape key and using semantic HTML.

In the Home component, we demonstrate how to use the Modal:


  1. We use the useState hook to manage the open state of the modal.
  2. A button is provided to open the modal.
  3. The Modal component is rendered with the necessary props, including the content as children.

This implementation provides a flexible, accessible, and easy-to-use modal component that leverages the native <dialog> element for better performance and built-in accessibility features.


Не то, чтобы к сгенерированному коду не было вопросов, но в качестве отправной точки вполне подойдет.


❯ ChatGPT и GPT4All


ChatGPT — это написанное на Rust десктопное приложение для Mac, Windows и Linux. О ChatGPT написано млн статей, так что, с вашего позволения, я сразу перейду к примеру использования приложения.


Скормим ChatGPT скриншот кода, сгенерированного codeium и попросим его улучшить:





Весь ответ:

Yes, I can improve this code snippet. I'll refine it to make it more concise and efficient while adhering to best practices. Here's the updated code:


Improved Code


function extractPropertiesByKeys<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    return keys.reduce((result, key) => {
        if (key in obj) {
            result[key] = obj[key];
        }
        return result;
    }, {} as Pick<T, K>);
}

Improvements Made


  • Replaced the for...of Loop with reduce:

Using Array.prototype.reduce is more functional and concise for building an object iteratively.


  • Removed the Need for Type Assertion in the Return Statement:

The result is initialized as {} as Pick<T, K> to ensure proper typing without needing assertions later.


  • Avoided the Partial Utility Type:

The Pick type is sufficient for ensuring that only the specified keys are included in the resulting object. Using Partial was unnecessary and could lead to confusion.


  • Better Readability and Clarity:

This version is shorter and aligns with modern JavaScript/TypeScript functional programming practices.


Example Usage


interface Person {
    name: string;
    age: number;
    address: string;
}

const person: Person = {
    name: "Alice",
    age: 30,
    address: "123 Main St"
};

const selectedProperties = extractPropertiesByKeys(person, ["name", "age"]);
console.log(selectedProperties); // Output: { name: "Alice", age: 30 }

Let me know if you’d like further refinements or additional explanations!


Попросим ChatGPT придать коду более функциональный вид:





Где-то такой вариант утилиты мы уже видели :)


Весь ответ:

Certainly! A functional programming approach emphasizes immutability and the use of higher-order functions. Here's the rewritten code using a functional programming paradigm:


Functional Programming Version


function extractPropertiesByKeys<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    return Object.fromEntries(
        keys
            .filter((key) => key in obj)
            .map((key) => [key, obj[key]])
    ) as Pick<T, K>;
}

Explanation of Functional Approach


  • filter:
    Filters the keys array to include only keys that exist in the object.


  • map:
    Transforms each valid key into a [key, value] pair (tuples) suitable for creating an object.


  • Object.fromEntries:
    Converts the array of key-value pairs into an object.



Example Usage


interface Person {
    name: string;
    age: number;
    address: string;
}

const person: Person = {
    name: "Alice",
    age: 30,
    address: "123 Main St"
};

const selectedProperties = extractPropertiesByKeys(person, ["name", "age"]);
console.log(selectedProperties); // Output: { name: "Alice", age: 30 }

Why This Is Functional


  • Immutability: No variables are mutated; each function call produces a new result.
  • Higher-Order Functions: filter and map are used for processing collections in a declarative way.
  • Declarative Readability: The code describes what to do rather than how to do it.

Let me know if you’d like more refinements or explanations!




В завершение кратко рассмотрим еще одно интересное решение — GPT4All.





GPT4All — это десктопное приложение для работы с локальными (!) большими языковыми моделями. Показывает хорошие результаты в написании кода по запросу, в частности на JavaScript/TypeScript/React. Дает развернутые и понятные комментарии к сгенерированному коду. Бесплатный, но для работы требуется БЯМ (например, модель Llama 3 весит 4,34 ГБ и требует 8 ГБ оперативной памяти, минимум). Работает как онлайн, так и оффлайн.


Это единственное готовое бесплатное решение, которое я обнаружил, позволяющее использовать всю мощь ИИ без доступа к Интернету, что может быть важно в некоторых ситуациях, например, когда вы опасаетесь, что ваши данные без вашего ведома могут оказаться в распоряжении сервиса, предоставляющего услуги.


В заключение отмечу, что в своей работе я часто использую codeium и ChatGPT и иногда прибегаю к помощи v0.


Делитесь вашими находками в комментариях.


Happy coding!




Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Комментарии (3)


  1. positroid
    22.01.2025 09:51

    Во всех перечисленных инструментах из списка вы тестировали только режим чата? У всех или почти всех под капотом одни и те же модели (claude sonnet 3.5 или openai gpt-4o-mini), в рамках сравнения чата логичнее было тогда уж сравнивать именно результаты, которые выдают сами модели напрямую (а заодно фичи чатов вроде артефактов у claude или canvas у openai).

    Для инструментов чат скорее вторичен - их нужно сравнивать уже по другому набору функций - качество подсказок, понимание и размер контекста, фичи, которые предоставляет инструмент для разработки (агенты, комментарии к коммитам, поиск по кодовой базе, возможность выполнять команды на устройстве).

    Copilot с недавних пор бесплатен с определенными лимитами, но наиболее функционален в рамках VS Code. Cursor имеет бесплатный период в 2 недели, но может в агентном режиме сам читать и писать код непосредственно в проекте, без ручного копирования между чатами.


  1. Alexandroppolus
    22.01.2025 09:51

    По поводу useResizeObserver, сотворенного devv_: там действительно не нужен ref.current в зависимостях эффекта, он ведь меняется после рендера. Более того, этот хук сможет работать, только если элемент, помещаемый в реф, создается в момент монтирования компонента с хуком и живет всё время, пока жив родитель. Чтобы работало во всех случаях, надо вообще убрать зависимости из эффекта, и уже вручную в функции эффекта проверять, поменялся ли box или ref.current. Дополнительно понадобится ещё эффект для очистки при размонтировании. Как-то так:

    Код
    const prevRef = useRef(ref.current);
    const prevBoxRef = useRef(box);
    const refObserver = useRef<ResizeObserver>(null);
    
    useLayoutEffect(() => {
        // проверяем, что по факту депсы не поменялись
        if (ref.current === prevRef.current && box === prevBoxRef.current) {
          return;
        }
    
        // дисконнект предыдущего
        refObserver.current?.disconnect();
        refObserver.current = null;
      
        if (!ref.current) return;
    
        refObserver.current = new ResizeObserver(handleResize);
        refObserver.current.observe(ref.current, { box });
    });
    
    // размонтирование компонента
    useLayoutEffect(() => () => refObserver.current?.disconnect(), []);

    Может ли devv_ (или кто-либо иной из списка) выдать что-то подобное, если ему объяснить проблему?


    1. Alexandroppolus
      22.01.2025 09:51

      забыл добавить обновления prevRef и prevBoxRef, сразу после проверки

      ...
      prevBoxRef.current = ref.current;
      prevBoxRef.current = box;
      ...