In your components, you might want to call the Examplary REST API.
API prop
All of your components will receive an api prop, which is an authenticated instance of Axios. You can use this prop to make API calls directly from your components.
In settings-area, results, and print components, this is authenticated against the currently signed-in user (teacher). In assessment components, it is authenticated against the student who is currently taking the assessment. Students usually only have access to public endpoints.
When your question type is exported as a Portable Custom Interaction (PCI) and rendered inside an external LMS, the api prop will be null. Any code that calls api.get(...), api.uploadFile(...), or any other method on api will throw an error in that context.
If your assessment component relies on API access, guard against this with a null check:
if (api) {
const data = await api.get("/some-endpoint");
}Basic usage
export default function MyQuestionSettingsArea({ api }) {
const fetchData = async () => {
try {
const response = await api.get("/question-types");
console.log(response.data);
} catch (error) {
console.error("Error fetching data:", error);
}
};
useEffect(() => {
fetchData();
}, []);
return <div>Check the console for data.</div>;
}Using the AI API
Public question components (i.e. assessment components) have the api object extended with an ai.generate() method that allows you to generate content using AI. It takes a list of messages in the standard chat format and returns the AI's response as a string.
import { useState, useEffect } from "react";
export default function MyQuestionAssessmentComponent({ question, api }) {
const [story, setStory] = useState("");
const generateStory = async () => {
const response = await api.ai.generate({
messages: [
{
role: "system",
content:
"You are a teacher telling the student a story about a subject.",
},
{
role: "user",
content: `Tell me a story about ${question.description}.`,
},
],
});
// response is a string when no schema is provided
setStory(response);
};
useEffect(() => {
generateStory();
}, []);
return <div>{story}</div>;
}You can also pass a schema key with a JSON Schema object to receive a structured data response instead of a plain string:
const response = await api.ai.generate({
messages: [
{
role: "user",
content: `Generate 3 quiz options for: ${question.description}`,
},
],
schema: {
type: "object",
properties: {
options: {
type: "array",
items: { type: "string" },
},
},
required: ["options"],
},
});
// response is a structured object matching the schema
console.log(response.options); // ["Option A", "Option B", "Option C"]File uploads
It's also possible to prompt the user to upload files, which will be stored in the Examplary cloud storage.
api.uploadFile(accept?)
Opens a file picker dialog and uploads the selected file. Available in all component types.
The optional accept parameter is a string that specifies the file types to accept (e.g., "image/*" for all image types).
import { useState } from "react";
export default function MyQuestionSettingsArea({ api }) {
const [fileUrl, setFileUrl] = useState("");
const handleUpload = async () => {
const { url } = await api.uploadFile("image/*");
setFileUrl(url);
};
return (
<div>
<button onClick={handleUpload}>Upload an image</button>
{fileUrl && <img src={fileUrl} alt="Uploaded" className="max-w-full" />}
</div>
);
}api.uploadPickedFile(file)
Uploads a File object that you have already obtained (e.g., from a drag-and-drop handler or a custom <input type="file">). Only available in settings-area, results, and print components.
import { useState } from "react";
export default function MyQuestionSettingsArea({ api }) {
const [fileUrl, setFileUrl] = useState("");
const handleDrop = async (event) => {
event.preventDefault();
const file = event.dataTransfer.files[0];
if (file) {
const { url } = await api.uploadPickedFile(file);
setFileUrl(url);
}
};
return (
<div
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
className="border-2 border-dashed p-8"
>
Drop a file here
{fileUrl && <img src={fileUrl} alt="Uploaded" className="max-w-full mt-4" />}
</div>
);
}