Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
442a3eef4This file is pinned to the selected release. Download the original to open it in your design tool.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="../assets/stackchan-favicon-32.png" type="image/png" />
<link rel="apple-touch-icon" href="../assets/stackchan-apple-touch-icon.png" />
<link rel="stylesheet" href="../global.css" />
<link rel="stylesheet" href="preference.css" />
<title>設定 | スタックチャン</title>
<script
src="https://unpkg.com/lucide@1.24.0/dist/umd/lucide.min.js"
integrity="sha384-mooE85Luwgx+AyykX7e90VcN8/QCFTSIwPuHLmvcsLVoA0en7lKYb9XlOzn5G2co"
crossorigin="anonymous"
defer
></script>
<script type="module">
import SimpleBLEClient from './ble-client.mjs'
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('form-preference')
const connectButton = document.getElementById('ble-connect-button')
const disconnectButton = document.getElementById('ble-disconnect-button')
const submitButton = document.getElementById('preference-submit-button')
const connectionStatus = document.getElementById('connection-status')
const saveStatus = document.getElementById('save-status')
const controls = [...form.querySelectorAll('input, select, textarea, button')]
let submitting = false
const currentValues = new Map()
const setStatus = (element, text, state = '') => {
element.textContent = text
element.dataset.state = state
}
const updateView = () => {
const connected = client.isConnected()
connectButton.hidden = connected
disconnectButton.hidden = !connected
controls.forEach((control) => {
control.disabled = !connected
})
setStatus(connectionStatus, connected ? '接続済み' : '未接続', connected ? 'success' : '')
}
connectButton.addEventListener('click', async () => {
connectButton.disabled = true
setStatus(connectionStatus, '接続中…')
try {
await client.connect()
} catch (error) {
console.error(error)
setStatus(connectionStatus, `接続できませんでした: ${error.message ?? error}`, 'error')
} finally {
connectButton.disabled = false
if (client.isConnected()) updateView()
}
})
disconnectButton.addEventListener('click', async () => {
disconnectButton.disabled = true
try {
await client.disconnect()
} catch (error) {
console.error(error)
setStatus(connectionStatus, `切断できませんでした: ${error.message ?? error}`, 'error')
} finally {
disconnectButton.disabled = false
if (!client.isConnected()) updateView()
}
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (!client.isConnected() || submitting) return
const props = [
'wifi.ssid',
'wifi.password',
'driver.type',
'driver.offsetPan',
'driver.offsetTilt',
'ui.type',
'tts.type',
'tts.host',
'tts.port',
'tts.token',
'tts.voice',
'tts.volume',
'ai.token',
'ai.context',
]
const payload = {}
for (const prop of props) {
const value = document.getElementById(prop)?.value
if (value != null && value.trimEnd().length > 0 && currentValues.get(prop) !== value) {
payload[prop] = value
}
}
if (Object.keys(payload).length === 0) {
setStatus(saveStatus, '変更する項目がありません。')
return
}
submitButton.disabled = true
setStatus(saveStatus, '保存中…')
submitting = true
try {
await client.send({ _batch: payload })
for (const [prop, value] of Object.entries(payload)) currentValues.set(prop, value)
setStatus(saveStatus, '設定を送信しました。', 'success')
} catch (error) {
console.error(error)
setStatus(saveStatus, `保存できませんでした: ${error.message ?? error}`, 'error')
} finally {
submitting = false
submitButton.disabled = !client.isConnected()
}
})
const client = new SimpleBLEClient({
deviceName: 'STK',
onCharacteristicValueChanged: ({ prop, value }) => {
currentValues.set(prop, String(value))
const element = document.querySelector(`[name="${prop}"]`)
if (element != null) element.value = value
},
})
client.onDisconnected = () => {
submitting = false
currentValues.clear()
setStatus(saveStatus, '接続が切れました。保存されていない項目を確認してください。', 'warning')
updateView()
}
updateView()
globalThis.lucide?.createIcons()
})
</script>
</head>
<body>
<main class="app-shell">
<header class="topbar">
<a class="brand-link" href="../"
><img src="../assets/stackchan-symbol.png" alt="" /><span
>スタックチャン<span class="surface-name"> 設定</span></span
></a
>
</header>
<div class="preference-layout">
<aside class="connection-pane">
<div class="page-heading">
<h1>本体設定</h1>
<p>BLEでスタックチャンに接続します。</p>
</div>
<button id="ble-connect-button" class="primary-button" type="button">
<i data-lucide="bluetooth"></i><span>BLEで接続</span>
</button>
<button id="ble-disconnect-button" class="danger-button" type="button" hidden>
<i data-lucide="unplug"></i><span>切断</span>
</button>
<p id="connection-status" class="status-line" role="status">未接続</p>
</aside>
<section id="settings-form" class="settings-pane" aria-label="設定項目">
<form id="form-preference">
<section class="section-block">
<h2>Wi-Fi</h2>
<div class="form-grid">
<label class="form-field"
><span>SSID</span><input type="text" name="wifi.ssid" id="wifi.ssid" autocomplete="off" /></label
><label class="form-field"
><span>パスワード</span
><input type="password" name="wifi.password" id="wifi.password" autocomplete="off"
/></label>
</div>
</section>
<section class="section-block">
<h2>外観</h2>
<div class="form-grid">
<label class="form-field"
><span>顔の種類</span
><select name="ui.type" id="ui.type">
<option value="simple">シンプル</option>
<option value="dog">いぬ</option>
</select></label
>
</div>
</section>
<section class="section-block">
<h2>サーボ</h2>
<div class="form-grid">
<label class="form-field form-field-wide"
><span>ドライバー</span
><select name="driver.type" id="driver.type">
<option value="scservo">SCServo</option>
<option value="dynamixel">Dynamixel(Protocol 2)</option>
<option value="rs30x">RS30X</option>
<option value="pwm">PWM(SG-90)</option>
<option value="none">なし</option>
</select></label
><label class="form-field"
><span>パン オフセット</span
><input type="number" name="driver.offsetPan" id="driver.offsetPan" value="0" /></label
><label class="form-field"
><span>チルト オフセット</span
><input type="number" name="driver.offsetTilt" id="driver.offsetTilt" value="0"
/></label>
</div>
</section>
<section class="section-block">
<h2>音声合成</h2>
<div class="form-grid">
<label class="form-field"
><span>サービス</span
><select name="tts.type" id="tts.type">
<option value="voicevox">VOICEVOX</option>
<option value="elevenlabs">ElevenLabs</option>
<option value="google-tts">Google TTS</option>
<option value="openai">OpenAI</option>
<option value="local">ローカル</option>
</select></label
><label class="form-field"
><span>ホスト</span
><input type="text" name="tts.host" id="tts.host" placeholder="my-tts-host.local" /></label
><label class="form-field"
><span>ポート</span><input type="number" name="tts.port" id="tts.port" placeholder="50021" /></label
><label class="form-field"
><span>音声</span><input type="text" name="tts.voice" id="tts.voice" placeholder="ally" /></label
><label class="form-field"
><span>トークン</span><input type="password" name="tts.token" id="tts.token" /></label
><label class="form-field"
><span>音量(0–1)</span
><input type="number" name="tts.volume" id="tts.volume" step="0.1" min="0" max="1"
/></label>
</div>
</section>
<section class="section-block">
<h2>AI</h2>
<div class="form-grid">
<label class="form-field"
><span>トークン</span><input type="password" name="ai.token" id="ai.token" /></label
><label class="form-field form-field-full"
><span>システムロール</span
><textarea
name="ai.context"
id="ai.context"
rows="5"
placeholder="You are Stack-chan(スタックチャン), the palm sized super kawaii companion robot."
></textarea>
</label>
</div>
</section>
<footer class="form-actions">
<p id="save-status" class="status-line" role="status"></p>
<button id="preference-submit-button" class="primary-button" type="submit">
<i data-lucide="save"></i><span>設定を保存</span>
</button>
</footer>
</form>
</section>
</div>
</main>
<script type="module" src="../tool-navigation.mjs"></script>
</body>
</html>