相棒AI開発の進捗報告[02]

ブログ生成

この記事でできるようになること

  • Anaconda 仮想環境+Flask の最小起動
  • 2ペインUI(左ナビ/右ペイン動的読み込み)の骨格
  • キャラ設定のJSON化&切替・吹き出し
  • Ollama(gemma3)と雑談チャットをつなぐ
  • 右上にネオン時計を重ねてもUIを壊さないコツ(Shadow DOM)
  • 左ナビから .bat ランチャーを叩く配線

全体構成(完成イメージ)

ui_ai/
├─ app/
│  ├─ main.py                # Flask本体(ルーティング/機能ロード/ランチャAPI)
│  ├─ templates/
│  │   └─ index.html         # 左ナビ+右ペイン枠、ネオン時計挿入
│  ├─ static/
│  │   ├─ js/app.js          # 右ペインに機能UIを差し替える
│  │   └─ img/character_ui.png
│  ├─ core/
│  │   ├─ feature_base.py    # FeatureSpec(name/title/icon/blueprint/ui_template)
│  │   └─ character_store.py # JSONからキャラを読む
│  ├─ features/
│  │   └─ chat/
│  │       ├─ __init__.py
│  │       ├─ feature.py     # /api/chat/send, /ping, (Ollama接続)
│  │       └─ ui/chat_ui.html# 右ペインのチャットUI
│  └─ config/
│      ├─ characters/        # うい.json / ○○.json …(分割OK)
│      └─ launchers.json     # 左ナビから起動する .bat のマップ
└─ ★起動用/
   └─ start_ui.bat

1) 仮想環境&最小起動

インストール

conda create -p U:\ui_ai\env python=3.11 -y
call U:\anaconda\condabin\conda.bat activate U:\ui_ai\env
pip install flask flask-cors requests

※各コマンドのフォルダ名は作成したファイル名を使用すること。

起動バッチ ★起動用\start_ui.bat

@echo off
chcp 65001 >nul
call "U:\anaconda\condabin\conda.bat" activate U:\ui_ai\env
set HOST=127.0.0.1
set PORT=5000
start http://%HOST%:%PORT%/
python U:\ui_ai\app\main.py
pause

起動チェック

GET http://127.0.0.1:5000/healthz  → {"ok":true}

注意:Flaskの「Debugger is active! / PIN: …」は開発サーバの通常表示


2) 2ペインUIの骨格(動的読み込み)

templates/index.html の要点

  • 左サイド:キャラ選択、ナビボタン
  • 右ペイン:/ui/<feature> で押した機能の画面に応じてUI切り替え
<body class="h-screen bg-neutral-900 text-neutral-100">
  <div class="h-full grid grid-cols-[280px_1fr]">
    <aside class="border-r border-neutral-800 p-3 space-y-4 relative">
      <!-- キャラ選択+吹き出し -->
      ...
      <!-- 機能ナビ:サーバ側で features を渡してボタン生成 -->
      <nav id="feature-nav" class="space-y-1 pt-2">
        {% for f in features %}
          <button class="w-full text-left px-2 py-1 rounded hover:bg-neutral-800"
                  data-feature="{{ f.name }}">{{ f.title }}</button>
        {% endfor %}
      </nav>
    </aside>

    <main id="right-pane" class="p-3 overflow-hidden"></main>
  </div>

  <script>
    window.__INITIAL_FEATURE__ = "{{ initial_feature }}"; // "chat"
  </script>
  <script src="/static/js/app.js"></script>
</body>

static/js/app.js の要点

  • 初回 __INITIAL_FEATURE__ をロード
    →今回はchatになっているので、起動時にチャット画面を読み込む設定
  • ナビボタンの data-feature をクリックで /ui/<name> をフェッチ
    →ボタン一つで右側UI部分を切り替え、雑談とブログ生成機能を再起動なしで行き来可能
    (他機能追加後も同様の仕様とする)

3) キャラ設定をJSONに分離&切替

  • config/characters/うい.jsonのように保存。
  • 仮に別キャラを登録する場合は分割でjsonを作成し保存すればOKな仕様とする。
  • 例(最低限):
{
  "id": "うい",
  "label": "うい",
  "prompt": "あなたは気さくで前向きな相棒AIです。敬体で簡潔に答えます。"
}

主なAPI

  • GET /api/characters{"default":"うい","characters":[{"id":"うい","label":"うい"},...]}
  • キャラ選択は localStorage("character") に保存。
  • アイコンをクリックで /api/chat/send に「こんにちは!」→ 吹き出し表示。

キャラに関しては自分の好きな名前を付けましょう。
自分はオリキャラである『うい』のキャラをベースに作っていくのでこの名称です。


4) チャット機能 × Ollama 連携

サーバ:features/chat/feature.py(抜粋)

@bp.post("/send")
def send():
    data = request.get_json(silent=True) or {}
    text = (data.get("text") or "").strip()
    character = (data.get("character") or "").strip() or "うい"

    system = _get_char_prompt(store, character)
    messages = ([{"role":"system","content":system}] if system else []) + \
               [{"role":"user","content":text}]

    # 既定は gemma3:12b(環境変数 OLLAMA_MODEL でも変更可)
    model = (data.get("model") or os.environ.get("OLLAMA_MODEL","gemma3:12b")).strip()

    ok, reply = _ollama_chat(messages, model=model)
    return jsonify({"reply": reply}) if ok else (jsonify({"error": reply}), 502)

動作確認

  • モデル生存確認(Ollama)
    GET http://127.0.0.1:11434/api/version → version文字列
    GET http://127.0.0.1:11434/api/tags → pull(ダウンロード)済みモデル一覧
  • 直叩きテスト(モデル側の切り分け)
curl -H "Content-Type: application/json" ^
  -d "{\"model\":\"gemma3:12b\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}],\"stream\":false}" ^
  http://127.0.0.1:11434/api/chat
  • アプリ経由テスト
curl -H "Content-Type: application/json" ^
  -d "{\"text\":\"やっほー\",\"character\":\"うい\"}" ^
  http://127.0.0.1:5000/api/chat/send

返らない時は:①Flask死んでないか ②/healthz OKか ③Ollamaに直接cURL通るか の順で切り分け。


5) ネオン時計を“壊さず”右上に重ねる(Shadow DOM)

ポイント

  • 既存CSSと衝突しないよう、Shadow DOM 内でテンプレそのまま読み込む
  • 位置・サイズ・色は CSS変数--clock-*)で外部から調整可能
  • 右ペインの被りは padding-top をCSS変数から計算して回避(この配置が一番大変…)

index.html(末尾付近)

<!-- 時計コンテナ -->
<div id="obs-clock" class="fixed z-50 pointer-events-none"></div>

<style>
  :root{
    --clock-size: 120px;
    --clock-time-size: 30px;
    --clock-date-size: 15px;
    --clock-top: 40px;
    --clock-right: 60px;
    --clock-color: #ffff00;
    --clock-color-rgb: 255,255,0;
  }
  #right-pane{ padding-top: calc(var(--clock-size) * 0.70 + var(--clock-top)); }
</style>

<script>
(function(){
  const mount = document.getElementById('obs-clock');
  const cs = getComputedStyle(document.documentElement);
  mount.style.top   = cs.getPropertyValue('--clock-top')   || '12px';
  mount.style.right = cs.getPropertyValue('--clock-right') || '40px';

  const root = mount.attachShadow({ mode:'open' });
  root.innerHTML = `
    <!-- ここに元テンプレのSVG/CSS/JSをShadow DOM内へ展開 -->
  `;
})();
</script>

コツ:円のサイズ(--clock-size)と文字サイズ(--clock-time-size/--clock-date-size)を独立させると後で楽。


6) .bat ランチャーをUIから叩く

config/launchers.json(例)

{
  "blog_flow": "app\\features\\blog\\tasks\\run_blog_flow.bat",
  "notebook":  "tools\\start_notebook.bat"
}

サーバ:LaunchService(抜粋、Windows)

subprocess.Popen(["cmd", "/c", "start", "", path], shell=False)

API

  • GET /api/launchers … 一覧
  • POST /api/launch{"target":"blog_flow"} で起動

置き場所は任意でOK。相対パスは launchers.json の場所から解決。


7) よくあったつまずき & 直し方

  • /static/img/character_ui.png が 404
    → ファイル置き忘れ or パス違い。static/img/ に実体を置く。
  • /ui/chat が 404
    → 該当 feature の FeatureSpec.ui_template が指しているHTMLが存在しない or blueprint未登録。
  • FeatureSpec.__init__() missing 'icon'
    FeatureSpec(name, title, icon, blueprint, ui_template)icon忘れ
  • Ollama へ届かない
    → まず 11434 を cURL で直叩き → OKなら Flask 側のログを追う。
  • Modelfile の hf:// が Windows でコケる
    → そのままローカルパス扱いになるケースあり。まずは ollama pull モデル名 の素直ルート推奨

次の一歩(予告)

  • ブログUIを編集ファーストに:題材→タイトル→構成→本文→リライトを右ペインで回す
  • ステップごとに使うモデルを切替(例:題材だけ Llama3、本文は Gemma)
  • 使ったモデル名をUIに表示(運用ダッシュボード化)

付録:確認コマンドまとめ

:: アプリ生存
curl http://127.0.0.1:5000/healthz

:: 機能のUIロード
curl http://127.0.0.1:5000/ui/chat

:: キャラ一覧
curl http://127.0.0.1:5000/api/characters

:: チャット(アプリ経由)
curl -H "Content-Type: application/json" ^
  -d "{\"text\":\"テスト\",\"character\":\"うい\"}" ^
  http://127.0.0.1:5000/api/chat/send

:: Ollama(モデル直叩き)
curl http://127.0.0.1:11434/api/version
curl http://127.0.0.1:11434/api/tags

コメント

タイトルとURLをコピーしました