Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
df5472a6d0 | |||
238790a2c7 | |||
7d86ad00a3 | |||
a5aa58f2a7 | |||
4059da4a20 | |||
9f57dfd770 | |||
|
5d2c600f63 | ||
|
c22fdc2ff8 | ||
|
7bcd1d34d3 | ||
c2c26c6e61 | |||
|
c8eddab032 | ||
|
c766364b43 | ||
8ffc23b503 | |||
|
8c1bcbf1d1 | ||
|
9a9a39649f | ||
|
080c07cc13 | ||
|
ab729ff378 | ||
|
6d8e3e8f54 | ||
|
9a38c5b218 | ||
|
aa29c52ee4 | ||
|
408480d9ad | ||
08efc1aa1a |
31
README.md
31
README.md
@ -1,27 +1,20 @@
|
||||
# vite-template-redux
|
||||
# Theia (Conversation Manager + Widget Element Flow for ownship-drone UI)
|
||||
|
||||
Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library) to create a modern [React](https://react.dev/) app compatible with [Create React App](https://create-react-app.dev/)
|
||||
## What Theia is
|
||||
Theia is a suite of tools that can be used to make any multi-modal adaptive UI. It works by taking in information about the outside world and user, deciding what action is best to convey the current world status to the user, and adapting the UI to accomodate the best action. Theia aims to give developers a way to create a UI that gives the user the right information at the right place at the right time.
|
||||
|
||||
## How to run
|
||||
Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library).
|
||||
|
||||
To run the app, use the following command in the src directory.
|
||||
|
||||
```sh
|
||||
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
|
||||
npm start
|
||||
```
|
||||
|
||||
## Goals
|
||||
For the Theia UI to receive and react to world events, you must also run the [World Simulator](https://git.tjdev.de/thi-sjsu-project/world-sim) <small>([GitHub mirror](https://github.com/thi-sjsu-project/world-sim))</small>.
|
||||
|
||||
- Easy migration from Create React App or Vite
|
||||
- As beginner friendly as Create React App
|
||||
- Optimized performance compared to Create React App
|
||||
- Customizable without ejecting
|
||||
## Documentation
|
||||
|
||||
## Scripts
|
||||
To find more information on Theia, its tools, and how to make a new UI, please refer to our [documentation](https://git.tjdev.de/thi-sjsu-project/theia/raw/branch/main/docs/UX%20and%20Theia%20Documentation.pdf) <small>([GitHub mirror](https://github.com/thi-sjsu-project/theia/blob/main/docs/UX%20and%20Theia%20Documentation.pdf))</small>.
|
||||
|
||||
- `dev`/`start` - start dev server and open browser
|
||||
- `build` - build for production
|
||||
- `preview` - locally preview production build
|
||||
- `test` - launch test runner
|
||||
|
||||
## Inspiration
|
||||
|
||||
- [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template)
|
||||
- [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib)
|
||||
|
BIN
docs/UX and Theia Documentation.pdf
Normal file
BIN
docs/UX and Theia Documentation.pdf
Normal file
Binary file not shown.
26
index.html
26
index.html
@ -11,7 +11,31 @@
|
||||
</head>
|
||||
<body oncontextmenu="return false;">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div style="width: 1920px; height: 1920px;" id="gaze-div">
|
||||
<div class="absolute cursor-none rounded-full ring-4 ring-blue-400 bg-blue-400 bg-opacity-20" style="z-index: 2000; width: 100px; height: 100px; display: none;" id="gaze-circle"></div>
|
||||
</div>
|
||||
<div id="root" style="margin-top: -1920px;"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
const gazeCircle = document.getElementById("gaze-circle");
|
||||
window.addEventListener("mousemove", function (ev) {
|
||||
gazeCircle.style.left = `${ev.clientX - 50}px`;
|
||||
gazeCircle.style.top = `${ev.clientY - 50}px`;
|
||||
});
|
||||
window.addEventListener("mouseout", function (ev) {
|
||||
if (ev.toElement == null && ev.relatedTarget == null) {
|
||||
gazeCircle.style.display = "none";
|
||||
}
|
||||
});
|
||||
window.addEventListener("mouseover", function (ev) {
|
||||
if (ev.toElement == null && ev.relatedTarget == null) {
|
||||
gazeCircle.style.display = "block";
|
||||
}
|
||||
});
|
||||
if (window.location.toString().indexOf("/prototype") >= 0) {
|
||||
document.getElementById("gaze-div").style.display = "none";
|
||||
document.getElementById("root").style.marginTop = "0px";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -322,7 +322,7 @@ const KEYFRAMES_MORE_INFO = [
|
||||
];
|
||||
|
||||
const FRAMETIME = 1.0 / 60.0;
|
||||
const SPEED = 0.15;
|
||||
const SPEED = 0.2;
|
||||
|
||||
const formatColor = (color: number) =>
|
||||
`#${('000000' + color.toString(16)).slice(-6)}`;
|
||||
@ -533,8 +533,8 @@ const ApproveDenyButtonElement = ({
|
||||
width: w,
|
||||
background: "linear-gradient(90deg, rgba(40, 40, 40, 0.00) 0%, rgba(76, 76, 76, 0.90) 40%, rgba(76, 76, 76, 0.90) 60%, rgba(40, 40, 40, 0.00) 100%)"
|
||||
}}></div>
|
||||
<svg style={{ width: w, height: 1.5 * w, position: "absolute", marginTop: -w * (1 + SIZES.button) }} viewBox={`0 ${-0.5 * w} ${w} ${0.5 * w}`}>
|
||||
<clipPath id="clip">
|
||||
<svg style={{ width: w, height: 1.5 * w + 1, position: "absolute", marginTop: -w * (1 + SIZES.button) - 1 }} viewBox={`0 ${-0.5 * w} ${w} ${0.5 * w}`}>
|
||||
<clipPath id="clippy">
|
||||
<rect x={0} y={0} width={w - 2} height={w * SIZES.button} rx={12} />
|
||||
</clipPath>
|
||||
|
||||
@ -557,23 +557,23 @@ const ApproveDenyButtonElement = ({
|
||||
|
||||
{moreInfoButtonActive ? <rect x={w * 0.5 * (1 - SIZES.moreInfoButton)} y={w * (SIZES.button + 0.01)} width={w * SIZES.moreInfoButton} height={w * SIZES.moreInfoButtonHeight} fill="#282828" rx={10} opacity={0.9} stroke="black" strokeWidth={2} /> : <></>}
|
||||
|
||||
<circle cx={w * 0.5} cy={w * SIZES.button * 0.5} r={state.bigCircleRadius * w * 0.5} fill="url(#bigCircleGradient)" clipPath="url(#clip)" />
|
||||
<circle cx={w * 0.5} cy={w * SIZES.button * 0.5} r={state.bigCircleRadius * w * 0.5} fill="url(#bigCircleGradient)" clipPath="url(#clippy)" />
|
||||
<circle cx={w * 0.5} cy={w * SIZES.button * 0.5} r={SIZES.tinyWhiteDot * w * 0.5} fill="#FFFFFF" opacity={state.tinyDotOpacity} />
|
||||
<circle cx={w * state.smallTurquoiseCirclePositionX} cy={w * (SIZES.button * 0.5 + state.smallTurquoiseCirclePositionY)} r={w * state.smallTurquoiseCircleRadius / 2} fill="url(#turquoiseCircleGradient)" opacity={state.smallTurquoiseCircleOpacity} clipPath={state.moreInfoClip ? 'url(#moreInfoClip)' : undefined} />
|
||||
|
||||
<polygon fill="#FFFFFF" opacity={state.denyArrowOpacity} stroke="black" stroke-width="1.5"
|
||||
<polygon fill="#FFFFFF" opacity={state.denyArrowOpacity} stroke="black" strokeWidth="1.5"
|
||||
points={`${w * (state.denyArrowPosition - 0.125 * SIZES.triangle)},${0.5 * w * SIZES.button} ` +
|
||||
`${w * (state.denyArrowPosition + 0.375 * SIZES.triangle)},${0.5 * w * (SIZES.button - SIZES.triangle)} ` +
|
||||
`${w * (state.denyArrowPosition + 0.375 * SIZES.triangle)},${0.5 * w * (SIZES.button + SIZES.triangle)}`} />
|
||||
<polygon fill="#FFFFFF" opacity={state.approveArrowOpacity} stroke="black" stroke-width="1.5"
|
||||
<polygon fill="#FFFFFF" opacity={state.approveArrowOpacity} stroke="black" strokeWidth="1.5"
|
||||
points={`${w * (state.approveArrowPosition + 0.125 * SIZES.triangle)},${0.5 * w * SIZES.button} ` +
|
||||
`${w * (state.approveArrowPosition - 0.375 * SIZES.triangle)},${0.5 * w * (SIZES.button - SIZES.triangle)} ` +
|
||||
`${w * (state.approveArrowPosition - 0.375 * SIZES.triangle)},${0.5 * w * (SIZES.button + SIZES.triangle)}`} />
|
||||
{moreInfoButtonActive ? <polygon fill="#FFFFFF" opacity={state.downArrowOpacity} stroke="black" stroke-width="1.5"
|
||||
{moreInfoButtonActive ? <polygon fill="#FFFFFF" opacity={state.downArrowOpacity} stroke="black" strokeWidth="1.5"
|
||||
points={`${0.5 * w * (1 - SIZES.triangle)},${0.5 * w * (SIZES.button + SIZES.smallTurquoiseCircle + state.downArrowPosition + 0.05)} ` +
|
||||
`${0.5 * w * (1 + SIZES.triangle)},${0.5 * w * (SIZES.button + SIZES.smallTurquoiseCircle + state.downArrowPosition + 0.05)} ` +
|
||||
`${0.5 * w},${0.5 * w * (SIZES.button + SIZES.smallTurquoiseCircle + state.downArrowPosition + 0.05 + SIZES.triangle)}`} /> : <></>}
|
||||
{element.showUpButton ? <polygon fill="#FFFFFF" opacity={state.upArrowOpacity} stroke="black" stroke-width="1.5"
|
||||
{element.showUpButton ? <polygon fill="#FFFFFF" opacity={state.upArrowOpacity} stroke="black" strokeWidth="1.5"
|
||||
points={`${0.5 * w * (1 - SIZES.triangle)},${0.5 * w * (SIZES.button - SIZES.smallTurquoiseCircle - state.upArrowPosition - 0.05)} ` +
|
||||
`${0.5 * w * (1 + SIZES.triangle)},${0.5 * w * (SIZES.button - SIZES.smallTurquoiseCircle - state.upArrowPosition - 0.05)} ` +
|
||||
`${0.5 * w},${0.5 * w * (SIZES.button - SIZES.smallTurquoiseCircle - state.upArrowPosition - 0.05 - SIZES.triangle)}`} /> : <></>}
|
||||
|
@ -59,23 +59,23 @@ const EscalationModeElement = ({
|
||||
setAnimationClass('animate-blur-away');
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (animation !== 'deny' && e.key === 'h') {
|
||||
// left mouse button
|
||||
setAnimation('deny');
|
||||
onAction?.('deny');
|
||||
} else if (animation !== 'approve' && e.key === 'l') {
|
||||
// right mouse button
|
||||
setAnimation('approve');
|
||||
onAction?.('approve');
|
||||
}
|
||||
};
|
||||
//const handleKeyPress = (e: KeyboardEvent) => {
|
||||
// if (animation !== 'deny' && e.key === 'h') {
|
||||
// // left mouse button
|
||||
// setAnimation('deny');
|
||||
// onAction?.('deny');
|
||||
// } else if (animation !== 'approve' && e.key === 'l') {
|
||||
// // right mouse button
|
||||
// setAnimation('approve');
|
||||
// onAction?.('approve');
|
||||
// }
|
||||
//};
|
||||
|
||||
window.addEventListener('keypress', handleKeyPress);
|
||||
//window.addEventListener('keypress', handleKeyPress);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keypress', handleKeyPress);
|
||||
};
|
||||
//return () => {
|
||||
// window.removeEventListener('keypress', handleKeyPress);
|
||||
//};
|
||||
}, [animation, onAction]);
|
||||
|
||||
return (
|
||||
@ -94,7 +94,7 @@ const EscalationModeElement = ({
|
||||
}}
|
||||
>
|
||||
<div className="w-1/2 pr-4">
|
||||
<div className="mb-4" style={{ width: '488px', height: '128px' }}>
|
||||
<div className="mb-4" style={{ width: '800px', height: '128px' }}>
|
||||
<div
|
||||
className="flex items-center mb-4"
|
||||
style={{ width: '800px' }}
|
||||
@ -200,10 +200,13 @@ const EscalationModeElement = ({
|
||||
style={{
|
||||
transform: 'scale(1.5)',
|
||||
marginTop: '100px',
|
||||
marginLeft: '106px',
|
||||
marginLeft: '175px',
|
||||
}}
|
||||
>
|
||||
<ApproveDenyButtonElement element={approveDenyButtonElement} />
|
||||
<ApproveDenyButtonElement element={approveDenyButtonElement} onAction={(action) => {
|
||||
onAction?.(action as "approve" | "deny")
|
||||
setAnimationClass('animate-blur-away')
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
|
||||
import { getStressLevel, updateElement } from 'src/redux/slices/cmSlice';
|
||||
import type {
|
||||
@ -8,8 +8,11 @@ import type {
|
||||
ApproveDenyButtonElement as ApproveDenyButtonElementType,
|
||||
} from 'src/types/element';
|
||||
import { capitalizeFirstLetter as cfl } from 'src/utils/helpers';
|
||||
import RequestApprovalElement from './RequestApprovalElement';
|
||||
import { getConversation } from 'src/redux/slices/conversationSlice';
|
||||
import RequestApprovalElement from 'src/components/Element/Complex/RequestApprovalElement';
|
||||
import {
|
||||
fulfillMessage,
|
||||
getConversation,
|
||||
} from 'src/redux/slices/conversationSlice';
|
||||
import { getElementsInGaze, getGazesAndKeys } from 'src/redux/slices/gazeSlice';
|
||||
import {
|
||||
getCommunication,
|
||||
@ -24,6 +27,19 @@ type Props = {
|
||||
const M_HEIGHT = 60;
|
||||
const L_HEIGHT = 488;
|
||||
|
||||
const mapStressToSize = (stressLevel: number) => {
|
||||
switch (stressLevel) {
|
||||
case 0:
|
||||
return 'L';
|
||||
case 1:
|
||||
return 'M';
|
||||
case 2:
|
||||
return 'M';
|
||||
default:
|
||||
return 'L';
|
||||
}
|
||||
};
|
||||
|
||||
const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
const stressLevel = useAppSelector(getStressLevel);
|
||||
|
||||
@ -32,6 +48,11 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
const gazesAndKeys = useAppSelector(getGazesAndKeys);
|
||||
const elementsInGaze = useAppSelector(getElementsInGaze);
|
||||
|
||||
const [respondToStressLevel, setRespondToStressLevel] = useState(true);
|
||||
const [displayState, setDisplayState] = useState<'S' | 'M' | 'L'>(
|
||||
mapStressToSize(stressLevel),
|
||||
);
|
||||
|
||||
// could also fetch messages from redux
|
||||
// provided there is a conversation number
|
||||
const [informationElement, requestApprovalElement, approveDenyButtonElement] =
|
||||
@ -50,10 +71,19 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
target = 'missile';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (respondToStressLevel) setDisplayState(mapStressToSize(stressLevel));
|
||||
}, [stressLevel, respondToStressLevel]);
|
||||
|
||||
// request video with middle mouse click
|
||||
useEffect(() => {
|
||||
if (
|
||||
gazesAndKeys.some((gk) => gk.keyPress === '1') &&
|
||||
elementsInGaze.some((element) => element.id === informationElement.id)
|
||||
elementsInGaze.some(
|
||||
(element) =>
|
||||
element.id === informationElement.id ||
|
||||
element.id === requestApprovalElement?.id,
|
||||
)
|
||||
) {
|
||||
dispatch(
|
||||
updateCommunication({
|
||||
@ -73,6 +103,32 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
informationElement,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
gazesAndKeys.some((gk) => gk.keyPress === 'KeyD') &&
|
||||
elementsInGaze.some((element) => element.id === informationElement.id)
|
||||
) {
|
||||
if (displayState !== 'L') {
|
||||
setDisplayState('L');
|
||||
setRespondToStressLevel(false);
|
||||
}
|
||||
} else if (
|
||||
gazesAndKeys.some((gk) => gk.keyPress === 'KeyE') &&
|
||||
requestApprovalElement &&
|
||||
elementsInGaze.some((element) => element.id === requestApprovalElement.id)
|
||||
) {
|
||||
setRespondToStressLevel(false);
|
||||
setDisplayState('S');
|
||||
}
|
||||
}, [
|
||||
gazesAndKeys,
|
||||
elementsInGaze,
|
||||
informationElement,
|
||||
displayState,
|
||||
requestApprovalElement,
|
||||
messages,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// react to deescalation
|
||||
if (deescalate) {
|
||||
@ -89,21 +145,50 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
|
||||
if (collapsed) return null;
|
||||
|
||||
const handleApproveOrDeny = (
|
||||
decision: 'approve' | 'deny' | 'moreInfo' | 'up',
|
||||
) => {
|
||||
// setDisplayState('M');
|
||||
// @ts-ignore
|
||||
console.log(informationElement.messageId);
|
||||
|
||||
if (decision === 'approve' || decision === 'deny') {
|
||||
dispatch(
|
||||
fulfillMessage((informationElement as InformationElement).message.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (decision === 'moreInfo') {
|
||||
dispatch(
|
||||
updateCommunication({
|
||||
...communication,
|
||||
activeConversationId: (informationElement as InformationElement)
|
||||
.message.conversationId,
|
||||
videoRequestConversationId: (informationElement as InformationElement)
|
||||
.message.conversationId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderElement = () => {
|
||||
switch (stressLevel) {
|
||||
case 0:
|
||||
switch (displayState) {
|
||||
case 'L':
|
||||
if (
|
||||
messages[0].kind === 'RequestApprovalToAttack' &&
|
||||
requestApprovalElement
|
||||
) {
|
||||
return (
|
||||
<RequestApprovalElement
|
||||
element={requestApprovalElement as RequestApprovalElementType}
|
||||
inGaze={inGaze}
|
||||
approveDenyButton={
|
||||
approveDenyButtonElement as ApproveDenyButtonElementType
|
||||
}
|
||||
/>
|
||||
<div id={requestApprovalElement.id}>
|
||||
<RequestApprovalElement
|
||||
element={requestApprovalElement as RequestApprovalElementType}
|
||||
inGaze={inGaze}
|
||||
approveDenyButton={
|
||||
approveDenyButtonElement as ApproveDenyButtonElementType
|
||||
}
|
||||
onApproveOrDeny={handleApproveOrDeny}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
@ -125,7 +210,7 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 1:
|
||||
case 'M':
|
||||
return (
|
||||
<div
|
||||
id={informationElement.id}
|
||||
@ -144,7 +229,7 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
|
||||
{inGaze ? <GazeHighlight /> : <></>}
|
||||
</div>
|
||||
);
|
||||
case 2:
|
||||
case 'S':
|
||||
return (
|
||||
<div
|
||||
id={informationElement.id}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import {
|
||||
type RequestApprovalElement as RequestApprovalElementType,
|
||||
type ApproveDenyButtonElement as ApproveDenyButtonElementType,
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from 'src/redux/slices/conversationSlice';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ApproveDenyButtonElement from './ApproveDenyButtonElement';
|
||||
import { getElementsInGaze } from 'src/redux/slices/gazeSlice';
|
||||
|
||||
type RequestApprovalProps = {
|
||||
element: RequestApprovalElementType;
|
||||
@ -22,6 +23,7 @@ type RequestApprovalProps = {
|
||||
children?: ReactNode;
|
||||
unreadCount?: number;
|
||||
approveDenyButton?: ApproveDenyButtonElementType;
|
||||
onApproveOrDeny?: (decision: 'approve' | 'deny' | 'moreInfo' | 'up') => void;
|
||||
};
|
||||
|
||||
const RequestApprovalElement = ({
|
||||
@ -30,6 +32,7 @@ const RequestApprovalElement = ({
|
||||
children,
|
||||
unreadCount,
|
||||
approveDenyButton,
|
||||
onApproveOrDeny,
|
||||
}: RequestApprovalProps) => {
|
||||
const { id, icon, messageId, conversationId } = element;
|
||||
const message = useAppSelector((state) => getMessage(state, messageId));
|
||||
@ -55,7 +58,7 @@ const RequestApprovalElement = ({
|
||||
getConversation(state, conversationId),
|
||||
);
|
||||
|
||||
const requests = conversation.messages;
|
||||
const requests = conversation?.messages;
|
||||
|
||||
// Transform threat level from a float number in a range of 0-1 to a string of low, medium, high
|
||||
const threatLevelString = (threatLevel: number) => {
|
||||
@ -74,7 +77,15 @@ const RequestApprovalElement = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleApproveDeny = (
|
||||
decision: 'approve' | 'deny' | 'moreInfo' | 'up',
|
||||
) => {
|
||||
if (onApproveOrDeny) onApproveOrDeny(decision);
|
||||
};
|
||||
|
||||
const renderMiniMapRequestApprovalElement = () => {
|
||||
if (!requests || requests.length === 0) return;
|
||||
|
||||
const mainRequest = requests[0];
|
||||
|
||||
return (
|
||||
@ -102,9 +113,9 @@ const RequestApprovalElement = ({
|
||||
x2="20"
|
||||
y2="230"
|
||||
stroke="#656566"
|
||||
stroke-width="4"
|
||||
stroke-dasharray="180,10,1,10,1,10,1"
|
||||
stroke-linecap="round"
|
||||
strokeWidth="4"
|
||||
strokeDasharray="180,10,1,10,1,10,1"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
@ -115,8 +126,8 @@ const RequestApprovalElement = ({
|
||||
x2="20"
|
||||
y2="230"
|
||||
stroke="#656566"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
@ -167,7 +178,10 @@ const RequestApprovalElement = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApproveDenyButtonElement element={approveDenyButton!} />
|
||||
<ApproveDenyButtonElement
|
||||
element={approveDenyButton!}
|
||||
onAction={handleApproveDeny}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -178,7 +192,7 @@ const RequestApprovalElement = ({
|
||||
return (
|
||||
<div
|
||||
className="rounded-full bg-white w-[35px] h-[35px] text-[#252526] flex
|
||||
items-center justify-center text-lg rounded-full"
|
||||
items-center justify-center text-lg"
|
||||
>
|
||||
{unreadCount}
|
||||
</div>
|
||||
|
@ -1,9 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { EscalationModeWidget as EscalationModeWidgetType } from 'src/types/widget';
|
||||
import type { EscalationModeElement as EscalationModeElementType } from 'src/types/element';
|
||||
import type {
|
||||
EscalationModeElement as EscalationModeElementType,
|
||||
IconElement as IconElementType,
|
||||
} from 'src/types/element';
|
||||
import EscalationModeElement from 'src/components/Element/Complex/EscalationModeElement';
|
||||
import { useAppDispatch } from 'src/redux/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
|
||||
import { fulfillMessage } from 'src/redux/slices/conversationSlice';
|
||||
import IconElement from '../Element/Simple/IconElement';
|
||||
import { getMessage } from 'src/redux/slices/conversationSlice';
|
||||
|
||||
type EscalationWidgetProps = {
|
||||
widget: EscalationModeWidgetType;
|
||||
@ -11,7 +16,14 @@ type EscalationWidgetProps = {
|
||||
|
||||
const EscalationWidget = ({ widget }: EscalationWidgetProps) => {
|
||||
const { x, y, h, w, elements } = widget;
|
||||
const [escalationModeElement] = elements;
|
||||
const [missileIconElement, escalationModeElement] = elements;
|
||||
|
||||
const missileIncomingMessage = useAppSelector((state) =>
|
||||
getMessage(
|
||||
state,
|
||||
(escalationModeElement as EscalationModeElementType)?.messageId,
|
||||
),
|
||||
);
|
||||
|
||||
const [initial, setInitial] = useState(true);
|
||||
const [animation, setAnimation] = useState<'approve' | 'deny' | undefined>(
|
||||
@ -23,10 +35,10 @@ const EscalationWidget = ({ widget }: EscalationWidgetProps) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setInitial(false), 10000); // remove slide-in after 10 seconds
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
//useEffect(() => {
|
||||
// const timer = setTimeout(() => setInitial(false), 10000); // remove slide-in after 10 seconds
|
||||
// return () => clearTimeout(timer);
|
||||
//}, []);
|
||||
|
||||
const handleAction = (action: 'approve' | 'deny') => {
|
||||
// set message to fulfilled when approved or denied
|
||||
@ -38,29 +50,39 @@ const EscalationWidget = ({ widget }: EscalationWidgetProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
top: y,
|
||||
left: x,
|
||||
width: w,
|
||||
height: h,
|
||||
zIndex: '1000',
|
||||
visibility: 'hidden',
|
||||
flexDirection: 'row',
|
||||
gap: '0px',
|
||||
}}
|
||||
className={`absolute bg-[#252526] flex gap-4 py-2 ${animationClass}`}
|
||||
>
|
||||
<EscalationModeElement
|
||||
key={escalationModeElement.id}
|
||||
element={escalationModeElement as EscalationModeElementType}
|
||||
onAction={handleAction}
|
||||
animation={animation!}
|
||||
animationClass={animationClass}
|
||||
setAnimation={setAnimation}
|
||||
setAnimationClass={setAnimationClass}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{!missileIncomingMessage?.fulfilled && (
|
||||
<div>
|
||||
<IconElement
|
||||
element={missileIconElement as IconElementType}
|
||||
className="absolute top-[350px] left-[380px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
top: 100,
|
||||
left: 550,
|
||||
width: w,
|
||||
height: h,
|
||||
zIndex: '1000',
|
||||
visibility: 'hidden',
|
||||
flexDirection: 'row',
|
||||
gap: '0px',
|
||||
}}
|
||||
className={`absolute bg-[#252526] flex gap-4 py-2 ${animationClass}`}
|
||||
>
|
||||
<EscalationModeElement
|
||||
key={escalationModeElement.id}
|
||||
element={escalationModeElement as EscalationModeElementType}
|
||||
onAction={handleAction}
|
||||
animation={animation!}
|
||||
animationClass={animationClass}
|
||||
setAnimation={setAnimation}
|
||||
setAnimationClass={setAnimationClass}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,8 +12,9 @@ import type {
|
||||
InformationElement as InformationElementType,
|
||||
RequestApprovalElement as RequestApprovalElementType,
|
||||
} from 'src/types/element';
|
||||
import MapThreatInfoElement from '../Element/Complex/MapThreatInfoElement';
|
||||
import IconElement from 'src/components/Element/Simple/IconElement';
|
||||
import MapThreatInfoElement from 'src/components/Element/Complex/MapThreatInfoElement';
|
||||
import { getMessage } from 'src/redux/slices/conversationSlice';
|
||||
|
||||
type MapWarningWidgetProps = {
|
||||
widget: MapWarningWidgetType;
|
||||
@ -31,14 +32,20 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
|
||||
approveDenyButtonElement,
|
||||
] = widget.elements;
|
||||
|
||||
const informationMessage = useAppSelector((state) =>
|
||||
getMessage(state, (threatInfoElement as InformationElementType).message.id),
|
||||
);
|
||||
|
||||
const elementsInGaze = useAppSelector(getElementsInGaze);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const warningIconInGaze = elementsInGaze.some(
|
||||
(element) => element.id === iconElement.id,
|
||||
);
|
||||
const threatInfoInGaze = elementsInGaze.some(
|
||||
(element) => element.id === threatInfoElement.id,
|
||||
const childElemsInGaze = elementsInGaze.some(
|
||||
(element) =>
|
||||
element.id === threatInfoElement.id ||
|
||||
element.id === requestApprovalElement?.id,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -64,6 +71,20 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
|
||||
widget.id,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (warningIconInGaze && requestApprovalElement?.expirationIntervalMs) {
|
||||
// update expiration even if only icon element is in gaze
|
||||
// keep displaying threat info element while we hover over the icon
|
||||
dispatch(updateElementExpiration(widget.id, requestApprovalElement.id));
|
||||
}
|
||||
}, [
|
||||
warningIconInGaze,
|
||||
dispatch,
|
||||
requestApprovalElement?.id,
|
||||
requestApprovalElement?.expirationIntervalMs,
|
||||
widget.id,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={widget.id}
|
||||
@ -77,7 +98,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
|
||||
<div className="inline-block">
|
||||
<div className="flex justify-center items-center">
|
||||
<IconElement element={iconElement as IconElementType} />
|
||||
{!threatInfoElement.collapsed && (
|
||||
{!threatInfoElement.collapsed && !informationMessage?.fulfilled && (
|
||||
<div
|
||||
style={{ height: 2, width: 75, border: '2px dashed white' }}
|
||||
className="inline-block align-[2.375rem]"
|
||||
@ -85,7 +106,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!threatInfoElement.collapsed && (
|
||||
{!threatInfoElement.collapsed && !informationMessage?.fulfilled && (
|
||||
<div className="inline-block align-top mt-2">
|
||||
<MapThreatInfoElement
|
||||
elements={[
|
||||
@ -93,7 +114,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
|
||||
requestApprovalElement as RequestApprovalElementType,
|
||||
approveDenyButtonElement as ApproveDenyButtonElementType,
|
||||
]}
|
||||
inGaze={warningIconInGaze || threatInfoInGaze}
|
||||
inGaze={childElemsInGaze}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import SortElement from '../Element/Complex/SortElement';
|
||||
import { useAppSelector } from 'src/redux/hooks';
|
||||
import { getActiveConversationId } from 'src/redux/slices/communicationSlice';
|
||||
import { getConversation } from 'src/redux/slices/conversationSlice';
|
||||
import { capitalizeFirstLetter as cfl } from 'src/utils/helpers';
|
||||
|
||||
type SortWidgetProps = {
|
||||
widget: Widget;
|
||||
@ -16,6 +17,17 @@ const PearceHeader = ({ widget }: SortWidgetProps) => {
|
||||
getConversation(state, activeConvoID),
|
||||
);
|
||||
|
||||
let title = '';
|
||||
if (activeConvo) {
|
||||
if (activeConvo.messages[0].kind === 'RequestApprovalToAttack') {
|
||||
// @ts-ignore
|
||||
title = cfl(activeConvo.messages[0].data.target.type);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
title = cfl(activeConvo.messages[0].data.target.type);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={widget.id}
|
||||
@ -29,7 +41,7 @@ const PearceHeader = ({ widget }: SortWidgetProps) => {
|
||||
}}
|
||||
>
|
||||
<span className="h-full text-6xl text-white flex flex-row items-center justify-center">
|
||||
{(activeConvo && activeConvo.id) ?? 'title'}
|
||||
{title}
|
||||
</span>
|
||||
<div
|
||||
className="absolute bg-[#1e1e1e] rounded-2xl p-1"
|
||||
|
@ -12,16 +12,10 @@ const LeftScreen = () => {
|
||||
|
||||
useGaze({ screen: '/pearce-screen' });
|
||||
|
||||
const elementsInGaze = useAppSelector(getElementsInGaze);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') e.preventDefault();
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log('elementsInGaze: ', elementsInGaze);
|
||||
// }, [elementsInGaze]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 bg-[#1E1E1E] w-[1920px] h-[1080px] hover:cursor-pointer">
|
||||
{/* Top Bar */}
|
||||
|
@ -33,7 +33,7 @@ const Minimap = () => {
|
||||
<Widget key={widgetId} widget={widgets[widgetId]} />
|
||||
))}
|
||||
</div>
|
||||
<StressLevelIndicator />
|
||||
{/* <StressLevelIndicator /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ import type {
|
||||
import type { WidgetCluster } from 'src/types/support-types';
|
||||
import { mapTargetTypeToWarningIcon } from 'src/prototype/utils/helpers';
|
||||
import {
|
||||
EXPIRATION_INTERVAL_MS,
|
||||
LIST_WIDGET_HEIGHT,
|
||||
LIST_WIDGET_WIDTH,
|
||||
} from 'src/prototype/utils/constants';
|
||||
@ -78,8 +79,6 @@ const threatDetectedMessageHigh = (message: ThreatDetected) => {
|
||||
w: 80,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
expirationIntervalMs: 3000,
|
||||
onExpiration: 'escalate',
|
||||
} satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
@ -90,7 +89,7 @@ const threatDetectedMessageHigh = (message: ThreatDetected) => {
|
||||
message,
|
||||
size: 'M', // size L when stress is low
|
||||
collapsed: true, // initially, the information elemnt is not displayed
|
||||
expirationIntervalMs: 3000,
|
||||
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
|
||||
onExpiration: 'deescalate',
|
||||
widgetId: minimapWidgetId1,
|
||||
} satisfies InformationElement,
|
||||
@ -264,17 +263,15 @@ const missileToOwnshipDetectedMessageHigh = (
|
||||
|
||||
const minimapWidgetId1 = uuid();
|
||||
const minimapElements: Element[] = [
|
||||
// {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'icon',
|
||||
// h: 128,
|
||||
// w: 128,
|
||||
// widgetId: minimapWidgetId1,
|
||||
// src: mapTargetTypeToWarningIcon('missile'),
|
||||
// expirationIntervalMs: 5000,
|
||||
// onExpiration: 'escalate',
|
||||
// } satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'icon',
|
||||
h: 128,
|
||||
w: 128,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon('missile'),
|
||||
} satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
|
@ -25,6 +25,7 @@ import type {
|
||||
import type { WidgetCluster } from 'src/types/support-types';
|
||||
import { mapTargetTypeToWarningIcon } from 'src/prototype/utils/helpers';
|
||||
import {
|
||||
EXPIRATION_INTERVAL_MS,
|
||||
LIST_WIDGET_HEIGHT,
|
||||
LIST_WIDGET_WIDTH,
|
||||
} from 'src/prototype/utils/constants';
|
||||
@ -79,8 +80,6 @@ const threatDetectedMessageLow = (message: ThreatDetected) => {
|
||||
w: 80,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
expirationIntervalMs: 3000,
|
||||
onExpiration: 'escalate',
|
||||
} satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
@ -91,34 +90,71 @@ const threatDetectedMessageLow = (message: ThreatDetected) => {
|
||||
message,
|
||||
size: 'L', // size L when stress is low
|
||||
collapsed: true, // initially, the information elemnt is not displayed
|
||||
expirationIntervalMs: 3000,
|
||||
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
|
||||
onExpiration: 'deescalate',
|
||||
widgetId: minimapWidgetId1,
|
||||
} satisfies InformationElement,
|
||||
lpdHelper.generateRequestApprovalElement(
|
||||
lpdHelper.generateBaseElement(
|
||||
uuid(),
|
||||
'visual',
|
||||
700,
|
||||
500,
|
||||
message.priority,
|
||||
),
|
||||
message.id,
|
||||
message.conversationId,
|
||||
minimapWidgetId1,
|
||||
lpdHelper.generateIconElement(
|
||||
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),
|
||||
mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
),
|
||||
lpdHelper.generateButtonElement(
|
||||
lpdHelper.generateBaseElement(uuid(), 'visual', 30, 80),
|
||||
'Deny',
|
||||
),
|
||||
lpdHelper.generateButtonElement(
|
||||
lpdHelper.generateBaseElement(uuid(), 'visual', 30, 80),
|
||||
'Approve',
|
||||
),
|
||||
),
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'request-approval',
|
||||
h: 700,
|
||||
w: 500,
|
||||
priority: message.priority,
|
||||
messageId: message.id,
|
||||
conversationId: message.conversationId,
|
||||
widgetId: minimapWidgetId1,
|
||||
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
|
||||
onExpiration: 'deescalate',
|
||||
icon: {
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'icon',
|
||||
h: 56,
|
||||
w: 56,
|
||||
src: mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
} satisfies IconElement,
|
||||
leftButton: {
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'button',
|
||||
h: 50,
|
||||
w: 30,
|
||||
text: 'Deny',
|
||||
},
|
||||
rightButton: {
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'button',
|
||||
h: 50,
|
||||
w: 30,
|
||||
text: 'Approve',
|
||||
},
|
||||
} satisfies RequestApprovalElement,
|
||||
// lpdHelper.generateRequestApprovalElement(
|
||||
// lpdHelper.generateBaseElement(
|
||||
// uuid(),
|
||||
// 'visual',
|
||||
// 700,
|
||||
// 500,
|
||||
// message.priority,
|
||||
// ),
|
||||
// message.id,
|
||||
// message.conversationId,
|
||||
// minimapWidgetId1,
|
||||
// lpdHelper.generateIconElement(
|
||||
// lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),
|
||||
// mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
// ),
|
||||
// lpdHelper.generateButtonElement(
|
||||
// lpdHelper.generateBaseElement(uuid(), 'visual', 30, 80),
|
||||
// 'Deny',
|
||||
// ),
|
||||
// lpdHelper.generateButtonElement(
|
||||
// lpdHelper.generateBaseElement(uuid(), 'visual', 30, 80),
|
||||
// 'Approve',
|
||||
// ),
|
||||
// ),
|
||||
{
|
||||
id: uuid(),
|
||||
h: 156,
|
||||
@ -279,15 +315,15 @@ const missileToOwnshipDetectedMessageLow = (
|
||||
|
||||
const minimapWidgetId1 = uuid();
|
||||
const minimapElements: Element[] = [
|
||||
// {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'icon',
|
||||
// h: 128,
|
||||
// w: 128,
|
||||
// widgetId: minimapWidgetId1,
|
||||
// src: mapTargetTypeToWarningIcon('missile'),
|
||||
// } satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'icon',
|
||||
h: 128,
|
||||
w: 128,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon('missile'),
|
||||
} satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
|
@ -25,6 +25,7 @@ import type {
|
||||
} from 'src/types/element';
|
||||
import { mapTargetTypeToWarningIcon } from 'src/prototype/utils/helpers';
|
||||
import {
|
||||
EXPIRATION_INTERVAL_MS,
|
||||
LIST_WIDGET_HEIGHT,
|
||||
LIST_WIDGET_WIDTH,
|
||||
} from 'src/prototype/utils/constants';
|
||||
@ -79,9 +80,42 @@ const threatDetectedMessageMedium = (message: ThreatDetected) => {
|
||||
w: 80,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
expirationIntervalMs: 3000,
|
||||
onExpiration: 'escalate',
|
||||
} satisfies IconElement,
|
||||
// {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'request-approval',
|
||||
// h: 700,
|
||||
// w: 500,
|
||||
// priority: message.priority,
|
||||
// messageId: message.id,
|
||||
// conversationId: message.conversationId,
|
||||
// widgetId: minimapWidgetId1,
|
||||
// icon: {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'icon',
|
||||
// h: 56,
|
||||
// w: 56,
|
||||
// src: mapTargetTypeToWarningIcon(message.data.target.type),
|
||||
// } satisfies IconElement,
|
||||
// leftButton: {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'button',
|
||||
// h: 50,
|
||||
// w: 30,
|
||||
// text: 'Deny',
|
||||
// },
|
||||
// rightButton: {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'button',
|
||||
// h: 50,
|
||||
// w: 30,
|
||||
// text: 'Approve',
|
||||
// },
|
||||
// } satisfies RequestApprovalElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
@ -91,7 +125,7 @@ const threatDetectedMessageMedium = (message: ThreatDetected) => {
|
||||
message,
|
||||
size: 'L', // size L when stress is low
|
||||
collapsed: true, // initially, the information elemnt is not displayed
|
||||
expirationIntervalMs: 3000,
|
||||
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
|
||||
onExpiration: 'deescalate',
|
||||
widgetId: minimapWidgetId1,
|
||||
} satisfies InformationElement,
|
||||
@ -263,16 +297,15 @@ const missileToOwnshipDetectedMessageMedium = (
|
||||
|
||||
const minimapWidgetId1 = uuid();
|
||||
const minimapElements: Element[] = [
|
||||
// {
|
||||
// id: uuid(),
|
||||
// modality: 'visual',
|
||||
// type: 'icon',
|
||||
// h: 128,
|
||||
// w: 128,
|
||||
// widgetId: minimapWidgetId1,
|
||||
// src: mapTargetTypeToWarningIcon('missile'),
|
||||
// onExpiration: 'escalate',
|
||||
// } satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
type: 'icon',
|
||||
h: 128,
|
||||
w: 128,
|
||||
widgetId: minimapWidgetId1,
|
||||
src: mapTargetTypeToWarningIcon('missile'),
|
||||
} satisfies IconElement,
|
||||
{
|
||||
id: uuid(),
|
||||
modality: 'visual',
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const LIST_WIDGET_HEIGHT = 850;
|
||||
export const LIST_WIDGET_WIDTH = 345;
|
||||
export const NUM_ACAS = 8;
|
||||
export const EXPIRATION_INTERVAL_MS = 5000;
|
||||
|
@ -17,7 +17,7 @@ const ownship: Widget = {
|
||||
lpdHelper.generateBaseWidget(
|
||||
ownshipUuid,
|
||||
'minimap',
|
||||
400,
|
||||
1400,
|
||||
950,
|
||||
50,
|
||||
50,
|
||||
@ -43,7 +43,7 @@ const ownship: Widget = {
|
||||
),
|
||||
0,
|
||||
1.5,
|
||||
0.2 * Math.PI, // 36deg
|
||||
-0.8 * Math.PI, // -144deg
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -13,17 +13,17 @@ const Gaze = ({ mousePosition }: GazeProps) => {
|
||||
// Don't render the gaze if the cursor is outside screen
|
||||
if (x - GAZE_RADIUS > width || y - GAZE_RADIUS > height) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`cursor-none absolute rounded-full ring-4 ring-blue-400 z-50 bg-blue-400 bg-opacity-20`}
|
||||
style={{
|
||||
width: GAZE_RADIUS * 2,
|
||||
height: GAZE_RADIUS * 2,
|
||||
top: y - GAZE_RADIUS,
|
||||
left: x - GAZE_RADIUS,
|
||||
zIndex: '2000'
|
||||
}}
|
||||
/>
|
||||
return ( <></>
|
||||
// <div
|
||||
// className={`cursor-none absolute rounded-full ring-4 ring-blue-400 z-50 bg-blue-400 bg-opacity-20`}
|
||||
// style={{
|
||||
// width: GAZE_RADIUS * 2,
|
||||
// height: GAZE_RADIUS * 2,
|
||||
// top: y - GAZE_RADIUS,
|
||||
// left: x - GAZE_RADIUS,
|
||||
// zIndex: '2000'
|
||||
// }}
|
||||
// />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit ce12c3c5617c1dd30124481e795b6791daa7f3b0
|
||||
Subproject commit e92d57ab4226c46e97bdd5164f8121db67caf808
|
@ -4,7 +4,7 @@ const config = {
|
||||
extend: {
|
||||
animation: {
|
||||
"slide-in-right": 'slide-in-right 2s ease-out forwards',
|
||||
"blur-away": 'blur-away 9s ease-out forwards',
|
||||
"blur-away": 'blur-away 2s ease-out forwards',
|
||||
},
|
||||
keyframes: {
|
||||
"slide-in-right": {
|
||||
@ -21,9 +21,6 @@ const config = {
|
||||
'0%': {
|
||||
opacity: 1,
|
||||
},
|
||||
'80%': {
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user