コードを一切書かずにGUIアプリ完成!バイブコーディングで作るJupyterノートブック比較ツール

AI,プログラミング

はじめに

仕事がら、学生が提出してくるソースコードをチェックすることがよくあるのですが、やはりプログラムを目視でチェックするのは大変です。特に最近はJupyter Notebook(ipynb)ファイルを確認することが多くて、それを少しでも楽にしたくfletで「2つのipynbファイルのコードセルだけを比較できるGUIツール」を作りました。

ただ、正確には私自身は一切実装をしていません。最近流行りのバイブコーディングを試し、全てをAIに任せる形で取り組んでみました。結果、個人で使用する分には十分実用に耐えられるレベルのアプリが完成しましたので、その際のつまずきや指示の工夫について紹介します。

対象読者

  • バイブコーディングでどこまでできるのか興味のある方
  • 「アプリを作ってみたいけど、実装が難しそう」と感じている方
  • 「プログラミングに、もっと生成AIを活用したい」と感じている方

バイブコーディング(Vibe Coding)とは?

バイブコーディングでは、人間は要件や画面イメージをAIに伝え、AIにプログラムを作成・修正させます。これまではAIに部分的なコーディングを支援してもらうレベルでしたが、最近は逆にほとんどをAIに任せ、人間はプログラムを確認するだけになってきています。

開発者はやりたいことを自然言語でAIに伝えるだけなので、プログラミングに精通していなくても実践できますし、むしろ実装内容の分からない部分をAIに教えてもらうことで勉強にもなります。

作成したアプリの概要

2つのipynbファイルを選ぶと、各ファイルのコードセルのみ抽出して左右に並べて表示します。左右の内容が同じならば○、違うならば×と表示します。

アプリ画面

アプリの主な機能と特徴

  • 2つのJupyter Notebookファイル(.ipynb)を選択する
  • 選択したファイルからコードセルのみを抽出する
  • 抽出したコードセルを左右に並べて表示する
  • 左右のコードセルの内容を比較し、同じか違うかを判定する
  • fletを使用したPythonによるGUIアプリ
  • MVVMパターン

アプリ開発の流れ

今回はCursorエディタを使用して開発しています。

2つのipynbファイルを表示するプログラム作成を指示

最初は以下のような指示を出しました。

以下のfletアプリのプログラムを作成してください。
画面
・2列のグリッド表示がある
動作
・2つのipynbを読み込む
・1つめのipynbのコードセルのみを、グリッドの1列目に順に表示する
・2つめのipynbのコードセルのみを、グリッドの2列目に順に表示する

生成されたのは以下のコードです。

import flet as ft
import nbformat
import os
from typing import List, Optional

def main(page: ft.Page):
    page.title = "IPYNBコードセル比較"
    page.padding = 20
    
    def load_code_cells(file_path: str) -> List[str]:
        """ipynbファイルからコードセルを抽出する"""
        if not os.path.exists(file_path):
            return []
            
        with open(file_path, 'r', encoding='utf-8') as f:
            nb = nbformat.read(f, as_version=4)
            
        code_cells = []
        for cell in nb.cells:
            if cell.cell_type == 'code':
                code_cells.append(cell.source)
                
        return code_cells

    def update_display():
        """両方のノートブックのコードセルを表示する"""
        # 左側のグリッドをクリア
        left_column.controls.clear()
        for code in left_code_cells:
            left_column.controls.append(
                ft.Container(
                    content=ft.TextField(
                        value=code,
                        multiline=True,
                        read_only=True,
                        min_lines=3,
                        max_lines=10,
                    ),
                    padding=10,
                    border=ft.border.all(1, ft.colors.GREY_400),
                    border_radius=5,
                )
            )

        # 右側のグリッドをクリア
        right_column.controls.clear()
        for code in right_code_cells:
            right_column.controls.append(
                ft.Container(
                    content=ft.TextField(
                        value=code,
                        multiline=True,
                        read_only=True,
                        min_lines=3,
                        max_lines=10,
                    ),
                    padding=10,
                    border=ft.border.all(1, ft.colors.GREY_400),
                    border_radius=5,
                )
            )
        page.update()

    def pick_files_result(e: ft.FilePickerResultEvent):
        """ファイル選択後の処理"""
        if e.files:
            file_path = e.files[0].path
            if left_file_picker.data == "left":
                global left_code_cells
                left_code_cells = load_code_cells(file_path)
                left_file_text.value = os.path.basename(file_path)
            else:
                global right_code_cells
                right_code_cells = load_code_cells(file_path)
                right_file_text.value = os.path.basename(file_path)
            
            update_display()
            page.update()

    # ファイル選択用のPicker
    file_picker = ft.FilePicker(on_result=pick_files_result)
    page.overlay.append(file_picker)

    # 左側のファイル選択ボタンとファイル名表示
    left_file_picker = ft.ElevatedButton(
        "左側のノートブックを選択",
        icon=ft.icons.UPLOAD_FILE,
        on_click=lambda _: pick_file("left")
    )
    left_file_text = ft.Text()
    
    # 右側のファイル選択ボタンとファイル名表示
    right_file_picker = ft.ElevatedButton(
        "右側のノートブックを選択",
        icon=ft.icons.UPLOAD_FILE,
        on_click=lambda _: pick_file("right")
    )
    right_file_text = ft.Text()

    def pick_file(side: str):
        """ファイル選択ダイアログを表示"""
        file_picker.data = side
        file_picker.pick_files(
            allow_multiple=False,
            allowed_extensions=["ipynb"]
        )

    # コードセルを格納するリスト
    left_code_cells: List[str] = []
    right_code_cells: List[str] = []

    # 2列のグリッドレイアウト
    left_column = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)
    right_column = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)

    # メインレイアウト
    page.add(
        ft.Row([
            ft.Column([left_file_picker, left_file_text], expand=True),
            ft.Column([right_file_picker, right_file_text], expand=True),
        ]),
        ft.Row([
            left_column,
            right_column,
        ], expand=True),
    )

ft.app(target=main)

実行すると以下の画面が表示されました。

初めの指示で生成されたアプリの画面

前述のようなざっくりとした指示でも、それっぽいGUIのアプリが作成されました。

ただ、実際に操作してみると以下のような問題がありました。

  • 片方のファイル選択ボタンが動作しない
  • ipynbファイルのコードが画面に表示されない

うまく動作していない内容を1つずつ伝えて修正を試みましたが、グリッドが表示されない状況が続き、一旦、いきなり2つのファイルのコードをセル表示するのを目指すのはあきらめることにしました。

グリッドを表示するプログラム作成を指示

元の要求のままでのプログラム修正はあきらめ、一度、別のソースファイルで単純にグリッドを表示するだけの指示を出しました。

"test.py"に1列3行のグリッドを表示してください。各行に"1","2","3"を順に表示してください。
import flet as ft

def main(page: ft.Page):
    page.title = "テストグリッド"
    page.padding = 20
    page.window_width = 400
    page.window_height = 600

    # 1列のグリッドレイアウト
    column = ft.Column(
        controls=[
            ft.Container(
                content=ft.Text("1", size=20, color=ft.colors.WHITE),
                bgcolor=ft.colors.BLACK,
                padding=10,
                border_radius=5,
                width=200,
                height=50,
                alignment=ft.alignment.center,
            ),
            ft.Container(
                content=ft.Text("2", size=20, color=ft.colors.WHITE),
                bgcolor=ft.colors.BLACK,
                padding=10,
                border_radius=5,
                width=200,
                height=50,
                alignment=ft.alignment.center,
            ),
            ft.Container(
                content=ft.Text("3", size=20, color=ft.colors.WHITE),
                bgcolor=ft.colors.BLACK,
                padding=10,
                border_radius=5,
                width=200,
                height=50,
                alignment=ft.alignment.center,
            ),
        ],
        spacing=10,
        alignment=ft.MainAxisAlignment.START,
    )

    # メインレイアウト
    page.add(column)

ft.app(target=main)

うまくグリッド表示できるプログラムを作成できることが確認できました。

1つのipynbファイルを表示するプログラム作成を指示、拡張

グリッド表示するプログラムを作成できることは分かり、ipynbのコードセルの取り出し部分のプログラムは大丈夫そうだったので、次は1つのipynbでコードセルをグリッド表示させることを試してみました。

"main.py"を、ipynbを一つ指定してコードセルのみ表示するように修正してください

これでも最初はグリッドが表示されませんでしたが、うまく動作していないことについて何度かやり取りをすると、今度はきちんとコードセルをグリッド表示できる状態になりました。

その後、以下のように順を追って拡張させていきました。こちらも、指示した内容がきちんと動作するかを確認できたら次の内容に移る、というようにしました。

  • 2つのipynbを表示できるようにする
  • 2つのipynbのコードセルが左右で同期して表示されるようにする
  • 2つのipynbのコードセルを比較して、同じか違うか判定して表示する

MVVM、UI改善を指示

これで最初に意図していた機能を実現できたので、MVVMパターンの適用と細かなUI改善を指示し、一旦は完成としました。MVVMパターンを適用することでファイル構成などが一気に変わりましたが、問題なく元の動作のままで修正されました。

完成版のプログラムは長いので、githubを参照してください。

うまくいった指示のポイント

AIとのバイブコーディングで効果があったのではないかと考えるポイントを以下に挙げておきます。

1. 一気に作らず、徐々に機能拡張・修正

複雑なものを一度に指示せず、段階的に完成させ拡張・修正していく
今回のケースでは、まずは1つのipynbでコードセルを表示可能な状態にするのを目指したことで、当初うまくいかなかった2つのファイルのコードセルの表示が解決できました。修正を指示してもなかなかうまくいかない場合は、少しステップをばらしてあげるのが良さそうです。

2. 単機能を別のファイルに実装させてみる

本体とは別のソースコードに単純な機能を実装させて、自分の指示でAIに実装させられるのかを確認する。実現できるのであれば本体のプログラムで作らせる。
今回のケースでは、グリッド表示する機能だけを別のソースコードに実装させ、きちんと実現できることが分かったので、本体のプログラムに反映するようにしました。

アーキテクチャの指定

不具合が出てもピンポイントで修正指示を出しやすくするよう、MVVMなどのアーキテクチャを指示しておく。今回は後出しの指示になってしまったのですが、最初に指示しておけばもっと効率よく開発できたと思います。

まとめ

今回、初めこそなかなか上手くいかなかったものの、軌道に乗り始めるとぐんぐん進み、わずか1時間半ほどでアプリが完成しました。私がfletについてそれほど詳しくないので、もしも調べながら自力で実装していたら丸一日はかかっていたと思います。

現時点では、例外ケースへの対処が不十分だったり、UIや操作性に改善の余地があったりするので、引き続きバイブコーディングを活用して改良していきたいと思います。

今回作成したプログラムは以下のGitHubに公開しています。興味のある方はご覧ください。

https://github.com/moto2g/ipynb-diff-viewer

参考リソース

Cursor公式サイト

Flet公式サイト

nbformat公式サイト