Как и многие начинающие разработчики, я давно мечтал сделать свой первый 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> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; 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. После этой правки всё заработало.

Логика работы кнопки «Удалить» и её расположение, конечно, потрясающее 🙂 Но уж исправлять я её не буду. По крайне мере не в этой статье.
Итоги вайбкодинга
-
Доступные и бесплатные инструменты для помощи в разработке есть уже сейчас
-
Качество генерации кода скорее радует, чем разочаровывает
-
Но вот поиск и объяснение ошибок «не в коде, а рядом с ним» у AI пока что вызывает настоящий отвал башки
Делитесь своим опытом работы с AI‑инструментами в комментариях, будет интересно почитать 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/944734/
Добавить комментарий