Compare commits

..

1 Commits
main ... demo7

Author SHA1 Message Date
bedlam343
bc8384ea00 Merge branch 'i-fucking-hate-svg' into demo7 2024-06-03 18:31:13 -07:00
20 changed files with 191 additions and 423 deletions

View File

@ -1,20 +1,27 @@
# Theia (Conversation Manager + Widget Element Flow for ownship-drone UI)
# vite-template-redux
## 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.
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/)
```sh
npm start
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
```
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>.
## Goals
## Documentation
- 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
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>.
## Scripts
- `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,31 +11,7 @@
</head>
<body oncontextmenu="return false;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
<div id="root"></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

@ -561,19 +561,19 @@ const ApproveDenyButtonElement = ({
<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" strokeWidth="1.5"
<polygon fill="#FFFFFF" opacity={state.denyArrowOpacity} stroke="black" stroke-width="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" strokeWidth="1.5"
<polygon fill="#FFFFFF" opacity={state.approveArrowOpacity} stroke="black" stroke-width="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" strokeWidth="1.5"
{moreInfoButtonActive ? <polygon fill="#FFFFFF" opacity={state.downArrowOpacity} stroke="black" stroke-width="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" strokeWidth="1.5"
{element.showUpButton ? <polygon fill="#FFFFFF" opacity={state.upArrowOpacity} stroke="black" stroke-width="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: '800px', height: '128px' }}>
<div className="mb-4" style={{ width: '488px', height: '128px' }}>
<div
className="flex items-center mb-4"
style={{ width: '800px' }}
@ -200,13 +200,10 @@ const EscalationModeElement = ({
style={{
transform: 'scale(1.5)',
marginTop: '100px',
marginLeft: '175px',
marginLeft: '106px',
}}
>
<ApproveDenyButtonElement element={approveDenyButtonElement} onAction={(action) => {
onAction?.(action as "approve" | "deny")
setAnimationClass('animate-blur-away')
}} />
<ApproveDenyButtonElement element={approveDenyButtonElement} />
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import { getStressLevel, updateElement } from 'src/redux/slices/cmSlice';
import type {
@ -8,11 +8,8 @@ import type {
ApproveDenyButtonElement as ApproveDenyButtonElementType,
} from 'src/types/element';
import { capitalizeFirstLetter as cfl } from 'src/utils/helpers';
import RequestApprovalElement from 'src/components/Element/Complex/RequestApprovalElement';
import {
fulfillMessage,
getConversation,
} from 'src/redux/slices/conversationSlice';
import RequestApprovalElement from './RequestApprovalElement';
import { getConversation } from 'src/redux/slices/conversationSlice';
import { getElementsInGaze, getGazesAndKeys } from 'src/redux/slices/gazeSlice';
import {
getCommunication,
@ -27,19 +24,6 @@ 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);
@ -48,11 +32,6 @@ 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] =
@ -71,19 +50,10 @@ 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 ||
element.id === requestApprovalElement?.id,
)
elementsInGaze.some((element) => element.id === informationElement.id)
) {
dispatch(
updateCommunication({
@ -103,32 +73,6 @@ 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) {
@ -145,50 +89,21 @@ 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 (displayState) {
case 'L':
switch (stressLevel) {
case 0:
if (
messages[0].kind === 'RequestApprovalToAttack' &&
requestApprovalElement
) {
return (
<div id={requestApprovalElement.id}>
<RequestApprovalElement
element={requestApprovalElement as RequestApprovalElementType}
inGaze={inGaze}
approveDenyButton={
approveDenyButtonElement as ApproveDenyButtonElementType
}
onApproveOrDeny={handleApproveOrDeny}
/>
</div>
<RequestApprovalElement
element={requestApprovalElement as RequestApprovalElementType}
inGaze={inGaze}
approveDenyButton={
approveDenyButtonElement as ApproveDenyButtonElementType
}
/>
);
} else {
return (
@ -210,7 +125,7 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
</div>
);
}
case 'M':
case 1:
return (
<div
id={informationElement.id}
@ -229,7 +144,7 @@ const MapThreatInfoElement = ({ elements, inGaze }: Props) => {
{inGaze ? <GazeHighlight /> : <></>}
</div>
);
case 'S':
case 2:
return (
<div
id={informationElement.id}

View File

@ -1,4 +1,4 @@
import { useEffect, type ReactNode } from 'react';
import { type ReactNode } from 'react';
import {
type RequestApprovalElement as RequestApprovalElementType,
type ApproveDenyButtonElement as ApproveDenyButtonElementType,
@ -15,7 +15,6 @@ 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;
@ -23,7 +22,6 @@ type RequestApprovalProps = {
children?: ReactNode;
unreadCount?: number;
approveDenyButton?: ApproveDenyButtonElementType;
onApproveOrDeny?: (decision: 'approve' | 'deny' | 'moreInfo' | 'up') => void;
};
const RequestApprovalElement = ({
@ -32,7 +30,6 @@ const RequestApprovalElement = ({
children,
unreadCount,
approveDenyButton,
onApproveOrDeny,
}: RequestApprovalProps) => {
const { id, icon, messageId, conversationId } = element;
const message = useAppSelector((state) => getMessage(state, messageId));
@ -58,7 +55,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) => {
@ -77,15 +74,7 @@ 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 (
@ -113,9 +102,9 @@ const RequestApprovalElement = ({
x2="20"
y2="230"
stroke="#656566"
strokeWidth="4"
strokeDasharray="180,10,1,10,1,10,1"
strokeLinecap="round"
stroke-width="4"
stroke-dasharray="180,10,1,10,1,10,1"
stroke-linecap="round"
/>
</svg>
) : (
@ -126,8 +115,8 @@ const RequestApprovalElement = ({
x2="20"
y2="230"
stroke="#656566"
strokeWidth="4"
strokeLinecap="round"
stroke-width="4"
stroke-linecap="round"
/>
</svg>
)}
@ -178,10 +167,7 @@ const RequestApprovalElement = ({
</div>
</div>
</div>
<ApproveDenyButtonElement
element={approveDenyButton!}
onAction={handleApproveDeny}
/>
<ApproveDenyButtonElement element={approveDenyButton!} />
</div>
);
};
@ -192,7 +178,7 @@ const RequestApprovalElement = ({
return (
<div
className="rounded-full bg-white w-[35px] h-[35px] text-[#252526] flex
items-center justify-center text-lg"
items-center justify-center text-lg rounded-full"
>
{unreadCount}
</div>

View File

@ -1,14 +1,9 @@
import { useState, useEffect } from 'react';
import type { EscalationModeWidget as EscalationModeWidgetType } from 'src/types/widget';
import type {
EscalationModeElement as EscalationModeElementType,
IconElement as IconElementType,
} from 'src/types/element';
import type { EscalationModeElement as EscalationModeElementType } from 'src/types/element';
import EscalationModeElement from 'src/components/Element/Complex/EscalationModeElement';
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import { useAppDispatch } 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;
@ -16,14 +11,7 @@ type EscalationWidgetProps = {
const EscalationWidget = ({ widget }: EscalationWidgetProps) => {
const { x, y, h, w, elements } = widget;
const [missileIconElement, escalationModeElement] = elements;
const missileIncomingMessage = useAppSelector((state) =>
getMessage(
state,
(escalationModeElement as EscalationModeElementType)?.messageId,
),
);
const [escalationModeElement] = elements;
const [initial, setInitial] = useState(true);
const [animation, setAnimation] = useState<'approve' | 'deny' | undefined>(
@ -35,10 +23,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
@ -50,39 +38,29 @@ const EscalationWidget = ({ widget }: EscalationWidgetProps) => {
};
return (
<>
{!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>
</>
<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>
);
};

View File

@ -12,9 +12,8 @@ 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 { getMessage } from 'src/redux/slices/conversationSlice';
import MapThreatInfoElement from 'src/components/Element/Complex/MapThreatInfoElement';
type MapWarningWidgetProps = {
widget: MapWarningWidgetType;
@ -32,20 +31,14 @@ 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 childElemsInGaze = elementsInGaze.some(
(element) =>
element.id === threatInfoElement.id ||
element.id === requestApprovalElement?.id,
const threatInfoInGaze = elementsInGaze.some(
(element) => element.id === threatInfoElement.id,
);
useEffect(() => {
@ -71,20 +64,6 @@ 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}
@ -98,7 +77,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
<div className="inline-block">
<div className="flex justify-center items-center">
<IconElement element={iconElement as IconElementType} />
{!threatInfoElement.collapsed && !informationMessage?.fulfilled && (
{!threatInfoElement.collapsed && (
<div
style={{ height: 2, width: 75, border: '2px dashed white' }}
className="inline-block align-[2.375rem]"
@ -106,7 +85,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
)}
</div>
</div>
{!threatInfoElement.collapsed && !informationMessage?.fulfilled && (
{!threatInfoElement.collapsed && (
<div className="inline-block align-top mt-2">
<MapThreatInfoElement
elements={[
@ -114,7 +93,7 @@ const MapWarningWidget = ({ widget }: MapWarningWidgetProps) => {
requestApprovalElement as RequestApprovalElementType,
approveDenyButtonElement as ApproveDenyButtonElementType,
]}
inGaze={childElemsInGaze}
inGaze={warningIconInGaze || threatInfoInGaze}
/>
</div>
)}

View File

@ -3,7 +3,6 @@ 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;
@ -17,17 +16,6 @@ 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}
@ -41,7 +29,7 @@ const PearceHeader = ({ widget }: SortWidgetProps) => {
}}
>
<span className="h-full text-6xl text-white flex flex-row items-center justify-center">
{title}
{(activeConvo && activeConvo.id) ?? 'title'}
</span>
<div
className="absolute bg-[#1e1e1e] rounded-2xl p-1"

View File

@ -12,10 +12,16 @@ 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,7 +24,6 @@ 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,6 +78,8 @@ const threatDetectedMessageHigh = (message: ThreatDetected) => {
w: 80,
widgetId: minimapWidgetId1,
src: mapTargetTypeToWarningIcon(message.data.target.type),
expirationIntervalMs: 3000,
onExpiration: 'escalate',
} satisfies IconElement,
{
id: uuid(),
@ -89,7 +90,7 @@ const threatDetectedMessageHigh = (message: ThreatDetected) => {
message,
size: 'M', // size L when stress is low
collapsed: true, // initially, the information elemnt is not displayed
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
expirationIntervalMs: 3000,
onExpiration: 'deescalate',
widgetId: minimapWidgetId1,
} satisfies InformationElement,
@ -263,15 +264,17 @@ const missileToOwnshipDetectedMessageHigh = (
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'),
// expirationIntervalMs: 5000,
// onExpiration: 'escalate',
// } satisfies IconElement,
{
id: uuid(),
modality: 'visual',

View File

@ -25,7 +25,6 @@ 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';
@ -80,6 +79,8 @@ const threatDetectedMessageLow = (message: ThreatDetected) => {
w: 80,
widgetId: minimapWidgetId1,
src: mapTargetTypeToWarningIcon(message.data.target.type),
expirationIntervalMs: 3000,
onExpiration: 'escalate',
} satisfies IconElement,
{
id: uuid(),
@ -90,71 +91,34 @@ const threatDetectedMessageLow = (message: ThreatDetected) => {
message,
size: 'L', // size L when stress is low
collapsed: true, // initially, the information elemnt is not displayed
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
expirationIntervalMs: 3000,
onExpiration: 'deescalate',
widgetId: minimapWidgetId1,
} satisfies InformationElement,
{
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',
// ),
// ),
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,
@ -315,15 +279,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,7 +25,6 @@ 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';
@ -80,42 +79,9 @@ 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',
@ -125,7 +91,7 @@ const threatDetectedMessageMedium = (message: ThreatDetected) => {
message,
size: 'L', // size L when stress is low
collapsed: true, // initially, the information elemnt is not displayed
expirationIntervalMs: EXPIRATION_INTERVAL_MS,
expirationIntervalMs: 3000,
onExpiration: 'deescalate',
widgetId: minimapWidgetId1,
} satisfies InformationElement,
@ -297,15 +263,16 @@ const missileToOwnshipDetectedMessageMedium = (
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'),
// onExpiration: 'escalate',
// } satisfies IconElement,
{
id: uuid(),
modality: 'visual',

View File

@ -1,4 +1,3 @@
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',
1400,
400,
950,
50,
50,
@ -43,7 +43,7 @@ const ownship: Widget = {
),
0,
1.5,
-0.8 * Math.PI, // -144deg
0.2 * Math.PI, // 36deg
),
};

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 e92d57ab4226c46e97bdd5164f8121db67caf808
Subproject commit ce12c3c5617c1dd30124481e795b6791daa7f3b0

View File

@ -4,7 +4,7 @@ const config = {
extend: {
animation: {
"slide-in-right": 'slide-in-right 2s ease-out forwards',
"blur-away": 'blur-away 2s ease-out forwards',
"blur-away": 'blur-away 9s ease-out forwards',
},
keyframes: {
"slide-in-right": {
@ -21,6 +21,9 @@ const config = {
'0%': {
opacity: 1,
},
'80%': {
opacity: 1,
},
'100%': {
opacity: 0,
},