Пет-проект с AI-помощником: мой первый опыт вайбкодинга

от автора

Как и многие начинающие разработчики, я давно мечтал сделать свой первый pet‑проект — чтобы почувствовать себя «настоящим программистом» и перестать бояться собеседований. В итоге решился: буду писать веб‑приложение для личных заметок.

На самом деле я не совсем новичок. Раньше у меня уже были попытки освоить разные языки программирования, но дальше пары строчек кода дело редко заходило. Умение «гуглить правильно» и искать ответы на StackOverflow пока давалось тяжело, поэтому довести что‑то до результата было сложно.

В этот раз я решил пойти по трендам и подключить в процесс AI. Спасибо Хабру, что в нужный момент подкинул статью про Koda. С Koda и начался мой эксперимент. А команде хочу выразить респект за то, что всё бесплатно.

Что я хотел сделать

Идея была простой: сделать приложение для заметок, где текст можно красиво отображать на UI. Для разработки выбрал TypeScript: с его синтаксисом я уже немного знаком.

Как AI помогал

Опыт с Koda оказался полезным, хотя и не без нюансов.

Первые шаги

Сначала я спросил у AI, какие файлы и команды нужны для старта. Он сразу выдал готовый набор инструкций, и приятно удивило, что всё это можно было выполнить прямо в интерфейсе, не копаясь вручную.

Потом я уточнил: «Что дальше?» — и Koda нагенерил модель, сервис и файл для проверки. И, к моему удивлению, всё заработало с первого раза, именно так, как я ожидал.

Сгенерированные файлы

tsconfig.json

{   "compilerOptions": {     "target": "ES2020",     "module": "commonjs",     "outDir": "./dist",     "rootDir": "./src",     "strict": true,     "esModuleInterop": true,     "skipLibCheck": true,     "forceConsistentCasingInFileNames": true   } }

Note.ts

export interface Note {   id: string;   title: string;   content: string;   createdAt: Date;   updatedAt: Date; }

NoteService.ts

import { Note } from '../models/Note';  export class NoteService {   private notes: Note[] = [];    getAllNotes(): Note[] {     return this.notes;   }    getNoteById(id: string): Note | undefined {     return this.notes.find(note => note.id === id);   }    createNote(note: Omit<Note, 'id' | 'createdAt' | 'updatedAt'>): Note {     const newNote: Note = {       ...note,       id: Date.now().toString(),       createdAt: new Date(),       updatedAt: new Date()     };          this.notes.push(newNote);     return newNote;   }    updateNote(id: string, updatedNote: Partial<Omit<Note, 'id' | 'createdAt'>>): Note | null {     const noteIndex = this.notes.findIndex(note => note.id === id);          if (noteIndex === -1) {       return null;     }          this.notes[noteIndex] = {       ...this.notes[noteIndex],       ...updatedNote,       updatedAt: new Date()     };          return this.notes[noteIndex];   }    deleteNote(id: string): boolean {     const initialLength = this.notes.length;     this.notes = this.notes.filter(note => note.id !== id);     return this.notes.length !== initialLength;   } }

index.ts

import { NoteService } from './services/NoteService';  const noteService = new NoteService();  // Демонстрация работы с заметками function main() {   // Создание заметки   const newNote = noteService.createNote({     title: 'Первая заметка',     content: 'Это содержимое моей первой заметки'   });   console.log('Создана заметка:', newNote);    // Создание ещё одной заметки   const secondNote = noteService.createNote({     title: 'Вторая заметка',     content: 'Текст второй заметки'   });    // Получение всех заметок   console.log('Все заметки:', noteService.getAllNotes());    // Обновление заметки   const updatedNote = noteService.updateNote(newNote.id, {     content: 'Обновлённое содержимое первой заметки'   });   console.log('Обновлённая заметка:', updatedNote);    // Удаление заметки   const isDeleted = noteService.deleteNote(secondNote.id);   console.log('Вторая заметка удалена:', isDeleted);   console.log('Оставшиеся заметки:', noteService.getAllNotes()); }  main();

Попытка с UI

Следующим шагом был интерфейс. Я снова обратился к AI — и вот тут начались приключения.

Первые варианты кода не работали, пришлось просить исправления.

После пары итераций UI всё же завёлся…

но радость быстро сменилась разочарованием: кнопка «Добавить заметку» не работала.

Тут и начался настоящий вайбкодинг: много экспериментов, вопросов к AI, несколько зависаний «агентного режима» и даже пара ложных догадок от самой модели. В чате поддержки подсказали, что часть из этого — известные баги, которые скоро должны поправить.

Сгенерированные файлы

ui.ts

import { NoteService } from './services/NoteService.js'; import { Note } from './models/Note.js';  export class NotesApp {   private noteService: NoteService;   private noteTitleInput!: HTMLInputElement;   private noteContentInput!: HTMLTextAreaElement;   private addNoteButton!: HTMLButtonElement;   private notesList!: HTMLDivElement;    constructor() {     this.noteService = new NoteService();     this.initializeElements();     this.attachEventListeners();     this.renderNotes();   }    private initializeElements() {     this.noteTitleInput = document.getElementById('noteTitle') as HTMLInputElement;     this.noteContentInput = document.getElementById('noteContent') as HTMLTextAreaElement;     this.addNoteButton = document.getElementById('addNoteBtn') as HTMLButtonElement;     this.notesList = document.getElementById('notesList') as HTMLDivElement;   }    private attachEventListeners() {     this.addNoteButton.addEventListener('click', () => this.addNote());   }    private addNote() {     const title = this.noteTitleInput.value.trim();     const content = this.noteContentInput.value.trim();      if (!title || !content) {       alert('Пожалуйста, заполните все поля');       return;     }          this.noteService.createNote({ title, content });     this.noteTitleInput.value = '';     this.noteContentInput.value = '';     this.renderNotes();   }    private deleteNote(id: string) {     this.noteService.deleteNote(id);     this.renderNotes();   }    private formatDate(date: Date): string {     return new Date(date).toLocaleString('ru-RU');   }    private renderNotes() {     const notes = this.noteService.getAllNotes();     this.notesList.innerHTML = '';          if (notes.length === 0) {       this.notesList.innerHTML = '<p>Заметок нет</p>';       return;     }          notes.forEach(note => {       const noteElement = document.createElement('div');       noteElement.className = 'note';       noteElement.innerHTML = `         <h3>${this.escapeHtml(note.title)}</h3>         <p>${this.escapeHtml(note.content)}</p>         <small>Создано: ${this.formatDate(note.createdAt)} | Обновлено: ${this.formatDate(note.updatedAt)}</small>         <button class="delete-btn" data-id="${note.id}">Удалить</button>       `;       this.notesList.appendChild(noteElement);     });      // Добавляем обработчики для кнопок удаления     document.querySelectorAll('.delete-btn').forEach(button => {       button.addEventListener('click', (e) => {         const id = (e.target as HTMLButtonElement).getAttribute('data-id');         if (id) this.deleteNote(id);       });     });   }    // Метод для экранирования HTML, чтобы предотвратить XSS-атаки   private escapeHtml(text: string): string {     const map: Record<string, string> = {       '&': '&amp;',       '<': '&lt;',       '>': '&gt;',       '"': '&quot;',       "'": '&#039;'     };          return text.replace(/[&<>"']/g, (m) => map[m]);   } }  // Инициализируем приложение при загрузке DOM document.addEventListener('DOMContentLoaded', () => {   new NotesApp(); });

index.html

<!DOCTYPE html> <html lang="ru"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>Приложение заметок</title>   <link rel="stylesheet" href="styles.css"> </head> <body>   <div class="container">     <h1>Мои заметки</h1>          <div class="form-container">       <h2>Добавить новую заметку</h2>       <div class="form-group">         <label for="noteTitle">Заголовок:</label>         <input type="text" id="noteTitle" placeholder="Введите заголовок">       </div>       <div class="form-group">         <label for="noteContent">Содержимое:</label>         <textarea id="noteContent" placeholder="Введите содержимое заметки"></textarea>       </div>       <button id="addNoteBtn">Добавить заметку</button>     </div>          <div class="notes-container">       <h2>Список заметок</h2>       <div id="notesList"></div>     </div>   </div>      <script type="module" src="dist/ui.js"></script> </body> </html>

styles.css

body {   font-family: Arial, sans-serif;   line-height: 1.6;   margin: 0;   padding: 20px;   background-color: #f5f5f5;   color: #333; }  .container {   max-width: 800px;   margin: 0 auto;   background-color: #fff;   padding: 20px;   border-radius: 8px;   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); }  h1, h2 {   color: #2c3e50; }  .form-container {   margin-bottom: 30px;   padding: 15px;   background-color: #f9f9f9;   border-radius: 5px; }  .form-group {   margin-bottom: 15px; }  label {   display: block;   margin-bottom: 5px;   font-weight: bold; }  input[type="text"], textarea {   width: 100%;   padding: 8px;   border: 1px solid #ddd;   border-radius: 4px;   font-size: 14px; }  textarea {   height: 100px; }  button {   background-color: #3498db;   color: white;   border: none;   padding: 10px 15px;   border-radius: 4px;   cursor: pointer;   font-size: 14px; }  button:hover {   background-color: #2980b9; }  .note-item {   border: 1px solid #eee;   padding: 15px;   margin-bottom: 15px;   border-radius: 5px;   position: relative; }  .note-title {   font-size: 18px;   font-weight: bold;   margin-bottom: 10px; }  .note-content {   margin-bottom: 15px; }  .note-date {   font-size: 12px;   color: #777; }  .delete-btn {   position: absolute;   top: 10px;   right: 10px;   background-color: #e74c3c; }  .delete-btn:hover {   background-color: #c0392b; }

Истинная причина

В итоге ошибка оказалась не в логике приложения, а в конфигурации проекта.

Было:

{   "compilerOptions": {     "target": "ES2020",     "module": "commonjs",     "outDir": "./dist",     "rootDir": "./src",     "strict": true,     "esModuleInterop": true,     "skipLibCheck": true,     "forceConsistentCasingInFileNames": true   } }

Стало:

{   "compilerOptions": {     "target": "ES2020",     "module": "ES2020",     "outDir": "./dist",     "rootDir": "./src",     "strict": true,     "esModuleInterop": true,     "skipLibCheck": true,     "forceConsistentCasingInFileNames": true   } }

Как объяснил AI, CommonJS используется в Node.js и грузит модули синхронно через require, а ES2020 — это современная модульная система для браузеров, поддерживающая top‑level await. После этой правки всё заработало.

Логика работы кнопки «Удалить» и её расположение, конечно, потрясающее 🙂 Но уж исправлять я её не буду. По крайне мере не в этой статье.

Итоги вайбкодинга

  1. Доступные и бесплатные инструменты для помощи в разработке есть уже сейчас

  2. Качество генерации кода скорее радует, чем разочаровывает

  3. Но вот поиск и объяснение ошибок «не в коде, а рядом с ним» у AI пока что вызывает настоящий отвал башки

Делитесь своим опытом работы с AI‑инструментами в комментариях, будет интересно почитать 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/944734/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *