Compare commits

...

22 Commits
demo7 ... main

Author SHA1 Message Date
df5472a6d0 fix links in readme 2024-06-30 22:19:32 +02:00
238790a2c7 docs in readme 2024-06-30 13:12:47 -07:00
7d86ad00a3 docs 2024-06-30 13:11:54 -07:00
a5aa58f2a7 Update README.md 2024-06-30 21:25:43 +02:00
4059da4a20 Update README.md 2024-06-30 21:09:05 +02:00
9f57dfd770 ownship now near missile 2024-06-04 13:38:25 +02:00
bedlam343
5d2c600f63 Merge branch 'final-demo' of https://git.tjdev.de/thi-sjsu-project/react-redux-app into final-demo 2024-06-04 03:01:26 -07:00
bedlam343
c22fdc2ff8 missile incoming icon 2024-06-04 03:01:23 -07:00
Polfish
7bcd1d34d3
comment out stress level indicator from minimap 2024-06-04 02:57:49 -07:00
c2c26c6e61 please dont look at this too closely 2024-06-04 11:47:06 +02:00
bedlam343
c8eddab032 e and d to toggle info elements on minimap. J to request video 2024-06-04 02:15:01 -07:00
bedlam343
c766364b43 collapse on approve/deny. Fullfill on approve/deny 2024-06-04 02:04:11 -07:00
8ffc23b503 actually close escalation widget after approving/denying 2024-06-04 09:49:03 +02:00
bedlam343
8c1bcbf1d1 Merge branch 'i-fucking-hate-svg' into final-demo 2024-06-03 18:34:17 -07:00
bedlam343
9a9a39649f changed 2024-06-03 18:28:40 -07:00
bedlam343
080c07cc13 fix escalation mode 2024-06-03 16:16:15 -07:00
bedlam343
ab729ff378 fiannly done 2024-06-03 09:32:46 -07:00
bedlam343
6d8e3e8f54 done done done done done 2024-06-03 09:31:47 -07:00
bedlam343
9a38c5b218 a more meaningul title on pearce header 2024-06-03 09:27:49 -07:00
bedlam343
aa29c52ee4 bug fix 2024-06-03 09:15:11 -07:00
bedlam343
408480d9ad manually switch from l to m states 2024-06-03 09:00:49 -07:00
08efc1aa1a i fucking hate svg 2024-06-03 11:28:01 +02:00
20 changed files with 427 additions and 195 deletions

View File

@ -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)

Binary file not shown.

View File

@ -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>

View File

@ -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)}`} /> : <></>}

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>
</>
);
};

View File

@ -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>
)}

View File

@ -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"

View File

@ -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 */}

View File

@ -33,7 +33,7 @@ const Minimap = () => {
<Widget key={widgetId} widget={widgets[widgetId]} />
))}
</div>
<StressLevelIndicator />
{/* <StressLevelIndicator /> */}
</>
);
};

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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;

View File

@ -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
),
};

View File

@ -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

View File

@ -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,
},