posimai-root/posimai-dev/index.html

403 lines
16 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-dev">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-dev-theme') || 'dark';
document.documentElement.setAttribute('data-theme', t === 'light' ? 'light' : 'dark');
document.documentElement.setAttribute('data-theme-pref', t);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="posimai development portal">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0C1221">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="posimai-dev">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>posimai-dev</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<link rel="stylesheet" href="https://unpkg.com/@xterm/xterm@6.0.0/css/xterm.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<script src="https://unpkg.com/@xterm/xterm@6.0.0/lib/xterm.js"></script>
<script src="https://unpkg.com/@xterm/addon-fit@0.11.0/lib/addon-fit.js"></script>
<style>
:root {
--accent: #A78BFA;
--accent-dim: rgba(167, 139, 250, 0.15);
--dev-bg: #0C1221;
}
[data-theme="light"] { --accent: #7C3AED; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
display: flex;
flex-direction: column;
height: 100dvh;
overflow: hidden;
background: var(--dev-bg);
font-family: Inter, sans-serif;
}
/* ── Header ── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 48px;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
background: rgba(12, 18, 33, 0.8);
backdrop-filter: blur(12px);
}
.header-left { display: flex; align-items: center; gap: 10px; }
.header-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.header-title { font-size: 14px; font-weight: 600; color: #F3F4F6; letter-spacing: -0.01em; }
.header-right { display: flex; align-items: center; gap: 4px; }
.status-badge {
font-size: 11px; font-weight: 500; padding: 2px 8px;
border-radius: 20px; background: var(--accent-dim); color: var(--accent);
transition: background 0.2s, color 0.2s;
}
.status-badge.disconnected { background: rgba(239,68,68,0.12); color: #F87171; }
/* Claude開始ボタン — アイコンのみ */
.claude-btn {
width: 36px; height: 36px; border-radius: 8px; border: none; cursor: pointer;
background: var(--accent-dim); color: var(--accent);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s;
}
.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-container {
flex: 1;
overflow: hidden;
padding: 12px 12px 0;
min-height: 0;
background:
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.14) 0%, transparent 55%),
var(--dev-bg);
}
#terminal-container .xterm { height: 100%; }
#terminal-container .xterm-viewport { border-radius: 8px 8px 0 0; }
/* スクロールバー — 細く・半透明 */
.xterm-viewport::-webkit-scrollbar { width: 4px; }
.xterm-viewport::-webkit-scrollbar-track { background: transparent; }
.xterm-viewport::-webkit-scrollbar-thumb { background: rgba(167,139,250,0.25); border-radius: 2px; }
.xterm-viewport::-webkit-scrollbar-thumb:hover { background: rgba(167,139,250,0.5); }
/* ── Chat input bar ── */
.chat-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
background: rgba(12, 18, 33, 0.9);
border-top: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
}
.chat-input {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 9px 14px;
color: #F3F4F6;
font-size: 14px;
font-family: Inter, sans-serif;
outline: none;
transition: border-color 0.15s;
}
.chat-input::placeholder { color: rgba(255,255,255,0.25); }
.chat-input:focus { border-color: rgba(167,139,250,0.4); }
.send-btn {
width: 38px; height: 38px; border-radius: 10px; border: none;
background: var(--accent); color: #0C1221; cursor: pointer;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: opacity 0.15s;
}
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.3; cursor: default; }
.overlay { display: none; }
.settings-panel { z-index: 1000; }
/* icon-btn override for dark bg */
.icon-btn { color: rgba(255,255,255,0.5); }
.icon-btn:hover { color: #F3F4F6; background: rgba(255,255,255,0.06); }
</style>
</head>
<body>
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="settings-panel-body">
<div>
<div class="settings-group-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark"><i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>ダーク</button>
<button class="theme-btn" data-theme-val="light"><i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>ライト</button>
<button class="theme-btn" data-theme-val="system"><i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>自動</button>
</div>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">セッション</div>
<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>
過去のセッションを見る
</a>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<header class="header">
<div class="header-left">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">posimai-dev</span>
<span class="status-badge disconnected" id="statusBadge">接続中...</span>
</div>
<div class="header-right">
<button class="claude-btn" id="claudeBtn" aria-label="Claude 開始">
<i data-lucide="bot" style="width:20px;height:20px;stroke-width:1.75"></i>
</button>
<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>
</button>
</div>
</header>
<div id="terminal-container"></div>
<div class="chat-bar">
<input
type="text"
class="chat-input"
id="chatInput"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
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="送信">
<i data-lucide="arrow-up" style="width:16px;height:16px;stroke-width:2.5"></i>
</button>
</div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script>
(function () {
const statusBadge = document.getElementById('statusBadge');
const settingsSessionRow = document.getElementById('settingsSessionBadge');
const settingsSessionId = document.getElementById('settingsSessionId');
const container = document.getElementById('terminal-container');
const chatInput = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn');
const claudeBtn = document.getElementById('claudeBtn');
const micBtn = document.getElementById('micBtn');
// xterm.js
const term = new Terminal({
fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
fontSize: 14,
lineHeight: 1.4,
cursorBlink: true,
allowTransparency: true,
theme: {
background: 'rgba(12, 18, 33, 0.0)',
foreground: '#F3F4F6',
cursor: '#A78BFA',
selectionBackground: 'rgba(167,139,250,0.3)',
black: '#1A1A1A', brightBlack: '#4B5563',
red: '#F87171', brightRed: '#FCA5A5',
green: '#6EE7B7', brightGreen: '#A7F3D0',
yellow: '#FCD34D',brightYellow: '#FDE68A',
blue: '#60A5FA', brightBlue: '#93C5FD',
magenta: '#A78BFA', brightMagenta: '#C4B5FD',
cyan: '#22D3EE', brightCyan: '#67E8F9',
white: '#F3F4F6', brightWhite: '#FFFFFF'
}
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(container);
fitAddon.fit();
// WebSocket
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
let ws;
function sendInput(text) {
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'input', data: text }));
}
function connect() {
ws = new WebSocket(`${proto}//${location.host}/terminal`);
ws.onopen = () => {
statusBadge.textContent = '接続済み';
statusBadge.classList.remove('disconnected');
sendBtn.disabled = false;
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
};
ws.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'output') term.write(msg.data);
if (msg.type === 'session') {
settingsSessionId.textContent = msg.id;
settingsSessionRow.classList.remove('hidden');
}
} catch {}
};
ws.onclose = () => {
statusBadge.textContent = '切断';
statusBadge.classList.add('disconnected');
sendBtn.disabled = true;
term.write('\r\n\x1b[31m[切断されました。再接続中...]\x1b[0m\r\n');
setTimeout(connect, 3000);
};
ws.onerror = () => ws.close();
}
connect();
// ターミナル直接入力
term.onData((data) => sendInput(data));
// チャットバー送信
function submitChat() {
const text = chatInput.value.trim();
if (!text) return;
sendInput(text + '\n');
chatInput.value = '';
term.focus();
}
sendBtn.addEventListener('click', submitChat);
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitChat(); }
});
// Claude 開始ボタン
claudeBtn.addEventListener('click', () => {
sendInput('claude\n');
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
const ro = new ResizeObserver(() => {
fitAddon.fit();
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
});
ro.observe(container);
window.addEventListener('resize', () => fitAddon.fit());
// Settings panel
const settingsBtn = document.getElementById('settingsBtn');
const settingsPanel = document.getElementById('settingsPanel');
settingsBtn.addEventListener('click', () => {
settingsPanel.classList.toggle('open');
settingsBtn.setAttribute('aria-expanded', settingsPanel.classList.contains('open'));
});
document.getElementById('settingsCloseBtn').addEventListener('click', () => {
settingsPanel.classList.remove('open');
settingsBtn.setAttribute('aria-expanded', 'false');
});
lucide.createIcons();
})();
</script>
</body>
</html>