Skip to content

Documentation Updated

AIReady PracticeLayer User Guide

Guidance and examples for adding AI feedback, practice conversations, document-grounded responses, and audio to learning experiences.

On this page

A learner submits an input, AIReady processes it, and the course displays the response.

Choose what you want to build

Start here: Build a simple AI feedback coach in Storyline

In this example, the learner types a response in Storyline, clicks Submit, and receives short AI-generated feedback on the slide.

Step 1: Create the Storyline variables

Create these Storyline text variables:

  • TextEntry — stores what the learner types
  • GPT_Response — stores the response returned by AIReady

Step 2: Set up the slide

  1. Add a text-entry field connected to TextEntry.
  2. Add a text box that displays %GPT_Response%.
  3. Add a Submit button.

Step 3: Add the JavaScript

Add an Execute JavaScript trigger to the Submit button and paste the complete code below. Replace REPLACE_WITH_YOUR_AIREADY_KEY. You can also change the coaching instructions inside askLearnerQuestion().

API key: Use the key provided for your AIReady implementation. Keep it out of screenshots, source control, and shared materials.

var player = GetPlayer();
var AIREADY_ENDPOINT = "https://api.arthalearning.com/v2/responses";
var AIREADY_API_KEY = "REPLACE_WITH_YOUR_AIREADY_KEY";
var FALLBACK_TEXT = "We can't analyze your answer right now. Please try again later. In the meantime, review and reflect on your course content.";

function outputText(data) {
  var message = (data.output || []).find(function (item) { return item.type === "message"; });
  var part = message && (message.content || []).find(function (item) { return item.type === "output_text"; });
  return part && typeof part.text === "string" ? part.text : "";
}

async function errorDetail(response) {
  var body = await response.text();
  try {
    var parsed = JSON.parse(body);
    return typeof parsed.detail === "string" ? parsed.detail : body;
  } catch (_error) {
    return body || response.statusText;
  }
}

async function requestAIReady(requestData) {
  try {
    var response = await fetch(AIREADY_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + AIREADY_API_KEY
      },
      body: JSON.stringify(requestData)
    });
    if (!response.ok) throw new Error("HTTP " + response.status + ": " + await errorDetail(response));

    var data = await response.json();
    var text = outputText(data);
    if (!text) throw new Error("The response did not include output text.");
    return { data: data, text: text, error: null };
  } catch (error) {
    console.error("AIReady request failed:", error.message);
    return { data: null, text: FALLBACK_TEXT, error: error };
  }
}

async function askLearnerQuestion() {
  var learnerInput = player.GetVar("TextEntry");
  var result = await requestAIReady({
    input: [
      { role: "system", content: "You are a supportive coach. Give feedback on the learner response in three sentences." },
      { role: "user", content: learnerInput }
    ],
    activity_id: "open-question"
  });
  player.SetVar("GPT_Response", result.text);
}

askLearnerQuestion();

Step 4: Test the interaction

  1. Publish the course to the web or your LMS.
  2. Enter a learner response.
  3. Click Submit.
  4. Confirm that the AIReady response appears on the slide.

Storyline Preview does not provide the hosted environment this interaction requires.

What the code is doing

GetPlayer() connects the JavaScript to Storyline variables. The AI instructions define what the coach should do, while the learner’s response is sent separately. AIReady returns the result and the code places it in GPT_Response.

Change what the coach does

The AI instructions control how the coach responds. The learner’s response is passed separately. Change the instructions to adjust the coach’s role, the type of feedback, tone, response length, or output format.

{ role: "system", content: "You are a communication-skills coach. Give supportive, specific feedback in no more than three sentences." }

Reuse this setup

The examples below use the connection and request code from this walkthrough. For each new interaction, start with the complete code above and replace the final interaction function with the function shown in that section. Keep the connection and request code at the top of the trigger.

Roleplay

Use a practice conversation when learners should exchange several messages with an AI character. The interaction keeps the conversation history so AIReady can respond in context.

  1. Create four Storyline text variables: UserInput, GPT_Response, ConversationHistory, and PreviousChat. Leave their default values blank.
  2. Add a text-entry field connected to UserInput.
  3. Add a conversation display area containing %PreviousChat%. Enable its overflow scrollbar if needed.
  4. Add a Send button.
  5. Add an Execute JavaScript trigger to the Send button.
  6. Start with the complete code from the first walkthrough. Replace askLearnerQuestion() and its final call with the code below.
var userInput = player.GetVar("UserInput");
var conversationHistoryText = player.GetVar("ConversationHistory") || "[]";
var displayHistory = player.GetVar("PreviousChat") || "";

function escapeHtml(value) {
  return String(value).replace(/[&<>"']/g, function (character) {
    return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[character];
  });
}

async function continueRoleplay() {
  var history;
  try { history = JSON.parse(conversationHistoryText); } catch (_error) { history = []; }
  if (!history.length) history.push({
    role: "system",
    content: "Roleplay as an angry airline customer. The learner is an airline agent. Reply in two lines and do not add speaker tags."
  });
  history.push({ role: "user", content: userInput });

  var result = await requestAIReady({ input: history, activity_id: "airline-roleplay" });
  if (result.error) return player.SetVar("GPT_Response", result.text);

  history.push({ role: "assistant", content: result.text });
  player.SetVar("ConversationHistory", JSON.stringify(history));
  player.SetVar("GPT_Response", result.text);
  player.SetVar("PreviousChat", displayHistory + (displayHistory ? "<br><br>" : "") +
    "<b>You:</b> " + escapeHtml(userInput) + "<br><br><b>Customer:</b> " + escapeHtml(result.text));
}

continueRoleplay();

Documents

The Documents add-on is available with AIReady Business and Enterprise subscriptions. If Documents is available for your account, you can reference one or more uploaded documents by adding their document IDs to the interaction. You can sign in to the AIReady portal to find the IDs for your documents.

You can select up to 15 unique document IDs in one interaction.

One document

Start with the complete code from the first walkthrough. Replace askLearnerQuestion() and its final call with the code below. Replace Acme_HR_Policy_12345 with the document ID shown in your portal.

async function answerFromOneDocument() {
  var result = await requestAIReady({
    input: [
      { role: "system", content: "<document>Acme_HR_Policy_12345</document>\nAnswer using the selected reference." },
      { role: "user", content: player.GetVar("TextEntry") }
    ],
    activity_id: "hr-policy-practice"
  });
  player.SetVar("GPT_Response", result.text);
}

answerFromOneDocument();

Multiple documents and document-grounded roleplay

Add another <document>...</document> tag for each document. This example uses the Storyline text variables UserInput, GPT_Response, and DocumentConversationHistory. Start with the complete code from the first walkthrough, then replace askLearnerQuestion() and its final call with the code below. Replace both example document IDs with IDs from your portal.

async function continueDocumentRoleplay() {
  var history;
  try {
    history = JSON.parse(player.GetVar("DocumentConversationHistory") || "[]");
  } catch (_error) {
    history = [];
  }
  if (!history.length) {
    history.push({
      role: "system",
      content: "You are a manager-coaching roleplay. <document>Acme_HR_Policy_12345</document> <document>Acme_Manager_Guide_67890</document>"
    });
    history.push({ role: "assistant", content: "What would you like to discuss?" });
  }

  history.push({ role: "user", content: player.GetVar("UserInput") });
  var result = await requestAIReady({ input: history, activity_id: "policy-roleplay" });
  player.SetVar("GPT_Response", result.text);
  if (!result.error) {
    history.push({ role: "assistant", content: result.text });
    player.SetVar("DocumentConversationHistory", JSON.stringify(history));
  }
}

continueDocumentRoleplay();

Helpful note: AIReady uses relevant passages from the selected documents as grounding context. If the documents may not contain the answer, tell the model to say when there is not enough information.

Do not promise citations or exhaustive reading. A missing, invalid, or unavailable document may still produce a response without useful grounding context.

Audio

Add audio when the interaction should speak the AIReady response as well as display it. The written response remains available if audio cannot be created or played.

  1. Create the Storyline text variables TextEntry, GPT_Response, and Audio_Status.
  2. Create the Storyline number variable Audio_Play_State.
  3. Add the learner input, response display, Submit button, and a visible Play Audio button to the slide.
  4. Add an Execute JavaScript trigger to the Submit button.
  5. Start with the complete code from the first walkthrough. Replace askLearnerQuestion() and its final call with the code below.
  6. Add a second Execute JavaScript trigger to the Play Audio button and use the retry code shown after the main example.
  7. Change the coaching instructions or audio settings if needed.
var audioState = window.__aiReadyAudioState || { audio: null, objectUrl: null };
window.__aiReadyAudioState = audioState;

function clearAudio() {
  if (audioState.audio) audioState.audio.pause();
  if (audioState.objectUrl) URL.revokeObjectURL(audioState.objectUrl);
  audioState.audio = null;
  audioState.objectUrl = null;
}

function playBase64Wav(base64Data) {
  clearAudio();
  var bytes = Uint8Array.from(atob(base64Data), function (character) { return character.charCodeAt(0); });
  audioState.objectUrl = URL.createObjectURL(new Blob([bytes], { type: "audio/wav" }));
  audioState.audio = new Audio(audioState.objectUrl);
  audioState.audio.addEventListener("ended", function () { player.SetVar("Audio_Play_State", 3); });
  player.SetVar("Audio_Play_State", 1);
  return audioState.audio.play();
}

async function askWithAudio() {
  clearAudio();
  var result = await requestAIReady({
    input: [
      { role: "system", content: "You are an AI coach. Answer the learner in one sentence." },
      { role: "user", content: player.GetVar("TextEntry") }
    ],
    activity_id: "spoken-coaching",
    modalities: ["text", "audio"],
    audio: { voice: "en-US-AriaNeural", format: "wav", style: "chat", rate: "medium", pitch: "default" }
  });
  player.SetVar("GPT_Response", result.text);
  if (!result.data) {
    player.SetVar("Audio_Play_State", 3);
    return player.SetVar("Audio_Status", "Audio is unavailable.");
  }

  var audioItem = (result.data.output || []).find(function (item) { return item.type === "aiready:output_audio"; });
  if (audioItem && audioItem.status === "completed" && audioItem.data) {
    player.SetVar("Audio_Status", "");
    playBase64Wav(audioItem.data).catch(function () {
      player.SetVar("Audio_Play_State", 2);
      player.SetVar("Audio_Status", "Audio is ready. Select Play Audio to hear it.");
    });
  } else {
    // A failed item has data: null and error.code: tts_failed. Written text remains shown.
    player.SetVar("Audio_Play_State", 3);
    player.SetVar("Audio_Status", "Audio is unavailable; the written response is still available.");
  }
}

askWithAudio();

If the browser blocks automatic playback while the response is loading, the audio remains available. Paste this code into the Play Audio button’s Execute JavaScript trigger. Because the learner selects the button directly, the browser can treat playback as a new user action.

var player = GetPlayer();
var audioState = window.__aiReadyAudioState;

if (!audioState || !audioState.audio) {
  player.SetVar("Audio_Play_State", 3);
  player.SetVar("Audio_Status", "No audio is available yet.");
} else {
  player.SetVar("Audio_Play_State", 1);
  player.SetVar("Audio_Status", "");
  audioState.audio.play().catch(function () {
    player.SetVar("Audio_Play_State", 2);
    player.SetVar("Audio_Status", "Playback is still blocked. Select Play Audio again or check the browser's audio settings.");
  });
}

Technical note: Audio requires modalities: ["text", "audio"]. The supported output format is WAV. voice, style, rate, and pitch are optional settings; a voice may not support every style. Keep the browser Audio object in JavaScript rather than in a Storyline variable.

Streaming

Use streaming when you want learners to see the response appear as it is generated. Get a normal interaction working first.

Create the Storyline text variables TextEntry and GPT_Response, then set up the same slide elements used in the first walkthrough. Start with the complete code from that walkthrough and replace askLearnerQuestion() and its final call with the code below.

async function streamAIReady(requestData, onDelta, onCompleted) {
  var response = await fetch(AIREADY_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "text/event-stream",
      "Authorization": "Bearer " + AIREADY_API_KEY
    },
    body: JSON.stringify(Object.assign({}, requestData, { stream: true }))
  });
  if (!response.ok) throw new Error("HTTP " + response.status + ": " + await errorDetail(response));

  var reader = response.body.getReader();
  var decoder = new TextDecoder();
  var buffer = "";
  var completed = false;
  while (true) {
    var chunk = await reader.read();
    buffer += decoder.decode(chunk.value || new Uint8Array(), { stream: !chunk.done });
    var boundary;
    while ((boundary = /\r\n\r\n|\n\n|\r\r/.exec(buffer)) !== null) {
      var lines = buffer.slice(0, boundary.index).split(/\r\n|\n|\r/);
      buffer = buffer.slice(boundary.index + boundary[0].length);
      var eventName = "message";
      var dataText = lines.filter(function (line) { return line.indexOf("data:") === 0; })
        .map(function (line) { return line.slice(5).trim(); }).join("\n");
      lines.forEach(function (line) { if (line.indexOf("event:") === 0) eventName = line.slice(6).trim(); });
      if (!dataText) continue;
      if (dataText === "[DONE]") {
        if (!completed) throw new Error("The stream ended before a completed response.");
        return;
      }
      var data;
      try { data = JSON.parse(dataText); } catch (_error) { continue; }
      if (eventName === "response.output_text.delta") onDelta(data.delta || "");
      else if (eventName === "response.completed") { completed = true; onCompleted(data.response); }
      else if (eventName === "error") throw new Error(data.error && data.error.message ? data.error.message : "The stream stopped unexpectedly.");
    }
    if (chunk.done) {
      if (!completed) throw new Error("The stream closed before a completed response.");
      return;
    }
  }
}

async function streamLearnerAnswer() {
  var text = "";
  player.SetVar("GPT_Response", "");
  try {
    await streamAIReady(
      { input: [
        { role: "system", content: "Answer the learner in three sentences." },
        { role: "user", content: player.GetVar("TextEntry") }
      ] },
      function (delta) { text += delta; player.SetVar("GPT_Response", text); },
      function (completedResponse) { player.SetVar("GPT_Response", outputText(completedResponse) || text); }
    );
  } catch (error) {
    console.error("AIReady stream failed:", error.message);
    player.SetVar("GPT_Response", text || FALLBACK_TEXT);
  }
}

streamLearnerAnswer();

Technical note: Browser streaming uses fetch because the request is a JSON POST with a Bearer header. The stream sends text updates, a completed response event, and then data: [DONE]. It may instead send an error event after the connection opens. Audio output is delivered after the text completes rather than in streamed chunks.

Advanced integrations and technical reference

The sections below are intended for developers and technically advanced users. They are not required for the Storyline workflows above.

Choose an integration method

Choose a method based on where the interaction runs and whether the response should appear incrementally.

PathStart here whenMain tradeoff
Direct HTTP JSONYou use Storyline, browser JavaScript, another language, or want no SDK dependencyYou parse the response and errors yourself.
Streaming HTTPThe learner should see text arrive incrementallyMore complex event and failure handling.
Official OpenAI SDKYou already use the SDK in a server application and want its client ergonomicsAIReady supports a Responses subset, and account/origin behavior must be validated.

Direct HTTP and response handling

The Storyline examples send JSON requests directly to the AIReady Responses endpoint at https://api.arthalearning.com/v2/responses. A request includes input; optional activity_id and user_id values associate it with activity and learner reporting. Set model only when you need a model available to your account. Otherwise, AIReady uses the account default.

For normal requests, check response.ok and read the JSON detail field when present. Extract output text by item type rather than assuming a fixed array position.

Streaming protocol details

Streaming uses the same endpoint with stream: true. Browser code should use fetch; the basic EventSource constructor cannot send the required JSON POST and Bearer header. Handle text delta events, response.completed, data: [DONE], streamed error events, and a connection that closes before completion.

Official OpenAI SDK

AIReady supports a subset of the OpenAI Responses API through the official OpenAI SDK in server applications. This subset does not include tools, image or file input, persistence, previous_response_id, background jobs, or WebSocket Responses.

This complete Python example requires the official openai package plus AIREADY_API_KEY and AIREADY_MODEL environment variables on your server. Set AIREADY_MODEL to a model approved for your AIReady account. The SDK requires an explicit model; use the direct HTTP example above if you want AIReady to select your account’s default. The base URL ends at /v2; the SDK adds /responses when client.responses.create() is called.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["AIREADY_API_KEY"],
    base_url="https://api.arthalearning.com/v2",
)

response = client.responses.create(
    model=os.environ["AIREADY_MODEL"],
    input="Give concise coaching feedback on: The learner listened before responding.",
)

def output_text(response):
    for item in response.output:
        if item.type == "message":
            for part in item.content:
                if part.type == "output_text":
                    return part.text
    raise RuntimeError("AIReady did not return output text")

print(output_text(response))

Server-side considerations

AIReady checks configured allowed domains. A server-side SDK request normally has no browser Origin header, so test this path with a representative non-production account and its allow-list configuration before using it in production. Do not fabricate an Origin header; resolve an unsupported server-origin setup with the product team.

Errors and troubleshooting

Check the following:

  • Publish the course to the web or LMS instead of using Storyline Preview.
  • Verify the exact Storyline variable names.
  • Confirm the API key and AIReady endpoint.
  • Confirm that the hosting domain is on the allow-list.
  • Inspect the browser Console and Network panel for status codes and response details.
StatusWhat to check
400Prompt or content problem, unsupported model or schema, or more than 15 unique document IDs.
401Missing, inactive, or incorrect API key.
403Inactive account or an origin outside the configured allow-list.
422Invalid request shape, field type, or audio configuration.
429Account usage quota reached.
500Transient AIReady processing failure; let the learner retry.

For deeper troubleshooting, use the browser Console and Network panel to review the request status and response details. Never log or share API keys or learner-sensitive content.

Build in Articulate Rise

Articulate Rise Code Blocks can run HTML, CSS, and JavaScript directly. Use a Code Block when you want to build the complete AIReady interaction inside a Rise lesson without creating a separate Storyline project.

  1. Open the Rise lesson and add a Code block.
  2. Choose Add code to paste a self-contained interaction, or Upload project for a ZIP whose index.html file is at the root.
  3. Add the complete Rise-specific HTML, CSS, and JavaScript for the interaction.
  4. Replace the AIReady API-key placeholder and confirm that the published course’s domain is allowed for the AIReady account.
  5. If the learner must finish the interaction before continuing, enable the block’s completion requirement and call window.parent.postMessage({ type: "complete" }, "*"); when the activity is complete.
  6. Test the interaction from its published web or LMS location. An exported Code Block does not run when the course files are opened locally.

API key: Use the key provided for your AIReady implementation. Keep it out of screenshots, source control, and shared materials.

The Storyline examples above use Storyline variables and GetPlayer(). They cannot be pasted into a Rise Code Block unchanged. A Rise interaction needs its own HTML interface and reads learner input from that interface.

Copy a complete AI Coach

This example includes the interface and the V2 request code. Replace REPLACE_WITH_YOUR_AIREADY_KEY with the API key provided for your implementation, then paste the complete code into Add code. You can also save it as index.html, place it at the root of a ZIP file, and use Upload project.

Show the complete Rise AI Coach code
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AIReady AI Coach</title>
  <style>
    :root {
      color-scheme: light;
      --ink: #1e052e;
      --plum: #4a0987;
      --purple: #6321a4;
      --purple-dark: #431071;
      --lilac: #e5e8ff;
      --sky: #e5f7ff;
      --line: #e2e4f3;
      --muted: #675c70;
      --danger: #a32132;
      --white: #ffffff;
      --shadow: 0 18px 50px rgba(30, 5, 46, 0.12);
    }

    * { box-sizing: border-box; }

    body {
      min-height: 600px;
      margin: 0;
      color: var(--ink);
      background:
        radial-gradient(circle at 10% 0%, rgba(229, 247, 255, 0.95), transparent 38%),
        radial-gradient(circle at 100% 100%, rgba(229, 232, 255, 0.9), transparent 42%),
        #fafaff;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.5;
    }

    button, textarea { font: inherit; }

    .shell {
      display: grid;
      min-height: 600px;
      place-items: center;
      padding: 28px 18px;
    }

    .coach {
      width: min(100%, 760px);
      overflow: hidden;
      border: 1px solid var(--line);
      border-radius: 22px;
      background: rgba(255, 255, 255, 0.96);
      box-shadow: var(--shadow);
    }

    .coach__header {
      display: flex;
      gap: 16px;
      align-items: center;
      padding: 24px 26px;
      border-bottom: 1px solid var(--line);
      background: linear-gradient(135deg, var(--white), #f5f2ff);
    }

    .avatar {
      display: grid;
      flex: 0 0 48px;
      width: 48px;
      height: 48px;
      place-items: center;
      border-radius: 15px;
      color: var(--white);
      background: linear-gradient(145deg, var(--purple), var(--plum));
      font-weight: 700;
      letter-spacing: 0.03em;
      box-shadow: 0 7px 18px rgba(99, 33, 164, 0.22);
    }

    h1 {
      margin: 0;
      font-size: clamp(1.35rem, 3vw, 1.75rem);
      line-height: 1.2;
    }

    .subtitle {
      margin: 4px 0 0;
      color: var(--muted);
      font-size: 0.96rem;
    }

    .coach__body { padding: 26px; }

    label {
      display: block;
      margin-bottom: 9px;
      font-weight: 700;
    }

    textarea {
      display: block;
      width: 100%;
      min-height: 150px;
      resize: vertical;
      padding: 15px 16px;
      border: 1px solid #bdbbd0;
      border-radius: 13px;
      outline: none;
      color: var(--ink);
      background: var(--white);
      transition: border-color 160ms ease, box-shadow 160ms ease;
    }

    textarea:focus {
      border-color: var(--purple);
      box-shadow: 0 0 0 4px rgba(99, 33, 164, 0.13);
    }

    textarea::placeholder { color: #857c8b; }

    .field-meta {
      display: flex;
      justify-content: space-between;
      gap: 16px;
      margin-top: 8px;
      color: var(--muted);
      font-size: 0.82rem;
    }

    .actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 19px;
    }

    button {
      min-height: 46px;
      padding: 11px 18px;
      border-radius: 11px;
      border: 1px solid transparent;
      cursor: pointer;
      font-weight: 700;
      transition: transform 120ms ease, background 160ms ease, box-shadow 160ms ease;
    }

    button:hover:not(:disabled) { transform: translateY(-1px); }
    button:focus-visible { outline: 3px solid rgba(99, 33, 164, 0.28); outline-offset: 2px; }

    .button--primary {
      color: var(--white);
      background: var(--purple);
      box-shadow: 0 7px 16px rgba(99, 33, 164, 0.2);
    }

    .button--primary:hover:not(:disabled) { background: var(--purple-dark); }

    .button--secondary {
      color: var(--ink);
      border-color: var(--line);
      background: var(--white);
    }

    button:disabled { cursor: wait; opacity: 0.62; }

    .status {
      min-height: 24px;
      margin: 16px 0 0;
      color: var(--muted);
      font-size: 0.9rem;
    }

    .status[data-state="error"] { color: var(--danger); }

    .response {
      display: none;
      margin-top: 18px;
      padding: 19px 20px;
      border: 1px solid #d5d9fa;
      border-radius: 15px;
      background: linear-gradient(135deg, #f7f8ff, var(--sky));
    }

    .response[data-visible="true"] { display: block; }

    .response__label {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 0 0 8px;
      color: var(--plum);
      font-size: 0.82rem;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
    }

    .response__label::before {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--purple);
      content: "";
    }

    .response__text {
      margin: 0;
      white-space: pre-wrap;
      font-size: 1rem;
    }

    .privacy-note {
      margin: 19px 0 0;
      padding-top: 16px;
      border-top: 1px solid var(--line);
      color: var(--muted);
      font-size: 0.8rem;
    }

    .spinner {
      display: inline-block;
      width: 15px;
      height: 15px;
      margin-right: 8px;
      border: 2px solid rgba(255, 255, 255, 0.48);
      border-top-color: var(--white);
      border-radius: 50%;
      vertical-align: -2px;
      animation: spin 700ms linear infinite;
    }

    @keyframes spin { to { transform: rotate(360deg); } }

    @media (max-width: 540px) {
      .shell { padding: 14px; place-items: start center; }
      .coach { border-radius: 17px; }
      .coach__header, .coach__body { padding: 20px; }
      .field-meta { display: block; }
      .shortcut { display: none; }
      .actions { display: grid; grid-template-columns: 1fr; }
      button { width: 100%; }
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
      .spinner { animation: none; }
    }
  </style>
</head>
<body>
  <main class="shell">
    <section class="coach" aria-labelledby="coach-title">
      <header class="coach__header">
        <div class="avatar" aria-hidden="true">AI</div>
        <div>
          <h1 id="coach-title">AI Coach</h1>
          <p class="subtitle">Share your response and receive concise coaching feedback.</p>
        </div>
      </header>

      <div class="coach__body">
        <form id="coach-form">
          <label for="learner-input">Your response</label>
          <textarea id="learner-input" maxlength="2000" required placeholder="Describe how you handled the situation…"></textarea>
          <div class="field-meta">
            <span>Do not include confidential or personal information.</span>
            <span><span id="character-count">0</span>/2000</span>
          </div>

          <div class="actions">
            <button class="button--primary" id="submit-button" type="submit">Get coaching feedback</button>
            <button class="button--secondary" id="reset-button" type="button">Start over</button>
          </div>
        </form>

        <p class="status" id="status" role="status" aria-live="polite"></p>

        <section class="response" id="response" data-visible="false" aria-labelledby="response-label" tabindex="-1">
          <p class="response__label" id="response-label">Coach feedback</p>
          <p class="response__text" id="response-text"></p>
        </section>

        <p class="privacy-note">Your response is sent to AIReady when you select <strong>Get coaching feedback</strong>.</p>
      </div>
    </section>
  </main>

  <script>
    var AIREADY_ENDPOINT = "https://api.arthalearning.com/v2/responses";
    var AIREADY_API_KEY = "REPLACE_WITH_YOUR_AIREADY_KEY";
    var FALLBACK_TEXT = "We can't analyze your answer right now. Please try again later. In the meantime, review and reflect on your course content.";

    function outputText(data) {
      var message = (data.output || []).find(function (item) { return item.type === "message"; });
      var part = message && (message.content || []).find(function (item) { return item.type === "output_text"; });
      return part && typeof part.text === "string" ? part.text : "";
    }

    async function errorDetail(response) {
      var body = await response.text();
      try {
        var parsed = JSON.parse(body);
        return typeof parsed.detail === "string" ? parsed.detail : body;
      } catch (_error) {
        return body || response.statusText;
      }
    }

    async function requestAIReady(requestData) {
      try {
        var response = await fetch(AIREADY_ENDPOINT, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + AIREADY_API_KEY
          },
          body: JSON.stringify(requestData)
        });
        if (!response.ok) throw new Error("HTTP " + response.status + ": " + await errorDetail(response));

        var data = await response.json();
        var text = outputText(data);
        if (!text) throw new Error("The response did not include output text.");
        return { data: data, text: text, error: null };
      } catch (error) {
        console.error("AIReady request failed:", error.message);
        return { data: null, text: FALLBACK_TEXT, error: error };
      }
    }

    var form = document.getElementById("coach-form");
    var learnerInput = document.getElementById("learner-input");
    var characterCount = document.getElementById("character-count");
    var submitButton = document.getElementById("submit-button");
    var resetButton = document.getElementById("reset-button");
    var statusMessage = document.getElementById("status");
    var responsePanel = document.getElementById("response");
    var responseText = document.getElementById("response-text");

    learnerInput.addEventListener("input", function () {
      characterCount.textContent = String(learnerInput.value.length);
    });

    learnerInput.addEventListener("keydown", function (event) {
      if ((event.ctrlKey || event.metaKey) && event.key === "Enter") form.requestSubmit();
    });

    resetButton.addEventListener("click", function () {
      form.reset();
      characterCount.textContent = "0";
      statusMessage.textContent = "";
      statusMessage.removeAttribute("data-state");
      responseText.textContent = "";
      responsePanel.dataset.visible = "false";
      learnerInput.focus();
    });

    form.addEventListener("submit", async function (event) {
      event.preventDefault();
      var answer = learnerInput.value.trim();
      if (!answer) {
        statusMessage.textContent = "Enter a response before requesting feedback.";
        statusMessage.dataset.state = "error";
        learnerInput.focus();
        return;
      }

      submitButton.disabled = true;
      resetButton.disabled = true;
      submitButton.innerHTML = '<span class="spinner" aria-hidden="true"></span>Reviewing response';
      statusMessage.textContent = "AIReady is preparing your feedback.";
      statusMessage.removeAttribute("data-state");
      responsePanel.dataset.visible = "false";

      var result = await requestAIReady({
        input: [
          { role: "system", content: "You are a supportive coach. Give feedback on the learner response in three sentences." },
          { role: "user", content: answer }
        ],
        activity_id: "rise-ai-coach-test"
      });

      responseText.textContent = result.text;
      responsePanel.dataset.visible = "true";
      if (result.error) {
        statusMessage.textContent = "AIReady could not return feedback. The fallback message is shown below.";
        statusMessage.dataset.state = "error";
      } else {
        statusMessage.textContent = "Feedback is ready.";
        statusMessage.removeAttribute("data-state");
      }
      submitButton.disabled = false;
      resetButton.disabled = false;
      submitButton.textContent = "Get coaching feedback";
      responsePanel.focus({ preventScroll: true });
      responsePanel.scrollIntoView({ behavior: "smooth", block: "nearest" });
    });
  </script>
</body>
</html>

Read Articulate's Code Block instructions.

Contact support

Email us with your Storyline file and a screenshot or recording of the issue.

Remove API keys and learner-sensitive data from every file, screenshot, recording, and message before sharing — we reply within 3-5 business days.

Email aiready@arthalearning.com

See AIReady PracticeLayer in action