feat: add homeai-desktop web assistant with LAN access

Adds the desktop web assistant app (Vite + React) with OpenClaw bridge
proxy and exposes it on the local network (host: 0.0.0.0, port 5174).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-13 18:16:09 +00:00
parent 2d063c7db7
commit 0c33de607f
26 changed files with 3123 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
function bridgeProxyPlugin() {
return {
name: 'bridge-proxy',
configureServer(server) {
// Proxy a request to the OpenClaw bridge
const proxyRequest = (targetPath) => async (req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
})
res.end()
return
}
try {
const { default: http } = await import('http')
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const body = Buffer.concat(chunks)
await new Promise((resolve, reject) => {
const proxyReq = http.request(
`http://localhost:8081${targetPath}`,
{
method: req.method,
headers: {
'Content-Type': req.headers['content-type'] || 'application/json',
'Content-Length': body.length,
},
timeout: 120000,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, {
'Content-Type': proxyRes.headers['content-type'] || 'application/json',
'Access-Control-Allow-Origin': '*',
})
proxyRes.pipe(res)
proxyRes.on('end', resolve)
proxyRes.on('error', resolve)
}
)
proxyReq.on('error', reject)
proxyReq.on('timeout', () => {
proxyReq.destroy()
reject(new Error('timeout'))
})
proxyReq.write(body)
proxyReq.end()
})
} catch {
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Bridge unreachable' }))
}
}
}
server.middlewares.use('/api/agent/message', proxyRequest('/api/agent/message'))
server.middlewares.use('/api/tts', proxyRequest('/api/tts'))
server.middlewares.use('/api/stt', proxyRequest('/api/stt'))
// Health check — direct to bridge
server.middlewares.use('/api/health', async (req, res) => {
try {
const { default: http } = await import('http')
const start = Date.now()
await new Promise((resolve, reject) => {
const reqObj = http.get('http://localhost:8081/', { timeout: 5000 }, (resp) => {
resp.resume()
resolve()
})
reqObj.on('error', reject)
reqObj.on('timeout', () => { reqObj.destroy(); reject(new Error('timeout')) })
})
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'online', responseTime: Date.now() - start }))
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'offline', responseTime: null }))
}
})
},
}
}
export default defineConfig({
plugins: [
bridgeProxyPlugin(),
tailwindcss(),
react(),
],
server: {
host: '0.0.0.0',
port: 5174,
},
})