0. はじめに
こんにちは!今回がQiitaでの初投稿になります!
最近、フロントエンドやAI技術の進化に刺激を受け、本格的に個人開発をスタートしました!
初めてということもあり、モダンな技術スタック(Next.js App Router, Cloudflare Pages, Drizzle ORM など)に悪戦苦闘しながらも、ようやく形にすることができました。
本記事では、私が開発した「AI壁打ち付き・リーンキャンバス作成アプリ(KOSOUM)」の裏側や、そこで得た実装知見を共有したいと思います。
拙い部分もあるかと思いますが、現在Webアプリ開発やAI組み込みに挑戦している方々の参考になれば幸いです。
1. プロダクト紹介
作ったアプリ「KOSOUM(コソウム)」の紹介
- アプリの概要:直感的なUIでリーンキャンバスを作成・管理できるツール。
- 最大の特徴:AIが壁打ち相手となり、ビジネスモデルのブラッシュアップをサポートしてくれる機能。
なぜこのアプリを作ったのか?
最近、個人開発を始めたものの、
「せっかく作っても誰にも使われない…」
「そもそもどんなサービスが世の中に求められているのか分からない…」
という壁にぶつかっていました。
そんな時、とあるエンジニアの方が勧めていた名著『起業の科学』で
「リーンキャンバス」 というフレームワークに出会いました。
「コードを書き始める前に、まずはアイデアの仮説を検証する」というアプローチに衝撃を受けると同時に、
「同じように個人開発のアイデア検証で悩んでいる人たちの助けになるツールが作れないか?」
と強く思い、このアプリ(KOSOUM)の開発に至りました。
2. 採用した技術スタック
全体アーキテクチャの概要
| カテゴリ | 技術・ツール | 選定理由・備考 |
|---|---|---|
| フレームワーク | Next.js (App Router, v15) | |
| ホスティング | Cloudflare Pages | (@cloudflare/next-on-pages) 個人開発なので、万が一バズった時にインフラ代で破産したくないという思いもあり、VercelではなくCloudflare Pageを採用。 |
| データベース & ORM | Drizzle ORM + Turso(libSQL) | Edge環境(Cloudflare)と相性の良いデータベースとDrizzleの組み合わせの快適さ。Tursoの無料枠が寛大なのもありこちらを選択。 |
| 認証 | better-auth | Tursoにはsupabaseのように標準の認証機能がないため選択。 |
| スタイリング & アニメーション | Tailwind CSS v4 Framer Motion next-view-transitions |
3. 開発過程で工夫したポイント・ハイライト
① AIとUIの融合(Tool Callingとストリーミング)
KOSOUMでは単にチャットとしてGemini APIを呼び出すだけでなく、一歩進んで
「AIからの提案結果を、アプリのUI(Reactコンポーネント)として表示し、ユーザーがクリック一つで適用できる」
という、いわゆる 「AG-UI」 の実装に挑戦しました。
チャットUIの限界と「AG-UI」への挑戦
初期に考えていた実装では
「チャット画面でAIに質問し、返ってきたテキストをコピーして、本来の入力欄に自分でペーストする」
という流れにしようと思っていたのですが、よくよく考えるとユーザーにとって二度手間だなーと思い...
ふと自分の開発AIエージェントを見るとAIが自律的にファイル検索、コード修正、テストまでやっているのをみてこれだ!
と思い調べてみたら、「AG-UI」というものに辿り着きました
KOSOUMでは、Gemini SDKのTool Calling機能とSSE(Server-Sent Events)を組み合わせ、ストリーミングで流れてくるJSONの断片をフロント側でリアルタイムにパースし、react-diff-view を使って差分をレンダリングする仕組みを構築しました。
1. Geminiに「UIを操作する権限(Tools)」を持たせる
まずバックエンド側で、AIに対して
「あなたはUI(キャンバス)を直接更新できる関数を持っています」
と定義づけ(Tool Callingの宣言)を行います。
// AIに渡すツールの定義
const OPENAI_TOOLS = [
{
type: "function",
function: {
name: "update_to_canvas",
description: "リーンキャンバスの特定のセクションの内容を更新・上書きします。",
parameters: {
type: "object",
properties: {
section_id: { type: "string", description: "適用先のセクションID" },
content: { type: "string", description: "適用するテキスト内容" }
},
required: ["section_id", "content"]
}
}
}
];
// Gemini SDK用にフォーマットを変換
const GEMINI_TOOLS = [{
function_declarations: OPENAI_TOOLS.map(t => ({
name: t.function.name,
description: t.function.description,
parameters: t.function.parameters
}))
}];
そしてシステムプロンプトで「キャンバスを更新する時は言葉で言うだけでなく、必ずこの関数を呼び出しなさい」と強く命令しておきます。
2. AIの出力をSSE(Server-Sent Events)でストリーミングする
AIが関数の呼び出し(Tool Call)を決定すると、JSONの断片が少しずつストリーミングで送られてきます。
それをそのままフロントに流すのではなく、バックエンドのEdge Function内でJSONの断片を検知し、フロントがパースしやすい独自タグ( <update section="..."> など)に変換してSSEで送信しています。
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 受信したチャンクをパース
const data = decoder.decode(value);
const parsed = JSON.parse(data);
const part = parsed.candidates?.[0]?.content?.parts?.[0];
// 1. 通常のテキスト回答の場合
if (part?.text) {
controller.enqueue(encoder.encode(`data: ${part.text}\n\n`));
}
// 2. AIが関数(update_to_canvas)を呼び出そうとした場合
if (part?.functionCall) {
const fc = part.functionCall;
const args = fc.args;
// フロントエンドが解釈しやすい独自タグに変換して流す
if (fc.name === "update_to_canvas" && args.section_id && args.content) {
const tagStr = `<update section="${args.section_id}">\n${args.content}\n</update>`;
controller.enqueue(encoder.encode(`data: ${tagStr}\n\n`));
}
}
}
3. フロントエンドでリアルタイムにパースし、差分(Diff)をレンダリング
サーバーから送られてくるテキストストリームを監視し、本文内に タグが含まれていれば、それをチャットのテキストから切り離し、「AIからの設計書の更新提案コンポーネント」として独立させてレンダリングします。
差分の表示には jsdiff と react-diff-view(を軽量に独自実装したもの)を使用しています。
const messages = [/* サーバーからストリーミング中の会話履歴 */];
return (
<div className="chat-container">
{messages.map((m, i) => {
// 1. 本文から <update> タグを正規表現で抽出
const updateMatches = Array.from(m.content.matchAll(/<update section="([^"]+)">([\s\S]*?)<\/update>/g));
const suggestions = updateMatches.map(match => ({ sectionId: match[1], content: match[2] }));
// 2. タグを除いた純粋な会話テキストだけにする
const cleanText = m.content.replace(/<update[\s\S]*?<\/update>/g, "").trim();
return (
<div key={i}>
{/* AIの通常の回答(会話) */}
<MarkdownContent content={cleanText} />
{/* AIがキャンバスの更新を提案してきた場合、Diffビューを表示 */}
{suggestions.map((suggest, sIdx) => {
const currentText = allItems[suggest.sectionId]; // 現在のキャンバス入力値
return (
<div key={sIdx} className="diff-panel">
<h4>更新提案: {suggest.sectionId}</h4>
{/* 既存のテキストとAIの提案を差分(Githubライク)で表示 */}
<DiffView
oldText={currentText}
newText={suggest.content}
onApplyLine={(action, oldLine, newLine) => handleApply(suggest.sectionId, newLine)}
/>
{/* ユーザーが承認すればキャンバスに適用 */}
<button onClick={() => updateCanvas(suggest.sectionId, suggest.content)}>
UPDATE
</button>
</div>
);
})}
</div>
);
})}
</div>
);
「AIの関数呼び出し ⇒ バックエンドで独自タグ化 ⇒ フロントで抽出してUI化」
という一連のパイプラインを自前で組むことで、独自性のあるリアルタイムなAG-UIを構築することができました。
Vercel AI SDK などの専用ライブラリを使えば、AG-UI簡単に実装できるみたいです。(私はまだ勉強中)
② UI/UXの追求
KOSOUMではView Transitions API を用いたシームレスな画面遷移を取り入れてみました。
Next.js App Routerでの画面遷移は、通常だとページ全体がパッと切り替わるだけ(いわゆるSPAの挙動)で、個人的に少し味気なさを感じる時がありました
そこで「View Transitions API」をReact(Next.js)で簡単に扱えるようにするライブラリ
next-view-transitions
を導入しました。
シームレスに遷移してカッコいい!
// 1. トップレベルを ViewTransitions プロバイダーで囲む
import { ViewTransitions } from 'next-view-transitions';
export default function RootLayout({ children }) {
return (
<ViewTransitions>
<html lang="ja">
<body>{children}</body>
</html>
</ViewTransitions>
);
}
使い方は非常にシンプルで、Next.js標準の コンポーネントを、このライブラリが提供するものに置き換え、アニメーションさせたい要素にCSSで同じ view-transition-name をつける
ただそれだけです!
// 2. 標準のLinkの代わりに next-view-transitions の Link を使う
import { Link } from 'next-view-transitions';
export function CanvasCard({ canvas }) {
return (
<Link
href={`/dashboard/${canvas.id}`}
// 遷移前(ダッシュボード側)と遷移後(詳細画面側)で同じ名前をつける
style={{ viewTransitionName: `canvas-card-${canvas.id}` }}
className="block p-4 bg-white rounded-xl shadow-sm"
>
<div style={{ viewTransitionName: `canvas-title-${canvas.id}` }}>
{canvas.title}
</div>
</Link>
);
}
このわずかな実装だけで、
「カードをクリックすると、そのカードが画面全体に広がりながら詳細ページに化ける」
という、非常にカッコいいUXを作ることができます!
ViewTransitionを使用するにはNext.js 15以上である必要があります
4. まとめと今後の展望
個人開発を始めて間もないですが、このアプリを作成したことでかなり勉強になったと思います!
AIの普及により開発がしやすくなった反面、競合アプリの品質も底上げされているので
依然として学び続ける姿勢は必須だと感じました。
また、今回は「Cloudflare Pages + Drizzle + better-auth」というモダンな技術スタックを初めて本格的に採用しました。
私自身まだ新参者であり、設計や実装において最適解ではない部分もあるかと思います。
もし「ここはこう書いた方が良いよ!」といったアドバイスがあれば、ぜひコメントで教えていただけると嬉しいです!
今後のアップデート予定ですが、「AG-UI」の領域をさらに深掘りし、キャンバス作成だけでなく、より多様なAIによる操作補助機能を追加していく予定です。
私と同じように
「どんなサービスを作ればいいか悩んでいる」
「自分のアイデアをリーンキャンバスで整理してみたい」
という方は、ぜひ一度KOSOUMを使っていただき、フィードバックをもらえると最高に嬉しいです!(もちろん、それ以外の方もジャンジャン触ってみてください!)


