Widget problems

A widget problem is solved inside a small web page you write, instead of by writing a program. The participant reads the statement, then clicks, drags or draws in the page below it, and submits what they built there. This is the shape of a Bebras task: the question is a picture you interact with, and the answer is a state — which cells are marked, which path was drawn, how the boxes were ordered.

You upload the page as a zip archive. Eolymp shows it under the statement and asks it for the answer when the participant submits. The answer is a JSON document of your own design, which only your page and your checker ever read.

A widget problem works wherever a problem does: in your space's problem archive, in contests and in courses.

Create a widget problem

  1. Open Problems, click New problem and choose Widget.

  2. Write the statement.

  3. On the problem's Widget tab, Upload the archive.

  4. On the same tab, set the Checker. It must be a program.

  5. Try the widget in the preview on the Widget tab and Submit an answer to see it graded.

  6. Publish the problem.

You need permission to write problems. A widget problem has no Solutions or Testing tab and does not open in the Studio. Re-upload replaces the page; submissions already made keep the page they were made in, so they can be shown back as they were.

Download a starter widget: a working 4×4 grid task with its own README, ready to zip and upload. Its checker shows the grading described below.

How a widget answer is graded

A widget problem has no tests. Each submission is judged once, worth 100 points, by the checker alone: the answer the widget produced is handed to it as the participant's output file, and the input and answer files are empty. The checker holds whatever the task expects.

Any checker mode works. In the Eolymp and Testlib modes the checker exits with 0 for a full score and 1 for a wrong answer; for part of the score it prints points <n> and exits with 7, which gives the Partially correct verdict. TEST_COST in the environment holds what the run is worth.

Compare answers as JSON rather than as text: key order and spacing carry no meaning, so a text comparison of two JSON documents rejects correct answers.

The archive

The archive holds a page and everything it needs. index.html is loaded first.

widget.zip
├── index.html
├── task.js
├── client.js
├── style.css
└── images/
    └── board.png

Zipping the folder itself works too; a single top-level folder is unwrapped for you. Everything the page loads must be inside the archive and referenced by a relative path. A widget cannot load a script, a stylesheet, a font or an image from anywhere else, and it cannot call an API. That is what makes it safe to run someone else's page next to a contest.

Unpacked size

50 MB, at most 1000 files, 10 MB each

Markup and code

.html .htm .js .mjs .css .json .map .txt .wasm

Images

.svg .png .jpg .jpeg .gif .webp .ico

Fonts

.woff .woff2 .ttf

Audio and video

.mp3 .ogg .wav .mp4 .webm

Any other file is refused, and so are symbolic links and paths pointing outside the archive.

The page runs sandboxed, in its own origin. It cannot read cookies, reach the surrounding page or open windows, and localStorage and sessionStorage throw rather than returning empty. Keep state in variables, and let Eolymp keep the answer.

Talking to Eolymp

Eolymp talks to the page through client.js, which is in the starter widget. Copy it into your archive, load it, and register the widget once the page is ready:

<script src="./client.js"></script>
<script>
  let cells = [];
  let readonly = false;

  eolymp.widget({
    load: ({ mode }) => {
      readonly = mode === "view";
      render();
    },
    getAnswer: () => ({ cells }),
    reloadAnswer: (answer) => {
      cells = answer.cells ?? [];
      render();
    },
  });
</script>

load({ mode, locale }) prepares the widget. Anything slow, such as loading an image or building a board, belongs here, because the participant sees a loading state until it returns. mode is "solve" while the participant is composing an answer and "view" when a submission is being shown back; in view mode, turn your own controls off. The widget is never told whether an answer was right.

getAnswer() returns the answer to submit. Return an object or an array and it is serialised for you.

reloadAnswer(answer) shows an answer produced earlier, a saved draft or a past submission, and receives it exactly as getAnswer returned it.

getHeight() is optional. The page's height is measured for you; implement it only if the document's own height is wrong.

Call eolymp.answerChanged() whenever the participant changes something. Eolymp then asks for the answer and keeps it as a draft, so the work survives a reload. Call eolymp.submit() only if the widget has a submit control of its own; the problem page already has one.