Пет-проект с AI-помощником: мой первый опыт вайбкодинга
- понедельник, 8 сентября 2025 г. в 00:00:04
Как и многие начинающие разработчики, я давно мечтал сделать свой первый pet‑проект — чтобы почувствовать себя «настоящим программистом» и перестать бояться собеседований. В итоге решился: буду писать веб‑приложение для личных заметок.
На самом деле я не совсем новичок. Раньше у меня уже были попытки освоить разные языки программирования, но дальше пары строчек кода дело редко заходило. Умение «гуглить правильно» и искать ответы на StackOverflow пока давалось тяжело, поэтому довести что‑то до результата было сложно.
В этот раз я решил пойти по трендам и подключить в процесс AI. Спасибо Хабру, что в нужный момент подкинул статью про Koda. С Koda и начался мой эксперимент. А команде хочу выразить респект за то, что всё бесплатно.
Идея была простой: сделать приложение для заметок, где текст можно красиво отображать на UI. Для разработки выбрал TypeScript: с его синтаксисом я уже немного знаком.
Опыт с 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();
Следующим шагом был интерфейс. Я снова обратился к 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‑инструментами в комментариях, будет интересно почитать :-)