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,488 +0,0 @@
1
- import { reflex } from "@host/api";
2
- import { callJsonAgent, snippet } from "./_json";
3
-
4
- /**
5
- * Compile a learning module: agent writes a full markdown article, then
6
- * Reflex resolves the image needs (real photos via search → local
7
- * download, schematics via generation) and embeds permanent local URLs
8
- * into the body. Persisted as a `course-module` KB entry, idempotent on
9
- * (courseId, moduleId).
10
- *
11
- * Why image queries/prompts instead of raw URLs:
12
- * The old contract asked the LLM to suggest image URLs from its
13
- * training corpus. ~90% of those URLs were dead or hallucinated.
14
- * Now the LLM emits *intent* (what to find / what to draw) and
15
- * reflex.images.search + reflex.images.generate do the real work,
16
- * yielding stable `/api/images/<rootId>/<sha>.<ext>` URLs that move
17
- * with the project.
18
- */
19
-
20
- export interface ModuleContent {
21
- courseId: string;
22
- moduleId: string;
23
- title: string;
24
- /** Long-form markdown body for the article view. */
25
- article: string;
26
- videos: Array<{ title: string; url: string; note?: string }>;
27
- links: Array<{ title: string; url: string; snippet?: string }>;
28
- /** Resolved images: each has a permanent `/api/images/...` URL. */
29
- images: Array<{
30
- /** Stable id from the LLM draft; used to substitute inline `[[IMG:<id>]]` placeholders. */
31
- id: string;
32
- alt: string;
33
- url: string;
34
- source: "search" | "generated";
35
- attribution?: { name: string; link: string };
36
- }>;
37
- /** Mermaid diagrams (kept for structural schemas like flowcharts). */
38
- diagrams: Array<{ title: string; mermaid: string }>;
39
- homework: string[];
40
- relPath: string;
41
- }
42
-
43
- export interface BuildModuleArgs {
44
- courseId: string;
45
- moduleId: string;
46
- moduleTitle: string;
47
- moduleObjective: string;
48
- topic: string;
49
- }
50
-
51
- interface LlmDraft {
52
- article?: string;
53
- videos?: Array<{ title: string; url: string; note?: string }>;
54
- links?: Array<{ title: string; url: string; snippet?: string }>;
55
- /**
56
- * "Find a real photo of X". Each carries an `id` so the LLM can drop
57
- * an inline `[[IMG:<id>]]` placeholder into `article` and we know
58
- * where to embed the resolved image.
59
- */
60
- imageQueries?: Array<{ id: string; alt: string; query: string }>;
61
- /** Same id-based contract as `imageQueries`, but the resolver calls
62
- * reflex.images.generate instead of search. */
63
- generatedFigures?: Array<{ id: string; alt: string; prompt: string }>;
64
- /** Mermaid code for diagrams that genuinely need flow/sequence syntax. */
65
- diagrams?: Array<{ title: string; mermaid: string }>;
66
- homework?: string[];
67
- }
68
-
69
- export default async function buildModule(
70
- args: BuildModuleArgs,
71
- ): Promise<ModuleContent> {
72
- // 1. Pull a few web sources to ground the article — saves the agent from
73
- // hallucinating sources. Best-effort; failures are silent.
74
- let webContext = "";
75
- try {
76
- const search = await reflex.web.search({
77
- query: `${args.topic} ${args.moduleTitle}`,
78
- });
79
- const top = (search.results ?? []).slice(0, 4);
80
- webContext = top
81
- .map(
82
- (r, i) =>
83
- `[${i + 1}] ${r.title}\n ${r.url}\n ${r.snippet ?? ""}`,
84
- )
85
- .join("\n");
86
- } catch {
87
- /* offline / no search — agent will rely on training data */
88
- }
89
-
90
- // 2. Ask the agent to draft the module. Note: NO bare image URLs —
91
- // the LLM tells us WHAT to look for / draw; Reflex resolves it via
92
- // reflex.images.search (Brave when available) + reflex.images.generate.
93
- const prompt = [
94
- `Course: "${args.topic}". Module: "${args.moduleTitle}" — ${args.moduleObjective}.`,
95
- "Produce the learning material. JSON reply shape:",
96
- "{",
97
- ` "article": "long markdown 800-2000 words; use # ## ### and [[IMG:<id>]] placeholders for inline images",`,
98
- ` "videos": [{"title":"...","url":"https://youtube.com/...","note":"..."}],`,
99
- ` "links": [{"title":"...","url":"...","snippet":"..."}],`,
100
- ` "imageQueries": [{"id":"i1","alt":"...","query":"short English search query"}],`,
101
- ` "generatedFigures": [{"id":"f1","alt":"...","prompt":"detailed English description for the AI generator"}],`,
102
- ` "diagrams": [{"title":"...","mermaid":"graph TD; A-->B;"}],`,
103
- ` "homework": ["...","..."]`,
104
- "}",
105
- "",
106
- "## VISUAL CONTENT — MANDATORY + INLINE PLACEMENT",
107
- "",
108
- "Every learning module MUST be visually rich. Each image is placed **inline** at the right spot in the text via a placeholder.",
109
- "",
110
- "### How it works",
111
- " 1. Each entry in `imageQueries` and `generatedFigures` gets a SHORT ID (`i1`, `i2`, `f1`, `f2`, ...). i = image-search (real photo), f = figure (AI-generated). Unique within the module.",
112
- " 2. In the `article` markdown insert the placeholder `[[IMG:i1]]` on its OWN LINE exactly where the image should appear (e.g. after the paragraph that discusses it).",
113
- " 3. Reflex automatically:",
114
- " – searches for real photos/diagrams via Brave Image Search (using `imageQueries`),",
115
- " – VISUALLY EVALUATES candidates (your chat agent inspects thumbnails via the Read tool) — clipart / off-topic items are rejected based on content,",
116
- " – generates unique illustrations via Gemini Nano Banana (using `generatedFigures`),",
117
- " – replaces `[[IMG:<id>]]` with `![alt](local-url)` plus attribution,",
118
- " – unresolved ids (image not found / rejected) are cleanly stripped from the text.",
119
- "",
120
- "### Example",
121
- "```markdown",
122
- "## Visual cortex",
123
- "The primary visual cortex V1 is the first area that processes information from the retina.",
124
- "",
125
- "[[IMG:i1]]",
126
- "",
127
- "The columnar organisation of V1 was described by Hubel and Wiesel in 1962...",
128
- "",
129
- "[[IMG:f1]]",
130
- "```",
131
- "...where `imageQueries: [{id:\"i1\", alt:\"Section of the primary visual cortex V1\", query:\"primary visual cortex V1 histology\"}]` and `generatedFigures: [{id:\"f1\", alt:\"Receptive field diagram\", prompt:\"educational diagram: receptive field of simple cells in V1, ON-OFF regions, labeled in English\"}]`.",
132
- "",
133
- "Your job is to fill both arrays AND place the placeholders in `article`:",
134
- "",
135
- "### `imageQueries` (real-image search) — AT LEAST 2-3 items.",
136
- " • For topics with real-world references (Eiffel Tower, a cell, the Civil War, lab equipment, a famous painting, a landscape, a historical document) — ALWAYS add 2-4 queries.",
137
- " • Each `query` is a short ENGLISH search string (Brave works best in English): \"Eiffel Tower iron lattice closeup\", \"mitochondria electron microscope\", \"American Civil War Gettysburg battlefield\".",
138
- " • `id` — short unique identifier: `i1`, `i2`, `i3`...",
139
- " • `alt` — short description of what the viewer will see.",
140
- " • Each `id` corresponds to exactly one `[[IMG:i1]]` placeholder in `article` — put it in a thematically appropriate spot.",
141
- "",
142
- "### `generatedFigures` (AI-generated unique figures) — 1-2 items when appropriate.",
143
- " • Use for unique schemes/illustrations that aren't available online: \"process N as a clear schematic\", \"anatomy of X in textbook style\", \"event timeline\", \"abstract visualisation of a concept\".",
144
- " • `id` — short unique identifier: `f1`, `f2`...",
145
- " • `prompt` — detailed ENGLISH descriptive prompt including style (\"minimalist educational diagram, white background, labeled parts in blue, isometric view\" / \"watercolor illustration, soft palette\" / \"photorealistic, studio lighting\").",
146
- " • DO NOT duplicate generatedFigures with imageQueries — only generate what cannot be found ready-made.",
147
- " • `alt` — short description.",
148
- " • Place the `[[IMG:f1]]` placeholder in `article` exactly where the figure is needed.",
149
- "",
150
- "### Rules for the other fields",
151
- " • article — main text, 800-2000 words. Headings # ## ###, dense content.",
152
- " • Images are placed ONLY via `[[IMG:<id>]]` on its own line. Do not write `[[ILLUSTRATION: ...]]`, `[[DIAGRAM: ...]]` — they don't work.",
153
- " • DO NOT MAKE UP image URLs. Any bare URL in `article` is ignored.",
154
- " • Every id declared in imageQueries/generatedFigures must appear in `article` exactly once. Every `[[IMG:<id>]]` in the text must have a matching entry in one of the arrays.",
155
- " • videos: 1-3 youtube/youtu.be links — the user verifies URLs themselves.",
156
- " • links: 2-5 authoritative articles.",
157
- " • diagrams (mermaid): only flowchart/sequence/class — where mermaid is genuinely more convenient than an image. For visual schemes use generatedFigures.",
158
- " • homework: 3-5 practical exercises with a verifiable result.",
159
- "",
160
- "Reply with JSON ONLY on a single line, no markdown fences.",
161
- "",
162
- webContext
163
- ? `## Web sources to ground on\n${webContext}`
164
- : "## Web sources unavailable — rely on your own knowledge.",
165
- ].join("\n");
166
-
167
- const result = await callJsonAgent<LlmDraft>({
168
- prompt,
169
- invoke: (p) => reflex.agent.invoke({ prompt: p, timeoutMs: 7 * 60_000 }),
170
- maxAttempts: 4,
171
- shapeHint:
172
- `{"article":"...","videos":[],"links":[],"imageQueries":[],"generatedFigures":[],"diagrams":[],"homework":[]}\n` +
173
- `article — markdown 800-2000 words. Every list field must be an array (empty is OK).`,
174
- validate: (p) => {
175
- const v = p as LlmDraft;
176
- return typeof v?.article === "string" && v.article.trim().length > 40
177
- ? v
178
- : null;
179
- },
180
- });
181
- if (!result.ok) {
182
- throw new Error(
183
- `Failed to build the module in ${result.attempts} attempts (${result.reason}). ` +
184
- `Last reply: "${snippet(result.lastText, 200)}".`,
185
- );
186
- }
187
- const draft = result.value;
188
-
189
- // 3. Resolve image needs in parallel. Each call is best-effort —
190
- // a failed search or generation just drops that image from the module
191
- // (we don't want one flaky API to fail the whole build).
192
- const [searchedImages, generatedImages] = await Promise.all([
193
- resolveSearches(draft.imageQueries ?? [], args.topic, args.moduleTitle),
194
- resolveGenerations(draft.generatedFigures ?? []),
195
- ]);
196
-
197
- // Inline-place images by substituting [[IMG:<id>]] placeholders in the
198
- // article body. Any image whose id wasn't referenced (LLM forgot)
199
- // falls back into the trailing "Additional illustrations" section so
200
- // nothing gets dropped silently. Residual unknown [[...]] markers are
201
- // stripped.
202
- const allImages = [...searchedImages, ...generatedImages];
203
- const { article: articleWithImages, placedIds } = substituteImagePlaceholders(
204
- typeof draft.article === "string" ? draft.article : "",
205
- allImages,
206
- );
207
- const articleClean = stripPlaceholderMarkers(articleWithImages);
208
- const unplaced = allImages.filter((im) => !placedIds.has(im.id));
209
-
210
- const content: Omit<ModuleContent, "relPath"> = {
211
- courseId: args.courseId,
212
- moduleId: args.moduleId,
213
- title: args.moduleTitle,
214
- article: articleClean,
215
- videos: sanitizeArr(draft.videos, ["title", "url"]) as ModuleContent["videos"],
216
- links: sanitizeArr(draft.links, ["title", "url"]) as ModuleContent["links"],
217
- images: allImages,
218
- diagrams: sanitizeArr(draft.diagrams, ["title", "mermaid"]) as ModuleContent["diagrams"],
219
- homework: Array.isArray(draft.homework)
220
- ? draft.homework.map(String).filter(Boolean).slice(0, 8)
221
- : [],
222
- };
223
-
224
- // 4. Persist as a KB entry. Frontmatter holds the structured bits;
225
- // body = article with images already inlined + optional fallback
226
- // section for images the LLM declared but never referenced.
227
- const body = [
228
- content.article,
229
- unplaced.length > 0
230
- ? "\n\n## Additional illustrations\n" +
231
- unplaced
232
- .map((im) => `${renderInlineImage(im)}`)
233
- .join("\n\n")
234
- : "",
235
- content.diagrams.length > 0
236
- ? "\n\n## Diagrams\n" +
237
- content.diagrams
238
- .map(
239
- (d) =>
240
- `### ${d.title}\n\n\`\`\`mermaid\n${d.mermaid}\n\`\`\``,
241
- )
242
- .join("\n\n")
243
- : "",
244
- ]
245
- .filter(Boolean)
246
- .join("");
247
-
248
- const saved = await reflex.kb.add({
249
- kind: "course-module",
250
- title: `${args.topic} · ${args.moduleTitle}`,
251
- body,
252
- meta: {
253
- courseId: args.courseId,
254
- moduleId: args.moduleId,
255
- videos: JSON.stringify(content.videos),
256
- links: JSON.stringify(content.links),
257
- images: JSON.stringify(content.images),
258
- diagrams: JSON.stringify(content.diagrams),
259
- homework: JSON.stringify(content.homework),
260
- title: args.moduleTitle,
261
- objective: args.moduleObjective,
262
- },
263
- slug: `${args.courseId}-${args.moduleId}`,
264
- });
265
-
266
- await reflex.audit.log({
267
- type: "module-built",
268
- payload: {
269
- courseId: args.courseId,
270
- moduleId: args.moduleId,
271
- videos: content.videos.length,
272
- images: content.images.length,
273
- searched: searchedImages.length,
274
- generated: generatedImages.length,
275
- },
276
- });
277
-
278
- return { ...content, relPath: saved.relPath };
279
- }
280
-
281
- /**
282
- * For each query: do a web image search (5 candidates), ask the LLM
283
- * which best fits the course material, attach the chosen one. If the
284
- * LLM rejects all candidates (-1), the image slot is left empty — better
285
- * than embedding clip-art or off-topic stock photo. Try/catch isolates
286
- * per-image failures from killing the module build.
287
- */
288
- async function resolveSearches(
289
- queries: Array<{ id?: unknown; alt?: unknown; query?: unknown }>,
290
- topic: string,
291
- moduleTitle: string,
292
- ): Promise<ModuleContent["images"]> {
293
- const clean = queries
294
- .filter(
295
- (q) =>
296
- q &&
297
- typeof q.query === "string" &&
298
- q.query.trim().length > 0,
299
- )
300
- .slice(0, 4);
301
- const out = await Promise.all(
302
- clean.map(async (q, idx) => {
303
- try {
304
- const query = q.query as string;
305
- const alt = typeof q.alt === "string" ? q.alt : query;
306
- const id =
307
- typeof q.id === "string" && q.id.trim().length > 0
308
- ? q.id.trim()
309
- : `i${idx + 1}`;
310
- // Default provider: Brave for breadth (real web), falls back to
311
- // Unsplash/Pexels via service-router based on which key exists.
312
- const search = await reflex.images.search({ query, count: 5 });
313
- if (search.results.length === 0) return null;
314
- // Vision-based pick: Reflex spawns the user's chat harness
315
- // (Codex / Claude Code) on the candidate thumbnails and asks it
316
- // to choose. The agent uses its Read tool — both runtimes get
317
- // real vision content for image files, so off-topic results
318
- // (clipart, mislabelled photos) are filtered by content, not
319
- // metadata.
320
- const pick = await reflex.images.pickBest({
321
- query,
322
- alt,
323
- context: `${topic} → ${moduleTitle}`,
324
- candidates: search.results.map((r) => ({
325
- url: r.url,
326
- thumb: r.thumb,
327
- attribution: r.attribution,
328
- })),
329
- });
330
- if (pick.pickIndex < 0 || pick.pickIndex >= search.results.length) {
331
- void reflex.audit.log({
332
- type: "image-rejected",
333
- payload: { query, reason: pick.reason, via: pick.via },
334
- });
335
- return null;
336
- }
337
- const chosen = search.results[pick.pickIndex];
338
- const attached = await reflex.images.attach({
339
- sourceUrl: chosen.url,
340
- });
341
- return {
342
- id,
343
- alt,
344
- url: attached.url,
345
- source: "search" as const,
346
- attribution: chosen.attribution,
347
- };
348
- } catch (err) {
349
- // Log to audit; the module survives without this image.
350
- void reflex.audit.log({
351
- type: "image-search-failed",
352
- payload: {
353
- query: q.query,
354
- error: err instanceof Error ? err.message : String(err),
355
- },
356
- });
357
- return null;
358
- }
359
- }),
360
- );
361
- return out.filter((x): x is NonNullable<typeof x> => x !== null);
362
- }
363
-
364
- /**
365
- * Walk through the article body, find `[[IMG:<id>]]` placeholders, and
366
- * replace each with a markdown image reference using the resolved
367
- * image's URL + alt + attribution.
368
- *
369
- * Returns the rewritten article + the set of ids successfully placed.
370
- * Unknown placeholders (id has no matching resolved image) get stripped
371
- * — likely a generation/search failure that already logged to audit.
372
- *
373
- * Whitespace around inline placeholders is normalized so the image sits
374
- * as its own block (Markdown then renders `![](...)` as a paragraph).
375
- */
376
- function substituteImagePlaceholders(
377
- article: string,
378
- images: ModuleContent["images"],
379
- ): { article: string; placedIds: Set<string> } {
380
- const byId = new Map(images.map((im) => [im.id, im]));
381
- const placedIds = new Set<string>();
382
- // Accept variants: `[[IMG:i1]]`, `[[img:i1]]`, `[[IMG: i1 ]]`,
383
- // `[[IMG i1]]`, and even single-bracket `[IMG:i1]` from sloppy LLMs.
384
- const re = /\[\[?\s*IMG\s*[:\s]\s*([A-Za-z0-9_-]+)\s*\]\]?/gi;
385
- const replaced = article.replace(re, (_, rawId: string) => {
386
- const id = rawId.trim();
387
- const img = byId.get(id);
388
- if (!img) return "";
389
- placedIds.add(id);
390
- return `\n\n${renderInlineImage(img)}\n\n`;
391
- });
392
- return { article: replaced, placedIds };
393
- }
394
-
395
- function renderInlineImage(im: ModuleContent["images"][number]): string {
396
- const safeAlt = im.alt.replace(/[\[\]\n]/g, " ").slice(0, 200);
397
- const credit =
398
- im.source === "search" && im.attribution?.name
399
- ? `\n\n_Source: [${im.attribution.name}](${im.attribution.link || im.url})_`
400
- : im.source === "generated"
401
- ? `\n\n_Generated by AI_`
402
- : "";
403
- return `![${safeAlt}](${im.url})${credit}`;
404
- }
405
-
406
- /**
407
- * Strip residual `[[ILLUSTRATION: ...]]` / `[[DIAGRAM: ...]]` / orphan
408
- * `[[IMG:<unknown-id>]]` placeholders the LLM might emit despite the
409
- * prompt forbidding them OR after `substituteImagePlaceholders` failed
410
- * to find the id. Drops the marker, collapses surrounding whitespace.
411
- *
412
- * Matches several spelling variants for safety. Case-insensitive.
413
- */
414
- function stripPlaceholderMarkers(text: string): string {
415
- const re =
416
- /\[\[?\s*(?:ILLUSTRATION|SCHEME|IMAGE|IMG|DIAGRAM)\b[^\]]*?\]\]?/giu;
417
- let out = text.replace(re, "");
418
- // Clean up double blank lines + trailing whitespace the removal may
419
- // have left behind.
420
- out = out.replace(/[ \t]+\n/g, "\n");
421
- out = out.replace(/\n{3,}/g, "\n\n");
422
- return out.trim() + "\n";
423
- }
424
-
425
- async function resolveGenerations(
426
- figures: Array<{ id?: unknown; alt?: unknown; prompt?: unknown }>,
427
- ): Promise<ModuleContent["images"]> {
428
- const clean = figures
429
- .filter(
430
- (f) =>
431
- f &&
432
- typeof f.prompt === "string" &&
433
- f.prompt.trim().length > 0,
434
- )
435
- .slice(0, 2);
436
- const out = await Promise.all(
437
- clean.map(async (f, idx) => {
438
- try {
439
- const gen = await reflex.images.generate({
440
- prompt: f.prompt as string,
441
- aspectRatio: "16:9",
442
- });
443
- const id =
444
- typeof f.id === "string" && f.id.trim().length > 0
445
- ? f.id.trim()
446
- : `f${idx + 1}`;
447
- return {
448
- id,
449
- alt: typeof f.alt === "string" ? f.alt : (f.prompt as string).slice(0, 80),
450
- url: gen.url,
451
- source: "generated" as const,
452
- };
453
- } catch (err) {
454
- void reflex.audit.log({
455
- type: "image-generate-failed",
456
- payload: {
457
- prompt: f.prompt,
458
- error: err instanceof Error ? err.message : String(err),
459
- },
460
- });
461
- return null;
462
- }
463
- }),
464
- );
465
- return out.filter((x): x is NonNullable<typeof x> => x !== null);
466
- }
467
-
468
- function sanitizeArr(
469
- v: unknown,
470
- required: string[],
471
- ): Array<Record<string, string>> {
472
- if (!Array.isArray(v)) return [];
473
- const out: Array<Record<string, string>> = [];
474
- for (const item of v.slice(0, 12)) {
475
- if (typeof item !== "object" || item === null) continue;
476
- const o = item as Record<string, unknown>;
477
- const ok = required.every(
478
- (k) => typeof o[k] === "string" && (o[k] as string).trim() !== "",
479
- );
480
- if (!ok) continue;
481
- const row: Record<string, string> = {};
482
- for (const [k, val] of Object.entries(o)) {
483
- if (typeof val === "string") row[k] = val;
484
- }
485
- out.push(row);
486
- }
487
- return out;
488
- }
@@ -1,65 +0,0 @@
1
- import { reflex } from "@host/api";
2
-
3
- /**
4
- * "Explain this" feature: user highlights a snippet inside the module
5
- * article and asks for a deeper explanation. The agent gets the
6
- * surrounding paragraph as context so it understands what "this" means.
7
- *
8
- * Two modes:
9
- * 1. Default — `question` omitted, agent gives a generic 2-5 paragraph
10
- * breakdown of the selected fragment.
11
- * 2. Custom — `question` supplied (book-style margin annotation), agent
12
- * answers that specific question with the selection as the focal
13
- * point. Lets the reader say things like "what does N have to do
14
- * with this?" or "give me an example" instead of always getting the
15
- * same boilerplate.
16
- */
17
-
18
- export interface ExplainSelectionArgs {
19
- selection: string;
20
- /** ~400-1000 chars around the selection for context. */
21
- context: string;
22
- topic: string;
23
- moduleTitle: string;
24
- /** Optional user question about the selection. */
25
- question?: string;
26
- }
27
-
28
- export default async function explainSelection(
29
- args: ExplainSelectionArgs,
30
- ): Promise<{ text: string }> {
31
- const userQuestion = (args.question ?? "").trim();
32
- const promptLines: string[] = [
33
- `Course: "${args.topic}". Module: "${args.moduleTitle}".`,
34
- ];
35
-
36
- if (userQuestion) {
37
- promptLines.push(
38
- "The user highlighted a fragment and asked a specific question about that fragment.",
39
- "Answer exactly their question, grounded in the selection + surrounding context. 2-4 paragraphs, no fluff, to the point.",
40
- "Markdown without headings; short sentences, with an example when appropriate.",
41
- );
42
- } else {
43
- promptLines.push(
44
- "The user highlighted a fragment and is asking for a deeper explanation.",
45
- "Give a thorough explanation in 2-5 paragraphs: what it means, how it works,",
46
- "why it works that way, with a concrete example. No fluff. Markdown without headings.",
47
- );
48
- }
49
-
50
- promptLines.push(
51
- "",
52
- `## Surrounding context\n${args.context.slice(0, 1500)}`,
53
- "",
54
- `## User selection\n"${args.selection.slice(0, 800)}"`,
55
- );
56
- if (userQuestion) {
57
- promptLines.push("", `## User question\n${userQuestion.slice(0, 600)}`);
58
- }
59
-
60
- const r = await reflex.llm.complete({
61
- task: "quick",
62
- prompt: promptLines.join("\n"),
63
- });
64
- return { text: (r.text ?? "").trim() };
65
- }