diff --git a/client/src/components/LeftPanel.tsx b/client/src/components/LeftPanel.tsx index b46ddb3..9457b38 100644 --- a/client/src/components/LeftPanel.tsx +++ b/client/src/components/LeftPanel.tsx @@ -71,12 +71,12 @@ function LemmaDocs({ lemmas }) { function LeftPanel({ spells, inventory }) { return ( - {spells.length > 0 && + {spells && spells.length > 0 && Spell book {spells.map((spell) => )} } - {inventory.length > 0 && } + {inventory && inventory.length > 0 && } ) } diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index f42efcc..9ecad39 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -7,12 +7,10 @@ import '@fontsource/roboto/700.css'; import ReactMarkdown from 'react-markdown'; import { MathJax } from "better-react-mathjax"; import { Link as RouterLink } from 'react-router-dom'; - - -import { Button, FormControlLabel, FormGroup, Switch } from '@mui/material'; +import { Box, Button, CircularProgress, FormControlLabel, FormGroup, Switch } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; - import LeftPanel from './LeftPanel'; +import { LeanTaskGutter } from 'lean4web/client/src/editor/taskgutter'; import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider'; import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter'; import { InfoProvider } from 'lean4web/client/src/editor/infoview'; @@ -23,7 +21,7 @@ import './level.css' import { ConnectionContext } from '../connection'; import Infoview from './Infoview'; import { useParams } from 'react-router-dom'; -import { MonacoServices } from 'monaco-languageclient'; +import { useLoadLevelQuery } from '../game/api'; @@ -33,52 +31,68 @@ function Level() { const levelId = parseInt(params.levelId) const worldId = params.worldId - const [tacticDocs, setTacticDocs] = useState([]) - const [lemmaDocs, setLemmaDocs] = useState([]) - const [editor, setEditor] = useState(null) - const [infoProvider, setInfoProvider] = useState(null) - const [infoviewApi, setInfoviewApi] = useState(null) const [expertInfoview, setExpertInfoview] = useState(false) - - const [leanData, setLeanData] = useState({goals: [], description: "Loading..."}) - const [history, setHistory] = useState([]) - const [lastTactic, setLastTactic] = useState("") - const [introduction, setIntroduction] = useState("") - const [description, setDescription] = useState("Loading...") - const [ppStatement, setPPStatement] = useState("") - const [errors, setErrors] = useState([]) const codeviewRef = useRef(null) const infoviewRef = useRef(null) const messagePanelRef = useRef(null) - const [ready, setReady] = useState(false) + useEffect(() => { + // Scroll to top when loading a new level + messagePanelRef.current!.scrollTo(0,0) + }, [levelId]) - const [message, setMessage] = useState("") - const [messageOpen, setMessageOpen] = useState(false) + const connection = React.useContext(ConnectionContext) + const level = useLoadLevelQuery({world: worldId, level: levelId}) - const [completed, setCompleted] = useState(false) + const {editor, infoProvider} = useLevelEditor(worldId, levelId, codeviewRef, infoviewRef) - const processResponse = (res:any) => { - setLeanData(res); - // setErrors(res.errors); - // if (res.message !== "" && res.errors?.length === 0) { - // setMessage(res.message) - // setMessageOpen(true) - // } - // if (res.goals?.length === 0 && res.errors?.length === 0) { - // setCompleted(true) - // } - } + return <> + + + + + + +
+ {level?.data?.introduction} +
+

Aufgabe:

+
{level?.data?.descrText}
+
{level?.data?.descrFormat}
+ {/*NOTE(TODO): currently this looks bad, so I disabled it. Maybe have a drop-down for it of Syntax highlighting... */} +
+
+ - useEffect(() => { - // Scroll to top when loading a new level - messagePanelRef.current!.scrollTo(0,0) - }, [levelId]) + + + +
+
+ +
+ + { setExpertInfoview(!expertInfoview) }} control={} label="Expert mode" /> + +
+
+ +} + +export default Level + + +function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewRef) { const connection = React.useContext(ConnectionContext) + const [editor, setEditor] = useState(null) + const [infoProvider, setInfoProvider] = useState(null) + const [infoviewApi, setInfoviewApi] = useState(null) + + // Create Editor useEffect(() => { const editor = monaco.editor.create(codeviewRef.current!, { glyphMargin: true, @@ -105,83 +119,28 @@ function Level() { setInfoProvider(infoProvider) setInfoviewApi(infoviewApi) - return () => { editor.dispose() } + return () => { editor.setModel(null); infoProvider.dispose(); } }, []) - const uri = `file:///${worldId}/${levelId}` - - // The next function will be called when the level changes + // Create model when level changes useEffect(() => { connection.startLeanClient().then((leanClient) => { if (editor) { - const model = monaco.editor.createModel('', 'lean4', monaco.Uri.parse(uri)) + const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`) + const model = monaco.editor.getModel(uri) ?? + monaco.editor.createModel('', 'lean4', uri) editor.setModel(model) infoviewApi.serverRestarted(leanClient.initializeResult) infoProvider.openPreview(editor, infoviewApi) - setReady(true) + new LeanTaskGutter(infoProvider.client, editor) new AbbreviationRewriter(new AbbreviationProvider(), model, editor) - - - leanClient.sendRequest("loadLevel", {world: worldId, level: levelId}).then((res) => { - // setLevelTitle("Level " + res["index"] + ": " + res["title"]) - // setIndex(parseInt(res["index"])) - setTacticDocs(res["tactics"]) - setLemmaDocs(res["lemmas"]) - setIntroduction(res["introduction"]) - setDescription(res["description"]) - setPPStatement(res["ppStatement"]) - processResponse(res) - }); - - return () => { model.dispose(); setReady(false) } } }) }, [editor, levelId, connection]) - function loadLevel(index) { - setCompleted(false) - setHistory([]) - // setCurLevel(index) - } - - return ( - - - - - -
- {introduction} -
-

Aufgabe:

-
{description}
- {/*
{ppStatement}
- NOTE(TODO): currently this looks bad, so I disabled it. Maybe have a drop-down for it of Syntax highlighting... */} -
- {/* */} - {/* */} -
-
- - - - - -
-
- -
- - { setExpertInfoview(!expertInfoview) }} control={} label="Expert mode" /> - -
-
- ) + return {editor, infoProvider} } - -export default Level diff --git a/client/src/game/api.ts b/client/src/game/api.ts index c23e18e..739b11b 100644 --- a/client/src/game/api.ts +++ b/client/src/game/api.ts @@ -1,7 +1,7 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { Connection } from '../connection' -interface GameState { +interface GameInfo { title: null|string, introduction: null|string, worlds: null|{nodes: string[], edges: string[][2]}, @@ -9,6 +9,16 @@ interface GameState { conclusion: null|string, } +interface LevelInfo { + title: null|string, + introduction: null|string, + index: number, + tactics: any[], + lemmas: any[], + descrText: null|string, + descrFormat: null|string, +} + const customBaseQuery = async ( args : {method: string, params?: any}, { signal, dispatch, getState, extra }, @@ -27,12 +37,15 @@ export const gameApi = createApi({ reducerPath: 'gameApi', baseQuery: customBaseQuery, endpoints: (builder) => ({ - getGameInfo: builder.query({ + getGameInfo: builder.query({ query: () => {return {method: 'info', params: {}}}, }), + loadLevel: builder.query({ + query: ({world, level}) => {return {method: "loadLevel", params: {world, level}}}, + }), }), }) // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints -export const { useGetGameInfoQuery } = gameApi +export const { useGetGameInfoQuery, useLoadLevelQuery } = gameApi diff --git a/server/leanserver/GameServer/Commands.lean b/server/leanserver/GameServer/Commands.lean index 0b8718e..dcc398e 100644 --- a/server/leanserver/GameServer/Commands.lean +++ b/server/leanserver/GameServer/Commands.lean @@ -82,8 +82,8 @@ elab "Statement" statementName:ident ? descr:str sig:declSig val:declVal : comma elabCommand thmStatement modifyCurLevel fun level => pure {level with goal := sig, - description := descr.getString, - ppStatement := match statementName with + descrText := descr.getString, + descrFormat := match statementName with | none => "example " ++ (toString <| reprint sig.raw) ++ " := by" | some name => (Format.join ["lemma ", reprint name.raw, " ", reprint sig.raw, " := by"]).pretty 10 -- "lemma " ++ (toString <| reprint name.raw) ++ " " ++ (Format.pretty (reprint sig.raw) 40) ++ " := by" } -- Format.pretty <| format thmStatement.raw } diff --git a/server/leanserver/GameServer/EnvExtensions.lean b/server/leanserver/GameServer/EnvExtensions.lean index 70ead1d..295ce60 100644 --- a/server/leanserver/GameServer/EnvExtensions.lean +++ b/server/leanserver/GameServer/EnvExtensions.lean @@ -202,7 +202,8 @@ structure GameLevel where lemmas: Array LemmaDocEntry := default messages: Array GoalMessageEntry := default goal : TSyntax `Lean.Parser.Command.declSig := default - ppStatement : String := default + descrText: String := default + descrFormat : String := default deriving Inhabited, Repr /-! ## World -/ diff --git a/server/leanserver/GameServer/Game.lean b/server/leanserver/GameServer/Game.lean index d0170be..b95161d 100644 --- a/server/leanserver/GameServer/Game.lean +++ b/server/leanserver/GameServer/Game.lean @@ -41,8 +41,8 @@ structure LevelInfo where tactics: Array TacticDocEntry lemmas: Array LemmaDocEntry introduction : String - description : String - ppStatement : String + descrText : String := "" + descrFormat : String := "" deriving ToJson structure LoadLevelParams where @@ -70,8 +70,8 @@ partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do title := lvl.title, tactics := lvl.tactics, lemmas := lvl.lemmas, - description := lvl.description, - ppStatement := lvl.ppStatement --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO + descrText := lvl.descrText, + descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO introduction := lvl.introduction } c.hOut.writeLspResponse ⟨id, ToJson.toJson levelInfo⟩ return true