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.
Files changed (151) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +83 -83
  3. package/.next/app-path-routes-manifest.json +6 -6
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/react-loadable-manifest.json +1 -1
  7. package/.next/server/app/_not-found/page.js +2 -2
  8. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/agents/[agentId]/page.js +2 -2
  10. package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/agents/[agentId]/respond/route.js +1 -1
  12. package/.next/server/app/api/agents/[agentId]/respond/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/images/[rootId]/[file]/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/oauth/callback/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/oauth/start/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/roots/[id]/attachments/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js +1 -1
  18. package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js +1 -1
  20. package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js +2 -2
  22. package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/roots/[id]/dashboard/route.js +1 -1
  24. package/.next/server/app/api/roots/[id]/dashboard/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/roots/[id]/suggestions/route.js +1 -1
  26. package/.next/server/app/api/roots/[id]/suggestions/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/utilities/[scope]/[id]/bundle.js/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/utilities/[scope]/[id]/host/route.js +1 -1
  29. package/.next/server/app/api/utilities/[scope]/[id]/host/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/utilities/[scope]/[id]/host-api.mjs/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/utilities/[scope]/[id]/host-ui.mjs/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/utilities/[scope]/[id]/iframe/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/utilities/[scope]/[id]/style.css/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/audit/page.js +1 -1
  35. package/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/server/app/onboarding/page.js +3 -3
  38. package/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/page.js +2 -2
  40. package/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/roots/[id]/chat/[topicId]/page.js +2 -2
  43. package/.next/server/app/roots/[id]/chat/[topicId]/page.js.nft.json +1 -1
  44. package/.next/server/app/roots/[id]/chat/[topicId]/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app/roots/[id]/kb/[...slug]/page.js +2 -2
  46. package/.next/server/app/roots/[id]/kb/[...slug]/page.js.nft.json +1 -1
  47. package/.next/server/app/roots/[id]/kb/[...slug]/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/roots/[id]/page.js +3 -3
  49. package/.next/server/app/roots/[id]/page.js.nft.json +1 -1
  50. package/.next/server/app/roots/[id]/page_client-reference-manifest.js +1 -1
  51. package/.next/server/app/roots/[id]/workflows/[wfId]/page.js +2 -2
  52. package/.next/server/app/roots/[id]/workflows/[wfId]/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/roots/[id]/workflows/page.js +1 -1
  54. package/.next/server/app/roots/[id]/workflows/page.js.nft.json +1 -1
  55. package/.next/server/app/roots/[id]/workflows/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/roots/new/page.js +4 -4
  57. package/.next/server/app/roots/new/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/settings/page.js +4 -4
  59. package/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/server/app/share/[id]/file/page.js +2 -2
  62. package/.next/server/app/share/[id]/file/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/share/[id]/page.js +2 -2
  64. package/.next/server/app/share/[id]/page.js.nft.json +1 -1
  65. package/.next/server/app/share/[id]/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/utilities/[scope]/[id]/page.js +2 -2
  67. package/.next/server/app/utilities/[scope]/[id]/page.js.nft.json +1 -1
  68. package/.next/server/app/utilities/[scope]/[id]/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/utilities/page.js +2 -2
  70. package/.next/server/app/utilities/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app-paths-manifest.json +6 -6
  72. package/.next/server/chunks/157.js +1 -0
  73. package/.next/server/chunks/285.js +2 -2
  74. package/.next/server/chunks/2995.js +1 -1
  75. package/.next/server/chunks/3332.js +1 -1
  76. package/.next/server/chunks/4812.js +1 -1
  77. package/.next/server/chunks/4925.js +1 -1
  78. package/.next/server/chunks/503.js +1 -0
  79. package/.next/server/chunks/5992.js +1 -0
  80. package/.next/server/chunks/6307.js +1 -0
  81. package/.next/server/chunks/{3512.js → 7908.js} +2 -2
  82. package/.next/server/chunks/8587.js +3 -0
  83. package/.next/server/chunks/9098.js +1 -1
  84. package/.next/server/functions-config-manifest.json +4 -4
  85. package/.next/server/middleware-build-manifest.js +1 -1
  86. package/.next/server/middleware-manifest.json +5 -5
  87. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  88. package/.next/server/pages/500.html +1 -1
  89. package/.next/server/server-reference-manifest.js +1 -1
  90. package/.next/server/server-reference-manifest.json +1 -1
  91. package/.next/static/chunks/1117-6fde4a3e0fe0443a.js +1 -0
  92. package/.next/static/chunks/1479-1d103a2aa91aa824.js +1 -0
  93. package/.next/static/chunks/1592-67762f29bd458262.js +1 -0
  94. package/.next/static/chunks/2052-31a610cee521cd24.js +1 -0
  95. package/.next/static/chunks/4108.e6f31641845cd071.js +1 -0
  96. package/.next/static/chunks/8322-71eceae9d2259389.js +1 -0
  97. package/.next/static/chunks/app/layout-b254fa87e3c3025e.js +1 -0
  98. package/.next/static/chunks/app/onboarding/page-a540d2a334e61279.js +1 -0
  99. package/.next/static/chunks/app/page-6127cec5577bbdea.js +1 -0
  100. package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-ce22f38ff1971ce7.js +1 -0
  101. package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-520df769ed074d58.js +1 -0
  102. package/.next/static/chunks/app/roots/[id]/page-667fac103c710695.js +1 -0
  103. package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-520200e8c167cb0f.js +1 -0
  104. package/.next/static/chunks/app/roots/new/page-72b4e06184ec3712.js +1 -0
  105. package/.next/static/chunks/app/settings/page-6a441614d14c707d.js +1 -0
  106. package/.next/static/chunks/app/share/[id]/page-10997d1668345672.js +1 -0
  107. package/.next/static/chunks/app/utilities/[scope]/[id]/page-4a33cee7cb9a7bf7.js +1 -0
  108. package/.next/static/chunks/app/utilities/page-df07d2ec05d7743a.js +1 -0
  109. package/.next/static/chunks/{webpack-2b0eab4ccdf44f63.js → webpack-ff7ea73bc08ce1d7.js} +1 -1
  110. package/.next/static/css/60e9b6cdf1283e83.css +1 -0
  111. package/.next/trace +47 -47
  112. package/package.json +1 -2
  113. package/.next/server/chunks/1.js +0 -3
  114. package/.next/server/chunks/2192.js +0 -1
  115. package/.next/server/chunks/6734.js +0 -1
  116. package/.next/server/chunks/7215.js +0 -1
  117. package/.next/server/chunks/9944.js +0 -1
  118. package/.next/static/chunks/1082-326e649fb24d4945.js +0 -1
  119. package/.next/static/chunks/3736-f4e42d6d38be50b0.js +0 -1
  120. package/.next/static/chunks/4108.ca0bdf3cbf3c56cc.js +0 -1
  121. package/.next/static/chunks/7482-7ef26030a10ce14f.js +0 -1
  122. package/.next/static/chunks/8944-c4f2406ecd61094f.js +0 -1
  123. package/.next/static/chunks/9415-eb6b5d4c2de3a7c0.js +0 -1
  124. package/.next/static/chunks/app/layout-85eb1fd21dab0895.js +0 -1
  125. package/.next/static/chunks/app/onboarding/page-2013bd8124b9162e.js +0 -1
  126. package/.next/static/chunks/app/page-558a224e13ffb52c.js +0 -1
  127. package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-b42f03fd58669d12.js +0 -1
  128. package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-7d17b4e6a5231f56.js +0 -1
  129. package/.next/static/chunks/app/roots/[id]/page-4aab5266f432e37e.js +0 -1
  130. package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-1ee3320bf5744efc.js +0 -1
  131. package/.next/static/chunks/app/roots/new/page-df8d2c1f0c64c37a.js +0 -1
  132. package/.next/static/chunks/app/settings/page-fdba798d9e243ad3.js +0 -1
  133. package/.next/static/chunks/app/share/[id]/page-818a451d05e08d26.js +0 -1
  134. package/.next/static/chunks/app/utilities/[scope]/[id]/page-2cee09cc2ab9b5e8.js +0 -1
  135. package/.next/static/chunks/app/utilities/page-44a51522b347f13e.js +0 -1
  136. package/.next/static/css/4b367c1d0fa99b78.css +0 -1
  137. package/packages/utilities/learn-anything/README.md +0 -41
  138. package/packages/utilities/learn-anything/actions/_json.ts +0 -191
  139. package/packages/utilities/learn-anything/actions/_store.ts +0 -248
  140. package/packages/utilities/learn-anything/actions/buildModule.ts +0 -488
  141. package/packages/utilities/learn-anything/actions/explainSelection.ts +0 -65
  142. package/packages/utilities/learn-anything/actions/generateOutline.ts +0 -170
  143. package/packages/utilities/learn-anything/actions/generateQuiz.ts +0 -72
  144. package/packages/utilities/learn-anything/actions/generateTrainer.ts +0 -106
  145. package/packages/utilities/learn-anything/actions/refreshCourseCard.ts +0 -76
  146. package/packages/utilities/learn-anything/actions/tutorAsk.ts +0 -93
  147. package/packages/utilities/learn-anything/article-view.tsx +0 -464
  148. package/packages/utilities/learn-anything/manifest.json +0 -42
  149. package/packages/utilities/learn-anything/ui.tsx +0 -1589
  150. /package/.next/static/{fhVNqfmJl5Mdfhyhg6orp → z5pbSy6TRHko5NGqhD4cn}/_buildManifest.js +0 -0
  151. /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
- }