baudy/src/pages/phone.tsx
Corey Johnson 7cce1f6bbd Merge probablycorey/refactor-server-split
Resolve conflicts: accept server split, apply hint reorder
to terminal.ts, add JSX pragma to phone.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:34:28 -07:00

402 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @jsxImportSource hono/jsx */
import { define, stylesToCSS } from '@because/forge'
// ---------------------------------------------------------------------------
// Forge components
// ---------------------------------------------------------------------------
const Page = define('Page', {
fontFamily: '-apple-system, system-ui, sans-serif',
background: '#111',
color: '#fff',
height: '100dvh',
width: '100vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
userSelect: 'none',
WebkitUserSelect: 'none',
position: 'relative',
touchAction: 'manipulation',
})
const Screen = define('Screen', {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
width: '100%',
padding: 20,
})
const Title = define('Title', {
fontSize: 18,
color: '#888',
textAlign: 'center',
lineHeight: 1.4,
})
const GuessDisplay = define('GuessDisplay', {
fontSize: 96,
fontWeight: 'bold',
fontFamily: 'monospace',
lineHeight: 1,
margin: '24px 0',
})
const Controls = define('Controls', {
display: 'flex',
gap: 12,
marginBottom: 24,
})
const StepBtn = define('StepBtn', {
base: 'button',
fontSize: 24,
fontWeight: 'bold',
width: 64,
height: 64,
border: 'none',
borderRadius: 12,
background: '#333',
color: '#fff',
cursor: 'pointer',
WebkitTapHighlightColor: 'transparent',
touchAction: 'manipulation',
states: { ':active': { background: '#555' } },
})
const GuessBtn = define('GuessBtn', {
base: 'button',
fontSize: 24,
fontWeight: 'bold',
padding: '16px 48px',
border: 'none',
borderRadius: 16,
background: '#22c55e',
color: '#fff',
cursor: 'pointer',
WebkitTapHighlightColor: 'transparent',
touchAction: 'manipulation',
states: { ':active': { background: '#16a34a' } },
})
const HintText = define('HintText', {
fontSize: 36,
fontWeight: 'bold',
marginTop: 32,
minHeight: 50,
display: 'flex',
alignItems: 'center',
transition: 'opacity 0.15s ease',
})
const VictoryEmoji = define('VictoryEmoji', {
fontSize: 64,
marginBottom: 16,
})
const VictoryText = define('VictoryText', {
fontSize: 22,
lineHeight: 1.5,
textAlign: 'center',
padding: '0 20px',
maxWidth: 380,
})
const StatusBar = define('StatusBar', {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
padding: 8,
fontSize: 12,
color: '#555',
textAlign: 'center',
})
const VolumeWarning = define('VolumeWarning', {
fontSize: 14,
color: '#f59e0b',
textAlign: 'center',
marginTop: 16,
padding: '8px 16px',
background: 'rgba(245, 158, 11, 0.1)',
borderRadius: 8,
border: '1px solid rgba(245, 158, 11, 0.3)',
})
const ConnectBtn = define('ConnectBtn', {
base: 'button',
fontSize: 28,
fontWeight: 'bold',
padding: '20px 48px',
border: 'none',
borderRadius: 16,
background: '#2563eb',
color: '#fff',
cursor: 'pointer',
WebkitTapHighlightColor: 'transparent',
touchAction: 'manipulation',
states: { ':active': { background: '#1d4ed8' } },
})
const ConnectStatus = define('ConnectStatus', {
fontSize: 18,
color: '#888',
marginTop: 24,
textAlign: 'center',
})
// ---------------------------------------------------------------------------
// Client-side code — real functions, serialized into <script>
// ---------------------------------------------------------------------------
/* eslint-disable */
// @ts-nocheck — this function is serialized and runs in the browser, not on the server
function clientMain() {
var currentGuess = 50
var playing = false
var busy = false
var gg, ggInstance, audioContext
var pendingResolve
var connectScreen = document.getElementById('connect-screen')
var connectStatus = document.getElementById('connect-status')
var guessDisplay = document.getElementById('guess-display')
var hintArea = document.getElementById('hint-area')
var statusEl = document.getElementById('status')
var guessBtn = document.getElementById('guess-btn')
var gameScreen = document.getElementById('game-screen')
var victoryScreen = document.getElementById('victory-screen')
var victoryText = document.getElementById('victory-text')
function bypassSilentMode() {
if ('audioSession' in navigator) navigator.audioSession.type = 'play-and-record'
}
function sleep(ms) {
return new Promise(function (resolve) { setTimeout(resolve, ms) })
}
function playChirp(text) {
var rawBytes = gg.encode(
ggInstance, text,
gg.ProtocolId.GGWAVE_PROTOCOL_AUDIBLE_FAST, 50
)
var bytesCopy = new Uint8Array(rawBytes.length)
bytesCopy.set(new Uint8Array(rawBytes.buffer, rawBytes.byteOffset, rawBytes.length))
var floats = new Float32Array(bytesCopy.buffer)
var buf = audioContext.createBuffer(1, floats.length, audioContext.sampleRate)
buf.getChannelData(0).set(floats)
var source = audioContext.createBufferSource()
source.buffer = buf
source.connect(audioContext.destination)
return new Promise(function (resolve) { source.onended = resolve; source.start() })
}
async function sendAndWait(text, timeoutMs) {
playing = true
if (audioContext.state === 'suspended') await audioContext.resume()
await playChirp(text)
await sleep(500)
playing = false
return new Promise(function (resolve) {
var timer
if (timeoutMs) {
timer = setTimeout(function () {
pendingResolve = undefined
resolve(undefined)
}, timeoutMs)
}
pendingResolve = function (msg) {
if (timer) clearTimeout(timer)
pendingResolve = undefined
resolve(msg)
}
})
}
function startListening(stream) {
var source = audioContext.createMediaStreamSource(stream)
var processor = audioContext.createScriptProcessor(4096, 1, 1)
var samplesPerFrame = gg.getDefaultParameters().samplesPerFrame
var buffer = new Float32Array(0)
processor.onaudioprocess = function (e) {
if (playing) return
var input = e.inputBuffer.getChannelData(0)
var newBuf = new Float32Array(buffer.length + input.length)
newBuf.set(buffer)
newBuf.set(input, buffer.length)
buffer = newBuf
while (buffer.length >= samplesPerFrame) {
var frame = buffer.slice(0, samplesPerFrame)
buffer = buffer.slice(samplesPerFrame)
var bytes = new Uint8Array(frame.buffer, frame.byteOffset, frame.byteLength)
var result = gg.decode(ggInstance, bytes)
if (result && result.length > 0) {
var text = Array.from(result)
.map(function (b) { return String.fromCharCode(b & 0xff) })
.join('')
if (pendingResolve) pendingResolve(text)
}
}
}
source.connect(processor)
processor.connect(audioContext.destination)
}
async function connect() {
try {
// AudioContext MUST be created synchronously in gesture handler (iOS)
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 })
var src = audioContext.createBufferSource()
src.buffer = audioContext.createBuffer(1, 1, 48000)
src.connect(audioContext.destination)
src.start()
bypassSilentMode()
connectStatus.textContent = 'Initializing audio (' + audioContext.sampleRate + ' Hz)...'
gg = await ggwave_factory()
gg.disableLog()
var ggParams = gg.getDefaultParameters()
ggParams.sampleRateInp = audioContext.sampleRate
ggParams.sampleRateOut = audioContext.sampleRate
ggParams.sampleRate = audioContext.sampleRate
ggInstance = gg.init(ggParams)
connectStatus.textContent = 'Requesting microphone...'
var stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false },
})
startListening(stream)
connectStatus.textContent = 'Connecting via audio...'
var response = await sendAndWait('HELLO', 8000)
if (response === 'HEY BUDDY') {
connectScreen.style.display = 'none'
gameScreen.style.display = 'flex'
statusEl.textContent = 'Ready'
} else {
connectStatus.textContent = 'No response heard. Make sure the server is running and volume is up!'
connectStatus.style.color = '#ef4444'
}
} catch (err) {
connectStatus.textContent = err.message || 'Failed to connect'
connectStatus.style.color = '#ef4444'
}
}
async function sendGuess() {
if (busy || !gg) return
busy = true
guessBtn.textContent = 'Sending...'
guessBtn.style.background = '#666'
hintArea.textContent = ''
hintArea.style.opacity = '0'
statusEl.textContent = 'Listening for response...'
guessBtn.textContent = 'Listening...'
var response = await sendAndWait(String(currentGuess), 8000)
if (response === 'Higher!' || response === 'Lower!') {
hintArea.style.opacity = '0'
hintArea.textContent = response
hintArea.style.color = response === 'Higher!' ? '#f59e0b' : '#3b82f6'
void hintArea.offsetWidth
hintArea.style.opacity = '1'
} else if (response === 'Correct!') {
gameScreen.style.display = 'none'
victoryScreen.style.display = 'flex'
victoryText.textContent = 'You found ' + currentGuess + '!'
statusEl.textContent = ''
} else {
hintArea.textContent = 'No response heard. Volume up on both devices!'
hintArea.style.color = '#ef4444'
hintArea.style.opacity = '1'
}
busy = false
guessBtn.textContent = 'Guess!'
guessBtn.style.background = '#22c55e'
statusEl.textContent = 'Ready'
}
function adjust(delta) {
if (busy) return
currentGuess = Math.max(1, Math.min(100, currentGuess + delta))
guessDisplay.textContent = currentGuess
hintArea.textContent = ''
hintArea.style.opacity = '0'
}
window.connect = connect
window.adjust = function (delta) { adjust(delta) }
window.sendGuess = function () { sendGuess() }
}
// ---------------------------------------------------------------------------
// Phone page component
// ---------------------------------------------------------------------------
export function PhonePage() {
return (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Corey's Screechy Audio Demo</title>
<link rel="stylesheet" href="/styles.css" />
<script src="/ggwave.js">{""}</script>
</head>
<body style="margin:0;overflow:hidden;touch-action:manipulation">
<Page>
<Screen id="connect-screen">
<Title>Corey's Screechy Audio Demo</Title>
<VolumeWarning>🔊 Turn your volume up on both devices!</VolumeWarning>
<div style="margin-top:32px">
<ConnectBtn onclick="connect()">Connect</ConnectBtn>
</div>
<ConnectStatus id="connect-status" />
</Screen>
<Screen id="game-screen" style="display:none">
<Title>Guess the Number<br />1 100</Title>
<GuessDisplay id="guess-display">50</GuessDisplay>
<Controls>
<StepBtn onclick="adjust(-10)">-10</StepBtn>
<StepBtn onclick="adjust(-1)">-1</StepBtn>
<StepBtn onclick="adjust(1)">+1</StepBtn>
<StepBtn onclick="adjust(10)">+10</StepBtn>
</Controls>
<GuessBtn id="guess-btn" onclick="sendGuess()">Guess!</GuessBtn>
<HintText id="hint-area" />
</Screen>
<Screen id="victory-screen" style="display:none">
<VictoryEmoji>🎉</VictoryEmoji>
<VictoryText id="victory-text" />
</Screen>
<StatusBar id="status" />
</Page>
<script dangerouslySetInnerHTML={{ __html: `(${clientMain})()` }} />
</body>
</html>
)
}
export { stylesToCSS }