mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
yayy!!!
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-frHSA8Fb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dx4QsRNa.css">
|
||||
<script type="module" crossorigin src="/assets/index-BY461TBg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DzG-Bp9_.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+8
-2
@@ -85,12 +85,18 @@ function MobileFeatureTabs({
|
||||
}) {
|
||||
const { session } = useSession();
|
||||
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-amber-400';
|
||||
const ownRoverId = String(session?.assignment?.roverId || '').trim();
|
||||
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
|
||||
const vipMicActive = Boolean(
|
||||
ownAudioForward?.source === 'mic-whip' &&
|
||||
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
|
||||
);
|
||||
return (
|
||||
<section className="panel text-base">
|
||||
<Tabs defaultTab="chat">
|
||||
<TabList>
|
||||
<Tab id="chat">Chat</Tab>
|
||||
<Tab id="vip">
|
||||
<Tab id="vip" highlight={vipMicActive ? 'pink' : 'none'}>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<span>VIP</span>
|
||||
<span
|
||||
@@ -111,7 +117,7 @@ function MobileFeatureTabs({
|
||||
<RawUserPilePanel />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="vip">
|
||||
<TabPanel id="vip" keepMounted>
|
||||
<VipPanel />
|
||||
</TabPanel>
|
||||
<TabPanel id="roomcontrols">
|
||||
|
||||
@@ -112,12 +112,18 @@ function DriveDockPanel() {
|
||||
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
const { session } = useSession();
|
||||
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-red-600';
|
||||
const ownRoverId = String(session?.assignment?.roverId || '').trim();
|
||||
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
|
||||
const vipMicActive = Boolean(
|
||||
ownAudioForward?.source === 'mic-whip' &&
|
||||
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
|
||||
);
|
||||
return (
|
||||
<section className="panel text-base">
|
||||
<Tabs defaultTab="telemetry">
|
||||
<TabList>
|
||||
<Tab id="telemetry">Controls</Tab>
|
||||
<Tab id="vip">
|
||||
<Tab id="vip" highlight={vipMicActive ? 'pink' : 'none'}>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span>VIP</span>
|
||||
<span
|
||||
@@ -153,7 +159,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="vip">
|
||||
<TabPanel id="vip" keepMounted>
|
||||
<VipPanel />
|
||||
</TabPanel>
|
||||
<TabPanel id="help">
|
||||
|
||||
@@ -18,6 +18,11 @@ const TAB_VARIANTS = {
|
||||
},
|
||||
};
|
||||
|
||||
const TAB_HIGHLIGHTS = {
|
||||
none: '',
|
||||
pink: 'bg-pink-500 text-white border-pink-200 hover:bg-pink-400',
|
||||
};
|
||||
|
||||
const DEFAULT_VARIANT = 'primary';
|
||||
|
||||
function classNames(...parts) {
|
||||
@@ -89,17 +94,18 @@ export function TabList({ children, className = '' }) {
|
||||
return <div className={classNames('flex gap-0.5', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function Tab({ id, children, className = '', disabled = false }) {
|
||||
export function Tab({ id, children, className = '', disabled = false, highlight = 'none' }) {
|
||||
const { activeTab, setActiveTab, registerTab, variant } = useTabsContext();
|
||||
|
||||
useEffect(() => registerTab(id), [id, registerTab]);
|
||||
|
||||
const variantStyles = TAB_VARIANTS[variant] ?? TAB_VARIANTS[DEFAULT_VARIANT];
|
||||
const isActive = activeTab === id;
|
||||
const highlightClass = TAB_HIGHLIGHTS[highlight] || TAB_HIGHLIGHTS.none;
|
||||
|
||||
const buttonClassName = classNames(
|
||||
variantStyles.base,
|
||||
isActive ? variantStyles.active : variantStyles.inactive,
|
||||
highlightClass || (isActive ? variantStyles.active : variantStyles.inactive),
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
className
|
||||
);
|
||||
|
||||
@@ -42,32 +42,66 @@ export default function VipPanel() {
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
{isVerified ? (
|
||||
<VipAudioUploadCard
|
||||
ownRoverId={ownRoverId}
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
playUploadedAudio={playUploadedAudio}
|
||||
stopUploadedAudio={stopUploadedAudio}
|
||||
startMicWhip={startMicWhip}
|
||||
readyMicWhip={readyMicWhip}
|
||||
stopMicWhip={stopMicWhip}
|
||||
/>
|
||||
) : (
|
||||
<VipVerificationCard
|
||||
pendingRequestId={pendingRequestId}
|
||||
currentStoredKey={currentStoredKey}
|
||||
nickname={nickname}
|
||||
requestVerification={requestVerification}
|
||||
applyIdentityKey={applyIdentityKey}
|
||||
onMessage={setMessage}
|
||||
/>
|
||||
)}
|
||||
<div className="grid gap-0.5 grid-cols-1 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,0.9fr)]">
|
||||
<div className="lg:col-span-2">
|
||||
{isVerified ? (
|
||||
<section className="surface w-full">
|
||||
<div className="mx-auto flex w-full max-w-md flex-col items-center space-y-0.5 text-center">
|
||||
<p className="text-sm text-emerald-300">Verification complete</p>
|
||||
<p className="text-xs text-slate-400">VIP audio controls are unlocked for your assigned rover.</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<VipVerificationCard
|
||||
pendingRequestId={pendingRequestId}
|
||||
currentStoredKey={currentStoredKey}
|
||||
nickname={nickname}
|
||||
requestVerification={requestVerification}
|
||||
applyIdentityKey={applyIdentityKey}
|
||||
onMessage={setMessage}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<VipIdentityCard
|
||||
currentStoredKey={currentStoredKey}
|
||||
applyIdentityKey={applyIdentityKey}
|
||||
onMessage={setMessage}
|
||||
/>
|
||||
{isVerified ? (
|
||||
<VipAudioUploadCard
|
||||
ownRoverId={ownRoverId}
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
playUploadedAudio={playUploadedAudio}
|
||||
stopUploadedAudio={stopUploadedAudio}
|
||||
startMicWhip={startMicWhip}
|
||||
readyMicWhip={readyMicWhip}
|
||||
stopMicWhip={stopMicWhip}
|
||||
/>
|
||||
) : (
|
||||
<section className="surface h-full">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
|
||||
Verify your account to unlock audio upload and microphone forwarding.
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="surface h-full">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">VIP Notes</p>
|
||||
<ul className="space-y-0.25 text-xs text-slate-400">
|
||||
<li>Audio forwarding is locked to your currently assigned rover.</li>
|
||||
<li>Losing control or turn ownership stops active forwarding.</li>
|
||||
<li>PTT works page-wide using your keybind (default: m).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<VipIdentityCard
|
||||
currentStoredKey={currentStoredKey}
|
||||
applyIdentityKey={applyIdentityKey}
|
||||
onMessage={setMessage}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message ? (
|
||||
<div className={flowWrapClass}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { fieldClass, flowWrapClass, innerFlowClass } from './constants.js';
|
||||
import { fieldClass } from './constants.js';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
|
||||
const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
||||
@@ -154,6 +154,18 @@ function waitForOutboundAudioFlow(pc, timeoutMs = 6000) {
|
||||
});
|
||||
}
|
||||
|
||||
function StatusLight({ label, active, detail = '' }) {
|
||||
return (
|
||||
<div className="surface-muted flex items-center justify-between px-0.5 py-0.25 text-xs">
|
||||
<span className="text-slate-300">{label}</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<span className={`h-2 w-2 rounded-full ${active ? 'bg-emerald-400' : 'bg-slate-600'}`} aria-hidden="true" />
|
||||
<span className={active ? 'text-emerald-300' : 'text-slate-500'}>{detail || (active ? 'on' : 'off')}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function VipAudioUploadCard({
|
||||
ownRoverId = '',
|
||||
audioForwardByRover = {},
|
||||
@@ -182,6 +194,18 @@ export default function VipAudioUploadCard({
|
||||
[audioForwardByRover, roverId],
|
||||
);
|
||||
|
||||
const pipelineConnected = Boolean(
|
||||
selectedForwardState && selectedForwardState.state !== 'offline' && !selectedForwardState.error,
|
||||
);
|
||||
const uploadPlaying = Boolean(
|
||||
selectedForwardState?.source === 'upload' && selectedForwardState?.state === 'playing',
|
||||
);
|
||||
const micRelayActive = Boolean(
|
||||
selectedForwardState?.source === 'mic-whip' &&
|
||||
(selectedForwardState?.state === 'starting' || selectedForwardState?.state === 'playing'),
|
||||
);
|
||||
const micTalking = Boolean(audioTrackRef.current && (openMicEnabled || pttActive));
|
||||
|
||||
const handleUploadPlay = async () => {
|
||||
if (!roverId) {
|
||||
setMessage('Take control of your rover first.');
|
||||
@@ -403,24 +427,27 @@ export default function VipAudioUploadCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={`surface ${flowWrapClass}`}>
|
||||
<div className={innerFlowClass}>
|
||||
<p className="text-sm text-slate-300">VIP Audio Upload</p>
|
||||
<label className="grid w-full gap-0.5 text-xs text-slate-300">
|
||||
<section className="surface h-full">
|
||||
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_auto_1fr_auto]">
|
||||
<p className="text-sm text-slate-200 text-center">VIP Audio Forwarding</p>
|
||||
|
||||
<label className="mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center">
|
||||
<span>Audio file (mp3 / wav / ogg)</span>
|
||||
<input
|
||||
className={fieldClass}
|
||||
className={`${fieldClass} text-center`}
|
||||
type="file"
|
||||
accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg"
|
||||
disabled={working || !roverId}
|
||||
onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{selectedUpload ? (
|
||||
<div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
|
||||
{selectedUpload.name} ({selectedUpload.size} bytes)
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-center gap-0.5">
|
||||
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadPlay}>
|
||||
{working ? 'Working...' : 'Play Upload'}
|
||||
@@ -430,28 +457,31 @@ export default function VipAudioUploadCard({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="surface-muted mx-auto flex w-full max-w-sm flex-col gap-0.5 p-0.5 text-xs text-slate-300 text-center">
|
||||
<p className="text-slate-200">Microphone Forwarding</p>
|
||||
<label className="flex items-center justify-center gap-0.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={openMicEnabled}
|
||||
disabled={!roverId}
|
||||
onChange={(event) => setOpenMicEnabled(Boolean(event.target.checked))}
|
||||
/>
|
||||
<span>Open mic</span>
|
||||
</label>
|
||||
<p className="text-slate-400">PTT key: {controlState?.keymap?.micPtt?.[0] || 'm'} (hold)</p>
|
||||
<p className="text-slate-400">mic: {micState}</p>
|
||||
<p className="text-slate-500">transport: whip</p>
|
||||
<div className="grid gap-0.5 lg:grid-cols-2">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-slate-300 text-center">Audio Path Status</p>
|
||||
<StatusLight label="Forward pipe" active={pipelineConnected} detail={pipelineConnected ? 'connected' : 'offline'} />
|
||||
<StatusLight label="Upload playing" active={uploadPlaying} detail={uploadPlaying ? 'active' : 'idle'} />
|
||||
<StatusLight label="Mic relay" active={micRelayActive} detail={micRelayActive ? 'active' : 'idle'} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-slate-300 text-center">Microphone</p>
|
||||
<label className="surface-muted flex items-center justify-center gap-0.5 px-0.5 py-0.25 text-xs text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={openMicEnabled}
|
||||
disabled={!roverId}
|
||||
onChange={(event) => setOpenMicEnabled(Boolean(event.target.checked))}
|
||||
/>
|
||||
<span>Open mic</span>
|
||||
</label>
|
||||
<StatusLight label="WHIP link" active={micState === 'live' || micState === 'starting'} detail={micState} />
|
||||
<StatusLight label="PTT transmit" active={micTalking} detail={micTalking ? 'talking' : 'muted'} />
|
||||
<div className="surface-muted text-center text-xs text-slate-400">PTT key: {controlState?.keymap?.micPtt?.[0] || 'm'} (hold)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedForwardState ? (
|
||||
<div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
|
||||
state: {selectedForwardState.state || 'idle'}
|
||||
{selectedForwardState.error ? ` | error: ${selectedForwardState.error}` : ''}
|
||||
</div>
|
||||
) : null}
|
||||
{message ? <div className="text-xs text-slate-400 text-center">{message}</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { fieldClass, flowWrapClass, innerFlowClass, maskKey } from './constants.js';
|
||||
|
||||
export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, onMessage }) {
|
||||
export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, onMessage, fullWidth = false }) {
|
||||
const [restoreFlowStep, setRestoreFlowStep] = useState(0);
|
||||
const [restoreKeyInput, setRestoreKeyInput] = useState('');
|
||||
const [working, setWorking] = useState(false);
|
||||
@@ -34,8 +34,10 @@ export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, on
|
||||
}
|
||||
};
|
||||
|
||||
const wrapClass = fullWidth ? 'w-full' : flowWrapClass;
|
||||
|
||||
return (
|
||||
<section className={`surface ${flowWrapClass}`}>
|
||||
<section className={`surface ${wrapClass}`}>
|
||||
<div className={innerFlowClass}>
|
||||
<p className="text-sm text-slate-300">Identity key</p>
|
||||
<p className="text-xs text-slate-500">Current: {maskKey(currentStoredKey) || 'not set yet'}</p>
|
||||
|
||||
@@ -9,6 +9,7 @@ export default function VipVerificationCard({
|
||||
requestVerification,
|
||||
applyIdentityKey,
|
||||
onMessage,
|
||||
fullWidth = false,
|
||||
}) {
|
||||
const [requestFlowStep, setRequestFlowStep] = useState(0);
|
||||
const [requestKeyInput, setRequestKeyInput] = useState('');
|
||||
@@ -53,9 +54,11 @@ export default function VipVerificationCard({
|
||||
}
|
||||
};
|
||||
|
||||
const wrapClass = fullWidth ? 'w-full' : flowWrapClass;
|
||||
|
||||
if (pendingRequestId) {
|
||||
return (
|
||||
<section className={`surface text-sm text-slate-300 ${flowWrapClass}`}>
|
||||
<section className={`surface text-sm text-slate-300 ${wrapClass}`}>
|
||||
<div className={innerFlowClass}>Verification request pending: {pendingRequestId}</div>
|
||||
</section>
|
||||
);
|
||||
@@ -63,7 +66,7 @@ export default function VipVerificationCard({
|
||||
|
||||
if (requestFlowStep === 0) {
|
||||
return (
|
||||
<section className={`surface ${flowWrapClass}`}>
|
||||
<section className={`surface ${wrapClass}`}>
|
||||
<div className={innerFlowClass}>
|
||||
<p className="text-sm text-slate-300">Verification</p>
|
||||
<button type="button" className="button-dark text-sm" onClick={beginRequestFlow} disabled={working}>
|
||||
@@ -75,7 +78,7 @@ export default function VipVerificationCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<form className={`surface ${flowWrapClass}`} onSubmit={handleRequestSubmit}>
|
||||
<form className={`surface ${wrapClass}`} onSubmit={handleRequestSubmit}>
|
||||
<div className={innerFlowClass}>
|
||||
<p className="text-sm text-slate-300">Request verification</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
|
||||
Reference in New Issue
Block a user