reflex-agent 0.3.0 → 0.3.3
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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +83 -83
- package/.next/app-path-routes-manifest.json +6 -6
- package/.next/build-manifest.json +5 -5
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js +2 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/agents/[agentId]/page.js +2 -2
- package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/[agentId]/respond/route.js +1 -1
- package/.next/server/app/api/agents/[agentId]/respond/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/images/[rootId]/[file]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/oauth/callback/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/oauth/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/attachments/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js +2 -2
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/dashboard/route.js +1 -1
- package/.next/server/app/api/roots/[id]/dashboard/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/suggestions/route.js +1 -1
- package/.next/server/app/api/roots/[id]/suggestions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/bundle.js/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host/route.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host-api.mjs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host-ui.mjs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/iframe/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/style.css/route_client-reference-manifest.js +1 -1
- package/.next/server/app/audit/page.js +1 -1
- package/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/server/app/onboarding/page.js +3 -3
- package/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/chat/[topicId]/page.js +2 -2
- package/.next/server/app/roots/[id]/chat/[topicId]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/chat/[topicId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/kb/[...slug]/page.js +2 -2
- package/.next/server/app/roots/[id]/kb/[...slug]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/kb/[...slug]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/page.js +3 -3
- package/.next/server/app/roots/[id]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/workflows/[wfId]/page.js +2 -2
- package/.next/server/app/roots/[id]/workflows/[wfId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/workflows/page.js +1 -1
- package/.next/server/app/roots/[id]/workflows/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/workflows/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/new/page.js +4 -4
- package/.next/server/app/roots/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page.js +4 -4
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/share/[id]/file/page.js +2 -2
- package/.next/server/app/share/[id]/file/page_client-reference-manifest.js +1 -1
- package/.next/server/app/share/[id]/page.js +2 -2
- package/.next/server/app/share/[id]/page.js.nft.json +1 -1
- package/.next/server/app/share/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/utilities/[scope]/[id]/page.js +2 -2
- package/.next/server/app/utilities/[scope]/[id]/page.js.nft.json +1 -1
- package/.next/server/app/utilities/[scope]/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/utilities/page.js +2 -2
- package/.next/server/app/utilities/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -6
- package/.next/server/chunks/157.js +1 -0
- package/.next/server/chunks/285.js +2 -2
- package/.next/server/chunks/2995.js +1 -1
- package/.next/server/chunks/3332.js +1 -1
- package/.next/server/chunks/4812.js +1 -1
- package/.next/server/chunks/4925.js +1 -1
- package/.next/server/chunks/503.js +1 -0
- package/.next/server/chunks/5992.js +1 -0
- package/.next/server/chunks/6307.js +1 -0
- package/.next/server/chunks/{3512.js → 7908.js} +2 -2
- package/.next/server/chunks/8587.js +3 -0
- package/.next/server/chunks/9098.js +1 -1
- package/.next/server/functions-config-manifest.json +4 -4
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/1117-6fde4a3e0fe0443a.js +1 -0
- package/.next/static/chunks/1479-1d103a2aa91aa824.js +1 -0
- package/.next/static/chunks/1592-67762f29bd458262.js +1 -0
- package/.next/static/chunks/2052-31a610cee521cd24.js +1 -0
- package/.next/static/chunks/4108.e6f31641845cd071.js +1 -0
- package/.next/static/chunks/8322-71eceae9d2259389.js +1 -0
- package/.next/static/chunks/app/layout-b254fa87e3c3025e.js +1 -0
- package/.next/static/chunks/app/onboarding/page-a540d2a334e61279.js +1 -0
- package/.next/static/chunks/app/page-6127cec5577bbdea.js +1 -0
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-ce22f38ff1971ce7.js +1 -0
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-520df769ed074d58.js +1 -0
- package/.next/static/chunks/app/roots/[id]/page-667fac103c710695.js +1 -0
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-520200e8c167cb0f.js +1 -0
- package/.next/static/chunks/app/roots/new/page-72b4e06184ec3712.js +1 -0
- package/.next/static/chunks/app/settings/page-6a441614d14c707d.js +1 -0
- package/.next/static/chunks/app/share/[id]/page-10997d1668345672.js +1 -0
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-4a33cee7cb9a7bf7.js +1 -0
- package/.next/static/chunks/app/utilities/page-df07d2ec05d7743a.js +1 -0
- package/.next/static/chunks/{webpack-2b0eab4ccdf44f63.js → webpack-ff7ea73bc08ce1d7.js} +1 -1
- package/.next/static/css/60e9b6cdf1283e83.css +1 -0
- package/.next/trace +47 -47
- package/package.json +1 -2
- package/.next/server/chunks/1.js +0 -3
- package/.next/server/chunks/2192.js +0 -1
- package/.next/server/chunks/6734.js +0 -1
- package/.next/server/chunks/7215.js +0 -1
- package/.next/server/chunks/9944.js +0 -1
- package/.next/static/chunks/1082-326e649fb24d4945.js +0 -1
- package/.next/static/chunks/3736-f4e42d6d38be50b0.js +0 -1
- package/.next/static/chunks/4108.ca0bdf3cbf3c56cc.js +0 -1
- package/.next/static/chunks/7482-7ef26030a10ce14f.js +0 -1
- package/.next/static/chunks/8944-c4f2406ecd61094f.js +0 -1
- package/.next/static/chunks/9415-eb6b5d4c2de3a7c0.js +0 -1
- package/.next/static/chunks/app/layout-85eb1fd21dab0895.js +0 -1
- package/.next/static/chunks/app/onboarding/page-2013bd8124b9162e.js +0 -1
- package/.next/static/chunks/app/page-558a224e13ffb52c.js +0 -1
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-b42f03fd58669d12.js +0 -1
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-7d17b4e6a5231f56.js +0 -1
- package/.next/static/chunks/app/roots/[id]/page-4aab5266f432e37e.js +0 -1
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-1ee3320bf5744efc.js +0 -1
- package/.next/static/chunks/app/roots/new/page-df8d2c1f0c64c37a.js +0 -1
- package/.next/static/chunks/app/settings/page-fdba798d9e243ad3.js +0 -1
- package/.next/static/chunks/app/share/[id]/page-818a451d05e08d26.js +0 -1
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-2cee09cc2ab9b5e8.js +0 -1
- package/.next/static/chunks/app/utilities/page-44a51522b347f13e.js +0 -1
- package/.next/static/css/4b367c1d0fa99b78.css +0 -1
- package/packages/utilities/learn-anything/README.md +0 -41
- package/packages/utilities/learn-anything/actions/_json.ts +0 -191
- package/packages/utilities/learn-anything/actions/_store.ts +0 -248
- package/packages/utilities/learn-anything/actions/buildModule.ts +0 -488
- package/packages/utilities/learn-anything/actions/explainSelection.ts +0 -65
- package/packages/utilities/learn-anything/actions/generateOutline.ts +0 -170
- package/packages/utilities/learn-anything/actions/generateQuiz.ts +0 -72
- package/packages/utilities/learn-anything/actions/generateTrainer.ts +0 -106
- package/packages/utilities/learn-anything/actions/refreshCourseCard.ts +0 -76
- package/packages/utilities/learn-anything/actions/tutorAsk.ts +0 -93
- package/packages/utilities/learn-anything/article-view.tsx +0 -464
- package/packages/utilities/learn-anything/manifest.json +0 -42
- package/packages/utilities/learn-anything/ui.tsx +0 -1589
- /package/.next/static/{fhVNqfmJl5Mdfhyhg6orp → z5pbSy6TRHko5NGqhD4cn}/_buildManifest.js +0 -0
- /package/.next/static/{fhVNqfmJl5Mdfhyhg6orp → z5pbSy6TRHko5NGqhD4cn}/_ssgManifest.js +0 -0
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { reflex } from "@host/api";
|
|
2
|
-
import type { TutorQA } from "./tutorAsk";
|
|
3
|
-
import { callJsonAgent, snippet } from "./_json";
|
|
4
|
-
import { writeCourse } from "./_store";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Build a course outline based on topic + wizard answers. Returns a
|
|
8
|
-
* list of 5-9 modules each with a short title, a 1-sentence objective,
|
|
9
|
-
* and an estimated duration. The course record is persisted as a KB
|
|
10
|
-
* entry of kind="course"; subsequent buildModule calls hang content
|
|
11
|
-
* off it module-by-module.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface OutlineModule {
|
|
15
|
-
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
objective: string;
|
|
18
|
-
estMinutes: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface GenerateOutlineArgs {
|
|
22
|
-
topic: string;
|
|
23
|
-
history: TutorQA[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CourseRecord {
|
|
27
|
-
courseId: string;
|
|
28
|
-
topic: string;
|
|
29
|
-
modules: OutlineModule[];
|
|
30
|
-
relPath: string;
|
|
31
|
-
createdAt: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export default async function generateOutline(
|
|
35
|
-
args: GenerateOutlineArgs,
|
|
36
|
-
): Promise<CourseRecord> {
|
|
37
|
-
const prior = args.history
|
|
38
|
-
.map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`)
|
|
39
|
-
.join("\n\n");
|
|
40
|
-
const prompt = [
|
|
41
|
-
"Design a learning course on the topic \"${TOPIC}\" tailored to a user with the following answers:".replace(
|
|
42
|
-
"${TOPIC}",
|
|
43
|
-
args.topic,
|
|
44
|
-
),
|
|
45
|
-
"",
|
|
46
|
-
prior || "(no answers)",
|
|
47
|
-
"",
|
|
48
|
-
"Requirements:",
|
|
49
|
-
" • 5-9 modules, from basic to advanced.",
|
|
50
|
-
" • Each module is self-contained (can be opened and completed in one sitting).",
|
|
51
|
-
" • objective — one precise phrase \"by the end of this module you will be able to …\".",
|
|
52
|
-
" • estMinutes — a realistic estimate of pure study time (15-60).",
|
|
53
|
-
" • module id — kebab-case, latin letters + digits.",
|
|
54
|
-
"Reply with JSON ONLY on a single line, no markdown:",
|
|
55
|
-
` {"modules":[{"id":"intro","title":"…","objective":"…","estMinutes":30}, ...]}`,
|
|
56
|
-
].join("\n");
|
|
57
|
-
|
|
58
|
-
const result = await callJsonAgent<OutlineModule[]>({
|
|
59
|
-
prompt,
|
|
60
|
-
invoke: (p) => reflex.agent.invoke({ prompt: p, timeoutMs: 4 * 60_000 }),
|
|
61
|
-
maxAttempts: 4,
|
|
62
|
-
shapeHint:
|
|
63
|
-
`{"modules":[{"id":"intro","title":"Introduction","objective":"by the end of this module you will be able to …","estMinutes":30}, ...]}\n` +
|
|
64
|
-
`At least 5 modules. id — kebab-case, no spaces, latin letters only.`,
|
|
65
|
-
validate: (parsed) => {
|
|
66
|
-
const v = parsed as { modules?: unknown };
|
|
67
|
-
const modules = sanitizeModules(v?.modules);
|
|
68
|
-
return modules.length > 0 ? modules : null;
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (!result.ok) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`Failed to build the course outline in ${result.attempts} attempts (${result.reason}). ` +
|
|
75
|
-
`Last agent reply: "${snippet(result.lastText, 200)}". Try rephrasing the topic.`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
const modules = result.value;
|
|
79
|
-
|
|
80
|
-
const courseId = slugify(args.topic) || `course-${Date.now()}`;
|
|
81
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
82
|
-
const body = [
|
|
83
|
-
`# ${args.topic}`,
|
|
84
|
-
"",
|
|
85
|
-
"## Program",
|
|
86
|
-
"",
|
|
87
|
-
...modules.map(
|
|
88
|
-
(mod, i) =>
|
|
89
|
-
`${i + 1}. **${mod.title}** — ${mod.objective} (~${mod.estMinutes} min)`,
|
|
90
|
-
),
|
|
91
|
-
].join("\n");
|
|
92
|
-
|
|
93
|
-
const nowIso = new Date().toISOString();
|
|
94
|
-
// Source of truth: sandboxed JSON. Lookup via fs.list — no YAML
|
|
95
|
-
// round-tripping headaches.
|
|
96
|
-
await writeCourse({
|
|
97
|
-
courseId,
|
|
98
|
-
topic: args.topic,
|
|
99
|
-
modules,
|
|
100
|
-
progress: {},
|
|
101
|
-
wizardAnswers: args.history,
|
|
102
|
-
createdAt: nowIso,
|
|
103
|
-
updatedAt: nowIso,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// KB entry is for human visibility (showing up in the KB tree with a
|
|
107
|
-
// "createdBy: utility" badge). Short meta only — no JSON-stringified
|
|
108
|
-
// blobs, so multi-line YAML never happens.
|
|
109
|
-
const saved = await reflex.kb.add({
|
|
110
|
-
kind: "course",
|
|
111
|
-
title: args.topic,
|
|
112
|
-
body,
|
|
113
|
-
meta: {
|
|
114
|
-
courseId,
|
|
115
|
-
topic: args.topic,
|
|
116
|
-
modulesCount: modules.length,
|
|
117
|
-
createdAt: today,
|
|
118
|
-
},
|
|
119
|
-
slug: courseId,
|
|
120
|
-
date: today,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
await reflex.audit.log({
|
|
124
|
-
type: "course-outlined",
|
|
125
|
-
payload: { topic: args.topic, modulesCount: modules.length },
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
courseId,
|
|
130
|
-
topic: args.topic,
|
|
131
|
-
modules,
|
|
132
|
-
relPath: saved.relPath,
|
|
133
|
-
createdAt: today,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function sanitizeModules(raw: unknown): OutlineModule[] {
|
|
138
|
-
if (!Array.isArray(raw)) return [];
|
|
139
|
-
const seen = new Set<string>();
|
|
140
|
-
const out: OutlineModule[] = [];
|
|
141
|
-
for (const m of raw) {
|
|
142
|
-
if (typeof m !== "object" || m === null) continue;
|
|
143
|
-
const o = m as Partial<OutlineModule>;
|
|
144
|
-
const id =
|
|
145
|
-
typeof o.id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(o.id)
|
|
146
|
-
? o.id
|
|
147
|
-
: slugify(typeof o.title === "string" ? o.title : "");
|
|
148
|
-
if (!id || seen.has(id)) continue;
|
|
149
|
-
seen.add(id);
|
|
150
|
-
out.push({
|
|
151
|
-
id,
|
|
152
|
-
title: typeof o.title === "string" ? o.title : id,
|
|
153
|
-
objective: typeof o.objective === "string" ? o.objective : "",
|
|
154
|
-
estMinutes:
|
|
155
|
-
typeof o.estMinutes === "number" && Number.isFinite(o.estMinutes)
|
|
156
|
-
? Math.max(5, Math.min(180, Math.round(o.estMinutes)))
|
|
157
|
-
: 30,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function slugify(s: string): string {
|
|
164
|
-
return s
|
|
165
|
-
.normalize("NFKD")
|
|
166
|
-
.toLowerCase()
|
|
167
|
-
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
168
|
-
.replace(/^-+|-+$/g, "")
|
|
169
|
-
.slice(0, 60);
|
|
170
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { reflex } from "@host/api";
|
|
2
|
-
import { extractJson } from "./_json";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generate a 5-question multiple-choice quiz for a module. Each item:
|
|
6
|
-
* stem (question), 4 options, one correct index, explanation. Quiz
|
|
7
|
-
* isn't persisted as a separate KB entry — it's transient state the UI
|
|
8
|
-
* holds while the user takes it; the score lands in course.meta.progress
|
|
9
|
-
* after completion.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface QuizQuestion {
|
|
13
|
-
stem: string;
|
|
14
|
-
options: string[];
|
|
15
|
-
correctIndex: number;
|
|
16
|
-
explanation: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface GenerateQuizArgs {
|
|
20
|
-
moduleTitle: string;
|
|
21
|
-
moduleObjective: string;
|
|
22
|
-
/** Article body — gives the LLM something to draw questions from. */
|
|
23
|
-
article: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default async function generateQuiz(
|
|
27
|
-
args: GenerateQuizArgs,
|
|
28
|
-
): Promise<{ questions: QuizQuestion[] }> {
|
|
29
|
-
const trimmed = (args.article ?? "").slice(0, 6000);
|
|
30
|
-
const prompt = [
|
|
31
|
-
`Module: "${args.moduleTitle}" — ${args.moduleObjective}.`,
|
|
32
|
-
"Compose a short knowledge-check quiz of 5 questions with 4 answer options each.",
|
|
33
|
-
"Rules:",
|
|
34
|
-
" • Questions test understanding, not rote memorisation of trivia.",
|
|
35
|
-
" • Exactly one correct answer; the others are plausible.",
|
|
36
|
-
" • Explanation of why the answer is correct — 1-2 sentences.",
|
|
37
|
-
"Reply with JSON ONLY on a single line:",
|
|
38
|
-
` {"questions":[{"stem":"...","options":["a","b","c","d"],"correctIndex":0,"explanation":"..."}, ...]}`,
|
|
39
|
-
"",
|
|
40
|
-
`## Module material\n${trimmed}`,
|
|
41
|
-
].join("\n");
|
|
42
|
-
|
|
43
|
-
const r = await reflex.llm.complete({ task: "quick", prompt });
|
|
44
|
-
const parsed = extractJson<{ questions?: unknown[] }>(r.text);
|
|
45
|
-
if (!parsed) return { questions: [] };
|
|
46
|
-
try {
|
|
47
|
-
const out: QuizQuestion[] = [];
|
|
48
|
-
for (const q of parsed.questions ?? []) {
|
|
49
|
-
if (typeof q !== "object" || q === null) continue;
|
|
50
|
-
const o = q as Partial<QuizQuestion>;
|
|
51
|
-
if (
|
|
52
|
-
typeof o.stem !== "string" ||
|
|
53
|
-
!Array.isArray(o.options) ||
|
|
54
|
-
o.options.length < 2 ||
|
|
55
|
-
typeof o.correctIndex !== "number" ||
|
|
56
|
-
o.correctIndex < 0 ||
|
|
57
|
-
o.correctIndex >= o.options.length
|
|
58
|
-
) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
out.push({
|
|
62
|
-
stem: o.stem,
|
|
63
|
-
options: o.options.map((x) => String(x)).slice(0, 6),
|
|
64
|
-
correctIndex: o.correctIndex,
|
|
65
|
-
explanation: typeof o.explanation === "string" ? o.explanation : "",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return { questions: out.slice(0, 8) };
|
|
69
|
-
} catch {
|
|
70
|
-
return { questions: [] };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { reflex } from "@host/api";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Ask the agent to write an interactive trainer (small standalone HTML
|
|
5
|
-
* page with inline JS) that practices a specific skill from a module —
|
|
6
|
-
* e.g. simulation of a binary tree, drag-shapes-on-canvas exercise,
|
|
7
|
-
* flashcard drill. The HTML runs inside a nested sandboxed iframe in
|
|
8
|
-
* the utility UI; it has NO host-RPC access — strictly client-only.
|
|
9
|
-
*
|
|
10
|
-
* Persisted as KB kind="course-trainer" so it survives between sessions
|
|
11
|
-
* and the user can re-open.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface TrainerSpec {
|
|
15
|
-
trainerId: string;
|
|
16
|
-
courseId: string;
|
|
17
|
-
moduleId: string;
|
|
18
|
-
title: string;
|
|
19
|
-
/** Self-contained HTML — head/body/script all inline. Loaded via
|
|
20
|
-
* iframe srcdoc inside the utility. */
|
|
21
|
-
html: string;
|
|
22
|
-
relPath: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface GenerateTrainerArgs {
|
|
26
|
-
courseId: string;
|
|
27
|
-
moduleId: string;
|
|
28
|
-
moduleTitle: string;
|
|
29
|
-
moduleObjective: string;
|
|
30
|
-
prompt?: string; // user idea
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default async function generateTrainer(
|
|
34
|
-
args: GenerateTrainerArgs,
|
|
35
|
-
): Promise<TrainerSpec> {
|
|
36
|
-
const userBrief =
|
|
37
|
-
(args.prompt ?? "").trim() ||
|
|
38
|
-
`Design a trainer that helps reinforce "${args.moduleObjective}".`;
|
|
39
|
-
const prompt = [
|
|
40
|
-
`Course/module: "${args.moduleTitle}".`,
|
|
41
|
-
"Build an interactive trainer — a standalone HTML file with inline JS.",
|
|
42
|
-
"Requirements:",
|
|
43
|
-
" • One full <!doctype html> file: <head>, <body>, <script>, optional <style>.",
|
|
44
|
-
" • NO external resources: no CDN scripts, no image URLs, no fetch calls. Everything inline.",
|
|
45
|
-
" • Use <canvas> where appropriate (drawing, physics, geometry).",
|
|
46
|
-
" • Size ≈ 600×400 px (responsive).",
|
|
47
|
-
" • Clear interface: what is shown, what to do, instant feedback (right/wrong, score).",
|
|
48
|
-
" • No navigator/window globals that break inside a sandbox iframe (no localStorage, no parent).",
|
|
49
|
-
" • THE CODE MUST WORK. No placeholder functions.",
|
|
50
|
-
"Reply with HTML ONLY (no JSON wrapper, no markdown fence).",
|
|
51
|
-
"",
|
|
52
|
-
`## Trainer idea\n${userBrief}`,
|
|
53
|
-
].join("\n");
|
|
54
|
-
|
|
55
|
-
// Trainer HTML is long — give the agent a generous 7 minutes; worker
|
|
56
|
-
// timeout (8 min in manifest) is the outer guardrail.
|
|
57
|
-
const r = await reflex.agent.invoke({ prompt, timeoutMs: 7 * 60_000 });
|
|
58
|
-
const text = r.text ?? "";
|
|
59
|
-
const html = extractHtml(text);
|
|
60
|
-
if (!html) {
|
|
61
|
-
throw new Error("Agent did not return valid HTML — please try again");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const trainerId = `${args.moduleId}-${Date.now().toString(36)}`;
|
|
65
|
-
const saved = await reflex.kb.add({
|
|
66
|
-
kind: "course-trainer",
|
|
67
|
-
title: `Trainer · ${args.moduleTitle}`,
|
|
68
|
-
body: "```html\n" + html.slice(0, 30_000) + "\n```",
|
|
69
|
-
meta: {
|
|
70
|
-
courseId: args.courseId,
|
|
71
|
-
moduleId: args.moduleId,
|
|
72
|
-
trainerId,
|
|
73
|
-
title: args.moduleTitle,
|
|
74
|
-
// Full HTML in meta is bulky but allows quick re-open without re-parsing.
|
|
75
|
-
// Capped at ~60KB to keep frontmatter sane.
|
|
76
|
-
html: html.slice(0, 60_000),
|
|
77
|
-
},
|
|
78
|
-
slug: `trainer-${trainerId}`,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
trainerId,
|
|
83
|
-
courseId: args.courseId,
|
|
84
|
-
moduleId: args.moduleId,
|
|
85
|
-
title: args.moduleTitle,
|
|
86
|
-
html,
|
|
87
|
-
relPath: saved.relPath,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Pluck HTML from possibly-fenced agent output. */
|
|
92
|
-
function extractHtml(text: string): string {
|
|
93
|
-
// Fence-bound: ```html ... ```
|
|
94
|
-
const fenced = /```(?:html)?\s*([\s\S]+?)\s*```/.exec(text);
|
|
95
|
-
if (fenced) {
|
|
96
|
-
const inner = fenced[1]!.trim();
|
|
97
|
-
if (looksLikeHtml(inner)) return inner;
|
|
98
|
-
}
|
|
99
|
-
if (looksLikeHtml(text)) return text.trim();
|
|
100
|
-
return "";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function looksLikeHtml(s: string): boolean {
|
|
104
|
-
const lower = s.toLowerCase();
|
|
105
|
-
return lower.includes("<!doctype html") || lower.includes("<html");
|
|
106
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { reflex } from "@host/api";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Update the dashboard card: number of active courses + average
|
|
5
|
-
* progress %. Called after key events (course created, module
|
|
6
|
-
* completed, quiz passed). Idempotent.
|
|
7
|
-
*/
|
|
8
|
-
export default async function refreshCourseCard(): Promise<{ courses: number; avg: number }> {
|
|
9
|
-
const list = (await reflex.kb.list({ kind: "course" })) ?? [];
|
|
10
|
-
let total = 0;
|
|
11
|
-
let avgSum = 0;
|
|
12
|
-
for (const c of list) {
|
|
13
|
-
try {
|
|
14
|
-
const { content } = await reflex.kb.read({ relPath: c.relPath });
|
|
15
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
16
|
-
if (!m) continue;
|
|
17
|
-
const meta = parseFrontmatter(m[1]!);
|
|
18
|
-
const modules = jsonOf(meta.modules);
|
|
19
|
-
const progress = jsonOf(meta.progress);
|
|
20
|
-
if (!Array.isArray(modules) || modules.length === 0) continue;
|
|
21
|
-
total++;
|
|
22
|
-
const done = Object.values(
|
|
23
|
-
progress as Record<string, { completed?: boolean }>,
|
|
24
|
-
).filter((p) => p && (p as { completed?: boolean }).completed).length;
|
|
25
|
-
avgSum += done / modules.length;
|
|
26
|
-
} catch {
|
|
27
|
-
/* skip */
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const courses = total;
|
|
31
|
-
const avg = total > 0 ? Math.round((avgSum / total) * 100) : 0;
|
|
32
|
-
await reflex.cards.update({
|
|
33
|
-
snapshot: {
|
|
34
|
-
kind: "kpi",
|
|
35
|
-
title: "🎓 Learning",
|
|
36
|
-
data: {
|
|
37
|
-
items: [
|
|
38
|
-
{ label: "Active courses", value: String(courses) },
|
|
39
|
-
{
|
|
40
|
-
label: "Progress",
|
|
41
|
-
value: courses === 0 ? "—" : `${avg}%`,
|
|
42
|
-
hint: "average across courses",
|
|
43
|
-
...(avg >= 60
|
|
44
|
-
? ({ delta: "up" } as const)
|
|
45
|
-
: avg < 30 && courses > 0
|
|
46
|
-
? ({ delta: "down" } as const)
|
|
47
|
-
: ({ delta: "flat" } as const)),
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
return { courses, avg };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function jsonOf(v: unknown): unknown {
|
|
57
|
-
if (typeof v !== "string") return v;
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(v);
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseFrontmatter(s: string): Record<string, unknown> {
|
|
66
|
-
const out: Record<string, unknown> = {};
|
|
67
|
-
for (const line of s.split("\n")) {
|
|
68
|
-
const i = line.indexOf(":");
|
|
69
|
-
if (i < 0) continue;
|
|
70
|
-
out[line.slice(0, i).trim()] = line
|
|
71
|
-
.slice(i + 1)
|
|
72
|
-
.trim()
|
|
73
|
-
.replace(/^["']|["']$/g, "");
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { reflex } from "@host/api";
|
|
2
|
-
import { extractJson } from "./_json";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Wizard step. Given the topic + prior Q&A, the agent decides whether
|
|
6
|
-
* it needs more context or has enough to design a course. Returns
|
|
7
|
-
* either the next question (open-ended or multiple-choice) or
|
|
8
|
-
* `{done:true}` so the UI advances to outline generation.
|
|
9
|
-
*
|
|
10
|
-
* Keeping the wizard agent-driven means we don't hardcode "3 questions
|
|
11
|
-
* about level/focus/format" — for "I want to learn pencil drawing"
|
|
12
|
-
* the agent will ask different questions than for "I want to learn
|
|
13
|
-
* Python". The UI just renders whatever comes back.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export interface TutorQA {
|
|
17
|
-
question: string;
|
|
18
|
-
answer: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface TutorAskArgs {
|
|
22
|
-
topic: string;
|
|
23
|
-
history: TutorQA[];
|
|
24
|
-
/** Force-finish even if the agent wants more. UI uses this when the
|
|
25
|
-
* user clicks "Enough questions — build the course already". */
|
|
26
|
-
forceFinish?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TutorAskResult {
|
|
30
|
-
done: boolean;
|
|
31
|
-
/** Free-form question; UI renders a textarea. */
|
|
32
|
-
question?: string;
|
|
33
|
-
/** Optional pre-baked choices for a multiple-choice ask. */
|
|
34
|
-
choices?: string[];
|
|
35
|
-
/** Hint to UI: short label like "level", "format", "goal". */
|
|
36
|
-
header?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const MAX_TURNS = 5;
|
|
40
|
-
|
|
41
|
-
export default async function tutorAsk(
|
|
42
|
-
args: TutorAskArgs,
|
|
43
|
-
): Promise<TutorAskResult> {
|
|
44
|
-
const turns = args.history.length;
|
|
45
|
-
if (args.forceFinish || turns >= MAX_TURNS) {
|
|
46
|
-
return { done: true };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const prior = args.history
|
|
50
|
-
.map((qa, i) => `Q${i + 1}: ${qa.question}\nA${i + 1}: ${qa.answer}`)
|
|
51
|
-
.join("\n\n");
|
|
52
|
-
|
|
53
|
-
const prompt = [
|
|
54
|
-
"You are a tutor who is about to design a personalised course for the user.",
|
|
55
|
-
`Topic: "${args.topic}".`,
|
|
56
|
-
"You need to gather the minimum information required to choose a good program: level, goal, time budget, format, specifics.",
|
|
57
|
-
"Ask ONE question at a time. Decide what to ask yourself — whatever is most important right now given the topic and prior answers.",
|
|
58
|
-
"If it makes sense to offer answer options — add up to 5 short choices.",
|
|
59
|
-
"If you already know enough (usually after 2-4 questions) — return done=true.",
|
|
60
|
-
"Reply with JSON ONLY on a single line, no markdown:",
|
|
61
|
-
` {"done":false,"question":"...","header":"level|goal|time|format|...","choices":["...","..."]}`,
|
|
62
|
-
" or",
|
|
63
|
-
` {"done":true}`,
|
|
64
|
-
"",
|
|
65
|
-
prior ? `## Previous answers\n${prior}` : "## This is the first question",
|
|
66
|
-
`\nQuestions asked so far: ${turns} (maximum ${MAX_TURNS}).`,
|
|
67
|
-
].join("\n");
|
|
68
|
-
|
|
69
|
-
const r = await reflex.llm.complete({ task: "quick", prompt });
|
|
70
|
-
const parsed = extractJson<TutorAskResult>(r.text);
|
|
71
|
-
if (!parsed) return { done: true };
|
|
72
|
-
try {
|
|
73
|
-
if (parsed.done) return { done: true };
|
|
74
|
-
if (typeof parsed.question !== "string" || !parsed.question.trim()) {
|
|
75
|
-
return { done: true };
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
done: false,
|
|
79
|
-
question: parsed.question.trim(),
|
|
80
|
-
...(parsed.header ? { header: parsed.header } : {}),
|
|
81
|
-
...(Array.isArray(parsed.choices) && parsed.choices.length > 0
|
|
82
|
-
? {
|
|
83
|
-
choices: parsed.choices
|
|
84
|
-
.map((c) => String(c).trim())
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
.slice(0, 5),
|
|
87
|
-
}
|
|
88
|
-
: {}),
|
|
89
|
-
};
|
|
90
|
-
} catch {
|
|
91
|
-
return { done: true };
|
|
92
|
-
}
|
|
93
|
-
}
|