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;
}| Function | When called | Required |
|---|---|---|
getCode | SDK polls every 500ms — changes propagate to chat context | Required |
setCode | When user picks a mode in the "Apply to editor" modal | Optional |
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_nm | coding_language |
d_test_case | coding_testcase |
content | coding_content |
io_example | coding_example |
restriction | coding_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 (<p>, ", 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) |
|---|---|
"<p>You must...</p>" | "<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:
| Mode | Behavior |
|---|---|
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 propertyReplacing the adapter
kit.userInputCodeTarget = newAdapter;
// → Previous adapter auto-unbound
// → New adapter bound + initial sync immediatelyPriority
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"