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

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


  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. BORIS-288X
      07.09.2025 13:32

      присоединяюсь


    1. slada
      07.09.2025 13:32

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


    1. fahitos44 Автор
      07.09.2025 13:32

      Спасибо за развернутый комментарий, будем пробовать)


    1. dan_sw
      07.09.2025 13:32

      По поводу программного кода (чтобы никого не запутать): я не обратил внимание на то, что в TypeScript класс NotesApp определён с атрибутами, в которых встречается Non-Null Assertion Operator (!):

      private noteService: NoteService;
      private noteTitleInput!: HTMLInputElement;
      private noteContentInput!: HTMLTextAreaElement;
      private addNoteButton!: HTMLButtonElement;
      private notesList!: HTMLDivElement;

      И при выполнении следующего программного кода:

      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, то приведение к HTMLInputElement (и другим типам элементов) приведёт к ошибке (будет вызвано исключение):

      Conversion of type 'null' to type 'HTMLInputElement' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

      Мои замечания были бы справедливы для JavaScript, в TypeScript с проверкой на null / undefined всегда всё +/- ясно (разумеется, если не использовать ts-ignore).

      Да и при таком раскладе:

      private noteService: NoteService;
      private noteTitleInput?: HTMLInputElement;
      private noteContentInput?: HTMLTextAreaElement;
      private addNoteButton?: HTMLButtonElement;
      private notesList?: HTMLDivElement;

      TypeScript обычно просит какие либо функции и свойства у атрибутов с ? вызывать через ?, как тут например:

      this.addNoteButton?.addEventListener(...)

      Однако для надёжности ПО такие вот конструкции: document.getElementById('noteTitle'), следует тщательно проверять и преждевременно не приводить к типам HTML-элементам, т.к. они могут быть равны null и в таком случае могут возникнуть исключительные ситуации (ошибки).


  1. Dhwtj
    07.09.2025 13:32

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

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


    1. fahitos44 Автор
      07.09.2025 13:32

      В итоге сейчас продолжаете писать на JS?


  1. Vedomir
    07.09.2025 13:32

    Статья выглядит откровенной рекламой - но если это вдруг реальный человек писал, то вопрос - какие именно навыки и знания вы покажете на собеседовании с таким пет-проектом? Навыки просить нейросеть сделать работу за вас? А они точно ценные?