โ Public packages
@kentcdodds/world-builder
Build a shared 3D voxel world with friends and your AI agents, live.
app/client/App.tsx
399 lines ยท 13.3 KB ยท TypeScriptimport { signInHref, signOutHref } from './nav.ts'
import { useQuery, useStatus, usePowerSync } from '@powersync/react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { agentPrompt } from '../../src/shared/agent-prompt.ts'
import { MATERIALS, blockId, type MaterialKey } from '../../src/shared/world-rules.ts'
import { BotIcon, PanelIcon, PersonIcon, PlaceIcon, RemoveIcon, SyncIcon } from './icons.tsx'
import type { BlockRow, BuildRow, PlayerRow } from './powersync.ts'
import { World, type Cell, type Mode } from './World.tsx'
export type Session = {
signedIn: boolean
worldId: string
powersyncUrl: string
player?: { id: string; displayName: string; color: string }
}
export type Room = { slug: string; name: string; worldId: string; isOwner: boolean }
const appBase = document.documentElement.dataset.appBase ?? ''
function RoomName({ room, name }: { room: Room; name: string }) {
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(name)
const [saving, setSaving] = useState(false)
if (!room.isOwner) return <span className="world-name">{name}</span>
if (!editing) {
return (
<button type="button" className="world-name editable" title="Rename room" onClick={() => (setDraft(name), setEditing(true))}>
{name}
<span className="edit-hint" aria-hidden="true">โ</span>
</button>
)
}
async function save() {
const next = draft.trim()
if (!next || next === name) return setEditing(false)
setSaving(true)
await fetch(`${appBase}/api/rooms/${room.slug}`, {
method: 'PATCH',
credentials: 'same-origin',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: next }),
}).catch(() => null)
setSaving(false)
setEditing(false)
}
return (
<form
className="rename"
onSubmit={(e) => {
e.preventDefault()
void save()
}}
>
<input
autoFocus
aria-label="Room name"
value={draft}
maxLength={40}
disabled={saving}
onChange={(e) => setDraft(e.target.value)}
onBlur={() => void save()}
onKeyDown={(e) => e.key === 'Escape' && setEditing(false)}
/>
</form>
)
}
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text)
return true
} catch {
const ta = document.createElement('textarea')
ta.value = text
ta.setAttribute('readonly', '')
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
const ok = document.execCommand('copy')
ta.remove()
return ok
}
}
function useCopied() {
const [copied, setCopied] = useState<string | null>(null)
useEffect(() => {
if (!copied) return
const t = setTimeout(() => setCopied(null), 2200)
return () => clearTimeout(t)
}, [copied])
return [copied, setCopied] as const
}
function useNow(ms: number) {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
const t = setInterval(() => setNow(Date.now()), ms)
return () => clearInterval(t)
}, [ms])
return now
}
const parseTime = (s: string | null) => {
if (!s) return 0
const iso = s.includes('T') ? s : s.replace(' ', 'T')
return Date.parse(/[zZ]|[+-]\d\d(:?\d\d)?$/.test(iso) ? iso : iso + 'Z')
}
export function App({ session, initialRoom }: { session: Session; initialRoom: Room }) {
const db = usePowerSync()
const status = useStatus()
const worldId = initialRoom.worldId
const me = session.player ?? null
const canEdit = session.signedIn && !!me
const [mode, setMode] = useState<Mode>('place')
const [material, setMaterial] = useState<MaterialKey>('stone')
const [panelOpen, setPanelOpen] = useState(() => window.matchMedia('(min-width: 900px)').matches)
const now = useNow(5000)
const { data: blocks } = useQuery<BlockRow>(
'SELECT id, world_id, x, y, z, material, build_id, placed_by, seq FROM blocks WHERE world_id = ?',
[worldId],
)
const { data: players } = useQuery<PlayerRow>('SELECT * FROM players WHERE world_id = ? ORDER BY display_name', [worldId])
const { data: builds } = useQuery<BuildRow>(
'SELECT * FROM builds WHERE world_id = ? ORDER BY created_at DESC LIMIT 20',
[worldId],
)
const { data: worldRows } = useQuery<{ id: string; name: string }>('SELECT id, name FROM worlds WHERE id = ?', [worldId])
const worldName = worldRows[0]?.name ?? initialRoom.name
const roomUrl = `${location.origin}${appBase}/r/${initialRoom.slug}`
const [copied, setCopied] = useCopied()
const copyPrompt = async () => {
const text = agentPrompt({
roomUrl,
roomCode: initialRoom.slug,
roomName: worldName,
userName: me?.displayName ?? 'Me',
})
;(window as unknown as { __wbPrompt: string }).__wbPrompt = text
if (await copyText(text)) setCopied('prompt')
}
const copyLink = async () => {
if (await copyText(roomUrl)) setCopied('link')
}
const playerById = useMemo(() => new Map(players.map((p) => [p.id, p])), [players])
const online = players.filter((p) => {
const seen = parseTime(p.last_seen)
const building = builds.some((b) => b.author_id === p.id && (b.status === 'building' || b.status === 'planning'))
return p.id === me?.id || building || now - seen < 120_000
})
const place = useCallback(
async (cell: Cell) => {
if (!me) return
const id = blockId(worldId, cell.x, cell.y, cell.z)
await db.writeTransaction(async (tx) => {
const existing = await tx.getOptional('SELECT id FROM blocks WHERE id = ?', [id])
if (existing) {
await tx.execute('UPDATE blocks SET material = ?, placed_by = ?, build_id = NULL WHERE id = ?', [material, me.id, id])
} else {
await tx.execute(
'INSERT INTO blocks (id, world_id, x, y, z, material, placed_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, worldId, cell.x, cell.y, cell.z, material, me.id],
)
}
})
},
[db, me, worldId, material],
)
const remove = useCallback(
async (block: BlockRow) => {
await db.execute('DELETE FROM blocks WHERE id = ?', [block.id])
},
[db],
)
// Share my cursor (throttled) so others see where I'm pointing.
const pendingCursor = useRef<Cell | null | undefined>(undefined)
useEffect(() => {
if (!me) return
let last = ''
const t = setInterval(() => {
const c = pendingCursor.current
if (c === undefined) return
const key = c ? `${c.x}:${c.y}:${c.z}` : 'none'
if (key === last) return
last = key
void db.execute('UPDATE players SET cursor_x = ?, cursor_y = ?, cursor_z = ?, world_id = ? WHERE id = ?', [
c?.x ?? null,
c?.y ?? null,
c?.z ?? null,
worldId,
me.id,
])
}, 350)
return () => clearInterval(t)
}, [db, me, worldId])
const onHover = useCallback((cell: Cell | null) => {
pendingCursor.current = cell
}, [])
const syncState: 'synced' | 'syncing' | 'offline' = !status.connected
? 'offline'
: status.dataFlowStatus?.uploading || status.dataFlowStatus?.downloading || !status.hasSynced
? 'syncing'
: 'synced'
const syncLabel = { synced: 'Live', syncing: 'Syncing', offline: 'Offline' }[syncState]
return (
<div className={`app ${panelOpen ? 'panel-open' : ''}`}>
<header className="topbar">
<div className="brand">
<img src={`${document.documentElement.dataset.assetBase}/icon.png`} alt="" width={28} height={28} />
<div className="brand-text">
<strong>World Builder</strong>
<RoomName room={initialRoom} name={worldName} />
</div>
</div>
<div className="top-right">
<button type="button" className="chip" onClick={copyLink} title="Copy the room link to share it">
{copied === 'link' ? 'Link copied!' : 'Share'}
</button>
{canEdit ? (
<button type="button" className="chip primary" onClick={copyPrompt} title="Copy a prompt that teaches your agent to build in this room">
<BotIcon />
<span className="chip-label">{copied === 'prompt' ? 'Copied!' : 'Copy prompt for your agent'}</span>
</button>
) : (
<a className="chip primary" href={signInHref()}>
Sign in to build
</a>
)}
<div className="avatars" aria-label={`${online.length} builders online`}>
{online.slice(0, 5).map((p) => (
<span key={p.id} className="avatar" style={{ background: p.color }} title={p.display_name}>
{p.kind === 'agent' ? <BotIcon /> : initials(p.display_name)}
</span>
))}
{online.length > 5 ? <span className="avatar more">+{online.length - 5}</span> : null}
</div>
<span className={`sync ${syncState}`} title={`Sync: ${syncLabel}`} aria-label={`Sync status: ${syncLabel}`}>
<SyncIcon state={syncState} />
</span>
{session.signedIn ? (
<a className="chip" href={signOutHref()} title="Sign out of Kody on this site">
Sign out
</a>
) : null}
<button type="button" className="icon-btn" onClick={() => setPanelOpen((v) => !v)} aria-label="Toggle side panel" aria-pressed={panelOpen}>
<PanelIcon />
</button>
</div>
</header>
<main className="stage">
<World
blocks={blocks}
players={players}
builds={builds}
meId={me?.id ?? null}
mode={mode}
canEdit={canEdit}
onPlace={place}
onRemove={remove}
onHover={onHover}
/>
{!status.hasSynced ? (
<div className="stage-overlay" aria-hidden="true">
<div className="boot-cube" />
</div>
) : null}
{!canEdit ? <div className="banner">You're watching. <a href={signInHref()}>Sign in to build</a></div> : null}
{copied === 'prompt' ? (
<div className="toast" role="status">
Copied! Paste it into Claude, ChatGPT or Grok, then ask for a castle.
</div>
) : null}
</main>
{canEdit ? <nav className="toolbar" aria-label="Build tools">
<div className="modes" role="radiogroup" aria-label="Mode">
<button type="button" role="radio" aria-checked={mode === 'place'} className={mode === 'place' ? 'on' : ''} onClick={() => setMode('place')} title="Place blocks">
<PlaceIcon />
</button>
<button type="button" role="radio" aria-checked={mode === 'remove'} className={mode === 'remove' ? 'on' : ''} onClick={() => setMode('remove')} title="Remove blocks (or right-click / shift-click)">
<RemoveIcon />
</button>
</div>
<div className="palette" role="radiogroup" aria-label="Block">
{MATERIALS.map((m) => (
<button
key={m.key}
type="button"
role="radio"
aria-checked={material === m.key}
aria-label={m.label}
title={m.label}
className={`swatch ${material === m.key ? 'on' : ''}`}
style={{ background: m.color, opacity: m.opacity ? 0.75 : 1 }}
onClick={() => {
setMaterial(m.key)
setMode('place')
}}
/>
))}
</div>
</nav> : null}
<aside className="panel" aria-label="World activity">
<section>
<h2>Builders <span className="count">{online.length}</span></h2>
<ul className="people">
{online.length === 0 ? <li className="muted">Nobody here yet.</li> : null}
{online.map((p) => (
<li key={p.id}>
<span className="dot" style={{ background: p.color }} />
<span className="kind">{p.kind === 'agent' ? <BotIcon /> : <PersonIcon />}</span>
<span className="name">{p.display_name}</span>
{p.id === me?.id ? <span className="pill">you</span> : null}
</li>
))}
</ul>
</section>
<section>
<h2>Build feed</h2>
<ul className="feed">
{builds.length === 0 ? (
<li className="muted">No agent builds yet. Ask your agent to build something!</li>
) : null}
{builds.map((b) => {
const author = playerById.get(b.author_id)
const active = b.status === 'building' || b.status === 'planning'
return (
<li key={b.id} className={active ? 'active' : ''}>
<div className="feed-head">
<span className="dot" style={{ background: author?.color ?? '#888' }} />
<span className="name">{author?.display_name ?? 'An agent'}</span>
<span className={`status ${b.status}`}>{active ? 'building' : b.status}</span>
</div>
{b.prompt ? <p className="prompt">{b.prompt}</p> : null}
<div className="feed-meta">{b.block_count.toLocaleString()} blocks</div>
</li>
)
})}
</ul>
</section>
<section className="howto">
<h2>Bring your agent</h2>
<p>
Connect Claude, ChatGPT or Grok to Kody. Copy the prompt, paste it into your agent, then ask: <em>"Build me a castle."</em> Its blocks stream in here live.
</p>
{canEdit ? (
<button type="button" className="cta small" onClick={copyPrompt}>
{copied === 'prompt' ? 'Copied!' : 'Copy prompt for your agent'}
</button>
) : (
<a className="cta small" href={signInHref()}>
Sign in to build
</a>
)}
</section>
<details className="details">
<summary>Details</summary>
<dl>
<dt>Blocks</dt>
<dd>{blocks.length.toLocaleString()}</dd>
<dt>Room code</dt>
<dd className="mono">{initialRoom.slug}</dd>
<dt>World id</dt>
<dd className="mono">{worldId}</dd>
<dt>Your player</dt>
<dd className="mono">{me?.id ?? 'spectator'}</dd>
<dt>Sync</dt>
<dd>
{syncLabel}
{status.lastSyncedAt ? ` ยท ${status.lastSyncedAt.toLocaleTimeString()}` : ''}
</dd>
<dt>PowerSync</dt>
<dd className="mono">{new URL(session.powersyncUrl).host}</dd>
</dl>
</details>
</aside>
</div>
)
}
function initials(name: string) {
return name
.split(/\s+/)
.map((w) => w[0])
.join('')
.slice(0, 2)
.toUpperCase()
}