Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3e98c97
refactor: eliminate old infoview; fix layout; insert default infoview
joneugster Apr 2, 2026
c052ee0
wip
joneugster Apr 3, 2026
889382e
Fix typo in import statement
matlorr Apr 10, 2026
cc7e450
Non-dynamic textbox in typewriter mode
matlorr Apr 10, 2026
9aec9f3
Adjust css for typewriter input-field such that typed inputs are full…
matlorr Apr 17, 2026
7cdc853
Add monaco editor creation logic to TypewriterCommandline and fix run…
matlorr Apr 17, 2026
65e05d5
npm audit fix
joneugster Apr 23, 2026
b3ff7e0
Merge remote-tracking branch 'origin/main' into feat/drop-old-infoview
joneugster Apr 23, 2026
9a2659c
add missing jotai-effect
joneugster Apr 23, 2026
17a3d6b
bump lean4monaco
joneugster Apr 23, 2026
69b49cb
fix
joneugster Apr 23, 2026
e7da7aa
wip
joneugster Apr 23, 2026
de2da5e
missing file
joneugster Apr 24, 2026
5380177
bump lean4monaco to import WithRpcSessions
joneugster Apr 24, 2026
66bee6d
Merge remote-tracking branch 'origin/main' into feat/drop-old-infoview
joneugster Apr 24, 2026
7afa09a
Instantiate LeanMonacoEditor in PlayableLevel component and set corre…
matlorr May 2, 2026
2395a8a
Disected main.tsx in its constituants and replaced context creations …
matlorr May 8, 2026
7bfd4e5
Replace contexts by respective atoms holding state.
matlorr May 19, 2026
070544b
Disected original main.tsx into several files and replaced context se…
matlorr May 19, 2026
1441339
Copied from vscode-lean4/lean4infoview/src/infoview as these files we…
matlorr May 19, 2026
36ad0b1
Copied over PlayableLevel component
matlorr May 19, 2026
dfd4752
Delete re-implementation of withRpcSessions as it is not needed to cr…
matlorr May 19, 2026
3b391c1
Copied over error message component
matlorr May 19, 2026
54c76fc
working RPC session initialisation and receiving proof step; and inst…
joneugster May 19, 2026
6447f48
reorganise stuff
joneugster May 19, 2026
659b713
reorganise more
joneugster May 19, 2026
b7717d2
Add text box displaying last executed command and show completion mes…
matlorr May 24, 2026
666da9a
wip
joneugster May 22, 2026
8382020
typewriter content
joneugster May 23, 2026
bdeea7e
fix: add missing appendLine
joneugster May 23, 2026
8dff53a
show non-interactive hyps and goal
joneugster May 23, 2026
f8a1106
wip
joneugster May 24, 2026
4858c37
cleanup merge
joneugster May 25, 2026
e8f365b
Re-introduce autoinsert feature into typewriter
matlorr May 28, 2026
5b6a184
Add custom hook for appending to typewrite input and reactivate inser…
matlorr May 28, 2026
7afa1c0
Display Error only if a text is available, that can be displyed.
matlorr May 29, 2026
ec151a0
Reactivate "Failed Command" notification in editor
matlorr May 29, 2026
86ec1c3
Centered circular progressbar
matlorr May 29, 2026
6d436cd
Merge remote-tracking branch 'origin/main' into feat/drop-old-infoview
joneugster Jul 10, 2026
10319f6
Merge remote-tracking branch 'origin/main' into feat/drop-old-infoview
joneugster Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.2.0",
"@leanprover/infoview": "^0.4.3",
"@leanprover/infoview-api": "^0.13.0",
"@mui/material": "^5.11.1",
"@reduxjs/toolkit": "^1.9.1",
"@tanstack/query-core": "^5.90.20",
Expand All @@ -20,9 +20,10 @@
"i18next": "^25.8.18",
"i18next-http-backend": "^3.0.2",
"jotai": "^2.13.1",
"jotai-effect": "^2.2.3",
"jotai-location": "^0.6.2",
"jotai-tanstack-query": "^0.11.0",
"lean4monaco": "^1.1.9",
"lean4monaco": "^1.1.14",
"monaco-editor-wrapper": "^5.3.1",
"react": "^18.2.0",
"react-country-flag": "^3.1.0",
Expand All @@ -34,8 +35,7 @@
"react-split": "^2.0.14",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"vscode-lean4": "github:leanprover/vscode-lean4#de0062c"
"remark-math": "^6.0.0"
},
"devDependencies": {
"@codingame/esbuild-import-meta-url-plugin": "^1.0.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
/**
* @fileOverview Defines the interface for the communication with the server.
*
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
*/
import type { Range } from 'vscode-languageserver-protocol';
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
import { CodeWithInfos, ContextInfo, FVarId, InteractiveDiagnostic, InteractiveGoalCore, MVarId, TermInfo } from '@leanprover/infoview-api'

export interface InteractiveHypothesisBundle {
/** The pretty names of the variables in the bundle. Anonymous names are rendered
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
names: string[];
fvarIds?: FVarId[];
type: CodeWithInfos;
val?: CodeWithInfos;
isInstance?: boolean;
isType?: boolean;
isInserted?: boolean;
isRemoved?: boolean;
isAssumption?: boolean;
}

export interface InteractiveGoalCore {
hyps: InteractiveHypothesisBundle[];
type: CodeWithInfos;
ctx?: ContextInfo;
}

export interface InteractiveGoal extends InteractiveGoalCore {
userName?: string;
Expand Down
17 changes: 13 additions & 4 deletions client/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import '@fontsource/roboto/700.css';

import './css/reset.css';
import './css/app.css';
import './css/error_page.css';
import "./css/infoview.css"
import "./css/landing_page.css"
import './css/layout.css';
import './css/level.css';
import "./css/popup.css"
import "./css/tab_bar.css"
import "./css/typewriter.css"
import "./css/welcome.css"
import "./css/world_tree.css"

import i18n from './i18n';
import { Popup } from './components/popup/popup';
import { leanMonacoAtom, leanMonacoOptionsAtom } from './store/editor-atoms';
Expand All @@ -17,7 +28,6 @@ import { preferencesAtom } from './store/preferences-atoms';

function App({ children }: { children?: React.ReactNode }) {

const infoviewRef = useRef<HTMLDivElement>(null)
const [leanMonaco, setLeanMonaco] = useAtom(leanMonacoAtom)
const [leanMonacoOptions] = useAtom(leanMonacoOptionsAtom)
const [preferences] = useAtom(preferencesAtom)
Expand All @@ -30,7 +40,6 @@ function App({ children }: { children?: React.ReactNode }) {
useEffect(() => {
const _leanMonaco = new LeanMonaco()
setLeanMonaco(_leanMonaco)
_leanMonaco.setInfoviewElement(infoviewRef.current!)

;(async () => {
await _leanMonaco.start(leanMonacoOptions)
Expand All @@ -45,12 +54,12 @@ function App({ children }: { children?: React.ReactNode }) {
}, [leanMonacoOptions, setLeanMonaco])

return (
<div className="app">
<>
<React.Suspense>
{children}
</React.Suspense>
<Popup />
</div>
</>
)
}

Expand Down
134 changes: 134 additions & 0 deletions client/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useGameTranslation } from '../../utils/translation'
import { useEffect, useRef } from 'react'
import { useAtom } from 'jotai'
import { mobileAtom } from '../../store/preferences-atoms'
import { gameIdAtom, levelIdAtom } from '../../store/location-atoms'
import { levelInfoAtom } from '../../store/query-atoms'
import { deletedChatAtom, helpAtom, selectedStepAtom } from '../../store/chat-atoms'
import { proofAtom } from '../../store/editor-atoms'
import { DeletedHints, filterHints, Hint, Hints, MoreHelpButton } from '../hints'
import Markdown from 'react-markdown'
import { faArrowRight, faHome } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Button } from '../button'

/** Appears on the left of playable levels */
export function ChatPanel({lastLevel, visible = true}: {lastLevel: boolean, visible: boolean}) {
let { t } = useTranslation()
const { t : gT } = useGameTranslation()
const chatRef = useRef<HTMLDivElement>(null)
const [mobile] = useAtom(mobileAtom)
const [gameId, navigateToGame] = useAtom(gameIdAtom)
const [levelId, navigateToLevel] = useAtom(levelIdAtom)
const [{ data: levelInfo }] = useAtom(levelInfoAtom)
const [help] = useAtom(helpAtom)
const [proof] = useAtom(proofAtom)
const [deletedChat] = useAtom(deletedChatAtom)
const [selectedStep, setSelectedStep] = useAtom(selectedStepAtom)

let k = proof?.steps.length ? proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) : 0

function toggleSelection(line: number) {
return (ev: any) => {
console.debug('toggled selection')
if (selectedStep == line) {
setSelectedStep(undefined)
} else {
setSelectedStep(line)
}
}
}

useEffect(() => {
// TODO: For some reason this is always called twice
console.debug('scroll chat')
if (!mobile) {
chatRef.current!.lastElementChild?.scrollIntoView() //scrollTo(0,0)
}
}, [proof, help])

// Scroll to element if selection changes
useEffect(() => {
if (typeof selectedStep !== 'undefined') {
Array.from(chatRef.current!.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
elem.scrollIntoView({block: "center"})
})
}
}, [selectedStep])

// useEffect(() => {
// // // Scroll to top when loading a new level
// // // TODO: Thats the wrong behaviour probably
// // chatRef.current!.scrollTo(0,0)
// }, [gameId, worldId, levelId])

let introText: Array<string> = gT(levelInfo?.introduction ?? "").split(/\n(\s*\n)+/)

const focusRef = useRef<HTMLButtonElement>()
useEffect(() => {
if (proof?.completed) {
focusRef.current?.focus()
}
}, [!!proof?.completed])

return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
<div ref={chatRef} className="chat">
{introText?.filter(it => it.trim()).map(((it, i) =>
// Show the level's intro text as hints, too
<Hint key={`intro-p-${i}`}
hint={{text: it, hidden: false, rawText: it, varNames: []}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
))}
{proof?.steps.map((step, i) => {
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
return <Hints key={`hints-${i}`}
hints={filteredHints} showHidden={help.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
}
})}

{/* {modifiedHints.map((step, i) => {
// It the last step has errors, it will have the same hints
// as the second-to-last step. Therefore we should not display them.
if (!(i == proof?.steps.length - 1 && withErr)) {
// TODO: Should not use index as key.
return <Hints key={`hints-${i}`}
hints={step} showHidden={showHelp?.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
}
})} */}
<DeletedHints hints={deletedChat}/>
{proof?.completed &&
<>
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
{t("Level completed! 🎉")}
</div>
{levelInfo?.conclusion?.trim() &&
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
<Markdown>{gT(levelInfo?.conclusion ?? "")}</Markdown>
</div>
}
</>
}
</div>
<div className="button-row">
{proof?.completed && (lastLevel ?
<Button ref={focusRef} onClick={() => {if (gameId) navigateToGame(gameId)}} >
<FontAwesomeIcon icon={faHome} />&nbsp;{t("Home")}
</Button> :
<Button ref={focusRef} onClick={() => {if(levelId !== undefined) navigateToLevel(levelId + 1)}} >
{t("Next")}&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>)
}
<MoreHelpButton selected={null} />
</div>
</div>
}

//FIXME: implement
function lastStepHasErrors(x: any) {return false}

//FIXME: implement
function isLastStepWithErrors(x: any, y: any) {return false}
52 changes: 52 additions & 0 deletions client/src/components/chat/IntroPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useTranslation } from "react-i18next"
import { useGameTranslation } from "../../utils/translation"
import { useAtom } from "jotai"
import { gameIdAtom, levelIdAtom, worldIdAtom } from "../../store/location-atoms"
import { gameInfoAtom } from "../../store/query-atoms"
import { readWorldIntroAtom } from "../../store/progress-atoms"
import { mobileAtom } from "../../store/preferences-atoms"
import { useEffect, useRef } from "react"
import React from "react"
import { Hint } from "../hints"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faArrowRight, faHome } from "@fortawesome/free-solid-svg-icons"
import { Button } from "../button"

/** Appears on the left side of the intro of a world */
export function IntroPanel() {
let { t } = useTranslation()
const { t : gT } = useGameTranslation()
const [gameId, navigateToGame] = useAtom(gameIdAtom)
const [worldId] = useAtom(worldIdAtom)
const [{ data: gameInfo }] = useAtom(gameInfoAtom)
const [, navigateToLevel] = useAtom(levelIdAtom)
const [, readWorldIntro] = useAtom(readWorldIntroAtom)

const [mobile] = useAtom(mobileAtom)

let text: Array<string> = gT(gameInfo?.worlds?.nodes[worldId ?? ""]?.introduction ?? "").split(/\n(\s*\n)+/)

const focusRef = useRef<HTMLButtonElement>()
useEffect(() => {
focusRef.current?.focus()
}, [])

return <div className="chat-panel">
<div className="chat">
{text?.filter(t => t.trim()).map(((t, i) =>
<Hint key={`intro-p-${i}`}
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={undefined} toggleSelection={undefined} />
))}
</div>
<div className={`button-row${mobile ? ' mobile' : ''}`}>
{gameInfo?.worldSize?.[worldId ?? ""] == 0 ?
<Button onClick={() => {if (gameId) navigateToGame(gameId)}}>
<FontAwesomeIcon icon={faHome} />
</Button> :
<Button ref={focusRef} onClick={() => {readWorldIntro(); navigateToLevel(1)}}>
{t("Start")}&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>
}
</div>
</div>
}
39 changes: 39 additions & 0 deletions client/src/components/editor/ExerciseStatement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react"
import { useTranslation } from "react-i18next"
import { useGameTranslation } from "../../utils/translation"
import { useAtom } from "jotai"
import { gameIdAtom } from "../../store/location-atoms"
import { levelInfoAtom } from "../../store/query-atoms"
import { Markdown } from "../markdown"

/** The mathematical formulation of the statement, supporting e.g. Latex
* It takes three forms, depending on the precence of name and description:
* - Theorem xyz: description
* - Theorem xyz
* - Exercises: description
*
* If `showLeanStatement` is true, it will additionally display the lean code.
*/
export function ExerciseStatement({ showLeanStatement = false }) {
const { t : gT } = useGameTranslation()
const { t } = useTranslation()
const [gameId] = useAtom(gameIdAtom)
const [{ data: levelInfo }] = useAtom(levelInfoAtom)

if (!(levelInfo?.descrText || levelInfo?.descrFormat)) { return <></> }
return <>
<div className="exercise-statement">
{levelInfo?.descrText ?
<Markdown>
{(levelInfo?.displayName ? `**${t("Theorem")}** \`${levelInfo?.displayName}\`: ` : '') + t(levelInfo?.descrText, {ns: gameId})}
</Markdown> : levelInfo?.displayName &&
<Markdown>
{(levelInfo?.displayName ? `**${t("Theorem")}** \`${levelInfo?.displayName}\`: ` : '') + gT(levelInfo?.descrText ?? "")}
</Markdown>
}
{levelInfo?.descrFormat && showLeanStatement &&
<p><code className="lean-code">{levelInfo?.descrFormat}</code></p>
}
</div>
</>
}
26 changes: 26 additions & 0 deletions client/src/components/editor/GameEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useAtom } from "jotai"
import { leanMonacoAtom, typewriterModeAtom } from "../../store/editor-atoms"
import React, { useEffect, useLayoutEffect, useRef } from "react"
import { ExerciseStatement } from "./ExerciseStatement"
import { Typewriter } from "./typewriter/Typewriter"

/**
* Note: It is important that the `div` with `codeViewRef` is
* always present, or the monaco editor cannot start.
*/
export function GameEditor({ codeviewRef } : { codeviewRef: React.MutableRefObject<HTMLDivElement> }) {
const infoviewRef = useRef<HTMLDivElement>(null)
const [leanMonaco] = useAtom(leanMonacoAtom)
const [typewriterMode] = useAtom(typewriterModeAtom)

useLayoutEffect(() => {
leanMonaco?.setInfoviewElement(infoviewRef.current!)
})

return <>
<ExerciseStatement showLeanStatement={true} />
<div ref={codeviewRef} className='codeview' />
<div className='infoview' ref={infoviewRef} />
{typewriterMode && <Typewriter />}
</>
}
Loading
Loading