Sync Canvas LMS export feature + refreshed Settings screenshot

Brings the share build up to date with the main project: QTI .zip export
and optional direct Canvas API push, plus the Canvas settings card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:43:35 -04:00
co-authored by Claude Opus 4.8
parent 3c4b80a6d8
commit 4036ad5839
15 changed files with 1125 additions and 1 deletions
+63
View File
@@ -22,6 +22,7 @@ export default function SettingsPage() {
const [modelsBusy, setModelsBusy] = useState("");
const [modelsErr, setModelsErr] = useState({}); // provider -> error
const [test, setTest] = useState({}); // provider -> {busy, ok, message}
const [canvasTest, setCanvasTest] = useState(null); // {busy, ok, message}
const [autoInfo, setAutoInfo] = useState(null); // resolved auto limits for the active model
const [saving, setSaving] = useState(false);
const [toast, setToast] = useState("");
@@ -78,6 +79,27 @@ export default function SettingsPage() {
setS((cur) => ({ ...cur, profile: { ...(cur.profile || {}), [field]: value } }));
}
function setCanvas(field, value) {
setCanvasTest(null);
setS((cur) => ({ ...cur, canvas: { ...(cur.canvas || {}), [field]: value } }));
}
async function testCanvas() {
setCanvasTest({ busy: true });
try {
const res = await fetch("/api/canvas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "test", settings: s }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Test failed.");
setCanvasTest({ ok: true, message: data.message });
} catch (e) {
setCanvasTest({ ok: false, message: String(e.message || e) });
}
}
// Downscale the logo client-side (max 512px) so it stays a small data URL in db.json
// while staying crisp at the printed header size.
function onLogoFile(e) {
@@ -308,6 +330,47 @@ export default function SettingsPage() {
</div>
</div>
<div className="card">
<h2>Canvas (LMS) integration</h2>
<p className="field-hint" style={{ marginTop: 4 }}>
Optional. Lets you push a finished assignment straight into a Canvas course from the editor.
You can always skip this and use the downloadable Canvas <code>.zip</code> instead.
</p>
<label className="field" style={{ marginTop: 14 }}>
<span className="field-label">Canvas web address</span>
<input
type="text" value={s.canvas?.baseUrl || ""}
onChange={(e) => setCanvas("baseUrl", e.target.value)}
placeholder="https://yourschool.instructure.com"
autoComplete="off"
/>
<span className="field-hint">The address you use to log into Canvas e.g. <code>https://yourschool.instructure.com</code>.</span>
</label>
<label className="field">
<span className="field-label">Access token</span>
<input
type="password" value={s.canvas?.token || ""}
onChange={(e) => setCanvas("token", e.target.value)}
placeholder="Paste your Canvas access token"
autoComplete="off"
/>
<span className="field-hint">
In Canvas: <b>Account Settings Approved Integrations + New Access Token</b>. Stored only on this computer.
Some schools restrict tokens if yours does, use the <code>.zip</code> export instead.
</span>
</label>
<div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
<button className="btn" onClick={testCanvas} disabled={canvasTest?.busy || !s.canvas?.baseUrl || !s.canvas?.token}>
{canvasTest?.busy ? <><span className="spinner" /> Testing</> : "Test connection"}
</button>
{canvasTest && !canvasTest.busy && (
<span className="small" style={canvasTest.ok ? { color: "var(--board)", fontWeight: 600 } : { fontWeight: 600 }}>
{canvasTest.ok ? "✓ " : "✕ "}{canvasTest.message}
</span>
)}
</div>
</div>
<div className="card">
<h2>Generation defaults</h2>
<label className="check" style={{ marginTop: 14 }}>