javascript

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

  • понедельник, 8 сентября 2025 г. в 00:00:04
https://habr.com/ru/articles/944734/

Как и многие начинающие разработчики, я давно мечтал сделать свой первый 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‑инструментами в комментариях, будет интересно почитать :-)