TL;DR
僕の作ってる AI キャラチャット (Kotonia) は「voice-first」を謳ってる。けどスマホで開いてみたら、底のド真ん中に直径 96px の巨大マイクボタンと「タップして会話開始」のコピーが鎮座していた。
これはバグじゃなくて 構造的な自己矛盾 だった。voice-first を名乗る製品が「voice モードに入るボタン」を画面中央に置いてる時点で、UI が暗黙的に「voice はオプションです」と宣言してる。
ユーザーからの「モバイル画面が壊れてる」というバグ報告から始まった調査が、結果的に「製品コンセプトと UI の整合性チェック」になった話を書きます。
始まりはただのモバイル CSS バグ
最初の報告はこれだけ:
キャラチャット画面のアバターありのときのモバイル画面が壊れてる。調査して。
調べると .voice-grid がモバイル時に単一カラムで、子 (アバター枠とチャット枠) が両方とも height: 100% を持っていた。CSS Grid が default の align-content: stretch で 50/50 に配分するため:
- アバター枠 → 全身が縦に半分しか見えない、顔は中央に寄って小さく
- チャット枠 → transcript が flex:1 でも mic ボタン (96px) + composer + 状態テキスト + hint で底が 250px 専有、可読領域が ~90px に潰れる
両方読めない。両方使えない。
まずは構造的 fix: B レイアウト (Reels 文法)
雑にやるなら grid-template-rows: minmax(40vh, 45vh) 1fr でアバター上 / チャット下に整理 (A 案)。でも提示したもう一つの案 (B 案):
アバターを全画面背景にして、transcript / control を半透明オーバーレイで重ねる。Reels / TikTok の縦長動画文法。
ユーザーは B を選んだ。実装してスクショ送ったら返事はこう:
画面の 8 割が会話とオーバーレイで埋まってしまうとせっかく可愛いアバターが必死で話しかけてくれても集中して楽しめない。
つまり B にしても 音声 + アバターが主役なのに、画面の主役が文字情報 という再矛盾が残ってた。ここから話が物理 UI から「製品哲学」に切り替わる。
マイクボタン疑惑
ユーザーの次のひと言が決定打だった (原文ほぼそのまま):
下のボタンがデカいのはもともと会話開始がわかりやすくなることが目的だとすると、はじめから臨戦状態でスタートさせることでこのボタンやタップして会話なんかの無駄スペースも省略できるかも
これを翻訳するとこうなる: voice-first を謳う製品が「voice モードに入るボタン」を中央に置いてる時点で、自己矛盾している。
なぜなら:
- voice-first なら voice はオプションじゃなくデフォ
- デフォなら「voice モードに入る」アクションは存在しない
- 中央の巨大ボタンは「voice はそこのボタンを押したときだけ動く副機能」と宣言してる
- それは voice-first じゃなくて voice-as-feature
Pi.ai や Sesame の immersive 体験が成功してる理由は「初手から voice on、UI は minimal」だから。Kotonia がそれを真似したいなら、まず大ボタンを殺す必要がある。
3 つの再設計判断
ユーザーと擦り合わせて以下の方針で実装:
1. 巨大マイクボタンを完全廃止
底の MicButton、状態テキスト (<p>tapToStart</p>)、hint (<p>VAD オートで聞いてます</p>) を全部削除。底には composer (テキスト入力欄) と送信ボタンだけ残す。
2. 状態 + 開始/停止を右上に統合 (statusPill)
<button onClick={handleSessionToggle} className="voice-statusPill">
<span className="voice-statusPill-dot" />
<span>{voiceState === "stopped" ? "START" : voiceState.toUpperCase()}</span>
</button>
機能集約:
- 状態表示 (LISTENING / THINKING / SPEAKING の脈動ドット)
- タップで開始 / 停止
- 既存の「右上の状態 badge」と冗長だったのでそれごと殺して合体
3. cinema / chat の 2 モード切替
localStorage で永続化、右上に 🎬 / 💬 のトグルボタン:
- cinema (default): transcript 非表示。アバターが画面の 9 割。底に composer + 必要ならチップだけ
- chat: transcript を max 48vh + backdrop-blur で底に重ねる。読み返したいとき用
「会話の中身を読みたい人」と「アバターに集中したい人」を一つの UI で両立させる手段としては 2 モードトグルが一番素直だった (透明グラデで中央に向かって透明にする案もあったけど、可読性が二値になるので却下)。
4. 起動トリガーは「ユーザーが意思を示した瞬間」
大ボタンを殺したのに、じゃあ session はいつ起動するのか? 答え:
- サジェストチップタップ → 自動
startSession() - composer 送信 → 自動
startSession() - 右上 statusPill タップ → 直接 voice 開始 (チップなし派向け)
意思表明と起動を 1 アクションに圧縮する。「voice モードに入るための前段ボタン」が消えた。
完成版

右上に START pill (停止時はオレンジドット、起動中は青/紫/オレンジが状態に応じて脈動) と 🎬 ⇄ 💬 のモード切替。底はチップ + composer の細い帯。中央の 9 割はアバターが占有して、何も邪魔しない。
「voice-first 製品」の意味が、ようやく UI と一致した。
学び
"voice-first" は技術選定じゃなくて UI 配分の話
Whisper を使ってるとか、TTS にこだわってるとか、LLM のレイテンシをチューニングしてるとか、それらは voice-first を 支える 技術ではあるけど、voice-first そのもの じゃない。
UI 上で voice が画面の主役を張ってるかどうか、テキスト入力が「補助手段」として小さく配置されてるかどうか、その判断こそが voice-first を名乗る資格。
「voice モードに入る」ボタンがあるなら、その主張は嘘
これは voice 以外にも応用が効くテスト方法だと思う:
- AI チャット製品で「AI に質問する」ボタンがあったら、それは AI-first じゃなくて AI-as-feature
- リアルタイム製品で「リアルタイム開始」ボタンがあったら、それは realtime-first じゃない
- collaboration 製品で「コラボモード切替」ボタンがあったら、それは collab-first じゃない
「製品の核」を名乗るものは、起動する必要すらない。最初からそこにある べき。
バグ報告は製品哲学の試金石になる
今回の話、最初は単なる CSS バグ報告だった。けど「モバイルで CSS が崩れてる」→「B レイアウト」→「8 割埋まってしまう」→「巨大ボタン疑惑」と段階的に抽象度が上がっていって、最終的には「voice-first って何だっけ」という製品定義の話に到達した。
一人で開発してると、UI の細部に対する「これでいいのか?」を自分で問い続ける機会が薄い。ユーザーからのバグ報告には、UI バグそのもの以外に「UI が製品の意図を裏切ってる兆候」が混ざってることがある。そういう兆候を吸い上げると、3 段階くらい上の抽象度で判断が変わることがある。
製品コンセプトと UI が一致してるか、定期的に巨大ボタンの 1 個でも疑ってみると、自分の voice-first 主張が建前じゃなく本気で言えるようになる、かもしれない。
Kotonia は kotonia.ai で触れます。スマホで /chat/voice 開いたら、巨大ボタンが消えてアバターが画面 9 割を占めてるはず。
