ローカルLLMをブラウザから扱えるチャットUIは、既製のWeb UIでも足ります。ただしモデル切り替えやダウンロード進捗まで含めて自分の画面に寄せたいなら、フロントエンドから組み立てる方が早いです。
この記事では、開発者Matt Palmerlee氏が公開した実装手順をもとに、OllamaとVueでローカルLLMチャットUIを作る流れを整理します。
この記事でわかること
- OllamaのJavaScriptライブラリでストリーミング応答を表示する方法
- PrimeVueでチャット画面の入力欄やメッセージ表示を整える手順
- TanStack Queryでモデル一覧を取得・更新する仕組み
- ollama.pullの進捗表示とモデル追加ダイアログの実装
https://medium.com/@mpalmerlee/building-an-llm-chat-interface-using-ollama-and-vue-5bf4e2fc65fd
なぜ自作するのか
Ollamaには公式のWeb UIもあり、すぐに会話を始められます。一方で、社内ツールに組み込む、モデル選択を画面に載せる、ダウンロード中の進捗を見せるといった要件は、既製UIだけでは満たしにくい場面があります。
Palmerlee氏の記事は、こうした「最低限のチャット体験を自分で持つ」ケースを想定しています。完成コードはGitHubのlliam-chatリポジトリにも公開されています。
前提条件
実装に必要な環境は次のとおりです。
- Ollamaがローカルで動作していること(デフォルトは
http://localhost:11434) - Node.jsとnpmが使えること
- Vue 3 + Viteの開発環境
OllamaのチャットAPI(/api/chat)は、公式ドキュメントのとおりストリーミングがデフォルトです。応答は改行区切りのJSON(NDJSON)で届き、トークン単位で画面に反映できます(Ollama API)。
ステップ1:Vueプロジェクトの作成
まずnpm create vue@latestでプロジェクトを作成します。Palmerlee氏の例ではTypeScriptとPiniaを有効にしています。Piniaは後述するモデル選択の状態管理に使います。
ボイラープレートを削り、ChatBox.vueコンポーネントを新規作成します。初期段階では入力欄と送信ボタンだけを置き、Enterキーまたはボタンで送信できる状態にします。
ステップ2:Ollamaと接続する
フロントエンドからOllamaを呼ぶには、公式のJavaScriptライブラリollamaを使います。
npm install ollama
ChatBox.vueでollama.chat()を呼び出し、まずは非ストリーミングで動作を確認します。モデル名にはllama2を指定する例が示されています。応答はコンソールに出るため、API接続の成否を切り分けやすいです。
ステップ3:ストリーミング表示を実装する
非ストリーミングでは応答完了まで画面が空のままになり、体感の待ち時間が長く感じられます。OllamaのチャットAPIはストリーミング前提の設計で、公式ドキュメントでも「体感レイテンシの低減」が利点として挙げられています(Streaming)。
ollama.chat()にstream: trueを渡し、for awaitでチャンクを順に受け取ります。VueのrefでcurrentOutputMessageContentを用意し、受信したたびに文字列を追記すれば、生成中のテキストがリアルタイムに描画されます。
会話履歴はmessages配列にユーザー発言とアシスタント応答を順に格納します。ストリーミング完了後にcurrentOutputMessageContentを履歴へ移し、一時表示用の変数を空に戻す流れが基本形です。
ステップ4:PrimeVueでUIを整える
機能が動いたら、PrimeVueで見た目と操作性を上げます。Palmerlee氏の実装では次のパッケージを導入しています。
npm install primevue primeicons primeflex
main.tsでPrimeVueを初期化し、Textarea・Button・Avatar・Dropdown・ProgressBar・Dialog・InputTextをグローバル登録します。入力欄は<Textarea>、送信は<Button>に置き換えるだけで、チャットらしい操作感に近づきます。
メッセージ表示ではAvatarでユーザーとアシスタントを区別し、PrimeFlexのユーティリティクラスで横並びレイアウトを組みます。画面全体を縦いっぱいに使うため、main.cssとHomeView.vueの高さ指定も調整します。
ステップ5:モデル選択を追加する
単一モデル固定では、用途に応じた切り替えができません。ModelSelector.vueを別コンポーネントとして切り出し、ollama.list()で取得したモデル一覧をDropdownに渡します。
選択中のモデル名はPiniaのストア(useModelStore)で保持し、ChatBox.vueのollama.chat()呼び出し時に参照します。初回マウント時はollama.list()の結果から最も新しいモデルを自動選択する処理も入れられます。
ステップ6:TanStack Queryでモデル一覧を管理する
モデルを新規ダウンロードしたあと、Dropdownが古い一覧のままになる問題が起きます。手動でページを再読み込みすれば解消しますが、運用では不便です。
ここで@tanstack/vue-queryを導入します。
npm install @tanstack/vue-query
main.tsでVueQueryPluginを登録し、useQueryでqueryKey: ['models']とqueryFn: getModelsを設定します。getModels内でollama.list()を呼び、あわせてPiniaへ最新モデル名を反映します。
モデル追加はuseMutationでollama.pull()を包み、成功時にqueryClient.invalidateQueries({ queryKey: ['models'] })を実行します。これでダウンロード完了後に一覧が自動再取得され、Dropdownへ新モデルが反映されます。
ステップ7:モデル追加と進捗表示
PullModelDialog.vueでモデル名を入力し、ollama.pull({ model, stream: true })を呼び出します。ストリーミングを有効にすると、ProgressResponse(status・completed・totalなど)が順次返り、進捗バーとステータス文字列で待ち時間を可視化できます。
大容量モデルは数分かかることもあるため、進捗表示はUX上の要点です。Palmerlee氏の実装では、読み込み中はモデル名・ステータス・数値(completed/total)・ProgressBarを並べて表示しています。
つまずきやすい点
CORSや接続エラー — ブラウザから直接localhost:11434へfetchする構成では、開発サーバーのオリジンとOllamaのオリジンが異なる場合に問題が出ることがあります。本番ではプロキシやバックエンド経由のAPIを挟む設計が安全です。
会話履歴の渡し方 — ストリーミング実装の途中では、直近のユーザー発言だけをmessagesに渡している例もあります。マルチターン会話を正しく続けるには、これまでのmessages配列全体をAPIに送る必要があります。
モデル名の指定 — ollama.list()が返す各モデルのnameフィールドと、DropdownのoptionValue設定を揃えないと、選択値が空になることがあります。APIレスポンスの構造に合わせてプロパティ名を確認してください。
発展させるなら
完成版のlliam-chatでは、記事本文に載っていない改善も入っています。markdown-itでアシスタント応答をMarkdown表示する対応や、直前の回答をもとに次の質問を自動生成するデモなどです。
ローカルLLMのチャットUIは、Ollamaの公式JSライブラリとVueのリアクティブ更新を組み合わせれば、小さなコンポーネント群で実用的な形まで持っていけます。ストリーミング表示・モデル切り替え・ダウンロード進捗の3点を押さえれば、社内検証用のプロトタイプにもそのまま転用できます。