mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
ui changes tweaks and flings. new quickstart overlay to replace help overlay.
This commit is contained in:
+2
-1
@@ -17,7 +17,8 @@
|
||||
13. add tool call embeds or something for the llm bot in discord, probably not in web ui
|
||||
14. add discord bot typing thing for when someone requests a replay
|
||||
15. change replay title for ones requested from discord, something other than "requester driving rover"
|
||||
16. better quickstart guide, something better than just a big list of controls. help overlay sucks i think.
|
||||
16. better quickstart guide, something better than just a big list of controls. help overlay sucks i think. [x]
|
||||
1. restyle fullscreen overlay... please..
|
||||
17. fix rover request spam queue cheat
|
||||
|
||||
|
||||
|
||||
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-CNqNUzUB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DC5igy66.css">
|
||||
<script type="module" crossorigin src="/assets/index-BeyfNX1F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C8rSwF1O.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,7 +4,7 @@ const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('overseerControl');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { getRole, roleEvents } = require('../roleService');
|
||||
const { getMode } = require('../modeManager');
|
||||
const { getMode, MODES, modeEvents } = require('../modeManager');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
const neatoService = require('../neatoService');
|
||||
const liftService = require('../liftService');
|
||||
@@ -370,8 +370,12 @@ async function tick() {
|
||||
});
|
||||
} finally {
|
||||
runtime.inFlight = false;
|
||||
updateStatus({ inFlight: false, currentRunId: null, phase: 'idle', nextRunAt: Date.now() + gateIntervalMs });
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
if (status.running) {
|
||||
updateStatus({ inFlight: false, currentRunId: null, phase: 'idle', nextRunAt: Date.now() + gateIntervalMs });
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
} else {
|
||||
updateStatus({ inFlight: false, currentRunId: null, nextRunAt: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +393,28 @@ function clearHistory() {
|
||||
updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared', lastLiveToolCalls: [] });
|
||||
}
|
||||
|
||||
function stopScheduler(reason = 'paused') {
|
||||
if (runtime.timer) {
|
||||
clearTimeout(runtime.timer);
|
||||
runtime.timer = null;
|
||||
}
|
||||
updateStatus({
|
||||
running: false,
|
||||
inFlight: false,
|
||||
currentRunId: null,
|
||||
nextRunAt: null,
|
||||
phase: 'paused',
|
||||
lastOutcome: 'paused',
|
||||
lastReason: reason,
|
||||
});
|
||||
}
|
||||
|
||||
function startScheduler(reason = null) {
|
||||
if (runtime.timer) return;
|
||||
updateStatus({ running: true, phase: 'idle', lastReason: reason });
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
emitStateToSocket(socket);
|
||||
socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => {
|
||||
@@ -407,14 +433,27 @@ homeAssistantEvents.on('update', () => updateStatus({ phase: status.phase }));
|
||||
neatoEvents.on('update', () => updateStatus({ phase: status.phase }));
|
||||
liftEvents.on('update', () => updateStatus({ phase: status.phase }));
|
||||
roverManager.managerEvents.on('rover', () => updateStatus({ phase: status.phase }));
|
||||
modeEvents.on('change', (mode) => {
|
||||
if (!enabled) return;
|
||||
if (mode === MODES.LOCKDOWN) {
|
||||
stopScheduler('paused during lockdown');
|
||||
logger.info('overseerControl paused due to lockdown mode');
|
||||
return;
|
||||
}
|
||||
startScheduler(observeOnly ? 'observe-only mode' : null);
|
||||
});
|
||||
|
||||
if (!enabled) {
|
||||
logger.info('overseerControl disabled');
|
||||
updateStatus({ running: false, lastReason: 'overseerControl.enabled is false' });
|
||||
} else {
|
||||
updateStatus({ running: true, lastReason: observeOnly ? 'observe-only mode' : null });
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly });
|
||||
if (getMode() === MODES.LOCKDOWN) {
|
||||
stopScheduler('paused during lockdown');
|
||||
logger.info('overseerControl paused on startup due to lockdown mode');
|
||||
} else {
|
||||
startScheduler(observeOnly ? 'observe-only mode' : null);
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {};
|
||||
|
||||
+29
-9
@@ -28,6 +28,7 @@ import FloatingFullscreenButton from './components/FloatingFullscreenButton/inde
|
||||
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
|
||||
import { useSettingsNamespace } from './settings/index.js';
|
||||
import HelpOverlay from './components/HelpOverlay/index.jsx';
|
||||
import QuickstartOverlay from './components/QuickstartOverlay/index.jsx';
|
||||
import HelpPanel from './components/HelpPanel/index.jsx';
|
||||
import SettingsPanel from './components/SettingsPanel/index.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './components/Tabs/index.jsx';
|
||||
@@ -250,9 +251,13 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
|
||||
const {
|
||||
value: helpSettings,
|
||||
status: helpStatus,
|
||||
save: saveHelpSettings,
|
||||
} = useSettingsNamespace('help', { showOnLoad: true });
|
||||
const {
|
||||
value: quickstartSettings,
|
||||
status: quickstartStatus,
|
||||
save: saveQuickstartSettings,
|
||||
} = useSettingsNamespace('quickstart', { showOnLoad: true });
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
swapMobileControlColumns: false,
|
||||
});
|
||||
@@ -260,15 +265,17 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
|
||||
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
|
||||
const [helpVisible, setHelpVisible] = useState(false);
|
||||
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (helpStatus === 'ready') {
|
||||
setHelpVisible(helpSettings?.showOnLoad !== false);
|
||||
if (quickstartStatus === 'ready') {
|
||||
setQuickstartVisible(quickstartSettings?.showOnLoad !== false);
|
||||
}
|
||||
}, [helpStatus, helpSettings?.showOnLoad]);
|
||||
}, [quickstartStatus, quickstartSettings?.showOnLoad]);
|
||||
|
||||
const openHelp = useCallback(() => setHelpVisible(true), []);
|
||||
const closeHelp = useCallback(() => setHelpVisible(false), []);
|
||||
const closeQuickstart = useCallback(() => setQuickstartVisible(false), []);
|
||||
const handleFloatingFullscreen = useCallback(async () => {
|
||||
if (fullscreenIsIOS) {
|
||||
showPrompt();
|
||||
@@ -279,16 +286,20 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
showPrompt();
|
||||
}
|
||||
}, [enterFullscreen, fullscreenIsIOS, showPrompt]);
|
||||
const setShowOnLoad = useCallback(
|
||||
const setQuickstartShowOnLoad = useCallback(
|
||||
(enabled) => {
|
||||
const next = Boolean(enabled);
|
||||
saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: next }));
|
||||
saveQuickstartSettings((current) => ({ ...(current ?? {}), showOnLoad: next }));
|
||||
if (!next) {
|
||||
setHelpVisible(false);
|
||||
setQuickstartVisible(false);
|
||||
}
|
||||
},
|
||||
[saveHelpSettings],
|
||||
[saveQuickstartSettings],
|
||||
);
|
||||
const openHelpFromQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
|
||||
const renderedLayout = useMemo(
|
||||
() =>
|
||||
@@ -312,12 +323,13 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
<RewardRunOverlay />
|
||||
<TurnAlertListener />
|
||||
<ModeGateOverlay />
|
||||
|
||||
<HelpOverlay
|
||||
visible={helpVisible}
|
||||
layout={layout}
|
||||
onClose={closeHelp}
|
||||
showOnLoad={helpSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setShowOnLoad}
|
||||
onToggleShowOnLoad={(enabled) => saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: Boolean(enabled) }))}
|
||||
/>
|
||||
<FullscreenPrompt
|
||||
visible={fullscreenVisible}
|
||||
@@ -325,6 +337,14 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
onEnterFullscreen={enterFullscreen}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
<QuickstartOverlay
|
||||
visible={quickstartVisible}
|
||||
layout={layout}
|
||||
showOnLoad={quickstartSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setQuickstartShowOnLoad}
|
||||
onOpenHelp={openHelpFromQuickstart}
|
||||
onClose={closeQuickstart}
|
||||
/>
|
||||
{showFloatingFullscreenButton ? (
|
||||
<FloatingFullscreenButton
|
||||
side={fullscreenButtonSide}
|
||||
|
||||
@@ -6,14 +6,14 @@ export default function FullscreenPrompt({ visible, mode, onEnterFullscreen, onD
|
||||
const isIOSMode = mode === 'pwa-hint';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-end justify-center p-2 pointer-events-none sm:items-center">
|
||||
<div className="pointer-events-auto w-full max-w-sm rounded-lg border border-cyan-500/40 bg-zinc-950/95 shadow-xl">
|
||||
<div className="space-y-0.5 p-4 text-sm text-slate-100">
|
||||
<h2 className="text-base font-semibold text-white">Better in fullscreen</h2>
|
||||
<div className="fixed inset-0 z-40 flex items-center justify-center p-1 pointer-events-none bg-black/20">
|
||||
<div className="pointer-events-auto w-full max-w-sm surface center">
|
||||
<div className="space-y-0.5 p-1 text-sm text-slate-100">
|
||||
<h2 className="text-base font-semibold text-white border-b border-slate-700">Better in fullscreen!</h2>
|
||||
{isIOSMode ? (
|
||||
<p className="text-slate-300">
|
||||
For fullscreen on iOS, open Safari's share menu and pick <strong>Add to Home Screen</strong>. Launching from
|
||||
the home screen removes the browser chrome.
|
||||
For fullscreen on iOS, open Safari's share menu and pick <strong>Add to Home Screen</strong>. Launching from
|
||||
the home screen then makes it fullscreen.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-slate-300">
|
||||
@@ -21,6 +21,8 @@ export default function FullscreenPrompt({ visible, mode, onEnterFullscreen, onD
|
||||
via the system back or home gesture.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs border-t border-b border-slate-700 p-0.5 text-blue-300 text-center">This will only show once just to let you know. There is a fullscreen button in the bottom right for later use.</p>
|
||||
|
||||
<div className="flex justify-end gap-0.5 pt-1 text-sm">
|
||||
<button type="button" className="rounded border border-slate-600 px-3 py-1 text-slate-200" onClick={onDismiss}>
|
||||
{isIOSMode ? 'Got it' : 'Not now'}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function ModeGateOverlay() {
|
||||
const details = getModeDetails(mode);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black/85 px-0.5 py-0.5">
|
||||
<div className="pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black px-0.5 py-0.5">
|
||||
<div className="surface w-full max-w-md space-y-0.5 text-slate-100 shadow-2xl">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-lg font-semibold">{details.title}</p>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import NicknameForm from '../NicknameForm/index.jsx';
|
||||
import SocialButton from '../SocialButton/index.jsx';
|
||||
import KeyPill from '../vip/VipAudioUploadCard/KeyPill.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
|
||||
function ControlRow({ label, keyLabel }) {
|
||||
return (
|
||||
<div className="surface-muted flex items-center justify-between gap-0.5 px-0.5 py-0.35 text-[0.8rem] text-slate-200">
|
||||
<span>{label}</span>
|
||||
<KeyPill label={keyLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopQuickstart({ keymap }) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">1. Press "Start Driving" put your rover into driving mode.</p>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">2. Use the drive controls to move your rover:</p>
|
||||
<div className="space-y-0.5">
|
||||
<ControlRow label="Forward" keyLabel={formatKeyLabel(keymap?.driveForward?.[0])} />
|
||||
<ControlRow label="Backward" keyLabel={formatKeyLabel(keymap?.driveBackward?.[0])} />
|
||||
<ControlRow label="Turn Left" keyLabel={formatKeyLabel(keymap?.driveLeft?.[0])} />
|
||||
<ControlRow label="Turn Right" keyLabel={formatKeyLabel(keymap?.driveRight?.[0])} />
|
||||
<ControlRow label="Move faster" keyLabel={formatKeyLabel(keymap?.boostModifier?.[0])} />
|
||||
<ControlRow label="Move slower" keyLabel={formatKeyLabel(keymap?.slowModifier?.[0])} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-200">3. When done, please dock your rover! Line up with the dock and press "Dock and Charge".</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileQuickstart() {
|
||||
return (
|
||||
<div className="space-y-0.5 text-sm text-slate-200">
|
||||
<p>1. Press "Start Driving" put your rover into driving mode.</p>
|
||||
<p>2. Touch and hold in Joystick area to move.</p>
|
||||
<p>3. Use the other column for motor, horn, and camera controls.</p>
|
||||
<p>4. When done, please dock your rover! Line up with the dock and press "Dock and Charge".</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuickstartOverlay({
|
||||
visible,
|
||||
layout,
|
||||
showOnLoad,
|
||||
onToggleShowOnLoad,
|
||||
onOpenHelp,
|
||||
onClose,
|
||||
}) {
|
||||
const { state } = useControlSystem();
|
||||
const isDesktop = layout === 'desktop';
|
||||
const discordUrl = useSessionSelector((sessionState) => {
|
||||
const socials = sessionState.session?.socials || [];
|
||||
const entry = socials.find((item) => {
|
||||
const key = String(item?.id || item?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
});
|
||||
return entry?.url || sessionState.session?.discord?.invite || null;
|
||||
});
|
||||
|
||||
const keymap = useMemo(() => state?.keymap || {}, [state?.keymap]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const handleCheckbox = (event) => {
|
||||
const keepShowing = !event.target.checked;
|
||||
onToggleShowOnLoad?.(keepShowing);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-end justify-center bg-black/75 p-0.5 items-center">
|
||||
<div className="pointer-events-auto surface w-full max-w-3xl overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-slate-700 px-0.5 py-0.35 text-sm text-slate-200">
|
||||
<span className="font-semibold text-xl">Welcome! To get started:</span>
|
||||
{/* <button type="button" onClick={onClose} className="button-dark px-1 py-0.25 text-[0.8rem]">
|
||||
Close
|
||||
</button> */}
|
||||
</div>
|
||||
<div className={`grid gap-0.5 p-0.5 ${isDesktop ? 'md:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)]' : 'grid-cols-1'}`}>
|
||||
<section className="space-y-0.5 border-b border-slate-700">
|
||||
{isDesktop ? <DesktopQuickstart keymap={keymap} /> : <MobileQuickstart />}
|
||||
</section>
|
||||
{/* {!isDesktop? <div className='w-full h-1 bg-blue-500'></div> : null} */}
|
||||
<section className="space-y-0.5">
|
||||
<p className='text-left'>Next...</p>
|
||||
<div className="surface space-y-0.5 p-0.5 border-b border-slate-700">
|
||||
<p className="text-xl font-semibold text-slate-200">Set your nickname</p>
|
||||
<p className="text-sm font-semibold text-slate-200">Nicknames are assigned randomly by default, you can change yours here.</p>
|
||||
<NicknameForm compact />
|
||||
</div>
|
||||
<div className="surface p-0.5">
|
||||
<p className="text-xl font-semibold text-slate-200">Join our Discord server!</p>
|
||||
<p className="text-sm font-semibold text-slate-200">We have an active and welcoming community :3</p>
|
||||
<SocialButton id="discord" label="Join Discord" url={discordUrl} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-0.5 border-t border-slate-700 px-0.5 py-0.35 text-[0.8rem]">
|
||||
<label className="flex items-center gap-0.5 text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!showOnLoad}
|
||||
onChange={handleCheckbox}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span>Don't show again</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{/* <button type="button" onClick={onOpenHelp} className="button-dark px-1 py-0.25">
|
||||
Open full Help
|
||||
</button> */}
|
||||
<button type="button" onClick={onClose} className="button-dark px-1 py-0.25 text-2xl bg-green-600 hover:bg-green-400 hover:border-green-100 border-green-400">
|
||||
Got it! Let me in!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -72,10 +72,10 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-md bg-slate-950/80 px-1.5 text-center">
|
||||
<div className="space-y-0.25">
|
||||
<p className="text-sm font-semibold text-slate-100">
|
||||
{busy ? 'Motion in progress' : 'Motion cooldown active'}
|
||||
{busy ? 'Preparing to move' : 'Moving!'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-300">
|
||||
Controls are disabled while the lift is moving, otherwise it's tiny brain would get confused.
|
||||
Controls are disabled while the lift is moving, otherwise its tiny brain would get confused.
|
||||
</p>
|
||||
{!busy && cooldownActive ? (
|
||||
<p className="text-xs text-slate-400">About {cooldownSeconds}s remaining.</p>
|
||||
|
||||
Reference in New Issue
Block a user