feat(posimai-dev): voice input, bigger Claude icon, aurora bg, session in settings

- Web Speech API mic button (ja-JP, pulses red while listening, hidden if unsupported)
- Claude bot icon 15px → 20px, button 32px → 36px
- Aurora gradient opacity 7% → 14% (visible through transparent xterm canvas)
- Session ID moved to settings panel (hidden in chat bar)
- Removed chat input placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-03-31 00:55:12 +09:00
parent 170dfaa7e0
commit d7f38faa59
1 changed files with 79 additions and 23 deletions

View File

@ -74,23 +74,38 @@
} }
.status-badge.disconnected { background: rgba(239,68,68,0.12); color: #F87171; } .status-badge.disconnected { background: rgba(239,68,68,0.12); color: #F87171; }
/* セッションバッジ(チャットバー内) */
.session-badge {
font-size: 10px; font-weight: 400; color: rgba(255,255,255,0.25);
font-family: monospace; display: none; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 180px;
}
.session-badge.visible { display: block; }
/* Claude開始ボタン — アイコンのみ */ /* Claude開始ボタン — アイコンのみ */
.claude-btn { .claude-btn {
width: 32px; height: 32px; border-radius: 8px; border: none; cursor: pointer; width: 36px; height: 36px; border-radius: 8px; border: none; cursor: pointer;
background: var(--accent-dim); color: var(--accent); background: var(--accent-dim); color: var(--accent);
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s; flex-shrink: 0; transition: background 0.15s;
} }
.claude-btn:hover { background: rgba(167,139,250,0.25); } .claude-btn:hover { background: rgba(167,139,250,0.25); }
/* マイクボタン */
.mic-btn {
width: 38px; height: 38px; border-radius: 10px; border: none;
background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.35); cursor: pointer;
display: none; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s, color 0.15s;
}
.mic-btn.available { display: flex; }
.mic-btn:hover { background: rgba(255,255,255,0.1); color: #F3F4F6; }
.mic-btn.listening {
background: rgba(239,68,68,0.15); color: #F87171;
animation: mic-pulse 1s ease-in-out infinite;
}
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
/* 設定パネル内セッション表示 */
.session-info-row {
display: flex; align-items: center; gap: 8px; margin-top: 8px;
font-size: 11px; color: rgba(255,255,255,0.35); font-family: monospace;
background: rgba(255,255,255,0.04); border-radius: 8px; padding: 8px 10px;
}
.session-info-row.hidden { display: none; }
/* ── Terminal ── */ /* ── Terminal ── */
#terminal-container { #terminal-container {
flex: 1; flex: 1;
@ -98,8 +113,8 @@
padding: 12px 12px 0; padding: 12px 12px 0;
min-height: 0; min-height: 0;
background: background:
radial-gradient(ellipse at 15% 60%, rgba(34,211,238,0.07) 0%, transparent 55%), radial-gradient(ellipse at 15% 60%, rgba(34,211,238,0.14) 0%, transparent 55%),
radial-gradient(ellipse at 85% 25%, rgba(167,139,250,0.07) 0%, transparent 55%), radial-gradient(ellipse at 85% 25%, rgba(167,139,250,0.14) 0%, transparent 55%),
var(--dev-bg); var(--dev-bg);
} }
#terminal-container .xterm { height: 100%; } #terminal-container .xterm { height: 100%; }
@ -173,7 +188,11 @@
</div> </div>
<div style="margin-top:20px"> <div style="margin-top:20px">
<div class="settings-group-label">セッション</div> <div class="settings-group-label">セッション</div>
<a href="/sessions.html" style="font-size:13px;color:var(--accent);text-decoration:none;display:flex;align-items:center;gap:6px;margin-top:8px"> <div class="session-info-row hidden" id="settingsSessionBadge">
<i data-lucide="terminal" style="width:12px;height:12px;stroke-width:1.75;color:var(--accent);flex-shrink:0"></i>
<span id="settingsSessionId"></span>
</div>
<a href="/sessions.html" style="font-size:13px;color:var(--accent);text-decoration:none;display:flex;align-items:center;gap:6px;margin-top:10px" rel="noopener">
<i data-lucide="history" style="width:14px;height:14px;stroke-width:1.75"></i> <i data-lucide="history" style="width:14px;height:14px;stroke-width:1.75"></i>
過去のセッションを見る 過去のセッションを見る
</a> </a>
@ -190,7 +209,7 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<button class="claude-btn" id="claudeBtn" aria-label="Claude 開始"> <button class="claude-btn" id="claudeBtn" aria-label="Claude 開始">
<i data-lucide="bot" style="width:15px;height:15px;stroke-width:1.75"></i> <i data-lucide="bot" style="width:20px;height:20px;stroke-width:1.75"></i>
</button> </button>
<button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false"> <button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i> <i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
@ -201,17 +220,18 @@
<div id="terminal-container"></div> <div id="terminal-container"></div>
<div class="chat-bar"> <div class="chat-bar">
<span class="session-badge" id="sessionBadge"></span>
<input <input
type="text" type="text"
class="chat-input" class="chat-input"
id="chatInput" id="chatInput"
placeholder="Claude に話しかける、またはコマンドを入力..."
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"
spellcheck="false" spellcheck="false"
> >
<button class="mic-btn" id="micBtn" aria-label="音声入力">
<i data-lucide="mic" style="width:16px;height:16px;stroke-width:1.75"></i>
</button>
<button class="send-btn" id="sendBtn" disabled aria-label="送信"> <button class="send-btn" id="sendBtn" disabled aria-label="送信">
<i data-lucide="arrow-up" style="width:16px;height:16px;stroke-width:2.5"></i> <i data-lucide="arrow-up" style="width:16px;height:16px;stroke-width:2.5"></i>
</button> </button>
@ -221,11 +241,13 @@
<script> <script>
(function () { (function () {
const statusBadge = document.getElementById('statusBadge'); const statusBadge = document.getElementById('statusBadge');
const sessionBadge = document.getElementById('sessionBadge'); const settingsSessionRow = document.getElementById('settingsSessionBadge');
const settingsSessionId = document.getElementById('settingsSessionId');
const container = document.getElementById('terminal-container'); const container = document.getElementById('terminal-container');
const chatInput = document.getElementById('chatInput'); const chatInput = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn'); const sendBtn = document.getElementById('sendBtn');
const claudeBtn = document.getElementById('claudeBtn'); const claudeBtn = document.getElementById('claudeBtn');
const micBtn = document.getElementById('micBtn');
// xterm.js // xterm.js
const term = new Terminal({ const term = new Terminal({
@ -277,8 +299,8 @@
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === 'output') term.write(msg.data); if (msg.type === 'output') term.write(msg.data);
if (msg.type === 'session') { if (msg.type === 'session') {
sessionBadge.textContent = msg.id; settingsSessionId.textContent = msg.id;
sessionBadge.classList.add('visible'); settingsSessionRow.classList.remove('hidden');
} }
} catch {} } catch {}
}; };
@ -319,6 +341,40 @@
term.focus(); term.focus();
}); });
// 音声入力
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
micBtn.classList.add('available');
const recognition = new SR();
recognition.lang = 'ja-JP';
recognition.continuous = false;
recognition.interimResults = false;
let listening = false;
micBtn.addEventListener('click', () => {
if (listening) { recognition.stop(); return; }
recognition.start();
});
recognition.onstart = () => {
listening = true;
micBtn.classList.add('listening');
};
recognition.onend = () => {
listening = false;
micBtn.classList.remove('listening');
};
recognition.onresult = (e) => {
const transcript = e.results[0][0].transcript;
chatInput.value = transcript;
chatInput.focus();
};
recognition.onerror = () => {
listening = false;
micBtn.classList.remove('listening');
};
}
// Resize // Resize
const ro = new ResizeObserver(() => { const ro = new ResizeObserver(() => {
fitAddon.fit(); fitAddon.fit();