ローカル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.vueollama.chat()を呼び出し、まずは非ストリーミングで動作を確認します。モデル名にはllama2を指定する例が示されています。応答はコンソールに出るため、API接続の成否を切り分けやすいです。

ステップ3:ストリーミング表示を実装する

非ストリーミングでは応答完了まで画面が空のままになり、体感の待ち時間が長く感じられます。OllamaのチャットAPIはストリーミング前提の設計で、公式ドキュメントでも「体感レイテンシの低減」が利点として挙げられています(Streaming)。

ollama.chat()stream: trueを渡し、for awaitでチャンクを順に受け取ります。VueのrefcurrentOutputMessageContentを用意し、受信したたびに文字列を追記すれば、生成中のテキストがリアルタイムに描画されます。

会話履歴は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.cssHomeView.vueの高さ指定も調整します。

ステップ5:モデル選択を追加する

単一モデル固定では、用途に応じた切り替えができません。ModelSelector.vueを別コンポーネントとして切り出し、ollama.list()で取得したモデル一覧をDropdownに渡します。

選択中のモデル名はPiniaのストア(useModelStore)で保持し、ChatBox.vueollama.chat()呼び出し時に参照します。初回マウント時はollama.list()の結果から最も新しいモデルを自動選択する処理も入れられます。

ステップ6:TanStack Queryでモデル一覧を管理する

モデルを新規ダウンロードしたあと、Dropdownが古い一覧のままになる問題が起きます。手動でページを再読み込みすれば解消しますが、運用では不便です。

ここで@tanstack/vue-queryを導入します。

npm install @tanstack/vue-query

main.tsVueQueryPluginを登録し、useQueryqueryKey: ['models']queryFn: getModelsを設定します。getModels内でollama.list()を呼び、あわせてPiniaへ最新モデル名を反映します。

モデル追加はuseMutationollama.pull()を包み、成功時にqueryClient.invalidateQueries({ queryKey: ['models'] })を実行します。これでダウンロード完了後に一覧が自動再取得され、Dropdownへ新モデルが反映されます。

ステップ7:モデル追加と進捗表示

PullModelDialog.vueでモデル名を入力し、ollama.pull({ model, stream: true })を呼び出します。ストリーミングを有効にすると、ProgressResponsestatuscompletedtotalなど)が順次返り、進捗バーとステータス文字列で待ち時間を可視化できます。

大容量モデルは数分かかることもあるため、進捗表示は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点を押さえれば、社内検証用のプロトタイプにもそのまま転用できます。