この記事でできるようになること
- 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
コメント