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

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


  1. dan_sw
    07.09.2025 13:32

    В этот раз я решил пойти по трендам и подключить в процесс AI

    Это скорее минус, чем плюс (учитывая, что в программировании Вы новичок). С LLM-агентами программированием занимаются именно LLM-агенты, а не оператор :) Так гораздо сложнее научиться важным концепциям программирования, которые послужат фундаментом для дальнейшей карьеры программиста, просто потому что соблазн перекинуть всю интеллектуальную нагрузку на LLM-агентов велика.

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

    Ключевое предложение тут "не копаясь вручную". Сейчас популяризируется мысль о том, что копание в программировании "вручную" (в т.ч. IDE, редакторах кода, инструментах разработки) - это уже устарело, да и не эффективно. Гораздо ведь проще переложить эту задачу на LLM, верно?

    Такие моменты, на самом деле, и делают разработчика разработчиком. Если Вы новичок, то без самостоятельного разбирательства в том, как лучше обустраивать файловую структуру проекта, какие конфигурационные файлы в проекте используются (и как они используются) и т.д. - дальше новичка не уйти. Вдруг окажетесь в ситуации, когда кроме Vim'a или максимум VSCode (без LLM-агентов) ничего не будет, что тогда? Правильно, не сможете разобраться с чего начать.

    Профессиональный программист (если он именно профессионал, эксперт, хакер) умеет развернуть проект в рамках своего стэка с нуля - будь то с использованием VS Code или Vim, без LLM-агентов. И лучше этому учиться сразу, без помощи LLM. В долгосрочной перспективе это даст свои плоды. В краткосрочной - LLM конечно всё это ускоряет, однако экспертом с LLM не стать. Для этого нужно самостоятельно преодолеть определённый путь (и он не лёгкий).

    В чате поддержки подсказали, что часть из этого — известные баги, которые скоро должны поправить.

    В целом, статья выглядит как просто реклама Kode, а это сообщение только это подчёркивает. Думаю моя догадка верна, но для читающих новичков, кои по своей неосторожности решились также начать программировать с "вайб-кодинга", думаю мой комментарий будет полезен. Сделаю вид, что статья не рекламная :)

    Но уж исправлять я её не буду.

    А стоит, если хотите научиться программировать :)

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

    Вывод: становитесь экспертом в программировании и не уповайте на "AI" :)

    Вообще, Вы уже сейчас можете внести свой вклад в данный проект (чтобы он уж совсем не был LLM написан). Например, вот эта строчка может быть доработана (и дальнейшие с ней связки):

    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;
      }

    Тут нет обработки ситуаций, когда document.getElementById не нашёл элемент. В случае, если он не найдёт элемент он просто ничего не вернёт (кроме null), а его и приводить к конкретному типу HTML-элементов нет смысла. Это можно (и нужно) доработать. И дальнейшие "связки" тоже требуют доработки. Вот эта например:

    private attachEventListeners() {
        this.addNoteButton.addEventListener('click', () => this.addNote());
    }

    Тут нет проверки на то, что this.addNoteButton вообще существует. Он может быть равен null (или undefined), и тогда вызов метода addEventListener вызовет ошибку (ведь null или undefined не может иметь методов). Хотя бы оператор ? использовать тут было бы хорошим тоном:

    this.addNoteButton?.addEventListener(...)

    Да и зачем нужен этот самый оператор ? - это уже тема для разбора новичком :)

    Такими вот подобными ошибками код пестрит, но в тоже время является неплохой возможностью для саморазвития как программиста :)

    LLM/IDE with agents - являются неплохим инструментов для уже опытного разработчика, который повидал в своей карьере всякое. Для начинающего же (Junior-, Junior, Junior+) это скорее лишний отвлекающий фактор, чем что-то полезное. LLM лучше использовать не для программирования, а в качестве источника информации о программировании. Например, у него можно поинтересоваться какие проекты можно сделать и делать их потихоньку, запрашивать у него информацию по каким-либо концепциям и их реализовывать самостоятельно и периодически руководствоваться примерами программного кода на какую-либо тему. Это реально может помочь в быстром освоении каких-либо концепций из программирования.


  1. Dhwtj
    07.09.2025 13:32

    Пару лет назад пробовал TS

    Примерно такие же проблемы. То есть на эти грабли вы бы вероятно наткнулись всё равно, LLM тут не виновата, просто не помогла