@wandelbots/wandelbots-js-react-components 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,8 +1,8 @@
1
- import type { SafetySetupSafetyZone } from "@wandelbots/wandelbots-js"
1
+ import { type GroupProps } from "@react-three/fiber"
2
2
  import type { Geometry } from "@wandelbots/wandelbots-api-client"
3
+ import type { SafetySetupSafetyZone } from "@wandelbots/wandelbots-js"
3
4
  import * as THREE from "three"
4
5
  import { ConvexGeometry } from "three-stdlib"
5
- import { type GroupProps } from "@react-three/fiber"
6
6
 
7
7
  export type SafetyZonesRendererProps = {
8
8
  safetyZones: SafetySetupSafetyZone[]
@@ -89,21 +89,19 @@ export function SafetyZonesRenderer({
89
89
  return null
90
90
  }
91
91
  return (
92
- <>
93
- <mesh key={`${index}-${i}`} geometry={convexGeometry}>
94
- <meshStandardMaterial
95
- key={index}
96
- attach="material"
97
- color="#009f4d"
98
- opacity={0.2}
99
- depthTest={false}
100
- depthWrite={false}
101
- transparent
102
- polygonOffset
103
- polygonOffsetFactor={-i}
104
- />
105
- </mesh>
106
- </>
92
+ <mesh key={`${index}-${i}`} geometry={convexGeometry}>
93
+ <meshStandardMaterial
94
+ key={index}
95
+ attach="material"
96
+ color="#009f4d"
97
+ opacity={0.2}
98
+ depthTest={false}
99
+ depthWrite={false}
100
+ transparent
101
+ polygonOffset
102
+ polygonOffsetFactor={-i}
103
+ />
104
+ </mesh>
107
105
  )
108
106
  })
109
107
  })}
@@ -13,7 +13,6 @@ import YAxisIcon from "../../icons/axis-y.svg"
13
13
  import ZAxisIcon from "../../icons/axis-z.svg"
14
14
  import RotationIcon from "../../icons/rotation.svg"
15
15
  import { useReaction } from "../utils/hooks"
16
- import { JoggingActivationRequired } from "./JoggingActivationRequired"
17
16
  import { JoggingCartesianAxisControl } from "./JoggingCartesianAxisControl"
18
17
  import { JoggingJointLimitDetector } from "./JoggingJointLimitDetector"
19
18
  import { JoggingOptions } from "./JoggingOptions"
@@ -67,11 +66,11 @@ export const JoggingCartesianTab = observer(
67
66
  opts: JoggingCartesianOpts,
68
67
  increment: DiscreteIncrementOption,
69
68
  ) {
70
- const tcpPose =
71
- store.jogger.motionStream.rapidlyChangingMotionState.tcp_pose
69
+ const jogger = await store.activate()
70
+
71
+ const tcpPose = jogger.motionStream.rapidlyChangingMotionState.tcp_pose
72
72
  const jointPosition =
73
- store.jogger.motionStream.rapidlyChangingMotionState.state
74
- .joint_position
73
+ jogger.motionStream.rapidlyChangingMotionState.state.joint_position
75
74
  if (!tcpPose) return
76
75
 
77
76
  await store.withMotionLock(async () => {
@@ -100,6 +99,7 @@ export const JoggingCartesianTab = observer(
100
99
  })
101
100
  } finally {
102
101
  store.setCurrentIncrementJog(null)
102
+ await store.deactivate()
103
103
  }
104
104
  })
105
105
  }
@@ -107,6 +107,7 @@ export const JoggingCartesianTab = observer(
107
107
  async function startCartesianJogging(opts: JoggingCartesianOpts) {
108
108
  if (store.isLocked) return
109
109
 
110
+ const jogger = await store.activate()
110
111
  if (store.activeDiscreteIncrement) {
111
112
  return runIncrementalCartesianJog(opts, store.activeDiscreteIncrement)
112
113
  }
@@ -133,7 +134,7 @@ export const JoggingCartesianTab = observer(
133
134
  return
134
135
  }
135
136
 
136
- await store.jogger.stop()
137
+ await store.deactivate()
137
138
  }
138
139
 
139
140
  const axisList = [
@@ -183,109 +184,112 @@ export const JoggingCartesianTab = observer(
183
184
  justifyContent="center"
184
185
  sx={{ flexGrow: "1" }}
185
186
  >
186
- {/* Translate or rotate toggle */}
187
- <JoggingToggleButtonGroup
188
- value={store.selectedCartesianMotionType}
189
- onChange={onMotionTypeChange}
190
- exclusive
191
- aria-label={t("Jogging.Cartesian.MotionType.lb")}
192
- sx={{ justifyContent: "center" }}
187
+ <Stack
188
+ alignItems="center"
189
+ justifyContent="center"
190
+ gap="24px"
191
+ sx={{ flexGrow: 1 }}
193
192
  >
194
- <ToggleButton value="translate">
195
- {t("Jogging.Cartesian.Translation.bt")}
196
- </ToggleButton>
197
- <ToggleButton value="rotate">
198
- {t("Jogging.Cartesian.Rotation.bt")}
199
- </ToggleButton>
200
- </JoggingToggleButtonGroup>
193
+ {/* Translate or rotate toggle */}
194
+ <JoggingToggleButtonGroup
195
+ value={store.selectedCartesianMotionType}
196
+ onChange={onMotionTypeChange}
197
+ exclusive
198
+ aria-label={t("Jogging.Cartesian.MotionType.lb")}
199
+ sx={{ justifyContent: "center" }}
200
+ >
201
+ <ToggleButton value="translate">
202
+ {t("Jogging.Cartesian.Translation.bt")}
203
+ </ToggleButton>
204
+ <ToggleButton value="rotate">
205
+ {t("Jogging.Cartesian.Rotation.bt")}
206
+ </ToggleButton>
207
+ </JoggingToggleButtonGroup>
201
208
 
202
- <JoggingActivationRequired store={store}>
203
- <Stack alignItems="center" gap="24px" sx={{ flexGrow: 1 }}>
204
- {/* Cartesian translate jogging */}
205
- {store.selectedCartesianMotionType === "translate" &&
206
- axisList.map((axis) => (
207
- <JoggingCartesianAxisControl
208
- key={axis.id}
209
- colors={axis.colors}
210
- disabled={store.isLocked}
211
- activeJoggingDirection={
212
- store.incrementJogInProgress?.axis === axis.id
213
- ? store.incrementJogInProgress.direction
214
- : undefined
215
- }
216
- label={
217
- <>
218
- {axis.icon}
219
- <Typography
220
- sx={{
221
- fontSize: "24px",
222
- color: theme.palette.text.primary,
223
- }}
224
- >
225
- {axis.id.toUpperCase()}
226
- </Typography>
227
- </>
228
- }
229
- getDisplayedValue={() =>
230
- formatMM(
231
- store.jogger.motionStream.rapidlyChangingMotionState
232
- .tcp_pose?.position[axis.id] || 0,
233
- )
234
- }
235
- startJogging={(direction: "-" | "+") =>
236
- startCartesianJogging({
237
- axis: axis.id,
238
- motionType: "translate",
239
- direction,
240
- })
241
- }
242
- stopJogging={stopJogging}
243
- />
244
- ))}
209
+ {/* Cartesian translate jogging */}
210
+ {store.selectedCartesianMotionType === "translate" &&
211
+ axisList.map((axis) => (
212
+ <JoggingCartesianAxisControl
213
+ key={axis.id}
214
+ colors={axis.colors}
215
+ disabled={store.isLocked}
216
+ activeJoggingDirection={
217
+ store.incrementJogInProgress?.axis === axis.id
218
+ ? store.incrementJogInProgress.direction
219
+ : undefined
220
+ }
221
+ label={
222
+ <>
223
+ {axis.icon}
224
+ <Typography
225
+ sx={{
226
+ fontSize: "24px",
227
+ color: theme.palette.text.primary,
228
+ }}
229
+ >
230
+ {axis.id.toUpperCase()}
231
+ </Typography>
232
+ </>
233
+ }
234
+ getDisplayedValue={() =>
235
+ formatMM(
236
+ store.jogger.motionStream.rapidlyChangingMotionState
237
+ .tcp_pose?.position[axis.id] || 0,
238
+ )
239
+ }
240
+ startJogging={(direction: "-" | "+") =>
241
+ startCartesianJogging({
242
+ axis: axis.id,
243
+ motionType: "translate",
244
+ direction,
245
+ })
246
+ }
247
+ stopJogging={stopJogging}
248
+ />
249
+ ))}
245
250
 
246
- {/* Cartesian rotate jogging */}
247
- {store.selectedCartesianMotionType === "rotate" &&
248
- axisList.map((axis) => (
249
- <JoggingCartesianAxisControl
250
- key={axis.id}
251
- colors={axis.colors}
252
- disabled={store.isLocked}
253
- activeJoggingDirection={
254
- store.incrementJogInProgress?.axis === axis.id
255
- ? store.incrementJogInProgress.direction
256
- : undefined
257
- }
258
- label={
259
- <>
260
- <RotationIcon />
261
- <Typography
262
- sx={{
263
- fontSize: "24px",
264
- color: theme.palette.text.primary,
265
- }}
266
- >
267
- {axis.id.toUpperCase()}
268
- </Typography>
269
- </>
270
- }
271
- getDisplayedValue={() =>
272
- formatDegrees(
273
- store.jogger.motionStream.rapidlyChangingMotionState
274
- .tcp_pose?.orientation?.[axis.id] || 0,
275
- )
276
- }
277
- startJogging={(direction: "-" | "+") =>
278
- startCartesianJogging({
279
- axis: axis.id,
280
- motionType: "rotate",
281
- direction,
282
- })
283
- }
284
- stopJogging={stopJogging}
285
- />
286
- ))}
287
- </Stack>
288
- </JoggingActivationRequired>
251
+ {/* Cartesian rotate jogging */}
252
+ {store.selectedCartesianMotionType === "rotate" &&
253
+ axisList.map((axis) => (
254
+ <JoggingCartesianAxisControl
255
+ key={axis.id}
256
+ colors={axis.colors}
257
+ disabled={store.isLocked}
258
+ activeJoggingDirection={
259
+ store.incrementJogInProgress?.axis === axis.id
260
+ ? store.incrementJogInProgress.direction
261
+ : undefined
262
+ }
263
+ label={
264
+ <>
265
+ <RotationIcon />
266
+ <Typography
267
+ sx={{
268
+ fontSize: "24px",
269
+ color: theme.palette.text.primary,
270
+ }}
271
+ >
272
+ {axis.id.toUpperCase()}
273
+ </Typography>
274
+ </>
275
+ }
276
+ getDisplayedValue={() =>
277
+ formatDegrees(
278
+ store.jogger.motionStream.rapidlyChangingMotionState
279
+ .tcp_pose?.orientation?.[axis.id] || 0,
280
+ )
281
+ }
282
+ startJogging={(direction: "-" | "+") =>
283
+ startCartesianJogging({
284
+ axis: axis.id,
285
+ motionType: "rotate",
286
+ direction,
287
+ })
288
+ }
289
+ stopJogging={stopJogging}
290
+ />
291
+ ))}
292
+ </Stack>
289
293
  </Stack>
290
294
 
291
295
  {/* Show message if joint limits reached */}
@@ -136,7 +136,7 @@ export const JoggingJointRotationControl = externalizeComponent(
136
136
  >
137
137
  <ChevronLeft
138
138
  sx={{
139
- "pointer-events": "none",
139
+ pointerEvents: "none",
140
140
  }}
141
141
  />
142
142
  </IconButton>
@@ -231,7 +231,7 @@ export const JoggingJointRotationControl = externalizeComponent(
231
231
  >
232
232
  <ChevronRight
233
233
  sx={{
234
- "pointer-events": "none",
234
+ pointerEvents: "none",
235
235
  }}
236
236
  />
237
237
  </IconButton>
@@ -2,7 +2,6 @@ import { Divider, Stack } from "@mui/material"
2
2
  import { radiansToDegrees } from "@wandelbots/wandelbots-js"
3
3
  import { observer } from "mobx-react-lite"
4
4
  import type { ReactNode } from "react"
5
- import { JoggingActivationRequired } from "./JoggingActivationRequired"
6
5
  import { JoggingJointLimitDetector } from "./JoggingJointLimitDetector"
7
6
  import { JoggingJointRotationControl } from "./JoggingJointRotationControl"
8
7
  import type { JoggingStore } from "./JoggingStore"
@@ -14,6 +13,8 @@ export const JoggingJointTab = observer(
14
13
  joint: number
15
14
  direction: "-" | "+"
16
15
  }) {
16
+ await store.activate()
17
+
17
18
  await store.jogger.startJointRotation({
18
19
  joint: opts.joint,
19
20
  direction: opts.direction,
@@ -36,46 +37,44 @@ export const JoggingJointTab = observer(
36
37
  sx={{ flexGrow: "1" }}
37
38
  id="JointControls"
38
39
  >
39
- <JoggingActivationRequired store={store}>
40
- <Stack alignItems="center" gap="24px" sx={{ flexGrow: 1 }}>
41
- {store.jogger.motionStream.joints.map((joint) => {
42
- const jointLimits =
43
- store.motionGroupSpec.mechanical_joint_limits?.[joint.index]
44
- const lowerLimitDegs =
45
- jointLimits?.lower_limit !== undefined
46
- ? radiansToDegrees(jointLimits.lower_limit)
47
- : undefined
48
- const upperLimitDegs =
49
- jointLimits?.upper_limit !== undefined
50
- ? radiansToDegrees(jointLimits.upper_limit)
51
- : undefined
40
+ <Stack alignItems="center" gap="24px">
41
+ {store.jogger.motionStream.joints.map((joint) => {
42
+ const jointLimits =
43
+ store.motionGroupSpec.mechanical_joint_limits?.[joint.index]
44
+ const lowerLimitDegs =
45
+ jointLimits?.lower_limit !== undefined
46
+ ? radiansToDegrees(jointLimits.lower_limit)
47
+ : undefined
48
+ const upperLimitDegs =
49
+ jointLimits?.upper_limit !== undefined
50
+ ? radiansToDegrees(jointLimits.upper_limit)
51
+ : undefined
52
52
 
53
- return (
54
- <JoggingJointRotationControl
55
- key={`joint-${joint.index}`}
56
- disabled={store.isLocked}
57
- lowerLimitDegs={lowerLimitDegs}
58
- upperLimitDegs={upperLimitDegs}
59
- getValueDegs={() => {
60
- const value =
61
- store.jogger.motionStream.rapidlyChangingMotionState
62
- .state.joint_position.joints[joint.index]
63
- return value !== undefined
64
- ? radiansToDegrees(value)
65
- : undefined
66
- }}
67
- startJogging={(direction: "-" | "+") =>
68
- startJointJogging({
69
- joint: joint.index,
70
- direction,
71
- })
72
- }
73
- stopJogging={stopJointJogging}
74
- />
75
- )
76
- })}
77
- </Stack>
78
- </JoggingActivationRequired>
53
+ return (
54
+ <JoggingJointRotationControl
55
+ key={`joint-${joint.index}`}
56
+ disabled={store.isLocked}
57
+ lowerLimitDegs={lowerLimitDegs}
58
+ upperLimitDegs={upperLimitDegs}
59
+ getValueDegs={() => {
60
+ const value =
61
+ store.jogger.motionStream.rapidlyChangingMotionState.state
62
+ .joint_position.joints[joint.index]
63
+ return value !== undefined
64
+ ? radiansToDegrees(value)
65
+ : undefined
66
+ }}
67
+ startJogging={(direction: "-" | "+") =>
68
+ startJointJogging({
69
+ joint: joint.index,
70
+ direction,
71
+ })
72
+ }
73
+ stopJogging={stopJointJogging}
74
+ />
75
+ )
76
+ })}
77
+ </Stack>
79
78
  </Stack>
80
79
  <JoggingJointLimitDetector store={store} />
81
80
 
@@ -6,7 +6,6 @@ import { observer, useLocalObservable } from "mobx-react-lite"
6
6
  import { useEffect } from "react"
7
7
  import { externalizeComponent } from "../../externalizeComponent"
8
8
  import { LoadingCover } from "../LoadingCover"
9
- import { useReaction } from "../utils/hooks"
10
9
  import { JoggingCartesianTab } from "./JoggingCartesianTab"
11
10
  import { JoggingJointTab } from "./JoggingJointTab"
12
11
  import { JoggingStore } from "./JoggingStore"
@@ -121,38 +120,6 @@ const JoggingPanelInner = observer(
121
120
  children?: React.ReactNode
122
121
  childrenJoint?: React.ReactNode
123
122
  }) => {
124
- // Jogger is only active as long as the tab is focused
125
- useEffect(() => {
126
- function deactivate() {
127
- store.deactivate()
128
- }
129
-
130
- function activate() {
131
- store.activate()
132
- }
133
-
134
- window.addEventListener("blur", deactivate)
135
- window.addEventListener("focus", activate)
136
-
137
- return () => {
138
- window.removeEventListener("blur", deactivate)
139
- window.removeEventListener("focus", activate)
140
- }
141
- })
142
-
143
- // Update jogging mode on jogger based on user selections
144
- useReaction(
145
- () => [
146
- store.currentTab.id,
147
- store.selectedTcpId,
148
- store.activeCoordSystemId,
149
- store.activeDiscreteIncrement,
150
- ],
151
- () => {
152
- if (store.activationState !== "inactive") store.activate()
153
- },
154
- )
155
-
156
123
  function renderTabContent() {
157
124
  if (store.currentTab.id === "cartesian") {
158
125
  return (
@@ -8,12 +8,7 @@ import { tryParseJson } from "@wandelbots/wandelbots-js"
8
8
  import { countBy } from "lodash-es"
9
9
  import keyBy from "lodash-es/keyBy"
10
10
  import uniqueId from "lodash-es/uniqueId"
11
- import {
12
- autorun,
13
- makeAutoObservable,
14
- runInAction,
15
- type IReactionDisposer,
16
- } from "mobx"
11
+ import { autorun, makeAutoObservable, type IReactionDisposer } from "mobx"
17
12
 
18
13
  const discreteIncrementOptions = [
19
14
  { id: "0.1", mm: 0.1, degrees: 0.05 },
@@ -44,12 +39,6 @@ export type IncrementJogInProgress = {
44
39
  export class JoggingStore {
45
40
  selectedTabId: "cartesian" | "joint" | "debug" = "cartesian"
46
41
 
47
- /**
48
- * Whether the user must manually interact to activate jogging, or
49
- * if it can be done automatically
50
- */
51
- manualActivationRequired: boolean = true
52
-
53
42
  /**
54
43
  * State of the jogging panel. Starts as "inactive"
55
44
  */
@@ -141,6 +130,18 @@ export class JoggingStore {
141
130
  ),
142
131
  ])
143
132
 
133
+ // Setting mode control makes jogging startup slightly faster
134
+ // on physical robots
135
+ // https://wandelbots.slack.com/archives/C06VA4J59PF/p1725523765976109?thread_ts=1725464963.859559&cid=C06VA4J59PF
136
+ try {
137
+ await jogger.nova.api.controller.setDefaultMode(
138
+ jogger.motionStream.controllerId,
139
+ "MODE_CONTROL",
140
+ )
141
+ } catch (err) {
142
+ console.error(err)
143
+ }
144
+
144
145
  return new JoggingStore(
145
146
  jogger,
146
147
  motionGroupSpec,
@@ -187,54 +188,18 @@ export class JoggingStore {
187
188
  return countBy(this.coordSystems, (cs) => cs.name)
188
189
  }
189
190
 
190
- async deactivate(opts: { requireManualReactivation?: boolean } = {}) {
191
- if (this.activationState === "inactive") return
191
+ async deactivate() {
192
192
  const websocket = this.jogger.activeWebsocket
193
193
 
194
- this.activationState = "inactive"
195
194
  this.jogger.setJoggingMode("increment")
196
195
 
197
196
  if (websocket) {
198
197
  await websocket.closed()
199
198
  }
200
-
201
- // Closing websocket sometimes isn't enough to stop interference
202
- try {
203
- await this.jogger.nova.api.motionGroupJogging.stopJogging(
204
- this.jogger.motionGroupId,
205
- )
206
- } catch (err) {
207
- console.error(err)
208
- }
209
-
210
- if (opts.requireManualReactivation) {
211
- runInAction(() => {
212
- this.manualActivationRequired = true
213
- })
214
- }
215
199
  }
216
200
 
217
201
  /** Activate the jogger with current settings */
218
- async activate(opts: { manual?: boolean } = {}) {
219
- if (this.manualActivationRequired && !opts.manual) return
220
-
221
- runInAction(() => {
222
- this.activationState = "loading"
223
- this.activationError = null
224
- })
225
-
226
- // Setting mode control makes jogging startup slightly faster
227
- // on physical robots
228
- // https://wandelbots.slack.com/archives/C06VA4J59PF/p1725523765976109?thread_ts=1725464963.859559&cid=C06VA4J59PF
229
- try {
230
- await this.jogger.nova.api.controller.setDefaultMode(
231
- this.jogger.motionStream.controllerId,
232
- "MODE_CONTROL",
233
- )
234
- } catch (err) {
235
- console.error(err)
236
- }
237
-
202
+ async activate() {
238
203
  if (this.currentTab.id === "cartesian") {
239
204
  const cartesianJoggingOpts = {
240
205
  tcpId: this.selectedTcpId,
@@ -250,25 +215,7 @@ export class JoggingStore {
250
215
  this.jogger.setJoggingMode("joint")
251
216
  }
252
217
 
253
- if (this.jogger.activeWebsocket) {
254
- try {
255
- this.jogger.stop()
256
- await this.jogger.activeWebsocket.nextMessage()
257
- } catch (err) {
258
- runInAction(() => {
259
- this.activationState = "inactive"
260
- this.activationError = err
261
- })
262
- return
263
- }
264
- }
265
-
266
- runInAction(() => {
267
- this.activationState = "active"
268
- if (opts.manual) {
269
- this.manualActivationRequired = false
270
- }
271
- })
218
+ return this.jogger
272
219
  }
273
220
 
274
221
  loadFromLocalStorage() {
@@ -10,8 +10,6 @@
10
10
  "Jogging.Joints.JointValues.lb": "Gelenkwerte",
11
11
  "Jogging.Increment.Continuous.dd": "Fortlaufend",
12
12
  "Jogging.Cartesian.Orientation.lb": "Orientierung",
13
- "Jogging.Activate.bt": "Jogging aktivieren",
14
- "Jogging.Activating.lb": "Jogging wird aktiviert",
15
13
  "Jogging.JointLimitsReached.lb": "Joint-Limit für Joint {{jointNumbers}} erreicht",
16
14
  "Jogging.Orientation.coordsys": "Base",
17
15
  "Jogging.Orientation.tool": "Tool"
@@ -10,7 +10,6 @@
10
10
  "Jogging.Joints.JointValues.lb": "Joint values",
11
11
  "Jogging.Increment.Continuous.dd": "Continuous",
12
12
  "Jogging.Cartesian.Orientation.lb": "Orientation",
13
- "Jogging.Activate.bt": "Activate jogging",
14
13
  "Jogging.Activating.lb": "Activating jogging",
15
14
  "Jogging.JointLimitsReached.lb": "Joint limit reached for joint {{jointNumbers}}",
16
15
  "Jogging.Orientation.coordsys": "Base",
@@ -1,9 +0,0 @@
1
- import type React from "react";
2
- import type { JoggingStore } from "./JoggingStore";
3
- export declare const JoggingActivationRequired: (({ store, children }: {
4
- store: JoggingStore;
5
- children: React.ReactNode;
6
- }) => import("react/jsx-runtime").JSX.Element) & {
7
- displayName: string;
8
- };
9
- //# sourceMappingURL=JoggingActivationRequired.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"JoggingActivationRequired.d.ts","sourceRoot":"","sources":["../../../src/components/jogging/JoggingActivationRequired.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,eAAO,MAAM,yBAAyB,yBACd;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE;;CA8CzE,CAAA"}