feat: unified HomeAI dashboard — merge character + desktop into single app

Combines homeai-character (service status, character profiles, editor) and
homeai-desktop (chat with voice I/O) into homeai-dashboard on port 5173.

- 4-page sidebar layout: Dashboard, Chat, Characters, Editor
- Merged Vite middleware: health checks, service restart, bridge proxy
- Bridge upgraded to ThreadingHTTPServer (fixes LAN request queuing)
- TTS strips emojis before synthesis
- Updated start.sh with new launchd service names
- Added preload-models to startup sequence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-13 18:40:04 +00:00
parent 0c33de607f
commit 3c0d905e64
37 changed files with 5158 additions and 2 deletions

View File

@@ -0,0 +1,74 @@
import { VOICES } from '../lib/constants'
export default function SettingsDrawer({ isOpen, onClose, settings, onUpdate }) {
if (!isOpen) return null
return (
<>
<div className="fixed inset-0 bg-black/50 z-40" onClick={onClose} />
<div className="fixed right-0 top-0 bottom-0 w-80 bg-gray-900 border-l border-gray-800 z-50 flex flex-col">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-800">
<h2 className="text-sm font-medium text-gray-200">Settings</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-300">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-5">
{/* Voice */}
<div>
<label className="block text-xs font-medium text-gray-400 mb-1.5">Voice</label>
<select
value={settings.voice}
onChange={(e) => onUpdate('voice', e.target.value)}
className="w-full bg-gray-800 text-gray-200 text-sm rounded-lg px-3 py-2 border border-gray-700 focus:outline-none focus:border-indigo-500"
>
{VOICES.map((v) => (
<option key={v.id} value={v.id}>{v.label}</option>
))}
</select>
</div>
{/* Auto TTS */}
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-200">Auto-speak responses</div>
<div className="text-xs text-gray-500">Speak assistant replies aloud</div>
</div>
<button
onClick={() => onUpdate('autoTts', !settings.autoTts)}
className={`relative w-10 h-6 rounded-full transition-colors ${
settings.autoTts ? 'bg-indigo-600' : 'bg-gray-700'
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform ${
settings.autoTts ? 'translate-x-4' : ''
}`}
/>
</button>
</div>
{/* STT Mode */}
<div>
<label className="block text-xs font-medium text-gray-400 mb-1.5">Speech recognition</label>
<select
value={settings.sttMode}
onChange={(e) => onUpdate('sttMode', e.target.value)}
className="w-full bg-gray-800 text-gray-200 text-sm rounded-lg px-3 py-2 border border-gray-700 focus:outline-none focus:border-indigo-500"
>
<option value="bridge">Wyoming STT (local)</option>
<option value="webspeech">Web Speech API (browser)</option>
</select>
<p className="text-xs text-gray-500 mt-1">
{settings.sttMode === 'bridge'
? 'Uses Whisper via the local bridge server'
: 'Uses browser built-in speech recognition'}
</p>
</div>
</div>
</div>
</>
)
}