@ai-uni/mcp-server 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/dist/auth.d.ts +6 -0
- package/dist/auth.js +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +341 -0
- package/dist/supabase.d.ts +1 -0
- package/dist/supabase.js +6 -0
- package/package.json +25 -0
- package/prompts/session-panel.html +574 -0
- package/prompts/tutor-system.md +95 -0
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { supabase } from "./supabase.js";
|
|
3
|
+
let cachedStudentId = null;
|
|
4
|
+
function hashToken(token) {
|
|
5
|
+
return crypto.createHash("sha256").update(token).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the AI_UNI_TOKEN env var to a student ID.
|
|
9
|
+
* Caches the result so Supabase is only queried once per server lifetime.
|
|
10
|
+
* Updates last_used_at on first call.
|
|
11
|
+
*/
|
|
12
|
+
export async function getStudentId() {
|
|
13
|
+
if (cachedStudentId)
|
|
14
|
+
return cachedStudentId;
|
|
15
|
+
const token = process.env.AI_UNI_TOKEN;
|
|
16
|
+
if (!token) {
|
|
17
|
+
throw new Error("AI_UNI_TOKEN environment variable is not set");
|
|
18
|
+
}
|
|
19
|
+
const hash = hashToken(token);
|
|
20
|
+
const { data, error } = await supabase
|
|
21
|
+
.from("api_tokens")
|
|
22
|
+
.select("student_id")
|
|
23
|
+
.eq("token_hash", hash)
|
|
24
|
+
.single();
|
|
25
|
+
if (error || !data) {
|
|
26
|
+
throw new Error("Invalid API token — check your AI_UNI_TOKEN");
|
|
27
|
+
}
|
|
28
|
+
// Update last_used_at
|
|
29
|
+
await supabase
|
|
30
|
+
.from("api_tokens")
|
|
31
|
+
.update({ last_used_at: new Date().toISOString() })
|
|
32
|
+
.eq("token_hash", hash);
|
|
33
|
+
cachedStudentId = data.student_id;
|
|
34
|
+
return cachedStudentId;
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { supabase } from "./supabase.js";
|
|
6
|
+
import { getStudentId } from "./auth.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "aiuni-mcp",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function textResult(text) {
|
|
15
|
+
return { content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
function jsonResult(data) {
|
|
18
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
19
|
+
}
|
|
20
|
+
function errorResult(msg) {
|
|
21
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
/** Get the next incomplete session for a student across their enrollments. */
|
|
24
|
+
async function findNextSession(studentId) {
|
|
25
|
+
// Get all enrollments
|
|
26
|
+
const { data: enrollments } = await supabase
|
|
27
|
+
.from("enrollments")
|
|
28
|
+
.select("course_id, courses(code, title, total_sessions)")
|
|
29
|
+
.eq("student_id", studentId)
|
|
30
|
+
.order("started_at", { ascending: true });
|
|
31
|
+
if (!enrollments?.length)
|
|
32
|
+
return null;
|
|
33
|
+
for (const enrollment of enrollments) {
|
|
34
|
+
const course = enrollment.courses;
|
|
35
|
+
// Get all sessions for this course, ordered
|
|
36
|
+
const { data: sessions } = await supabase
|
|
37
|
+
.from("sessions")
|
|
38
|
+
.select("id, sequence_num, title")
|
|
39
|
+
.eq("course_id", enrollment.course_id)
|
|
40
|
+
.order("sequence_num", { ascending: true });
|
|
41
|
+
if (!sessions?.length)
|
|
42
|
+
continue;
|
|
43
|
+
// Get completed/in-progress session IDs
|
|
44
|
+
const { data: progress } = await supabase
|
|
45
|
+
.from("session_progress")
|
|
46
|
+
.select("session_id, status")
|
|
47
|
+
.eq("student_id", studentId)
|
|
48
|
+
.in("session_id", sessions.map((s) => s.id));
|
|
49
|
+
const completedIds = new Set((progress ?? [])
|
|
50
|
+
.filter((p) => p.status === "completed")
|
|
51
|
+
.map((p) => p.session_id));
|
|
52
|
+
// Find first non-completed session
|
|
53
|
+
for (const session of sessions) {
|
|
54
|
+
if (!completedIds.has(session.id)) {
|
|
55
|
+
return {
|
|
56
|
+
session_id: session.id,
|
|
57
|
+
course_code: course.code,
|
|
58
|
+
course_title: course.title,
|
|
59
|
+
sequence_num: session.sequence_num,
|
|
60
|
+
total_sessions: course.total_sessions,
|
|
61
|
+
session_title: session.title,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Tool: get_student_context
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
server.tool("get_student_context", "Returns the student's profile, enrollments, course progress, and next assignment. Call this at the start of every tutoring session.", {}, async () => {
|
|
72
|
+
try {
|
|
73
|
+
const studentId = await getStudentId();
|
|
74
|
+
// Student profile
|
|
75
|
+
const { data: student } = await supabase
|
|
76
|
+
.from("students")
|
|
77
|
+
.select("id, name, email, onboarding_complete, role, created_at")
|
|
78
|
+
.eq("id", studentId)
|
|
79
|
+
.single();
|
|
80
|
+
if (!student)
|
|
81
|
+
return errorResult("Student not found");
|
|
82
|
+
// Enrollments with course info
|
|
83
|
+
const { data: enrollments } = await supabase
|
|
84
|
+
.from("enrollments")
|
|
85
|
+
.select("course_id, started_at, courses(code, title, total_sessions)")
|
|
86
|
+
.eq("student_id", studentId);
|
|
87
|
+
// All progress
|
|
88
|
+
const { data: allProgress } = await supabase
|
|
89
|
+
.from("session_progress")
|
|
90
|
+
.select("session_id, status, completed_at, sessions(sequence_num, title, course_id)")
|
|
91
|
+
.eq("student_id", studentId);
|
|
92
|
+
// Build per-course progress summary
|
|
93
|
+
const courseProgress = (enrollments ?? []).map((e) => {
|
|
94
|
+
const course = e.courses;
|
|
95
|
+
const courseSessionProgress = (allProgress ?? []).filter((p) => {
|
|
96
|
+
const sess = p.sessions;
|
|
97
|
+
return sess?.course_id === e.course_id;
|
|
98
|
+
});
|
|
99
|
+
const completed = courseSessionProgress.filter((p) => p.status === "completed").length;
|
|
100
|
+
return {
|
|
101
|
+
course_code: course.code,
|
|
102
|
+
course_title: course.title,
|
|
103
|
+
total_sessions: course.total_sessions,
|
|
104
|
+
completed_sessions: completed,
|
|
105
|
+
in_progress: courseSessionProgress.filter((p) => p.status === "in_progress").length,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
// Next assignment
|
|
109
|
+
const next = await findNextSession(studentId);
|
|
110
|
+
return jsonResult({
|
|
111
|
+
student: {
|
|
112
|
+
name: student.name,
|
|
113
|
+
email: student.email,
|
|
114
|
+
onboarding_complete: student.onboarding_complete,
|
|
115
|
+
member_since: student.created_at,
|
|
116
|
+
},
|
|
117
|
+
courses: courseProgress,
|
|
118
|
+
next_assignment: next
|
|
119
|
+
? {
|
|
120
|
+
session_id: next.session_id,
|
|
121
|
+
course: `${next.course_code}: ${next.course_title}`,
|
|
122
|
+
session: `Session ${next.sequence_num}: ${next.session_title}`,
|
|
123
|
+
progress: `${next.sequence_num - 1}/${next.total_sessions} completed`,
|
|
124
|
+
}
|
|
125
|
+
: null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return errorResult(err.message);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Tool: get_current_assignment
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
server.tool("get_current_assignment", "Returns the full session brief for the student's current (next incomplete) assignment, including overview, learning objectives, topics, and exercise details.", {}, async () => {
|
|
136
|
+
try {
|
|
137
|
+
const studentId = await getStudentId();
|
|
138
|
+
const next = await findNextSession(studentId);
|
|
139
|
+
if (!next) {
|
|
140
|
+
return textResult("All sessions completed! The student has finished all enrolled courses.");
|
|
141
|
+
}
|
|
142
|
+
// Get full session data
|
|
143
|
+
const { data: session } = await supabase
|
|
144
|
+
.from("sessions")
|
|
145
|
+
.select("id, sequence_num, title, brief_json, hints_json")
|
|
146
|
+
.eq("id", next.session_id)
|
|
147
|
+
.single();
|
|
148
|
+
if (!session)
|
|
149
|
+
return errorResult("Session not found");
|
|
150
|
+
// Check if there's existing progress (in_progress)
|
|
151
|
+
const { data: progress } = await supabase
|
|
152
|
+
.from("session_progress")
|
|
153
|
+
.select("status, notes, session_notes")
|
|
154
|
+
.eq("student_id", studentId)
|
|
155
|
+
.eq("session_id", session.id)
|
|
156
|
+
.single();
|
|
157
|
+
return jsonResult({
|
|
158
|
+
session_id: session.id,
|
|
159
|
+
course: `${next.course_code}: ${next.course_title}`,
|
|
160
|
+
session_number: session.sequence_num,
|
|
161
|
+
total_sessions: next.total_sessions,
|
|
162
|
+
title: session.title,
|
|
163
|
+
brief: session.brief_json,
|
|
164
|
+
tutor_guidance: session.hints_json,
|
|
165
|
+
current_status: progress?.status ?? "not_started",
|
|
166
|
+
existing_notes: progress?.session_notes ?? [],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
return errorResult(err.message);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Tool: get_rubric
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
server.tool("get_rubric", "Returns the evaluation rubric and acceptance criteria for a specific session.", {
|
|
177
|
+
session_id: z.string().describe("Session UUID to get rubric for"),
|
|
178
|
+
}, async ({ session_id }) => {
|
|
179
|
+
try {
|
|
180
|
+
await getStudentId(); // auth check
|
|
181
|
+
const { data: session } = await supabase
|
|
182
|
+
.from("sessions")
|
|
183
|
+
.select("id, sequence_num, title, rubric_json, brief_json")
|
|
184
|
+
.eq("id", session_id)
|
|
185
|
+
.single();
|
|
186
|
+
if (!session)
|
|
187
|
+
return errorResult("Session not found");
|
|
188
|
+
// Extract exercise info from brief for context
|
|
189
|
+
const exercise = session.brief_json?.exercise;
|
|
190
|
+
return jsonResult({
|
|
191
|
+
session_id: session.id,
|
|
192
|
+
session_number: session.sequence_num,
|
|
193
|
+
title: session.title,
|
|
194
|
+
rubric: session.rubric_json,
|
|
195
|
+
exercise_summary: exercise ?? null,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return errorResult(err.message);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Tool: submit_session_complete
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
server.tool("submit_session_complete", "Marks a session as completed. Optionally stores an artifact URL, artifact text, and notes. Use this when the student has met all acceptance criteria.", {
|
|
206
|
+
session_id: z.string().describe("Session UUID to mark complete"),
|
|
207
|
+
artifact_url: z.string().optional().describe("URL to the student's submitted work"),
|
|
208
|
+
artifact_text: z.string().optional().describe("Pasted text of the student's work"),
|
|
209
|
+
notes: z.string().optional().describe("Tutor summary of the session"),
|
|
210
|
+
}, async ({ session_id, artifact_url, artifact_text, notes }) => {
|
|
211
|
+
try {
|
|
212
|
+
const studentId = await getStudentId();
|
|
213
|
+
// Verify session exists
|
|
214
|
+
const { data: session } = await supabase
|
|
215
|
+
.from("sessions")
|
|
216
|
+
.select("id, sequence_num, title, course_id")
|
|
217
|
+
.eq("id", session_id)
|
|
218
|
+
.single();
|
|
219
|
+
if (!session)
|
|
220
|
+
return errorResult("Session not found");
|
|
221
|
+
// Upsert progress
|
|
222
|
+
const { error } = await supabase.from("session_progress").upsert({
|
|
223
|
+
student_id: studentId,
|
|
224
|
+
session_id: session_id,
|
|
225
|
+
status: "completed",
|
|
226
|
+
completed_at: new Date().toISOString(),
|
|
227
|
+
artifact_url: artifact_url ?? null,
|
|
228
|
+
artifact_text: artifact_text ?? null,
|
|
229
|
+
notes: notes ?? null,
|
|
230
|
+
}, { onConflict: "student_id,session_id" });
|
|
231
|
+
if (error)
|
|
232
|
+
return errorResult(`Failed to update progress: ${error.message}`);
|
|
233
|
+
// Find what's next
|
|
234
|
+
const next = await findNextSession(studentId);
|
|
235
|
+
return jsonResult({
|
|
236
|
+
success: true,
|
|
237
|
+
completed: {
|
|
238
|
+
session_number: session.sequence_num,
|
|
239
|
+
title: session.title,
|
|
240
|
+
},
|
|
241
|
+
next_assignment: next
|
|
242
|
+
? {
|
|
243
|
+
session_id: next.session_id,
|
|
244
|
+
session: `Session ${next.sequence_num}: ${next.session_title}`,
|
|
245
|
+
course: `${next.course_code}: ${next.course_title}`,
|
|
246
|
+
}
|
|
247
|
+
: "All sessions complete! 🎓",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return errorResult(err.message);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Tool: get_next_assignment
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
server.tool("get_next_assignment", "Returns the next incomplete session for the student. Use this after completing a session or to check what's coming up.", {}, async () => {
|
|
258
|
+
try {
|
|
259
|
+
const studentId = await getStudentId();
|
|
260
|
+
const next = await findNextSession(studentId);
|
|
261
|
+
if (!next) {
|
|
262
|
+
return textResult("All sessions completed! The student has finished all enrolled courses. 🎓");
|
|
263
|
+
}
|
|
264
|
+
// Get the full brief for the next session
|
|
265
|
+
const { data: session } = await supabase
|
|
266
|
+
.from("sessions")
|
|
267
|
+
.select("id, sequence_num, title, brief_json")
|
|
268
|
+
.eq("id", next.session_id)
|
|
269
|
+
.single();
|
|
270
|
+
return jsonResult({
|
|
271
|
+
session_id: next.session_id,
|
|
272
|
+
course: `${next.course_code}: ${next.course_title}`,
|
|
273
|
+
session_number: next.sequence_num,
|
|
274
|
+
total_sessions: next.total_sessions,
|
|
275
|
+
title: next.session_title,
|
|
276
|
+
overview: session?.brief_json?.overview ?? null,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
return errorResult(err.message);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Tool: save_session_note
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
server.tool("save_session_note", "Saves a tutor observation or note during a session. Notes are appended to the session's note log and persist across conversations. Use this to record student strengths, struggles, or key moments.", {
|
|
287
|
+
session_id: z.string().describe("Session UUID"),
|
|
288
|
+
topic: z.string().describe("Note category (e.g. 'strength', 'struggle', 'insight', 'feedback')"),
|
|
289
|
+
note: z.string().describe("The note content"),
|
|
290
|
+
}, async ({ session_id, topic, note }) => {
|
|
291
|
+
try {
|
|
292
|
+
const studentId = await getStudentId();
|
|
293
|
+
// Ensure progress row exists (create as in_progress if not)
|
|
294
|
+
const { data: existing } = await supabase
|
|
295
|
+
.from("session_progress")
|
|
296
|
+
.select("id, session_notes, status")
|
|
297
|
+
.eq("student_id", studentId)
|
|
298
|
+
.eq("session_id", session_id)
|
|
299
|
+
.single();
|
|
300
|
+
const newNote = {
|
|
301
|
+
topic,
|
|
302
|
+
note,
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
if (existing) {
|
|
306
|
+
const currentNotes = existing.session_notes ?? [];
|
|
307
|
+
const { error } = await supabase
|
|
308
|
+
.from("session_progress")
|
|
309
|
+
.update({ session_notes: [...currentNotes, newNote] })
|
|
310
|
+
.eq("id", existing.id);
|
|
311
|
+
if (error)
|
|
312
|
+
return errorResult(`Failed to save note: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Create progress row as in_progress
|
|
316
|
+
const { error } = await supabase.from("session_progress").insert({
|
|
317
|
+
student_id: studentId,
|
|
318
|
+
session_id: session_id,
|
|
319
|
+
status: "in_progress",
|
|
320
|
+
session_notes: [newNote],
|
|
321
|
+
});
|
|
322
|
+
if (error)
|
|
323
|
+
return errorResult(`Failed to save note: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
return jsonResult({
|
|
326
|
+
success: true,
|
|
327
|
+
note_saved: { topic, note },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
return errorResult(err.message);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Start server
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
async function main() {
|
|
338
|
+
const transport = new StdioServerTransport();
|
|
339
|
+
await server.connect(transport);
|
|
340
|
+
}
|
|
341
|
+
main().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
const DEFAULT_URL = "https://kirwyicyoyogjrsautvl.supabase.co";
|
|
3
|
+
const DEFAULT_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtpcnd5aWN5b3lvZ2pyc2F1dHZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI4MjkwOTIsImV4cCI6MjA4ODQwNTA5Mn0.vkNhemuocxkg5jIPGIljsl2hYxp5ONhXSGKmdHCIlAI";
|
|
4
|
+
const url = process.env.SUPABASE_URL ?? DEFAULT_URL;
|
|
5
|
+
const key = process.env.SUPABASE_ANON_KEY ?? DEFAULT_ANON_KEY;
|
|
6
|
+
export const supabase = createClient(url, key);
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-uni/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-uni-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && node -e \"const f=require('fs');const p='dist/index.js';const c=f.readFileSync(p,'utf8');if(!c.startsWith('#!'))f.writeFileSync(p,'#!/usr/bin/env node\\n'+c)\"",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"description": "AI Uni MCP server for Claude Desktop integration",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
18
|
+
"@supabase/supabase-js": "^2.98.0",
|
|
19
|
+
"zod": "^3.23.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.3.5",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Uni — Session Panel</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap');
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d0f0e;
|
|
12
|
+
--surface: #131614;
|
|
13
|
+
--surface2: #181c19;
|
|
14
|
+
--border: #232825;
|
|
15
|
+
--border-hi: #2d3530;
|
|
16
|
+
--green: #4ade80;
|
|
17
|
+
--green-dim: #163324;
|
|
18
|
+
--green-glow: rgba(74,222,128,0.08);
|
|
19
|
+
--text: #dde8e1;
|
|
20
|
+
--dim: #6b7d72;
|
|
21
|
+
--muted: #323d36;
|
|
22
|
+
--blue: #7ab8f5;
|
|
23
|
+
--blue-dim: #172233;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
font-family: 'IBM Plex Sans', sans-serif;
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
padding: 18px 16px 24px;
|
|
35
|
+
width: 320px;
|
|
36
|
+
max-width: 320px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.hd {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: space-between;
|
|
43
|
+
margin-bottom: 14px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.brand {
|
|
47
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
48
|
+
font-size: 12px;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
color: var(--dim);
|
|
51
|
+
letter-spacing: 0.04em;
|
|
52
|
+
}
|
|
53
|
+
.brand em { color: var(--green); font-style: normal; }
|
|
54
|
+
|
|
55
|
+
.session-badge {
|
|
56
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
57
|
+
font-size: 10px;
|
|
58
|
+
color: var(--dim);
|
|
59
|
+
background: var(--surface2);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
padding: 3px 8px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.status {
|
|
66
|
+
background: var(--surface);
|
|
67
|
+
border: 1px solid var(--border);
|
|
68
|
+
border-radius: 7px;
|
|
69
|
+
padding: 11px 13px;
|
|
70
|
+
margin-bottom: 16px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stu-name {
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
color: var(--text);
|
|
79
|
+
margin-bottom: 1px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stu-sub {
|
|
83
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
84
|
+
font-size: 10px;
|
|
85
|
+
color: var(--dim);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.status-right {
|
|
89
|
+
margin-left: auto;
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
align-items: flex-end;
|
|
93
|
+
gap: 5px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.prog-wrap {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 7px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.prog-track {
|
|
103
|
+
width: 72px;
|
|
104
|
+
height: 3px;
|
|
105
|
+
background: var(--muted);
|
|
106
|
+
border-radius: 2px;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.prog-fill {
|
|
111
|
+
height: 100%;
|
|
112
|
+
background: var(--green);
|
|
113
|
+
border-radius: 2px;
|
|
114
|
+
width: 10%;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.prog-text {
|
|
118
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
119
|
+
font-size: 10px;
|
|
120
|
+
color: var(--dim);
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.state-pill {
|
|
125
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
126
|
+
font-size: 9px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
letter-spacing: 0.08em;
|
|
129
|
+
text-transform: uppercase;
|
|
130
|
+
padding: 2px 7px;
|
|
131
|
+
border-radius: 10px;
|
|
132
|
+
background: var(--green-dim);
|
|
133
|
+
color: var(--green);
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 4px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dot {
|
|
140
|
+
width: 5px; height: 5px;
|
|
141
|
+
border-radius: 50%;
|
|
142
|
+
background: var(--green);
|
|
143
|
+
animation: blink 2s ease-in-out infinite;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
|
147
|
+
|
|
148
|
+
.sec { margin-bottom: 12px; }
|
|
149
|
+
|
|
150
|
+
.sec-label {
|
|
151
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
152
|
+
font-size: 9px;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
letter-spacing: 0.12em;
|
|
156
|
+
color: var(--muted);
|
|
157
|
+
margin-bottom: 5px;
|
|
158
|
+
padding-left: 2px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.cmd-list {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 2px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.cmd {
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
border-radius: 6px;
|
|
171
|
+
border: 1px solid transparent;
|
|
172
|
+
background: transparent;
|
|
173
|
+
transition: background 0.12s, border-color 0.12s;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cmd-row {
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.cmd.hi {
|
|
183
|
+
background: var(--green-glow);
|
|
184
|
+
border-color: var(--green-dim);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.cmd.open {
|
|
188
|
+
background: var(--surface2);
|
|
189
|
+
border-color: var(--border-hi);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cmd.hi.open {
|
|
193
|
+
background: rgba(74,222,128,0.11);
|
|
194
|
+
border-color: rgba(74,222,128,0.3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cmd-inner {
|
|
198
|
+
flex: 1;
|
|
199
|
+
padding: 10px 10px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.cmd-label {
|
|
203
|
+
font-size: 14px;
|
|
204
|
+
color: var(--text);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.cmd.hi .cmd-label {
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
color: var(--green);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* tooltip — click toggled */
|
|
213
|
+
.cmd-tip {
|
|
214
|
+
display: none;
|
|
215
|
+
left: 0; right: 0;
|
|
216
|
+
background: #19211e;
|
|
217
|
+
border-top: 1px solid var(--border-hi);
|
|
218
|
+
padding: 10px 12px 11px;
|
|
219
|
+
margin: 0 -1px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cmd.open .cmd-tip { display: block; }
|
|
223
|
+
|
|
224
|
+
.tip-text {
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
color: var(--dim);
|
|
227
|
+
line-height: 1.5;
|
|
228
|
+
margin-bottom: 7px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.tip-tools {
|
|
232
|
+
display: flex;
|
|
233
|
+
gap: 4px;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.tip-tool {
|
|
238
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
239
|
+
font-size: 10px;
|
|
240
|
+
padding: 2px 6px;
|
|
241
|
+
background: var(--blue-dim);
|
|
242
|
+
color: var(--blue);
|
|
243
|
+
border-radius: 3px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* info toggle button */
|
|
247
|
+
.info-btn {
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
width: 32px;
|
|
250
|
+
height: 32px;
|
|
251
|
+
margin: 0 4px 0 0;
|
|
252
|
+
background: transparent;
|
|
253
|
+
border: 1px solid transparent;
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
color: var(--muted);
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: center;
|
|
261
|
+
transition: all 0.12s;
|
|
262
|
+
position: relative;
|
|
263
|
+
z-index: 20;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.info-btn:hover, .cmd.open .info-btn {
|
|
267
|
+
color: var(--dim);
|
|
268
|
+
border-color: var(--border-hi);
|
|
269
|
+
background: var(--surface2);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.copy-btn {
|
|
273
|
+
flex-shrink: 0;
|
|
274
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
275
|
+
font-size: 15px;
|
|
276
|
+
font-weight: 500;
|
|
277
|
+
padding: 10px 18px;
|
|
278
|
+
margin: 6px 6px 6px 0;
|
|
279
|
+
background: transparent;
|
|
280
|
+
border: 1px solid var(--border-hi);
|
|
281
|
+
border-radius: 6px;
|
|
282
|
+
color: var(--dim);
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
transition: all 0.12s;
|
|
286
|
+
position: relative;
|
|
287
|
+
z-index: 20;
|
|
288
|
+
min-width: 72px;
|
|
289
|
+
text-align: center;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.copy-btn:active {
|
|
293
|
+
transform: scale(0.96);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.copy-btn:hover {
|
|
297
|
+
background: var(--muted);
|
|
298
|
+
color: var(--text);
|
|
299
|
+
border-color: var(--dim);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.copy-btn.ok {
|
|
303
|
+
background: var(--green-dim);
|
|
304
|
+
border-color: var(--green);
|
|
305
|
+
color: var(--green);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.cmd.hi .copy-btn {
|
|
309
|
+
border-color: rgba(74,222,128,0.35);
|
|
310
|
+
color: var(--green);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.div {
|
|
314
|
+
height: 1px;
|
|
315
|
+
background: var(--border);
|
|
316
|
+
margin: 4px 0 12px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.foot {
|
|
320
|
+
margin-top: 4px;
|
|
321
|
+
position: relative;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.webapp-btn {
|
|
325
|
+
display: block;
|
|
326
|
+
width: 100%;
|
|
327
|
+
padding: 14px;
|
|
328
|
+
background: var(--surface);
|
|
329
|
+
border: 1px solid var(--border);
|
|
330
|
+
border-radius: 6px;
|
|
331
|
+
color: var(--dim);
|
|
332
|
+
font-family: 'IBM Plex Sans', sans-serif;
|
|
333
|
+
font-size: 15px;
|
|
334
|
+
font-weight: 500;
|
|
335
|
+
text-align: center;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
338
|
+
text-decoration: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.webapp-btn:active {
|
|
342
|
+
transform: scale(0.98);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.webapp-btn:hover, .webapp-btn.open {
|
|
346
|
+
background: var(--surface2);
|
|
347
|
+
border-color: var(--border-hi);
|
|
348
|
+
color: var(--text);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.webapp-tip {
|
|
352
|
+
display: none;
|
|
353
|
+
background: #19211e;
|
|
354
|
+
border: 1px solid var(--border-hi);
|
|
355
|
+
border-top: none;
|
|
356
|
+
border-radius: 0 0 6px 6px;
|
|
357
|
+
padding: 10px 12px;
|
|
358
|
+
font-size: 12px;
|
|
359
|
+
color: var(--dim);
|
|
360
|
+
line-height: 1.5;
|
|
361
|
+
margin-top: -1px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.webapp-wrap.open .webapp-tip { display: block; }
|
|
365
|
+
.webapp-wrap.open .webapp-btn {
|
|
366
|
+
border-radius: 6px 6px 0 0;
|
|
367
|
+
border-color: var(--border-hi);
|
|
368
|
+
background: var(--surface2);
|
|
369
|
+
color: var(--text);
|
|
370
|
+
}
|
|
371
|
+
</style>
|
|
372
|
+
</head>
|
|
373
|
+
<body>
|
|
374
|
+
|
|
375
|
+
<div class="hd">
|
|
376
|
+
<div class="brand">ai<em>-uni</em></div>
|
|
377
|
+
<div class="session-badge">CORE-01 · S1 of 10</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div class="status">
|
|
381
|
+
<div>
|
|
382
|
+
<div class="stu-name">Jon</div>
|
|
383
|
+
<div class="stu-sub">AI Fluency · Session 1</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="status-right">
|
|
386
|
+
<div class="state-pill"><div class="dot"></div>In progress</div>
|
|
387
|
+
<div class="prog-wrap">
|
|
388
|
+
<div class="prog-track"><div class="prog-fill"></div></div>
|
|
389
|
+
<div class="prog-text">1 / 10</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- Starting out -->
|
|
395
|
+
<div class="sec">
|
|
396
|
+
<div class="sec-label">Starting out</div>
|
|
397
|
+
<div class="cmd-list">
|
|
398
|
+
|
|
399
|
+
<div class="cmd hi">
|
|
400
|
+
<div class="cmd-row">
|
|
401
|
+
<div class="cmd-inner"><div class="cmd-label">Let's go</div></div>
|
|
402
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
403
|
+
<button class="copy-btn" onclick="cp(this,`Let's go`)">Copy</button>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="cmd-tip">
|
|
406
|
+
<div class="tip-text">Opens your session. Claude checks where you are and picks up in context — or starts fresh if this is session 1.</div>
|
|
407
|
+
<div class="tip-tools"><span class="tip-tool">get_student_context</span><span class="tip-tool">get_current_assignment</span></div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div class="cmd">
|
|
412
|
+
<div class="cmd-row">
|
|
413
|
+
<div class="cmd-inner"><div class="cmd-label">Pick up where I left off</div></div>
|
|
414
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
415
|
+
<button class="copy-btn" onclick="cp(this,`Pick up where I left off`)">Copy</button>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="cmd-tip">
|
|
418
|
+
<div class="tip-text">Restores your last saved note. Use this if you closed Claude mid-session and are returning.</div>
|
|
419
|
+
<div class="tip-tools"><span class="tip-tool">get_student_context</span></div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="div"></div>
|
|
427
|
+
|
|
428
|
+
<!-- Mid session -->
|
|
429
|
+
<div class="sec">
|
|
430
|
+
<div class="sec-label">Mid session</div>
|
|
431
|
+
<div class="cmd-list">
|
|
432
|
+
|
|
433
|
+
<div class="cmd">
|
|
434
|
+
<div class="cmd-row">
|
|
435
|
+
<div class="cmd-inner"><div class="cmd-label">What's my exercise?</div></div>
|
|
436
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
437
|
+
<button class="copy-btn" onclick="cp(this,`What's my exercise?`)">Copy</button>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="cmd-tip">
|
|
440
|
+
<div class="tip-text">Shows the full exercise, format, and what done looks like. Pull this up before you start working.</div>
|
|
441
|
+
<div class="tip-tools"><span class="tip-tool">get_current_assignment</span></div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div class="cmd">
|
|
446
|
+
<div class="cmd-row">
|
|
447
|
+
<div class="cmd-inner"><div class="cmd-label">How will this be graded?</div></div>
|
|
448
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
449
|
+
<button class="copy-btn" onclick="cp(this,`How will this be graded?`)">Copy</button>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="cmd-tip">
|
|
452
|
+
<div class="tip-text">Shows the exact rubric. Know these before you submit — Claude evaluates against each criterion specifically.</div>
|
|
453
|
+
<div class="tip-tools"><span class="tip-tool">get_rubric</span></div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<div class="cmd">
|
|
458
|
+
<div class="cmd-row">
|
|
459
|
+
<div class="cmd-inner"><div class="cmd-label">Save and stop</div></div>
|
|
460
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
461
|
+
<button class="copy-btn" onclick="cp(this,`Save and stop for now`)">Copy</button>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="cmd-tip">
|
|
464
|
+
<div class="tip-text">Saves a note so you can resume here later. Always do this before closing Claude mid-session.</div>
|
|
465
|
+
<div class="tip-tools"><span class="tip-tool">save_session_note</span></div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div class="div"></div>
|
|
473
|
+
|
|
474
|
+
<!-- Wrapping up -->
|
|
475
|
+
<div class="sec">
|
|
476
|
+
<div class="sec-label">Wrapping up</div>
|
|
477
|
+
<div class="cmd-list">
|
|
478
|
+
|
|
479
|
+
<div class="cmd">
|
|
480
|
+
<div class="cmd-row">
|
|
481
|
+
<div class="cmd-inner"><div class="cmd-label">Check my work</div></div>
|
|
482
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
483
|
+
<button class="copy-btn" onclick="cp(this,`Check my work against the rubric`)">Copy</button>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="cmd-tip">
|
|
486
|
+
<div class="tip-text">Claude reviews your work against the rubric and gives specific feedback. Do this before marking complete.</div>
|
|
487
|
+
<div class="tip-tools"><span class="tip-tool">get_rubric</span></div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div class="cmd hi">
|
|
492
|
+
<div class="cmd-row">
|
|
493
|
+
<div class="cmd-inner"><div class="cmd-label">Mark complete</div></div>
|
|
494
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
495
|
+
<button class="copy-btn" onclick="cp(this,`Mark this session complete`)">Copy</button>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="cmd-tip">
|
|
498
|
+
<div class="tip-text">Records completion and unlocks your next session. Only after Claude confirms your work passes. Then confirm in the web app.</div>
|
|
499
|
+
<div class="tip-tools"><span class="tip-tool">submit_session_complete</span><span class="tip-tool">get_next_assignment</span></div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="div"></div>
|
|
507
|
+
|
|
508
|
+
<!-- Between sessions -->
|
|
509
|
+
<div class="sec">
|
|
510
|
+
<div class="sec-label">Between sessions</div>
|
|
511
|
+
<div class="cmd-list">
|
|
512
|
+
|
|
513
|
+
<div class="cmd">
|
|
514
|
+
<div class="cmd-row">
|
|
515
|
+
<div class="cmd-inner"><div class="cmd-label">What's next?</div></div>
|
|
516
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
517
|
+
<button class="copy-btn" onclick="cp(this,`What's next?`)">Copy</button>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="cmd-tip">
|
|
520
|
+
<div class="tip-text">Previews your next session without advancing. Good for planning.</div>
|
|
521
|
+
<div class="tip-tools"><span class="tip-tool">get_next_assignment</span></div>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<div class="cmd">
|
|
526
|
+
<div class="cmd-row">
|
|
527
|
+
<div class="cmd-inner"><div class="cmd-label">My progress</div></div>
|
|
528
|
+
<button class="info-btn" onclick="tog(this)">?</button>
|
|
529
|
+
<button class="copy-btn" onclick="cp(this,`Show my full course progress`)">Copy</button>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="cmd-tip">
|
|
532
|
+
<div class="tip-text">All sessions — done, active, upcoming — with your notes. Should match the web app.</div>
|
|
533
|
+
<div class="tip-tools"><span class="tip-tool">get_student_context</span></div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div class="foot">
|
|
541
|
+
<div class="webapp-wrap" id="webappWrap">
|
|
542
|
+
<a class="webapp-btn" href="https://app.ai-uni.tech" target="_blank" onclick="togWebapp(event)">
|
|
543
|
+
Open web app
|
|
544
|
+
</a>
|
|
545
|
+
<div class="webapp-tip">Check session status, confirm completions, and view your portfolio. If a session isn't marked done here, record it from the app — you don't need Claude for that.</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
<script>
|
|
550
|
+
function tog(infoBtn) {
|
|
551
|
+
const cmd = infoBtn.closest('.cmd');
|
|
552
|
+
cmd.classList.toggle('open');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function togWebapp(e) {
|
|
556
|
+
const wrap = document.getElementById('webappWrap');
|
|
557
|
+
if (wrap.classList.contains('open')) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
e.preventDefault();
|
|
561
|
+
wrap.classList.add('open');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function cp(btn, text) {
|
|
565
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
566
|
+
const p = btn.textContent;
|
|
567
|
+
btn.textContent = '✓';
|
|
568
|
+
btn.classList.add('ok');
|
|
569
|
+
setTimeout(() => { btn.textContent = p; btn.classList.remove('ok'); }, 1400);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
</script>
|
|
573
|
+
</body>
|
|
574
|
+
</html>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# AI Uni Tutor — System Prompt
|
|
2
|
+
|
|
3
|
+
*Paste this into your Claude Desktop Project instructions for AI Uni.*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the AI Uni tutor — a Socratic learning partner for students in AI Uni, a structured college alternative for the AI economy. You teach through questions and dialogue, not lectures. You have access to the student's curriculum, progress, and assignments through MCP tools.
|
|
8
|
+
|
|
9
|
+
## First Action (Every Conversation)
|
|
10
|
+
|
|
11
|
+
**Before saying anything**, call `get_student_context`. This tells you who the student is, what courses they're enrolled in, where they are in the curriculum, and what's next. Never skip this step.
|
|
12
|
+
|
|
13
|
+
After reviewing context, greet the student by name and summarize where they are:
|
|
14
|
+
- If they have a session in progress, offer to pick up where they left off.
|
|
15
|
+
- If they're starting a new session, call `get_current_assignment` and introduce it.
|
|
16
|
+
- If all sessions are complete, congratulate them and discuss what they've learned.
|
|
17
|
+
|
|
18
|
+
## Teaching Method: Socratic Dialogue
|
|
19
|
+
|
|
20
|
+
You are a tutor, not a textbook. Your job is to help students *think*, not to deliver information.
|
|
21
|
+
|
|
22
|
+
**Do:**
|
|
23
|
+
- Ask questions that lead to understanding: "What do you think would happen if...?" "Why might that approach cause problems?" "How would you explain this to someone who's never seen it?"
|
|
24
|
+
- Start with what the student already knows and build from there.
|
|
25
|
+
- Give the student space to struggle productively before offering help.
|
|
26
|
+
- When a student is stuck, ask a simpler question that points toward the answer — don't give it away.
|
|
27
|
+
- Celebrate genuine understanding, not just correct answers.
|
|
28
|
+
- Use concrete examples from the student's interests or prior work.
|
|
29
|
+
|
|
30
|
+
**Don't:**
|
|
31
|
+
- Lecture for more than 2-3 sentences at a time. If you're explaining for a paragraph, stop and ask a question instead.
|
|
32
|
+
- Give away answers. If a student asks "what's the answer?", respond with a question that helps them find it.
|
|
33
|
+
- Be condescending. These are motivated adults who chose this path. Treat them as capable.
|
|
34
|
+
- Use jargon without checking understanding. If you introduce a term, make sure the student can define it back to you.
|
|
35
|
+
- Rush. If a student needs more time on a concept, spend more time. The curriculum adapts to them, not the other way around.
|
|
36
|
+
|
|
37
|
+
## Session Flow
|
|
38
|
+
|
|
39
|
+
A typical tutoring session follows this arc:
|
|
40
|
+
|
|
41
|
+
1. **Context check** — Call `get_student_context`. Greet the student. Summarize their position.
|
|
42
|
+
2. **Assignment intro** — Call `get_current_assignment`. Walk through the session's learning objectives and topics. Ask what the student already knows about the subject.
|
|
43
|
+
3. **Guided exploration** — Work through each topic using Socratic questioning. After each major concept, call `save_session_note` to record the student's understanding, strengths, or struggles.
|
|
44
|
+
4. **Exercise** — When the student is ready, present the session's exercise. Guide them through it without doing it for them.
|
|
45
|
+
5. **Evaluation** — When the student believes they're done, call `get_rubric` to get the acceptance criteria. Evaluate their work against each criterion honestly. If they haven't met a criterion, explain what's missing and help them get there.
|
|
46
|
+
6. **Completion** — Only when all acceptance criteria are clearly met, call `submit_session_complete` with a summary of what the student accomplished. Then call `get_next_assignment` to preview what's coming.
|
|
47
|
+
|
|
48
|
+
## Using Your Tools
|
|
49
|
+
|
|
50
|
+
You have six MCP tools. Use them proactively — don't wait for the student to ask.
|
|
51
|
+
|
|
52
|
+
| Tool | When to Use |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `get_student_context` | Start of every conversation. No exceptions. |
|
|
55
|
+
| `get_current_assignment` | After context check, to load the session brief. |
|
|
56
|
+
| `get_rubric` | Before evaluating student work. Never evaluate without it. |
|
|
57
|
+
| `submit_session_complete` | Only when all acceptance criteria are met. Never premature. |
|
|
58
|
+
| `get_next_assignment` | After completing a session, or when student asks what's ahead. |
|
|
59
|
+
| `save_session_note` | After each topic block, when you notice a strength or struggle, or when the student has an insight. These notes persist across conversations — they're how future sessions know what happened. |
|
|
60
|
+
|
|
61
|
+
**save_session_note is critical.** Students may close the conversation and come back later. Your notes are the only continuity between conversations. Write notes that would help a different tutor pick up exactly where you left off. Include:
|
|
62
|
+
- What topics were covered and how well the student understood them
|
|
63
|
+
- What part of the exercise was reached
|
|
64
|
+
- Any misconceptions that surfaced
|
|
65
|
+
- The student's confidence level and engagement
|
|
66
|
+
|
|
67
|
+
## Tone and Style
|
|
68
|
+
|
|
69
|
+
- **Direct and honest.** Don't sugarcoat. If the work isn't good enough, say so — then help fix it.
|
|
70
|
+
- **Warm but professional.** You're a mentor, not a friend. Encouraging without being patronizing.
|
|
71
|
+
- **Concise.** Students are here to learn, not to read essays. Keep explanations tight.
|
|
72
|
+
- **Adaptive.** Match the student's communication style. Some want formal, some want casual. Mirror what they give you.
|
|
73
|
+
- **Patient.** Never express frustration. If a student is struggling, that's useful information — adjust your approach.
|
|
74
|
+
|
|
75
|
+
## What You Don't Do
|
|
76
|
+
|
|
77
|
+
- **Don't do the student's work.** Guide, question, hint — but the student must produce the output. If they paste code they want you to write, ask them to try first.
|
|
78
|
+
- **Don't grade on a curve.** The acceptance criteria are the standard. Met or not met.
|
|
79
|
+
- **Don't go off-curriculum.** If a student wants to explore a tangent, briefly address it, then bring them back to the session objectives.
|
|
80
|
+
- **Don't make up information.** If you're unsure about something technical, say so. Better to be honest than confidently wrong.
|
|
81
|
+
- **Don't mark sessions complete prematurely.** A student saying "I think I'm done" is not the same as meeting all acceptance criteria. Always check the rubric.
|
|
82
|
+
|
|
83
|
+
## Handling Common Situations
|
|
84
|
+
|
|
85
|
+
**Student is lost:** Back up. Ask what they *do* understand. Find the gap and fill it with questions, not lectures.
|
|
86
|
+
|
|
87
|
+
**Student wants to skip ahead:** Explain that each session builds on the last. Ask them to demonstrate understanding of the current material — if they truly know it, the session will go quickly.
|
|
88
|
+
|
|
89
|
+
**Student is frustrated:** Acknowledge it. "This is genuinely difficult — that's normal." Then simplify: break the problem into smaller pieces.
|
|
90
|
+
|
|
91
|
+
**Student submits incomplete work:** Call `get_rubric`, point to the specific criteria not met, and help them address each one.
|
|
92
|
+
|
|
93
|
+
**Student hasn't been here in a while:** Check `get_student_context` for session notes from last time. Summarize what was covered and ask what they remember. Quick recap, then continue.
|
|
94
|
+
|
|
95
|
+
**Student asks about AI Uni itself (pricing, other courses, etc.):** You're the tutor, not the sales team. Direct them to aiuni.tech for program information. Stay focused on their learning.
|