diff --git a/client/src/app.tsx b/client/src/app.tsx index 9c15c84..5b2aadd 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Outlet, useParams, useSearchParams } from "react-router-dom"; -import { useEffect, useState } from 'react'; +import { Suspense, useEffect, useState } from 'react'; import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; @@ -13,7 +13,7 @@ import { PageContext, PreferencesContext} from './components/infoview/context'; import UsePreferences from "./state/hooks/use_preferences" import { Navigation } from './components/navigation'; import { useSelector } from 'react-redux'; -import { changeTypewriterMode, selectOpenedIntro, selectTypewriterMode } from './state/progress'; +import { changeTypewriterMode, selectReadIntro, selectTypewriterMode } from './state/progress'; import { useAppDispatch } from './hooks'; import { Popup, PopupContext } from './components/popup/popup'; import { useGetGameInfoQuery } from './state/api'; @@ -45,7 +45,7 @@ function App() { const gameInfo = useGetGameInfoQuery({game: gameId}) const [searchParams, setSearchParams] = useSearchParams() - const openedIntro = useSelector(selectOpenedIntro(gameId)) + const readIntro = useSelector(selectReadIntro(gameId, worldId)) // mobile only: game intro should only be shown once and skipped afterwards useEffect(() => { @@ -53,14 +53,14 @@ function App() { console.log('setting page to 1') setPage(1) } else { - if (openedIntro && page == 0) { + if (readIntro && page == 0) { console.log('setting page to 1') setPage(1) } else { // setPage(0) } } - }, [openedIntro, worldId, levelId]) + }, [worldId, levelId]) // option to pass language as `?lang=de` in the URL useEffect(() => { diff --git a/client/src/components/chat.tsx b/client/src/components/chat.tsx index 4ce6602..8b734ff 100644 --- a/client/src/components/chat.tsx +++ b/client/src/components/chat.tsx @@ -1,15 +1,19 @@ import * as React from 'react' -import { PageContext, PreferencesContext } from './infoview/context' +import { ChatContext, PageContext, PreferencesContext, ProofContext } from './infoview/context' import { GameIdContext } from '../app' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from '../hooks' -import { Hint } from './hints' +import { useAppDispatch, useAppSelector } from '../hooks' import { Button } from './button' -import { changedOpenedIntro } from '../state/progress' +import { changedReadIntro, selectCompleted, selectReadIntro } from '../state/progress' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowRight } from '@fortawesome/free-solid-svg-icons' import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api' import { useContext, useEffect, useRef, useState } from 'react' +import { GameHint, InteractiveGoalsWithHints } from './infoview/rpc_api' +import Markdown from './markdown' +import { useSelector } from 'react-redux' +import { lastStepHasErrors } from './infoview/goals' + import '../css/level.css' import '../css/chat.css' @@ -18,33 +22,236 @@ function splitIntro (intro : string) { return intro.split(/\n(\s*\n)+/).filter(t => t.trim()) } -/** The buttons at the bottom of chat */ -export function ChatButtons () { +/** Helper to check if a step has any hidden hints. */ +function hasHiddenHints(step: InteractiveGoalsWithHints): boolean { + return step?.goals[0]?.hints.some((hint) => hint.hidden) +} + +/** Button which only appears if the current step has hidden hints that are not shown yet. */ +export function MoreHelpButton({selected=null} : {selected?: number}) { + + const { t } = useTranslation() + + const { proof } = React.useContext(ProofContext) + const { showHelp, setShowHelp } = React.useContext(ChatContext) + + let k = proof?.steps.length ? + ((selected === null) ? (proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)) : selected) + : 0 + + const activateHiddenHints = (ev) => { + // If the last step (`k`) has errors, we want the hidden hints from the + // second-to-last step to be affected + if (!(proof?.steps.length)) {return <>>} + + // state must not be mutated, therefore we need to clone the set + let tmp = new Set(showHelp) + if (tmp.has(k)) { + tmp.delete(k) + } else { + tmp.add(k) + } + setShowHelp(tmp) + console.debug(`help: ${Array.from(tmp.values())}`) + } + + if (hasHiddenHints(proof?.steps[k]) && !showHelp.has(k)) { + return + } +} + +/** Placeholder that takes the same space as a button. */ +function ButtonPlaceholder() { + return
+} + +/** The buttons at the bottom of chat. */ +export function ChatButtons ({counter=undefined, setCounter=()=>{}, introMessages=[]} : { + counter?: number + setCounter?: React.Dispatch+ I'm an editor +
+} diff --git a/client/src/components/game.tsx b/client/src/components/game.tsx index 45a2b31..75e6d5a 100644 --- a/client/src/components/game.tsx +++ b/client/src/components/game.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useEffect, useRef } from 'react' +import { useContext, useEffect, useRef, useState } from 'react' import Split from 'react-split' import { Box, CircularProgress } from '@mui/material' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -7,10 +7,10 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons' import { GameIdContext } from '../app' import { useAppDispatch, useAppSelector } from '../hooks' -import { changedOpenedIntro, selectOpenedIntro } from '../state/progress' +import { changeTypewriterMode, changedReadIntro, changedSelection, codeEdited, selectCode, selectReadIntro, selectSelections, selectTypewriterMode } from '../state/progress' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api' import { Button } from './button' -import { PageContext, PreferencesContext } from './infoview/context' +import { ChatContext, PageContext, PreferencesContext, ProofContext } from './infoview/context' import { InventoryPanel } from './inventory' import { ErasePopup } from './popup/erase' import { InfoPopup } from './popup/info' @@ -22,25 +22,25 @@ import { WorldTreePanel } from './world_tree' import '../css/game.css' import '../css/welcome.css' import '../css/level.css' -import { Hint } from './hints' import i18next from 'i18next' import { useTranslation } from 'react-i18next' import { LoadingIcon } from './utils' import { ChatPanel } from './chat' import { DualEditor } from './infoview/main' -import { Level } from './level' +import { Level, LevelWrapper } from './level' +import { GameHint, ProofState } from './infoview/rpc_api' +import { useSelector } from 'react-redux' +import { Diagnostic } from 'vscode-languageserver-types' /** main page of the game showing among others the tree of worlds/levels */ function Game() { - const codeviewRef = useRef{data?.descrFormat}
{levelInfo.data?.descrFormat}
{t("Crashed! Go to editor mode and fix your proof! Last server response:")}
{interimDiags.map(diag => { @@ -569,8 +566,7 @@ export function TypewriterInterface({props}) { } {mobile &&+ I'm a typewriter +
+} diff --git a/client/src/components/welcome.tsx b/client/src/components/welcome.tsx index 84bcb9e..fb6a80e 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -7,7 +7,7 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons' import { GameIdContext } from '../app' import { useAppDispatch, useAppSelector } from '../hooks' -import { changedOpenedIntro, selectOpenedIntro } from '../state/progress' +import { changedReadIntro, selectReadIntro } from '../state/progress' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api' import { Button } from './button' import { PageContext, PreferencesContext } from './infoview/context' @@ -55,7 +55,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, @@ -66,7 +66,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, /** main page of the game showing among others the tree of worlds/levels */ function Welcome() { - const {gameId} = React.useContext(GameIdContext) + const {gameId, worldId} = React.useContext(GameIdContext) // Load the namespace of the game i18next.loadNamespaces(gameId) @@ -78,13 +78,13 @@ function Welcome() { const inventory = useLoadInventoryOverviewQuery({game: gameId}) // For mobile only - const openedIntro = useAppSelector(selectOpenedIntro(gameId)) + const readIntro = useAppSelector(selectReadIntro(gameId, worldId)) const {page, setPage} = React.useContext(PageContext) - // TODO: recover `openedIntro` functionality + // TODO: recover `readIntro` functionality - // const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0) + // const [pageNumber, setPageNumber] = React.useState(readIntro ? 1 : 0) // pop-ups const [eraseMenu, setEraseMenu] = React.useState(false) diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx index 636a1a6..f4d6361 100644 --- a/client/src/components/world_tree.tsx +++ b/client/src/components/world_tree.tsx @@ -197,47 +197,6 @@ export const downloadFile = ({ data, fileName, fileType } : a.remove() } -/** The menu that is shown next to the world selection graph */ -export function WorldSelectionMenu({rulesHelp, setRulesHelp}) { - const { t, i18n } = useTranslation() - const {gameId} = React.useContext(GameIdContext) - const difficulty = useSelector(selectDifficulty(gameId)) - const dispatch = useAppDispatch() - const { mobile } = React.useContext(PreferencesContext) - - - function label(x : number) { - return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular") - } - - - return -} - export function computeWorldLayout(worlds) { let elements = [] for (let id in worlds.nodes) { @@ -360,7 +319,7 @@ export function WorldTreePanel ({visible = true}) { let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null - return