From dc6a4b3c3a68c7f9dd1b80d68135158c21fb52c3 Mon Sep 17 00:00:00 2001 From: legop3 Date: Mon, 17 Nov 2025 21:18:43 -0500 Subject: [PATCH] guh --- server/public/assets/index-BMozRdoG.css | 1 + server/public/assets/index-De1-bHUg.js | 11 + server/public/assets/index-Dk59njim.css | 1 - server/public/assets/index-DwtMH0qf.js | 11 - server/public/index.html | 4 +- webui/src/App.jsx | 21 +- webui/src/components/AdvancedSettings.jsx | 15 +- webui/src/components/CameraServoPanel.jsx | 23 +- webui/src/components/DrivePanel.jsx | 23 +- webui/src/components/MobileControls.jsx | 204 ++++++----- .../components/controls/DriveModeToggle.jsx | 66 ++++ webui/src/context/DriveControlContext.jsx | 19 - webui/src/controls/ControlContext.jsx | 235 ++++++++++++ webui/src/controls/commandPipeline.js | 163 +++++++++ webui/src/controls/constants.js | 65 ++++ webui/src/controls/controlMath.js | 59 +++ webui/src/controls/controlReducer.js | 166 +++++++++ webui/src/controls/index.js | 3 + .../controls/inputs/GamepadInputManager.jsx | 143 ++++++++ .../controls/inputs/KeyboardInputManager.jsx | 202 +++++++++++ webui/src/controls/persistence.js | 36 ++ webui/src/hooks/useDriveControls.js | 340 ------------------ 22 files changed, 1318 insertions(+), 493 deletions(-) create mode 100644 server/public/assets/index-BMozRdoG.css create mode 100644 server/public/assets/index-De1-bHUg.js delete mode 100644 server/public/assets/index-Dk59njim.css delete mode 100644 server/public/assets/index-DwtMH0qf.js create mode 100644 webui/src/components/controls/DriveModeToggle.jsx delete mode 100644 webui/src/context/DriveControlContext.jsx create mode 100644 webui/src/controls/ControlContext.jsx create mode 100644 webui/src/controls/commandPipeline.js create mode 100644 webui/src/controls/constants.js create mode 100644 webui/src/controls/controlMath.js create mode 100644 webui/src/controls/controlReducer.js create mode 100644 webui/src/controls/index.js create mode 100644 webui/src/controls/inputs/GamepadInputManager.jsx create mode 100644 webui/src/controls/inputs/KeyboardInputManager.jsx create mode 100644 webui/src/controls/persistence.js delete mode 100644 webui/src/hooks/useDriveControls.js diff --git a/server/public/assets/index-BMozRdoG.css b/server/public/assets/index-BMozRdoG.css new file mode 100644 index 00000000..53087ba9 --- /dev/null +++ b/server/public/assets/index-BMozRdoG.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.left-1\/2{left:50%}.top-1{top:.25rem}.z-50{z-index:50}.col-span-full{grid-column:1 / -1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.flex{display:flex}.grid{display:grid}.aspect-video{aspect-ratio:16 / 9}.h-2{height:.5rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-60{height:15rem}.h-64{height:16rem}.h-full{height:100%}.min-h-\[8rem\]{min-height:8rem}.min-h-screen{min-height:100vh}.w-1\/4{width:25%}.w-2{width:.5rem}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[minmax\(0\,0\.7fr\)_minmax\(0\,2\.1fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,.7fr) minmax(0,2.1fr) minmax(0,.7fr)}.grid-cols-\[minmax\(0\,1\.8fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.8fr) minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.text-wrap{text-wrap:wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.border{border-width:1px}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}.border-slate-800\/80{border-color:#1e293bcc}.bg-\[\#141821\]{--tw-bg-opacity: 1;background-color:rgb(20 24 33 / var(--tw-bg-opacity))}.bg-\[\#1a1d23\]{--tw-bg-opacity: 1;background-color:rgb(26 29 35 / var(--tw-bg-opacity))}.bg-\[\#1e1e1e\]{--tw-bg-opacity: 1;background-color:rgb(30 30 30 / var(--tw-bg-opacity))}.bg-\[\#242a32\]{--tw-bg-opacity: 1;background-color:rgb(36 42 50 / var(--tw-bg-opacity))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity))}.bg-amber-500\/30{background-color:#f59e0b4d}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity))}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/50{background-color:#00000080}.bg-black\/85{background-color:#000000d9}.bg-blue-600\/40{background-color:#2563eb66}.bg-emerald-300{--tw-bg-opacity: 1;background-color:rgb(110 231 183 / var(--tw-bg-opacity))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity))}.bg-emerald-500\/20{background-color:#10b98133}.bg-emerald-500\/30{background-color:#10b9814d}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity))}.bg-emerald-900{--tw-bg-opacity: 1;background-color:rgb(6 78 59 / var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.bg-lime-300{--tw-bg-opacity: 1;background-color:rgb(190 242 100 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/30{background-color:#ef44444d}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.bg-red-900\/60{background-color:#7f1d1d99}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-slate-900\/70{background-color:#0f172ab3}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity))}.bg-slate-950\/40{background-color:#02061766}.bg-slate-950\/50{background-color:#02061780}.bg-transparent{background-color:transparent}.object-contain{-o-object-fit:contain;object-fit:contain}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[0\.55rem\]{font-size:.55rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.75rem\]{font-size:.75rem}.text-\[0\.7rem\]{font-size:.7rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wide{letter-spacing:.025em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-black\/80{color:#000c}.text-blue-100{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity))}.text-cyan-300{--tw-text-opacity: 1;color:rgb(103 232 249 / var(--tw-text-opacity))}.text-emerald-100{--tw-text-opacity: 1;color:rgb(209 250 229 / var(--tw-text-opacity))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity))}.text-emerald-50\/80{color:#ecfdf5cc}.text-emerald-900{--tw-text-opacity: 1;color:rgb(6 78 59 / var(--tw-text-opacity))}.text-indigo-50{--tw-text-opacity: 1;color:rgb(238 242 255 / var(--tw-text-opacity))}.text-lime-300{--tw-text-opacity: 1;color:rgb(190 242 100 / var(--tw-text-opacity))}.text-lime-400{--tw-text-opacity: 1;color:rgb(163 230 53 / var(--tw-text-opacity))}.text-pink-300{--tw-text-opacity: 1;color:rgb(249 168 212 / var(--tw-text-opacity))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-50{--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-white\/90{color:#ffffffe6}.accent-emerald-400{accent-color:#34d399}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_4px_rgba\(16\,185\,129\,0\.8\)\]{--tw-shadow: 0 0 4px rgba(16,185,129,.8);--tw-shadow-colored: 0 0 4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/60{--tw-shadow-color: rgb(0 0 0 / .6);--tw-shadow: var(--tw-shadow-colored)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#0f172a;background-color:#f8fafc}body{margin:0;min-height:100vh;background-color:#0f172a}.hover\:bg-slate-600:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.hover\:bg-slate-700:hover{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} diff --git a/server/public/assets/index-De1-bHUg.js b/server/public/assets/index-De1-bHUg.js new file mode 100644 index 00000000..44c3021d --- /dev/null +++ b/server/public/assets/index-De1-bHUg.js @@ -0,0 +1,11 @@ +(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const f of document.querySelectorAll('link[rel="modulepreload"]'))c(f);new MutationObserver(f=>{for(const d of f)if(d.type==="childList")for(const y of d.addedNodes)y.tagName==="LINK"&&y.rel==="modulepreload"&&c(y)}).observe(document,{childList:!0,subtree:!0});function s(f){const d={};return f.integrity&&(d.integrity=f.integrity),f.referrerPolicy&&(d.referrerPolicy=f.referrerPolicy),f.crossOrigin==="use-credentials"?d.credentials="include":f.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function c(f){if(f.ep)return;f.ep=!0;const d=s(f);fetch(f.href,d)}})();var cc={exports:{}},Xa={};var sm;function F0(){if(sm)return Xa;sm=1;var i=Symbol.for("react.transitional.element"),a=Symbol.for("react.fragment");function s(c,f,d){var y=null;if(d!==void 0&&(y=""+d),f.key!==void 0&&(y=""+f.key),"key"in f){d={};for(var g in f)g!=="key"&&(d[g]=f[g])}else d=f;return f=d.ref,{$$typeof:i,type:c,key:y,ref:f!==void 0?f:null,props:d}}return Xa.Fragment=a,Xa.jsx=s,Xa.jsxs=s,Xa}var rm;function W0(){return rm||(rm=1,cc.exports=F0()),cc.exports}var m=W0(),oc={exports:{}},P={};var cm;function I0(){if(cm)return P;cm=1;var i=Symbol.for("react.transitional.element"),a=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),c=Symbol.for("react.strict_mode"),f=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),y=Symbol.for("react.context"),g=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),v=Symbol.for("react.memo"),E=Symbol.for("react.lazy"),S=Symbol.for("react.activity"),_=Symbol.iterator;function C(T){return T===null||typeof T!="object"?null:(T=_&&T[_]||T["@@iterator"],typeof T=="function"?T:null)}var D={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Y=Object.assign,k={};function V(T,q,X){this.props=T,this.context=q,this.refs=k,this.updater=X||D}V.prototype.isReactComponent={},V.prototype.setState=function(T,q){if(typeof T!="object"&&typeof T!="function"&&T!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,T,q,"setState")},V.prototype.forceUpdate=function(T){this.updater.enqueueForceUpdate(this,T,"forceUpdate")};function U(){}U.prototype=V.prototype;function H(T,q,X){this.props=T,this.context=q,this.refs=k,this.updater=X||D}var J=H.prototype=new U;J.constructor=H,Y(J,V.prototype),J.isPureReactComponent=!0;var oe=Array.isArray;function Ae(){}var F={H:null,A:null,T:null,S:null},Te=Object.prototype.hasOwnProperty;function Se(T,q,X){var Z=X.ref;return{$$typeof:i,type:T,key:q,ref:Z!==void 0?Z:null,props:X}}function Ue(T,q){return Se(T.type,q,T.props)}function Ve(T){return typeof T=="object"&&T!==null&&T.$$typeof===i}function Fe(T){var q={"=":"=0",":":"=2"};return"$"+T.replace(/[=:]/g,function(X){return q[X]})}var kn=/\/+/g;function Gt(T,q){return typeof T=="object"&&T!==null&&T.key!=null?Fe(""+T.key):q.toString(36)}function Ot(T){switch(T.status){case"fulfilled":return T.value;case"rejected":throw T.reason;default:switch(typeof T.status=="string"?T.then(Ae,Ae):(T.status="pending",T.then(function(q){T.status==="pending"&&(T.status="fulfilled",T.value=q)},function(q){T.status==="pending"&&(T.status="rejected",T.reason=q)})),T.status){case"fulfilled":return T.value;case"rejected":throw T.reason}}throw T}function j(T,q,X,Z,ee){var le=typeof T;(le==="undefined"||le==="boolean")&&(T=null);var me=!1;if(T===null)me=!0;else switch(le){case"bigint":case"string":case"number":me=!0;break;case"object":switch(T.$$typeof){case i:case a:me=!0;break;case E:return me=T._init,j(me(T._payload),q,X,Z,ee)}}if(me)return ee=ee(T),me=Z===""?"."+Gt(T,0):Z,oe(ee)?(X="",me!=null&&(X=me.replace(kn,"$&/")+"/"),j(ee,q,X,"",function(Wl){return Wl})):ee!=null&&(Ve(ee)&&(ee=Ue(ee,X+(ee.key==null||T&&T.key===ee.key?"":(""+ee.key).replace(kn,"$&/")+"/")+me)),q.push(ee)),1;me=0;var Ie=Z===""?".":Z+":";if(oe(T))for(var De=0;De>>1,_e=j[ve];if(0>>1;vef(X,I))Z<_e&&0>f(ee,X)?(j[ve]=ee,j[Z]=I,ve=Z):(j[ve]=X,j[q]=I,ve=q);else if(Z<_e&&0>f(ee,I))j[ve]=ee,j[Z]=I,ve=Z;else break e}}return G}function f(j,G){var I=j.sortIndex-G.sortIndex;return I!==0?I:j.id-G.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;i.unstable_now=function(){return d.now()}}else{var y=Date,g=y.now();i.unstable_now=function(){return y.now()-g}}var p=[],v=[],E=1,S=null,_=3,C=!1,D=!1,Y=!1,k=!1,V=typeof setTimeout=="function"?setTimeout:null,U=typeof clearTimeout=="function"?clearTimeout:null,H=typeof setImmediate<"u"?setImmediate:null;function J(j){for(var G=s(v);G!==null;){if(G.callback===null)c(v);else if(G.startTime<=j)c(v),G.sortIndex=G.expirationTime,a(p,G);else break;G=s(v)}}function oe(j){if(Y=!1,J(j),!D)if(s(p)!==null)D=!0,Ae||(Ae=!0,Fe());else{var G=s(v);G!==null&&Ot(oe,G.startTime-j)}}var Ae=!1,F=-1,Te=5,Se=-1;function Ue(){return k?!0:!(i.unstable_now()-Sej&&Ue());){var ve=S.callback;if(typeof ve=="function"){S.callback=null,_=S.priorityLevel;var _e=ve(S.expirationTime<=j);if(j=i.unstable_now(),typeof _e=="function"){S.callback=_e,J(j),G=!0;break t}S===s(p)&&c(p),J(j)}else c(p);S=s(p)}if(S!==null)G=!0;else{var T=s(v);T!==null&&Ot(oe,T.startTime-j),G=!1}}break e}finally{S=null,_=I,C=!1}G=void 0}}finally{G?Fe():Ae=!1}}}var Fe;if(typeof H=="function")Fe=function(){H(Ve)};else if(typeof MessageChannel<"u"){var kn=new MessageChannel,Gt=kn.port2;kn.port1.onmessage=Ve,Fe=function(){Gt.postMessage(null)}}else Fe=function(){V(Ve,0)};function Ot(j,G){F=V(function(){j(i.unstable_now())},G)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(j){j.callback=null},i.unstable_forceFrameRate=function(j){0>j||125ve?(j.sortIndex=I,a(v,j),s(p)===null&&j===s(v)&&(Y?(U(F),F=-1):Y=!0,Ot(oe,I-ve))):(j.sortIndex=_e,a(p,j),D||C||(D=!0,Ae||(Ae=!0,Fe()))),j},i.unstable_shouldYield=Ue,i.unstable_wrapCallback=function(j){var G=_;return function(){var I=_;_=G;try{return j.apply(this,arguments)}finally{_=I}}}})(hc)),hc}var dm;function ev(){return dm||(dm=1,dc.exports=P0()),dc.exports}var mc={exports:{}},We={};var hm;function tv(){if(hm)return We;hm=1;var i=Lu();function a(p){var v="https://react.dev/errors/"+p;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(a){console.error(a)}}return i(),mc.exports=tv(),mc.exports}var pm;function lv(){if(pm)return Qa;pm=1;var i=ev(),a=Lu(),s=nv();function c(e){var t="https://react.dev/errors/"+e;if(1_e||(e.current=ve[_e],ve[_e]=null,_e--)}function X(e,t){_e++,ve[_e]=e.current,e.current=t}var Z=T(null),ee=T(null),le=T(null),me=T(null);function Ie(e,t){switch(X(le,t),X(ee,e),X(Z,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Mh(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Mh(t),e=zh(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}q(Z),X(Z,e)}function De(){q(Z),q(ee),q(le)}function Wl(e){e.memoizedState!==null&&X(me,e);var t=Z.current,n=zh(t,e.type);t!==n&&(X(ee,e),X(Z,n))}function li(e){ee.current===e&&(q(Z),q(ee)),me.current===e&&(q(me),ka._currentValue=I)}var Qu,ao;function Yn(e){if(Qu===void 0)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);Qu=t&&t[1]||"",ao=-1)":-1u||b[l]!==O[u]){var w=` +`+b[l].replace(" at new "," at ");return e.displayName&&w.includes("")&&(w=w.replace("",e.displayName)),w}while(1<=l&&0<=u);break}}}finally{Zu=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?Yn(n):""}function Rp(e,t){switch(e.tag){case 26:case 27:case 5:return Yn(e.type);case 16:return Yn("Lazy");case 13:return e.child!==t&&t!==null?Yn("Suspense Fallback"):Yn("Suspense");case 19:return Yn("SuspenseList");case 0:case 15:return Ku(e.type,!1);case 11:return Ku(e.type.render,!1);case 1:return Ku(e.type,!0);case 31:return Yn("Activity");default:return""}}function io(e){try{var t="",n=null;do t+=Rp(e,n),n=e,e=e.return;while(e);return t}catch(l){return` +Error generating stack: `+l.message+` +`+l.stack}}var Ju=Object.prototype.hasOwnProperty,$u=i.unstable_scheduleCallback,Fu=i.unstable_cancelCallback,Np=i.unstable_shouldYield,Op=i.unstable_requestPaint,st=i.unstable_now,Mp=i.unstable_getCurrentPriorityLevel,uo=i.unstable_ImmediatePriority,so=i.unstable_UserBlockingPriority,ai=i.unstable_NormalPriority,zp=i.unstable_LowPriority,ro=i.unstable_IdlePriority,Dp=i.log,wp=i.unstable_setDisableYieldValue,Il=null,rt=null;function hn(e){if(typeof Dp=="function"&&wp(e),rt&&typeof rt.setStrictMode=="function")try{rt.setStrictMode(Il,e)}catch{}}var ct=Math.clz32?Math.clz32:Bp,jp=Math.log,Up=Math.LN2;function Bp(e){return e>>>=0,e===0?32:31-(jp(e)/Up|0)|0}var ii=256,ui=262144,si=4194304;function Vn(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function ri(e,t,n){var l=e.pendingLanes;if(l===0)return 0;var u=0,r=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var h=l&134217727;return h!==0?(l=h&~r,l!==0?u=Vn(l):(o&=h,o!==0?u=Vn(o):n||(n=h&~e,n!==0&&(u=Vn(n))))):(h=l&~r,h!==0?u=Vn(h):o!==0?u=Vn(o):n||(n=l&~e,n!==0&&(u=Vn(n)))),u===0?0:t!==0&&t!==u&&(t&r)===0&&(r=u&-u,n=t&-t,r>=n||r===32&&(n&4194048)!==0)?t:u}function Pl(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Lp(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function co(){var e=si;return si<<=1,(si&62914560)===0&&(si=4194304),e}function Wu(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function ea(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Hp(e,t,n,l,u,r){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var h=e.entanglements,b=e.expirationTimes,O=e.hiddenUpdates;for(n=o&~n;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Xp=/[\n"\\]/g;function bt(e){return e.replace(Xp,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function ls(e,t,n,l,u,r,o,h){e.name="",o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?e.type=o:e.removeAttribute("type"),t!=null?o==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+gt(t)):e.value!==""+gt(t)&&(e.value=""+gt(t)):o!=="submit"&&o!=="reset"||e.removeAttribute("value"),t!=null?as(e,o,gt(t)):n!=null?as(e,o,gt(n)):l!=null&&e.removeAttribute("value"),u==null&&r!=null&&(e.defaultChecked=!!r),u!=null&&(e.checked=u&&typeof u!="function"&&typeof u!="symbol"),h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?e.name=""+gt(h):e.removeAttribute("name")}function Eo(e,t,n,l,u,r,o,h){if(r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"&&(e.type=r),t!=null||n!=null){if(!(r!=="submit"&&r!=="reset"||t!=null)){ns(e);return}n=n!=null?""+gt(n):"",t=t!=null?""+gt(t):n,h||t===e.value||(e.value=t),e.defaultValue=t}l=l??u,l=typeof l!="function"&&typeof l!="symbol"&&!!l,e.checked=h?e.checked:!!l,e.defaultChecked=!!l,o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"&&(e.name=o),ns(e)}function as(e,t,n){t==="number"&&fi(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function pl(e,t,n,l){if(e=e.options,t){t={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),cs=!1;if(Zt)try{var aa={};Object.defineProperty(aa,"passive",{get:function(){cs=!0}}),window.addEventListener("test",aa,aa),window.removeEventListener("test",aa,aa)}catch{cs=!1}var pn=null,os=null,hi=null;function Mo(){if(hi)return hi;var e,t=os,n=t.length,l,u="value"in pn?pn.value:pn.textContent,r=u.length;for(e=0;e=sa),Bo=" ",Lo=!1;function Ho(e,t){switch(e){case"keyup":return gy.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function qo(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var bl=!1;function xy(e,t){switch(e){case"compositionend":return qo(t);case"keypress":return t.which!==32?null:(Lo=!0,Bo);case"textInput":return e=t.data,e===Bo&&Lo?null:e;default:return null}}function Sy(e,t){if(bl)return e==="compositionend"||!ps&&Ho(e,t)?(e=Mo(),hi=os=pn=null,bl=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=l}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Ko(n)}}function $o(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?$o(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Fo(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=fi(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=fi(e.document)}return t}function gs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Oy=Zt&&"documentMode"in document&&11>=document.documentMode,xl=null,bs=null,fa=null,xs=!1;function Wo(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;xs||xl==null||xl!==fi(l)||(l=xl,"selectionStart"in l&&gs(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),fa&&oa(fa,l)||(fa=l,l=uu(bs,"onSelect"),0>=o,u-=o,Bt=1<<32-ct(t)+u|n<ne?(se=K,K=null):se=K.sibling;var de=M(R,K,N[ne],B);if(de===null){K===null&&(K=se);break}e&&K&&de.alternate===null&&t(R,K),A=r(de,A,ne),fe===null?$=de:fe.sibling=de,fe=de,K=se}if(ne===N.length)return n(R,K),re&&Jt(R,ne),$;if(K===null){for(;nene?(se=K,K=null):se=K.sibling;var Ln=M(R,K,de.value,B);if(Ln===null){K===null&&(K=se);break}e&&K&&Ln.alternate===null&&t(R,K),A=r(Ln,A,ne),fe===null?$=Ln:fe.sibling=Ln,fe=Ln,K=se}if(de.done)return n(R,K),re&&Jt(R,ne),$;if(K===null){for(;!de.done;ne++,de=N.next())de=L(R,de.value,B),de!==null&&(A=r(de,A,ne),fe===null?$=de:fe.sibling=de,fe=de);return re&&Jt(R,ne),$}for(K=l(K);!de.done;ne++,de=N.next())de=z(K,R,ne,de.value,B),de!==null&&(e&&de.alternate!==null&&K.delete(de.key===null?ne:de.key),A=r(de,A,ne),fe===null?$=de:fe.sibling=de,fe=de);return e&&K.forEach(function($0){return t(R,$0)}),re&&Jt(R,ne),$}function xe(R,A,N,B){if(typeof N=="object"&&N!==null&&N.type===Y&&N.key===null&&(N=N.props.children),typeof N=="object"&&N!==null){switch(N.$$typeof){case C:e:{for(var $=N.key;A!==null;){if(A.key===$){if($=N.type,$===Y){if(A.tag===7){n(R,A.sibling),B=u(A,N.props.children),B.return=R,R=B;break e}}else if(A.elementType===$||typeof $=="object"&&$!==null&&$.$$typeof===Te&&Pn($)===A.type){n(R,A.sibling),B=u(A,N.props),va(B,N),B.return=R,R=B;break e}n(R,A);break}else t(R,A);A=A.sibling}N.type===Y?(B=Jn(N.props.children,R.mode,B,N.key),B.return=R,R=B):(B=Ei(N.type,N.key,N.props,null,R.mode,B),va(B,N),B.return=R,R=B)}return o(R);case D:e:{for($=N.key;A!==null;){if(A.key===$)if(A.tag===4&&A.stateNode.containerInfo===N.containerInfo&&A.stateNode.implementation===N.implementation){n(R,A.sibling),B=u(A,N.children||[]),B.return=R,R=B;break e}else{n(R,A);break}else t(R,A);A=A.sibling}B=Rs(N,R.mode,B),B.return=R,R=B}return o(R);case Te:return N=Pn(N),xe(R,A,N,B)}if(Ot(N))return Q(R,A,N,B);if(Fe(N)){if($=Fe(N),typeof $!="function")throw Error(c(150));return N=$.call(N),W(R,A,N,B)}if(typeof N.then=="function")return xe(R,A,Mi(N),B);if(N.$$typeof===H)return xe(R,A,Ci(R,N),B);zi(R,N)}return typeof N=="string"&&N!==""||typeof N=="number"||typeof N=="bigint"?(N=""+N,A!==null&&A.tag===6?(n(R,A.sibling),B=u(A,N),B.return=R,R=B):(n(R,A),B=Cs(N,R.mode,B),B.return=R,R=B),o(R)):n(R,A)}return function(R,A,N,B){try{ya=0;var $=xe(R,A,N,B);return zl=null,$}catch(K){if(K===Ml||K===Ni)throw K;var fe=ft(29,K,null,R.mode);return fe.lanes=B,fe.return=R,fe}finally{}}}var tl=Sf(!0),_f=Sf(!1),xn=!1;function qs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ks(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Sn(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function _n(e,t,n){var l=e.updateQueue;if(l===null)return null;if(l=l.shared,(he&2)!==0){var u=l.pending;return u===null?t.next=t:(t.next=u.next,u.next=t),l.pending=t,t=_i(e),af(e,null,n),t}return Si(e,l,t,n),_i(e)}function ga(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194048)!==0)){var l=t.lanes;l&=e.pendingLanes,n|=l,t.lanes=n,fo(e,n)}}function Ys(e,t){var n=e.updateQueue,l=e.alternate;if(l!==null&&(l=l.updateQueue,n===l)){var u=null,r=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};r===null?u=r=o:r=r.next=o,n=n.next}while(n!==null);r===null?u=r=t:r=r.next=t}else u=r=t;n={baseState:l.baseState,firstBaseUpdate:u,lastBaseUpdate:r,shared:l.shared,callbacks:l.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Vs=!1;function ba(){if(Vs){var e=Ol;if(e!==null)throw e}}function xa(e,t,n,l){Vs=!1;var u=e.updateQueue;xn=!1;var r=u.firstBaseUpdate,o=u.lastBaseUpdate,h=u.shared.pending;if(h!==null){u.shared.pending=null;var b=h,O=b.next;b.next=null,o===null?r=O:o.next=O,o=b;var w=e.alternate;w!==null&&(w=w.updateQueue,h=w.lastBaseUpdate,h!==o&&(h===null?w.firstBaseUpdate=O:h.next=O,w.lastBaseUpdate=b))}if(r!==null){var L=u.baseState;o=0,w=O=b=null,h=r;do{var M=h.lane&-536870913,z=M!==h.lane;if(z?(ue&M)===M:(l&M)===M){M!==0&&M===Nl&&(Vs=!0),w!==null&&(w=w.next={lane:0,tag:h.tag,payload:h.payload,callback:null,next:null});e:{var Q=e,W=h;M=t;var xe=n;switch(W.tag){case 1:if(Q=W.payload,typeof Q=="function"){L=Q.call(xe,L,M);break e}L=Q;break e;case 3:Q.flags=Q.flags&-65537|128;case 0:if(Q=W.payload,M=typeof Q=="function"?Q.call(xe,L,M):Q,M==null)break e;L=S({},L,M);break e;case 2:xn=!0}}M=h.callback,M!==null&&(e.flags|=64,z&&(e.flags|=8192),z=u.callbacks,z===null?u.callbacks=[M]:z.push(M))}else z={lane:M,tag:h.tag,payload:h.payload,callback:h.callback,next:null},w===null?(O=w=z,b=L):w=w.next=z,o|=M;if(h=h.next,h===null){if(h=u.shared.pending,h===null)break;z=h,h=z.next,z.next=null,u.lastBaseUpdate=z,u.shared.pending=null}}while(!0);w===null&&(b=L),u.baseState=b,u.firstBaseUpdate=O,u.lastBaseUpdate=w,r===null&&(u.shared.lanes=0),Rn|=o,e.lanes=o,e.memoizedState=L}}function Ef(e,t){if(typeof e!="function")throw Error(c(191,e));e.call(t)}function Af(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;er?r:8;var o=j.T,h={};j.T=h,sr(e,!1,t,n);try{var b=u(),O=j.S;if(O!==null&&O(h,b),b!==null&&typeof b=="object"&&typeof b.then=="function"){var w=Hy(b,l);Ea(e,t,w,yt(e))}else Ea(e,t,l,yt(e))}catch(L){Ea(e,t,{then:function(){},status:"rejected",reason:L},yt())}finally{G.p=r,o!==null&&h.types!==null&&(o.types=h.types),j.T=o}}function Xy(){}function ir(e,t,n,l){if(e.tag!==5)throw Error(c(476));var u=nd(e).queue;td(e,u,t,I,n===null?Xy:function(){return ld(e),n(l)})}function nd(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:I,baseState:I,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:It,lastRenderedState:I},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:It,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ld(e){var t=nd(e);t.next===null&&(t=e.alternate.memoizedState),Ea(e,t.next.queue,{},yt())}function ur(){return Ke(ka)}function ad(){return je().memoizedState}function id(){return je().memoizedState}function Qy(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=yt();e=Sn(n);var l=_n(t,e,n);l!==null&&(ut(l,t,n),ga(l,t,n)),t={cache:Us()},e.payload=t;return}t=t.return}}function Zy(e,t,n){var l=yt();n={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Yi(e)?sd(t,n):(n=As(e,t,n,l),n!==null&&(ut(n,e,l),rd(n,t,l)))}function ud(e,t,n){var l=yt();Ea(e,t,n,l)}function Ea(e,t,n,l){var u={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Yi(e))sd(t,u);else{var r=e.alternate;if(e.lanes===0&&(r===null||r.lanes===0)&&(r=t.lastRenderedReducer,r!==null))try{var o=t.lastRenderedState,h=r(o,n);if(u.hasEagerState=!0,u.eagerState=h,ot(h,o))return Si(e,t,u,0),Ee===null&&xi(),!1}catch{}finally{}if(n=As(e,t,u,l),n!==null)return ut(n,e,l),rd(n,t,l),!0}return!1}function sr(e,t,n,l){if(l={lane:2,revertLane:qr(),gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Yi(e)){if(t)throw Error(c(479))}else t=As(e,n,l,2),t!==null&&ut(t,e,2)}function Yi(e){var t=e.alternate;return e===te||t!==null&&t===te}function sd(e,t){wl=ji=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rd(e,t,n){if((n&4194048)!==0){var l=t.lanes;l&=e.pendingLanes,n|=l,t.lanes=n,fo(e,n)}}var Aa={readContext:Ke,use:Li,useCallback:Me,useContext:Me,useEffect:Me,useImperativeHandle:Me,useLayoutEffect:Me,useInsertionEffect:Me,useMemo:Me,useReducer:Me,useRef:Me,useState:Me,useDebugValue:Me,useDeferredValue:Me,useTransition:Me,useSyncExternalStore:Me,useId:Me,useHostTransitionStatus:Me,useFormState:Me,useActionState:Me,useOptimistic:Me,useMemoCache:Me,useCacheRefresh:Me};Aa.useEffectEvent=Me;var cd={readContext:Ke,use:Li,useCallback:function(e,t){return Pe().memoizedState=[e,t===void 0?null:t],e},useContext:Ke,useEffect:Zf,useImperativeHandle:function(e,t,n){n=n!=null?n.concat([e]):null,qi(4194308,4,Ff.bind(null,t,e),n)},useLayoutEffect:function(e,t){return qi(4194308,4,e,t)},useInsertionEffect:function(e,t){qi(4,2,e,t)},useMemo:function(e,t){var n=Pe();t=t===void 0?null:t;var l=e();if(nl){hn(!0);try{e()}finally{hn(!1)}}return n.memoizedState=[l,t],l},useReducer:function(e,t,n){var l=Pe();if(n!==void 0){var u=n(t);if(nl){hn(!0);try{n(t)}finally{hn(!1)}}}else u=t;return l.memoizedState=l.baseState=u,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:u},l.queue=e,e=e.dispatch=Zy.bind(null,te,e),[l.memoizedState,e]},useRef:function(e){var t=Pe();return e={current:e},t.memoizedState=e},useState:function(e){e=er(e);var t=e.queue,n=ud.bind(null,te,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:lr,useDeferredValue:function(e,t){var n=Pe();return ar(n,e,t)},useTransition:function(){var e=er(!1);return e=td.bind(null,te,e.queue,!0,!1),Pe().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var l=te,u=Pe();if(re){if(n===void 0)throw Error(c(407));n=n()}else{if(n=t(),Ee===null)throw Error(c(349));(ue&127)!==0||Mf(l,t,n)}u.memoizedState=n;var r={value:n,getSnapshot:t};return u.queue=r,Zf(Df.bind(null,l,r,e),[e]),l.flags|=2048,Ul(9,{destroy:void 0},zf.bind(null,l,r,n,t),null),n},useId:function(){var e=Pe(),t=Ee.identifierPrefix;if(re){var n=Lt,l=Bt;n=(l&~(1<<32-ct(l)-1)).toString(32)+n,t="_"+t+"R_"+n,n=Ui++,0<\/script>",r=r.removeChild(r.firstChild);break;case"select":r=typeof l.is=="string"?o.createElement("select",{is:l.is}):o.createElement("select"),l.multiple?r.multiple=!0:l.size&&(r.size=l.size);break;default:r=typeof l.is=="string"?o.createElement(u,{is:l.is}):o.createElement(u)}}r[Qe]=t,r[et]=l;e:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)r.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break e;for(;o.sibling===null;){if(o.return===null||o.return===t)break e;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=r;e:switch($e(r,u,l),u){case"button":case"input":case"select":case"textarea":l=!!l.autoFocus;break e;case"img":l=!0;break e;default:l=!1}l&&en(t)}}return Re(t),Sr(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==l&&en(t);else{if(typeof l!="string"&&t.stateNode===null)throw Error(c(166));if(e=le.current,Cl(t)){if(e=t.stateNode,n=t.memoizedProps,l=null,u=Ze,u!==null)switch(u.tag){case 27:case 5:l=u.memoizedProps}e[Qe]=t,e=!!(e.nodeValue===n||l!==null&&l.suppressHydrationWarning===!0||Nh(e.nodeValue,n)),e||gn(t,!0)}else e=su(e).createTextNode(l),e[Qe]=t,t.stateNode=e}return Re(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(l=Cl(t),n!==null){if(e===null){if(!l)throw Error(c(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(c(557));e[Qe]=t}else $n(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Re(t),e=!1}else n=zs(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(ht(t),t):(ht(t),null);if((t.flags&128)!==0)throw Error(c(558))}return Re(t),null;case 13:if(l=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(u=Cl(t),l!==null&&l.dehydrated!==null){if(e===null){if(!u)throw Error(c(318));if(u=t.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(c(317));u[Qe]=t}else $n(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Re(t),u=!1}else u=zs(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=u),u=!0;if(!u)return t.flags&256?(ht(t),t):(ht(t),null)}return ht(t),(t.flags&128)!==0?(t.lanes=n,t):(n=l!==null,e=e!==null&&e.memoizedState!==null,n&&(l=t.child,u=null,l.alternate!==null&&l.alternate.memoizedState!==null&&l.alternate.memoizedState.cachePool!==null&&(u=l.alternate.memoizedState.cachePool.pool),r=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(r=l.memoizedState.cachePool.pool),r!==u&&(l.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Zi(t,t.updateQueue),Re(t),null);case 4:return De(),e===null&&Gr(t.stateNode.containerInfo),Re(t),null;case 10:return Ft(t.type),Re(t),null;case 19:if(q(we),l=t.memoizedState,l===null)return Re(t),null;if(u=(t.flags&128)!==0,r=l.rendering,r===null)if(u)Ca(l,!1);else{if(ze!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(r=wi(e),r!==null){for(t.flags|=128,Ca(l,!1),e=r.updateQueue,t.updateQueue=e,Zi(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)uf(n,e),n=n.sibling;return X(we,we.current&1|2),re&&Jt(t,l.treeForkCount),t.child}e=e.sibling}l.tail!==null&&st()>Wi&&(t.flags|=128,u=!0,Ca(l,!1),t.lanes=4194304)}else{if(!u)if(e=wi(r),e!==null){if(t.flags|=128,u=!0,e=e.updateQueue,t.updateQueue=e,Zi(t,e),Ca(l,!0),l.tail===null&&l.tailMode==="hidden"&&!r.alternate&&!re)return Re(t),null}else 2*st()-l.renderingStartTime>Wi&&n!==536870912&&(t.flags|=128,u=!0,Ca(l,!1),t.lanes=4194304);l.isBackwards?(r.sibling=t.child,t.child=r):(e=l.last,e!==null?e.sibling=r:t.child=r,l.last=r)}return l.tail!==null?(e=l.tail,l.rendering=e,l.tail=e.sibling,l.renderingStartTime=st(),e.sibling=null,n=we.current,X(we,u?n&1|2:n&1),re&&Jt(t,l.treeForkCount),e):(Re(t),null);case 22:case 23:return ht(t),Xs(),l=t.memoizedState!==null,e!==null?e.memoizedState!==null!==l&&(t.flags|=8192):l&&(t.flags|=8192),l?(n&536870912)!==0&&(t.flags&128)===0&&(Re(t),t.subtreeFlags&6&&(t.flags|=8192)):Re(t),n=t.updateQueue,n!==null&&Zi(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),l=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),l!==n&&(t.flags|=2048),e!==null&&q(In),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Ft(Be),Re(t),null;case 25:return null;case 30:return null}throw Error(c(156,t.tag))}function Wy(e,t){switch(Os(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ft(Be),De(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return li(t),null;case 31:if(t.memoizedState!==null){if(ht(t),t.alternate===null)throw Error(c(340));$n()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(ht(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(c(340));$n()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return q(we),null;case 4:return De(),null;case 10:return Ft(t.type),null;case 22:case 23:return ht(t),Xs(),e!==null&&q(In),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ft(Be),null;case 25:return null;default:return null}}function wd(e,t){switch(Os(t),t.tag){case 3:Ft(Be),De();break;case 26:case 27:case 5:li(t);break;case 4:De();break;case 31:t.memoizedState!==null&&ht(t);break;case 13:ht(t);break;case 19:q(we);break;case 10:Ft(t.type);break;case 22:case 23:ht(t),Xs(),e!==null&&q(In);break;case 24:Ft(Be)}}function Ra(e,t){try{var n=t.updateQueue,l=n!==null?n.lastEffect:null;if(l!==null){var u=l.next;n=u;do{if((n.tag&e)===e){l=void 0;var r=n.create,o=n.inst;l=r(),o.destroy=l}n=n.next}while(n!==u)}}catch(h){ye(t,t.return,h)}}function Tn(e,t,n){try{var l=t.updateQueue,u=l!==null?l.lastEffect:null;if(u!==null){var r=u.next;l=r;do{if((l.tag&e)===e){var o=l.inst,h=o.destroy;if(h!==void 0){o.destroy=void 0,u=t;var b=n,O=h;try{O()}catch(w){ye(u,b,w)}}}l=l.next}while(l!==r)}}catch(w){ye(t,t.return,w)}}function jd(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Af(t,n)}catch(l){ye(e,e.return,l)}}}function Ud(e,t,n){n.props=ll(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(l){ye(e,t,l)}}function Na(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var l=e.stateNode;break;case 30:l=e.stateNode;break;default:l=e.stateNode}typeof n=="function"?e.refCleanup=n(l):n.current=l}}catch(u){ye(e,t,u)}}function Ht(e,t){var n=e.ref,l=e.refCleanup;if(n!==null)if(typeof l=="function")try{l()}catch(u){ye(e,t,u)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(u){ye(e,t,u)}else n.current=null}function Bd(e){var t=e.type,n=e.memoizedProps,l=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&l.focus();break e;case"img":n.src?l.src=n.src:n.srcSet&&(l.srcset=n.srcSet)}}catch(u){ye(e,e.return,u)}}function _r(e,t,n){try{var l=e.stateNode;b0(l,e.type,n,t),l[et]=t}catch(u){ye(e,e.return,u)}}function Ld(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Dn(e.type)||e.tag===4}function Er(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ld(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Dn(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ar(e,t,n){var l=e.tag;if(l===5||l===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qt));else if(l!==4&&(l===27&&Dn(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Ar(e,t,n),e=e.sibling;e!==null;)Ar(e,t,n),e=e.sibling}function Ki(e,t,n){var l=e.tag;if(l===5||l===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(l===27&&Dn(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Ki(e,t,n),e=e.sibling;e!==null;)Ki(e,t,n),e=e.sibling}function Hd(e){var t=e.stateNode,n=e.memoizedProps;try{for(var l=e.type,u=t.attributes;u.length;)t.removeAttributeNode(u[0]);$e(t,l,n),t[Qe]=e,t[et]=n}catch(r){ye(e,e.return,r)}}var tn=!1,qe=!1,Tr=!1,qd=typeof WeakSet=="function"?WeakSet:Set,Xe=null;function Iy(e,t){if(e=e.containerInfo,Zr=mu,e=Fo(e),gs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var l=n.getSelection&&n.getSelection();if(l&&l.rangeCount!==0){n=l.anchorNode;var u=l.anchorOffset,r=l.focusNode;l=l.focusOffset;try{n.nodeType,r.nodeType}catch{n=null;break e}var o=0,h=-1,b=-1,O=0,w=0,L=e,M=null;t:for(;;){for(var z;L!==n||u!==0&&L.nodeType!==3||(h=o+u),L!==r||l!==0&&L.nodeType!==3||(b=o+l),L.nodeType===3&&(o+=L.nodeValue.length),(z=L.firstChild)!==null;)M=L,L=z;for(;;){if(L===e)break t;if(M===n&&++O===u&&(h=o),M===r&&++w===l&&(b=o),(z=L.nextSibling)!==null)break;L=M,M=L.parentNode}L=z}n=h===-1||b===-1?null:{start:h,end:b}}else n=null}n=n||{start:0,end:0}}else n=null;for(Kr={focusedElem:e,selectionRange:n},mu=!1,Xe=t;Xe!==null;)if(t=Xe,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Xe=e;else for(;Xe!==null;){switch(t=Xe,r=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(n=0;n title"))),$e(r,l,n),r[Qe]=e,Ge(r),l=r;break e;case"link":var o=Qh("link","href",u).get(l+(n.href||""));if(o){for(var h=0;hxe&&(o=xe,xe=W,W=o);var R=Jo(h,W),A=Jo(h,xe);if(R&&A&&(z.rangeCount!==1||z.anchorNode!==R.node||z.anchorOffset!==R.offset||z.focusNode!==A.node||z.focusOffset!==A.offset)){var N=L.createRange();N.setStart(R.node,R.offset),z.removeAllRanges(),W>xe?(z.addRange(N),z.extend(A.node,A.offset)):(N.setEnd(A.node,A.offset),z.addRange(N))}}}}for(L=[],z=h;z=z.parentNode;)z.nodeType===1&&L.push({element:z,left:z.scrollLeft,top:z.scrollTop});for(typeof h.focus=="function"&&h.focus(),h=0;hn?32:n,j.T=null,n=Dr,Dr=null;var r=On,o=sn;if(Ye=0,kl=On=null,sn=0,(he&6)!==0)throw Error(c(331));var h=he;if(he|=4,Fd(r.current),Kd(r,r.current,o,n),he=h,ja(0,!1),rt&&typeof rt.onPostCommitFiberRoot=="function")try{rt.onPostCommitFiberRoot(Il,r)}catch{}return!0}finally{G.p=u,j.T=l,mh(e,t)}}function yh(e,t,n){t=St(n,t),t=fr(e.stateNode,t,2),e=_n(e,t,2),e!==null&&(ea(e,2),qt(e))}function ye(e,t,n){if(e.tag===3)yh(e,e,n);else for(;t!==null;){if(t.tag===3){yh(t,e,n);break}else if(t.tag===1){var l=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(Nn===null||!Nn.has(l))){e=St(n,e),n=vd(2),l=_n(t,n,2),l!==null&&(gd(n,l,t,e),ea(l,2),qt(l));break}}t=t.return}}function Br(e,t,n){var l=e.pingCache;if(l===null){l=e.pingCache=new t0;var u=new Set;l.set(t,u)}else u=l.get(t),u===void 0&&(u=new Set,l.set(t,u));u.has(n)||(Nr=!0,u.add(n),e=u0.bind(null,e,t,n),t.then(e,e))}function u0(e,t,n){var l=e.pingCache;l!==null&&l.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ee===e&&(ue&n)===n&&(ze===4||ze===3&&(ue&62914560)===ue&&300>st()-Fi?(he&2)===0&&Yl(e,0):Or|=n,ql===ue&&(ql=0)),qt(e)}function vh(e,t){t===0&&(t=co()),e=Kn(e,t),e!==null&&(ea(e,t),qt(e))}function s0(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),vh(e,n)}function r0(e,t){var n=0;switch(e.tag){case 31:case 13:var l=e.stateNode,u=e.memoizedState;u!==null&&(n=u.retryLane);break;case 19:l=e.stateNode;break;case 22:l=e.stateNode._retryCache;break;default:throw Error(c(314))}l!==null&&l.delete(t),vh(e,n)}function c0(e,t){return $u(e,t)}var lu=null,Gl=null,Lr=!1,au=!1,Hr=!1,zn=0;function qt(e){e!==Gl&&e.next===null&&(Gl===null?lu=Gl=e:Gl=Gl.next=e),au=!0,Lr||(Lr=!0,f0())}function ja(e,t){if(!Hr&&au){Hr=!0;do for(var n=!1,l=lu;l!==null;){if(e!==0){var u=l.pendingLanes;if(u===0)var r=0;else{var o=l.suspendedLanes,h=l.pingedLanes;r=(1<<31-ct(42|e)+1)-1,r&=u&~(o&~h),r=r&201326741?r&201326741|1:r?r|2:0}r!==0&&(n=!0,Sh(l,r))}else r=ue,r=ri(l,l===Ee?r:0,l.cancelPendingCommit!==null||l.timeoutHandle!==-1),(r&3)===0||Pl(l,r)||(n=!0,Sh(l,r));l=l.next}while(n);Hr=!1}}function o0(){gh()}function gh(){au=Lr=!1;var e=0;zn!==0&&S0()&&(e=zn);for(var t=st(),n=null,l=lu;l!==null;){var u=l.next,r=bh(l,t);r===0?(l.next=null,n===null?lu=u:n.next=u,u===null&&(Gl=n)):(n=l,(e!==0||(r&3)!==0)&&(au=!0)),l=u}Ye!==0&&Ye!==5||ja(e),zn!==0&&(zn=0)}function bh(e,t){for(var n=e.suspendedLanes,l=e.pingedLanes,u=e.expirationTimes,r=e.pendingLanes&-62914561;0h)break;var w=b.transferSize,L=b.initiatorType;w&&Oh(L)&&(b=b.responseEnd,o+=w*(b"u"?null:document;function Yh(e,t,n){var l=Xl;if(l&&typeof t=="string"&&t){var u=bt(t);u='link[rel="'+e+'"][href="'+u+'"]',typeof n=="string"&&(u+='[crossorigin="'+n+'"]'),kh.has(u)||(kh.add(u),e={rel:e,crossOrigin:n,href:t},l.querySelector(u)===null&&(t=l.createElement("link"),$e(t,"link",e),Ge(t),l.head.appendChild(t)))}}function M0(e){rn.D(e),Yh("dns-prefetch",e,null)}function z0(e,t){rn.C(e,t),Yh("preconnect",e,t)}function D0(e,t,n){rn.L(e,t,n);var l=Xl;if(l&&e&&t){var u='link[rel="preload"][as="'+bt(t)+'"]';t==="image"&&n&&n.imageSrcSet?(u+='[imagesrcset="'+bt(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(u+='[imagesizes="'+bt(n.imageSizes)+'"]')):u+='[href="'+bt(e)+'"]';var r=u;switch(t){case"style":r=Ql(e);break;case"script":r=Zl(e)}Rt.has(r)||(e=S({rel:"preload",href:t==="image"&&n&&n.imageSrcSet?void 0:e,as:t},n),Rt.set(r,e),l.querySelector(u)!==null||t==="style"&&l.querySelector(Ha(r))||t==="script"&&l.querySelector(qa(r))||(t=l.createElement("link"),$e(t,"link",e),Ge(t),l.head.appendChild(t)))}}function w0(e,t){rn.m(e,t);var n=Xl;if(n&&e){var l=t&&typeof t.as=="string"?t.as:"script",u='link[rel="modulepreload"][as="'+bt(l)+'"][href="'+bt(e)+'"]',r=u;switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":r=Zl(e)}if(!Rt.has(r)&&(e=S({rel:"modulepreload",href:e},t),Rt.set(r,e),n.querySelector(u)===null)){switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(qa(r)))return}l=n.createElement("link"),$e(l,"link",e),Ge(l),n.head.appendChild(l)}}}function j0(e,t,n){rn.S(e,t,n);var l=Xl;if(l&&e){var u=hl(l).hoistableStyles,r=Ql(e);t=t||"default";var o=u.get(r);if(!o){var h={loading:0,preload:null};if(o=l.querySelector(Ha(r)))h.loading=5;else{e=S({rel:"stylesheet",href:e,"data-precedence":t},n),(n=Rt.get(r))&&ec(e,n);var b=o=l.createElement("link");Ge(b),$e(b,"link",e),b._p=new Promise(function(O,w){b.onload=O,b.onerror=w}),b.addEventListener("load",function(){h.loading|=1}),b.addEventListener("error",function(){h.loading|=2}),h.loading|=4,cu(o,t,l)}o={type:"stylesheet",instance:o,count:1,state:h},u.set(r,o)}}}function U0(e,t){rn.X(e,t);var n=Xl;if(n&&e){var l=hl(n).hoistableScripts,u=Zl(e),r=l.get(u);r||(r=n.querySelector(qa(u)),r||(e=S({src:e,async:!0},t),(t=Rt.get(u))&&tc(e,t),r=n.createElement("script"),Ge(r),$e(r,"link",e),n.head.appendChild(r)),r={type:"script",instance:r,count:1,state:null},l.set(u,r))}}function B0(e,t){rn.M(e,t);var n=Xl;if(n&&e){var l=hl(n).hoistableScripts,u=Zl(e),r=l.get(u);r||(r=n.querySelector(qa(u)),r||(e=S({src:e,async:!0,type:"module"},t),(t=Rt.get(u))&&tc(e,t),r=n.createElement("script"),Ge(r),$e(r,"link",e),n.head.appendChild(r)),r={type:"script",instance:r,count:1,state:null},l.set(u,r))}}function Vh(e,t,n,l){var u=(u=le.current)?ru(u):null;if(!u)throw Error(c(446));switch(e){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(t=Ql(n.href),n=hl(u).hoistableStyles,l=n.get(t),l||(l={type:"style",instance:null,count:0,state:null},n.set(t,l)),l):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){e=Ql(n.href);var r=hl(u).hoistableStyles,o=r.get(e);if(o||(u=u.ownerDocument||u,o={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},r.set(e,o),(r=u.querySelector(Ha(e)))&&!r._p&&(o.instance=r,o.state.loading=5),Rt.has(e)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Rt.set(e,n),r||L0(u,e,n,o.state))),t&&l===null)throw Error(c(528,""));return o}if(t&&l!==null)throw Error(c(529,""));return null;case"script":return t=n.async,n=n.src,typeof n=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Zl(n),n=hl(u).hoistableScripts,l=n.get(t),l||(l={type:"script",instance:null,count:0,state:null},n.set(t,l)),l):{type:"void",instance:null,count:0,state:null};default:throw Error(c(444,e))}}function Ql(e){return'href="'+bt(e)+'"'}function Ha(e){return'link[rel="stylesheet"]['+e+"]"}function Gh(e){return S({},e,{"data-precedence":e.precedence,precedence:null})}function L0(e,t,n,l){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?l.loading=1:(t=e.createElement("link"),l.preload=t,t.addEventListener("load",function(){return l.loading|=1}),t.addEventListener("error",function(){return l.loading|=2}),$e(t,"link",n),Ge(t),e.head.appendChild(t))}function Zl(e){return'[src="'+bt(e)+'"]'}function qa(e){return"script[async]"+e}function Xh(e,t,n){if(t.count++,t.instance===null)switch(t.type){case"style":var l=e.querySelector('style[data-href~="'+bt(n.href)+'"]');if(l)return t.instance=l,Ge(l),l;var u=S({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return l=(e.ownerDocument||e).createElement("style"),Ge(l),$e(l,"style",u),cu(l,n.precedence,e),t.instance=l;case"stylesheet":u=Ql(n.href);var r=e.querySelector(Ha(u));if(r)return t.state.loading|=4,t.instance=r,Ge(r),r;l=Gh(n),(u=Rt.get(u))&&ec(l,u),r=(e.ownerDocument||e).createElement("link"),Ge(r);var o=r;return o._p=new Promise(function(h,b){o.onload=h,o.onerror=b}),$e(r,"link",l),t.state.loading|=4,cu(r,n.precedence,e),t.instance=r;case"script":return r=Zl(n.src),(u=e.querySelector(qa(r)))?(t.instance=u,Ge(u),u):(l=n,(u=Rt.get(r))&&(l=S({},n),tc(l,u)),e=e.ownerDocument||e,u=e.createElement("script"),Ge(u),$e(u,"link",l),e.head.appendChild(u),t.instance=u);case"void":return null;default:throw Error(c(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(l=t.instance,t.state.loading|=4,cu(l,n.precedence,e));return t.instance}function cu(e,t,n){for(var l=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=l.length?l[l.length-1]:null,r=u,o=0;o title"):null)}function H0(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Kh(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function q0(e,t,n,l){if(n.type==="stylesheet"&&(typeof l.media!="string"||matchMedia(l.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var u=Ql(l.href),r=t.querySelector(Ha(u));if(r){t=r._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=fu.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=r,Ge(r);return}r=t.ownerDocument||t,l=Gh(l),(u=Rt.get(u))&&ec(l,u),r=r.createElement("link"),Ge(r);var o=r;o._p=new Promise(function(h,b){o.onload=h,o.onerror=b}),$e(r,"link",l),n.instance=r}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&(n.state.loading&3)===0&&(e.count++,n=fu.bind(e),t.addEventListener("load",n),t.addEventListener("error",n))}}var nc=0;function k0(e,t){return e.stylesheets&&e.count===0&&hu(e,e.stylesheets),0nc?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(l),clearTimeout(u)}}:null}function fu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)hu(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var du=null;function hu(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,du=new Map,t.forEach(Y0,e),du=null,fu.call(e))}function Y0(e,t){if(!(t.state.loading&4)){var n=du.get(e);if(n)var l=n.get(null);else{n=new Map,du.set(e,n);for(var u=e.querySelectorAll("link[data-precedence],style[data-precedence]"),r=0;r"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(a){console.error(a)}}return i(),fc.exports=lv(),fc.exports}var iv=av();var vm="popstate";function uv(i={}){function a(c,f){let{pathname:d,search:y,hash:g}=c.location;return Nc("",{pathname:d,search:y,hash:g},f.state&&f.state.usr||null,f.state&&f.state.key||"default")}function s(c,f){return typeof f=="string"?f:Ia(f)}return rv(a,s,null,i)}function Oe(i,a){if(i===!1||i===null||typeof i>"u")throw new Error(a)}function jt(i,a){if(!i){typeof console<"u"&&console.warn(a);try{throw new Error(a)}catch{}}}function sv(){return Math.random().toString(36).substring(2,10)}function gm(i,a){return{usr:i.state,key:i.key,idx:a}}function Nc(i,a,s=null,c){return{pathname:typeof i=="string"?i:i.pathname,search:"",hash:"",...typeof a=="string"?Jl(a):a,state:s,key:a&&a.key||c||sv()}}function Ia({pathname:i="/",search:a="",hash:s=""}){return a&&a!=="?"&&(i+=a.charAt(0)==="?"?a:"?"+a),s&&s!=="#"&&(i+=s.charAt(0)==="#"?s:"#"+s),i}function Jl(i){let a={};if(i){let s=i.indexOf("#");s>=0&&(a.hash=i.substring(s),i=i.substring(0,s));let c=i.indexOf("?");c>=0&&(a.search=i.substring(c),i=i.substring(0,c)),i&&(a.pathname=i)}return a}function rv(i,a,s,c={}){let{window:f=document.defaultView,v5Compat:d=!1}=c,y=f.history,g="POP",p=null,v=E();v==null&&(v=0,y.replaceState({...y.state,idx:v},""));function E(){return(y.state||{idx:null}).idx}function S(){g="POP";let k=E(),V=k==null?null:k-v;v=k,p&&p({action:g,location:Y.location,delta:V})}function _(k,V){g="PUSH";let U=Nc(Y.location,k,V);v=E()+1;let H=gm(U,v),J=Y.createHref(U);try{y.pushState(H,"",J)}catch(oe){if(oe instanceof DOMException&&oe.name==="DataCloneError")throw oe;f.location.assign(J)}d&&p&&p({action:g,location:Y.location,delta:1})}function C(k,V){g="REPLACE";let U=Nc(Y.location,k,V);v=E();let H=gm(U,v),J=Y.createHref(U);y.replaceState(H,"",J),d&&p&&p({action:g,location:Y.location,delta:0})}function D(k){return cv(k)}let Y={get action(){return g},get location(){return i(f,y)},listen(k){if(p)throw new Error("A history only accepts one active listener");return f.addEventListener(vm,S),p=k,()=>{f.removeEventListener(vm,S),p=null}},createHref(k){return a(f,k)},createURL:D,encodeLocation(k){let V=D(k);return{pathname:V.pathname,search:V.search,hash:V.hash}},push:_,replace:C,go(k){return y.go(k)}};return Y}function cv(i,a=!1){let s="http://localhost";typeof window<"u"&&(s=window.location.origin!=="null"?window.location.origin:window.location.href),Oe(s,"No window.location.(origin|href) available to create URL");let c=typeof i=="string"?i:Ia(i);return c=c.replace(/ $/,"%20"),!a&&c.startsWith("//")&&(c=s+c),new URL(c,s)}function km(i,a,s="/"){return ov(i,a,s,!1)}function ov(i,a,s,c){let f=typeof a=="string"?Jl(a):a,d=fn(f.pathname||"/",s);if(d==null)return null;let y=Ym(i);fv(y);let g=null;for(let p=0;g==null&&p{let E={relativePath:v===void 0?y.path||"":v,caseSensitive:y.caseSensitive===!0,childrenIndex:g,route:y};if(E.relativePath.startsWith("/")){if(!E.relativePath.startsWith(c)&&p)return;Oe(E.relativePath.startsWith(c),`Absolute route path "${E.relativePath}" nested under path "${c}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),E.relativePath=E.relativePath.slice(c.length)}let S=on([c,E.relativePath]),_=s.concat(E);y.children&&y.children.length>0&&(Oe(y.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${S}".`),Ym(y.children,a,_,S,p)),!(y.path==null&&!y.index)&&a.push({path:S,score:gv(S,y.index),routesMeta:_})};return i.forEach((y,g)=>{if(y.path===""||!y.path?.includes("?"))d(y,g);else for(let p of Vm(y.path))d(y,g,!0,p)}),a}function Vm(i){let a=i.split("/");if(a.length===0)return[];let[s,...c]=a,f=s.endsWith("?"),d=s.replace(/\?$/,"");if(c.length===0)return f?[d,""]:[d];let y=Vm(c.join("/")),g=[];return g.push(...y.map(p=>p===""?d:[d,p].join("/"))),f&&g.push(...y),g.map(p=>i.startsWith("/")&&p===""?"/":p)}function fv(i){i.sort((a,s)=>a.score!==s.score?s.score-a.score:bv(a.routesMeta.map(c=>c.childrenIndex),s.routesMeta.map(c=>c.childrenIndex)))}var dv=/^:[\w-]+$/,hv=3,mv=2,pv=1,yv=10,vv=-2,bm=i=>i==="*";function gv(i,a){let s=i.split("/"),c=s.length;return s.some(bm)&&(c+=vv),a&&(c+=mv),s.filter(f=>!bm(f)).reduce((f,d)=>f+(dv.test(d)?hv:d===""?pv:yv),c)}function bv(i,a){return i.length===a.length&&i.slice(0,-1).every((c,f)=>c===a[f])?i[i.length-1]-a[a.length-1]:0}function xv(i,a,s=!1){let{routesMeta:c}=i,f={},d="/",y=[];for(let g=0;g{if(E==="*"){let D=g[_]||"";y=d.slice(0,d.length-D.length).replace(/(.)\/+$/,"$1")}const C=g[_];return S&&!C?v[E]=void 0:v[E]=(C||"").replace(/%2F/g,"/"),v},{}),pathname:d,pathnameBase:y,pattern:i}}function Sv(i,a=!1,s=!0){jt(i==="*"||!i.endsWith("*")||i.endsWith("/*"),`Route path "${i}" will be treated as if it were "${i.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${i.replace(/\*$/,"/*")}".`);let c=[],f="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(y,g,p)=>(c.push({paramName:g,isOptional:p!=null}),p?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return i.endsWith("*")?(c.push({paramName:"*"}),f+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?f+="\\/*$":i!==""&&i!=="/"&&(f+="(?:(?=\\/|$))"),[new RegExp(f,a?void 0:"i"),c]}function _v(i){try{return i.split("/").map(a=>decodeURIComponent(a).replace(/\//g,"%2F")).join("/")}catch(a){return jt(!1,`The URL path "${i}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${a}).`),i}}function fn(i,a){if(a==="/")return i;if(!i.toLowerCase().startsWith(a.toLowerCase()))return null;let s=a.endsWith("/")?a.length-1:a.length,c=i.charAt(s);return c&&c!=="/"?null:i.slice(s)||"/"}var Ev=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Av=i=>Ev.test(i);function Tv(i,a="/"){let{pathname:s,search:c="",hash:f=""}=typeof i=="string"?Jl(i):i,d;if(s)if(Av(s))d=s;else{if(s.includes("//")){let y=s;s=s.replace(/\/\/+/g,"/"),jt(!1,`Pathnames cannot have embedded double slashes - normalizing ${y} -> ${s}`)}s.startsWith("/")?d=xm(s.substring(1),"/"):d=xm(s,a)}else d=a;return{pathname:d,search:Nv(c),hash:Ov(f)}}function xm(i,a){let s=a.replace(/\/+$/,"").split("/");return i.split("/").forEach(f=>{f===".."?s.length>1&&s.pop():f!=="."&&s.push(f)}),s.length>1?s.join("/"):"/"}function pc(i,a,s,c){return`Cannot include a '${i}' character in a manually specified \`to.${a}\` field [${JSON.stringify(c)}]. Please separate it out to the \`to.${s}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Cv(i){return i.filter((a,s)=>s===0||a.route.path&&a.route.path.length>0)}function Gm(i){let a=Cv(i);return a.map((s,c)=>c===a.length-1?s.pathname:s.pathnameBase)}function Xm(i,a,s,c=!1){let f;typeof i=="string"?f=Jl(i):(f={...i},Oe(!f.pathname||!f.pathname.includes("?"),pc("?","pathname","search",f)),Oe(!f.pathname||!f.pathname.includes("#"),pc("#","pathname","hash",f)),Oe(!f.search||!f.search.includes("#"),pc("#","search","hash",f)));let d=i===""||f.pathname==="",y=d?"/":f.pathname,g;if(y==null)g=s;else{let S=a.length-1;if(!c&&y.startsWith("..")){let _=y.split("/");for(;_[0]==="..";)_.shift(),S-=1;f.pathname=_.join("/")}g=S>=0?a[S]:"/"}let p=Tv(f,g),v=y&&y!=="/"&&y.endsWith("/"),E=(d||y===".")&&s.endsWith("/");return!p.pathname.endsWith("/")&&(v||E)&&(p.pathname+="/"),p}var on=i=>i.join("/").replace(/\/\/+/g,"/"),Rv=i=>i.replace(/\/+$/,"").replace(/^\/*/,"/"),Nv=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,Ov=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i;function Mv(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Qm=["POST","PUT","PATCH","DELETE"];new Set(Qm);var zv=["GET",...Qm];new Set(zv);var $l=x.createContext(null);$l.displayName="DataRouter";var Hu=x.createContext(null);Hu.displayName="DataRouterState";x.createContext(!1);var Zm=x.createContext({isTransitioning:!1});Zm.displayName="ViewTransition";var Dv=x.createContext(new Map);Dv.displayName="Fetchers";var wv=x.createContext(null);wv.displayName="Await";var Vt=x.createContext(null);Vt.displayName="Navigation";var ei=x.createContext(null);ei.displayName="Location";var dn=x.createContext({outlet:null,matches:[],isDataRoute:!1});dn.displayName="Route";var kc=x.createContext(null);kc.displayName="RouteError";function jv(i,{relative:a}={}){Oe(ti(),"useHref() may be used only in the context of a component.");let{basename:s,navigator:c}=x.useContext(Vt),{hash:f,pathname:d,search:y}=ni(i,{relative:a}),g=d;return s!=="/"&&(g=d==="/"?s:on([s,d])),c.createHref({pathname:g,search:y,hash:f})}function ti(){return x.useContext(ei)!=null}function cl(){return Oe(ti(),"useLocation() may be used only in the context of a component."),x.useContext(ei).location}var Km="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Jm(i){x.useContext(Vt).static||x.useLayoutEffect(i)}function Uv(){let{isDataRoute:i}=x.useContext(dn);return i?Jv():Bv()}function Bv(){Oe(ti(),"useNavigate() may be used only in the context of a component.");let i=x.useContext($l),{basename:a,navigator:s}=x.useContext(Vt),{matches:c}=x.useContext(dn),{pathname:f}=cl(),d=JSON.stringify(Gm(c)),y=x.useRef(!1);return Jm(()=>{y.current=!0}),x.useCallback((p,v={})=>{if(jt(y.current,Km),!y.current)return;if(typeof p=="number"){s.go(p);return}let E=Xm(p,JSON.parse(d),f,v.relative==="path");i==null&&a!=="/"&&(E.pathname=E.pathname==="/"?a:on([a,E.pathname])),(v.replace?s.replace:s.push)(E,v.state,v)},[a,s,d,f,i])}x.createContext(null);function ni(i,{relative:a}={}){let{matches:s}=x.useContext(dn),{pathname:c}=cl(),f=JSON.stringify(Gm(s));return x.useMemo(()=>Xm(i,JSON.parse(f),c,a==="path"),[i,f,c,a])}function Lv(i,a){return $m(i,a)}function $m(i,a,s,c,f){Oe(ti(),"useRoutes() may be used only in the context of a component.");let{navigator:d}=x.useContext(Vt),{matches:y}=x.useContext(dn),g=y[y.length-1],p=g?g.params:{},v=g?g.pathname:"/",E=g?g.pathnameBase:"/",S=g&&g.route;{let U=S&&S.path||"";Fm(v,!S||U.endsWith("*")||U.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${v}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let _=cl(),C;if(a){let U=typeof a=="string"?Jl(a):a;Oe(E==="/"||U.pathname?.startsWith(E),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${E}" but pathname "${U.pathname}" was given in the \`location\` prop.`),C=U}else C=_;let D=C.pathname||"/",Y=D;if(E!=="/"){let U=E.replace(/^\//,"").split("/");Y="/"+D.replace(/^\//,"").split("/").slice(U.length).join("/")}let k=km(i,{pathname:Y});jt(S||k!=null,`No routes matched location "${C.pathname}${C.search}${C.hash}" `),jt(k==null||k[k.length-1].route.element!==void 0||k[k.length-1].route.Component!==void 0||k[k.length-1].route.lazy!==void 0,`Matched leaf route at location "${C.pathname}${C.search}${C.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let V=Vv(k&&k.map(U=>Object.assign({},U,{params:Object.assign({},p,U.params),pathname:on([E,d.encodeLocation?d.encodeLocation(U.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:U.pathname]),pathnameBase:U.pathnameBase==="/"?E:on([E,d.encodeLocation?d.encodeLocation(U.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:U.pathnameBase])})),y,s,c,f);return a&&V?x.createElement(ei.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...C},navigationType:"POP"}},V):V}function Hv(){let i=Kv(),a=Mv(i)?`${i.status} ${i.statusText}`:i instanceof Error?i.message:JSON.stringify(i),s=i instanceof Error?i.stack:null,c="rgba(200,200,200, 0.5)",f={padding:"0.5rem",backgroundColor:c},d={padding:"2px 4px",backgroundColor:c},y=null;return console.error("Error handled by React Router default ErrorBoundary:",i),y=x.createElement(x.Fragment,null,x.createElement("p",null,"💿 Hey developer 👋"),x.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",x.createElement("code",{style:d},"ErrorBoundary")," or"," ",x.createElement("code",{style:d},"errorElement")," prop on your route.")),x.createElement(x.Fragment,null,x.createElement("h2",null,"Unexpected Application Error!"),x.createElement("h3",{style:{fontStyle:"italic"}},a),s?x.createElement("pre",{style:f},s):null,y)}var qv=x.createElement(Hv,null),kv=class extends x.Component{constructor(i){super(i),this.state={location:i.location,revalidation:i.revalidation,error:i.error}}static getDerivedStateFromError(i){return{error:i}}static getDerivedStateFromProps(i,a){return a.location!==i.location||a.revalidation!=="idle"&&i.revalidation==="idle"?{error:i.error,location:i.location,revalidation:i.revalidation}:{error:i.error!==void 0?i.error:a.error,location:a.location,revalidation:i.revalidation||a.revalidation}}componentDidCatch(i,a){this.props.onError?this.props.onError(i,a):console.error("React Router caught the following error during render",i)}render(){return this.state.error!==void 0?x.createElement(dn.Provider,{value:this.props.routeContext},x.createElement(kc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function Yv({routeContext:i,match:a,children:s}){let c=x.useContext($l);return c&&c.static&&c.staticContext&&(a.route.errorElement||a.route.ErrorBoundary)&&(c.staticContext._deepestRenderedBoundaryId=a.route.id),x.createElement(dn.Provider,{value:i},s)}function Vv(i,a=[],s=null,c=null,f=null){if(i==null){if(!s)return null;if(s.errors)i=s.matches;else if(a.length===0&&!s.initialized&&s.matches.length>0)i=s.matches;else return null}let d=i,y=s?.errors;if(y!=null){let E=d.findIndex(S=>S.route.id&&y?.[S.route.id]!==void 0);Oe(E>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(y).join(",")}`),d=d.slice(0,Math.min(d.length,E+1))}let g=!1,p=-1;if(s)for(let E=0;E=0?d=d.slice(0,p+1):d=[d[0]];break}}}let v=s&&c?(E,S)=>{c(E,{location:s.location,params:s.matches?.[0]?.params??{},errorInfo:S})}:void 0;return d.reduceRight((E,S,_)=>{let C,D=!1,Y=null,k=null;s&&(C=y&&S.route.id?y[S.route.id]:void 0,Y=S.route.errorElement||qv,g&&(p<0&&_===0?(Fm("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),D=!0,k=null):p===_&&(D=!0,k=S.route.hydrateFallbackElement||null)));let V=a.concat(d.slice(0,_+1)),U=()=>{let H;return C?H=Y:D?H=k:S.route.Component?H=x.createElement(S.route.Component,null):S.route.element?H=S.route.element:H=E,x.createElement(Yv,{match:S,routeContext:{outlet:E,matches:V,isDataRoute:s!=null},children:H})};return s&&(S.route.ErrorBoundary||S.route.errorElement||_===0)?x.createElement(kv,{location:s.location,revalidation:s.revalidation,component:Y,error:C,children:U(),routeContext:{outlet:null,matches:V,isDataRoute:!0},onError:v}):U()},null)}function Yc(i){return`${i} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Gv(i){let a=x.useContext($l);return Oe(a,Yc(i)),a}function Xv(i){let a=x.useContext(Hu);return Oe(a,Yc(i)),a}function Qv(i){let a=x.useContext(dn);return Oe(a,Yc(i)),a}function Vc(i){let a=Qv(i),s=a.matches[a.matches.length-1];return Oe(s.route.id,`${i} can only be used on routes that contain a unique "id"`),s.route.id}function Zv(){return Vc("useRouteId")}function Kv(){let i=x.useContext(kc),a=Xv("useRouteError"),s=Vc("useRouteError");return i!==void 0?i:a.errors?.[s]}function Jv(){let{router:i}=Gv("useNavigate"),a=Vc("useNavigate"),s=x.useRef(!1);return Jm(()=>{s.current=!0}),x.useCallback(async(f,d={})=>{jt(s.current,Km),s.current&&(typeof f=="number"?i.navigate(f):await i.navigate(f,{fromRouteId:a,...d}))},[i,a])}var Sm={};function Fm(i,a,s){!a&&!Sm[i]&&(Sm[i]=!0,jt(!1,s))}x.memo($v);function $v({routes:i,future:a,state:s,unstable_onError:c}){return $m(i,void 0,s,c,a)}function Oc(i){Oe(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Fv({basename:i="/",children:a=null,location:s,navigationType:c="POP",navigator:f,static:d=!1}){Oe(!ti(),"You cannot render a inside another . You should never have more than one in your app.");let y=i.replace(/^\/*/,"/"),g=x.useMemo(()=>({basename:y,navigator:f,static:d,future:{}}),[y,f,d]);typeof s=="string"&&(s=Jl(s));let{pathname:p="/",search:v="",hash:E="",state:S=null,key:_="default"}=s,C=x.useMemo(()=>{let D=fn(p,y);return D==null?null:{location:{pathname:D,search:v,hash:E,state:S,key:_},navigationType:c}},[y,p,v,E,S,_,c]);return jt(C!=null,` is not able to match the URL "${p}${v}${E}" because it does not start with the basename, so the won't render anything.`),C==null?null:x.createElement(Vt.Provider,{value:g},x.createElement(ei.Provider,{children:a,value:C}))}function Wv({children:i,location:a}){return Lv(Mc(i),a)}function Mc(i,a=[]){let s=[];return x.Children.forEach(i,(c,f)=>{if(!x.isValidElement(c))return;let d=[...a,f];if(c.type===x.Fragment){s.push.apply(s,Mc(c.props.children,d));return}Oe(c.type===Oc,`[${typeof c.type=="string"?c.type:c.type.name}] is not a component. All component children of must be a or `),Oe(!c.props.index||!c.props.children,"An index route cannot have child routes.");let y={id:c.props.id||d.join("-"),caseSensitive:c.props.caseSensitive,element:c.props.element,Component:c.props.Component,index:c.props.index,path:c.props.path,middleware:c.props.middleware,loader:c.props.loader,action:c.props.action,hydrateFallbackElement:c.props.hydrateFallbackElement,HydrateFallback:c.props.HydrateFallback,errorElement:c.props.errorElement,ErrorBoundary:c.props.ErrorBoundary,hasErrorBoundary:c.props.hasErrorBoundary===!0||c.props.ErrorBoundary!=null||c.props.errorElement!=null,shouldRevalidate:c.props.shouldRevalidate,handle:c.props.handle,lazy:c.props.lazy};c.props.children&&(y.children=Mc(c.props.children,d)),s.push(y)}),s}var Mu="get",zu="application/x-www-form-urlencoded";function qu(i){return i!=null&&typeof i.tagName=="string"}function Iv(i){return qu(i)&&i.tagName.toLowerCase()==="button"}function Pv(i){return qu(i)&&i.tagName.toLowerCase()==="form"}function eg(i){return qu(i)&&i.tagName.toLowerCase()==="input"}function tg(i){return!!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)}function ng(i,a){return i.button===0&&(!a||a==="_self")&&!tg(i)}var Su=null;function lg(){if(Su===null)try{new FormData(document.createElement("form"),0),Su=!1}catch{Su=!0}return Su}var ag=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function yc(i){return i!=null&&!ag.has(i)?(jt(!1,`"${i}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${zu}"`),null):i}function ig(i,a){let s,c,f,d,y;if(Pv(i)){let g=i.getAttribute("action");c=g?fn(g,a):null,s=i.getAttribute("method")||Mu,f=yc(i.getAttribute("enctype"))||zu,d=new FormData(i)}else if(Iv(i)||eg(i)&&(i.type==="submit"||i.type==="image")){let g=i.form;if(g==null)throw new Error('Cannot submit a diff --git a/webui/src/components/DrivePanel.jsx b/webui/src/components/DrivePanel.jsx index dcbc77a1..951d6405 100644 --- a/webui/src/components/DrivePanel.jsx +++ b/webui/src/components/DrivePanel.jsx @@ -1,8 +1,11 @@ -import { useDriveControl } from '../context/DriveControlContext.jsx'; import { useTelemetryFrame } from '../context/TelemetryContext.jsx'; +import { useControlSystem } from '../controls/index.js'; export default function DrivePanel() { - const { roverId, runStartDockFull, seekDock } = useDriveControl(); + const { + state: { roverId }, + actions, + } = useControlSystem(); const frame = useTelemetryFrame(roverId); const sensors = frame?.sensors || {}; const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full'; @@ -11,6 +14,18 @@ export default function DrivePanel() { sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging', ); + const handleStartDrive = () => { + if (!roverId) return; + actions.setMode('drive'); + actions.runMacro('drive-sequence'); + }; + + const handleDock = () => { + if (!roverId) return; + actions.setMode('dock'); + actions.runMacro('seek-dock'); + }; + return (
@@ -23,7 +38,7 @@ export default function DrivePanel() { description="Press to enable driving mode, then start moving. The headlamps should illuminate." statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]} tone="emerald" - onClick={runStartDockFull} + onClick={handleStartDrive} disabled={!roverId} />
diff --git a/webui/src/components/MobileControls.jsx b/webui/src/components/MobileControls.jsx index 20e4acfc..610f7f2b 100644 --- a/webui/src/components/MobileControls.jsx +++ b/webui/src/components/MobileControls.jsx @@ -1,114 +1,132 @@ -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { Joystick } from 'react-joystick-component'; -import { useDriveControl } from '../context/DriveControlContext.jsx'; +import { useControlSystem } from '../controls/index.js'; +import { clampUnit } from '../controls/controlMath.js'; +import DriveModeToggle from './controls/DriveModeToggle.jsx'; -function clampUnit(value = 0) { - if (typeof value !== 'number' || Number.isNaN(value)) return 0; - return Math.max(-1, Math.min(1, value)); -} +const SOURCE = 'mobile-joystick'; +const AUX_BUTTONS = [ + { id: 'main-forward', label: 'Main +', values: { main: 127 }, hold: true }, + { id: 'main-reverse', label: 'Main -', values: { main: -127 }, hold: true }, + { id: 'side-forward', label: 'Side +', values: { side: 127 }, hold: true }, + { id: 'side-reverse', label: 'Side -', values: { side: -70 }, hold: true }, + { id: 'vacuum-fast', label: 'Vacuum Max', values: { vacuum: 127 }, hold: true }, + { id: 'vacuum-slow', label: 'Vacuum Low', values: { vacuum: 50 }, hold: true }, + { id: 'all-forward', label: 'All +', values: { main: 127, side: 127, vacuum: 127 }, hold: true }, + { id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false }, +]; -function MobileJoystick() { - const { roverId, driveWithVector, stopMotors } = useDriveControl(); +function MobileJoystickPanel({ layout }) { + const { + state: { roverId }, + actions: { setDriveVector, registerInputState, stopAllMotion }, + } = useControlSystem(); const disabled = !roverId; const handleMove = useCallback( (event = {}) => { if (disabled) return; - const x = clampUnit(event.x ?? 0); - const y = clampUnit(event.y ?? 0); - driveWithVector({ x, y }); + const vector = { + x: clampUnit(event.x ?? 0), + y: clampUnit(event.y ?? 0), + boost: Boolean(event.shiftKey), + }; + setDriveVector(vector, { source: SOURCE }); + registerInputState(SOURCE, { vector, lastEvent: 'move' }); }, - [disabled, driveWithVector], + [disabled, registerInputState, setDriveVector], ); const handleStop = useCallback(() => { if (disabled) return; - driveWithVector({ x: 0, y: 0 }); - }, [disabled, driveWithVector]); + const zero = { x: 0, y: 0, boost: false }; + setDriveVector(zero, { source: SOURCE }); + registerInputState(SOURCE, { vector: zero, lastEvent: 'stop' }); + }, [disabled, registerInputState, setDriveVector]); return (
-
Joystick
-
- +
+
+ +
+

Drag to steer · Release to stop sending drive commands.

+
-

- Drag to drive. Release to stop sending drive commands. -

-
); } -function AuxMotorControls() { - const { roverId, setAuxMotors } = useDriveControl(); +function AuxMotorPanel({ orientation }) { + const { + state: { roverId }, + actions: { setAuxMotors }, + } = useControlSystem(); + const activeRef = useRef(null); const disabled = !roverId; - const runAllForward = () => { - if (disabled) return; - setAuxMotors({ main: 127, side: 127, vacuum: 127 }); - }; + const handlePress = useCallback( + (button) => { + if (disabled) return; + if (button.hold === false) { + setAuxMotors(button.values); + activeRef.current = null; + return; + } + activeRef.current = button.id; + setAuxMotors(button.values); + }, + [disabled, setAuxMotors], + ); - const stopAll = () => { - if (disabled) return; - setAuxMotors({ main: 0, side: 0, vacuum: 0 }); - }; + const handleRelease = useCallback( + (button) => { + if (disabled || button.hold === false) return; + if (activeRef.current === button.id) { + activeRef.current = null; + setAuxMotors({ main: 0, side: 0, vacuum: 0 }); + } + }, + [disabled, setAuxMotors], + ); - const auxButtons = [ - { label: 'Main +', values: { main: 127 } }, - { label: 'Main -', values: { main: -127 } }, - { label: 'Side +', values: { side: 127 } }, - { label: 'Side -', values: { side: -127 } }, - { label: 'Vacuum Max', values: { vacuum: 127 } }, - { label: 'Vacuum Off', values: { vacuum: 0 } }, - ]; + const buttonClasses = orientation === 'landscape' ? 'text-xs' : 'text-[0.75rem]'; return (
-
Aux motors
-
- - -
-
- {auxButtons.map((btn) => ( +

Aux motors · hold to run

+
+ {AUX_BUTTONS.map((button) => ( ))}
@@ -116,17 +134,29 @@ function AuxMotorControls() { ); } -export default function MobileControlsStack() { +export function MobileLandscapeAuxColumn() { return ( -
-
- -
-
- -
+
+
); } -export { MobileJoystick, AuxMotorControls }; +export function MobileLandscapeControlColumn() { + return ( +
+ +
+ ); +} + +export default function MobilePortraitControls() { + return ( +
+
+ + +
+
+ ); +} diff --git a/webui/src/components/controls/DriveModeToggle.jsx b/webui/src/components/controls/DriveModeToggle.jsx new file mode 100644 index 00000000..8e6ab02a --- /dev/null +++ b/webui/src/components/controls/DriveModeToggle.jsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { useControlSystem } from '../../controls/index.js'; + +export default function DriveModeToggle({ size = 'default' }) { + const { + state: { roverId, mode }, + actions, + } = useControlSystem(); + const [pending, setPending] = useState(null); + const disabled = !roverId || pending !== null; + + const handleDrive = async () => { + if (!roverId) return; + setPending('drive'); + try { + actions.setMode('drive'); + await actions.runMacro('drive-sequence'); + } finally { + setPending(null); + } + }; + + const handleDock = async () => { + if (!roverId) return; + setPending('dock'); + try { + actions.setMode('dock'); + await actions.runMacro('seek-dock'); + } finally { + setPending(null); + } + }; + + const pillClass = + size === 'compact' + ? 'text-xs px-1 py-0.5' + : 'text-sm px-1.5 py-0.5'; + const currentLabel = mode === 'dock' ? 'Dock mode' : 'Drive mode'; + + return ( +
+
+ {currentLabel} + Rover {roverId ?? '—'} +
+
+ + +
+
+ ); +} diff --git a/webui/src/context/DriveControlContext.jsx b/webui/src/context/DriveControlContext.jsx deleted file mode 100644 index 121d63ba..00000000 --- a/webui/src/context/DriveControlContext.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createContext, useContext } from 'react'; -import { useDriveControls as useDriveControlsHook } from '../hooks/useDriveControls.js'; - -/* eslint-disable react-refresh/only-export-components */ - -const DriveControlContext = createContext(null); - -export function DriveControlProvider({ children }) { - const controls = useDriveControlsHook(); - return {children}; -} - -export function useDriveControl() { - const context = useContext(DriveControlContext); - if (!context) { - throw new Error('useDriveControl must be used within DriveControlProvider'); - } - return context; -} diff --git a/webui/src/controls/ControlContext.jsx b/webui/src/controls/ControlContext.jsx new file mode 100644 index 00000000..f7ebba42 --- /dev/null +++ b/webui/src/controls/ControlContext.jsx @@ -0,0 +1,235 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react'; +import { controlReducer, initialControlState } from './controlReducer.js'; +import { computeDifferentialSpeeds, clamp } from './controlMath.js'; +import { useCommandPipeline } from './commandPipeline.js'; +import { loadControlSettings, saveControlSettings } from './persistence.js'; + +const ControlSystemContext = createContext(null); + +function clampServoAngle(config, value) { + if (!config) return value; + const min = typeof config.minAngle === 'number' ? config.minAngle : -45; + const max = typeof config.maxAngle === 'number' ? config.maxAngle : 45; + return clamp(value, min, max); +} + +export function ControlSystemProvider({ children }) { + const pipeline = useCommandPipeline(); + const [state, dispatch] = useReducer(controlReducer, initialControlState); + + useEffect(() => { + dispatch({ type: 'control/set-rover', payload: pipeline.roverId }); + }, [pipeline.roverId]); + + useEffect(() => { + dispatch({ type: 'control/settings/loading' }); + const data = loadControlSettings(); + dispatch({ type: 'control/settings/loaded', payload: data }); + }, []); + + useEffect(() => { + const config = pipeline.servoConfig; + if (!config) { + dispatch({ type: 'control/set-camera-config', payload: { config: null } }); + return; + } + const min = typeof config.minAngle === 'number' ? config.minAngle : -45; + const max = typeof config.maxAngle === 'number' ? config.maxAngle : 45; + const base = typeof config.homeAngle === 'number' ? config.homeAngle : (min + max) / 2; + dispatch({ + type: 'control/set-camera-config', + payload: { config, angle: clamp(base, min, max) }, + }); + }, [pipeline.servoConfig]); + + useEffect(() => { + if (pipeline.roverId) { + pipeline.enableSensorStream(); + } + }, [pipeline.roverId, pipeline.enableSensorStream]); + + const setMode = useCallback( + (mode) => { + dispatch({ type: 'control/set-mode', payload: mode }); + }, + [], + ); + + const setDriveVector = useCallback( + (vector, meta = {}) => { + const computed = computeDifferentialSpeeds(vector); + dispatch({ + type: 'control/update-drive', + payload: { ...computed, source: meta.source ?? null }, + }); + pipeline.sendDriveDirect(computed.speeds); + }, + [pipeline], + ); + + const setAuxMotors = useCallback( + (values = {}) => { + const payload = pipeline.sendAuxMotors(values) ?? values; + dispatch({ + type: 'control/set-aux-motors', + payload, + }); + }, + [pipeline], + ); + + const setServoAngle = useCallback( + (value) => { + if (!pipeline.servoConfig) return; + const clamped = clampServoAngle(pipeline.servoConfig, value); + dispatch({ type: 'control/set-camera-angle', payload: clamped }); + pipeline.sendServoAngle(clamped); + }, + [pipeline], + ); + + const nudgeServo = useCallback( + (delta = 0) => { + const config = pipeline.servoConfig; + if (!config) return; + const step = typeof delta === 'number' && delta !== 0 ? delta : config.nudgeDegrees || 1; + const baseline = + typeof state.camera.angle === 'number' + ? state.camera.angle + : typeof config.homeAngle === 'number' + ? config.homeAngle + : 0; + setServoAngle(baseline + step); + }, + [pipeline.servoConfig, setServoAngle, state.camera.angle], + ); + + const goServoHome = useCallback(() => { + const config = pipeline.servoConfig; + if (!config) return; + const target = + typeof config.homeAngle === 'number' + ? config.homeAngle + : typeof config.minAngle === 'number' && typeof config.maxAngle === 'number' + ? (config.minAngle + config.maxAngle) / 2 + : 0; + setServoAngle(target); + }, [pipeline.servoConfig, setServoAngle]); + + const runMacro = useCallback( + async (macroId) => { + const macro = state.macros.find((item) => item.id === macroId) || null; + if (!macro) return; + await pipeline.runMacroSteps(macro); + }, + [pipeline, state.macros], + ); + + const stopAllMotion = useCallback(() => { + dispatch({ + type: 'control/update-drive', + payload: { + vector: { x: 0, y: 0, boost: false }, + speeds: { left: 0, right: 0 }, + source: 'system-stop', + }, + }); + pipeline.sendDriveDirect({ left: 0, right: 0 }); + pipeline.sendAuxMotors({ main: 0, side: 0, vacuum: 0 }); + }, [pipeline]); + + const sendOiCommand = useCallback( + (command) => { + pipeline.sendOiCommand(command); + }, + [pipeline], + ); + + const setSensorStream = useCallback( + (enable) => { + if (!pipeline.roverId) return; + pipeline.emitCommand({ + type: 'sensorStream', + data: { sensorStream: { enable } }, + }); + }, + [pipeline], + ); + + const registerInputState = useCallback((source, data) => { + dispatch({ type: 'control/register-input-state', payload: { source, state: data } }); + }, []); + + const reloadSettings = useCallback(() => { + dispatch({ type: 'control/settings/loading' }); + const next = loadControlSettings(); + dispatch({ type: 'control/settings/loaded', payload: next }); + }, []); + + const persistSettings = useCallback( + (partial) => { + const merged = { ...(state.settings.data ?? {}), ...(partial ?? {}) }; + const success = saveControlSettings(merged); + if (success) { + dispatch({ type: 'control/settings/loaded', payload: merged }); + } else { + dispatch({ + type: 'control/settings/error', + payload: new Error('Failed to save control settings'), + }); + } + return success; + }, + [state.settings.data], + ); + + const contextValue = useMemo( + () => ({ + state, + dispatch, + pipeline, + actions: { + setMode, + setDriveVector, + setAuxMotors, + setServoAngle, + nudgeServo, + goServoHome, + runMacro, + stopAllMotion, + sendOiCommand, + setSensorStream, + registerInputState, + reloadSettings, + persistSettings, + }, + }), + [ + state, + pipeline, + setMode, + setDriveVector, + setAuxMotors, + setServoAngle, + nudgeServo, + goServoHome, + runMacro, + stopAllMotion, + sendOiCommand, + setSensorStream, + registerInputState, + reloadSettings, + persistSettings, + ], + ); + + return {children}; +} + +export function useControlSystem() { + const context = useContext(ControlSystemContext); + if (!context) { + throw new Error('useControlSystem must be used within ControlSystemProvider'); + } + return context; +} diff --git a/webui/src/controls/commandPipeline.js b/webui/src/controls/commandPipeline.js new file mode 100644 index 00000000..38ded3ba --- /dev/null +++ b/webui/src/controls/commandPipeline.js @@ -0,0 +1,163 @@ +import { useCallback, useMemo } from 'react'; +import { useSocket } from '../context/SocketContext.jsx'; +import { useSession } from '../context/SessionContext.jsx'; +import { AUX_LIMITS, COMMAND_DELAY_MS, OI_COMMANDS } from './constants.js'; +import { bytesToBase64, clampRange, sleep } from './controlMath.js'; + +export function useCommandPipeline() { + const socket = useSocket(); + const { session } = useSession(); + const roverId = session?.assignment?.roverId; + + const rosterEntry = useMemo(() => { + if (!roverId || !Array.isArray(session?.roster)) return null; + return session.roster.find((entry) => String(entry.id) === String(roverId)) || null; + }, [roverId, session?.roster]); + + const servoConfig = useMemo(() => { + if (!rosterEntry?.cameraServo || !rosterEntry.cameraServo.enabled) return null; + return rosterEntry.cameraServo; + }, [rosterEntry]); + + const emitCommand = useCallback( + (payload, cb) => { + if (!roverId) return; + socket.emit('command', { roverId, ...payload }, cb); + }, + [socket, roverId], + ); + + const enableSensorStream = useCallback(() => { + if (!roverId) return; + emitCommand({ + type: 'sensorStream', + data: { sensorStream: { enable: true } }, + }); + }, [emitCommand, roverId]); + + const sendDriveDirect = useCallback( + (speeds) => { + if (!roverId) return null; + const payload = { + left: clampRange(speeds?.left ?? 0, [-500, 500]), + right: clampRange(speeds?.right ?? 0, [-500, 500]), + }; + emitCommand({ + type: 'drive', + data: { driveDirect: payload }, + }); + return payload; + }, + [emitCommand, roverId], + ); + + const sendAuxMotors = useCallback( + ({ main = 0, side = 0, vacuum = 0 } = {}) => { + if (!roverId) return null; + const payload = { + main: clampRange(main, AUX_LIMITS.main), + side: clampRange(side, AUX_LIMITS.side), + vacuum: clampRange(vacuum, AUX_LIMITS.vacuum), + }; + emitCommand({ + type: 'motors', + data: { motorPwm: payload }, + }); + return payload; + }, + [emitCommand, roverId], + ); + + const sendServoAngle = useCallback( + (angle) => { + if (!roverId || !servoConfig) return null; + emitCommand({ + type: 'servo', + data: { servo: { angle } }, + }); + return angle; + }, + [emitCommand, roverId, servoConfig], + ); + + const sendOiCommand = useCallback( + (keyOrBytes) => { + if (!roverId) return false; + const bytes = Array.isArray(keyOrBytes) + ? keyOrBytes + : typeof keyOrBytes === 'string' + ? OI_COMMANDS[keyOrBytes] + : null; + if (!bytes) return false; + emitCommand({ + type: 'raw', + data: { raw: bytesToBase64(bytes) }, + }); + enableSensorStream(); + return true; + }, + [emitCommand, enableSensorStream, roverId], + ); + + const runMacroSteps = useCallback( + async (macro) => { + if (!macro || !Array.isArray(macro.steps) || !roverId) return; + for (const step of macro.steps) { + if (!roverId) break; + switch (step.type) { + case 'oi': + sendOiCommand(step.command); + break; + case 'drive': + sendDriveDirect(step.speeds ?? { left: 0, right: 0 }); + break; + case 'motors': + sendAuxMotors(step.values ?? {}); + break; + case 'servo': + sendServoAngle(step.angle); + break; + case 'pause': + await sleep(step.duration ?? COMMAND_DELAY_MS); // eslint-disable-line no-await-in-loop + break; + default: + break; + } + if (step.delay || step.delayMs) { + const delay = step.delayMs ?? step.delay; + if (typeof delay === 'number' && delay > 0) { + await sleep(delay); // eslint-disable-line no-await-in-loop + } + } + } + }, + [roverId, sendOiCommand, sendDriveDirect, sendAuxMotors, sendServoAngle], + ); + + return useMemo( + () => ({ + roverId, + rosterEntry, + servoConfig, + emitCommand, + enableSensorStream, + sendDriveDirect, + sendAuxMotors, + sendServoAngle, + sendOiCommand, + runMacroSteps, + }), + [ + roverId, + rosterEntry, + servoConfig, + emitCommand, + enableSensorStream, + sendDriveDirect, + sendAuxMotors, + sendServoAngle, + sendOiCommand, + runMacroSteps, + ], + ); +} diff --git a/webui/src/controls/constants.js b/webui/src/controls/constants.js new file mode 100644 index 00000000..919eb205 --- /dev/null +++ b/webui/src/controls/constants.js @@ -0,0 +1,65 @@ +export const AUX_LIMITS = { + main: [-127, 127], + side: [-127, 127], + vacuum: [0, 127], +}; + +export const DRIVE_LIMITS = { + maxSpeed: 500, + baseSpeed: 250, + boostSpeed: 400, +}; + +export const COMMAND_DELAY_MS = 200; + +export const OI_COMMANDS = { + start: [128], + safe: [131], + full: [132], + passive: [128], + dock: [143], +}; + +export const DEFAULT_KEYMAP = { + driveForward: ['w'], + driveBackward: ['s'], + driveLeft: ['a'], + driveRight: ['d'], + boostModifier: ['\\\\'], + slowModifier: ['shift'], + auxMainForward: ['o'], + auxMainReverse: ['l'], + auxSideForward: ['p'], + auxSideReverse: [';'], + auxVacuumFast: ['['], + auxVacuumSlow: ["'"], + auxAllForward: ['.'], + cameraUp: ['i'], + cameraDown: ['k'], + driveMacro: ['g'], + dockMacro: ['h'], +}; + +export const DEFAULT_MACROS = [ + { + id: 'drive-sequence', + label: 'Drive', + description: 'Start, dock, and full command sequence used by the drive button.', + steps: [ + { type: 'oi', command: 'start' }, + { type: 'pause', duration: COMMAND_DELAY_MS }, + { type: 'oi', command: 'dock' }, + { type: 'pause', duration: COMMAND_DELAY_MS }, + { type: 'oi', command: 'full' }, + ], + }, + { + id: 'seek-dock', + label: 'Dock', + description: 'Send the seek dock command.', + steps: [{ type: 'oi', command: 'dock' }], + }, +]; + +export const CONTROL_SETTINGS_COOKIE = 'roverControlSettings'; +export const CONTROL_SETTINGS_MAX_AGE = 60 * 60 * 24 * 365; // 1 year diff --git a/webui/src/controls/controlMath.js b/webui/src/controls/controlMath.js new file mode 100644 index 00000000..b3404843 --- /dev/null +++ b/webui/src/controls/controlMath.js @@ -0,0 +1,59 @@ +/* global Buffer */ + +import { DRIVE_LIMITS } from './constants.js'; + +export function clamp(value, min, max) { + if (typeof value !== 'number' || Number.isNaN(value)) return min; + return Math.max(min, Math.min(max, value)); +} + +export function clampUnit(value) { + if (typeof value !== 'number' || Number.isNaN(value)) return 0; + return Math.max(-1, Math.min(1, value)); +} + +export function clampRange(value, range) { + if (!Array.isArray(range) || range.length !== 2) return value; + return clamp(value, range[0], range[1]); +} + +export function normalizeDriveVector(vector = {}) { + const x = clampUnit(vector.x ?? 0); + const y = clampUnit(vector.y ?? 0); + return { + x, + y, + boost: Boolean(vector.boost), + }; +} + +export function computeDifferentialSpeeds(vector = {}, options = {}) { + const normalized = normalizeDriveVector(vector); + const maxSpeed = typeof options.maxSpeed === 'number' ? options.maxSpeed : DRIVE_LIMITS.maxSpeed; + const baseSpeed = typeof options.baseSpeed === 'number' ? options.baseSpeed : DRIVE_LIMITS.baseSpeed; + const boostSpeed = typeof options.boostSpeed === 'number' ? options.boostSpeed : DRIVE_LIMITS.boostSpeed; + const base = normalized.boost ? boostSpeed : baseSpeed; + const forward = normalized.y * base; + const turn = normalized.x * base; + return { + vector: normalized, + speeds: { + left: clamp(Math.round(forward + turn), -maxSpeed, maxSpeed), + right: clamp(Math.round(forward - turn), -maxSpeed, maxSpeed), + }, + }; +} + +export function bytesToBase64(bytes) { + const safeBytes = Array.isArray(bytes) ? bytes : []; + const binary = String.fromCharCode(...safeBytes); + if (typeof btoa === 'function') return btoa(binary); + if (typeof Buffer !== 'undefined') { + return Buffer.from(safeBytes).toString('base64'); + } + throw new Error('No base64 encoder available'); +} + +export function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/webui/src/controls/controlReducer.js b/webui/src/controls/controlReducer.js new file mode 100644 index 00000000..065d927b --- /dev/null +++ b/webui/src/controls/controlReducer.js @@ -0,0 +1,166 @@ +import { DEFAULT_KEYMAP, DEFAULT_MACROS } from './constants.js'; + +function createDriveState() { + return { + vector: { x: 0, y: 0, boost: false }, + speeds: { left: 0, right: 0 }, + source: null, + lastUpdatedAt: 0, + }; +} + +function createAuxState() { + return { main: 0, side: 0, vacuum: 0 }; +} + +function createCameraState() { + return { + enabled: false, + angle: null, + config: null, + }; +} + +export const initialControlState = { + roverId: null, + mode: 'drive', + drive: createDriveState(), + aux: createAuxState(), + camera: createCameraState(), + macros: DEFAULT_MACROS, + keymap: DEFAULT_KEYMAP, + inputs: {}, + settings: { + status: 'idle', + error: null, + lastLoadedAt: null, + lastSavedAt: null, + data: null, + }, +}; + +function mergeSettings(state, payload) { + const nextSettings = { + status: 'ready', + error: null, + lastLoadedAt: Date.now(), + lastSavedAt: state.settings.lastSavedAt, + data: payload ?? state.settings.data, + }; + const nextState = { ...state, settings: nextSettings }; + if (payload?.keymap) { + nextState.keymap = { ...state.keymap, ...payload.keymap }; + } + if (payload?.macros) { + nextState.macros = payload.macros; + } + return nextState; +} + +export function controlReducer(state, action) { + switch (action.type) { + case 'control/set-rover': + return { + ...state, + roverId: action.payload ?? null, + drive: action.payload ? state.drive : createDriveState(), + aux: action.payload ? state.aux : createAuxState(), + }; + case 'control/set-mode': + return state.mode === action.payload + ? state + : { ...state, mode: action.payload === 'dock' ? 'dock' : 'drive' }; + case 'control/update-drive': { + const { vector, speeds, source } = action.payload; + return { + ...state, + drive: { + vector: vector ?? state.drive.vector, + speeds: speeds ?? state.drive.speeds, + source: source ?? state.drive.source, + lastUpdatedAt: Date.now(), + }, + }; + } + case 'control/set-aux-motors': + return { + ...state, + aux: { + ...state.aux, + ...action.payload, + }, + }; + case 'control/set-camera-config': + return { + ...state, + camera: { + ...state.camera, + enabled: Boolean(action.payload?.config), + config: action.payload?.config ?? null, + angle: + typeof action.payload?.angle === 'number' + ? action.payload.angle + : action.payload?.config + ? action.payload.config.homeAngle ?? state.camera.angle + : null, + }, + }; + case 'control/set-camera-angle': + return { + ...state, + camera: { ...state.camera, angle: action.payload }, + }; + case 'control/register-input-state': { + const sourceKey = action.payload?.source || 'unknown'; + return { + ...state, + inputs: { + ...state.inputs, + [sourceKey]: { + ...(state.inputs[sourceKey] ?? {}), + ...action.payload?.state, + updatedAt: Date.now(), + }, + }, + }; + } + case 'control/settings/loading': + return { + ...state, + settings: { + ...state.settings, + status: 'loading', + error: null, + }, + }; + case 'control/settings/loaded': + return mergeSettings(state, action.payload); + case 'control/settings/error': + return { + ...state, + settings: { + ...state.settings, + status: 'error', + error: action.payload, + }, + }; + case 'control/set-keymap': + return { + ...state, + keymap: { ...state.keymap, ...(action.payload ?? {}) }, + }; + case 'control/set-macros': + return { + ...state, + macros: Array.isArray(action.payload) ? action.payload : state.macros, + }; + case 'control/reset': + return { + ...state, + drive: createDriveState(), + aux: createAuxState(), + }; + default: + return state; + } +} diff --git a/webui/src/controls/index.js b/webui/src/controls/index.js new file mode 100644 index 00000000..f7da3b4d --- /dev/null +++ b/webui/src/controls/index.js @@ -0,0 +1,3 @@ +export { ControlSystemProvider, useControlSystem } from './ControlContext.jsx'; +export { default as KeyboardInputManager } from './inputs/KeyboardInputManager.jsx'; +export { default as GamepadInputManager } from './inputs/GamepadInputManager.jsx'; diff --git a/webui/src/controls/inputs/GamepadInputManager.jsx b/webui/src/controls/inputs/GamepadInputManager.jsx new file mode 100644 index 00000000..0c8f4ed7 --- /dev/null +++ b/webui/src/controls/inputs/GamepadInputManager.jsx @@ -0,0 +1,143 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { useControlSystem } from '../ControlContext.jsx'; +import { clampUnit } from '../controlMath.js'; + +const SOURCE = 'gamepad'; +const DEADZONE = 0.2; + +function applyDeadzone(value) { + return Math.abs(value) < DEADZONE ? 0 : value; +} + +function computeTriggerSpeed(button, reverseButton, max = 127, reverseScale = 1) { + if (!button) return 0; + const value = typeof button.value === 'number' ? button.value : button.pressed ? 1 : 0; + if (value < 0.05) return 0; + const direction = reverseButton?.pressed ? -1 : 1; + return Math.round(value * max * direction * reverseScale); +} + +function vectorsEqual(a, b) { + return ( + a && + b && + a.x === b.x && + a.y === b.y && + a.boost === b.boost + ); +} + +function auxEqual(a, b) { + return a && b && a.main === b.main && a.side === b.side && a.vacuum === b.vacuum; +} + +export default function GamepadInputManager() { + const { + actions: { + setMode, + setDriveVector, + setAuxMotors, + nudgeServo, + runMacro, + registerInputState, + }, + } = useControlSystem(); + const rafRef = useRef(null); + const lastVectorRef = useRef({ x: 0, y: 0, boost: false }); + const lastAuxRef = useRef({ main: 0, side: 0, vacuum: 0 }); + const buttonStateRef = useRef(new Map()); + const servoThrottleRef = useRef(0); + + const handleButtonEdge = useCallback( + (index, pressed) => { + const prev = buttonStateRef.current.get(index) || false; + buttonStateRef.current.set(index, pressed); + return pressed && !prev; + }, + [], + ); + + const pollGamepads = useCallback(() => { + if (typeof navigator === 'undefined' || !navigator.getGamepads) { + return; + } + const pads = navigator.getGamepads(); + const pad = pads && Array.from(pads).find(Boolean); + if (!pad) { + if (!vectorsEqual(lastVectorRef.current, { x: 0, y: 0, boost: false })) { + lastVectorRef.current = { x: 0, y: 0, boost: false }; + setDriveVector({ x: 0, y: 0, boost: false }, { source: SOURCE }); + } + if (!auxEqual(lastAuxRef.current, { main: 0, side: 0, vacuum: 0 })) { + lastAuxRef.current = { main: 0, side: 0, vacuum: 0 }; + setAuxMotors({ main: 0, side: 0, vacuum: 0 }); + } + registerInputState(SOURCE, { connected: false }); + return; + } + + const axisLX = clampUnit(applyDeadzone(pad.axes?.[0] ?? 0)); + const axisLY = clampUnit(applyDeadzone(-(pad.axes?.[1] ?? 0))); + const vector = { x: axisLX, y: axisLY, boost: false }; + if (!vectorsEqual(vector, lastVectorRef.current)) { + lastVectorRef.current = vector; + setDriveVector(vector, { source: SOURCE }); + } + + const main = computeTriggerSpeed(pad.buttons?.[7], pad.buttons?.[5]); + const side = computeTriggerSpeed(pad.buttons?.[6], pad.buttons?.[4], 127, 0.55); + const vacuum = pad.buttons?.[1]?.pressed ? 127 : 0; + let aux = { main, side, vacuum }; + if (pad.buttons?.[0]?.pressed) { + aux = { main: 127, side: 127, vacuum: 127 }; + } + if (!auxEqual(aux, lastAuxRef.current)) { + lastAuxRef.current = aux; + setAuxMotors(aux); + } + + const cameraAxis = clampUnit(applyDeadzone(-(pad.axes?.[3] ?? 0))); + const now = performance.now(); + if (Math.abs(cameraAxis) > 0.25 && now - servoThrottleRef.current > 120) { + servoThrottleRef.current = now; + nudgeServo(cameraAxis > 0 ? 2 : -2); + } + + if (handleButtonEdge(14, pad.buttons?.[14]?.pressed)) { + setMode('drive'); + runMacro('drive-sequence'); + } + if (handleButtonEdge(15, pad.buttons?.[15]?.pressed)) { + setMode('dock'); + runMacro('seek-dock'); + } + + registerInputState(SOURCE, { + connected: true, + id: pad.id, + index: pad.index, + axes: [axisLX, axisLY, cameraAxis], + buttons: { + a: pad.buttons?.[0]?.pressed || false, + b: pad.buttons?.[1]?.pressed || false, + lb: pad.buttons?.[4]?.pressed || false, + rb: pad.buttons?.[5]?.pressed || false, + }, + }); + }, [handleButtonEdge, nudgeServo, registerInputState, runMacro, setAuxMotors, setDriveVector, setMode]); + + useEffect(() => { + function loop() { + pollGamepads(); + rafRef.current = requestAnimationFrame(loop); + } + rafRef.current = requestAnimationFrame(loop); + return () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [pollGamepads]); + + return null; +} diff --git a/webui/src/controls/inputs/KeyboardInputManager.jsx b/webui/src/controls/inputs/KeyboardInputManager.jsx new file mode 100644 index 00000000..21754a3d --- /dev/null +++ b/webui/src/controls/inputs/KeyboardInputManager.jsx @@ -0,0 +1,202 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useControlSystem } from '../ControlContext.jsx'; + +const SOURCE = 'keyboard'; +const ZERO_VECTOR = { x: 0, y: 0, boost: false }; +const ZERO_AUX = { main: 0, side: 0, vacuum: 0 }; + +function shouldIgnoreEvent(event) { + const target = event.target; + if (!target) return false; + const tag = target.tagName; + return ( + tag === 'INPUT' || + tag === 'TEXTAREA' || + target.isContentEditable || + tag === 'SELECT' + ); +} + +function normalizeKeymap(keymap = {}) { + const entries = Object.entries(keymap).map(([action, bindings]) => { + const values = Array.isArray(bindings) ? bindings : [bindings]; + const normalized = new Set(values.map((value) => String(value).toLowerCase())); + return [action, normalized]; + }); + return Object.fromEntries(entries); +} + +function bindingHas(bindingSet, key) { + if (!bindingSet || bindingSet.size === 0) return false; + return bindingSet.has(key); +} + +function bindingActive(bindingSet, keys) { + if (!bindingSet || bindingSet.size === 0) return false; + for (const key of keys) { + if (bindingSet.has(key)) return true; + } + return false; +} + +function computeDriveVector(keys, keymap) { + const forward = bindingActive(keymap.driveForward, keys); + const backward = bindingActive(keymap.driveBackward, keys); + const left = bindingActive(keymap.driveLeft, keys); + const right = bindingActive(keymap.driveRight, keys); + const boost = bindingActive(keymap.boostModifier, keys); + const slow = bindingActive(keymap.slowModifier, keys); + + let y = 0; + if (forward && !backward) y = 1; + else if (backward && !forward) y = -1; + + let x = 0; + if (left && !right) x = -1; + else if (right && !left) x = 1; + + const scale = slow ? 0.4 : 1; + return { + x: x * scale, + y: y * scale, + boost: boost && !slow, + }; +} + +function computeAuxMotors(keys, keymap) { + const allForward = bindingActive(keymap.auxAllForward, keys); + if (allForward) { + return { main: 127, side: 127, vacuum: 127 }; + } + const main = bindingActive(keymap.auxMainForward, keys) + ? 127 + : bindingActive(keymap.auxMainReverse, keys) + ? -127 + : 0; + const side = bindingActive(keymap.auxSideForward, keys) + ? 127 + : bindingActive(keymap.auxSideReverse, keys) + ? -70 + : 0; + const vacuum = bindingActive(keymap.auxVacuumFast, keys) + ? 127 + : bindingActive(keymap.auxVacuumSlow, keys) + ? 50 + : 0; + return { main, side, vacuum }; +} + +function vectorsEqual(a, b) { + return ( + a && + b && + a.x === b.x && + a.y === b.y && + a.boost === b.boost + ); +} + +function auxEqual(a, b) { + return a && b && a.main === b.main && a.side === b.side && a.vacuum === b.vacuum; +} + +export default function KeyboardInputManager() { + const { + state, + actions: { + setMode, + setDriveVector, + setAuxMotors, + nudgeServo, + runMacro, + stopAllMotion, + registerInputState, + }, + } = useControlSystem(); + const keymap = useMemo(() => normalizeKeymap(state.keymap), [state.keymap]); + const servoStep = Math.abs(state.camera?.config?.nudgeDegrees) || 1; + const pressedKeysRef = useRef(new Set()); + const lastVectorRef = useRef(ZERO_VECTOR); + const lastAuxRef = useRef(ZERO_AUX); + + const updateFromKeys = useCallback(() => { + const keys = pressedKeysRef.current; + const vector = computeDriveVector(keys, keymap); + const aux = computeAuxMotors(keys, keymap); + if (!vectorsEqual(vector, lastVectorRef.current)) { + lastVectorRef.current = vector; + setDriveVector(vector, { source: SOURCE }); + } + if (!auxEqual(aux, lastAuxRef.current)) { + lastAuxRef.current = aux; + setAuxMotors(aux); + } + registerInputState(SOURCE, { + keys: Array.from(keys), + vector, + aux, + }); + }, [keymap, registerInputState, setAuxMotors, setDriveVector]); + + const resetAll = useCallback(() => { + pressedKeysRef.current.clear(); + lastVectorRef.current = ZERO_VECTOR; + lastAuxRef.current = ZERO_AUX; + stopAllMotion(); + registerInputState(SOURCE, { keys: [], vector: ZERO_VECTOR, aux: ZERO_AUX }); + }, [registerInputState, stopAllMotion]); + + useEffect(() => { + function handleKeyDown(event) { + if (shouldIgnoreEvent(event)) return; + const key = event.key?.toLowerCase(); + if (!key) return; + if (pressedKeysRef.current.has(key)) return; + pressedKeysRef.current.add(key); + + if (bindingHas(keymap.driveMacro, key)) { + event.preventDefault(); + setMode('drive'); + runMacro('drive-sequence'); + } else if (bindingHas(keymap.dockMacro, key)) { + event.preventDefault(); + setMode('dock'); + runMacro('seek-dock'); + } else if (bindingHas(keymap.cameraUp, key)) { + event.preventDefault(); + nudgeServo(servoStep); + } else if (bindingHas(keymap.cameraDown, key)) { + event.preventDefault(); + nudgeServo(-servoStep); + } + + updateFromKeys(); + } + + function handleKeyUp(event) { + const key = event.key?.toLowerCase(); + if (!key || !pressedKeysRef.current.has(key)) return; + pressedKeysRef.current.delete(key); + updateFromKeys(); + } + + function handleBlur() { + resetAll(); + } + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + }; + }, [keymap, nudgeServo, resetAll, runMacro, servoStep, setMode, updateFromKeys]); + + useEffect(() => { + resetAll(); + }, [state.roverId, resetAll]); + + return null; +} diff --git a/webui/src/controls/persistence.js b/webui/src/controls/persistence.js new file mode 100644 index 00000000..4c26230d --- /dev/null +++ b/webui/src/controls/persistence.js @@ -0,0 +1,36 @@ +import { CONTROL_SETTINGS_COOKIE, CONTROL_SETTINGS_MAX_AGE } from './constants.js'; + +function parseCookieValue(raw) { + try { + const decoded = decodeURIComponent(raw ?? ''); + return JSON.parse(decoded); + } catch (error) { + console.warn('Failed to parse control settings cookie', error); // eslint-disable-line no-console + return null; + } +} + +export function loadControlSettings() { + if (typeof document === 'undefined') return null; + const cookiePrefix = `${CONTROL_SETTINGS_COOKIE}=`; + const entry = document.cookie + .split(';') + .map((part) => part.trim()) + .find((part) => part.startsWith(cookiePrefix)); + if (!entry) return null; + const raw = entry.substring(cookiePrefix.length); + return parseCookieValue(raw); +} + +export function saveControlSettings(settings) { + if (typeof document === 'undefined') return false; + try { + const serialized = encodeURIComponent(JSON.stringify(settings ?? {})); + const cookie = `${CONTROL_SETTINGS_COOKIE}=${serialized}; path=/; max-age=${CONTROL_SETTINGS_MAX_AGE}; samesite=strict`; + document.cookie = cookie; + return true; + } catch (error) { + console.warn('Failed to write control settings cookie', error); // eslint-disable-line no-console + return false; + } +} diff --git a/webui/src/hooks/useDriveControls.js b/webui/src/hooks/useDriveControls.js deleted file mode 100644 index 266deb8e..00000000 --- a/webui/src/hooks/useDriveControls.js +++ /dev/null @@ -1,340 +0,0 @@ -/* global Buffer */ - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useSocket } from '../context/SocketContext.jsx'; -import { useSession } from '../context/SessionContext.jsx'; - -const OI_COMMANDS = { - start: [128], - safe: [131], - full: [132], - passive: [128], - dock: [143], -}; - -const AUX_LIMITS = { - main: [-127, 127], - side: [-127, 127], - vacuum: [0, 127], -}; - -const COMMAND_DELAY_MS = 200; - -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -function clamp(value, min, max) { - return Math.max(min, Math.min(max, value)); -} - -function computeSpeeds(keys) { - const forward = keys.has('w'); - const backward = keys.has('s'); - const left = keys.has('a'); - const right = keys.has('d'); - const fast = keys.has('shift'); - const base = fast ? 300 : 150; - let leftSpeed = 0; - let rightSpeed = 0; - - if (forward && !backward) { - leftSpeed += base; - rightSpeed += base; - } else if (backward && !forward) { - leftSpeed -= base; - rightSpeed -= base; - } - - if (left && !right) { - leftSpeed -= base; - rightSpeed += base; - } else if (right && !left) { - leftSpeed += base; - rightSpeed -= base; - } - - if (!forward && !backward && (left || right)) { - leftSpeed = left ? -base : base; - rightSpeed = left ? base : -base; - } - - return { - left: clamp(leftSpeed, -500, 500), - right: clamp(rightSpeed, -500, 500), - }; -} - -function clampUnit(value) { - if (typeof value !== 'number' || Number.isNaN(value)) return 0; - return Math.max(-1, Math.min(1, value)); -} - -function shouldIgnoreEvent(event) { - const target = event.target; - if (!target) return false; - const tag = target.tagName; - return ( - tag === 'INPUT' || - tag === 'TEXTAREA' || - target.isContentEditable || - (tag === 'SELECT' && ['w', 'a', 's', 'd', 'Shift'].includes(event.key)) - ); -} - -function bytesToBase64(bytes) { - const binary = String.fromCharCode(...bytes); - if (typeof btoa === 'function') return btoa(binary); - if (typeof Buffer !== 'undefined') { - return Buffer.from(bytes).toString('base64'); - } - throw new Error('No base64 encoder available'); -} - -export function useDriveControls() { - const socket = useSocket(); - const { session } = useSession(); - const roverId = session?.assignment?.roverId; - const keysRef = useRef(new Set()); - const lastSpeedsRef = useRef({ left: 0, right: 0 }); - const [currentSpeeds, setCurrentSpeeds] = useState(() => ({ left: 0, right: 0 })); - const [servoAngle, setServoAngle] = useState(null); - - const emitCommand = useCallback( - (payload, cb) => { - if (!roverId) return; - socket.emit('command', { roverId, ...payload }, cb); - }, - [socket, roverId], - ); - - const currentRosterEntry = useMemo(() => { - if (!roverId || !Array.isArray(session?.roster)) return null; - return session.roster.find((entry) => String(entry.id) === String(roverId)) || null; - }, [roverId, session?.roster]); - - const servoConfig = useMemo(() => { - if (!currentRosterEntry?.cameraServo || !currentRosterEntry.cameraServo.enabled) { - return null; - } - return currentRosterEntry.cameraServo; - }, [currentRosterEntry]); - - const enableSensorStream = useCallback(() => { - if (!roverId) return; - emitCommand({ - type: 'sensorStream', - data: { sensorStream: { enable: true } }, - }); - }, [emitCommand, roverId]); - - const sendDriveUpdate = useCallback(() => { - if (!roverId) return; - const speeds = computeSpeeds(keysRef.current); - const last = lastSpeedsRef.current; - if (speeds.left === last.left && speeds.right === last.right) return; - lastSpeedsRef.current = speeds; - setCurrentSpeeds(speeds); - emitCommand({ - type: 'drive', - data: { driveDirect: speeds }, - }); - }, [emitCommand, roverId]); - - const driveWithVector = useCallback( - ({ x = 0, y = 0, boost = false } = {}) => { - if (!roverId) return; - const base = boost ? 400 : 250; - const forward = clampUnit(y) * base; - const turn = clampUnit(x) * base; - const speeds = { - left: clamp(Math.round(forward + turn), -500, 500), - right: clamp(Math.round(forward - turn), -500, 500), - }; - lastSpeedsRef.current = speeds; - setCurrentSpeeds(speeds); - emitCommand({ - type: 'drive', - data: { driveDirect: speeds }, - }); - }, - [emitCommand, roverId], - ); - - const sendMotorPwm = useCallback( - ({ main = 0, side = 0, vacuum = 0 } = {}) => { - if (!roverId) return; - const payload = { - main: clamp(main, AUX_LIMITS.main[0], AUX_LIMITS.main[1]), - side: clamp(side, AUX_LIMITS.side[0], AUX_LIMITS.side[1]), - vacuum: clamp(vacuum, AUX_LIMITS.vacuum[0], AUX_LIMITS.vacuum[1]), - }; - emitCommand({ - type: 'motors', - data: { motorPwm: payload }, - }); - }, - [emitCommand, roverId], - ); - - const stopMotors = useCallback(() => { - if (!roverId) return; - keysRef.current.clear(); - lastSpeedsRef.current = { left: 0, right: 0 }; - setCurrentSpeeds(lastSpeedsRef.current); - sendMotorPwm({ main: 0, side: 0, vacuum: 0 }); - }, [roverId, sendMotorPwm]); - - const sendOiCommand = useCallback( - (key) => { - if (!roverId) return; - const bytes = OI_COMMANDS[key]; - if (!bytes) return; - emitCommand({ - type: 'raw', - data: { raw: bytesToBase64(bytes) }, - }); - enableSensorStream(); - }, - [emitCommand, enableSensorStream, roverId], - ); - - const runStartDockFull = useCallback(async () => { - if (!roverId) return; - for (const key of ['start', 'dock', 'full']) { - sendOiCommand(key); - // brief pause so the commands aren't collapsed by the rover - await sleep(COMMAND_DELAY_MS); - } - }, [roverId, sendOiCommand]); - - const seekDock = useCallback(() => { - sendOiCommand('dock'); - }, [sendOiCommand]); - - const setAuxMotors = useCallback( - (values) => { - if (!roverId) return; - sendMotorPwm(values); - }, - [roverId, sendMotorPwm], - ); - - const sendServoCommand = useCallback( - (payload) => { - if (!roverId || !servoConfig) return; - emitCommand({ - type: 'servo', - data: { servo: payload }, - }); - }, - [emitCommand, roverId, servoConfig], - ); - - const applyServoAngle = useCallback( - (angle) => { - if (!servoConfig) return; - const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45; - const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45; - const clamped = clamp(angle, min, max); - setServoAngle(clamped); - sendServoCommand({ angle: clamped }); - }, - [sendServoCommand, servoConfig], - ); - - const nudgeServo = useCallback( - (delta) => { - if (!servoConfig) return; - const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45; - const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45; - const midpoint = (min + max) / 2; - const step = - typeof delta === 'number' && !Number.isNaN(delta) && delta !== 0 - ? delta - : servoConfig.nudgeDegrees || 1; - const baseline = - typeof servoAngle === 'number' - ? servoAngle - : servoConfig.homeAngle ?? midpoint; - applyServoAngle(baseline + step); - }, - [applyServoAngle, servoAngle, servoConfig], - ); - - useEffect(() => { - if (!servoConfig) { - setServoAngle(null); - if (roverId) { - // eslint-disable-next-line no-console - console.debug('Camera servo unavailable for rover', roverId, { - rosterEntry: currentRosterEntry, - }); - } - return; - } - const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45; - const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45; - const initial = - typeof servoConfig.homeAngle === 'number' ? servoConfig.homeAngle : (min + max) / 2; - setServoAngle(clamp(initial, min, max)); - }, [currentRosterEntry, roverId, servoConfig]); - - useEffect(() => { - if (!roverId) { - keysRef.current.clear(); - lastSpeedsRef.current = { left: 0, right: 0 }; - setCurrentSpeeds(lastSpeedsRef.current); - return undefined; - } - - function onKeyDown(event) { - if (shouldIgnoreEvent(event)) return; - const key = event.key.toLowerCase(); - if (!['w', 'a', 's', 'd', 'shift'].includes(key)) return; - if (keysRef.current.has(key)) return; - keysRef.current.add(key); - sendDriveUpdate(); - } - - function onKeyUp(event) { - const key = event.key.toLowerCase(); - if (!keysRef.current.has(key)) return; - keysRef.current.delete(key); - sendDriveUpdate(); - } - - window.addEventListener('keydown', onKeyDown); - window.addEventListener('keyup', onKeyUp); - - enableSensorStream(); - - return () => { - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - }; - }, [roverId, sendDriveUpdate, enableSensorStream]); - - return { - roverId, - speeds: currentSpeeds, - stopMotors, - sendOiCommand, - runStartDockFull, - seekDock, - setAuxMotors, - driveWithVector, - servo: { - enabled: Boolean(roverId && servoConfig), - config: servoConfig, - angle: - typeof servoAngle === 'number' - ? servoAngle - : servoConfig?.homeAngle ?? servoConfig?.minAngle ?? 0, - setAngle: applyServoAngle, - nudge: nudgeServo, - goHome: () => { - if (!servoConfig) return; - applyServoAngle(servoConfig.homeAngle ?? 0); - }, - }, - }; -}