The AI UX Playbook
miibo で実現する AI ネイティブなプロダクト設計
目次
Part 0: イントロダクション
Part 1: 基盤技術
- Chapter 1: miibo APIの基本
- Chapter 2: ストリーミング対応
- Chapter 3: ChatFilesでコンテキストを渡す
- Chapter 4: Extensionsの基本
- Chapter 5: 共通UIコンポーネント
Part 2: AI UXパターン実装ガイド
- Pattern 1: AI情報検索
- Pattern 2: AIヒアリング&提案
- Pattern 3: AI操作補助
- Pattern 4: AI分析&改善提案
- Pattern 5: AIエージェント自律実行
Part 3: 応用トピック
- Chapter 11: プロダクト操作の高度なパターン
- Chapter 12: 動的コンテキストストア
- Chapter 13: AIページ分析 & タスク自動実行
- Chapter 14: クロスパターン設計指針
- Chapter 15: プロンプト設計ガイドライン
Appendix
Part 0: イントロダクション
0.1 はじめに
miibo = Backend as a Service(BaaS)
miibo は AI エージェントのバックエンドをまるごと提供する BaaS です。LLM のインフラ構築、プロンプト管理のバックエンド開発、会話履歴の永続化、RAG パイプラインの構築 — これらはすべて miibo が担います。開発者は 1本の API を叩くだけ で AI 機能を組み込め、UX の設計と実装に 100% 集中 できます。
| 機能 | 自前構築 | miibo |
|---|---|---|
| 会話ログ | DB設計・ストレージ構築が必要 | 自動保存 |
| ステート管理 | セッションDB構築が必要 | APIパラメータで完結 |
| インサイト | 分析基盤の開発が必要 | 管理画面で確認 |
| アナリティクス | 計測・集計の開発が必要 | ダッシュボードで確認 |
| ナレッジ / RAG | ベクトルDB等の構築が必要 | ファイルをアップロードするだけ |
| Function Calling | スキーマ定義・実行エンジンが必要 | 管理画面でノーコード定義 |
| プロンプト管理 | デプロイが必要 | 管理画面で即時反映 |
| LLM | API契約・キー管理等が必要 | 1本のAPIで即利用 |
| → インフラに大半の工数 | → UXだけに集中 |
本ガイドが「UI / UX パターン」に特化できるのは、miiboがバックエンドを丸ごと引き受けているからです。
miibo AIをWebプロダクトに組み込むことで、ユーザー体験を根本から変えることができます。
AIのプロダクト組み込みは「チャットボットを右下に置く」だけではありません。ユーザーの目的やタスクに応じて、AIの役割・UI構成・インタラクションの流れを最適化する必要があります。本ガイドでは、基盤技術と設計原則を体系的に解説した上で、代表的な5つの実装パターンを具体例として示します。
代表的なAI UXパターン
以下の5つはショーケースで実装した代表例です。実際のプロダクトではこれらを参考に、独自のパターンを設計してください。
| パターン | プロダクト例 | AIの役割 | ユーザー操作 |
|---|---|---|---|
| 1. AI情報検索 | 旅行、不動産、求人 | 自然言語から構造化された検索結果を生成 | 質問するだけ |
| 2. AIヒアリング&提案 | 見積もり、保険、査定 | ヒアリング→最適な提案を自動生成 | 質問に答えるだけ |
| 3. AI操作補助 | 人事評価、物件登録、設定入力 | フォームを自動入力+根拠を説明 | 確認・修正するだけ |
| 4. AI分析&改善提案 | 経営ダッシュボード、EC最適化 | データ分析→改善施策を提案→実行 | 承認するだけ |
| 5. AIエージェント自律実行 | ナレッジ改善、コンテンツ管理 | タスクを自動生成→人間が承認→AIが実行 | approve/rejectするだけ |
その他の一般的なパターン
上記5つ以外にも、以下のような基本的・応用的なパターンが考えられます。これらは基盤技術(Part 1)の組み合わせで実現できます。
| パターン | 概要 | 実現方法 |
|---|---|---|
| チャットQ&A | 画面右側にチャットウィジェットを配置し、FAQ・お問い合わせにAIが応答 | ChatFiles(INSTRUCTION + FAQ/ドキュメント)のみで実現。Extensionは不要 |
| ページ遷移アシスタント | 「設定画面に行きたい」「レポートを作りたい」→ AIがナビゲーション実行 | Extension navigate + 関数レジストリ(Chapter 11) |
| マルチステップ操作 | 「通知をオフにして保存して」→ AIがページ遷移→フォーム入力→保存を逐次実行 | Extension navigate + call_function + ナビゲーションキュー(Chapter 11) |
| コンテキスト付きヘルプ | ユーザーが見ている画面に応じて、適切なヘルプを提供 | ChatFiles(ページコンテキスト)+ state(ユーザー属性) |
| オンボーディングガイド | 新規ユーザーに操作手順をステップバイステップで案内 | Extension call_function + ビジュアルフィードバック(Chapter 13) |
| リッチUI表示 | 比較表、メトリクス、選択チップなどをチャット内に表示 | 表示系Extension(show_card, show_table, show_form等) |
ポイント: すべてのパターンは同じ基盤技術(API、ストリーミング、ChatFiles、Extensions)の組み合わせです。Part 1で基盤を理解すれば、Part 2の5パターンに限らず、あなたのプロダクトに最適なパターンを自由に設計できます。
前提知識
- HTML / CSS / JavaScript の基本
- React(Next.js 推奨)の基本的な開発経験
- REST API の基本概念
必要なもの
- miiboアカウント: miibo にサインアップし、エージェントを作成して API キーを取得
- Webアプリ: Next.js プロジェクト(推奨)または任意のフロントエンド
注: miibo は REST API 経由で AI エージェントとやりとりするため、API を呼び出せる環境であれば Web アプリに限らず、モバイルアプリ・デスクトップアプリ・IoT デバイスなどでも利用できます。本ガイドでは Web(Next.js)を前提に解説しますが、基本的な考え方は他のプラットフォームにも応用可能です。
公式ドキュメント
本ガイドはAI UXの設計パターンに焦点を当てています。miiboプラットフォーム自体の詳細な機能・設定については、以下の公式ドキュメントを参照してください。
- miiboマニュアル: https://docs.miibo.tech/ — 管理画面の操作方法、RAG設定、プロンプト設計、コネクター設定など
- miibo API リファレンス: https://docs.miibo.tech/reference/チャット — チャットAPI の詳細なパラメータ仕様、レスポンス形式、エラーコード
0.2 AI UXパターン選択ガイド
Part 2の5パターンのうちどれから始めるか迷ったら、以下のフローチャートを参考にしてください。もちろんこれ以外のパターンも可能です — 基盤技術を組み合わせて独自のパターンを設計してください。
flowchart TD
START(("START"))
Q1{"Q&A対応?"}
Q2{"情報検索?"}
Q3{"ヒアリング
& 提案?"}
Q4{"操作補助?"}
Q5{"分析 &
改善提案?"}
A0["💬 チャットQ&A
Part 1 基盤のみで実現"]
A1["🔍 Pattern 1
AI情報検索"]
A2["📋 Pattern 2
AIヒアリング & 提案"]
A3["⚡ Pattern 3
AI操作補助"]
A4["📊 Pattern 4
AI分析 & 改善提案"]
A5["🤖 Pattern 5
AIエージェント自律実行"]
START --> Q1
Q1 -- "Yes" --> A0
Q1 -- "No" --> Q2
Q2 -- "Yes" --> A1
Q2 -- "No" --> Q3
Q3 -- "Yes" --> A2
Q3 -- "No" --> Q4
Q4 -- "Yes" --> A3
Q4 -- "No" --> Q5
Q5 -- "Yes" --> A4
Q5 -- "No" --> A5
パターン詳細比較
| Pattern 1 | Pattern 2 | Pattern 3 | Pattern 4 | Pattern 5 | |
|---|---|---|---|---|---|
| UI形態 | チャット+結果カード | フォーム→チャット+提案カード | フォーム+チャット | ダッシュボード+チャット | カードスタック+ジョブリスト |
| 難易度 | ★★☆ | ★★★ | ★★☆ | ★★★ | ★★☆ |
| チャット | あり | あり(後半) | あり | あり | なし |
| Extension | show_*系 | show_*系 | fill_*系 | show_*+update_*系 | なし(ローカル状態) |
おすすめ: 最もシンプルなのはチャットQ&A(Extension不要、ChatFilesだけで実現)です。既存のフォームを持つプロダクトなら**Pattern 3(操作補助)**がすぐに価値を生みます。ChatFilesにフォーム構造を渡すだけでAIが操作できるようになります。
0.3 全体アーキテクチャ
すべてのパターンに共通するアーキテクチャを示します。
graph TB
subgraph PRODUCT["あなたのプロダクト"]
direction LR
UI["既存の UI / ページ
フォーム・テーブル・ダッシュボード"]
AI["AI チャット / エージェント
① メッセージ送信
② Extensions 受信・実行
③ ChatFiles でコンテキスト送信"]
AI -- "UI 更新" --> UI
end
AI -- "HTTP POST(REST API)" --> API
subgraph MIIBO["miibo API"]
direction TB
API["AI エージェント
意図理解 → 応答生成 → Extensions 出力"]
end
データフロー
- ユーザーが自然言語を入力(
utterance)し、画面の状態をchat_filesとして同時に送信 — AIに「何を求めているか」と「今何を見ているか」を伝える - miibo AI がこれらを受け取り、エージェントに紐づいた RAG(ナレッジ)も合わせて総合的に判断
- 応答テキスト + Extensions(構造化JSON)を返す
- クライアントがExtensionsを解析し、UIの更新やプロダクト操作を実行
この「utterance + ChatFiles → miibo API(+ RAG) → Extensions → UI更新」のサイクルが、すべてのAI UXパターンの基盤です。
文字数上限:
utteranceとchat_filesの合計は最大 15,000文字 です。上限はmiiboのエージェント設定画面から変更できます。
miiboプラットフォームの活用
本ガイドでは主にフロントエンド側のコード実装を解説しますが、AIエージェントの「頭脳」にあたる部分は miibo管理画面 で柔軟にカスタマイズできます。miibo をBaaS(Backend as a Service)として活用することで、コードのデプロイなしにエージェントの振る舞いを継続的に改善できます。
| 機能 | 概要 | メリット |
|---|---|---|
| RAG(検索拡張生成) | ドキュメント・FAQ・ナレッジベースをアップロードし、AIが参照して回答 | 専門知識をコードなしで追加・更新 |
| コネクター | 外部API・データベース・SaaSとノーコードで接続 | AIが最新のデータにアクセス可能に |
| プロンプト管理 | システムプロンプトやペルソナを管理画面から編集 | リリースなしでAIの振る舞いを即座に更新 |
| ログ分析 | ユーザーとの全会話ログを閲覧・検索 | 改善すべき応答パターンを特定 |
| インサイト抽出 | 会話データからユーザーの傾向・要望を自動分析 | プロダクト改善のヒントを発見 |
| アナリティクス | 応答品質・利用状況・満足度のダッシュボード | エージェントのパフォーマンスを定量的に把握 |
リリースなしのアップデート: miibo管理画面でプロンプトやRAGデータを更新すると、即座にエージェントの応答が変わります。フロントエンドのコードを変更・デプロイする必要はありません。これにより、エージェントの改善サイクルを高速に回すことができます。
graph TB
subgraph CONSOLE["miibo 管理画面(ノーコード)"]
direction LR
EDIT["⚙️ 設定
プロンプト編集
RAG データ追加
コネクター設定"]
ANALYZE["📊 分析
ログ分析
アナリティクス
インサイト抽出"]
end
CONSOLE -- "設定が即座に反映(デプロイ不要)" --> AGENT
subgraph ENGINE["miibo AI エージェント"]
AGENT["更新されたプロンプトで応答
最新の RAG データを参照
外部 API からデータ取得"]
end
YOUR["あなたのプロダクト"] -- "API 接続" --> AGENT
つまり、本ガイドで解説するフロントエンド実装は「AIの出力をどう活用するか」に集中しており、「AIをどう賢くするか」はmiibo管理画面で継続的に改善できるという役割分担になっています。
Part 1: 基盤技術
Chapter 1: miibo APIの基本
公式APIリファレンス: 本章ではAI UX実装に必要なAPIの使い方を解説しますが、パラメータの完全な仕様やエラーコードについては miibo API リファレンス を参照してください。
1.1 エンドポイント
POST https://api-mebo.dev/api
1.2 最小限のリクエスト
curl -X POST https://api-mebo.dev/api \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"agent_id": "YOUR_AGENT_ID",
"utterance": "こんにちは",
"uid": "user_001"
}'
1.3 レスポンス構造
{
"utterance": "こんにちは",
"bestResponse": {
"utterance": "こんにちは!何かお手伝いできますか?",
"score": 0.95,
"options": ["設定変更", "レポート作成"],
"topic": "",
"isAutoResponse": true,
"extensions": null
},
"userState": {}
}
bestResponse.utterance— AIの応答テキストbestResponse.options— クイックリプライの選択肢(AIが生成)bestResponse.extensions— 構造化データ(Extensionsとして後述)
1.4 主要パラメータ
| パラメータ | 必須 | 説明 |
|---|---|---|
| api_key | ○ | miiboのAPIキー |
| agent_id | ○ | エージェントID |
| utterance | ○ | ユーザーの発話(空文字で会話開始) |
| uid | △ | ユーザー識別子(会話履歴の維持に必要) |
| stream | true でストリーミング応答 |
|
| chat_files | 画面コンテキスト等のファイル情報 | |
| state | ユーザー属性(Key-Value) | |
| third_party_token | 外部トークン。コネクターのURL・ペイロード内で @{thirdPartyToken} として展開される |
utterance 空文字送信 — AIから会話を開始する
utterance に空文字 "" を送信すると、ユーザーの入力を待たずにAI側から最初のメッセージを生成させることができます。これは「AIから先に話しかける」UXを実現するための重要なテクニックです。
// ページ表示時にAIから先に話しかける
useEffect(() => {
// utterance を空文字で送信 → AIが初回メッセージを生成
sendMessage({ utterance: "", uid, chatFiles, state });
}, []);
ユースケースと使い分け:
| パターン | 空文字送信 | 理由 |
|---|---|---|
| Pattern 1 (検索) | しない | ユーザーが検索クエリを入力してから開始 |
| Pattern 2 (ヒアリング) | しない | フォームで要望を入力してから開始 |
| Pattern 3 (操作補助) | する | ページ表示時にAIが「評価シートの入力をお手伝いします」と先に案内 |
| Pattern 4 (分析) | する | ダッシュボード表示時にAIが分析結果を提示 |
空文字送信時も chat_files と state を一緒に渡すことで、AIは現在の画面コンテキストやユーザー情報を踏まえた適切な初回メッセージを生成します。例えばPattern 3では、フォームの現在の入力状態を chat_files で渡すことで、「未入力の項目がありますね、お手伝いしましょうか?」のようなコンテキスト依存の案内が可能になります。
1.5 APIルート(Next.js)
フロントエンドからAPI Keyを直接送信するのはセキュリティ上問題があるため、Next.jsのAPIルートをプロキシとして利用します。以下は本番運用可能なAPIルートの例です。
// src/app/api/chat/route.ts
import { NextRequest } from "next/server";
const MIIBO_API_URL = "https://api-mebo.dev/api";
export async function POST(request: NextRequest) {
const { utterance, uid, chatFiles, state } = await request.json();
const apiKey = process.env.MIIBO_API_KEY;
const agentId = process.env.MIIBO_AGENT_ID;
if (!apiKey || !agentId) {
return new Response(
JSON.stringify({ error: "API credentials not configured" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
const body: Record<string, unknown> = {
api_key: apiKey,
agent_id: agentId,
utterance: utterance || "",
uid,
stream: true,
};
if (chatFiles?.length) {
body.chat_files = chatFiles;
}
if (state && Object.keys(state).length > 0) {
body.state = state;
}
const miiboResponse = await fetch(MIIBO_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
accept: "text/plain",
},
body: JSON.stringify(body),
});
if (!miiboResponse.ok) {
return new Response(
JSON.stringify({ error: `miibo API error: ${miiboResponse.status}` }),
{ status: miiboResponse.status, headers: { "Content-Type": "application/json" } }
);
}
return new Response(miiboResponse.body, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Transfer-Encoding": "chunked",
},
});
}
ポイント:
api_keyとagent_idはサーバーサイドの環境変数から取得(.env.localに設定)stream: trueを常に指定し、ストリーミングレスポンスをそのままクライアントに転送chat_filesとstateはオプショナルで、存在する場合のみリクエストに含める
1.6 stateパラメータ
state パラメータを使うと、ユーザーの属性情報をKey-Value(string:string)形式でAIに渡すことができます。
const userState = {
userName: "田中 太郎",
age: "32",
region: "東京都渋谷区",
memberTier: "ゴールド会員",
travelHistory: "沖縄2回・北海道1回",
preferredStyle: "グルメ・温泉好き",
};
プロンプトでの変数参照: stateに渡した値は、miiboのエージェント設定画面のプロンプト内で #{キー名} の記法で変数として挿入できます。例えば region というキーを渡した場合:
あなたは旅行アドバイザーです。
ユーザーの地域: #{region}
ユーザーの会員ランク: #{memberTier}
地域や会員ランクに応じて、パーソナライズされた提案をしてください。
分析・マーケティングへの活用: stateはプロンプトのカスタマイズだけでなく、miibo管理画面のログ分析でも集計パラメータとして利用できます。例えば
regionやmemberTierで会話ログをフィルタリング・集計することで、「地域別の問い合わせ傾向」「会員ランク別の満足度」といった分析やマーケティング施策に活用できます。
stateの情報源は多岐にわたります:
- CRM: 会員ランク、購買履歴、問い合わせ履歴
- CDP: 行動履歴、興味関心カテゴリ
- 広告パラメータ: 流入元、キャンペーンID
- アプリ内データ: 設定値、利用頻度、過去の検索履歴
Chapter 2: ストリーミング対応
2.1 概要
ストリーミングを使うことで、AIの応答をリアルタイムに逐次表示できます。ユーザーは応答の完了を待つことなく、生成される内容を順次確認できます。
stream: trueでリクエストaccept: "text/plain"ヘッダーでSSEレスポンスを受信- レスポンスは改行区切りのJSON行で届く
- 最後の行にfinalレスポンス(
score付き)が含まれる
2.2 ストリーミング受信パターン
以下は、すべてのデモで共通して使用するストリーミング受信の基本パターンです。
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ utterance: query, uid, chatFiles, state: userState }),
});
const reader = response.body?.getReader();
if (!reader) throw new Error("No reader available");
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 改行で分割してJSON行を処理
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 未完了の行をバッファに戻す
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (parsed.bestResponse?.utterance) {
// テキストを逐次更新
updateMessage(parsed.bestResponse.utterance);
// optionsがあればクイックリプライとして保持
if (parsed.bestResponse?.options?.length) {
setQuickReplies(parsed.bestResponse.options);
}
}
} catch {
// 不完全なJSON行はスキップ
}
}
}
// バッファに残った最後の行を処理
if (buffer.trim()) {
try {
const parsed = JSON.parse(buffer.trim());
if (parsed.bestResponse?.utterance) {
updateMessage(parsed.bestResponse.utterance);
}
} catch {
// ignore
}
}
処理のポイント:
TextDecoderの{ stream: true }オプションで、マルチバイト文字の途中切れに対応- 改行で分割し、未完了の行はバッファに戻して次のチャンクと結合
- JSON.parseに失敗した行は安全にスキップ(不完全なチャンクの可能性)
- 最後にバッファ残りを処理して取りこぼしを防止
Chapter 3: ChatFilesでコンテキストを渡す
3.1 ChatFilesとは
ChatFilesは、AIに「今ユーザーが何を見ているか」「どんなデータがあるか」を伝える仕組みです。miibo APIの chat_files パラメータとして送信します。
type ChatFile = {
fileName: string; // ファイル名(AIが参照する名前)
fileType: string; // "txt" or "json"
content: string; // 内容(テキストまたはJSON文字列)
};
AIはこれらのファイルを「添付資料」として認識し、内容を踏まえて応答を生成します。
3.2 管理画面プロンプトとChatFiles INSTRUCTIONの使い分け
miibo には「プロンプトを設定する場所」が2つあります。それぞれ役割が異なります。
| miibo管理画面のプロンプト | ChatFiles 00_INSTRUCTION.md |
|
|---|---|---|
| 設定場所 | miibo管理画面(ノーコード) | フロントエンドのコード内 |
| 適用範囲 | 全リクエストに常に適用 | リクエストごとに動的に変更可能 |
| 主な内容 | ペルソナ、基本ルール、禁止事項、トーン | 画面固有のExtension仕様、データの読み方、出力形式 |
| 変更方法 | 管理画面で即時反映(デプロイ不要) | コード変更+デプロイが必要 |
| state参照 | #{変数名} で参照可能 |
フロントエンドで値を埋め込む |
使い分けの考え方:
- 管理画面プロンプト = 「このエージェントは常にこう振る舞え」という基盤レイヤー。全画面で共通のペルソナや基本ルールを定義する。管理画面から即座に更新できるため、PdMやオペレーターが日常的にチューニングできる
- ChatFiles INSTRUCTION = 「今この画面ではこう動け」という画面固有のオーバーレイ。画面ごとに異なるExtension仕様、利用可能なデータ構造、出力フォーマットを指定する
実践例: 旅行検索画面なら、管理画面プロンプトには「丁寧な旅行アドバイザー」という共通ペルソナを設定し、ChatFiles INSTRUCTIONには「
show_travel_planExtensionを使い、以下のJSON形式で出力してください」という画面固有の指示を含めます。同じエージェントでも、別の画面(例: 車見積もり)からはまったく異なるINSTRUCTIONを送ることで、画面に応じた振る舞いを実現できます。
3.3 推奨するChatFiles構成
| ファイル名 | 用途 | 例 |
|---|---|---|
00_INSTRUCTION.md |
AIへの動作指示 | ペルソナ、回答形式、Extension仕様 |
available_data.json |
AIが参照するデータ | 商品リスト、スポット情報、車両データ |
current_values.json |
現在の画面状態 | フォームの入力値、フィルター状態 |
3.4 ChatFiles設計の原則
- INSTRUCTIONは最重要: AIの振る舞いの95%はここで決まります。ペルソナ、回答形式、禁止事項、Extension仕様を明確に記述してください。
- Extension仕様を明記: 出力形式のJSON例を必ず含めてください。AIは「このフォーマットで出力してください」という指示に忠実に従います。
- データは必要最小限: AIに渡すデータは、現在のクエリに関連するものだけにフィルタリングしてください。全データを渡すとコストが増加し、精度も低下します。
- utterance + chat_files の合計は最大15,000文字: この上限はmiiboのエージェント設定画面から変更可能です(デフォルトは低めに設定されています)。合計サイズが大きすぎるとAPIコストが増加し、レスポンス速度も低下するため、必要最小限に抑えてください。
3.5 実践例: 旅行検索デモのChatFiles
function buildTravelChatFiles(query: string) {
// クエリから関連エリアのスポットだけにフィルタリング
const relevantSpots = matchedArea
? TRAVEL_SPOTS.filter((s) => s.area === matchedArea)
: TRAVEL_SPOTS.slice(0, 6);
return [
{
fileName: "00_INSTRUCTION.md",
fileType: "txt",
content: `# 指示
あなたは旅行プランナーAIです。
...(Extension仕様、絶対ルール等)...`,
},
{
fileName: "available_spots.json",
fileType: "json",
content: JSON.stringify(relevantSpots, null, 2),
},
];
}
ここで重要なのは、TRAVEL_SPOTS の全データを渡すのではなく、クエリに関連するエリアのスポットだけにフィルタリングしている点です。これにより、APIコストを抑えつつ、AIの応答精度を高めています。
3.6 バックエンドからのデータ供給: ナレッジデータストア & コネクター
ChatFilesはフロントエンドからデータを渡す手段ですが、すべての情報をフロントエンドから渡す必要はありません。miiboの管理画面には、バックエンド側でデータを供給する2つの仕組みがあります。これらを活用すれば、utterance + chat_files の文字数制限を消費せずにAIへ情報を注入できます。
ナレッジデータストア(RAG)
ナレッジデータストアは、AIに与える専門知識をプールしておくデータベースです。登録した情報は自動的にEmbedding(ベクトル化)され、ユーザーの質問に関連するデータが自動検索されてAIのプロンプトに注入されます。自前でRAG環境を構築する必要はありません。
対応データ形式:
| 形式 | 制限 |
|---|---|
| テキスト入力 | 最大50,000文字 |
| URL | ページ最大25,000文字 |
| PDF / Word / Excel / PowerPoint | 最大1,000MB、テキスト200,000文字 |
| Notion連携 | ページID指定 |
| JSON / CSV / マークダウン | 最大50,000文字 |
設定方法: miibo管理画面 → 左メニュー「ナレッジ」→「ナレッジデータストアを作成する」→「データを追加する」
API経由でのデータ追加も可能です:
PUT https://api-mebo.dev/datastore/create
{
"api_key": "YOUR_API_KEY",
"agent_id": "YOUR_AGENT_ID",
"label": "データのラベル",
"text": "登録するテキストデータ"
}
データ入稿のポイント:
- 1つのデータに複数の話題を含めない(検索精度が低下する)
- 全データのフォーマットに統一感を持たせる
コネクター(Webhook / 外部API連携)
コネクターは、外部APIを呼び出してその結果をAIの応答に活用する仕組みです。ユーザーの発話やAIの判断をトリガーに、リアルタイムで外部データを取得できます。
5つの連携タイプ:
| タイプ | 用途 |
|---|---|
| 通常のコネクター | 任意の外部APIと連携(REST API) |
| miiboエージェント | 別のmiiboエージェントを呼び出す(マルチエージェント) |
| カスタムアクション | Custom Actionで実装した機能を呼び出す |
| エージェントグループ | 複数エージェントのグループに自律的タスクを依頼 |
| MCP (SSE / Streamable HTTP) | Model Context Protocol対応サーバーと連携 |
トリガーの種類:
| トリガー | 説明 | 使用例 |
|---|---|---|
| AIによる判定 | Function Callingで会話内容から自動判定 | 「天気を教えて」→ 天気APIを呼ぶ |
| ユーザー発話 | 特定の発話パターンで発火 | 「注文」を含む発話 → 注文APIを呼ぶ |
| エージェント応答時 | AI応答後に実行 | 応答ログの保存、通知送信 |
ペイロードで使える変数:
| 変数 | 内容 |
|---|---|
@{query} |
生成された検索クエリー |
@{utterance} |
ユーザーの発話 |
@{thirdPartyToken} |
フロントエンドから渡された外部トークン |
@{sessionId} |
セッションID |
@{history} |
会話履歴 |
#{stateVariable} |
ステートの値 |
フロントエンドのトークンをコネクターで使う:
コネクターから外部APIを呼ぶ際に、フロントエンド側で保持している認証トークン(例: ログインユーザーのアクセストークン)を使いたい場合があります。miibo Chat APIの third_party_token パラメータで渡したトークンは、コネクターのURL・ヘッダー・ペイロード内で @{thirdPartyToken} として展開されます。
// フロントエンドからのAPIリクエスト例
const response = await fetch("https://api-mebo.dev/api", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: MIIBO_API_KEY,
agent_id: MIIBO_AGENT_ID,
utterance: userMessage,
uid: userId,
third_party_token: userAccessToken, // ← ユーザーの外部サービストークン
}),
});
// コネクター側の認証ヘッダー設定:
// X-API-TOKEN: @{thirdPartyToken}
// → 実行時に userAccessToken の値に置換される
これにより、miiboのコネクターがユーザーごとの権限で外部APIを呼び出すことが可能になります。例えば、社内システムのAPIをユーザーの認証トークンで呼び出し、そのユーザーがアクセスできるデータだけをAIに注入するといった使い方ができます。
コネクターの結果をAIのプロンプトに注入するには、「プロンプト挿入時のプレフィックス」を設定します。これにより、外部APIのレスポンスがベースプロンプトに自動的に組み込まれます。
📘 詳細: コネクターの利用 - miibo公式ドキュメント
ChatFiles vs バックエンド: 使い分けガイド
| 判断基準 | ChatFiles(フロントエンド) | ナレッジ / コネクター(バックエンド) |
|---|---|---|
| データの性質 | 画面の状態、ユーザーが今見ているデータ | 商品マスタ、FAQ、社内ナレッジなど静的〜準静的データ |
| データ量 | 少量(utterance + chat_filesの文字数制限内) | 大量でもOK(文字数制限を消費しない) |
| 更新頻度 | リクエストごとに動的に変更 | 管理画面やAPIで随時更新 |
| 設定者 | 開発者(コード変更が必要) | PdM / オペレーター(ノーコードで設定可能) |
| 典型的な用途 | Extension仕様の指示、フォームの現在値、画面固有の絞り込みデータ | FAQ、商品情報、外部API連携(天気・在庫・予約等) |
💡 ベストプラクティス: ChatFilesには「画面固有の指示(INSTRUCTION)」と「今このリクエストで必要な最小限のデータ」だけを渡し、汎用的な知識や大量データはナレッジデータストアやコネクターに任せましょう。これにより文字数制限を節約しつつ、AIの応答精度を最大化できます。
Chapter 4: Extensionsの基本
4.1 Extensionsとは
Extensionsは、AIのレスポンスに含まれる構造化JSON命令です。テキストの応答とは別に、UIの更新やプロダクト操作を指示します。
AIの応答テキスト内に、以下の形式で埋め込まれます:
```extension
{
"action": "アクション名",
"payload": { ... }
}
```
クライアント側でこのコードブロックを抽出・パースし、action に応じた処理を実行します。これにより、AIが「テキストで話す」だけでなく「UIを操作する」ことが可能になります。
4.2 Extension設計の考え方
Extensionの構造(どんな action を定義し、payload に何を含めるか)は、プロダクトごとにまったく異なります。本ガイドで紹介する型定義はあくまでショーケース用の一例であり、そのまま使うことを想定していません。
自分のプロダクトに合ったExtensionはAIと一緒に設計するのがおすすめです。 具体的には:
- プロダクトのUIを説明する — 「この画面には検索結果のカードリスト、フィルター、詳細モーダルがある」
- AIに何をさせたいかを伝える — 「検索結果を構造化して表示したい」「フォームを自動入力させたい」
- AIと型定義を一緒に作る — Claude Code等に「このUIに合うExtension型を設計して」と指示すれば、プロダクトに最適な
actionとpayloadの構造を提案してくれます
設計のポイント: Extension は「AIが返すJSON」と「UIが受け取るデータ」の契約(コントラクト)です。UIコンポーネントが必要とするフィールドから逆算して設計すると、無駄のない構造になります。
4.3 Extension型定義(ショーケース例)
以下はこのショーケースで使用する5つのExtension型です。自身のプロダクトでは、上記の考え方に基づいて独自の型を設計してください。
// show_travel_plan: 旅行プランの構造化表示
export type TravelPlanExtension = {
action: "show_travel_plan";
payload: {
title: string;
area: string;
duration: string;
totalBudget: string;
bestSeason: string;
days: {
day: number;
title: string;
spots: { id: string; name: string; time: string; memo: string }[];
}[];
highlights: string[];
};
};
// show_car_estimate: 中古車見積もりの構造化表示
export type CarEstimateExtension = {
action: "show_car_estimate";
payload: {
title: string;
summary: string;
userProfile: { budget: string; usage: string; priority: string };
recommendations: {
rank: number; id: string; make: string; model: string;
year: number; price: number; matchReason: string;
pros: string[]; cons: string[];
}[];
estimateBreakdown: {
carPrice: number; tax: number; insurance: number;
registration: number; totalEstimate: number;
};
highlights: string[];
};
};
// fill_evaluation_form: 評価フォームの自動入力
export type FormFillExtension = {
action: "fill_evaluation_form";
payload: {
fields: { fieldId: string; value: string }[];
explanation: string;
};
};
// show_improvements: 改善施策の提案表示
export type ImprovementExtension = {
action: "show_improvements";
payload: {
improvements: {
id: string; targetMetric: string; title: string;
description: string; currentValue: string; projectedValue: string;
effort: string; impact: string;
}[];
};
};
// update_filter: ダッシュボードフィルターの操作
export type DashboardFilterExtension = {
action: "update_filter";
payload: { period: string; category: string; keyword: string };
};
export type ParsedExtension =
| TravelPlanExtension
| CarEstimateExtension
| FormFillExtension
| ImprovementExtension
| DashboardFilterExtension;
各Extensionの使いどころ:
- show_*系 (Pattern 1, 2, 4): AIが生成した構造化データをリッチなUIカードとして表示
- fill_*系 (Pattern 3): AIがフォームの各フィールドに値を自動入力
- update_*系 (Pattern 4): AIがダッシュボードのフィルターや表示を操作
4.4 Extensionのパース
AIレスポンスから extension コードブロックを抽出するユーティリティ関数です。
/**
* AIレスポンスからextensionコードブロックを抽出
*/
export function parseExtensions(text: string): ParsedExtension[] {
const pattern = /```(?:extension|json:extension)\s*\n([\s\S]*?)\n```/g;
const results: ParsedExtension[] = [];
let match;
while ((match = pattern.exec(text)) !== null) {
try {
const parsed = JSON.parse(match[1].trim());
results.push(parsed);
} catch {
// skip invalid JSON
}
}
return results;
}
/**
* extensionコードブロックを除いた表示用テキストを返す
*/
export function stripExtensions(text: string): string {
return text
.replace(/```(?:extension|json:extension)\s*\n[\s\S]*?\n```/g, "")
.trim();
}
4.5 ストリーミング中のExtension処理
ストリーミング中はextensionブロックが途中で切れることがあります。ユーザーに途中のJSONを見せないために、ストリーミング用のstrip関数を使います。
/**
* ストリーミング中にも使える: 途中のextensionブロックも除去
*/
export function stripExtensionsStreaming(text: string): string {
// 完了したextensionブロックを除去
let cleaned = text.replace(
/```(?:extension|json:extension)\s*\n[\s\S]*?\n```/g,
""
);
// 途中のextensionブロック(閉じていない)も除去
cleaned = cleaned.replace(
/```(?:extension|json:extension)[\s\S]*$/,
""
);
return cleaned.trim();
}
UI側では、stripExtensionsStreaming で表示テキストをクリーニングしつつ、extension生成中は「生成中...」アニメーションを表示します。
// メッセージ表示時
const displayText = stripExtensionsStreaming(message.text);
const isGenerating = message.text.includes("```extension") &&
!message.text.includes("\n```", message.text.indexOf("```extension") + 14);
isGenerating が true の間は、extension の JSON がまだ生成途中であることを意味します。この状態では結果カードの代わりにローディングアニメーションを表示し、完了後に parseExtensions で最終的な構造化データを取得してUIに反映します。
Chapter 5: 共通UIコンポーネント
5.1 SplitPane(スプリットレイアウト)
チャットと結果表示を並べるレスポンシブ対応のスプリットペインです。
PC表示:
- 左ペイン: チャット(デフォルト38%)
- 右ペイン: 結果表示(デフォルト62%)
- ドラッグでリサイズ可能
スマホ表示:
- タブ切替(チャット / 結果)
- 画面幅に応じて自動切替
SplitPaneはすべてのチャット型パターン(Pattern 1〜4)で共通して使用します。左ペインのチャットでユーザーが自然言語で指示を出し、右ペインに構造化された結果が表示されるという一貫したUXを提供します。
5.2 FollowUpInput(フォローアップ入力)
画面下部に固定される追加質問入力欄です。初回の検索・提案の後、ユーザーが追加の質問や条件変更を行う際に使用します。
- 画面下部に固定表示(
position: sticky) - ストリーミング中は入力を無効化し、誤送信を防止
- Enterキーまたは送信ボタンで送信
5.3 ストリーミングメッセージ表示
メッセージリスト内でのストリーミング表示は、以下の5ステップで行います。
- ユーザーメッセージを即座に表示 — 送信と同時にメッセージリストに追加
- 空のAIメッセージを追加 — ローディングインジケーター(「...」アニメーション等)を表示
- ストリーミングで逐次テキスト更新 —
stripExtensionsStreamingでクリーニングしたテキストを表示 - Extension生成中は「構成中...」アニメーション — extensionブロックの開始を検知したらローディング表示に切替
- 完了後にExtensionをパースして結果UIを表示 —
parseExtensionsで構造化データを取得し、結果カードを描画
5.4 クイック返信
AIレスポンスの options フィールドから生成されるクイック返信チップです。
- AIが応答に含めた
options配列(例:["沖縄の詳細", "予算を下げたい", "別エリアを探す"])をチップとして表示 - タップするとフォローアップ入力として自動送信
- 新しい応答が届くと前のチップは非表示になる
- ユーザーが次のアクションを考えずに済むため、会話の継続率が向上する
5.5 Enterキー送信とIME(日本語入力)対応
日本語環境でチャットUIを実装する際、Enterキーの送信処理には特別な配慮が必要です。日本語入力(IME)では、漢字変換の確定にもEnterキーを使用するため、不適切な実装では「変換を確定しようとしただけなのにメッセージが送信される」という致命的なUXバグが発生します。
アンチパターン: onKeyDown で直接送信
// ❌ これは日本語入力で問題を起こす
<input
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(); // IMEの変換確定でも送信されてしまう!
}
}}
/>
この実装では、ユーザーが「旅行」と入力しようとして「りょこう」→ 変換候補選択 → Enter(確定)の操作をした瞬間に、未完成のメッセージが送信されてしまいます。
推奨パターン: <form onSubmit> を活用
// ✅ ブラウザのフォーム送信を利用する
<form onSubmit={(e) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
onSubmit(input.trim());
setInput("");
}}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">送信</button>
</form>
なぜこれで解決するのか:
- ブラウザは
<form>内の<input>でEnterが押された際、IMEの変換中(composition中)かどうかを自動判別します - IME変換中のEnterは「変換確定」として処理され、
submitイベントは発火しません - IMEが閉じている状態でのEnterのみが
submitイベントを発火させます - この挙動はChrome、Safari、Firefox等のモダンブラウザで一貫しています
isComposing を使う代替パターン
<textarea> を使用する場合はフォーム送信がEnterで自動発火しないため、onKeyDown で手動処理する必要があります。その場合は isComposing プロパティを使います。
// ✅ textareaでEnter送信する場合
<textarea
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}}
/>
e.nativeEvent.isComposing— IME変換中ならtrue(変換確定Enterを無視)!e.shiftKey— Shift+Enterは改行として扱い、Enterのみで送信
本ショーケースでの実装
本ショーケースでは、すべてのチャット入力(SearchHero、FollowUpInput、AcquisitionHero)で <form onSubmit> + <input type="text"> パターンを採用しています。これにより、日本語・中国語・韓国語などIMEを使用する言語環境で、特別なコードを追加することなく正しく動作します。
| コンポーネント | 方式 | IME対応 |
|---|---|---|
SearchHero |
<form> + <input> |
ブラウザが自動処理 |
FollowUpInput |
<form> + <input> |
ブラウザが自動処理 |
AcquisitionHero |
<form> + <input> |
ブラウザが自動処理 |
設計指針: チャット入力欄は可能な限り
<form>+<input type="text">で実装し、ブラウザネイティブのIME処理に任せるのが最もシンプルで確実な方法です。複数行入力が必要な場合のみ<textarea>+isComposingチェックを使用してください。
Part 2: AI UXパターン実装ガイド
本パートでは、ショーケースで実装した5つの代表的パターンを、それぞれ 概要 → UXデザイン原則 → Extension仕様 → プロンプト設計 → コンポーネント実装 → チェックリスト の統一構造で解説します。
これらは「すべてのパターン」ではなく、よくあるユースケースの代表例です。各パターンで示す設計原則やExtensionの使い方を理解すれば、チャットQ&A、ページ遷移アシスタント、オンボーディングガイドなど、あなたのプロダクトに最適なパターンを自由に設計できます。
各パターンは独立しており、自分のプロダクトに最も近いものから読み進めてください。
| # | パターン名 | AI の役割 | 典型的なUI構成 |
|---|---|---|---|
| 1 | AI情報検索 | 自然言語で検索し、構造化された結果を表示 | ヒーロー検索 → スプリットペイン |
| 2 | AIヒアリング&提案 | 適応型フォームで要望を把握し、最適な提案を自動生成 | フォーム → ローディング → 結果 |
| 3 | AI操作補助 | AIがフォームを自動入力し、根拠とともに提示 | スプリットペイン(フォーム + チャット) |
| 4 | AI分析&改善提案 | データを分析し、実行可能な改善施策を提案 | ダッシュボード + チャット + アクションカード |
| 5 | AIエージェント自律実行 | AIが提案し、人間が承認し、AIが実行 | スワイプカード + ジョブキュー |
パターン1: AI情報検索 -- 自然言語で検索、構造化された結果を表示
1. 概要と解決する課題
Before(従来のUX): ユーザーが自分でキーワード検索し、複数ページを閲覧し、情報を比較・整理する。検索スキルに依存し、最適な結果にたどり着けないことも多い。
After(AIによるUX): ユーザーはAIに自然言語で質問するだけで、最適な結果が構造化されたカードで即座に表示される。追加の要望はクイック返信で伝えるだけ。
適用ユースケース:
- 旅行プラン検索
- 不動産物件検索
- 求人検索・マッチング
- 商品カタログ検索
- 飲食店・施設検索
2. UXデザイン原則
- プログレッシブ開示: 初期状態ではヒーロー検索画面(大きな入力欄 + サジェスト)を表示し、結果が返ってきたらスプリットペイン(チャット + 結果カード)に遷移する。画面の複雑さを段階的に上げることで、初回ユーザーの認知負荷を最小化する。
- 構造化された可視化: AIの応答をプレーンテキストのまま表示せず、カード・タイムライン・タグなどのリッチUIで結果を表示する。Extension JSONをパースして専用コンポーネントでレンダリングすることで実現する。
- ストリーミング対応: テキスト部分はストリーミングで逐次表示し、Extension JSONの生成中は「プランを構成中...」のバウンスドットアニメーションで待ち時間を演出する。ユーザーは「AIが作業している」ことを視覚的に認識できる。
- マルチターン改善: クイック返信ボタンで「予算を抑えたい」「グルメ中心にして」などの改善指示をワンタップで送信可能にする。毎回ゼロから入力させない。
- プラン履歴: 過去の提案をすべてメッセージ履歴から抽出し、最新のプランをカード表示する。会話を遡れば以前のプランも確認できる。
3. Extension仕様
TypeScript型定義:
export type TravelPlanExtension = {
action: "show_travel_plan";
payload: {
title: string;
area: string;
duration: string;
totalBudget: string;
bestSeason: string;
days: {
day: number;
title: string;
spots: {
id: string;
name: string;
time: string;
memo: string;
}[];
}[];
highlights: string[];
};
};
JSONレスポンス例:
{
"action": "show_travel_plan",
"payload": {
"title": "沖縄グルメ満喫プラン",
"area": "沖縄",
"duration": "2泊3日",
"totalBudget": "¥80,000〜",
"bestSeason": "3月〜5月",
"days": [
{
"day": 1,
"title": "那覇エリアの名所とグルメ",
"spots": [
{
"id": "oki-1",
"name": "美ら海水族館",
"time": "10:00",
"memo": "世界最大級の水槽でジンベエザメを鑑賞"
},
{
"id": "oki-3",
"name": "国際通り",
"time": "15:00",
"memo": "お土産探しと食べ歩き"
}
]
},
{
"day": 2,
"title": "美ら海エリア",
"spots": [
{
"id": "oki-2",
"name": "古宇利島",
"time": "09:00",
"memo": "エメラルドグリーンの海に囲まれた絶景の島"
},
{
"id": "oki-4",
"name": "瀬長島ウミカジテラス",
"time": "14:00",
"memo": "海を眺めながらカフェを楽しむ"
}
]
}
],
"highlights": [
"沖縄そばの名店めぐり",
"世界遺産・首里城",
"美ら海水族館の特別プログラム"
]
}
}
4. ChatFiles / プロンプト設計
このパターンでは、ChatFilesを2つのファイルで構成します。
ファイル1: 00_INSTRUCTION.md -- AIへの指示書
# 指示
あなたは旅行プランナーAIです。
## ユーザー情報(stateから取得済み)
以下のユーザー属性がstateとして共有されています。提案のパーソナライズに活用してください。
- ユーザー名、居住地、年齢、家族構成、会員ランク、旅行履歴、好みのスタイルなどが含まれます。
- 例: 子連れユーザーなら子供向けスポットを優先、グルメ好きならグルメスポットを多めに。
- stateの情報を踏まえて「○○さんへのおすすめ」のようにパーソナライズしてください。
## 重要: すぐにプランを提案してください
ユーザーが旅行先や条件を伝えたら、追加の質問をせずにすぐにプランを提案してください。
テキスト部分は2〜3行の簡潔な概要のみにして、詳細はextensionに任せてください。
## 絶対ルール(情報量の統一)
- 必ず2日以上のプランにしてください。1日だけのプランは禁止です。
- 各日に必ず2〜3スポットを含めてください。
- スポットIDは必ずavailable_spotsのidを使ってください。
- highlightsは必ず3つ以上含めてください。
- extensionコードブロックは必ず含めてください。省略は禁止です。
## 回答形式
必ず以下のJSON形式のextensionコードブロックを回答の最後に含めてください。
(Extension仕様のJSON構造を記載)
ファイル2: available_spots.json -- スポットデータ(フロントエンドからのデータ渡し例)
プロダクト側(フロントエンド)でfetchしたデータをChatFiles経由でAIに渡す例です。クエリに関連するエリアのスポットデータのみをフィルタリングして渡します。
💡 データ供給はChatFilesだけではない この例ではフロントエンドからChatFilesでコンテンツを渡していますが、miiboの管理画面で設定できるRAG(ナレッジストア)やAPIコネクターを使えば、バックエンド側でデータを取得・注入することも可能です。ChatFilesで渡すコンテンツは発話文字数にカウントされるため、大量のデータを扱う場合はバックエンド側の仕組みを活用する方が効率的です。用途に応じて使い分けましょう。
export function buildTravelChatFiles(query: string) {
// クエリから関連エリアを推定(簡易マッチング)
const areas = ["沖縄", "京都", "北海道"];
const matchedArea = areas.find((a) => query.includes(a));
const relevantSpots = matchedArea
? TRAVEL_SPOTS.filter((s) => s.area === matchedArea)
: TRAVEL_SPOTS.slice(0, 6);
return [
{
fileName: "00_INSTRUCTION.md",
fileType: "txt",
content: `# 指示\nあなたは旅行プランナーAIです。\n...(上記の指示書)`,
},
{
fileName: "available_spots.json",
fileType: "json",
content: JSON.stringify(
relevantSpots.map((s) => ({
id: s.id,
name: s.name,
area: s.area,
category: s.category,
description: s.description,
rating: s.rating,
priceRange: s.priceRange,
duration: s.duration,
tags: s.tags,
})),
null,
2
),
},
];
}
プロンプト設計のポイント:
- 「すぐに提案」の明示: AIが追加質問をせず、即座にプランを返すよう指示する。情報検索パターンでは、ユーザーの最初のクエリに対して結果を返すことが最優先。
- スポットIDの制約: AIが自由にスポット名を創作するのではなく、データファイルのIDを使うよう制約する。これにより、フロントエンド側でスポットの追加情報(画像、座標など)を紐づけられる。
- 情報量の統一: 「2日以上」「各日2〜3スポット」「highlights 3つ以上」のようにExtensionの中身の分量を制約し、UIの見た目を安定させる。
5. 主要コンポーネント実装
状態遷移(ヒーロー → スプリットペイン):
const [messages, setMessages] = useState<Message[]>([]);
const hasMessages = messages.length > 0;
return (
<div className="flex h-[calc(100vh-57px)] flex-col bg-th-page">
{!hasMessages ? (
// 初期状態: 大きな検索画面
<SearchHero onSearch={handleSearch} isLoading={isStreaming} />
) : (
// 結果あり: チャット + フォローアップ入力
<>
<div className="flex-1 overflow-hidden">
<MessageList
messages={messages}
isStreaming={isStreaming}
quickReplies={quickReplies}
onQuickReply={handleSearch}
/>
</div>
<FollowUpInput onSubmit={handleSearch} isLoading={isStreaming} />
</>
)}
</div>
);
messages 配列が空かどうかだけで画面構成を切り替えます。最初の検索がトリガーとなってメッセージが追加されると、自動的にチャット表示に遷移します。
プランの抽出(全メッセージからExtensionを集約):
import { parseExtensions, type TravelPlanExtension } from "@/lib/parse-extensions";
// 全メッセージからextensionを抽出してプラン一覧を構築
const allPlans = useMemo(() => {
const plans: TravelPlanExtension["payload"][] = [];
for (const msg of messages) {
if (msg.role === "assistant") {
const extensions = parseExtensions(msg.text);
for (const ext of extensions) {
if (ext.action === "show_travel_plan") {
plans.push(ext.payload);
}
}
}
}
return plans;
}, [messages]);
parseExtensions は、AIレスポンス中の ```extension コードブロックからJSONを抽出するユーティリティです。メッセージが更新されるたびに全プランを再計算し、最新のプランをカードに表示します。
ストリーミング中のExtensionブロック検出:
import {
stripExtensionsStreaming,
stripExtensions,
} from "@/lib/parse-extensions";
// ストリーミング中 → 途中のextensionブロックも除去して表示
// 完了後 → 完了したextensionブロックのみ除去
const displayText = isStreamingThis
? stripExtensionsStreaming(msg.text)
: stripExtensions(msg.text);
ストリーミング中は、まだ閉じていない ```extension ブロックも除去することで、ユーザーにJSON生データが見えないようにします。
6. 実装チェックリスト
- データソース準備(スポット/商品の構造化データJSONファイル)
- ChatFiles構築関数(INSTRUCTION + データJSON)
- Extension型定義(
show_travel_plan等のaction+payload) -
parseExtensions/stripExtensionsユーティリティ - 結果表示カードコンポーネント(
TravelPlanCard等) - ヒーロー検索画面コンポーネント(
SearchHero) - ヒーロー → メッセージリストへの状態遷移
- ストリーミング表示(テキスト逐次更新 + Extension生成中アニメーション)
- クイック返信ボタンによるフォローアップ
- フォローアップ入力欄(
FollowUpInput)
パターン2: AIヒアリング&提案 -- 適応型フォームで要望を把握、最適な提案を自動生成
1. 概要と解決する課題
Before(従来のUX): LP閲覧 → 資料ダウンロード → 商談申込 → ヒアリング → 提案書作成 → 再商談。ユーザーが最適な選択にたどり着くまでに数日から数週間かかる。
After(AIによるUX): フォームで希望条件を回答 → AIが即座に最適プランを提案 → その場で商談 or 利用開始。数分で完了する。
適用ユースケース:
- 中古車見積もり・マッチング
- 保険プラン見積もり
- 不動産査定
- 旅行パッケージ提案
- SaaS料金プラン提案
2. UXデザイン原則
- 適応型質問: 基本質問(5問程度)の回答に応じて追加質問を動的に挿入する。例えば用途が「ファミリー」なら家族構成を追加、予算が「350万円以上」ならブランド志向を追加する。「あなたの回答を分析中...」のアニメーションで、AIが考えている感を演出する。
- フェーズ管理:
form→loading→resultの3フェーズで画面全体を遷移させる。フォーム入力中にチャットが見えてしまうと情報が多すぎるため、フェーズを明確に分離する。 - ランキング表示: 複数の候補をランク付きで表示し、それぞれにマッチ理由・pros/consを明示する。ユーザーが「なぜこれが1位なのか」を即座に理解できるようにする。
- 見積もり透明性: 総額だけでなく内訳(車両本体、税金、保険、登録費用など)を明示し、信頼感を構築する。
- Extension生成アニメーション: AIがExtension JSONを生成している間は「お見積もりを作成中...」のバウンスドットで待ち時間を演出する。ストリーミング中にExtensionブロックの開始を検出して表示を切り替える。
3. Extension仕様
TypeScript型定義:
export type CarEstimateExtension = {
action: "show_car_estimate";
payload: {
title: string;
summary: string;
userProfile: {
budget: string;
usage: string;
priority: string;
};
recommendations: {
rank: number;
id: string;
make: string;
model: string;
year: number;
price: number;
matchReason: string;
pros: string[];
cons: string[];
}[];
estimateBreakdown: {
carPrice: number;
tax: number;
insurance: number;
registration: number;
totalEstimate: number;
};
highlights: string[];
};
};
JSONレスポンス例:
{
"action": "show_car_estimate",
"payload": {
"title": "あなた専用のおすすめプラン",
"summary": "ファミリー向け・予算200万円以内・燃費重視の条件にぴったり",
"userProfile": {
"budget": "〜200万円",
"usage": "ファミリー",
"priority": "燃費重視"
},
"recommendations": [
{
"rank": 1,
"id": "car-01",
"make": "Toyota",
"model": "Aqua",
"year": 2021,
"price": 1280000,
"matchReason": "低燃費35.8km/Lでご予算内、ファミリーでの街乗りに最適",
"pros": ["燃費35.8km/L", "修復歴なし・ワンオーナー", "衝突安全ブレーキ搭載"],
"cons": ["後席はやや狭め"]
},
{
"rank": 2,
"id": "car-02",
"make": "Honda",
"model": "N-BOX",
"year": 2022,
"price": 1480000,
"matchReason": "軽自動車とは思えない室内空間、子育て世代に人気",
"pros": ["室内空間が広い", "スライドドア", "低走行1.5万km"],
"cons": ["高速道路でのパワー不足"]
}
],
"estimateBreakdown": {
"carPrice": 1280000,
"tax": 64000,
"insurance": 35000,
"registration": 45000,
"totalEstimate": 1424000
},
"highlights": [
"ご予算内でベストマッチ",
"ファミリー安心の安全装備",
"維持費も経済的"
]
}
}
4. ChatFiles / プロンプト設計
フォーム回答をChatFilesに変換:
export function buildCarAssessmentChatFiles(answers: {
budget: string;
usage: string;
bodyType: string;
priority: string;
lifestyle: string;
[key: string]: string;
}) {
return [
{
fileName: "00_INSTRUCTION.md",
fileType: "txt",
content: `# 指示
あなたは中古車コンサルタントAIです。
ユーザーはすでに5つのステップで希望条件を回答済みです。
**すぐに最適な車を提案してください。追加の質問は不要です。**
## ユーザーの希望条件
- 予算: ${answers.budget}
- 用途: ${answers.usage}
- ボディタイプ: ${answers.bodyType}
- こだわり: ${answers.priority}
- ライフスタイル: ${answers.lifestyle}
${answers.familySize ? `- 家族構成: ${answers.familySize}` : ""}
${answers.commuteDistance ? `- 通勤距離: ${answers.commuteDistance}` : ""}
## 絶対ルール
- 必ず車を2〜3台提案してください。「見つかりません」は禁止です。
- extensionコードブロックは必ず含めてください。
## 回答形式
テキスト部分は2〜3行の概要のみ。
(Extension仕様のJSON構造を記載)`,
},
{
fileName: "available_cars.json",
fileType: "json",
content: JSON.stringify(USED_CARS, null, 2),
},
];
}
プロンプト設計のポイント:
- 回答の埋め込み: フォームの回答をINSTRUCTIONテンプレートに直接埋め込む。AIは「すでにヒアリング済み」の前提でスタートするため、追加質問を抑制できる。
- 動的フィールドの条件付き追加:
answers.familySizeやanswers.commuteDistanceなど、適応型質問で追加された回答は存在する場合のみINSTRUCTIONに含める。 - 「見つかりません」の禁止: AIが条件に完全一致するデータがないと回答を拒否する傾向があるため、「完全一致しなくても最も近いものを提案」するよう明示する。
適応型質問の実装:
function generateDynamicSteps(answers: Partial<AssessmentAnswers>): StepConfig[] {
const extra: StepConfig[] = [];
// ファミリー → 家族構成を聞く
if (answers.usage === "ファミリー") {
extra.push({
id: "familySize",
label: "家族構成",
question: "ご家族は何人ですか?",
isDynamic: true,
options: [
{ value: "2人(夫婦)", label: "2人", sub: "夫婦・カップル", icon: "..." },
{ value: "3〜4人(小家族)", label: "3〜4人", sub: "お子様1〜2人", icon: "..." },
// ...
],
});
}
// 高予算 → ブランド志向を聞く
if (answers.budget === "350万円〜") {
extra.push({
id: "brandPreference",
label: "ブランド",
question: "好みのブランドはありますか?",
isDynamic: true,
options: [/* ... */],
});
}
return extra;
}
基本5ステップの完了後、回答内容に応じて動的ステップを生成し、ステップ一覧に挿入します。左のプログレスバーには「AI追加」バッジが表示され、質問が追加されたことがユーザーに伝わります。
5. 主要コンポーネント実装
3フェーズの状態管理:
type Phase = "form" | "loading" | "result";
const [phase, setPhase] = useState<Phase>("form");
// フォーム完了 → ローディング → API送信 → 結果表示
const handleComplete = async (ans: AssessmentAnswers) => {
setAnswers(ans);
setPhase("loading");
const chatFiles = buildCarAssessmentChatFiles(ans);
chatFilesRef.current = chatFiles;
const summary = `予算${ans.budget}、${ans.usage}用途、${ans.bodyType}希望、${ans.priority}重視`;
const aiMsgId = `ai-${Date.now()}`;
setMessages([{ id: aiMsgId, role: "assistant", text: "" }]);
setIsStreaming(true);
try {
await streamChat(summary, aiMsgId);
setPhase("result");
} catch (error) {
console.error("Assessment error:", error);
setPhase("result");
} finally {
setIsStreaming(false);
}
};
画面の切り替え:
return (
<div className="flex h-[calc(100vh-57px)] flex-col bg-th-page">
{/* フェーズ1: フォーム */}
{phase === "form" && (
<CarAssessmentForm onComplete={handleComplete} />
)}
{/* フェーズ2: ローディング */}
{phase === "loading" && (
<div className="flex flex-1 items-center justify-center">
<div className="text-center">
<div className="mb-6 flex justify-center gap-2">
<span className="h-3 w-3 rounded-full bg-th-primary animate-bounce" />
<span className="h-3 w-3 rounded-full bg-th-primary animate-bounce [animation-delay:150ms]" />
<span className="h-3 w-3 rounded-full bg-th-primary animate-bounce [animation-delay:300ms]" />
</div>
<p className="text-lg font-light text-th-primary">
あなた専用のプランを作成中...
</p>
</div>
</div>
)}
{/* フェーズ3: 結果(スプリットペイン) */}
{phase === "result" && (
<>
<div className="flex-1 overflow-hidden">
<SplitPane
left={chatPanel}
right={estimatePanel}
defaultLeftWidth={38}
minLeftWidth={25}
maxLeftWidth={65}
/>
</div>
<FollowUpInput onSubmit={handleFollowUp} isLoading={isStreaming} />
</>
)}
</div>
);
Extension生成中のアニメーション:
// ストリーミング中にextensionブロックが途中で来ているかを検出
function isGeneratingExtension(text: string): boolean {
const openPattern = /```(?:extension|json:extension)/g;
const closePattern = /```(?:extension|json:extension)\s*\n[\s\S]*?\n```/g;
const opens = (text.match(openPattern) || []).length;
const closes = (text.match(closePattern) || []).length;
return opens > closes; // 開始タグ > 終了タグ なら生成途中
}
// UIでの表示制御
const isExtensionGenerating =
isStreaming && lastMsg?.role === "assistant" && isGeneratingExtension(lastMsg.text);
{isExtensionGenerating && (
<div className="inline-flex items-center gap-3 rounded-xl border px-5 py-3">
<div className="flex gap-1">
<span className="h-2 w-2 rounded-full bg-th-primary animate-bounce" />
<span className="h-2 w-2 rounded-full bg-th-primary animate-bounce [animation-delay:150ms]" />
<span className="h-2 w-2 rounded-full bg-th-primary animate-bounce [animation-delay:300ms]" />
</div>
<span className="text-sm text-th-muted">お見積もりを更新中...</span>
</div>
)}
6. 実装チェックリスト
- アセスメントフォーム(マルチステップUI + プログレスバー)
- 動的質問追加ロジック(回答内容に応じた条件分岐)
- 「あなたの回答を分析中...」アニメーション
- 商品/サービスデータの準備(JSONファイル)
- ChatFiles構築関数(フォーム回答をINSTRUCTIONテンプレートに埋め込み)
- Extension型定義(
show_car_estimate等) - ランク付き提案カードコンポーネント(pros/cons、見積もり内訳表示)
- 3フェーズ遷移(
form→loading→result) - Extension生成中アニメーション(バウンスドット)
- フォローアップ入力によるプラン改善
- 「もう一度査定する」によるリセット機能
パターン3: AI操作補助 -- AIがフォームを自動入力、根拠とともに
1. 概要と解決する課題
Before(従来のUX): ユーザーがマニュアルを読み、入力ルールを覚え、すべてのフィールドに手動で入力する。特に人事評価のような複雑なフォームでは、1件あたり30分以上かかることもある。
After(AIによるUX): ユーザーがAIに「全項目を入力して」と頼むだけで、過去のデータ(1on1議事録など)に基づいてフォームが自動入力される。各評価には具体的なエピソードが根拠として示され、ユーザーはレビューと微調整のみ行えばよい。
適用ユースケース:
- 人事評価フォームの入力支援
- 物件情報の登録支援
- MBO/OKRの設定支援
- 設定画面の一括操作
- レポートテンプレートの自動入力
2. UXデザイン原則
- 具現化されたインタラクション: AIが「入力している」様子をアニメーションで可視化する。すべてのフィールドが瞬時に埋まるのではなく、フィールドごとに400msのディレイを入れて逐次的に入力する。ユーザーは「AIが一つずつ考えながら入力している」と認識し、信頼感が生まれる。
- アクティブフィールド表示: 現在AIが入力中のフィールドを紫色の枠線でハイライトし、そのフィールドまで自動スクロールする。ユーザーの視線がAIの動作を追従できる。
- 可逆性: AIが入力した値はユーザーが自由に編集できる。AIの提案は「出発点」であり「確定」ではない。編集可能であることを視覚的に示す。
- 根拠の明示: チャットのテキスト部分で、各評価の根拠を具体的なエピソードとともに説明する。「4月の1on1でプロジェクトリードとして技術選定を主導し...」のように、いつ・何を・どうしたかを明記する。
- 段階的な表示: フィールド間に400msのディレイを入れることで、情報の洪水を防ぎ、ユーザーが各フィールドの値を一つずつ認識できるようにする。
3. Extension仕様
TypeScript型定義:
export type FormFillExtension = {
action: "fill_evaluation_form";
payload: {
fields: {
fieldId: string;
value: string;
}[];
explanation: string;
};
};
JSONレスポンス例:
{
"action": "fill_evaluation_form",
"payload": {
"fields": [
{ "fieldId": "goalAchievement", "value": "A" },
{ "fieldId": "goalComment", "value": "売上目標120%達成に技術面で大きく貢献。チームvelocity18%向上も目標超過。" },
{ "fieldId": "leadership", "value": "4" },
{ "fieldId": "communication", "value": "4" },
{ "fieldId": "problemSolving", "value": "5" },
{ "fieldId": "expertise", "value": "5" },
{ "fieldId": "strengths", "value": "技術選定の的確さとプロジェクトリードとしての推進力。後輩育成にも積極的。" },
{ "fieldId": "improvements", "value": "ドキュメント整備の継続。チーム外連携の強化。" },
{ "fieldId": "nextGoals", "value": "ドキュメント整備率80%達成。チーム外ステークホルダーとの協業強化。" },
{ "fieldId": "overallRating", "value": "A" },
{ "fieldId": "overallComment", "value": "技術・成果・育成の各面で高いパフォーマンスを発揮。" }
],
"explanation": "4月の1on1でのプロジェクトリード就任と技術選定、6月のスプリント遅延リカバリー、8月の売上120%達成を総合的に評価。"
}
}
4. ChatFiles / プロンプト設計
このパターンでは、ChatFilesにフォームのフィールド定義とコンテキストデータ(1on1議事録など)を含めます。
export function buildEvaluationChatFiles() {
const instruction = `あなたは人事評価の入力を支援するAIアシスタントです。
過去の1on1議事録を通じて、従業員の活動記録を把握しています。
## 対象従業員情報
- 氏名: ${EMPLOYEE.name}
- 所属: ${EMPLOYEE.department}
- 役職: ${EMPLOYEE.role}
- 在籍: ${EMPLOYEE.tenure}
## MBO目標
${MBO_GOALS.map(
(g) => `${g.id}. ${g.title}(目標: ${g.target}、ウェイト: ${g.weight}%)`
).join("\n")}
## 1on1議事録データ
${ONE_ON_ONE_NOTES.map(
(note) => `### ${note.date} - ${note.summary}\n${note.content}`
).join("\n\n")}
## 評価フォームのフィールド一覧
- goalAchievement: 目標達成度(S / A / B / C / D から選択)
- goalComment: 目標達成に関するコメント(自由記述)
- leadership: リーダーシップ(1〜5の数値)
- communication: コミュニケーション(1〜5の数値)
...(全フィールドの定義)
## 絶対ルール
- extensionコードブロックは必ず含めてください
- すべてのvalueは文字列にしてください(数値も "4" のように)
## 回答形式
テキスト部分では、必ず1on1議事録の具体的なエピソードを引用しながら各評価の根拠を述べてください。
その後、必ずextensionコードブロックを含めてください。`;
return [{ filename: "instruction", content: instruction }];
}
プロンプト設計のポイント:
- コンテキストの充実: 1on1議事録のように、AIが根拠として引用できるリッチなソースデータを渡す。データが具体的であるほど、AIの出力も具体的になる。
- フィールドIDの明示: フォームの全フィールドIDと入力仕様(選択肢、数値範囲、自由記述)をプロンプトに含める。AIが不正なfieldIdを返すとフォームに反映できないため。
- 値の文字列制約: 数値フィールドも
"4"のように文字列で返すよう指示する。JSONパースの一貫性を保つため。 - 部分入力のサポート: 「目標達成度だけ評価して」のような部分指示にも対応できるよう、全フィールド必須ではなく「依頼されたフィールドのみ含める」ことも許容する。
5. 主要コンポーネント実装
逐次入力アニメーション:
const DELAY_PER_FIELD = 400;
const animationTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
// ExtensionからフォームFill情報を抽出
const allFormFills = useMemo(() => {
const fills: FormFillExtension["payload"][] = [];
for (const msg of messages) {
if (msg.role !== "assistant") continue;
const exts = parseExtensions(msg.text);
for (const ext of exts) {
if (ext.action === "fill_evaluation_form") {
fills.push(ext.payload);
}
}
}
return fills;
}, [messages]);
// 最新のFormFillをアニメーション付きで適用
useEffect(() => {
if (allFormFills.length === 0) return;
const latest = allFormFills[allFormFills.length - 1];
const validFields = latest.fields.filter((f) => f.fieldId in INITIAL_FORM_DATA);
// 前回のアニメーションタイマーをクリア
for (const t of animationTimers.current) clearTimeout(t);
animationTimers.current = [];
validFields.forEach((field, index) => {
const timer = setTimeout(() => {
// フィールドに値をセット
setFormData((prev) => ({
...prev,
[field.fieldId]: String(field.value),
}));
// アクティブフィールドをハイライト
setActiveField(field.fieldId);
setHighlightedFields((prev) => new Set(prev).add(field.fieldId));
// フィールドまで自動スクロール
const el = document.getElementById(`field-${field.fieldId}`);
if (el) {
const scrollContainer = document.getElementById("evaluation-form-scroll");
if (scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const offset = elRect.top - containerRect.top - containerRect.height / 3;
scrollContainer.scrollBy({ top: offset, behavior: "smooth" });
}
}
// ハイライトを次のフィールドへ移動
const deactivate = setTimeout(() => {
setActiveField((current) =>
current === field.fieldId ? null : current
);
}, DELAY_PER_FIELD * 0.8);
animationTimers.current.push(deactivate);
}, index * DELAY_PER_FIELD);
animationTimers.current.push(timer);
});
// すべてのフィールド入力後、3秒でハイライトを全解除
const clearTimer = setTimeout(
() => setHighlightedFields(new Set()),
validFields.length * DELAY_PER_FIELD + 3000
);
animationTimers.current.push(clearTimer);
}, [allFormFills]);
フィールドのハイライト表示:
<div
id={`field-${fieldId}`}
className={`
transition-all duration-300
${activeField === fieldId
? "ring-2 ring-violet-500 bg-violet-500/5" // 現在入力中: 強いハイライト
: ""}
${highlightedFields.has(fieldId)
? "bg-violet-500/3 ring-1 ring-violet-500/30" // 入力済み: 薄いハイライト
: ""}
`}
>
{/* フォームフィールド(select, textarea, rating etc) */}
</div>
スプリットペインのレイアウト:
return (
<div className="flex h-[calc(100vh-57px)] flex-col bg-th-page">
<div className="flex-1 overflow-hidden">
<SplitPane
left={formPanel} // 左: 評価フォーム
right={chatPanel} // 右: AIアシスタント
defaultLeftWidth={55}
minLeftWidth={30}
maxLeftWidth={70}
leftTabLabel="評価フォーム"
rightTabLabel="AIアシスタント"
/>
</div>
</div>
);
フォームが主役のため、左ペインを55%に設定してフォームを広く表示します。
6. 実装チェックリスト
- フォームコンポーネント(各フィールドに
id属性を付与、ハイライト/アクティブフィールド対応) - フォームフィールドの定義ファイル(fieldId、label、type の一覧)
- コンテキストデータの準備(1on1議事録、MBO目標など)
- ChatFiles構築関数(フィールド定義 + コンテキストデータ)
- Extension型定義(
fill_evaluation_form) - 逐次入力アニメーションシステム(
setTimeoutチェーン、activeField状態管理) - スクロール追従(入力中フィールドへの自動スクロール)
- ハイライトの段階的解除(入力完了3秒後に全解除)
- 手動編集の維持(AI入力後もフォームは自由に編集可能)
- 部分入力サポート(「リーダーシップだけ評価し直して」への対応)
パターン4: AI分析&改善提案 -- データを分析し、実行可能な改善施策を提案
1. 概要と解決する課題
Before(従来のUX): ユーザーがダッシュボードのデータを自分で読み解き、課題を特定し、改善策を考え、手動で実行する。データリテラシーに依存し、アクションにつながらないことが多い。
After(AIによるUX): AIがダッシュボードのKPIデータを自動分析し、具体的な改善施策を「実行ボタン付き」で提案する。ユーザーはボタンを押すだけで施策を適用でき、ダッシュボードのメトリクスが即座に更新される。
適用ユースケース:
- SaaS経営ダッシュボード
- ECサイト最適化(CVR改善、離脱率改善)
- マーケティング分析(広告効果、コスト最適化)
- カスタマーサクセス改善(チャーン防止、NPS向上)
- 在庫管理の最適化
2. UXデザイン原則
- アクション内蔵型: 改善提案が単なるテキストではなく、「実行する」ボタン付きのカードで表示される。提案から実行までのフリクションを最小化する。
- インパクト可視化: 改善施策を「実行」すると、ダッシュボードの該当メトリクスが即座に更新され、ハイライトアニメーションが発生する。施策の効果がリアルタイムに可視化される。
- 努力-効果マトリクス: 各施策に「工数: 低/中/高」「効果: 低/中/高」のバッジを表示し、どの施策を優先すべきか直感的に判断できるようにする。
- メトリクス-アクション連結: 各改善施策が影響するKPIを
targetMetricで明示的に紐づけ、施策実行時にどのメトリクスが変わるかを明確にする。 - フィルター操作: AIがダッシュボードの検索フィルターを操作して関連データに絞り込む。
update_filterExtensionにより、AIの分析文脈に合わせたデータビューを自動的に設定する。
3. Extension仕様
このパターンでは、2つのExtension型を同時に使用します。
Extension 1: show_improvements(改善施策の表示)
export type ImprovementExtension = {
action: "show_improvements";
payload: {
improvements: {
id: string;
targetMetric: string; // KPI ID: "revenue" | "activeUsers" | "conversionRate" | "churnRate"
title: string;
description: string;
currentValue: string;
projectedValue: string;
effort: string; // "低" | "中" | "高"
impact: string; // "低" | "中" | "高"
}[];
};
};
Extension 2: update_filter(ダッシュボードフィルター操作)
export type DashboardFilterExtension = {
action: "update_filter";
payload: {
period: string; // "2024年4月" 〜 "2024年9月"
category: string; // "all" | "marketing" | "server" | ...
keyword: string; // 検索キーワード
};
};
JSONレスポンス例(2つのExtensionを同時に返す):
// Extension 1: 改善施策
{
"action": "show_improvements",
"payload": {
"improvements": [
{
"id": "imp-1",
"targetMetric": "conversionRate",
"title": "LPのCTAボタン改善",
"description": "CTAボタンのテキストを「無料で始める」に変更し、色をオレンジに。",
"currentValue": "3.2%",
"projectedValue": "3.7%",
"effort": "低",
"impact": "高"
},
{
"id": "imp-2",
"targetMetric": "churnRate",
"title": "オンボーディングフロー改善",
"description": "新規ユーザーの初回体験を改善。チュートリアルの追加。",
"currentValue": "4.5%",
"projectedValue": "3.2%",
"effort": "中",
"impact": "高"
}
]
}
}
// Extension 2: フィルター操作(ユーザーがフィルター変更を求めた場合のみ)
{
"action": "update_filter",
"payload": {
"period": "2024年6月",
"category": "marketing",
"keyword": ""
}
}
4. ChatFiles / プロンプト設計
export function buildAnalyticsChatFiles() {
const metricsText = INITIAL_METRICS.map(
(m) =>
`- ${m.label} (${m.id}): ${m.format(m.value)}(前月比 ${m.change >= 0 ? "+" : ""}${m.change}${m.changeUnit})\n 6ヶ月トレンド: ${m.trend.join(" → ")}`
).join("\n");
const pageText = PAGE_DATA.map(
(p) => `- ${p.path}: PV ${p.pv.toLocaleString()} / CVR ${p.cvr}% / 直帰率 ${p.bounceRate}%`
).join("\n");
const costText = COST_DATA.map(
(c) => `- ${c.label}: ¥${(c.amount / 10000).toFixed(0)}万(前月比 ...)`
).join("\n");
const instruction = `あなたは経営ダッシュボードを分析するAIアナリストです。
## 現在のダッシュボードデータ
### KPI指標
${metricsText}
### ページ別アクセスデータ
${pageText}
### コスト分析(月次)
${costText}
## 回答形式
### Extension 1: 改善施策(show_improvements) -- 必ず含める
### Extension 2: フィルター操作(update_filter) -- ユーザーがフィルター操作を求めた場合のみ追加
## ルール
- improvementsは必ず2〜3件
- targetMetricはKPIのIDを指定
- effort/impactは「低」「中」「高」のいずれか`;
return [{ filename: "instruction", content: instruction }];
}
プロンプト設計のポイント:
- メトリクスの動的埋め込み: ダッシュボードの現在の値をINSTRUCTIONテンプレートに動的に埋め込む。AIは最新のデータに基づいて分析できる。
- 複数Extension型の使い分け:
show_improvementsは毎回必須、update_filterは条件付きと明記する。AIが不要なフィルター操作を行わないように。 - targetMetricの制約: KPIのIDを列挙し、AIがそこから選ぶよう制約する。存在しないIDを返されるとメトリクス更新ロジックが動作しない。
5. 主要コンポーネント実装
改善施策の「実行」処理:
const handleExecute = useCallback(
(improvementId: string) => {
const imp = improvements.find((i) => i.id === improvementId);
if (!imp) return;
// 実行済みとしてマーク
setExecutedIds((prev) => new Set(prev).add(improvementId));
// ダッシュボードのメトリクスを更新
const newValue = parseMetricValue(imp.projectedValue);
setMetrics((prev) =>
prev.map((m) => {
if (m.id === imp.targetMetric) {
const updated = { ...m, value: newValue, trend: [...m.trend] };
updated.trend.push(newValue);
if (updated.trend.length > 6) updated.trend.shift();
// 変化率を再計算
const prevValue = updated.trend[updated.trend.length - 2] || m.value;
updated.change = /* 差分計算 */;
return updated;
}
return m;
})
);
// メトリクスカードにハイライトアニメーション(3秒後に解除)
setUpdatedMetricIds((prev) => new Set(prev).add(imp.targetMetric));
setTimeout(() => {
setUpdatedMetricIds((prev) => {
const next = new Set(prev);
next.delete(imp.targetMetric);
return next;
});
}, 3000);
},
[improvements]
);
ImprovementCardコンポーネント:
export function ImprovementCard({ improvement, onExecute, executed }: Props) {
return (
<div className={`rounded-xl border p-4 transition-all duration-500 ${
executed
? "border-emerald-500/30 bg-emerald-500/5"
: "border-th-border-sec bg-th-card-alt/50"
}`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<p className="text-sm font-semibold text-th-primary">{improvement.title}</p>
<p className="mt-1 text-xs text-th-muted">{improvement.description}</p>
</div>
<div className="flex flex-col gap-1">
<Badge label={`効果: ${improvement.impact}`} variant={mapLevel(improvement.impact)} />
<Badge label={`工数: ${improvement.effort}`} variant={mapLevel(improvement.effort)} />
</div>
</div>
{/* 現在値 → 予測値 */}
<div className="mt-3 flex items-center gap-2">
<div className="rounded-md border bg-th-card px-2.5 py-1">
<p className="text-[10px] text-th-dim">現在</p>
<p className="text-sm font-bold text-th-tertiary">{improvement.currentValue}</p>
</div>
<span className="text-th-subtle">→</span>
<div className="rounded-md border border-blue-500/30 bg-blue-500/10 px-2.5 py-1">
<p className="text-[10px] text-blue-400">予測</p>
<p className="text-sm font-bold text-blue-300">{improvement.projectedValue}</p>
</div>
<div className="ml-auto">
{executed ? (
<span className="inline-flex items-center gap-1 rounded-lg bg-emerald-500/20 px-3 py-1.5 text-xs text-emerald-400">
実行済み
</span>
) : (
<button
onClick={() => onExecute(improvement.id)}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-th-primary"
>
実行する
</button>
)}
</div>
</div>
</div>
);
}
フィルター操作の反映:
// ストリーミング完了後にフィルターExtensionを適用
const applyFilterFromMessage = useCallback((text: string) => {
const exts = parseExtensions(text);
for (const ext of exts) {
if (ext.action === "update_filter") {
setFilter({
period: ext.payload.period || INITIAL_FILTER.period,
category: ext.payload.category || "all",
keyword: ext.payload.keyword || "",
});
// フィルター変更をハイライトで通知
setFilterHighlight(true);
setTimeout(() => setFilterHighlight(false), 3000);
}
}
}, []);
6. 実装チェックリスト
- ダッシュボードコンポーネント(KPIカード、トレンドチャート、テーブル)
- メトリクスデータ構造の定義(id、値、トレンド、変化率、フォーマット関数)
- ChatFiles構築関数(メトリクス値、ページデータ、コスト分析をテンプレートに埋め込み)
- Extension型定義(
show_improvements+update_filter) - ImprovementCardコンポーネント(実行ボタン、努力-効果バッジ、現在値→予測値)
- メトリクス更新ロジック(値、トレンド配列、変化率の再計算)
- ダッシュボードフィルター操作の反映
- 更新ハイライトアニメーション(メトリクスカード、フィルター部分)
- スプリットペイン(左: ダッシュボード、右: AIアナリスト)
パターン5: AIエージェント自律実行 -- AIが提案、人間が承認、AIが実行
1. 概要と解決する課題
Before(従来のUX): すべてのタスクを人間が計画・調査・判断・実行・管理する。繰り返しの改善作業(ナレッジベースの更新、コンテンツの品質管理など)に多大な工数がかかる。
After(AIによるUX): AIがバックグラウンドでデータを分析し、改善提案をカードとして提示する。人間はTinder風のスワイプ操作で承認/却下を判断するだけ。承認された提案はジョブキューに入り、AIが自律的に実行する。
適用ユースケース:
- ナレッジベース(FAQ)の改善
- コンテンツ管理(古い記事の更新、不足記事の作成)
- データクリーンアップ(重複除去、不整合修正)
- 定型レポート生成
- カスタマーサポートの品質改善
このパターンの特徴:
他の4パターンと異なり、このパターンはチャットUIを使いません。AIはバックグラウンドで分析を行い、結果を提案カードとして提示します。ユーザーとAIのインタラクションは「承認/却下」の二択のみです。また、Extensionも使用せず、AIの分析結果をローカルのデータモデルとして直接定義します。
2. UXデザイン原則
- 高速意思決定: Tinder風スワイプカードで、承認(右スワイプ)と却下(左スワイプ)の二択を直感的に操作する。詳細を読まなくても、カードの優先度バッジとタイトルだけで判断できるようにする。
- カードスタック: 3枚のカードを重ねて表示し、「まだ続きがある」感を演出する。奥のカードは
scale(0.96)+translateY(8px)で少し見えるようにする。 - エビデンス提示: 各提案カードに統計グリッド(4つのKPI)、ミニチャート(比較/トレンド/内訳の3種)、関連キーワードを含め、判断材料を十分に提供する。
- ジョブキュー: 承認された提案が即座にジョブとして右側のリストに追加される(
pending→running→completed)。個別実行と一括実行の両方をサポートする。 - 自律度の調整: 設定パネルで実行頻度(daily/weekly/monthly)、自動承認レベル(高優先度のみ自動/全て手動)、通知先(Slack/Email)を制御できる。本番環境では、これらの設定に基づいてAIの自律度を段階的に引き上げられる。
3. データモデル(Extensionではなくローカル状態)
このパターンでは、AIの分析結果をローカルのデータモデルとして定義します。
提案カードのデータ型:
export type ProposalType = "knowledge_gap" | "user_need" | "quality_improvement";
export type Proposal = {
id: string;
type: ProposalType;
title: string;
evidence: string;
action: string;
priority: "高" | "中" | "低";
category: string;
stats: { label: string; value: string; sub?: string }[];
relatedKeywords: string[];
chart?: MiniChart;
};
export type MiniChart =
| { type: "comparison"; label: string; current: number; baseline: number; unit: string }
| { type: "trend"; label: string; data: number[]; labels: string[] }
| { type: "breakdown"; label: string; segments: { name: string; value: number; color: string }[] };
ジョブのデータ型:
export type Job = {
id: string;
proposalId: string;
title: string;
action: string;
status: "pending" | "running" | "completed";
type: ProposalType;
};
エージェント設定のデータ型:
export type AgentSettings = {
scheduledEnabled: boolean;
frequency: "daily" | "weekly" | "monthly";
autoApprove: "high_only" | "all_manual";
notifySlack: boolean;
notifyEmail: boolean;
};
提案データの例:
export const PROPOSALS: Proposal[] = [
{
id: "prop-1",
type: "knowledge_gap",
title: "「返品期限の延長」に関するナレッジが不足",
evidence:
"直近30日で42件の問い合わせが発生。AI回答率23%(平均85%)。適切なナレッジ記事が存在しない。",
action: "返品ポリシーのFAQ記事を新規作成し、延長条件・申請手順を明記する",
priority: "高",
category: "返品・交換",
stats: [
{ label: "問い合わせ数", value: "42件", sub: "直近30日" },
{ label: "AI回答率", value: "23%", sub: "平均85%" },
{ label: "有人対応率", value: "77%", sub: "+52pt" },
{ label: "推定削減コスト", value: "¥126,000", sub: "/月" },
],
relatedKeywords: ["返品期限", "延長申請", "返品ポリシー"],
chart: {
type: "comparison",
label: "AI回答率 vs 平均",
current: 23,
baseline: 85,
unit: "%",
},
},
// ...
];
4. 提案カードの実装
カードスタックの表現:
// 残りの提案をスタックとして表示(最大3枚)
const stackCards = [];
for (let i = Math.min(currentIndex + 2, PROPOSALS.length - 1); i >= currentIndex; i--) {
stackCards.push({ proposal: PROPOSALS[i], depth: i - currentIndex });
}
{stackCards.map(({ proposal, depth }) => (
<div
key={proposal.id}
className="absolute inset-0 transition-all duration-300"
style={{
transform: `scale(${1 - depth * 0.04}) translateY(${depth * 8}px)`,
opacity: depth === 0 ? 1 : 0.6 - depth * 0.2,
zIndex: 10 - depth,
pointerEvents: depth === 0 ? "auto" : "none",
}}
>
<ProposalCard
proposal={proposal}
onApprove={handleApprove}
onReject={handleReject}
exitDirection={depth === 0 ? exitDirection : null}
/>
</div>
))}
奥のカードは scale を小さく、translateY で下にずらし、opacity を下げることで「重なっている」表現を実現します。pointerEvents: "none" で奥のカードへのクリックを無効化します。
スワイプアニメーション:
@keyframes swipeRight {
to {
transform: translateX(150%) rotate(20deg);
opacity: 0;
}
}
@keyframes swipeLeft {
to {
transform: translateX(-150%) rotate(-20deg);
opacity: 0;
}
}
// ProposalCardコンポーネント内
const exitClass = exitDirection === "right"
? "animate-[swipeRight_0.4s_ease-in_forwards]"
: exitDirection === "left"
? "animate-[swipeLeft_0.4s_ease-in_forwards]"
: "";
Tinder風の承認/却下ボタン:
<div className="flex items-center justify-center gap-6">
{/* 却下ボタン(X マーク) */}
<button
onClick={() => onReject(proposal.id)}
className="flex h-16 w-16 items-center justify-center rounded-full
border-2 border-red-500/30 bg-red-500/10 text-red-400
hover:border-red-500/60 hover:bg-red-500/20 hover:scale-110"
>
<XIcon />
</button>
{/* 承認ボタン(チェックマーク、少し大きい) */}
<button
onClick={() => onApprove(proposal.id)}
className="flex h-[72px] w-[72px] items-center justify-center rounded-full
border-2 border-emerald-500/30 bg-emerald-500/10 text-emerald-400
hover:border-emerald-500/60 hover:bg-emerald-500/20 hover:scale-110"
>
<CheckIcon />
</button>
</div>
承認ボタンを却下ボタンより一回り大きくすることで、ポジティブなアクションを視覚的に促します。
5. ジョブ実行システム
承認 → ジョブ作成:
const handleApprove = useCallback(
(id: string) => {
const proposal = PROPOSALS.find((p) => p.id === id);
if (!proposal) return;
setExitDirection("right"); // 右スワイプアニメーション開始
setProcessedCount((c) => c + 1);
const job: Job = {
id: `job-${id}`,
proposalId: id,
title: proposal.title,
action: proposal.action,
status: "pending",
type: proposal.type,
};
setTimeout(() => {
setJobs((prev) => [...prev, job]); // ジョブリストに追加
advanceCard(); // 次のカードへ
}, 400); // スワイプアニメーション完了後
},
[advanceCard]
);
バッチ実行:
const handleRunAll = useCallback(() => {
const pendingJobs = jobs.filter((j) => j.status === "pending");
pendingJobs.forEach((job, i) => {
// 各ジョブを800ms間隔で順次実行
setTimeout(() => {
// ステータスを「実行中」に
setJobs((prev) =>
prev.map((j) => (j.id === job.id ? { ...j, status: "running" } : j))
);
// 2秒後に「完了」
setTimeout(() => {
setJobs((prev) =>
prev.map((j) => (j.id === job.id ? { ...j, status: "completed" } : j))
);
}, 2000);
}, i * 800);
});
}, [jobs]);
全体レイアウト:
return (
<div className="flex h-[calc(100vh-57px)] flex-col bg-th-page">
{/* ヘッダー: エージェント名 + ステータス + 一括実行ボタン + 設定 */}
<header>...</header>
{/* モバイルタブ切替(カード / ジョブリスト) */}
<MobileTabSwitcher />
{/* メインコンテンツ */}
<div className="flex h-full">
{/* 左: カードスタック(デスクトップ 60%) */}
<div className="md:flex-[3]">
{isAnalyzing && <AnalyzingAnimation />}
{analysisReady && !allDone && <CardStack />}
{analysisReady && allDone && <CompletionSummary />}
</div>
{/* 右: ジョブリスト + 設定パネル(デスクトップ 40%) */}
<div className="md:flex-[2]">
<JobList jobs={jobs} onRunJob={handleRunJob} />
{showSettings && <SettingsPanel settings={settings} onChange={setSettings} />}
</div>
</div>
</div>
);
6. 実装チェックリスト
- 提案データモデルの定義(type, evidence, stats, chart, relatedKeywords)
- 提案タイプの設定(knowledge_gap / user_need / quality_improvement それぞれのラベル・色)
- ProposalCardコンポーネント(タイプバッジ、優先度バッジ、統計グリッド、ミニチャート、関連キーワード、提案アクション)
- スワイプアニメーション(CSS @keyframes + exitDirection状態管理)
- カードスタック表現(scale + translateY + opacity + zIndex)
- ミニチャートコンポーネント(比較チャート、トレンドチャート、内訳チャートの3種)
- ジョブ管理(作成、個別実行、一括実行、ステータス遷移)
- ジョブリストコンポーネント(pending/running/completedのステータス表示)
- 設定パネル(実行頻度、自動承認レベル、通知先の制御)
- プログレスインジケーター(処理済み件数 / 全件数)
- レスポンシブ対応(モバイルではタブ切替でカードとジョブリストを表示分け)
- 初期分析アニメーション(「お問い合わせログを分析中...」)
- (本番用)AIバッチ分析パイプラインの構築(定期的にデータを分析し提案を生成する仕組み)
独自パターンの設計
上記5つのパターンはあくまで代表例です。あなたのプロダクトに最適なパターンを設計するには、以下の3つを決めるだけです。
1. ChatFiles(AIに何を伝えるか)
- INSTRUCTION: AIのペルソナ、回答ルール、Extension仕様
- データ: AIが参照すべき情報(商品、ドキュメント、フォーム構造など)
- state: ユーザー属性(パーソナライゼーション用)
2. Extension(AIに何をさせるか)
- 表示系(
show_*): 検索結果カード、比較表、提案カードなどのリッチUI - 操作系(
fill_*,update_*): フォーム入力、フィルター変更、値の更新 - ナビゲーション系(
navigate): ページ遷移 - 関数呼び出し(
call_function): 任意のプロダクト内関数の実行 - なし: シンプルなテキスト応答のみ(チャットQ&A)
3. UI構成(どう見せるか)
- チャットウィジェット(右下フローティング): Q&A、ヘルプ
- スプリットペイン(チャット + 結果): 検索、提案、分析
- フォーム + チャット: 操作補助
- カードスタック + リスト: 自律実行
- 全画面チャット: オンボーディング、ヒアリング
この3要素を組み合わせることで、無限のバリエーションが生まれます。例えば「チャットQ&A」はExtension不要でChatFilesだけ、「ページ遷移アシスタント」はExtension navigate + 関数レジストリ、「マルチステップ操作」はExtension連鎖 + ナビゲーションキューで実現できます。
Part 3: 応用トピック
Chapter 11: プロダクト操作の高度なパターン
AIによるプロダクト操作を本格的に実装する際には、関数レジストリ、ページコンテキスト、フォーム連携、ページ遷移、セキュリティといった複数の仕組みを組み合わせる必要があります。本章ではそれぞれのパターンを簡潔に解説します。
11.1 関数レジストリ
AIが call_function Extensionで呼び出す関数を、レジストリに一元登録するパターンです。各関数には名前・説明・パラメータ定義・ハンドラを持たせ、AIが返すJSON内の name と args でディスパッチします。
type RegisteredFunction = {
name: string;
description: string;
parameters: {
name: string;
type: string;
required: boolean;
description: string;
}[];
handler: (args: Record<string, unknown>) => Promise<FunctionResult>;
autoFollowUp?: boolean;
};
const registry = new Map<string, RegisteredFunction>();
function registerFunction(fn: RegisteredFunction) {
registry.set(fn.name, fn);
}
async function executeFunction(
name: string,
args: Record<string, unknown>
): Promise<FunctionResult> {
const fn = registry.get(name);
if (!fn) return { success: false, message: `Unknown function: ${name}` };
return fn.handler(args);
}
登録する関数の例としては、以下のようなものがあります。
navigateTo-- 指定パスへのページ遷移setFormValue-- フォームフィールドへの値設定getCurrentUser-- ログインユーザー情報の取得showNotification-- トースト通知の表示
autoFollowUp を true に設定した関数は、実行結果を自動的にAIへ送り返し、次の応答を取得します。たとえば getCurrentUser の結果をAIに渡すことで、「田中さん、こんにちは」のようなパーソナライズされた応答を続けて得られます。
// autoFollowUpの利用例
registerFunction({
name: "getCurrentUser",
description: "ログイン中のユーザー情報を取得する",
parameters: [],
handler: async () => ({
success: true,
data: { name: "田中 太郎", role: "manager", plan: "Pro" },
}),
autoFollowUp: true, // 結果をAIに自動送信して次の応答を得る
});
11.2 ページコンテキストシステム
各ページで「何ができるか」「どの要素を操作できるか」をPageContextとして定義します。これにより、AIはページごとに適切な操作を提案できます。
type PageContext = {
pageName: string;
path: string;
description: string;
capabilities: string[];
elements: Record<
string,
| string
| {
selector: string;
allowedValues?: string[];
readOnly?: boolean;
}
>;
};
特に重要なのは elements の設計です。単純なテキスト入力であればセレクタ文字列だけで十分ですが、selectフィールドの場合は allowedValues で選択肢を明示します。AIはこれを参照して、有効な値だけを提案できるようになります。
const settingsPageContext: PageContext = {
pageName: "設定ページ",
path: "/settings",
description: "アカウント設定とプロファイル編集を行うページ",
capabilities: [
"プロファイル情報の編集",
"通知設定の変更",
"言語設定の変更",
],
elements: {
displayName: "#display-name-input",
language: {
selector: "#language-select",
allowedValues: ["ja", "en", "zh", "ko"],
},
bio: {
selector: "#bio-textarea",
},
emailVerified: {
selector: "#email-verified-badge",
readOnly: true,
},
},
};
このPageContextをChatFilesに含めることで、AIは「このページではプロファイル編集ができ、言語はja/en/zh/koから選べる」という情報を得た上で操作を提案します。
11.3 React Hook Form連携
useAssistantFormConnector フックを使って、AIからの setValue 呼び出しでReact Hook Formのフィールドを外部から操作可能にします。
// グローバルレジストリ
const globalFormRegistry = {
current: null as FormConnector | null,
register(connector: FormConnector) {
this.current = connector;
},
unregister() {
this.current = null;
},
};
// フックの定義
function useAssistantFormConnector<T extends FieldValues>(
form: UseFormReturn<T>
) {
useEffect(() => {
globalFormRegistry.register({
setValue: (name, value) =>
form.setValue(name as any, value as any, {
shouldDirty: true,
shouldValidate: true,
}),
getValues: () => form.getValues(),
});
return () => globalFormRegistry.unregister();
}, [form]);
}
このフックを使う際に注意すべき点が2つあります。
- 数値型の自動変換: AIが
"85"のような文字列を返した場合、フィールドのスキーマがnumberならNumber変換を行う - バリデーショントリガー:
shouldValidate: trueを渡すことで、setValue直後にバリデーションが走り、エラー表示が即座に反映される
// フォームコンポーネントでの利用
function EvaluationForm() {
const form = useForm<EvaluationSchema>({
resolver: zodResolver(evaluationSchema),
});
// このフックを呼ぶだけでAIからのフォーム操作が可能になる
useAssistantFormConnector(form);
return <form onSubmit={form.handleSubmit(onSubmit)}>{/* ... */}</form>;
}
11.4 ページ遷移と逐次実行
AIが「設定ページに移動して、言語をjaに変更して」のように、ページ遷移を含む複数ステップの操作を指示する場合に問題が生じます。navigate 実行後にページが再マウントされるため、後続のExtension(言語変更)が失われてしまうのです。
解決策: sessionStorageにキューを保存し、遷移先のページで復元して実行します。TTLは30秒とし、古いキューは無視します。
// navigate時: 残りのExtensionをsessionStorageに保存
function saveNavigationQueue(extensions: Extension[]) {
sessionStorage.setItem(
"ai_nav_queue",
JSON.stringify({
extensions,
timestamp: Date.now(),
})
);
}
// 新しいページのuseEffect: キューを復元して実行
useEffect(() => {
const queue = sessionStorage.getItem("ai_nav_queue");
if (queue) {
const { extensions, timestamp } = JSON.parse(queue);
if (Date.now() - timestamp < 30000) {
sessionStorage.removeItem("ai_nav_queue");
executeExtensionsSequentially(extensions);
}
}
}, []);
executeExtensionsSequentially では、各Extensionを順番に実行し、それぞれの完了を待ってから次に進みます。これにより、「ページ遷移 → フォーム入力 → 通知表示」のような一連の操作が確実に実行されます。
11.5 セキュリティとユーザー承認
AIがDOMを操作するシステムでは、セキュリティ対策が不可欠です。
DOMセレクタのバリデーション
AIが生成するセレクタに悪意のある要素が含まれないよう、ブラックリストでフィルタリングします。
const BLOCKED_SELECTORS = ["script", "iframe", "object", "embed"];
const BLOCKED_ATTRIBUTES = ["onclick", "onerror", "onload", "onmouseover"];
function validateSelector(selector: string): boolean {
const lower = selector.toLowerCase();
if (BLOCKED_SELECTORS.some((s) => lower.includes(s))) return false;
if (BLOCKED_ATTRIBUTES.some((a) => lower.includes(a))) return false;
return true;
}
値のサニタイズ
AIが入力する値についても、XSS対策としてHTMLタグやスクリプトを除去します。
function sanitizeValue(value: string): string {
return value.replace(/<[^>]*>/g, "").replace(/javascript:/gi, "");
}
request_action Extension: 危険な操作の承認
データ削除や課金操作など、取り消しが困難な操作については request_action Extensionを使い、ユーザーに確認ダイアログを表示します。
{
"action": "request_action",
"payload": {
"title": "ナレッジデータを全削除",
"description": "すべてのナレッジを削除します。この操作は元に戻せません。",
"danger": true,
"actionToExecute": {
"action": "call_function",
"payload": { "name": "deleteAllKnowledge", "args": {} }
}
}
}
ユーザーが「承認」を押した場合のみ、actionToExecute に含まれる本来の操作が実行されます。danger: true のフラグにより、UIは赤色の警告スタイルで表示され、誤操作を防ぎます。
Chapter 12: 動的コンテキストストア
問題
ChatFilesはリクエストごとに再構築されるため、「前のターンでAPIから取得したデータ」を次のターンで参照できません。たとえば、Turn 1で「Webhook一覧を見せて」と聞いて一覧を取得しても、Turn 2で「2番目を無効にして」と言ったとき、AIはどのWebhookが「2番目」なのかわかりません。
解決策: Dynamic Context Store
会話中にfetchしたデータをTTL付きでメモリに保持し、ChatFiles構築時に fetched_data.md として自動的に追加します。
type ContextEntry = {
key: string;
label: string;
data: unknown; // 関数アクセス用の生データ
summary: string; // AI参照用のテキスト要約
createdAt: number;
ttl: number; // ミリ秒
};
const contextStore = new Map<string, ContextEntry>();
function setDynamicContext(entry: Omit<ContextEntry, "createdAt">) {
contextStore.set(entry.key, {
...entry,
createdAt: Date.now(),
});
}
function getDynamicContext(key: string): ContextEntry | null {
const entry = contextStore.get(key);
if (!entry) return null;
if (Date.now() - entry.createdAt > entry.ttl) {
contextStore.delete(key);
return null;
}
return entry;
}
function removeDynamicContext(key: string) {
contextStore.delete(key);
}
ChatFilesへの統合
ChatFiles構築時に、有効なContextEntryの summary をまとめて fetched_data.md として追加します。
function buildChatFiles(baseFiles: ChatFile[]): ChatFile[] {
const dynamicEntries = getAllValidContextEntries();
if (dynamicEntries.length === 0) return baseFiles;
const fetchedDataContent = dynamicEntries
.map((e) => `## ${e.label}\n${e.summary}`)
.join("\n\n");
return [
...baseFiles,
{
filename: "fetched_data.md",
content: fetchedDataContent,
},
];
}
具体的なフロー
Turn 1: ユーザー「Webhook一覧を見せて」
→ AI: call_function("getWebhookList")
→ 関数実行 → 結果を取得:
[
{ id: "wh_001", name: "注文通知", enabled: true },
{ id: "wh_002", name: "在庫アラート", enabled: true },
{ id: "wh_003", name: "日次レポート", enabled: false }
]
→ Dynamic Contextに保存:
key: "webhook_list"
summary: "1. 注文通知 (wh_001) - 有効\n2. 在庫アラート (wh_002) - 有効\n3. 日次レポート (wh_003) - 無効"
ttl: 300000 (5分)
→ AI応答:「3件のWebhookがあります」
Turn 2: ユーザー「2番目を無効にして」
→ ChatFiles構築時にfetched_data.mdが追加される
(内容: Webhook一覧のsummary)
→ AIが「2番目」=「在庫アラート (wh_002)」と解決
→ call_function("disableWebhook", { id: "wh_002" })
→ AI応答:「在庫アラートを無効にしました」
このパターンにより、会話の文脈を維持しながらマルチターンの操作が可能になります。TTLの設定により、古くなったデータが自動的に破棄される点も重要です。
Chapter 13: AIページ分析 & タスク自動実行
「分析」ボタン1クリックでAIが現在のページを走査し、改善タスクを自動生成するパターンです。ユーザーはタスクを個別に実行することも、一括実行することもできます。
分析プロセス
- buildLightweightContext: ページのDOM情報を軽量なテキストに変換する。全DOMを送るのではなく、フォームフィールド・テーブル・ナビゲーションなど重要な要素だけを抽出する
- API送信: 軽量コンテキストをAIに送り、分析を依頼する
- analysis_result パース: AIの応答から
analysis_resultExtensionを抽出する - タスク表示: 各タスクをカードとしてレンダリングし、実行ボタンを付与する
// 分析結果のExtension例
{
"action": "analysis_result",
"payload": {
"summary": "このページには3つの改善点があります",
"tasks": [
{
"id": "task_1",
"title": "フォームの未入力フィールドを補完",
"description": "「目標」フィールドが空欄です。過去データから推奨値を入力します",
"priority": "high",
"extensions": [
{
"action": "fill_evaluation_form",
"payload": {
"fields": [{ "name": "goal", "value": "売上前年比120%" }]
}
}
]
},
{
"id": "task_2",
"title": "ダッシュボードフィルターの最適化",
"description": "現在「全期間」が選択されています。直近30日に絞ると傾向が見やすくなります",
"priority": "medium",
"extensions": [
{
"action": "update_filter",
"payload": { "period": "last_30_days" }
}
]
}
]
}
}
ビジュアルフィードバック
タスク実行時には、ユーザーが「AIが何をしているか」を視覚的に理解できるフィードバックを提供します。
- Element highlighting: 操作対象の要素をハイライト表示する。該当要素の周囲に青い枠線を表示し、操作中であることを示す
- AI cursor animation: マウスカーソルのようなアニメーションで、AIが「ここを操作している」という感覚を伝える
- Completion effects: タスク完了時にチェックマークアニメーションや緑色のフラッシュで成功を伝える
これらのフィードバックにより、AIの自動操作がブラックボックスにならず、ユーザーが安心して利用できるようになります。
Chapter 14: クロスパターン設計指針
Part 2で紹介した5つのパターンに共通する設計原則をまとめます。
14.1 Extension設計の原則
1. action名は 動詞_対象 形式にする
show_travel_plan -- 旅行プランを表示する
fill_evaluation_form -- 評価フォームを入力する
update_filter -- フィルターを更新する
show_improvements -- 改善施策を表示する
action名がそのまま「何をするか」を説明するようにします。
2. payloadにはUIレンダリングに必要な全情報を含める
Extensionを受け取ったコンポーネントが、追加のAPIコールなしにUIを描画できるようにします。たとえば show_travel_plan には、タイトル・日程・スポット情報・画像URL・地図座標など、プランカードの表示に必要なすべてのデータを含めます。
3. 1レスポンスに複数Extensionを含めてよい
たとえば「ダッシュボードを分析して改善してほしい」というリクエストに対し、show_improvements と update_filter を同時に返すことで、施策の提案とフィルター変更を1回の応答で実行できます。
4. 型定義は明示的に
AIがJSON例を参照してレスポンスを生成するため、TypeScriptの型定義と具体的なJSON例の両方をINSTRUCTIONに含めます。型だけでは曖昧な場合があるため、具体例が確実な出力につながります。
14.2 ChatFiles INSTRUCTION テンプレート
すべてのパターンに共通するINSTRUCTIONの構造は以下のとおりです。
# 指示
あなたは[ペルソナ]です。
## ユーザー情報(stateから取得済み)
[stateの活用方法]
## コンテキストデータ
[この画面で参照すべきデータの説明]
## 絶対ルール
- extensionコードブロックは必ず含めてください。省略は禁止です
- [パターン固有のルール]
## 回答形式
テキスト部分は[簡潔さの指定]。
その後、必ず以下のextensionコードブロックを含めてください。
```extension
{ JSON例 }
フォローアップ対応
- [フォローアップ時の振る舞い]
- フォローアップ時も毎回必ずextensionを含めてください
各セクションの役割を理解しておくと、新しいパターンを作る際にスムーズに設計できます。
- **ペルソナ**: AIの振る舞いの基調を決める(「旅行コンシェルジュ」「評価サポーター」など)
- **ユーザー情報**: パーソナライゼーションの根拠となるデータ
- **コンテキストデータ**: 参照すべきデータの所在と使い方
- **絶対ルール**: 守らせたい制約(Extension出力の必須化など)
- **回答形式**: テキストの長さとExtensionの構造を具体例で指定
- **フォローアップ対応**: 2回目以降のターンでの振る舞い
#### 14.3 stateによるパーソナライゼーション
全パターン共通で、ユーザーの属性をstateとしてmiiboに渡します。stateはCRM、CDP、広告パラメータなど様々なソースから取得できます。
```typescript
// CRM / CDP / 広告パラメータ等から取得
const userState = {
userName: "田中 太郎",
age: "32",
region: "東京都渋谷区",
memberTier: "ゴールド会員",
// パターン固有の属性
travelHistory: "沖縄2回・北海道1回", // Pattern 1: AI情報検索
adSource: "Instagram広告", // Pattern 2: AIヒアリング&提案
role: "エンジニアリング部マネージャー", // Pattern 3: AI操作補助
plan: "Proプラン", // Pattern 4: AI分析&改善提案
};
miiboのプロンプト内で #{キー名} 記法を使うと、stateの値を変数として直接参照できます。例: ユーザーの地域: #{region}、会員ランク: #{memberTier}。これにより「田中さん、ゴールド会員限定のプランをご提案します」のようなパーソナライズされた応答が実現できます。
分析パラメータとしての活用: stateはプロンプトのパーソナライズだけでなく、miibo管理画面のログ分析画面で集計用パラメータとしても機能します。
region・memberTier・adSourceなどで会話ログをセグメント分析でき、マーケティング施策やプロダクト改善のデータとして活用できます。
14.4 AIに何を任せ、何を人間に残すか
| パターン | AIの役割 | 人間の役割 | 自律度 |
|---|---|---|---|
| 1. AI情報検索 | 検索・構造化・提案 | 質問・選択・改善指示 | ★★☆☆☆ |
| 2. AIヒアリング&提案 | ヒアリング・分析・提案 | 回答・比較・決定 | ★★☆☆☆ |
| 3. AI操作補助 | フォーム入力・根拠説明 | 確認・修正・承認 | ★★★☆☆ |
| 4. AI分析&改善提案 | 分析・施策提案・実行 | 承認・優先順位付け | ★★★★☆ |
| 5. AIエージェント自律実行 | 分析・計画・実行 | 承認のみ(approve/reject) | ★★★★★ |
Pattern 1から5に進むほど、AIの自律度が上がります。それに伴い、人間の役割は「作業者」から「承認者」へと変化します。
重要なのは、自律度が高ければ良いというわけではないということです。操作の重要度やリスクに応じて適切な自律度を選択してください。たとえば、情報検索(Pattern 1)を完全自律にする必要はなく、逆に単純な設定変更を毎回承認制にするのはユーザー体験を損ねます。
Chapter 15: プロンプト設計ガイドライン
INSTRUCTIONの書き方次第で、AIの出力品質は大きく変わります。本章では、実装を通じて得られた実践的なプロンプト設計のガイドラインを紹介します。
15.1 Extension出力の確実化
AIがExtensionを省略してしまう問題は最も頻繁に発生します。以下のルールをINSTRUCTIONに必ず含めてください。
## 絶対ルール
- extensionコードブロックは必ず含めてください。省略は禁止です。
- フォローアップ時も毎回必ずextensionを含めてください。
「省略は禁止」という強い表現がポイントです。「できれば含めてください」のような弱い表現では、AIが省略する場合があります。
15.2 テキストの簡潔さ
AIは放っておくと長文を生成しがちです。Extensionで構造化データを返すパターンでは、テキスト部分は短くするよう明示します。
テキスト部分は2〜3行の簡潔な概要のみにして、詳細はextensionに任せてください。
長い前置きは不要です。
これにより、ストリーミング表示中にユーザーが長文を読まされることなく、すぐにExtensionによるリッチなUI表示に移行できます。
15.3 データの活用
ChatFilesに含めたデータを正しく参照させるための指示です。
- available_dataのデータから最適なものを選んでください
- IDはavailable_dataのidをそのまま使ってください
「idをそのまま使え」という指示がないと、AIが存在しないIDを生成してしまうことがあります。
15.4 否定の排除
AIが「該当するものがありません」と回答してしまうと、ユーザー体験が途切れます。
- 必ず2〜3件提案してください。「見つかりません」「該当なし」は絶対に言わないでください。
- 完全一致しなくても、条件に最も近いものを前向きに提案してください。
このルールにより、AIは常に何かしらの提案を行い、会話が前に進みます。
15.5 optionsの活用
フォローアップのクイック返信ボタン(options)を活用し、ユーザーが次のアクションを取りやすくします。
- optionsには必ず次のアクション候補を3つ入れてください
(例:「予算をもっと抑えたい」「もう1件見たい」「条件を変更」)
optionsがあることで、ユーザーはテキスト入力の手間なく会話を続けられます。特にモバイル環境では、タップだけで操作できることの価値が大きくなります。
Appendix
A: トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| AIがExtensionを出力しない | INSTRUCTIONにExtension形式の指定がない | 「必ずextensionコードブロックを含めてください」を追加 |
| フォームの値が変更されない | useAssistantFormConnectorが未適用 | フォームコンポーネントでhookを呼ぶ |
| ページ遷移後の操作が失われる | navigationQueue未実装 | sessionStorageでExtensionを保存・復元 |
| Extensionのパースが失敗する | JSONが不正 | fixJsonEscapingでエスケープを修正 |
| ChatFilesが大きすぎてコスト高 | データをフィルタリングしていない | クエリに関連するデータだけに絞る |
| ストリーミング中にJSONが見える | stripExtensionsStreamingを使っていない | 表示テキストにstripExtensionsStreamingを適用 |
B: ファイル構成の全体像
src/
├── app/
│ ├── api/chat/route.ts # miibo APIプロキシ
│ ├── page.tsx # トップページ
│ └── demos/
│ ├── layout.tsx # デモ共通レイアウト
│ ├── search/page.tsx # Pattern 1: AI情報検索
│ ├── acquisition/page.tsx # Pattern 2: AIヒアリング&提案
│ ├── operation/page.tsx # Pattern 3: AI操作補助
│ ├── analytics/page.tsx # Pattern 4: AI分析&改善提案
│ └── autonomous/page.tsx # Pattern 5: AIエージェント自律実行
├── components/
│ ├── chat/
│ │ ├── SplitPane.tsx # 共通: スプリットレイアウト
│ │ ├── FollowUpInput.tsx # 共通: フォローアップ入力
│ │ ├── SearchHero.tsx # Pattern 1: 検索ヒーロー
│ │ ├── MessageList.tsx # Pattern 1: メッセージ+プラン
│ │ ├── TravelPlanCard.tsx # Pattern 1: 旅行プランカード
│ │ ├── TravelMap.tsx # Pattern 1: 地図表示
│ │ ├── AcquisitionHero.tsx # Pattern 2: 見積もりヒーロー
│ │ ├── AcquisitionMessageList.tsx # Pattern 2: メッセージ+見積もり
│ │ └── CarEstimateCard.tsx # Pattern 2: 見積もりカード
│ ├── acquisition/
│ │ └── CarAssessmentForm.tsx # Pattern 2: アセスメントフォーム
│ ├── operation/
│ │ └── EvaluationForm.tsx # Pattern 3: 評価フォーム
│ ├── analytics/
│ │ ├── Dashboard.tsx # Pattern 4: ダッシュボード
│ │ └── ImprovementCard.tsx # Pattern 4: 改善カード
│ └── autonomous/
│ ├── ProposalCard.tsx # Pattern 5: 提案カード
│ ├── JobList.tsx # Pattern 5: ジョブリスト
│ └── SettingsPanel.tsx # Pattern 5: 設定パネル
├── lib/
│ ├── parse-extensions.ts # Extension型定義+パーサー
│ ├── travel-data.ts # Pattern 1: データ+ChatFiles
│ ├── car-data.ts # Pattern 2: データ+ChatFiles
│ ├── operation-data.ts # Pattern 3: データ+ChatFiles
│ ├── analytics-data.ts # Pattern 4: データ+ChatFiles
│ └── autonomous-data.ts # Pattern 5: データモデル
└── constants/
└── demos.ts # デモ一覧定義
C: Extension型定義一覧
| Extension | action | 用途 | パターン |
|---|---|---|---|
| TravelPlanExtension | show_travel_plan | 旅行プランの構造化表示 | 1: AI情報検索 |
| CarEstimateExtension | show_car_estimate | 車見積もりの構造化表示 | 2: AIヒアリング&提案 |
| FormFillExtension | fill_evaluation_form | フォーム自動入力 | 3: AI操作補助 |
| ImprovementExtension | show_improvements | 改善施策の提案表示 | 4: AI分析&改善提案 |
| DashboardFilterExtension | update_filter | ダッシュボードフィルター操作 | 4: AI分析&改善提案 |
D: クイックスタートチェックリスト
- miiboアカウント作成、エージェント作成
- api_key と agent_id を取得
- Next.js APIルートでmiiboプロキシを作成(Chapter 1)
- ストリーミング受信を実装(Chapter 2)
- パターンを選択(Section 0.2)
- Extension型を定義(Chapter 4 or 各パターンセクション)
- ChatFiles(INSTRUCTION + データ)を設計(Chapter 3 + 各パターンセクション)
- UIコンポーネントを実装(各パターンセクション)
- stateでパーソナライゼーション(Chapter 14)
- テスト: ストリーミング表示、Extension解析、クイック返信
E: 導入で実現できる未来
フロントエンドで実現できること
- CSコスト 70%削減: AIチャットが一次対応を自動化し、人間のオペレーターは複雑な案件に集中できる
- オンボーディング革命: マニュアル不要。AIが操作をリアルタイムでガイドし、新規ユーザーの離脱を防ぐ
- アクセシビリティ向上: キーボードやマウスに依存せず、自然言語でソフトウェアを操作できる
- ワークフロー簡略化: 複数ステップの操作が一言で完了。「先月の売上レポートをSlackに送って」がワンアクションになる
- AIネイティブプロダクト層: 既存のUIを変更することなく、AI操作レイヤーを上から被せて追加できる。段階的な導入が可能
その先の展望: バックエンド・APIレイヤーへの拡張
本ガイドはフロントエンドのAI UXに焦点を当てていますが、miibo AIの活用はフロントエンドに限りません。
APIインターフェースのラッピング
miibo AIでバックエンドのAPIをラッピングすることで、フロントエンドを持たない領域でも自律的に動作するAIエージェントを構築できます。
graph TB
subgraph agent["miibo AIエージェント"]
direction TB
cf["ChatFiles
API仕様書 (OpenAPI / Swagger)
ビジネスルール
実行コンテキスト"]
ext["Extensions
call_function → API呼び出し
バッチ処理の実行
外部サービス連携"]
io["入力: 自然言語 or イベントトリガー
出力: API操作 + 結果レポート"]
end
cf --> ext --> io
io --> apis["既存のAPI群
REST / gRPC / DB / SaaS"]
MCPインターフェースとの統合
MCP(Model Context Protocol)サーバーをmiibo AIのExtensionsとして接続することで、AIが外部ツール・データベース・SaaSを直接操作する自律エージェントが実現できます。
想定されるユースケース:
| ユースケース | 動作 |
|---|---|
| データパイプライン管理 | 「昨日のETLが失敗しているから再実行して」→ AIがログを分析、原因を特定、修正して再実行 |
| インフラ監視・対応 | アラート受信 → AIが原因を調査 → 自動スケーリングや再起動を実行 → Slackに報告 |
| 定型業務の自動化 | 「月末レポートを作成して全部署に配信して」→ DB集計 → レポート生成 → メール送信 |
| クロスサービス連携 | 「この顧客の問い合わせ履歴をCRMとヘルプデスクから集めて」→ 複数API呼び出し → 統合レポート |
本ガイドとの関係: バックエンドAIエージェントも、本ガイドと同じ基盤(ChatFiles でコンテキストを渡し、Extensions で操作を指示する)で動作します。フロントエンドの「ユーザーの代わりにUIを操作する」が、バックエンドでは「ユーザーの代わりにAPIを操作する」に変わるだけです。フロントエンドとバックエンド両方のAIを組み合わせれば、ユーザーとの対話からバックエンド処理まで、エンドツーエンドでAIが一貫して動作するプロダクトが実現します。
このガイドは AI UX Showcase の実装に基づいています。 miibo: https://miibo.jp
関連リンク:
- miiboマニュアル — プラットフォームの詳細な使い方
- miibo API リファレンス — チャットAPIの完全な仕様
by miibo, Inc.