Spaces:
Running
Running
| <script> | |
| /** @type {{ store: ReturnType<import('../lib/stores/robot.svelte.js').createRobotStore>, vision: ReturnType<import('../lib/stores/vision.svelte.js').createVisionStore>, auth: ReturnType<import('../lib/stores/auth.svelte.js').createAuthStore> }} */ | |
| let { store, vision, auth } = $props(); | |
| let expanded = $state(false); | |
| let tokenInput = $state(""); | |
| const handleDepth = () => vision.toggleDepth(); | |
| const handlePanoptic = () => vision.togglePanoptic(); | |
| const handleFaceParse = () => vision.toggleFaceParse(); | |
| const handleTrack = () => vision.toggleTrack(); | |
| const handleResetOrientation = () => { | |
| store.commands.gotoTarget({ | |
| head: [ | |
| [1, 0, 0, 0], | |
| [0, 1, 0, 0], | |
| [0, 0, 1, 0], | |
| [0, 0, 0, 1], | |
| ], | |
| }); | |
| }; | |
| </script> | |
| <div class="controls"> | |
| <div class="controls-bar"> | |
| {#if store.connectionStatus === 'disconnected'} | |
| {#if auth.isLoggedIn} | |
| <div class="btn-group"> | |
| <button class="btn btn-hf" onclick={() => store.connectCentral(auth.token)}> | |
| Connect as @{auth.username} | |
| </button> | |
| <button class="btn btn-small" onclick={() => auth.logout()}> | |
| Sign out | |
| </button> | |
| </div> | |
| {:else if auth.isHfSpace} | |
| <button class="btn btn-hf" onclick={() => auth.login()}> | |
| Sign in with HF | |
| </button> | |
| {:else} | |
| <div class="btn-group"> | |
| <input | |
| class="host-input" | |
| type="password" | |
| placeholder="hf_... token" | |
| bind:value={tokenInput} | |
| /> | |
| <button class="btn btn-hf" disabled={!tokenInput.trim()} onclick={() => { auth.setToken(tokenInput); store.connectCentral(tokenInput.trim()); }}> | |
| Connect | |
| </button> | |
| </div> | |
| {/if} | |
| {:else if store.connectionStatus === 'connecting'} | |
| {#if store.producers.length > 1} | |
| <div class="btn-group"> | |
| {#each store.producers as producer} | |
| <button class="btn btn-primary" onclick={() => store.connectProducer(auth.token, producer.id)}> | |
| {producer.meta?.name || producer.id} | |
| </button> | |
| {/each} | |
| </div> | |
| {:else} | |
| <button class="btn" disabled> | |
| Connecting... | |
| </button> | |
| {/if} | |
| {:else} | |
| <div class="btn-group"> | |
| <select | |
| class="model-select" | |
| value={vision.detectorModelId} | |
| onchange={(e) => vision.setDetectorModel(e.target.value)} | |
| > | |
| {#each vision.detectorModels as m} | |
| <option value={m.id}>{m.label}</option> | |
| {/each} | |
| </select> | |
| <button class="btn btn-ai" class:active={vision.depthActive} onclick={handleDepth}> | |
| {vision.depthActive ? 'Stop Depth' : 'Depth'} | |
| </button> | |
| <button class="btn btn-ai" class:active={vision.panopticActive} onclick={handlePanoptic}> | |
| {vision.panopticActive ? 'Stop Segment' : 'Segment'} | |
| </button> | |
| <button class="btn btn-ai" class:active={vision.faceParseActive} onclick={handleFaceParse}> | |
| {vision.faceParseActive ? 'Stop Face' : 'Face'} | |
| </button> | |
| <button class="btn btn-ai" class:active={vision.trackActive} onclick={handleTrack}> | |
| {vision.trackActive ? 'Stop Track' : 'Track'} | |
| </button> | |
| <button class="btn btn-ai btn-small" onclick={handleResetOrientation}> | |
| Reset | |
| </button> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-disconnect" onclick={() => store.disconnect()}> | |
| Disconnect | |
| </button> | |
| </div> | |
| {/if} | |
| {#if store.robotState} | |
| <button | |
| class="btn btn-small" | |
| onclick={() => expanded = !expanded} | |
| > | |
| {expanded ? 'Hide' : 'State'} | |
| </button> | |
| {/if} | |
| </div> | |
| {#if expanded && store.robotState} | |
| <pre class="state-view">{JSON.stringify(store.robotState, null, 2)}</pre> | |
| {/if} | |
| </div> | |
| <style> | |
| .controls { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 16px; | |
| background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); | |
| pointer-events: none; | |
| } | |
| .controls-bar { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| align-items: center; | |
| pointer-events: auto; | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 8px; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: opacity 0.15s; | |
| color: #fff; | |
| background: rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(8px); | |
| } | |
| .btn:hover:not(:disabled) { | |
| opacity: 0.85; | |
| } | |
| .btn:disabled { | |
| opacity: 0.4; | |
| cursor: not-allowed; | |
| } | |
| .btn.active { | |
| opacity: 1; | |
| background: rgba(59, 130, 246, 0.6); | |
| animation: pulse 1s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| .btn-primary { | |
| background: #3b82f6; | |
| } | |
| .btn-ai { | |
| background: rgba(168, 85, 247, 0.6); | |
| } | |
| .btn-hf { | |
| background: rgba(255, 159, 28, 0.7); | |
| } | |
| .btn-disconnect { | |
| background: rgba(239, 68, 68, 0.7); | |
| } | |
| .host-input { | |
| padding: 10px 14px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| color: #fff; | |
| background: rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(8px); | |
| outline: none; | |
| width: 180px; | |
| } | |
| .host-input::placeholder { | |
| color: rgba(255, 255, 255, 0.4); | |
| } | |
| .host-input:focus { | |
| background: rgba(255, 255, 255, 0.25); | |
| } | |
| .btn-small { | |
| padding: 6px 12px; | |
| font-size: 0.8rem; | |
| } | |
| .model-select { | |
| padding: 6px 10px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| color: #fff; | |
| background: rgba(168, 85, 247, 0.4); | |
| backdrop-filter: blur(8px); | |
| cursor: pointer; | |
| outline: none; | |
| } | |
| .model-select option { | |
| background: #1a1a2e; | |
| color: #fff; | |
| } | |
| .state-view { | |
| margin-top: 12px; | |
| padding: 12px; | |
| background: rgba(0, 0, 0, 0.7); | |
| border-radius: 8px; | |
| color: #a5f3a5; | |
| font-size: 0.75rem; | |
| line-height: 1.4; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| pointer-events: auto; | |
| backdrop-filter: blur(8px); | |
| } | |
| </style> | |