add unlocked inventory items to local storage
This commit is contained in:
@@ -21,7 +21,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|||||||
import { GameIdContext } from '../../app';
|
import { GameIdContext } from '../../app';
|
||||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||||
import { LevelInfo } from '../../state/api';
|
import { LevelInfo } from '../../state/api';
|
||||||
import { levelCompleted, selectCompleted } from '../../state/progress';
|
import { changedInventory, levelCompleted, selectCompleted, selectInventory } from '../../state/progress';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
|
||||||
import { Infos } from './infos';
|
import { Infos } from './infos';
|
||||||
@@ -33,16 +33,17 @@ import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
|||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { GameHint } from './rpc_api';
|
import { GameHint } from './rpc_api';
|
||||||
|
import { store } from '../../state/store';
|
||||||
|
|
||||||
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
||||||
* always present, or the monaco editor cannot start.
|
* always present, or the monaco editor cannot start.
|
||||||
*/
|
*/
|
||||||
export function DualEditor({level, codeviewRef, levelId, worldId}) {
|
export function DualEditor({ level, codeviewRef, levelId, worldId }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const {commandLineMode} = React.useContext(InputModeContext)
|
const { commandLineMode } = React.useContext(InputModeContext)
|
||||||
return <>
|
return <>
|
||||||
<div className={commandLineMode ? 'hidden' : ''}>
|
<div className={commandLineMode ? 'hidden' : ''}>
|
||||||
<ExerciseStatement data={level?.data} />
|
<ExerciseStatement data={level} />
|
||||||
<div ref={codeviewRef} className={'codeview'}></div>
|
<div ref={codeviewRef} className={'codeview'}></div>
|
||||||
</div>
|
</div>
|
||||||
{ec ?
|
{ec ?
|
||||||
@@ -57,10 +58,36 @@ export function DualEditor({level, codeviewRef, levelId, worldId}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The part of the two editors that needs the editor connection first */
|
/** The part of the two editors that needs the editor connection first */
|
||||||
function DualEditorMain({worldId, levelId, level}) {
|
function DualEditorMain({ worldId, levelId, level }: { worldId: string, levelId: number, level: LevelInfo }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {commandLineMode} = React.useContext(InputModeContext)
|
const { commandLineMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
|
// Mark level as completed when server gives notification
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
useServerNotificationEffect(
|
||||||
|
'$/game/completed',
|
||||||
|
(params: any) => {
|
||||||
|
if (ec.events.changedCursorLocation.current &&
|
||||||
|
ec.events.changedCursorLocation.current.uri === params.uri) {
|
||||||
|
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
||||||
|
|
||||||
|
// On completion, add the names of all new items to the local storage
|
||||||
|
let newTiles = [
|
||||||
|
...level?.tactics,
|
||||||
|
...level?.lemmas,
|
||||||
|
...level?.definitions
|
||||||
|
].filter((tile) => tile.new).map((tile) => tile.name)
|
||||||
|
|
||||||
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
|
|
||||||
|
// add new items and remove duplicates
|
||||||
|
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
|
||||||
|
|
||||||
|
dispatch(changedInventory({ game: gameId, inventory: newInv }))
|
||||||
|
}
|
||||||
|
}, [level]
|
||||||
|
)
|
||||||
|
|
||||||
/* Set up updates to the global infoview state on editor events. */
|
/* Set up updates to the global infoview state on editor events. */
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
@@ -75,22 +102,22 @@ function DualEditorMain({worldId, levelId, level}) {
|
|||||||
const serverVersion = useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
const serverVersion = useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<VersionContext.Provider value={serverVersion}>
|
<VersionContext.Provider value={serverVersion}>
|
||||||
<WithRpcSessions>
|
<WithRpcSessions>
|
||||||
<WithLspDiagnosticsContext>
|
<WithLspDiagnosticsContext>
|
||||||
<ProgressContext.Provider value={allProgress}>
|
<ProgressContext.Provider value={allProgress}>
|
||||||
{commandLineMode ?
|
{commandLineMode ?
|
||||||
<CommandLineInterface world={worldId} level={levelId} data={level?.data}/>
|
<CommandLineInterface world={worldId} level={levelId} data={level} />
|
||||||
:
|
:
|
||||||
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />
|
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />
|
||||||
}
|
}
|
||||||
</ProgressContext.Provider>
|
</ProgressContext.Provider>
|
||||||
</WithLspDiagnosticsContext>
|
</WithLspDiagnosticsContext>
|
||||||
</WithRpcSessions>
|
</WithRpcSessions>
|
||||||
</VersionContext.Provider>
|
</VersionContext.Provider>
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The mathematical formulation of the statement, supporting e.g. Latex
|
/** The mathematical formulation of the statement, supporting e.g. Latex
|
||||||
@@ -99,81 +126,66 @@ function DualEditorMain({worldId, levelId, level}) {
|
|||||||
* - Theorem xyz
|
* - Theorem xyz
|
||||||
* - Exercises: description
|
* - Exercises: description
|
||||||
*/
|
*/
|
||||||
function ExerciseStatement({data}) {
|
function ExerciseStatement({ data }) {
|
||||||
if (!data?.descrText) { return <></> }
|
if (!data?.descrText) { return <></> }
|
||||||
return <div className="exercise-statement"><Markdown>
|
return <div className="exercise-statement"><Markdown>
|
||||||
{(data?.statementName ? `**Theorem** \`${data?.statementName}\`: ` : data?.descrText && "**Exercise**: ") + `${data?.descrText}` }
|
{(data?.statementName ? `**Theorem** \`${data?.statementName}\`: ` : data?.descrText && "**Exercise**: ") + `${data?.descrText}`}
|
||||||
</Markdown></div>
|
</Markdown></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is only used in `EditorInterface`
|
// TODO: This is only used in `EditorInterface`
|
||||||
// while `CommandLineInterface` has this copy-pasted in.
|
// while `CommandLineInterface` has this copy-pasted in.
|
||||||
export function Main(props: {world: string, level: number}) {
|
export function Main(props: { world: string, level: number }) {
|
||||||
const ec = React.useContext(EditorContext);
|
const ec = React.useContext(EditorContext);
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||||
|
|
||||||
// Mark level as completed when server gives notification
|
/* Set up updates to the global infoview state on editor events. */
|
||||||
useServerNotificationEffect(
|
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
'$/game/completed',
|
|
||||||
(params: any) => {
|
|
||||||
|
|
||||||
if (ec.events.changedCursorLocation.current &&
|
const [allProgress, _1] = useServerNotificationState(
|
||||||
ec.events.changedCursorLocation.current.uri === params.uri) {
|
'$/lean/fileProgress',
|
||||||
dispatch(levelCompleted({game: gameId, world: props.world, level: props.level}))
|
new Map<DocumentUri, LeanFileProgressProcessingInfo[]>(),
|
||||||
}
|
async (params: LeanFileProgressParams) => (allProgress) => {
|
||||||
},
|
const newProgress = new Map(allProgress);
|
||||||
[]
|
return newProgress.set(params.textDocument.uri, params.processing);
|
||||||
);
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||||
|
|
||||||
/* Set up updates to the global infoview state on editor events. */
|
useClientNotificationEffect(
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
'textDocument/didClose',
|
||||||
|
(params: DidCloseTextDocumentParams) => {
|
||||||
|
if (ec.events.changedCursorLocation.current &&
|
||||||
|
ec.events.changedCursorLocation.current.uri === params.textDocument.uri) {
|
||||||
|
ec.events.changedCursorLocation.fire(undefined)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const [allProgress, _1] = useServerNotificationState(
|
const serverVersion =
|
||||||
'$/lean/fileProgress',
|
useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
||||||
new Map<DocumentUri, LeanFileProgressProcessingInfo[]>(),
|
const serverStoppedResult = useEventResult(ec.events.serverStopped);
|
||||||
async (params: LeanFileProgressParams) => (allProgress) => {
|
// NB: the cursor may temporarily become `undefined` when a file is closed. In this case
|
||||||
const newProgress = new Map(allProgress);
|
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
||||||
return newProgress.set(params.textDocument.uri, params.processing);
|
// that we want to persist.
|
||||||
},
|
let ret
|
||||||
[]
|
if (!serverVersion) {
|
||||||
);
|
ret = <p>Waiting for Lean server to start...</p>
|
||||||
|
} else if (serverStoppedResult) {
|
||||||
|
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
||||||
|
} else {
|
||||||
|
ret = <div className="infoview vscode-light">
|
||||||
|
{completed && <div className="level-completed">Level completed! 🎉</div>}
|
||||||
|
<Infos />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
return ret
|
||||||
|
|
||||||
useClientNotificationEffect(
|
|
||||||
'textDocument/didClose',
|
|
||||||
(params: DidCloseTextDocumentParams) => {
|
|
||||||
if (ec.events.changedCursorLocation.current &&
|
|
||||||
ec.events.changedCursorLocation.current.uri === params.textDocument.uri) {
|
|
||||||
ec.events.changedCursorLocation.fire(undefined)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const serverVersion =
|
|
||||||
useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
|
||||||
const serverStoppedResult = useEventResult(ec.events.serverStopped);
|
|
||||||
// NB: the cursor may temporarily become `undefined` when a file is closed. In this case
|
|
||||||
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
|
||||||
// that we want to persist.
|
|
||||||
let ret
|
|
||||||
if (!serverVersion) {
|
|
||||||
ret = <p>Waiting for Lean server to start...</p>
|
|
||||||
} else if (serverStoppedResult){
|
|
||||||
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
|
||||||
} else {
|
|
||||||
ret = <div className="infoview vscode-light">
|
|
||||||
{completed && <div className="level-completed">Level completed! 🎉</div>}
|
|
||||||
<Infos />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goalFilter = {
|
const goalFilter = {
|
||||||
@@ -185,9 +197,9 @@ const goalFilter = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The display of a single entered lean command */
|
/** The display of a single entered lean command */
|
||||||
function Command({command, deleteProof} : {command: string, deleteProof: any}) {
|
function Command({ command, deleteProof }: { command: string, deleteProof: any }) {
|
||||||
// The first step will always have an empty command
|
// The first step will always have an empty command
|
||||||
if (!command) {return <></>}
|
if (!command) { return <></> }
|
||||||
return <div className="command">
|
return <div className="command">
|
||||||
{command}
|
{command}
|
||||||
<div className="undo-button" title="Delete this and future commands" onClick={deleteProof}>
|
<div className="undo-button" title="Delete this and future commands" onClick={deleteProof}>
|
||||||
@@ -248,17 +260,17 @@ function Command({command, deleteProof} : {command: string, deleteProof: any}) {
|
|||||||
// }, fastIsEqual)
|
// }, fastIsEqual)
|
||||||
|
|
||||||
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
||||||
function GoalsTab({proofStep} : {proofStep: ProofStep}) {
|
function GoalsTab({ proofStep }: { proofStep: ProofStep }) {
|
||||||
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
||||||
|
|
||||||
if (!proofStep.goals.length) {return <></>}
|
if (!proofStep.goals.length) { return <></> }
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className="tab-bar">
|
<div className="tab-bar">
|
||||||
{proofStep.goals.map((goal, i) => (
|
{proofStep.goals.map((goal, i) => (
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active": ""}`} onClick={(ev) => { setSelectedGoal(i); ev.stopPropagation() }}>
|
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { setSelectedGoal(i); ev.stopPropagation() }}>
|
||||||
{i ? `Goal ${i+1}` : "Active Goal"}
|
{i ? `Goal ${i + 1}` : "Active Goal"}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -269,31 +281,17 @@ function GoalsTab({proofStep} : {proofStep: ProofStep}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The interface in command line mode */
|
/** The interface in command line mode */
|
||||||
export function CommandLineInterface(props: {world: string, level: number, data: LevelInfo}) {
|
export function CommandLineInterface(props: { world: string, level: number, data: LevelInfo }) {
|
||||||
|
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const editor = React.useContext(MonacoEditorContext)
|
const editor = React.useContext(MonacoEditorContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {proof} = React.useContext(ProofContext)
|
const { proof } = React.useContext(ProofContext)
|
||||||
const {selectedStep, setSelectedStep} = React.useContext(SelectionContext)
|
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||||
const {setDeletedChat, showHelp} = React.useContext(DeletedChatContext)
|
const { setDeletedChat, showHelp } = React.useContext(DeletedChatContext)
|
||||||
|
|
||||||
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
// Mark level as completed when server gives notification
|
|
||||||
useServerNotificationEffect(
|
|
||||||
'$/game/completed',
|
|
||||||
(params: any) => {
|
|
||||||
|
|
||||||
if (ec.events.changedCursorLocation.current &&
|
|
||||||
ec.events.changedCursorLocation.current.uri === params.uri) {
|
|
||||||
dispatch(levelCompleted({game: gameId, world: props.world, level: props.level}))
|
|
||||||
}
|
|
||||||
}, []
|
|
||||||
)
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
// console.debug('updated proof')
|
// console.debug('updated proof')
|
||||||
// // proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
// // proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
||||||
@@ -307,13 +305,13 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
let deletedChat: Array<GameHint> = []
|
let deletedChat: Array<GameHint> = []
|
||||||
proof.slice(line).map((step, i) => {
|
proof.slice(line).map((step, i) => {
|
||||||
// Only add these hidden hints to the deletion stack which were visible
|
// Only add these hidden hints to the deletion stack which were visible
|
||||||
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line+i)))]
|
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||||
})
|
})
|
||||||
setDeletedChat(deletedChat)
|
setDeletedChat(deletedChat)
|
||||||
|
|
||||||
editor.executeEdits("command-line", [{
|
editor.executeEdits("command-line", [{
|
||||||
range: monaco.Selection.fromPositions(
|
range: monaco.Selection.fromPositions(
|
||||||
{lineNumber: line, column: 1},
|
{ lineNumber: line, column: 1 },
|
||||||
editor.getModel().getFullModelRange().getEndPosition()
|
editor.getModel().getFullModelRange().getEndPosition()
|
||||||
),
|
),
|
||||||
text: '',
|
text: '',
|
||||||
@@ -340,7 +338,7 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof selectedStep !== 'undefined') {
|
if (typeof selectedStep !== 'undefined') {
|
||||||
Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
||||||
elem.scrollIntoView({block: "center"})
|
elem.scrollIntoView({ block: "center" })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [selectedStep])
|
}, [selectedStep])
|
||||||
@@ -349,15 +347,6 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
|
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
|
|
||||||
const [allProgress, _1] = useServerNotificationState(
|
|
||||||
'$/lean/fileProgress',
|
|
||||||
new Map<DocumentUri, LeanFileProgressProcessingInfo[]>(),
|
|
||||||
async (params: LeanFileProgressParams) => (allProgress) => {
|
|
||||||
const newProgress = new Map(allProgress);
|
|
||||||
return newProgress.set(params.textDocument.uri, params.processing);
|
|
||||||
}, []
|
|
||||||
)
|
|
||||||
|
|
||||||
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||||
|
|
||||||
useClientNotificationEffect(
|
useClientNotificationEffect(
|
||||||
@@ -371,17 +360,19 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
)
|
)
|
||||||
|
|
||||||
const serverVersion =
|
const serverVersion =
|
||||||
useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
|
||||||
const serverStoppedResult = useEventResult(ec.events.serverStopped);
|
const serverStoppedResult = useEventResult(ec.events.serverStopped);
|
||||||
// NB: the cursor may temporarily become `undefined` when a file is closed. In this case
|
// NB: the cursor may temporarily become `undefined` when a file is closed. In this case
|
||||||
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
||||||
// that we want to persist.
|
// that we want to persist.
|
||||||
|
|
||||||
if (!serverVersion) {return <p>Waiting for Lean server to start...</p>}
|
if (!serverVersion) { return <p>Waiting for Lean server to start...</p> }
|
||||||
if (serverStoppedResult) {return <div>
|
if (serverStoppedResult) {
|
||||||
<p>{serverStoppedResult.message}</p>
|
return <div>
|
||||||
<p className="error">{serverStoppedResult.reason}</p>
|
<p>{serverStoppedResult.message}</p>
|
||||||
</div>}
|
<p className="error">{serverStoppedResult.reason}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="commandline-interface">
|
return <div className="commandline-interface">
|
||||||
<div className="content" ref={proofPanelRef}>
|
<div className="content" ref={proofPanelRef}>
|
||||||
@@ -393,13 +384,13 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
// entered command as it is still present in the command line.
|
// entered command as it is still present in the command line.
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
return <div key={`proof-step-${i}`}>
|
return <div key={`proof-step-${i}`}>
|
||||||
<Errors errors={step.errors} commandLineMode={true}/>
|
<Errors errors={step.errors} commandLineMode={true} />
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')} onClick={toggleSelectStep(i)}>
|
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')} onClick={toggleSelectStep(i)}>
|
||||||
<Command command={step.command} deleteProof={deleteProof(i)}/>
|
<Command command={step.command} deleteProof={deleteProof(i)} />
|
||||||
<Errors errors={step.errors} commandLineMode={true}/>
|
<Errors errors={step.errors} commandLineMode={true} />
|
||||||
<GoalsTab proofStep={step}/>
|
<GoalsTab proofStep={step} />
|
||||||
{/* Show a message that there are no goals left */}
|
{/* Show a message that there are no goals left */}
|
||||||
{!step.goals.length && (
|
{!step.goals.length && (
|
||||||
<div className="message information">
|
<div className="message information">
|
||||||
@@ -416,6 +407,6 @@ export function CommandLineInterface(props: {world: string, level: number, data:
|
|||||||
}
|
}
|
||||||
}) : <CircularProgress />}
|
}) : <CircularProgress />}
|
||||||
</div>
|
</div>
|
||||||
<CommandLine proofPanelRef={proofPanelRef}/>
|
<CommandLine proofPanelRef={proofPanelRef} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-s
|
|||||||
import { GameIdContext } from '../app';
|
import { GameIdContext } from '../app';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview } from '../state/api';
|
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview } from '../state/api';
|
||||||
|
import { selectInventory } from '../state/progress';
|
||||||
|
import { store } from '../state/store';
|
||||||
|
|
||||||
export function Inventory({levelInfo, openDoc, enableAll=false} :
|
export function Inventory({levelInfo, openDoc, enableAll=false} :
|
||||||
{
|
{
|
||||||
@@ -46,13 +48,22 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
||||||
// been loaded. Is there a better way to observe this?
|
// been loaded. Is there a better way to observe this?
|
||||||
|
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
const categorySet = new Set<string>()
|
const categorySet = new Set<string>()
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
categorySet.add(item.category)
|
categorySet.add(item.category)
|
||||||
}
|
}
|
||||||
const categories = Array.from(categorySet).sort()
|
const categories = Array.from(categorySet).sort()
|
||||||
|
|
||||||
const [tab, setTab] = useState(defaultTab);
|
const [tab, setTab] = useState(defaultTab)
|
||||||
|
|
||||||
|
// Add inventory items from local store as unlocked.
|
||||||
|
// Items are unlocked if they are in the local store, or if the server says they should be
|
||||||
|
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
||||||
|
// modi)
|
||||||
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
|
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
||||||
@@ -69,7 +80,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
onClick={() => { setTab(cat) }}>{cat}</div>)}
|
onClick={() => { setTab(cat) }}>{cat}</div>)}
|
||||||
</div>}
|
</div>}
|
||||||
<div className="inventory-list">
|
<div className="inventory-list">
|
||||||
{[...items].sort(
|
{[...modifiedItems].sort(
|
||||||
// For lemas, sort entries `available > disabled > locked`
|
// For lemas, sort entries `available > disabled > locked`
|
||||||
// otherwise alphabetically
|
// otherwise alphabetically
|
||||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
|
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ function PlayableLevel({worldId, levelId}) {
|
|||||||
<EditorContext.Provider value={editorConnection}>
|
<EditorContext.Provider value={editorConnection}>
|
||||||
<MonacoEditorContext.Provider value={editor}>
|
<MonacoEditorContext.Provider value={editor}>
|
||||||
<div className="exercise">
|
<div className="exercise">
|
||||||
<DualEditor level={level} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} />
|
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} />
|
||||||
</div>
|
</div>
|
||||||
</MonacoEditorContext.Provider>
|
</MonacoEditorContext.Provider>
|
||||||
</EditorContext.Provider>
|
</EditorContext.Provider>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit'
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { loadState } from "./local_storage";
|
import { loadState } from "./local_storage";
|
||||||
|
import { WorkDoneProgressBegin } from 'vscode-languageserver-protocol';
|
||||||
|
|
||||||
interface Selection {
|
interface Selection {
|
||||||
selectionStartLineNumber: number,
|
selectionStartLineNumber: number,
|
||||||
@@ -17,9 +18,13 @@ interface LevelProgressState {
|
|||||||
completed: boolean,
|
completed: boolean,
|
||||||
help: number[], // A set of rows where hidden hints have been displayed
|
help: number[], // A set of rows where hidden hints have been displayed
|
||||||
}
|
}
|
||||||
|
interface WorldProgressState {
|
||||||
|
[world: string] : {[level: number]: LevelProgressState},
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameProgressState {
|
export interface GameProgressState {
|
||||||
[world: string] : {[level: number]: LevelProgressState}
|
inventory: string[],
|
||||||
|
data: WorldProgressState
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The progress made on all lean4-games */
|
/** The progress made on all lean4-games */
|
||||||
@@ -32,16 +37,21 @@ const initialProgressState: ProgressState = loadState() ?? { games: {} }
|
|||||||
// TODO: There was some weird unreproducible bug with removing `as LevelProgressState` here...
|
// TODO: There was some weird unreproducible bug with removing `as LevelProgressState` here...
|
||||||
const initalLevelProgressState: LevelProgressState = {code: "", completed: false, selections: [], help: []}
|
const initalLevelProgressState: LevelProgressState = {code: "", completed: false, selections: [], help: []}
|
||||||
|
|
||||||
|
/** Add an empty skeleton with progress for the current game */
|
||||||
|
function addGameProgress (state: ProgressState, action: PayloadAction<{game: string}>) {
|
||||||
|
if (!state.games[action.payload.game]) {
|
||||||
|
state.games[action.payload.game] = {inventory: [], data: {}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Add an empty skeleton with progress for the current level */
|
/** Add an empty skeleton with progress for the current level */
|
||||||
function addLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
function addLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
if (!state.games[action.payload.game]) {
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game] = {}
|
if (!state.games[action.payload.game].data[action.payload.world]) {
|
||||||
|
state.games[action.payload.game].data[action.payload.world] = {}
|
||||||
}
|
}
|
||||||
if (!state.games[action.payload.game][action.payload.world]) {
|
if (!state.games[action.payload.game].data[action.payload.world][action.payload.level]) {
|
||||||
state.games[action.payload.game][action.payload.world] = {}
|
state.games[action.payload.game].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
||||||
}
|
|
||||||
if (!state.games[action.payload.game][action.payload.world][action.payload.level]) {
|
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,39 +62,44 @@ export const progressSlice = createSlice({
|
|||||||
/** put edited code in the state and set completed to false */
|
/** put edited code in the state and set completed to false */
|
||||||
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
|
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level].code = action.payload.code
|
state.games[action.payload.game].data[action.payload.world][action.payload.level].code = action.payload.code
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level].completed = false
|
state.games[action.payload.game].data[action.payload.world][action.payload.level].completed = false
|
||||||
},
|
},
|
||||||
/** TODO: docstring */
|
/** TODO: docstring */
|
||||||
changedSelection(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, selections: Selection[]}>) {
|
changedSelection(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, selections: Selection[]}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level].selections = action.payload.selections
|
state.games[action.payload.game].data[action.payload.world][action.payload.level].selections = action.payload.selections
|
||||||
},
|
},
|
||||||
/** mark level as completed */
|
/** mark level as completed */
|
||||||
levelCompleted(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
levelCompleted(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level].completed = true
|
state.games[action.payload.game].data[action.payload.world][action.payload.level].completed = true
|
||||||
},
|
},
|
||||||
/** Set the list of rows where help is displayed */
|
/** Set the list of rows where help is displayed */
|
||||||
helpEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, help: number[]}>) {
|
helpEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, help: number[]}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
console.debug(`!setting help to: ${action.payload.help}`)
|
console.debug(`!setting help to: ${action.payload.help}`)
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level].help = action.payload.help
|
state.games[action.payload.game].data[action.payload.world][action.payload.level].help = action.payload.help
|
||||||
},
|
},
|
||||||
/** delete all progress for this game */
|
/** delete all progress for this game */
|
||||||
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
|
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
|
||||||
state.games[action.payload.game] = {}
|
state.games[action.payload.game] = {inventory: [], data: {}}
|
||||||
},
|
},
|
||||||
/** delete progress for this level */
|
/** delete progress for this level */
|
||||||
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game][action.payload.world][action.payload.level] = initalLevelProgressState
|
state.games[action.payload.game].data[action.payload.world][action.payload.level] = initalLevelProgressState
|
||||||
},
|
},
|
||||||
/** load progress, e.g. from external import */
|
/** load progress, e.g. from external import */
|
||||||
loadProgress(state: ProgressState, action: PayloadAction<{game: string, data:GameProgressState}>) {
|
loadProgress(state: ProgressState, action: PayloadAction<{game: string, data:GameProgressState}>) {
|
||||||
console.debug(`setting data to:\n ${action.payload.data}`)
|
console.debug(`setting data to:\n ${action.payload.data}`)
|
||||||
state.games[action.payload.game] = action.payload.data
|
state.games[action.payload.game] = action.payload.data
|
||||||
},
|
},
|
||||||
|
/** set the current inventory */
|
||||||
|
changedInventory(state: ProgressState, action: PayloadAction<{game: string, inventory: string[]}>) {
|
||||||
|
addGameProgress(state, action)
|
||||||
|
state.games[action.payload.game].inventory = action.payload.inventory
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,9 +107,9 @@ export const progressSlice = createSlice({
|
|||||||
export function selectLevel(game: string, world: string, level: number) {
|
export function selectLevel(game: string, world: string, level: number) {
|
||||||
return (state) =>{
|
return (state) =>{
|
||||||
if (!state.progress.games[game]) { return initalLevelProgressState }
|
if (!state.progress.games[game]) { return initalLevelProgressState }
|
||||||
if (!state.progress.games[game][world]) { return initalLevelProgressState }
|
if (!state.progress.games[game].data[world]) { return initalLevelProgressState }
|
||||||
if (!state.progress.games[game][world][level]) { return initalLevelProgressState }
|
if (!state.progress.games[game].data[world][level]) { return initalLevelProgressState }
|
||||||
return state.progress.games[game][world][level]
|
return state.progress.games[game].data[world][level]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +120,14 @@ export function selectCode(game: string, world: string, level: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** return the current inventory */
|
||||||
|
export function selectInventory(game: string) {
|
||||||
|
return (state) => {
|
||||||
|
if (!state.progress.games[game]) { return [] }
|
||||||
|
return state.progress.games[game].inventory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** return the code of the current level */
|
/** return the code of the current level */
|
||||||
export function selectHelp(game: string, world: string, level: number) {
|
export function selectHelp(game: string, world: string, level: number) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
@@ -135,4 +158,4 @@ export function selectProgress(game: string) {
|
|||||||
|
|
||||||
/** Export actions to modify the progress */
|
/** Export actions to modify the progress */
|
||||||
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||||
deleteLevelProgress, loadProgress, helpEdited } = progressSlice.actions
|
deleteLevelProgress, loadProgress, helpEdited, changedInventory } = progressSlice.actions
|
||||||
|
|||||||
Reference in New Issue
Block a user