@layrs_ai/dsa-tutor-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @layrs/dsa-tutor-sdk
2
+
3
+ Shared React SDK for opening the Layrs DSA voice tutor from another product.
4
+
5
+ The SDK owns:
6
+
7
+ - LiveKit room connection and microphone publishing.
8
+ - Agent audio playback.
9
+ - `app.control` data-channel publishing and receiving.
10
+ - Tutor transcript state.
11
+ - A reusable `DsaTutorLauncher` room UI.
12
+
13
+ The host app owns:
14
+
15
+ - Authentication.
16
+ - Entitlement checks.
17
+ - Token minting.
18
+ - Problem lookup.
19
+ - Code execution, submissions, history, and app-level theme state through
20
+ optional `DsaTutorLauncher` callbacks.
21
+
22
+ ## Usage
23
+
24
+ ```tsx
25
+ import {
26
+ DsaTutorLauncher,
27
+ type DsaTutorTokenProvider,
28
+ } from "@layrs/dsa-tutor-sdk";
29
+
30
+ const tokenProvider: DsaTutorTokenProvider = async ({ problem, sheetId }) => {
31
+ const session = await api.voiceTutor.createDsaSession.mutate({
32
+ questionId: problem.id,
33
+ sheetId: Number(sheetId),
34
+ });
35
+
36
+ return {
37
+ token: session.token,
38
+ livekitUrl: session.livekitUrl,
39
+ roomName: session.roomName,
40
+ identity: session.identity,
41
+ problemSlug: session.problemSlug,
42
+ problemId: session.problemId,
43
+ };
44
+ };
45
+
46
+ <DsaTutorLauncher
47
+ problem={{
48
+ id: question.id,
49
+ title: question.title,
50
+ difficulty: question.difficulty,
51
+ problemLink: question.problemLink,
52
+ }}
53
+ sheetId={sheetId}
54
+ tokenProvider={tokenProvider}
55
+ onRun={async ({ code, language, problem }) => {
56
+ await runCode({ code, language, problemId: problem.id });
57
+ }}
58
+ onSubmit={async ({ code, language, problem }) => {
59
+ await submitCode({ code, language, problemId: problem.id });
60
+ }}
61
+ />;
62
+ ```
63
+
64
+ Without `onRun` / `onSubmit`, those buttons remain visible but show a host-not-wired
65
+ notice instead of silently doing nothing.
66
+
67
+ ## Token Provider Contract
68
+
69
+ `tokenProvider` must return a LiveKit participant token that dispatches the
70
+ existing voice-agent worker with metadata including:
71
+
72
+ - `agentType: "dsa_practice_coach"`
73
+ - `problemSlug` / `problem_slug`
74
+ - `problemId` / `problem_id`
75
+ - `userId` / `user_id`
76
+
77
+ In LearnYard, the browser calls the LearnYard API. LearnYard API calls Tryst.
78
+ Tryst mints the LiveKit token and injects the agent metadata.
79
+
80
+ ## Publishing
81
+
82
+ This package is configured for a private npm publish under the `@layrs` scope.
83
+
84
+ ```bash
85
+ cd /Users/sameer/Documents/voice-agent/dsa-tutor-sdk
86
+ pnpm run clean
87
+ pnpm run check-types
88
+ pnpm run build
89
+ npm login
90
+ npm publish --access restricted
91
+ ```
92
+
93
+ Consumers can then install it as:
94
+
95
+ ```json
96
+ "@layrs/dsa-tutor-sdk": "^0.1.0"
97
+ ```
@@ -0,0 +1,34 @@
1
+ import type { DsaTutorControlMessage, DsaTutorLanguage, DsaTutorMode, DsaTutorProblem, DsaTutorRunResponse, DsaTutorTokenProvider } from "./types";
2
+ export interface DsaTutorLauncherProps {
3
+ problem: DsaTutorProblem;
4
+ tokenProvider: DsaTutorTokenProvider;
5
+ sheetId?: string | number;
6
+ buttonLabel?: string;
7
+ buttonClassName?: string;
8
+ codeTemplate?: string;
9
+ testcaseText?: string;
10
+ autoStart?: boolean;
11
+ initialMode?: DsaTutorMode;
12
+ onModeChange?: (mode: DsaTutorMode) => void;
13
+ onRun?: (context: {
14
+ code: string;
15
+ language: DsaTutorLanguage;
16
+ problem: DsaTutorProblem;
17
+ }) => DsaTutorRunResponse | void | Promise<DsaTutorRunResponse | void>;
18
+ onSubmit?: (context: {
19
+ code: string;
20
+ language: DsaTutorLanguage;
21
+ problem: DsaTutorProblem;
22
+ }) => DsaTutorRunResponse | void | Promise<DsaTutorRunResponse | void>;
23
+ onHistory?: () => void | Promise<void>;
24
+ onReset?: (context: {
25
+ problem: DsaTutorProblem;
26
+ }) => string | void | Promise<string | void>;
27
+ onThemeChange?: (theme: "light" | "dark") => void;
28
+ onCodeChange?: (code: string) => void;
29
+ onLanguageChange?: (language: string) => void;
30
+ onAgentMessage?: (message: DsaTutorControlMessage) => void;
31
+ onOpenChange?: (isOpen: boolean) => void;
32
+ }
33
+ export declare function DsaTutorLauncher({ problem, tokenProvider, sheetId, buttonLabel, buttonClassName, codeTemplate, testcaseText, autoStart, initialMode, onModeChange, onRun, onSubmit, onHistory, onReset, onThemeChange, onCodeChange, onLanguageChange, onAgentMessage, onOpenChange, }: DsaTutorLauncherProps): import("react/jsx-runtime").JSX.Element;
34
+ //# sourceMappingURL=DsaTutorLauncher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DsaTutorLauncher.d.ts","sourceRoot":"","sources":["../src/DsaTutorLauncher.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAajB,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,CAAC;IACzB,aAAa,EAAE,qBAAqB,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,YAAY,CAAC;IAC3B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,KAAK,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IAC9J,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,KAAK,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACjK,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,eAAe,CAAA;KAAE,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5F,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;IAClD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAC3D,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAcD,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,aAAa,EACb,OAAO,EACP,WAAkB,EAClB,eAAe,EACf,YAAkC,EAClC,YAAmB,EACnB,SAAiB,EACjB,WAAqB,EACrB,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAO,EACP,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,YAAY,GACb,EAAE,qBAAqB,2CA66BvB"}
@@ -0,0 +1,493 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { Editor } from "@monaco-editor/react";
5
+ import { ArrowLeft, Bug, CheckCircle2, Clock3, Footprints, Loader2, Mic, MicOff, Moon, PhoneOff, Play, RotateCcw, Send, Sparkles, Sun, X, } from "lucide-react";
6
+ import { buildStudentRunResult } from "./dsaTutorRedaction";
7
+ import { useDsaTutorLiveKit, } from "./useDsaTutorLiveKit";
8
+ import { EditProposalPopover, applyLineReplacement, } from "./EditProposalPopover";
9
+ import { WidgetStack } from "./WidgetStack";
10
+ const defaultCodeTemplate = `class Solution {
11
+ public:
12
+ int solve() {
13
+ // Your code here
14
+ return 0;
15
+ }
16
+ };`;
17
+ function getStarterCode(problem, language, fallback) {
18
+ return problem.starterCode?.[language] || fallback;
19
+ }
20
+ export function DsaTutorLauncher({ problem, tokenProvider, sheetId, buttonLabel = "AI", buttonClassName, codeTemplate = defaultCodeTemplate, testcaseText = "[]", autoStart = false, initialMode = "coach", onModeChange, onRun, onSubmit, onHistory, onReset, onThemeChange, onCodeChange, onLanguageChange, onAgentMessage, onOpenChange, }) {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const [isStarting, setIsStarting] = useState(false);
23
+ const [localError, setLocalError] = useState(null);
24
+ const [actionNotice, setActionNotice] = useState(null);
25
+ const [text, setText] = useState("");
26
+ const [code, setCode] = useState(() => getStarterCode(problem, "cpp", codeTemplate));
27
+ const [language, setLanguage] = useState("cpp");
28
+ const [activeMode, setActiveMode] = useState(initialMode);
29
+ const [theme, setTheme] = useState("light");
30
+ const [lastRunResult, setLastRunResult] = useState(null);
31
+ const [activeBottomTab, setActiveBottomTab] = useState("testcase");
32
+ const [selectedCase, setSelectedCase] = useState(0);
33
+ const [isRunning, setIsRunning] = useState(false);
34
+ const [isSubmitting, setIsSubmitting] = useState(false);
35
+ const [editProposals, setEditProposals] = useState([]);
36
+ const [widgets, setWidgets] = useState(() => new Map());
37
+ const [studyNote, setStudyNote] = useState(null);
38
+ const [highlightNotice, setHighlightNotice] = useState(null);
39
+ const [proposalTopOffset, setProposalTopOffset] = useState(24);
40
+ const tutorRef = useRef(null);
41
+ const autoStartedRef = useRef(false);
42
+ const runActionRef = useRef(null);
43
+ const codeRef = useRef(code);
44
+ const languageRef = useRef(language);
45
+ const editorRef = useRef(null);
46
+ const monacoRef = useRef(null);
47
+ const highlightDecorationsRef = useRef([]);
48
+ const highlightTimerRef = useRef(null);
49
+ const clearEditorHighlight = useCallback(() => {
50
+ const editor = editorRef.current;
51
+ if (editor && highlightDecorationsRef.current.length > 0) {
52
+ highlightDecorationsRef.current = editor.deltaDecorations(highlightDecorationsRef.current, []);
53
+ }
54
+ setHighlightNotice(null);
55
+ }, []);
56
+ const highlightEditorLines = useCallback((startLine, endLine = startLine) => {
57
+ const safeStart = Math.max(1, Math.floor(startLine || 1));
58
+ const safeEnd = Math.max(safeStart, Math.floor(endLine || safeStart));
59
+ setHighlightNotice(safeStart === safeEnd
60
+ ? `Tutor highlighted line ${safeStart}.`
61
+ : `Tutor highlighted lines ${safeStart}-${safeEnd}.`);
62
+ const editor = editorRef.current;
63
+ const monaco = monacoRef.current;
64
+ if (editor && monaco) {
65
+ highlightDecorationsRef.current = editor.deltaDecorations(highlightDecorationsRef.current, [
66
+ {
67
+ range: new monaco.Range(safeStart, 1, safeEnd, 1),
68
+ options: {
69
+ isWholeLine: true,
70
+ className: "layrs-dsa-monaco-highlight-line",
71
+ },
72
+ },
73
+ ]);
74
+ editor.revealLineInCenterIfOutsideViewport(safeStart);
75
+ }
76
+ if (highlightTimerRef.current) {
77
+ clearTimeout(highlightTimerRef.current);
78
+ }
79
+ highlightTimerRef.current = setTimeout(clearEditorHighlight, 9000);
80
+ }, [clearEditorHighlight]);
81
+ const focusProposalLine = useCallback((startLine) => {
82
+ const editor = editorRef.current;
83
+ if (!editor) {
84
+ setProposalTopOffset(24);
85
+ return;
86
+ }
87
+ const safeStart = Math.max(1, Math.floor(startLine || 1));
88
+ editor.revealLineInCenterIfOutsideViewport(safeStart);
89
+ requestAnimationFrame(() => {
90
+ const top = editor.getTopForLineNumber(safeStart) - editor.getScrollTop();
91
+ setProposalTopOffset(Number.isFinite(top) ? top : 24);
92
+ });
93
+ }, []);
94
+ const tutor = useDsaTutorLiveKit({
95
+ onAgentMessage: (message) => {
96
+ if (message.type === "agent.request_code") {
97
+ tutorRef.current?.publishControlMessage({
98
+ type: "student.code_update",
99
+ code: codeRef.current,
100
+ language: languageRef.current,
101
+ timestamp: Date.now(),
102
+ source: "dsa_tutor_sdk",
103
+ });
104
+ }
105
+ if (message.type === "agent.request_run") {
106
+ void runActionRef.current?.("run");
107
+ }
108
+ if (message.type === "agent.request_submit") {
109
+ void runActionRef.current?.("submit");
110
+ }
111
+ if (message.type === "monaco_init") {
112
+ const nextLanguage = String(message.language || "").toLowerCase();
113
+ if (nextLanguage === "cpp" || nextLanguage === "java") {
114
+ setLanguage(nextLanguage);
115
+ }
116
+ }
117
+ if (message.type === "monaco_file") {
118
+ const content = message.content;
119
+ if (typeof content === "string" &&
120
+ (message.action === "create" || message.action === "update")) {
121
+ codeRef.current = content;
122
+ if (editorRef.current?.getValue() !== content) {
123
+ editorRef.current?.setValue(content);
124
+ }
125
+ setCode(content);
126
+ setActionNotice("Tutor updated the editor.");
127
+ }
128
+ }
129
+ if (message.type === "monaco_highlight") {
130
+ const startLine = Number(message.startLine || message.start_line || 1);
131
+ const endLine = Number(message.endLine || message.end_line || startLine);
132
+ highlightEditorLines(startLine, endLine);
133
+ }
134
+ if (message.type === "agent.propose_code_edit") {
135
+ const proposal = message;
136
+ if (typeof proposal.proposal_id === "string" &&
137
+ typeof proposal.replacement === "string") {
138
+ focusProposalLine(Number(proposal.start_line || 1));
139
+ setEditProposals((current) => current.some((item) => item.proposal_id === proposal.proposal_id)
140
+ ? current
141
+ : [...current, proposal].slice(-5));
142
+ }
143
+ }
144
+ if (message.type === "agent.mode_ack") {
145
+ const mode = String(message.mode || "");
146
+ if (mode === "coach" || mode === "debug" || mode === "walkthrough") {
147
+ setActiveMode(mode);
148
+ }
149
+ }
150
+ if (message.type === "agent.show_widget") {
151
+ const widget = message;
152
+ if (typeof widget.widget_id === "string") {
153
+ setWidgets((current) => {
154
+ const next = new Map(current);
155
+ next.set(widget.widget_id, widget);
156
+ return next;
157
+ });
158
+ }
159
+ }
160
+ if (message.type === "agent.clear_widget") {
161
+ const widgetId = String(message.widget_id || "");
162
+ setWidgets((current) => {
163
+ if (!current.has(widgetId))
164
+ return current;
165
+ const next = new Map(current);
166
+ next.delete(widgetId);
167
+ return next;
168
+ });
169
+ }
170
+ if (message.type === "agent.study_note") {
171
+ setStudyNote(String(message.body_md || ""));
172
+ }
173
+ if (message.type === "agent.session_end") {
174
+ void tutorRef.current?.disconnect();
175
+ }
176
+ onAgentMessage?.(message);
177
+ },
178
+ });
179
+ useEffect(() => {
180
+ codeRef.current = code;
181
+ }, [code]);
182
+ useEffect(() => {
183
+ languageRef.current = language;
184
+ }, [language]);
185
+ useEffect(() => {
186
+ return () => {
187
+ if (highlightTimerRef.current) {
188
+ clearTimeout(highlightTimerRef.current);
189
+ }
190
+ };
191
+ }, []);
192
+ useEffect(() => {
193
+ setCode(getStarterCode(problem, language, codeTemplate));
194
+ setLastRunResult(null);
195
+ setActiveBottomTab("testcase");
196
+ }, [
197
+ codeTemplate,
198
+ language,
199
+ problem.id,
200
+ problem.slug,
201
+ problem.starterCode?.cpp,
202
+ problem.starterCode?.java,
203
+ ]);
204
+ useEffect(() => {
205
+ tutorRef.current = tutor;
206
+ }, [tutor]);
207
+ const setOpen = useCallback((nextOpen) => {
208
+ setIsOpen(nextOpen);
209
+ onOpenChange?.(nextOpen);
210
+ }, [onOpenChange]);
211
+ const start = useCallback(async () => {
212
+ if (isStarting || tutor.isBusy)
213
+ return;
214
+ setOpen(true);
215
+ setLocalError(null);
216
+ setIsStarting(true);
217
+ try {
218
+ const session = await tokenProvider({ problem, sheetId });
219
+ await tutor.connect(session);
220
+ }
221
+ catch (error) {
222
+ setLocalError(error?.message || "Could not start the AI tutor.");
223
+ }
224
+ finally {
225
+ setIsStarting(false);
226
+ }
227
+ }, [isStarting, problem, setOpen, sheetId, tokenProvider, tutor]);
228
+ useEffect(() => {
229
+ if (!autoStart || autoStartedRef.current)
230
+ return;
231
+ autoStartedRef.current = true;
232
+ start();
233
+ }, [autoStart, start]);
234
+ const close = useCallback(async () => {
235
+ await tutor.disconnect();
236
+ setOpen(false);
237
+ }, [setOpen, tutor]);
238
+ const sendText = useCallback(() => {
239
+ const trimmed = text.trim();
240
+ if (!trimmed)
241
+ return;
242
+ tutor.sendText(trimmed);
243
+ setText("");
244
+ }, [text, tutor]);
245
+ const updateCode = useCallback((nextCode) => {
246
+ setCode(nextCode);
247
+ onCodeChange?.(nextCode);
248
+ tutor.publishControlMessage({
249
+ type: "student.code_update",
250
+ code: nextCode,
251
+ language,
252
+ timestamp: Date.now(),
253
+ source: "dsa_tutor_sdk",
254
+ });
255
+ }, [language, onCodeChange, tutor]);
256
+ const changeLanguage = useCallback((nextLanguage) => {
257
+ setLanguage(nextLanguage);
258
+ onLanguageChange?.(nextLanguage);
259
+ const nextCode = getStarterCode(problem, nextLanguage, codeTemplate);
260
+ setCode(nextCode);
261
+ setLastRunResult(null);
262
+ setActiveBottomTab("testcase");
263
+ tutor.publishControlMessage({
264
+ type: "student.code_update",
265
+ code: nextCode,
266
+ language: nextLanguage,
267
+ timestamp: Date.now(),
268
+ source: "dsa_tutor_sdk",
269
+ });
270
+ }, [codeTemplate, onLanguageChange, problem, tutor]);
271
+ const changeMode = useCallback((mode) => {
272
+ if (mode === activeMode)
273
+ return;
274
+ setActiveMode(mode);
275
+ setActionNotice(null);
276
+ onModeChange?.(mode);
277
+ tutor.publishControlMessage({
278
+ type: "student.mode_change",
279
+ mode,
280
+ timestamp: Date.now(),
281
+ });
282
+ }, [activeMode, onModeChange, tutor]);
283
+ const toggleTheme = useCallback(() => {
284
+ const nextTheme = theme === "light" ? "dark" : "light";
285
+ setTheme(nextTheme);
286
+ onThemeChange?.(nextTheme);
287
+ }, [onThemeChange, theme]);
288
+ const runHostAction = useCallback(async () => {
289
+ setLocalError(null);
290
+ if (!onHistory) {
291
+ setActionNotice("History is not wired in this host yet.");
292
+ return;
293
+ }
294
+ setActionNotice(null);
295
+ try {
296
+ await onHistory();
297
+ }
298
+ catch (error) {
299
+ setLocalError(error?.message || "History failed.");
300
+ }
301
+ }, [onHistory]);
302
+ const executeCodeAction = useCallback(async (kind) => {
303
+ const action = kind === "run" ? onRun : onSubmit;
304
+ const label = kind === "run" ? "Run" : "Submit";
305
+ setLocalError(null);
306
+ if (!code.trim()) {
307
+ setLocalError("Editor is empty.");
308
+ return;
309
+ }
310
+ if (!action) {
311
+ setActionNotice(`${label} is not wired in this host yet.`);
312
+ return;
313
+ }
314
+ setActionNotice(null);
315
+ setActiveBottomTab("result");
316
+ if (kind === "run")
317
+ setIsRunning(true);
318
+ else
319
+ setIsSubmitting(true);
320
+ try {
321
+ const result = await action({ code, language, problem });
322
+ if (result) {
323
+ setLastRunResult(result);
324
+ tutor.publishControlMessage(buildStudentRunResult(result, kind));
325
+ }
326
+ }
327
+ catch (error) {
328
+ setLocalError(error?.message || `${label} failed.`);
329
+ }
330
+ finally {
331
+ if (kind === "run")
332
+ setIsRunning(false);
333
+ else
334
+ setIsSubmitting(false);
335
+ }
336
+ }, [code, language, onRun, onSubmit, problem, tutor]);
337
+ useEffect(() => {
338
+ runActionRef.current = executeCodeAction;
339
+ }, [executeCodeAction]);
340
+ const resetCode = useCallback(async () => {
341
+ setLocalError(null);
342
+ try {
343
+ const replacement = await onReset?.({ problem });
344
+ const nextCode = typeof replacement === "string"
345
+ ? replacement
346
+ : getStarterCode(problem, language, codeTemplate);
347
+ setCode(nextCode);
348
+ setLastRunResult(null);
349
+ setActiveBottomTab("testcase");
350
+ onCodeChange?.(nextCode);
351
+ setActionNotice("Code reset.");
352
+ tutor.publishControlMessage({
353
+ type: "student.code_update",
354
+ code: nextCode,
355
+ language,
356
+ timestamp: Date.now(),
357
+ source: "dsa_tutor_sdk",
358
+ });
359
+ }
360
+ catch (error) {
361
+ setLocalError(error?.message || "Reset failed.");
362
+ }
363
+ }, [codeTemplate, language, onCodeChange, onReset, problem, tutor]);
364
+ const endSession = useCallback(async () => {
365
+ tutor.publishControlMessage({
366
+ type: "student.complete",
367
+ timestamp: Date.now(),
368
+ });
369
+ await tutor.disconnect();
370
+ }, [tutor]);
371
+ const activeProposal = editProposals[0] || null;
372
+ const acceptProposal = useCallback(() => {
373
+ if (!activeProposal)
374
+ return;
375
+ const nextCode = applyLineReplacement(codeRef.current, activeProposal);
376
+ codeRef.current = nextCode;
377
+ setCode(nextCode);
378
+ onCodeChange?.(nextCode);
379
+ setEditProposals((current) => current.slice(1));
380
+ setActionNotice("Tutor edit applied.");
381
+ tutor.publishControlMessage({
382
+ type: "student.edit_decision",
383
+ proposal_id: activeProposal.proposal_id,
384
+ decision: "accepted",
385
+ timestamp: Date.now(),
386
+ });
387
+ tutor.publishControlMessage({
388
+ type: "student.code_update",
389
+ code: nextCode,
390
+ language: languageRef.current,
391
+ timestamp: Date.now(),
392
+ source: "dsa_tutor_sdk",
393
+ });
394
+ }, [activeProposal, onCodeChange, tutor]);
395
+ const rejectProposal = useCallback(() => {
396
+ if (!activeProposal)
397
+ return;
398
+ setEditProposals((current) => current.slice(1));
399
+ tutor.publishControlMessage({
400
+ type: "student.edit_decision",
401
+ proposal_id: activeProposal.proposal_id,
402
+ decision: "rejected",
403
+ timestamp: Date.now(),
404
+ });
405
+ }, [activeProposal, tutor]);
406
+ const isBusy = isStarting || tutor.isBusy;
407
+ const isConnected = tutor.isConnected;
408
+ const statusLabel = tutor.state === "speaking"
409
+ ? "Tutor is speaking"
410
+ : tutor.state === "muted"
411
+ ? "Mic muted"
412
+ : isConnected
413
+ ? "Tutor is listening"
414
+ : isBusy
415
+ ? "Starting tutor"
416
+ : "Ready";
417
+ const visibleError = localError || tutor.lastError;
418
+ const isDark = theme === "dark";
419
+ const monacoTheme = isDark ? "vs-dark" : "vs";
420
+ const monacoLanguage = language === "cpp" ? "cpp" : "java";
421
+ const shellClass = isDark ? "bg-[#0d1020] text-slate-100" : "bg-[#fbfaf8] text-slate-950";
422
+ const surfaceClass = isDark ? "border-slate-800 bg-[#12172a]" : "border-[#e7e2d9] bg-white";
423
+ const panelClass = isDark ? "border-slate-800 bg-[#111827]" : "border-[#e7e2d9] bg-white";
424
+ const subtlePanelClass = isDark ? "border-slate-800 bg-[#0f172a]" : "border-[#e7e2d9] bg-[#fffdf9]";
425
+ const mutedTextClass = isDark ? "text-slate-300" : "text-slate-700";
426
+ const secondaryTextClass = isDark ? "text-slate-400" : "text-slate-500";
427
+ const hoverClass = isDark ? "hover:bg-slate-800" : "hover:bg-slate-100";
428
+ const inactiveChipClass = isDark
429
+ ? "border border-slate-700 bg-[#111827] text-slate-100 hover:bg-slate-800"
430
+ : "border border-[#e7e2d9] bg-white text-slate-900 hover:bg-slate-50";
431
+ const activeChipClass = "bg-[#4b74f2] text-white";
432
+ const modeButtons = [
433
+ { mode: "coach", label: "Coach", icon: Sparkles },
434
+ { mode: "debug", label: "Debug", icon: Bug },
435
+ { mode: "walkthrough", label: "Walkthrough", icon: Footprints },
436
+ ];
437
+ const visibleTestCases = problem.visibleTestCases || [];
438
+ const currentCase = visibleTestCases[selectedCase] || visibleTestCases[0];
439
+ const currentCaseText = currentCase?.input?.join("\n") || testcaseText || "[]";
440
+ const widgetList = Array.from(widgets.values());
441
+ const resultColor = lastRunResult?.verdict === "accepted"
442
+ ? "text-emerald-600"
443
+ : lastRunResult
444
+ ? "text-red-600"
445
+ : secondaryTextClass;
446
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
447
+ .layrs-dsa-monaco-highlight-line {
448
+ background-color: rgba(255, 213, 79, 0.18);
449
+ border-left: 2px solid #4b74f2;
450
+ }
451
+ ` }), _jsxs("button", { type: "button", className: buttonClassName ||
452
+ "ml-2 inline-flex h-7 items-center gap-1 rounded-md border border-[#6D4AFF]/30 bg-[#6D4AFF]/10 px-2 text-xs font-semibold text-[#5B3FE8] transition hover:bg-[#6D4AFF]/15 disabled:cursor-not-allowed disabled:opacity-60", disabled: isBusy, title: "Practice this problem with AI", onClick: (event) => {
453
+ event.preventDefault();
454
+ event.stopPropagation();
455
+ start();
456
+ }, children: [isBusy ? _jsx(Loader2, { size: 14, className: "animate-spin" }) : _jsx(Sparkles, { size: 14 }), buttonLabel] }), isOpen ? (_jsx("div", { className: `fixed inset-0 z-[1000] ${shellClass}`, onClick: (event) => event.stopPropagation(), children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("header", { className: `flex h-[70px] shrink-0 items-center justify-between border-b px-5 ${surfaceClass}`, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsxs("button", { type: "button", className: `inline-flex h-10 items-center gap-2 rounded-md px-2 text-sm font-semibold ${hoverClass}`, onClick: close, children: [_jsx(ArrowLeft, { size: 18 }), "Back"] }), _jsx("div", { className: `h-8 w-px ${isDark ? "bg-slate-800" : "bg-[#e7e2d9]"}` }), _jsx("h1", { className: "truncate text-xl font-bold", children: problem.title }), problem.difficulty ? (_jsx("span", { className: "rounded-md bg-amber-50 px-2.5 py-1 text-sm font-semibold capitalize text-amber-700", children: problem.difficulty })) : null, _jsxs("span", { className: "inline-flex items-center gap-1.5 text-sm font-semibold text-emerald-600", children: [_jsx(CheckCircle2, { size: 18 }), "Tutor"] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("button", { type: "button", className: `inline-flex h-11 items-center gap-2 rounded-md border px-3 text-sm font-semibold ${inactiveChipClass}`, onClick: runHostAction, children: [_jsx(Clock3, { size: 18 }), "History"] }), _jsxs("button", { type: "button", className: `inline-flex h-11 items-center gap-2 rounded-md border px-4 text-sm font-semibold ${inactiveChipClass}`, disabled: isRunning || isSubmitting, onClick: () => executeCodeAction("run"), children: [isRunning ? _jsx(Loader2, { size: 18, className: "animate-spin" }) : _jsx(Play, { size: 18 }), "Run"] }), _jsxs("button", { type: "button", className: "inline-flex h-11 items-center gap-2 rounded-md bg-[#4b74f2] px-4 text-sm font-semibold text-white", disabled: isRunning || isSubmitting, onClick: () => executeCodeAction("submit"), children: [isSubmitting ? _jsx(Loader2, { size: 18, className: "animate-spin" }) : _jsx(CheckCircle2, { size: 18 }), "Submit"] }), _jsx("button", { type: "button", className: `grid h-11 w-11 place-items-center rounded-full border ${inactiveChipClass}`, title: "Theme", onClick: toggleTheme, children: isDark ? _jsx(Sun, { size: 18 }) : _jsx(Moon, { size: 18 }) }), _jsx("button", { type: "button", className: `grid h-11 w-11 place-items-center rounded-md ${secondaryTextClass} ${hoverClass}`, title: "Close", onClick: close, children: _jsx(X, { size: 18 }) })] })] }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[28%_1fr_400px] overflow-hidden", children: [_jsxs("aside", { className: `min-h-0 overflow-y-auto border-r ${panelClass}`, children: [_jsxs("div", { className: `flex h-[62px] items-center gap-3 border-b px-4 ${isDark ? "border-slate-800" : "border-[#ece7df]"}`, children: [_jsx("button", { className: `rounded-md px-3 py-2 text-sm font-bold shadow-sm ${isDark ? "bg-slate-800" : "bg-white"}`, children: "Description" }), _jsx("button", { className: `px-2 py-2 text-sm font-bold ${mutedTextClass}`, children: "Submissions" }), _jsx("button", { className: `px-2 py-2 text-sm font-bold ${mutedTextClass}`, children: "Notes" })] }), _jsxs("div", { className: `space-y-6 px-5 py-6 text-[15px] leading-7 ${mutedTextClass}`, children: [_jsx("span", { className: "inline-flex rounded-md bg-blue-50 px-3 py-1 text-sm font-medium text-blue-500", children: problem.tag || "dsa-practice" }), _jsx("h2", { className: "text-2xl font-bold", children: problem.title }), problem.statement ? (_jsx("div", { className: "whitespace-pre-wrap", children: problem.statement })) : (_jsx("p", { children: "Work through this problem with the AI tutor. Use the center workspace for notes or code while the tutor coaches you on approach, edge cases, and complexity." })), problem.problemLink && problem.problemLink !== "NA" ? (_jsx("a", { href: problem.problemLink, target: "_blank", rel: "noreferrer", className: `inline-flex rounded-md border px-3 py-2 text-sm font-semibold ${inactiveChipClass}`, children: "Open original problem" })) : null, _jsxs("div", { children: [_jsx("h3", { className: "mb-3 text-lg font-bold", children: "Scratch Examples" }), _jsxs("div", { className: "rounded-md bg-slate-900 p-4 font-mono text-sm text-slate-100", children: [_jsx("div", { children: "Input:" }), _jsx("pre", { className: "whitespace-pre-wrap", children: currentCaseText })] })] })] })] }), _jsxs("main", { className: `grid min-h-0 grid-rows-[70px_1fr_220px] ${panelClass}`, children: [_jsxs("div", { className: `flex items-center justify-between border-b px-5 ${isDark ? "border-slate-800" : "border-[#ece7df]"}`, children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("select", { className: `h-10 rounded-md border px-3 text-sm font-semibold ${inactiveChipClass}`, value: language, onChange: (event) => changeLanguage(event.target.value), children: [_jsx("option", { value: "cpp", children: "C++" }), _jsx("option", { value: "java", children: "Java" })] }), _jsx("span", { className: `text-sm ${secondaryTextClass}`, children: "Saved" })] }), _jsxs("button", { type: "button", className: `inline-flex h-10 items-center gap-2 rounded-md border px-3 text-sm font-semibold ${inactiveChipClass}`, onClick: resetCode, children: [_jsx(RotateCcw, { size: 18 }), "Reset"] })] }), _jsxs("div", { className: `relative min-h-0 ${isDark ? "bg-[#0b1120]" : "bg-white"}`, children: [_jsx(Editor, { height: "100%", language: monacoLanguage, theme: monacoTheme, value: code, onChange: (value) => updateCode(value ?? ""), onMount: (editor, monaco) => {
457
+ editorRef.current = editor;
458
+ monacoRef.current = monaco;
459
+ }, options: {
460
+ minimap: { enabled: false },
461
+ fontSize: 15,
462
+ lineHeight: 26,
463
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
464
+ scrollBeyondLastLine: false,
465
+ automaticLayout: true,
466
+ wordWrap: "on",
467
+ padding: { top: 28, bottom: 28 },
468
+ renderLineHighlight: "line",
469
+ overviewRulerBorder: false,
470
+ } }), activeProposal ? (_jsx(EditProposalPopover, { proposal: activeProposal, currentCode: code, language: language, monacoTheme: monacoTheme, topOffsetPx: proposalTopOffset, queueDepth: Math.max(0, editProposals.length - 1), isDark: isDark, onAccept: acceptProposal, onReject: rejectProposal })) : null] }), _jsxs("div", { className: `border-t ${panelClass}`, children: [_jsxs("div", { className: "flex h-14 items-center gap-3 px-5", children: [_jsx("button", { type: "button", className: `rounded-md px-3 py-2 text-base font-bold ${activeBottomTab === "testcase"
471
+ ? `${isDark ? "bg-slate-800" : "bg-white"} shadow-sm`
472
+ : mutedTextClass}`, onClick: () => setActiveBottomTab("testcase"), children: "Testcase" }), _jsx("button", { type: "button", className: `rounded-md px-3 py-2 text-base font-bold ${activeBottomTab === "result"
473
+ ? `${isDark ? "bg-slate-800" : "bg-white"} shadow-sm`
474
+ : mutedTextClass}`, onClick: () => setActiveBottomTab("result"), children: "Test Result" })] }), _jsx("div", { className: "px-5 pb-5", children: activeBottomTab === "testcase" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mb-3 flex gap-2", children: (visibleTestCases.length > 0
475
+ ? visibleTestCases
476
+ : [{ idx: 0, input: [testcaseText] }]).map((testCase, index) => (_jsxs("button", { type: "button", className: `rounded-md px-3 py-2 text-sm font-semibold ${selectedCase === index
477
+ ? "bg-[#4b74f2] text-white"
478
+ : inactiveChipClass}`, onClick: () => setSelectedCase(index), children: ["Case ", index + 1] }, `${testCase.idx}-${index}`))) }), _jsx("div", { className: `rounded-md border px-4 py-3 font-mono text-sm ${subtlePanelClass}`, children: _jsx("pre", { className: "whitespace-pre-wrap", children: currentCaseText }) })] })) : lastRunResult ? (_jsxs("div", { className: `rounded-md border px-4 py-3 text-sm ${subtlePanelClass}`, children: [_jsx("div", { className: `mb-2 text-base font-bold ${resultColor}`, children: lastRunResult.verdict_label }), _jsxs("div", { className: secondaryTextClass, children: [lastRunResult.tests_passed, " / ", lastRunResult.tests_total, " cases passed", lastRunResult.is_submit && lastRunResult.hidden_tests_total > 0
479
+ ? `, including ${lastRunResult.hidden_tests_total} hidden`
480
+ : ""] }), lastRunResult.runtime_ms != null ? (_jsxs("div", { className: secondaryTextClass, children: ["Runtime: ", lastRunResult.runtime_ms, "ms"] })) : null, lastRunResult.memory_kb != null ? (_jsxs("div", { className: secondaryTextClass, children: ["Memory: ", (lastRunResult.memory_kb / 1024).toFixed(2), "MB"] })) : null, lastRunResult.compile_error ? (_jsx("pre", { className: "mt-3 max-h-24 overflow-auto whitespace-pre-wrap rounded-md bg-red-50 p-3 font-mono text-xs text-red-700", children: lastRunResult.compile_error.message })) : null, lastRunResult.first_failure ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsxs("div", { className: "font-semibold", children: ["Failed case #", lastRunResult.first_failure.case_idx + 1, lastRunResult.first_failure.is_hidden ? " (hidden)" : ""] }), lastRunResult.first_failure.expected != null ? (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("pre", { className: `whitespace-pre-wrap rounded-md border p-2 font-mono text-xs ${subtlePanelClass}`, children: ["Expected: ", lastRunResult.first_failure.expected] }), _jsxs("pre", { className: `whitespace-pre-wrap rounded-md border p-2 font-mono text-xs ${subtlePanelClass}`, children: ["Actual: ", lastRunResult.first_failure.actual] })] })) : null] })) : null] })) : (_jsx("div", { className: `rounded-md border px-4 py-3 text-sm ${subtlePanelClass}`, children: "Run your code to see results here." })) })] })] }), _jsxs("aside", { className: `grid min-h-0 grid-rows-[70px_auto_1fr_auto] border-l ${panelClass}`, children: [_jsxs("div", { className: `flex items-center gap-2 border-b px-5 ${isDark ? "border-slate-800" : "border-[#ece7df]"}`, children: [_jsx(Sparkles, { size: 20, className: "text-[#4b74f2]" }), _jsx("h2", { className: "text-xl font-bold", children: "Tutor" })] }), _jsx("div", { className: `flex gap-2 overflow-x-auto border-b px-5 py-4 ${isDark ? "border-slate-800" : "border-[#f1ede6]"}`, children: modeButtons.map(({ mode, label, icon: Icon }) => (_jsxs("button", { type: "button", className: `inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-bold ${activeMode === mode ? activeChipClass : inactiveChipClass}`, onClick: () => changeMode(mode), children: [_jsx(Icon, { size: 16 }), label] }, mode))) }), _jsxs("div", { className: "min-h-0 overflow-y-auto px-5 py-5", children: [_jsx("div", { className: "mb-4 text-xs font-bold uppercase tracking-wide text-slate-400", children: statusLabel }), actionNotice ? (_jsx("div", { className: `mb-3 rounded-md border px-3 py-2 text-sm ${isDark ? "border-blue-900 bg-blue-950/40 text-blue-100" : "border-blue-200 bg-blue-50 text-blue-700"}`, children: actionNotice })) : null, highlightNotice ? (_jsx("div", { className: `mb-3 rounded-md border px-3 py-2 text-sm ${isDark ? "border-amber-900 bg-amber-950/40 text-amber-100" : "border-amber-200 bg-amber-50 text-amber-700"}`, children: highlightNotice })) : null, visibleError ? (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: visibleError })) : null, _jsx(WidgetStack, { widgets: widgetList, isDark: isDark, onLineClick: (line) => highlightEditorLines(line), onClose: (widgetId) => setWidgets((current) => {
481
+ const next = new Map(current);
482
+ next.delete(widgetId);
483
+ return next;
484
+ }) }), studyNote ? (_jsxs("details", { className: `mb-4 rounded-md border p-3 text-sm ${subtlePanelClass}`, children: [_jsx("summary", { className: "cursor-pointer font-bold", children: "Study note" }), _jsx("pre", { className: "mt-2 max-h-44 overflow-auto whitespace-pre-wrap font-sans text-sm", children: studyNote })] })) : null, tutor.transcript.length === 0 ? (_jsxs("div", { className: "flex h-full min-h-[260px] flex-col items-center justify-center text-center text-sm text-slate-500", children: [isBusy ? (_jsx(Loader2, { className: "mb-3 animate-spin text-[#4b74f2]" })) : (_jsx(Mic, { className: "mb-3 text-[#4b74f2]" })), statusLabel] })) : (_jsx("div", { className: "space-y-4", children: tutor.transcript.map((turn) => (_jsxs("div", { className: `max-w-[92%] rounded-md px-3 py-2 text-sm leading-6 ${turn.speaker === "agent"
485
+ ? isDark
486
+ ? "bg-slate-800 text-slate-100"
487
+ : "bg-slate-50 text-slate-800"
488
+ : "ml-auto bg-[#4b74f2] text-white"}`, children: [_jsx("div", { className: "mb-1 text-[11px] font-bold uppercase opacity-60", children: turn.speaker === "agent" ? "Tutor" : "You" }), turn.text] }, turn.id))) }))] }), _jsxs("div", { className: `border-t p-4 ${isDark ? "border-slate-800" : "border-[#f1ede6]"}`, children: [_jsxs("div", { className: `flex min-h-[72px] items-center gap-3 rounded-3xl border px-4 py-3 shadow-sm ${subtlePanelClass}`, children: [_jsxs("button", { type: "button", className: `inline-flex h-11 shrink-0 items-center gap-2 rounded-full border px-3 text-sm font-semibold disabled:opacity-50 ${inactiveChipClass}`, disabled: !isConnected, onClick: tutor.toggleMute, title: tutor.state === "muted" ? "Unmute" : "Mute", children: [tutor.state === "muted" ? _jsx(Mic, { size: 18 }) : _jsx(MicOff, { size: 18 }), tutor.state === "muted" ? "Unmute" : "Mute"] }), _jsx("input", { className: "min-w-0 flex-1 bg-transparent text-sm outline-none", value: text, placeholder: "Speak or type a message...", onChange: (event) => setText(event.target.value), onKeyDown: (event) => {
489
+ if (event.key === "Enter")
490
+ sendText();
491
+ }, disabled: !isConnected }), _jsx("button", { type: "button", className: "grid h-12 w-12 shrink-0 place-items-center rounded-full bg-[#bcd0ff] text-white disabled:cursor-not-allowed disabled:opacity-50", title: "Send message", disabled: !isConnected || !text.trim(), onClick: sendText, children: _jsx(Send, { size: 18 }) })] }), _jsxs("button", { type: "button", className: "mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-red-600 px-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50", disabled: !isConnected && !isBusy, onClick: endSession, children: [_jsx(PhoneOff, { size: 16 }), "End Session"] })] })] })] })] }) })) : null] }));
492
+ }
493
+ //# sourceMappingURL=DsaTutorLauncher.js.map