@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 +97 -0
- package/dist/DsaTutorLauncher.d.ts +34 -0
- package/dist/DsaTutorLauncher.d.ts.map +1 -0
- package/dist/DsaTutorLauncher.js +493 -0
- package/dist/DsaTutorLauncher.js.map +1 -0
- package/dist/EditProposalPopover.d.ts +25 -0
- package/dist/EditProposalPopover.d.ts.map +1 -0
- package/dist/EditProposalPopover.js +60 -0
- package/dist/EditProposalPopover.js.map +1 -0
- package/dist/WidgetStack.d.ts +16 -0
- package/dist/WidgetStack.d.ts.map +1 -0
- package/dist/WidgetStack.js +135 -0
- package/dist/WidgetStack.js.map +1 -0
- package/dist/dsaTutorRedaction.d.ts +21 -0
- package/dist/dsaTutorRedaction.d.ts.map +1 -0
- package/dist/dsaTutorRedaction.js +36 -0
- package/dist/dsaTutorRedaction.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/useDsaTutorLiveKit.d.ts +22 -0
- package/dist/useDsaTutorLiveKit.d.ts.map +1 -0
- package/dist/useDsaTutorLiveKit.js +219 -0
- package/dist/useDsaTutorLiveKit.js.map +1 -0
- package/package.json +50 -0
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
|