Skip to content

Latest commit

 

History

149 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GradeBridge Assignment Maker

Author structured assignments with LaTeX and figures, and export the printable sheet, the student file and the grading materials your grading workflow needs — entirely in your browser.

Assignments come in two input modes. In electronic mode the student types answers and uploads images in the Student Submission app. In handwritten mode the app generates a printable QR-registered sheet: the student prints it, writes on it by hand, photographs the pages, and the Submission app crops each answer region from the photographs. One assignment is one mode — you choose it in the editor, and it decides which question types are available and what the export contains.

License

Live App | Student Submission App


If you are an instructor, the guide is in the app

Press Help in the app's header. The whole instructor guide is in there — how to start, the two things to choose first, who decides the grades, figures, Finalize, what each export is for, and how to write mathematics — and it works with the network off. Small ? links beside the figure card, the assignment kind and Finalize open it at the right section.

Its source is docs/INSTRUCTOR_GUIDE.md, and that file is the only copy: the app bundles it at build time, and this README does not repeat it. One guide cannot drift out of step with itself; two can, and the one on GitHub is the one nobody updates. An instructor should never need GitHub to learn how to use the tool (Andre, 2026-09-22).

Everything below is the developer file: the export contract, the .md format, the grading rubric JSON, privacy, and local development.


The Apps

GradeBridge apps share an encryption contract and a Gradescope-Docker autograder pattern. This app handles lab reports, mini-projects and homework — typed and image-based responses in electronic mode, and printed-and-photographed work in handwritten mode.

App Who uses it What it does
Assignment Maker (this app) Instructor Create assignments, configure grading, export ZIP
Student Submission Student Load assignment, fill answers, download submission files

What the export ZIP contains

The export contract, for anyone writing a consumer of it. Four files in instructor/ contain the answer key, and {stem}_FOR_STUDENTS.zip is the only entry that may reach a student. The archive is laid out so that is easy to get right, and it carries a generated notice at the root naming every file. What an instructor does with it is in the guide.

{stem} below is {CourseCode}_{Title}, with spaces replaced by underscores.

Entry Mode What it is
00_INSTRUCTOR_ONLY_DO_NOT_DISTRIBUTE.txt both Generated at export time from the actual entry list; names every file and who it is for
{stem}_FOR_STUDENTS.zip both The only entry a student may receive. The student package, ready to post as it comes. Holds exactly the two files below and nothing else. Stored uncompressed, since it is already an archive
{stem}.pdf both Electronic: the handout. Handwritten: the sheet itself — the QR-registered pages the student prints and writes on
{stem}_OPEN_IN_APP.json both The one file they load into the Submission app. Encoded (gb1: envelope) and built from a whitelist, so it carries no grading prompt, grader note or answer key — and on a handwritten assignment it carries the layout map inside it, so there is no second file to load and nothing to open
instructor/layout_{TemplateID}.csv handwritten only Your copy of the map the Submission app crops answers by, for setting up the Gradescope outline. The student's copy is inside {stem}_OPEN_IN_APP.json, byte for byte. Do not post this: students no longer need it, and it is the map their answers are cut from
instructor/{stem}_authoring_backup.json both The backup that restores everything. Unencrypted, the whole authoring object. This is what Import JSON should be given
instructor/{stem}.md both The authored source, for the .md round trip
instructor/{stem}_grading_rubric.json both Private — for the Gradescope autograder
instructor/{stem}_grader_document.html both Private — rubrics and answer keys, for you and your TAs
instructor/assignment.html both Readable copy
instructor/assignment.tex both Editable LaTeX source
instructor/template.pdf electronic only The boxed answer-region sheet for setting up the Gradescope outline. That is an instructor task, so it is not a student file. A handwritten assignment does not produce one — {stem}.pdf already is the answer surface, and a second boxed PDF only invites printing the wrong one

Ten entries in either mode. Built by buildExportEntries in services/exportService.ts; ASSIGNMENT_MD_SPEC.md §13 is the contract.

Changed 2026-09-06, twice. The two student files were briefly loose at the archive root, because the package then held three files — one of them an editable CSV — and a student opening it faced a confusing choice. The map now travels inside the spec, so that reason has gone, and the loose arrangement had a worse problem: an instructor building a Canvas assignment attaches one file, and two loose files means selecting and attaching both. The package is back, holding exactly two files, and it is the single entry an instructor posts.


Question types

The type-to-export mapping, for a developer or for whoever writes a consumer of the rubric. Which type to choose, and what each asks of a student, is in the guide.

Which types are available depends on the assignment's input mode. An electronic assignment uses Text, Image and Text + Image; a handwritten one uses Handwritten for every part. The editor enforces this, and switching an existing assignment between modes converts its parts.

A person decides every grade. The last column says what the grading side produces for each type. Nothing here awards a mark on its own, and a suggested score reaches the teaching assistant only if they choose to see it.

Editor selection .md tag SubmissionType grading_type What the grading side produces
Text · Human [text] Text human Nothing automatic; the answer is passed through
Text · AI: Binary [ai-graded:binary] AI Graded: Binary ai A suggested score and feedback — 2 bands, 20 word guide
Text · AI: Short [ai-graded:short] AI Graded: Short ai A suggested score and feedback — 3 bands, 50 word guide
Text · AI: Medium [ai-graded:medium] AI Graded: Medium ai A suggested score and feedback — 4 bands, 100 word guide
Text · AI: Long [ai-graded:long] AI Graded: Long ai A suggested score and feedback — 5 bands, 150 word guide
Image [image], [image:N] Image human_image Nothing automatic; the upload is passed through. There is no other option for an image
Text + Image [text+image], [text+image:N] Text and Image human Nothing automatic; the text and the images are passed through together
Handwritten · AI [handwritten] Handwritten ai_handwritten A transcript of the page crop, and a suggested score
Handwritten · Human [handwritten:human] Handwritten human_handwritten A transcript of the page crop

The word figures are guidance shown to the student, not gates — neither app blocks a submission on word count. pages / image pages sets maxImages on an electronic image part; a handwritten part takes no page count, because pages are an assignment-level pool.

The handwritten sheet

The app generates a QR-registered sheet: the student prints it at 100%, writes in the bordered answer box under each part, photographs the pages, and the Submission app uses the QR codes and corner marks to find each box and crop it.

Two things are set per part rather than per assignment, both on a > template: line — see the .md format below:

  • How much room the student gets. Each part reserves a number of writing lines. A part that asks for more room than a page can give moves to a page that can hold it — page count flexes, and question text is never shrunk to fit.
  • Whether the answer is a drawing. A sketch part reserves the same space with no ruled lines, and declares itself as a figure rather than as writing so the grading side does not send a circuit diagram to a text transcriber.

Page 1 is always an instructions page, carrying the standing instructions and the author's preamble and no questions; problems begin on page 2. It is not cropped and is not graded. The full page format is in ASSIGNMENT_MD_SPEC.md §10.

There is no name, student ID or date field on the sheet, deliberately — see Data & Privacy.

Point totals

Subsection points are scaled to targetPoints — the editor's Target field, 100 on a new assignment. An imported file's own total becomes the target, so a round trip through Import and Export rescales nothing. When the authored total and the target disagree, every download stops and asks first; the question is raised inside services/exportService.ts rather than at the four call sites, so a fifth download cannot forget it.


Markdown Assignment Format

Assignments can be authored as plain .md files and imported directly into the app.

File Structure

Two equivalent formats are supported — use whichever is more natural:

Multi-subsection problems (standard — required when a problem has more than one part):

# {CourseCode}: {Assignment Title}

**Preamble:** One or two sentences of general instructions for students.

## Problem {N}: {Problem Name}
Optional problem description shared across subsections.

### ({letter}) {Subsection Name} [{points} pts] [{type}]
Subsection description. LaTeX supported (see Math notation).

> grading_prompt: Rubric text here. (ai-graded subsections only)

Flat single-subsection problems (shorthand — points and type on the ## line):

## Problem {N}: {Problem Name} [{points} pts] [{type}]
Subsection description. LaTeX supported (see Math notation).

> grading_prompt: Rubric text here. (ai-graded subsections only)

The parser auto-promotes a flat problem into a single (a) subsection on import. Both formats round-trip correctly through Export .md.

Submission Type Tags

Tag Creates Notes
[text] Text answer box Reviewed by a person
[image] Single image upload Reviewed by a person
[image:N] Image upload, N pages e.g. [image:6] for a quiz transcript
[text+image] Text answer + single image upload Reviewed by a person, text and images together
[text+image:N] Text answer + N image pages e.g. [text+image:2]
[ai-graded:binary] Yes/no free-text; grading_type: "ai" 20 word guide; 2 grading bands
[ai-graded:short] Short free-text; grading_type: "ai" 50 word guide; 3 grading bands
[ai-graded:medium] Medium free-text; grading_type: "ai" 100 word guide; 4 grading bands
[ai-graded:long] Long free-text; grading_type: "ai" 150 word guide; 5 grading bands
[handwritten] Handwritten answer; transcribed, with a suggested score Handwritten assignments only
[handwritten:human] Handwritten answer; transcribed, and the TA grades from the crop Handwritten assignments only

The first seven are for electronic assignments; the two handwritten tags are for handwritten ones. A handwritten assignment declares itself with a **Input:** handwritten line under the title — absent means electronic. A practice assignment declares **Kind:** reader the same way — absent means conventional, and reader requires **Input:** handwritten, so a file that declares one without the other is refused on import. Both travel to the grading rubric and neither travels to the student.

Handwritten parts take no image count. Pages are an assignment-level pool, not a per-part count, so [handwritten:3] is not a thing.

The > template: line (handwritten only)

One optional line per handwritten sub-part, controlling the printed sheet:

### (a) Derive the cutoff frequency [12 pts] [handwritten]
State your assumptions and show the algebra.

> template: lines=14
> grading_prompt: Required elements: (1) ...; (2) ...

### (b) Sketch the field pattern [8 pts] [handwritten:human]
Label the direction and the relative magnitude.

> template: sketch
> grader_note: Look for ...
Option Effect
lines=N Reserve N writing lines for this answer. The lines printed are exactly the region the layout map crops, so this is the space the grader sees. Absent means 6
sketch A drawing region: the same reserved space, left blank with no rules, and declared as answer_modality: "figure" in the rubric

Both are optional and order-free — > template: sketch alone is valid, and so is > template: lines=20, sketch. A part that asks for more lines than fit beneath its own problem stem is moved to a page that can give them rather than being silently shortened.

Which grading block a handwritten part takes follows its tag: [handwritten] takes > grading_prompt:, and [handwritten:human] takes > grader_note:.

Math notation (LaTeX)

Subsection descriptions support LaTeX math, rendered with KaTeX.

  • Inline: single dollars, $...$ — e.g. $V_x = 6\,\text{V}$, $I = 0.1\,V_x$.
  • Display: double dollars, $$...$$ — a centered block equation.
  • Use LaTeX for anything with structure: subscripts $V_x$, fractions $\frac{17}{7}$, exponentials $e^{-0.2(t-8)}$, Greek and units $\Omega$. Plain text is fine for a bare symbol with no structure.
  • Every $ must be paired; an inline expression may not contain a $; a literal dollar sign in prose will be mis-parsed as a delimiter. Invalid LaTeX is never dropped silently: KaTeX flags the offending part in the rendered output (rendering uses throwOnError: false), and if rendering fails outright the raw expression is shown with its delimiters. Keep the LaTeX valid.

Single-dollar inline works because rendering uses a custom splitter (components/FormattedText.tsx), not KaTeX auto-render. The exported PDF (services/exportService.ts) uses the same $...$ and $$...$$ delimiters, and the Student Submission app uses the same convention, so what you preview is what the student sees.

Grading Prompt Format

Every [ai-graded:*] subsection must have a > grading_prompt: block. The rubric must be fully self-contained — the grading side sees only the rubric and the student's response, nothing else.

Every rubric (except binary) must begin with a Required elements: list. Bands are defined by how many elements are present.

Binary (2 bands):

> grading_prompt: The correct answer is YES. Award full marks for any response that
> clearly and correctly answers yes to the question, regardless of phrasing used.
> Award no credit for responses that give the incorrect answer or are non-committal.
> Do not deduct marks for grammar or writing style.

Short (3 bands):

> grading_prompt: Required elements: (1) [complete technical statement]; (2) [complete technical statement].
> Award full marks for responses that correctly address both elements.
> Award partial credit for responses that correctly address only one element, or address both with a significant inaccuracy.
> Award no credit for responses that address neither element or are off-topic.
> Do not deduct marks for grammar or writing style.

Medium (4 bands) and Long (5 bands) follow the same pattern with 3 and 4 required elements respectively.

Complete Example

# DEMO101: Lab 1 Prelab

**Preamble:** Complete all problems before your scheduled lab session.

## Problem 1: AI Exploration

### (a) Original quiz prompt draft [5 pts] [image]
Take a screenshot of your draft prompt and add your name before uploading.

Your name must be visible in the image before uploading.

### (b) Quiz transcript [10 pts] [image:6]
Run the quiz and capture the complete exchange. Zoom your browser out if needed to fit more content per image.

Your name must be visible in the image before uploading.

## Problem 2: Formal Reflection

### (a) Written reflection [75 pts] [ai-graded:long]
Write a formal reflection of 150–250 words addressing the three required points.

> grading_prompt: Required elements: (1) differential wiring protects signal quality by measuring
> the voltage difference between two lines rather than one line against ground, so equal noise on
> both lines cancels at the differential input; (2) a specific mechanism term (common-mode rejection
> or quantization error) is used to explain a physical process, not merely named; (3) a specific
> concrete wiring mistake is identified with its observable consequence.
> Award full marks for responses that correctly address all three elements.
> Award most marks for responses that correctly address two elements, with one minor gap.
> Award partial credit for responses that correctly address one element.
> Award minimal credit for responses that correctly address only one element partially.
> Award no credit for responses that address none of the elements or are off-topic.
> Do not deduct marks for grammar or writing style.

## Problem 3: Software Installation

### (a) Scopy screenshot [10 pts] [image]
Connect your M2K, open Scopy, and upload a screenshot confirming device recognition.

Your name must be visible in the image before uploading.

Point total: 5 + 10 + 75 + 10 = 100 pts


Grading Rubric JSON

The exported {Course}_{Title}_grading_rubric.json is the file your Gradescope autograder reads. Keep it private — do not distribute to students.

{
  "assignment_id": "DEMO101_Lab1_Prelab",
  "course_code": "DEMO101",
  "assignment_title": "Lab 1 Prelab",
  "assignment_kind": "conventional",
  "input_mode": "electronic",
  "rubrics": {
    "p1s0": {
      "subsection_id": "p1s0",
      "max_points": 75,
      "grading_type": "ai",
      "answer_modality": "text",
      "grading_prompt": "Required elements: (1) ...; (2) ...",
      "min_words": 150
    },
    "p0s0": {
      "subsection_id": "p0s0",
      "max_points": 5,
      "grading_type": "human_image",
      "grading_prompt": ""
    }
  }
}

Top-level fields, all five always present:

Field Values What it is
assignment_id {CourseCode}_{Title} The id the submission is matched against
course_code free text As authored
assignment_title free text As authored
assignment_kind "conventional" | "reader" Whether this is graded at all. conventional is a graded assignment; reader is practice — handwritten work is read back to the student and nothing enters the course record
input_mode "handwritten" | "electronic" How the work was produced

assignment_kind and input_mode are always emitted, never conditional. A consumer that has to tell conventional apart from the field was absent so I assumed conventional is a consumer that will one day assume wrong, and this is the field that decides whether a submission is graded or read back as practice. input_mode is resolved at export rather than passed through, so an assignment with no mode set says "electronic" rather than saying nothing. Neither field reaches the student: the rubric is the instructor's file.

The rubric never carries a model name, a temperature or a token budget. The Assignment Maker describes the work; the grading system decides how to grade it and allocates its own resources. A test fails if any exported artifact grows one. See ASSIGNMENT_MD_SPEC.md §12.

grading_type values:

A person decides every grade. Each value below says what the grading side produces; none of them awards a mark on its own.

Value What the grading side produces Who decides the grade
"ai" A suggested score and feedback for a written answer A person
"human" Nothing automatic; the answer is passed through (also used for Text + Image) A person
"human_image" Nothing automatic; the upload is passed through A person
"ai_handwritten" A transcript of the page crop, and a suggested score A person
"human_handwritten" A transcript of the page crop A person

"ai_image_completion" was removed on 2026-09-22. It awarded full marks for any upload with nobody looking at it, which is the one thing no grading type may do. An older file that sets an image part to it is reported on import and the part is reviewed by a person instead.

answer_modality is optional: "text" (a written answer), "figure" (a drawing — a handwritten part authored > template: sketch), or absent where the app does not know — an [image] or [text+image] part is answered with a picture but carries no modality declaration. "hybrid" is reserved and not emitted. Do not read an absent field as "text". Every part of a handwritten assignment carries it, and it agrees with is_drawing in layout_{TemplateID}.csv.


Data & Privacy

Student privacy

This tool collects no student-identifying information, and prints none.

  • No export path carries a name or student ID field, in either input mode. There is no such line on a handwritten sheet, on a typed sheet, or in the LaTeX source. A test asserts it on the built artifact in both modes and fails if one is reintroduced.
  • The handwritten sheet tells the student not to write one: "Do not write your name or student ID anywhere on these pages. You are identified when you upload."
  • Identity is established by the student's authenticated upload to their institution's learning management system, under the agreement the institution already holds. This tool is not part of that step and never sees the result.
  • The student file carries no grading material. {stem}_OPEN_IN_APP.json is built from an explicit whitelist, and a content check asserts that no grading prompt, grader note or reference answer reaches any student-facing artifact, by content rather than by field name.

Where your data goes

Your assignments never leave your browser. They are held in localStorage, and every export is generated locally and saved by your browser's own download. There is no account, no backend and no telemetry: nothing you author is transmitted anywhere.

The page contacts nothing but the server it was served from. Not the CSS framework, not the typefaces, not the maths renderer — all of them are compiled or embedded into the app's own files and served from the same origin. Loading the built app and recording every request, the list of third-party origins fetched at page load, and after exercising the dashboard, the editor and the preview, is empty.

Exports are built in the browser and saved through your browser's own download. There is no upload step anywhere in this app, which is why your assignments staying local is a property of its design rather than a promise about a server.

A test asserts it, on the built output rather than on the source: any host in a fetch position — in the HTML, in the stylesheet, or in any emitted script — fails the suite. Inter and Merriweather are served from this site under their SIL Open Font Licences, which the build emits alongside them.

  • Export your JSON regularly — data is lost if browser cache is cleared
  • The export ZIP is instructor-only and MUST NOT be given to students. Four files in its instructor/ folder contain answers: {stem}_grader_document.html, {stem}_grading_rubric.json, {stem}_authoring_backup.json and {stem}.md. Students receive only {stem}_FOR_STUDENTS.zip, the one entry at the archive root that is not in a folder. The ZIP carries a generated 00_INSTRUCTOR_ONLY_DO_NOT_DISTRIBUTE.txt at its root naming every file.
  • {stem}_authoring_backup.json (in instructor/) is the file that restores an assignment completely — the whole authoring object, unencrypted, including the grading prompts, grader notes, answer-space settings and the point target. It is what Import JSON should be given. {stem}_OPEN_IN_APP.json (the student spec) restores only what a student needs, and Import Markdown misses config; both now say so on import rather than losing your work silently. (Import Markdown used to miss targetPoints too — fixed 2026-09-01, it now reads the file's own total.)
  • aiGradingPrompt and graderNote are NOT in the student spec ({stem}_OPEN_IN_APP.json). The student's file is built from an explicit whitelist of the fields the Submission app reads, so no grading prompt, grader note, answer key or reference solution travels to a student's browser. Until 2026-08-31 the spec was the whole assignment object and did carry every prompt — if you hold an export made before then, treat its rubrics as disclosed. Nothing had been distributed.
  • To reload an assignment as a template, use Export .mdImport Markdown, which carries the prompts and grader notes in full. Importing an exported student spec restores the questions but not the grading material, because that material is no longer in the file.
  • Your rubrics reach the autograder by their proper route, {Course}_{Title}_grading_rubric.json, which stays with you

Course public key — removed 2026-09-21

There is no longer a course public key, and no key box in the editor. Submission encryption has been removed from the pipeline: student work now travels in plaintext over TLS with integrity by hash, and the campus host receives only the answer-region crops. There is nothing a per-course key would do, so nothing asks you for one.

If you already set one, you need do nothing. An assignment, an authoring backup, or a .md with a ```pem block at the top still imports. The key is discarded and the import tells you so — not because the leftover key is harmful, but because otherwise you might reasonably go on believing your students' submissions are still being sealed with it. They are not.

What a submission actually contains is not specified here. This app does not build submissions, and a second copy of another app's contract drifts the moment that contract moves. The payload, its encodings and its filenames are specified in the Student Submission app's AUTOGRADER_ZIP_SPEC.md, which governs.

Identity comes from the student's authenticated upload to their institution's LMS.

gb1 is unchanged and is a different thing. The gb1: encoding on {stem}_OPEN_IN_APP.json — the assignment file students load — is still applied. It is tamper resistance on the assignment, not confidentiality of a submission, and its key ships inside the app by design.


Local Development

git clone https://github.com/BridgeSuite/GradeBridge-Assignment-Maker.git
cd GradeBridge-Assignment-Maker
npm install
git config core.hooksPath .githooks   # runs tsc and the suite before a push

npm run dev       # → http://localhost:3000/GradeBridge-Assignment-Maker/
npm run build     # production build
npm run test      # the full suite — see tests/README.md
npm run deploy    # deploy to GitHub Pages (SSH remote required)

npm test runs five suites in one command: the app's own tests, the page-format template tests, a real production build inspected for what it emits, and two scans of the repository itself (no personal name, absolute path or image metadata; no tracked session document). CI runs the same thing on every push.

Tech stack: React 18 · TypeScript · Vite · Tailwind CSS · KaTeX · jsPDF · JSZip · Lucide


Troubleshooting

Issue Solution
Import Markdown fails Check heading levels (#, ##, ###) and tag format ([N pts] [type]). Both flat and subsection formats are supported — see Markdown Format above
LaTeX not rendering Check the expression: every $ must be paired, and an inline $...$ may not contain a $. KaTeX is bundled into the app, so this is never a network problem — invalid LaTeX is flagged in place rather than dropped
PDF generation slow Large images slow down PDF generation — reduce image count or size
Lost work Export JSON backup regularly; localStorage is cleared with browser cache
Deploy returns 403 SSH remote required — run git remote set-url origin git@github.com:BridgeSuite/GradeBridge-Assignment-Maker.git

Links


MIT License · © 2026 The Regents of the University of California · Provided free by UC Davis