edutap.ai developers
SDK Integration

Code Editor Integration

Bidirectional synchronization between TapKit and a host code editor

Code Editor Integration

Initial Setup

rowDataForCodeEditor carries static problem metadata; userInputCodeTarget carries the learner's current code. For coding-practice scenarios, registering both options together is the recommended baseline configuration.

const tapkit = useTapKit({
  apiKey,
  userId,
  courseId,
  rowDataForCodeEditor: rowData,            // static problem
  userInputCodeTarget: { getCode: () => editor.getValue() }, // live code
});

See the Adapter Pattern and Code Context Data sections below for the details of each option.


Adapter Pattern

Same bidirectional adapter pattern as videoTarget. The host registers a single object with getCode/setCode functions; the SDK handles polling and pushing automatically.

interface UserInputCodeAdapter {
  getCode: () => string;
  setCode?: (code: string) => void;
}
FunctionWhen calledRequired
getCodeSDK polls every 500ms — changes propagate to chat contextRequired
setCodeWhen user picks a mode in the "Apply to editor" modalOptional

Omitting setCode creates a read-only adapter — code flows to the SDK but the SDK cannot push code back into the host editor.

Unlike videoTarget which can accept an <video> element directly, code editors have no standard browser element so an adapter object is always required. Internally videoTarget is also converted to an adapter, so the runtime flow is identical.


Basic Usage

<tap-kit user-id="user-1" course-id="course-1" clip-id="clip-1"></tap-kit>

<script>
  const kit = document.querySelector('tap-kit');
  kit.apiKey = 'your-api-key';
  kit.userInputCodeTarget = {
    getCode: () => myEditor.getValue(),
    setCode: (code) => myEditor.setValue(code),
  };
</script>

In React, use the userInputCodeTarget option in the useTapKit Hook:

import { TapKit, useTapKit } from '@coxwave/tap-kit/react';
import { useRef, useMemo } from 'react';

function CodingLearningPage() {
  const editorRef = useRef<MyEditor>(null);

  // useMemo keeps the adapter reference stable; otherwise SDK rebinds every render
  const adapter = useMemo(() => ({
    getCode: () => editorRef.current?.getValue() ?? '',
    setCode: (code: string) => editorRef.current?.setValue(code),
  }), []);

  const tapkit = useTapKit({
    apiKey: 'your-api-key',
    userId: 'user-123',
    courseId: 'course-456',
    clipId: 'clip-789',
    userInputCodeTarget: adapter,
  });

  return (
    <div className="flex gap-4">
      <MyEditor ref={editorRef} className="flex-1" />
      <TapKit control={tapkit.control} style={{ height: '600px' }} />
    </div>
  );
}

Code Context Data (rowDataForCodeEditor)

For coding-practice scenarios where the host LMS/platform serves problem metadata as JSON, pass the response to TapKit via the rowDataForCodeEditor option. The SDK extracts 5 required fields and attaches them to every chat stream request body, so the AI tutor knows the language, test cases, problem statement, examples, and constraints without separate API calls.

Input shape — both object and JSON string are accepted

rowDataForCodeEditor accepts either a parsed object (CodingRowData) or a JSON string. The SDK normalizes the input internally (parse + validate).

  • Result of fetch().then(r => r.json()) / axios.get(...).then(r => r.data) — pass the object directly.
  • LMS returns a raw JSON string — pass the string directly.

Either way, missing or empty required fields throw synchronously at initialization.

Field Mapping

rowDataForCodeEditor field→ Chat stream body field
language_id_nmcoding_language
d_test_casecoding_testcase
contentcoding_content
io_examplecoding_example
restrictioncoding_restriction

Required Fields

If rowDataForCodeEditor is provided, all 5 fields must be non-empty strings. The SDK throws synchronously on init if any required field is missing or empty.

React

import { useMemo } from 'react';
import { TapKit, useTapKit } from '@coxwave/tap-kit/react';

function CodingPractice({ apiKey, userId, courseId, problemJson }) {
  // Wrap in useMemo to keep object reference stable across renders
  const rowData = useMemo(() => ({
    language_id_nm: problemJson.language_id_nm,
    d_test_case: problemJson.d_test_case,
    content: problemJson.content,
    io_example: problemJson.io_example,
    restriction: problemJson.restriction,
  }), [problemJson]);

  const tapkit = useTapKit({
    apiKey,
    userId,
    courseId,
    rowDataForCodeEditor: rowData,
  });

  return <TapKit control={tapkit.control} />;
}

You can also pass the raw JSON string from your LMS response without calling JSON.parse — the SDK normalizes it internally.

const tapkit = useTapKit({
  apiKey,
  userId,
  courseId,
  // Pass the raw JSON string directly instead of an object
  rowDataForCodeEditor: rawJsonStringFromLMS,
});

CDN / Web Component

<tap-kit id="kit" user-id="user-123" course-id="course-456"></tap-kit>
<script>
  const kit = document.getElementById('kit');
  kit.apiKey = 'your-key';
  kit.rowDataForCodeEditor = {
    language_id_nm: 'C++ (GCC 9.2.0)',
    d_test_case: '<p>...</p>',
    content: 'Problem description...',
    io_example: 'input: 1234 / output: 4S 0B',
    restriction: '1s / 256MB',
  };
</script>

Passing a raw JSON string

You can pass the LMS response without calling JSON.parse — the SDK will parse and validate it internally.

const tapkit = useTapKit({
  apiKey,
  userId,
  courseId,
  // Pass the raw JSON string directly instead of an object
  rowDataForCodeEditor: '{"language_id_nm":"C++","d_test_case":"<p>...</p>","content":"...","io_example":"...","restriction":"..."}',
});

Or vanilla JS:

kit.rowDataForCodeEditor = rawJsonStringFromLMS;

Automatic HTML Entity Decoding

Many host LMS systems deliver body fields as entity-encoded HTML (&lt;p&gt;, &quot;, etc.). The SDK applies automatic entity decoding to all 5 fields during the chat stream build step, so hosts do not need any pre-processing.

Input (from host JSON)After decoding (in chat stream body)
"&lt;p&gt;You must...&lt;/p&gt;""<p>You must...</p>"
"<p>raw HTML</p>" (already raw)"<p>raw HTML</p>" (no change — idempotent)

The LLM sees real HTML tags, which reduces token waste and improves context comprehension.

Fetching JSON from a Host API

In production, hosts usually fetch the problem JSON from an LMS or backend API before initializing the SDK.

'use client';

import { useEffect, useState, useMemo } from 'react';
import { TapKit, useTapKit, type CodingRowData } from '@coxwave/tap-kit/react';

const REQUIRED_KEYS = ['language_id_nm', 'd_test_case', 'content', 'io_example', 'restriction'] as const;

function isValidRowData(data: unknown): data is CodingRowData {
  if (!data || typeof data !== 'object') return false;
  const obj = data as Record<string, unknown>;
  return REQUIRED_KEYS.every(
    (k) => typeof obj[k] === 'string' && (obj[k] as string).length > 0
  );
}

function CodingPractice({ apiKey, userId, courseId, problemId }) {
  const [rowData, setRowData] = useState<CodingRowData | null>(null);

  useEffect(() => {
    fetch(`/api/lms/problems/${problemId}`, { credentials: 'include' })
      .then((r) => r.json())
      .then((json) => {
        // Validate — SDK init throws if any of the 5 required fields are missing or empty.
        if (isValidRowData(json)) {
          setRowData(json);
        } else {
          console.error('[host] LMS response missing required fields', json);
        }
      });
  }, [problemId]);

  // While rowData is null the SDK renders without rowDataForCodeEditor (tutor in normal mode).
  const tapkit = useTapKit({
    apiKey,
    userId,
    courseId,
    rowDataForCodeEditor: rowData ?? undefined,
  });

  return <TapKit control={tapkit.control} />;
}

Why validate on the host side too

The SDK validates via validateCodingRowData() at init time and throws if anything is missing. Because that exception aborts the entire chat initialization, it is safer to validate on the host first and pass undefined when invalid (the tutor falls back to normal mode).


Adapter Examples by Editor

Monaco Editor

const editor = monaco.editor.create(container, { ... });

kit.userInputCodeTarget = {
  getCode: () => editor.getValue(),
  setCode: (code) => editor.setValue(code),
};

CodeMirror 6

import { EditorView } from '@codemirror/view';

const view = new EditorView({ ... });

kit.userInputCodeTarget = {
  getCode: () => view.state.doc.toString(),
  setCode: (code) => view.dispatch({
    changes: { from: 0, to: view.state.doc.length, insert: code },
  }),
};

Plain <textarea>

const ta = document.querySelector('textarea');

kit.userInputCodeTarget = {
  getCode: () => ta.value,
  setCode: (code) => { ta.value = code; },
};

External IDE inside an iframe (Judge0, etc.)

When the host page doesn't mount an editor directly but embeds an external IDE inside an iframe. Expose getter/setter on the iframe's window and call them from the parent adapter:

// Inside the iframe (one-time setup, e.g. patched into Judge0's ide.js)
window.__getCode = () => editor.getValue();
window.__setCode = (code) => editor.setValue(code);

// Host page (vanilla JS)
kit.userInputCodeTarget = {
  getCode: () => ideIframe.contentWindow.__getCode(),
  setCode: (code) => ideIframe.contentWindow.__setCode(code),
};

In React, the same pattern with useTapKit:

import { TapKit, useTapKit } from '@coxwave/tap-kit/react';
import { useRef, useMemo } from 'react';

function IframeIdePage() {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  const adapter = useMemo(() => ({
    getCode: () => {
      const win = iframeRef.current?.contentWindow as (Window & { __getCode?: () => string }) | null;
      return win?.__getCode?.() ?? '';
    },
    setCode: (code: string) => {
      const win = iframeRef.current?.contentWindow as (Window & { __setCode?: (c: string) => void }) | null;
      win?.__setCode?.(code);
    },
  }), []);

  const tapkit = useTapKit({
    apiKey: 'your-api-key',
    userId: 'user-123',
    courseId: 'course-456',
    clipId: 'clip-789',
    userInputCodeTarget: adapter,
  });

  return (
    <div className="flex gap-4">
      <iframe ref={iframeRef} src="/my-ide/index.html" className="flex-1" />
      <TapKit control={tapkit.control} style={{ height: '600px' }} />
    </div>
  );
}

Playground's /practice page is a working example using Judge0 IDE inside an iframe. You can use it as a direct reference.


Apply Modes — Replace vs Append

When a learner clicks "Apply to editor" on a code block in a chat response, a modal appears with two options:

ModeBehavior
Replace (replace)adapter.setCode(newCode) — overwrites the host editor's content
Append (append)adapter.getCode() + newline + new code → adapter.setCode(merged)

append requires getCode, so read-only adapters automatically fall back to replace.

The replace/append modal UI is provided by the SDK inside the chat iframe. No additional host work is needed beyond registering the adapter.


Imperative API

kit.setUserInputCode(code)

For the host to update code directly. Updates the static property AND calls adapter.setCode in one call:

kit.setUserInputCode('function example() { return 42; }');
// → Updates SDK's internal userInputCode (reflected in next chat send)
// → Calls adapter.setCode (updates host editor)

Assigning kit.userInputCode = '...' directly is also possible, but doesn't reach the host editor. Use setUserInputCode when both should sync.

Unbinding the adapter

kit.userInputCodeTarget = undefined;
// → SDK stops polling
// → Falls back to the static userInputCode property

Replacing the adapter

kit.userInputCodeTarget = newAdapter;
// → Previous adapter auto-unbound
// → New adapter bound + initial sync immediately

Priority

kit.userInputCode = 'static value';
kit.userInputCodeTarget = { getCode: () => 'adapter value' };

// → SDK uses "adapter value" (adapter always wins)
// → Setting userInputCodeTarget to undefined falls back to "static value"

Next Steps