masta + assistant-uiでclaude artifact風の機能を作成する方法(わかる人向け)
MastraとAssistant-uiでClaudeのArtifact風の機能を作ると起動しているかという部分での解説をしてほしいという要望があったので、かなりざっくり書いています。
assistant-uiとは
Assistant-uiは、AIチャットUI構築用のオープンソースTypeScript/Reactライブラリです。
簡単にチャット風の画面が作れるものになります。
Vercel AI SDK、LangGraph、Mastraと連携可能で、shadcn/ui風のcomposableなprimitiveでストリーミングやアクセシビリティを標準サポートしています。
こう言ったものが、ある程度簡単に作れるUIライブラリです。

assistant-ui公式のサンプルコード
以下に掲載されています。
大まかな流れ
-
artifact用のツールをmastraおよびLLMを呼び出すbackend側に用意しておきます。
-
ユーザーのメッセージをMastra経由でLLMに送信し、LLMが「HTMLを表示すべき」と判断するとartifactsツールを呼び出します。
-
フロントエンドはストリーム応答内のtool-callを検出し、ToolUIコンポーネントでボタンを表示。
tool-uiはこんな感じ
tool-callの結果を表示するUIコンポーネントは、assistant-uiでは 「Tool UI」 と呼ばれています。
MCPとかtoolの呼び出しをしたときにその結果をビジュアルに表示してくれるやつ。

これを使ってArtifact風のデザインをします
- ユーザーがクリックするとカスタムイベント(show-artifact)が発火し、ArtifactsViewがiframeでHTMLをレンダリングする。ソース表示時はsugar-highでハイライト表示。
という風な形にしてます。
フロントエンド ↔ Mastra 間のシーケンス
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
│ ユーザー │ │ Frontend │ │ Mastra │ │ LLM API │
│ │ │ (React/Next.js) │ │ │ │ (OpenAI等) │
│ │ │ │ │ │ │ │
└──────┬──────┘ └────────┬────────┘ └──────┬───────┘ └──────┬──────┘
│ │ │ │
│ 1. メッセージ入力 │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ 2. メッセージ変換 │ │
│ │ convertMessages │ │
│ │ ToMastraFormat() │ │
│ │ │ │
│ │ 3. POST /api/agents │ │
│ │ /{agentId}/stream│ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ 4. agent.stream() │
│ │ │───────────────────>│
│ │ │ │
│ │ │ 5. ストリーム応答 │
│ │ │<───────────────────│
│ │ │ (tool_call含む) │
│ │ │ │
│ │ 6. DataStreamDecoder│ │
│ │<────────────────────│ │
│ │ ストリームをパース │ │
│ │ │ │
│ │ 7. tool-call検出 │ │
│ │ toolName='artifacts' │
│ │ │ │
│ 8. ToolUI表示 │ │ │
│<────────────────────│ │ │
│ 「クリックして表示」 │ │ │
│ │ │ │
│ 9. ボタンクリック │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ 10. CustomEvent │ │
│ │ 'show-artifact'発火 │ │
│ │ │ │
│ 11. ArtifactsView │ │ │
│<────────────────────│ │ │
│ でHTML表示(iframe)│ │ │
│ │ │ │
処理の詳細
1. メッセージ送信(Frontend)
ファイル: app/assistant.tsx 内の MyCustomAdapter
const MyCustomAdapter: ChatModelAdapter = {
async *run({ messages, abortSignal }) {
// 最新メッセージをMastra形式に変換
const convertedMessages = convertMessagesToMastraFormat([latestMessage]);
// Mastraへストリームリクエスト
const response = await fetch(
`http://mastraのURL:port番号/api/agents/${agentId}/stream`,
{
method: 'POST',
body: JSON.stringify({
messages: convertedMessages,
threadId,
resourceId,
maxSteps
})
}
);
// ストリームをパースして順次yield
const stream = response.body
.pipeThrough(new DataStreamDecoder())
.pipeThrough(new AssistantMessageAccumulator());
// ...
}
};
2. Mastra側のエージェント処理
ファイル: server/src/mastra/agents/index.ts
class CustomAgent extends Agent {
async stream(messages, options) {
// LLMへリクエスト(tool定義含む)
return super.stream(messages, {
...options,
experimental_generateMessageId: generateCustomMessageId
});
}
}
3. Artifactsツール定義
ファイル: components/artifacts-tool.tsx
export const ArtifactsTool = createTool({
toolName: 'artifacts',
parameters: z.object({
code: z.string().describe('The HTML code to render')
}),
execute: async ({ code }) => {
// フロントエンドで実行されるため、成功を返すのみ
return { success: true, message: 'HTML artifact created successfully' };
}
});
4. ToolUI表示
ファイル: components/artifacts-tool.tsx
const RenderComponent = ({ args, status }) => {
const showArtifact = () => {
// カスタムイベントを発火
window.dispatchEvent(
new CustomEvent('show-artifact', { detail: { code: args.code } })
);
};
return (
<button onClick={showArtifact}>
HTMLアーティファクト • クリックして表示
</button>
);
};
5. ArtifactsView表示
ファイル: components/artifacts-view.tsx
export const ArtifactsView = ({ children }) => {
useEffect(() => {
const handleShowArtifact = (event) => {
setCurrentArtifact(event.detail.code);
setIsVisible(true);
};
window.addEventListener('show-artifact', handleShowArtifact);
// ...
}, []);
return (
<iframe srcDoc={artifact} sandbox="allow-scripts" />
);
};
HTMLのプレビューと、コードの切り替え(コードのハイライトも含む)
┌─────────────────────────────────────────────┐
│ アーティファクト用のコンポーネント │
│ ┌─────────────────────────────────────┐ │
│ │ Toggle: [Preview] [Source] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ viewMode === 'preview' │ │
│ │ → <iframe srcDoc={artifact} /> │ │
│ │ │ │
│ │ viewMode === 'source' │ │
│ │ → <pre><code>{highlight(...)}</code> │
│ │ ↑ sugar-high でハイライト │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
実現方法
1. 状態管理
const [viewMode, setViewMode] = useState<'preview' | 'source'>('preview');
2. 条件分岐による表示切り替え
{viewMode === 'preview' ? (
// iframe でHTMLをレンダリング
<iframe srcDoc={artifact} sandbox="allow-scripts" />
) : (
// コードをシンタックスハイライト付きで表示
<pre>
<code dangerouslySetInnerHTML={{ __html: highlight(artifact) }} />
</pre>
)}
コードハイライト
使用ライブラリ: sugar-high
import { highlight } from 'sugar-high';
呼び出し方
「HTMLで~を出力して」とか「artifactで~を出力して」みたいに指示すればartifactっぽく呼び出せます。
※ 諸事情ありスクリーンショットは掲載できないですが(個別にいってもらえればイメージは見せることが可能です)
Discussion