【AI】CursorのAI機能を使ってTODOアプリを作ってみます

チュートリアル

はじめに

2022年にOpenAI社がChatGPTを公開したことにより、昨今では「生成AI」という分野が急速に発展しています。生成AIの発展により、自然言語処理の他にも画像や音声、動画など、さまざまなコンテンツを自動生成するサービスが多くの企業から公開されています。

Cursorというコードエディタもその一つで、生成AI発展の先駆けとなったChatGPTをエディタ上で扱うことができます。Cursorは開発を行うためのAIアシスタント機能を提供しており、この機能を利用することで従来とは違った開発体験を得ることができます。

クリエイティブな活動においては生成AIを活用することで生産性の向上が期待できます。しかし、生成AIの普及によりエンジニアやデザイナーの仕事が奪われるという見解をしている方もいるでしょう。

この記事では、なるべく既存の知識で判断せずにCursorのAIアシスタント機能を積極的に利用して、簡単なTodoアプリを作成してみます。ChatGPTは同じ質問に必ず同じ回答を行うとは限らないので再現性が低いことにご留意ください。

Cursorを使っての開発は私もこの記事で作るTodoアプリが初めてなので、Cursorでの開発体験を得て、Cursorが普及した場合に「エンジニアがAIに仕事を奪われる可能性があるか」について考えてみます。

この記事の対象者

  • ウェブ制作初学者の方
  • Cursorの利用を検討している方
  • 生成AIを脅威に感じている方
  • 開発にAIを活用したいと考えている方
  • 非エンジニアだけどウェブ開発をしてみたい方

プロジェクトの作成

Cursorの強力な機能として、プロジェクトの作成時に自動でコードを生成してもらうことができます。この機能を利用してプロジェクトを作成し、AIに適宜修正を指示してTodoアプリを完成させることを目指します。

前提として、質問にはプロンプトエンジニアリングのような手法は用いず、なるべく自然な表現でやりとりを行っていきます。また、自分ではコードを直接変更せずにAIの指示に従って作成していきます。

Todoアプリの仕様

当サイトはウェブ制作初学者の方向けのメディアサイトなので、なるべく趣旨に反しないように以下の仕様でシンプルなTodoアプリを作ります。

  • 言語:HTML、CSS、JavaScript
  • 機能:タスクの追加・削除、タスクの完了
  • データ:ローカルストレージに保存

TodoアプリをAIに作ってもらう

プロジェクトの作成時に指示を出すことでコードを自動で生成してくれると述べました。この機能を利用するにはChatGPTのバージョンを「GPT-4」に変更する必要があるようなので、まずはこれを行います。

続いて、メニューバーの「ファイル」をクリックします。表示言語の設定を日本語にしている場合は「ファイルを開く…」の項目が二つあるかと思います。下部の方は英語だと「New AI Project…」と表示されるようですが、日本語ではなぜか「ファイルを開く…」と表示されていて分かりづらいです。ここでは下部の「ファイルを開く…」を選択します。

以下のような画面が表示されるので、ここに作成するプロジェクトの指示を記述します。

ここでは以下のような指示を出してみます。プロンプトのクオリティとしてはいまいちですが、とりあえず「Next」をクリックして先に進みます。

基本的なHTML、CSS、JavaScriptでシンプルなTodoアプリを作成します。機能はタスクの追加・編集・削除ができるように実装します。また、各タスクは初期のステータスとして「未完了」の状態を持っており、ユーザーの操作によって「完了」に変更することができるように実装します。

続いて以下のような画面が表示されます。

「parent Folder」の欄ではどこにプロジェクトのディレクトリを作成するかを選択し、「Project Name」の欄でプロジェクトのディレクトリ名を決めます。ここでは「simple-todo」としておきます。「Done」をクリックして先に進むと、プロジェクトがCurosor上で開いて以下のような画面が表示されました。

「Step 0 Computing project structure」にはこのプロジェクトの説明が書かれていきます。この内容は英語で出力されるようなので、今後のアップデートで日本語に対応してもらえるよう期待したいところです。

ちなみに、Google翻訳に投げると以下のように翻訳されました。

プロジェクトの説明に基づいて、HTML、CSS、JavaScript を使用して単純な Todo アプリケーションを作成します。データはローカル ストレージに保存されます。アプリケーションにはタスクを追加、編集、削除する機能があります。各タスクの初期ステータスは「未完了」ですが、ユーザーがこれを「完了」に変更できます。

ファイルが必要になる理由は次のとおりです。

1. `index.html`: これは、アプリケーションの構造を含むメインの HTML ファイルです。これには、CSS および JavaScript ファイルへのリンクが含まれます。

2. `styles.css`: これは、アプリケーションのすべてのスタイルを含む CSS ファイルです。 HTMLファイルからのリンクとなります。

3. `app.js`: これは、アプリケーションのロジックを含むメインの JavaScript ファイルです。タスクの追加、編集、削除、タスクのステータスの変更などのタスクを処理します。また、ローカル ストレージへのデータの保存と取得も処理します。

ここで、次のファイル名を使用して関数を呼び出してみましょう。 “`JavaScript 関数.outputFileNames({ ファイル名: [“index.html”、”styles.css”、”app.js”] });

プロジェクトの作成時に渡した指示を理解してくれていることが確認できました。しばらく待機していると次々とコードが生成されていきます。コードの生成は複数のステップに分けて行われ、ステップ毎にGPT-4のリクエスト数が消費されるようです。ちなみに、今回の生成では7回のリクエストが行われました。

最終的に以下のようなコードを出力してくれました。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todoアプリ</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <h1>シンプルなTodoアプリ</h1>
    <form id="task-form">
        <input type="text" id="task-input" placeholder="新しいタスクを追加">
        <button type="submit">追加</button>
    </form>
    <ul id="task-list">
        <!-- Tasks will be added here dynamically -->
    </ul>
    <script src="app.js"></script>
</body>
</html>
styles.css
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f4f4f4;
}

h1 {
    text-align: center;
    padding: 20px;
    background-color: #333;
    color: #fff;
}

#task-form {
    display: flex;
    justify-content: center;
    margin-bottom: 20px;
}

#task-input {
    flex: 1;
    height: 40px;
    padding: 0 20px;
    font-size: 18px;
}

#task-form button {
    height: 40px;
    padding: 0 20px;
    font-size: 18px;
    background-color: #333;
    color: #fff;
    border: none;
    cursor: pointer;
}

#task-list {
    width: 80%;
    margin: 0 auto;
    list-style: none;
}

#task-list li {
    background-color: #fff;
    padding: 20px;
    margin-bottom: 10px;
    border-radius: 4px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

#task-list li.completed {
    text-decoration: line-through;
    color: #ccc;
}

app.js
// Select elements
const taskForm = document.getElementById('task-form');
const taskInput = document.getElementById('task-input');
const taskList = document.getElementById('task-list');

// Load all event listeners
loadEventListeners();

function loadEventListeners() {
    // DOM Load event
    document.addEventListener('DOMContentLoaded', getTasks);
    // Add task event
    taskForm.addEventListener('submit', addTask);
    // Remove task event
    taskList.addEventListener('click', removeTask);
    // Mark task as completed event
    taskList.addEventListener('click', markCompleted);
}

// Get tasks from local storage
function getTasks() {
    let tasks;
    if(localStorage.getItem('tasks') === null) {
        tasks = [];
    } else {
        tasks = JSON.parse(localStorage.getItem('tasks'));
    }

    tasks.forEach(function(task) {
        // Create li element
        const li = document.createElement('li');
        // Add class
        li.className = task.isCompleted ? 'completed' : '';
        // Create text node and append to li
        li.appendChild(document.createTextNode(task.title));
        // Create new delete button element
        const deleteBtn = document.createElement('button');
        // Add classes to delete button
        deleteBtn.className = 'delete';
        // Add event listener to delete button
        deleteBtn.addEventListener('click', removeTask);
        // Append the button to li
        li.appendChild(deleteBtn);
        // Append li to ul
        taskList.appendChild(li);
    });
}

// Add task
function addTask(e) {
    if(taskInput.value === '') {
        alert('タスクを追加してください');
    } else {
        // Create li element
        const li = document.createElement('li');
        // Create text node and append to li
        li.appendChild(document.createTextNode(taskInput.value));
        // Create new delete button element
        const deleteBtn = document.createElement('button');
        // Add classes to delete button
        deleteBtn.className = 'delete';
        // Add event listener to delete button
        deleteBtn.addEventListener('click', removeTask);
        // Append the button to li
        li.appendChild(deleteBtn);
        // Append li to ul
        taskList.appendChild(li);

        // Store in local storage
        storeTaskInLocalStorage(taskInput.value);

        // Clear input
        taskInput.value = '';
    }

    e.preventDefault();
}

// Store task
function storeTaskInLocalStorage(task) {
    let tasks;
    if(localStorage.getItem('tasks') === null) {
        tasks = [];
    } else {
        tasks = JSON.parse(localStorage.getItem('tasks'));
    }

    tasks.push({ title: task, isCompleted: false });

    localStorage.setItem('tasks', JSON.stringify(tasks));
}

// Remove task
function removeTask(e) {
    if(e.target.classList.contains('delete')) {
        if(confirm('タスクを削除しますか?')) {
            e.target.parentElement.remove();

            // Remove from local storage
            removeTaskFromLocalStorage(e.target.parentElement);
        }
    }
}

// Remove from local storage
function removeTaskFromLocalStorage(taskItem) {
    let tasks;
    if(localStorage.getItem('tasks') === null) {
        tasks = [];
    } else {
        tasks = JSON.parse(localStorage.getItem('tasks'));
    }

    tasks.forEach(function(task, index) {
        if(taskItem.textContent === task.title) {
            tasks.splice(index, 1);
        }
    });

    localStorage.setItem('tasks', JSON.stringify(tasks));
}

// Mark task as completed
function markCompleted(e) {
    if(e.target.tagName === 'LI') {
        e.target.classList.toggle('completed');

        // Update task in local storage
        updateTaskInLocalStorage(e.target);
    }
}

// Update task in local storage
function updateTaskInLocalStorage(taskItem) {
    let tasks;
    if(localStorage.getItem('tasks') === null) {
        tasks = [];
    } else {
        tasks = JSON.parse(localStorage.getItem('tasks'));
    }

    tasks.forEach(function(task) {
        if(taskItem.textContent === task.title) {
            task.isCompleted = !task.isCompleted;
        }
    });

    localStorage.setItem('tasks', JSON.stringify(tasks));
}

app.jsのコメントも日本語に翻訳しておいてもらえると嬉しいですが、十分強力な機能ですね。修正したい箇所もいろいろありますが好みにもよるので気にせず進みます。

続いて、このTodoアプリが意図したとおりに正しく動いてくれるかを確認していき、AIに適宜修正を求めます。

コードを修正してもらう

自動生成されたindex.htmlをブラウザで見てみると以下のように表示されました。

実にシンプルなTodoアプリです。このアプリを使ってみて問題を洗い出すと、以下のような不具合がありました。

  • タスクの入力欄と追加ボタンの高さが違うのが気になる
  • 削除ボタンにボタンテキストが無い
  • 削除ボタンを押すと確認のアラートが二回表示される

ユーザビリティを考えると上記以外にも修正してもらいたいところはいくつかありますが、記事がすごく長くなりそうなので控えておきます。ここでは上記の3つの問題について修正を行っていきます。

入力欄と追加ボタンの高さを揃える

入力欄と追加ボタンの高さが若干ずれているのでこれを修正してもらいます。Codebase機能を利用してプロジェクトフォルダを読み込み、修正案を提案してもらいます。ちなみに、ChatGPTのバージョンはGPT-4を使用しています(GPT-3.5ではまともな回答が得られませんでした)。

以下のような回答が返ってきました。

修正方法を具体的に教えてくれました。heightプロパティの値は既に統一されているので、各セレクタにbox-sizingプロパティのみ追加します。これで入力欄と追加ボタンの高さが揃いました。

削除ボタンにボタンテキストを表示する

いくつかタスクを追加してみると、タスクの右側にボタンのようなものが表示されています。

このボタンが何のためのものかはクリックするまでわかりません。クリックすると以下のようなアラートが表示されました。

このボタンが削除ボタンであることが分かりました。しかしボタンテキストが無いのは明らかな不具合なので修正してもらいます。

あえて「ボタンの機能が分からない」という振る舞いで質問しましたが、削除ボタンを指していることを正しく認識してくれたようです。提案のとおりにコードを修正してタスクを追加し直すと意図したとおりに削除ボタンが表示されました。

しかし、ページをリロードすると再び削除ボタンのテキストが消えてしまいます。

これも修正してもらう必要があります。以下のように質問をしました。

提案された修正案は以下です。

何も考えずにgetTasks()関数の内容をコピペして修正すると問題が解決しました。

タスクの削除機能が壊れたので修正してもらう

削除ボタンのテキストの問題は解決しましたが、この時点で「タスクが削除されない」という新たな不具合が発生しました。具体的には、タスクの削除を行うと削除したタスクは一旦消えますが、ページをリロードすると再び表示されます。これは初期の状態では発生しなかったので不可解です。とりあえず修正を依頼します。

以下のような回答が返ってきました。

さも当然のように間違ったことをおっしゃるのもChatGPTさんの可愛げです。現状のコードと同じ内容なので、一応コピペしましたが当然解決されません。再度修正を依頼します。

以下のような回答が返ってきました。

これでも解決しませんでした。恐らくは問題を解決するための情報をAIが持っていないのだと思われます。@Symbols機能でapp.jsファイルを読み込ませてから再度修正案をいただきます。

すると、今度はremoveTaskFromLocalStorage()関数の修正だけでなくremoveTask()関数の修正も必要だと言われました。

適用すると正常にタスクの削除を行うことができました。

「完了」ステータスを保持できなくなったので修正してもらう

このアプリのプロジェクトを作成する際、各タスクはステータスとして「完了」と「未完了」の状態を持っており、初期状態は「未完了」であることを指示しました。

ステータスを「完了」にするには、タスクのタイトルをクリックすることで変更できるようです。完了状態のタスクは文字が薄くなって打ち消し線が表示されています。

デザイン的に分かりづらいので修正したいところですが長くなるのでこのまま進めます。

仕様上の問題として、ページをリロードすると「完了」にしたはずのタスクのステータスが「未完了」に戻ってしまっています。初期状態では問題なかったはずなので、ここまでで行った変更が影響しているようです。これの修正を依頼します。

以下のような回答が返ってきました。

出力されたコードは元のgetTask()関数のコードと何ら変わりはないようなので解決にはいたりませんでした。温かい気持ちでもう一度修正案を提案してもらいます。

回答は以下です。

素直に自分の非を認められるとは素晴らしい対人スキルです。モヤモヤやイライラがすっとびます。上記の修正案を反映させると、無事に完了ステータスが保持されるようになりました。

このように、ある不具合の修正によって新たな問題が発生するようなことは開発において日常茶飯事ですが、AIと簡単なやりとりを繰り返すことで新たな問題を解決することができました。

アラートが二回表示される不具合を修正してもらう

現状では、削除ボタンをクリックすると確認アラートが二回表示されるという不具合があります。こちらについて修正してもらいます。

以下のような回答が返ってきました。具体的に解説してくれていますが、非エンジニアでも開発できるかどうかという検証を含めているので深く考えずにコピペします。

とりあえずは解決できたような挙動になりましたが、ページをリロードした際の削除ボタンのテキストが消えてしまいました。ここではgetTasks()関数のコードを選択して「Ctrl + K」で直接コードを修正してもらいます。

「Ctrl + Shift + Y」を押して変更を適用します。これで再び削除ボタンのテキストが表示されました。

アラートが二回表示される不具合が再発

再度このアプリのテストを行っていると、アラートが二回表示される現象が再び発生していることに気が付きました。どうやらページをリロードした場合は不具合が解決するようですが、リロード前に追加したタスクを削除しようとするとそのタスクを削除しようとした場合のみにアラートが二回表示されるようです。

app.jsをもう一度読み込んでもらったほうが確実な気がするので、@Symbols機能を使ってから修正を依頼します。

回答は以下です。

アラートが二回表示される不具合が解決しました。最低限の機能ですがこれでTodoアプリとして使うことができるかと思います。

エンジニアの仕事を奪う可能性

CursorのAI機能を駆使して簡単なTodoアプリを作ってみました。さらにAIとやりとりを行うことで機能の追加やデザインの修正なども行うことができるかと思います。

今回の開発体験から、非エンジニアの方でもAIと上手くやり取りをすることで、簡単なものであればアプリ開発を行うことができるという印象は確かにあります。

しかし、そのためにはエンジニアの知識を持つ方と比べて多くのAIとのやりとりが必要になることが考えられます。非エンジニアの方がCursorを使ってアプリ開発を行うには、プロンプトエンジニアリングの技術習得、DocsやCodebase、@SymbolsなどのCursorの機能について熟知する必要があるかも知れません。また、動作確認のテストにとても時間が掛かる可能性もあります。

生成AIの技術は目まぐるしい速度で進歩しているので「エンジニアがAIに仕事を奪われる可能性」については安易に見解を述べることはできませんが、Cursorを使ってみた感想として、Cursorはコードエディタという性質上「エンジニア向けのツールである」というところは揺るがないかと思います。エンジニアとしての知識を持つ方であれば、AIによって自動で生成されたコードが目的に沿うものであるかなどの判断ができ、その修正によってどのような問題が発生する可能性があるかまでを考えることができます。

前述のとおり、ChatGPTは当然のように間違ったコードを提案することもあるため、これを見極めるには非エンジニアの方には少し難しいかと思います。しかし、非エンジニアの方がCursorを使って開発体験を得ることは、非エンジニアからエンジニアになるための最初の一歩としてとても効率的な学習方法だと感じました。

したがって、Cursorによってエンジニアの仕事が奪われるようなことはなく、エンジニアにとっての開発の効率化や学習に貢献してくれるツールであるというのが結論です。

最後に

この記事では、なるべく既存の知識で判断せずにCursorのAIアシスタント機能を積極的に利用して簡単なTodoアプリを作成してみました。

自動生成されたコードについてはリファクタリングをする必要性があるかもしれませんが、Cursorの利用は開発効率の向上に大きく貢献するものだと感じました。また、Cursorをプログラミングの学習に利用することで、学習の初期段階で実際に動作するものが作れるということも大きなメリットを感じます。これは学習のモチベーションを維持するためにとても効果的なアプローチです。

今回のTodoアプリ開発ではGPT-4を利用しましたが、無料で利用できるBasicプランではGPT-4のリクエスト回数は50回という制限があり、この回数は恐らく月をまたいでも回復しないため、継続してGPT-4を利用するにはプランをアップグレードする必要があります。

ChatGPT-3.5であれば毎月200回のリクエストを行うことができますが、精度はGPT-4と比べるとかなり劣るという印象です。積極的にCursorを利用したいと考えるのであれば、GPT-4を無制限で使用できる(fastは制限あり)ProプランやBusinessプランへのアップグレードを検討してみても良いかと思います。

タイトルとURLをコピーしました