reflex-agent 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +116 -103
- package/.next/app-path-routes-manifest.json +10 -10
- package/.next/build-manifest.json +5 -5
- package/.next/prerender-manifest.json +4 -54
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/agents/[agentId]/page.js +3 -3
- package/.next/server/app/agents/[agentId]/page.js.nft.json +1 -1
- package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/[agentId]/respond/route.js +2 -2
- package/.next/server/app/api/agents/[agentId]/respond/route.js.nft.json +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.js +4 -4
- 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.js +0 -0
- 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 +2 -2
- package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js.nft.json +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 +2 -2
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js.nft.json +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 +3 -3
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js.nft.json +1 -1
- 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.js.nft.json +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.js.nft.json +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 +2 -2
- package/.next/server/app/api/utilities/[scope]/[id]/host/route.js.nft.json +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 +2 -2
- 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.js.nft.json +1 -1
- 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 -6
- 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 -6
- 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.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/workflows/[wfId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/workflows/page.js +2 -2
- 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 +5 -3
- package/.next/server/app/roots/new/page.js.nft.json +1 -1
- package/.next/server/app/roots/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page.js +6 -6
- 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.js.nft.json +1 -1
- 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 -17
- package/.next/server/app/utilities/page.js.nft.json +1 -1
- package/.next/server/app/utilities/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -10
- package/.next/server/chunks/1.js +3 -0
- package/.next/server/chunks/1223.js +1 -1
- package/.next/server/chunks/133.js +1 -490
- package/.next/server/chunks/1888.js +1 -1
- package/.next/server/chunks/{9739.js → 1988.js} +13 -9
- package/.next/server/chunks/2192.js +1 -0
- package/.next/server/chunks/2433.js +1 -1
- package/.next/server/chunks/2503.js +1 -1
- package/.next/server/chunks/285.js +471 -0
- package/.next/server/chunks/2995.js +1 -0
- package/.next/server/chunks/3240.js +1 -1
- package/.next/server/chunks/3332.js +1 -1
- package/.next/server/chunks/{2528.js → 3512.js} +2 -2
- package/.next/server/chunks/3657.js +1 -1
- package/.next/server/chunks/4066.js +1 -1
- package/.next/server/chunks/4438.js +1 -0
- package/.next/server/chunks/4553.js +1 -1
- package/.next/server/chunks/4812.js +179 -0
- package/.next/server/chunks/4925.js +1 -1
- package/.next/server/chunks/5319.js +1 -1
- package/.next/server/chunks/569.js +1 -1
- package/.next/server/chunks/6730.js +1 -1
- package/.next/server/chunks/6734.js +1 -0
- package/.next/server/chunks/6909.js +142 -161
- package/.next/server/chunks/7215.js +1 -0
- package/.next/server/chunks/8262.js +1 -1
- package/.next/server/chunks/9098.js +1 -1
- package/.next/server/chunks/94.js +1 -1
- package/.next/server/chunks/9835.js +1 -1
- package/.next/server/chunks/9944.js +1 -0
- package/.next/server/functions-config-manifest.json +1 -1
- 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/pages-manifest.json +1 -2
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/1082-326e649fb24d4945.js +1 -0
- package/.next/static/chunks/3736-f4e42d6d38be50b0.js +1 -0
- package/.next/static/chunks/4108.ca0bdf3cbf3c56cc.js +1 -0
- package/.next/static/chunks/6445-99824866a51b582a.js +1 -0
- package/.next/static/chunks/7482-7ef26030a10ce14f.js +1 -0
- package/.next/static/chunks/8944-c4f2406ecd61094f.js +1 -0
- package/.next/static/chunks/9411-af5f758c57741929.js +3 -0
- package/.next/static/chunks/9415-eb6b5d4c2de3a7c0.js +1 -0
- package/.next/static/chunks/app/agents/[agentId]/page-5d6f4cb16b42d02b.js +1 -0
- package/.next/static/chunks/app/layout-85eb1fd21dab0895.js +1 -0
- package/.next/static/chunks/app/onboarding/page-2013bd8124b9162e.js +1 -0
- package/.next/static/chunks/app/page-558a224e13ffb52c.js +1 -0
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-b42f03fd58669d12.js +1 -0
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-7d17b4e6a5231f56.js +1 -0
- package/.next/static/chunks/app/roots/[id]/page-4aab5266f432e37e.js +1 -0
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-1ee3320bf5744efc.js +1 -0
- package/.next/static/chunks/app/roots/new/page-df8d2c1f0c64c37a.js +1 -0
- package/.next/static/chunks/app/settings/page-fdba798d9e243ad3.js +1 -0
- package/.next/static/chunks/app/share/[id]/page-818a451d05e08d26.js +1 -0
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-2cee09cc2ab9b5e8.js +1 -0
- package/.next/static/chunks/app/utilities/page-44a51522b347f13e.js +1 -0
- package/.next/static/chunks/{webpack-87b4bb79fdc48563.js → webpack-2b0eab4ccdf44f63.js} +1 -1
- package/.next/static/css/4b367c1d0fa99b78.css +1 -0
- package/.next/trace +47 -46
- package/README.md +4 -1
- package/dist/lib/reflex/agents/prompts.js +46 -46
- package/dist/lib/reflex/agents/prompts.js.map +1 -1
- package/dist/lib/reflex/prompts/defaults.js +102 -102
- package/next.config.ts +4 -1
- package/package.json +4 -3
- package/packages/utilities/learn-anything/README.md +29 -29
- package/packages/utilities/learn-anything/actions/_json.ts +11 -11
- package/packages/utilities/learn-anything/actions/_store.ts +2 -2
- package/packages/utilities/learn-anything/actions/buildModule.ts +60 -59
- package/packages/utilities/learn-anything/actions/explainSelection.ts +14 -13
- package/packages/utilities/learn-anything/actions/generateOutline.ts +15 -15
- package/packages/utilities/learn-anything/actions/generateQuiz.ts +8 -8
- package/packages/utilities/learn-anything/actions/generateTrainer.ts +15 -15
- package/packages/utilities/learn-anything/actions/refreshCourseCard.ts +4 -4
- package/packages/utilities/learn-anything/actions/tutorAsk.ts +15 -15
- package/packages/utilities/learn-anything/article-view.tsx +4 -4
- package/packages/utilities/learn-anything/manifest.json +5 -5
- package/packages/utilities/learn-anything/ui.tsx +57 -57
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -18
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -9
- package/.next/server/app/index.rsc +0 -19
- package/.next/server/chunks/1986.js +0 -1
- package/.next/server/chunks/4065.js +0 -1
- package/.next/server/chunks/6981.js +0 -1
- package/.next/server/chunks/7017.js +0 -3
- package/.next/server/chunks/7800.js +0 -1
- package/.next/server/chunks/8494.js +0 -1
- package/.next/server/chunks/8843.js +0 -1
- package/.next/server/chunks/9328.js +0 -179
- package/.next/server/chunks/9423.js +0 -3
- package/.next/server/chunks/9455.js +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/static/chunks/1384-d28fb6c6c058876f.js +0 -1
- package/.next/static/chunks/2718-78fee2c0de782178.js +0 -1
- package/.next/static/chunks/4108.1ef6b5e7679b56ac.js +0 -1
- package/.next/static/chunks/7238-11701befb3ca3e41.js +0 -1
- package/.next/static/chunks/7951-590bf2004d7935b5.js +0 -1
- package/.next/static/chunks/8423-ffded33a21b27360.js +0 -1
- package/.next/static/chunks/9045-731ff0865352dd95.js +0 -1
- package/.next/static/chunks/app/agents/[agentId]/page-0b5c2838354d0eba.js +0 -1
- package/.next/static/chunks/app/layout-4fbf9f91ad45e221.js +0 -1
- package/.next/static/chunks/app/onboarding/page-532b193d1c4b0dee.js +0 -1
- package/.next/static/chunks/app/page-e3ec0990b78ce7c7.js +0 -1
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-11aad9a40def2e0d.js +0 -1
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-d8757a85e873dfa1.js +0 -1
- package/.next/static/chunks/app/roots/[id]/page-76216026efb90ae0.js +0 -1
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-c4b6e3825f8626f5.js +0 -1
- package/.next/static/chunks/app/roots/new/page-53ea8d2787e79425.js +0 -1
- package/.next/static/chunks/app/settings/page-7ebaf2b62f256538.js +0 -1
- package/.next/static/chunks/app/share/[id]/page-d5bbbb7e454d1375.js +0 -1
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-e2f928a37483d113.js +0 -1
- package/.next/static/chunks/app/utilities/page-839262ff726a52a2.js +0 -1
- package/.next/static/css/87e01f779d555d04.css +0 -1
- /package/.next/static/{vTfQfQnAWV_hFVZjWEYvZ → fhVNqfmJl5Mdfhyhg6orp}/_buildManifest.js +0 -0
- /package/.next/static/{vTfQfQnAWV_hFVZjWEYvZ → fhVNqfmJl5Mdfhyhg6orp}/_ssgManifest.js +0 -0
|
@@ -91,77 +91,77 @@ export default async function buildModule(
|
|
|
91
91
|
// the LLM tells us WHAT to look for / draw; Reflex resolves it via
|
|
92
92
|
// reflex.images.search (Brave when available) + reflex.images.generate.
|
|
93
93
|
const prompt = [
|
|
94
|
-
|
|
95
|
-
"
|
|
94
|
+
`Course: "${args.topic}". Module: "${args.moduleTitle}" — ${args.moduleObjective}.`,
|
|
95
|
+
"Produce the learning material. JSON reply shape:",
|
|
96
96
|
"{",
|
|
97
|
-
` "article": "
|
|
97
|
+
` "article": "long markdown 800-2000 words; use # ## ### and [[IMG:<id>]] placeholders for inline images",`,
|
|
98
98
|
` "videos": [{"title":"...","url":"https://youtube.com/...","note":"..."}],`,
|
|
99
99
|
` "links": [{"title":"...","url":"...","snippet":"..."}],`,
|
|
100
|
-
` "imageQueries": [{"id":"i1","alt":"...","query":"
|
|
101
|
-
` "generatedFigures": [{"id":"f1","alt":"...","prompt":"
|
|
100
|
+
` "imageQueries": [{"id":"i1","alt":"...","query":"short English search query"}],`,
|
|
101
|
+
` "generatedFigures": [{"id":"f1","alt":"...","prompt":"detailed English description for the AI generator"}],`,
|
|
102
102
|
` "diagrams": [{"title":"...","mermaid":"graph TD; A-->B;"}],`,
|
|
103
103
|
` "homework": ["...","..."]`,
|
|
104
104
|
"}",
|
|
105
105
|
"",
|
|
106
|
-
"##
|
|
106
|
+
"## VISUAL CONTENT — MANDATORY + INLINE PLACEMENT",
|
|
107
107
|
"",
|
|
108
|
-
"
|
|
108
|
+
"Every learning module MUST be visually rich. Each image is placed **inline** at the right spot in the text via a placeholder.",
|
|
109
109
|
"",
|
|
110
|
-
"###
|
|
111
|
-
" 1.
|
|
112
|
-
" 2.
|
|
113
|
-
" 3. Reflex
|
|
114
|
-
" –
|
|
115
|
-
" –
|
|
116
|
-
" –
|
|
117
|
-
" –
|
|
118
|
-
" –
|
|
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 `` plus attribution,",
|
|
118
|
+
" – unresolved ids (image not found / rejected) are cleanly stripped from the text.",
|
|
119
119
|
"",
|
|
120
|
-
"###
|
|
120
|
+
"### Example",
|
|
121
121
|
"```markdown",
|
|
122
|
-
"##
|
|
123
|
-
"
|
|
122
|
+
"## Visual cortex",
|
|
123
|
+
"The primary visual cortex V1 is the first area that processes information from the retina.",
|
|
124
124
|
"",
|
|
125
125
|
"[[IMG:i1]]",
|
|
126
126
|
"",
|
|
127
|
-
"
|
|
127
|
+
"The columnar organisation of V1 was described by Hubel and Wiesel in 1962...",
|
|
128
128
|
"",
|
|
129
129
|
"[[IMG:f1]]",
|
|
130
130
|
"```",
|
|
131
|
-
"
|
|
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
132
|
"",
|
|
133
|
-
"
|
|
133
|
+
"Your job is to fill both arrays AND place the placeholders in `article`:",
|
|
134
134
|
"",
|
|
135
|
-
"### `imageQueries` (
|
|
136
|
-
" •
|
|
137
|
-
" •
|
|
138
|
-
" • `id` —
|
|
139
|
-
" • `alt` —
|
|
140
|
-
" •
|
|
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
141
|
"",
|
|
142
|
-
"### `generatedFigures` (AI
|
|
143
|
-
" •
|
|
144
|
-
" • `id` —
|
|
145
|
-
" • `prompt` —
|
|
146
|
-
" •
|
|
147
|
-
" • `alt` —
|
|
148
|
-
" •
|
|
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
149
|
"",
|
|
150
|
-
"###
|
|
151
|
-
" • article —
|
|
152
|
-
" •
|
|
153
|
-
" •
|
|
154
|
-
" •
|
|
155
|
-
" • videos: 1-3
|
|
156
|
-
" • links: 2-5
|
|
157
|
-
" • diagrams (mermaid):
|
|
158
|
-
" • homework: 3-5
|
|
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
159
|
"",
|
|
160
|
-
"
|
|
160
|
+
"Reply with JSON ONLY on a single line, no markdown fences.",
|
|
161
161
|
"",
|
|
162
162
|
webContext
|
|
163
|
-
? `## Web
|
|
164
|
-
: "## Web
|
|
163
|
+
? `## Web sources to ground on\n${webContext}`
|
|
164
|
+
: "## Web sources unavailable — rely on your own knowledge.",
|
|
165
165
|
].join("\n");
|
|
166
166
|
|
|
167
167
|
const result = await callJsonAgent<LlmDraft>({
|
|
@@ -170,7 +170,7 @@ export default async function buildModule(
|
|
|
170
170
|
maxAttempts: 4,
|
|
171
171
|
shapeHint:
|
|
172
172
|
`{"article":"...","videos":[],"links":[],"imageQueries":[],"generatedFigures":[],"diagrams":[],"homework":[]}\n` +
|
|
173
|
-
`article — markdown 800-2000
|
|
173
|
+
`article — markdown 800-2000 words. Every list field must be an array (empty is OK).`,
|
|
174
174
|
validate: (p) => {
|
|
175
175
|
const v = p as LlmDraft;
|
|
176
176
|
return typeof v?.article === "string" && v.article.trim().length > 40
|
|
@@ -180,8 +180,8 @@ export default async function buildModule(
|
|
|
180
180
|
});
|
|
181
181
|
if (!result.ok) {
|
|
182
182
|
throw new Error(
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
`Failed to build the module in ${result.attempts} attempts (${result.reason}). ` +
|
|
184
|
+
`Last reply: "${snippet(result.lastText, 200)}".`,
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
const draft = result.value;
|
|
@@ -196,8 +196,9 @@ export default async function buildModule(
|
|
|
196
196
|
|
|
197
197
|
// Inline-place images by substituting [[IMG:<id>]] placeholders in the
|
|
198
198
|
// article body. Any image whose id wasn't referenced (LLM forgot)
|
|
199
|
-
// falls back into the trailing
|
|
200
|
-
// dropped silently. Residual unknown [[...]] markers are
|
|
199
|
+
// falls back into the trailing "Additional illustrations" section so
|
|
200
|
+
// nothing gets dropped silently. Residual unknown [[...]] markers are
|
|
201
|
+
// stripped.
|
|
201
202
|
const allImages = [...searchedImages, ...generatedImages];
|
|
202
203
|
const { article: articleWithImages, placedIds } = substituteImagePlaceholders(
|
|
203
204
|
typeof draft.article === "string" ? draft.article : "",
|
|
@@ -226,13 +227,13 @@ export default async function buildModule(
|
|
|
226
227
|
const body = [
|
|
227
228
|
content.article,
|
|
228
229
|
unplaced.length > 0
|
|
229
|
-
? "\n\n##
|
|
230
|
+
? "\n\n## Additional illustrations\n" +
|
|
230
231
|
unplaced
|
|
231
232
|
.map((im) => `${renderInlineImage(im)}`)
|
|
232
233
|
.join("\n\n")
|
|
233
234
|
: "",
|
|
234
235
|
content.diagrams.length > 0
|
|
235
|
-
? "\n\n##
|
|
236
|
+
? "\n\n## Diagrams\n" +
|
|
236
237
|
content.diagrams
|
|
237
238
|
.map(
|
|
238
239
|
(d) =>
|
|
@@ -395,24 +396,24 @@ function renderInlineImage(im: ModuleContent["images"][number]): string {
|
|
|
395
396
|
const safeAlt = im.alt.replace(/[\[\]\n]/g, " ").slice(0, 200);
|
|
396
397
|
const credit =
|
|
397
398
|
im.source === "search" && im.attribution?.name
|
|
398
|
-
? `\n\
|
|
399
|
+
? `\n\n_Source: [${im.attribution.name}](${im.attribution.link || im.url})_`
|
|
399
400
|
: im.source === "generated"
|
|
400
|
-
? `\n\
|
|
401
|
+
? `\n\n_Generated by AI_`
|
|
401
402
|
: "";
|
|
402
403
|
return `${credit}`;
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
/**
|
|
406
|
-
* Strip residual `[[
|
|
407
|
+
* Strip residual `[[ILLUSTRATION: ...]]` / `[[DIAGRAM: ...]]` / orphan
|
|
407
408
|
* `[[IMG:<unknown-id>]]` placeholders the LLM might emit despite the
|
|
408
409
|
* prompt forbidding them OR after `substituteImagePlaceholders` failed
|
|
409
410
|
* to find the id. Drops the marker, collapses surrounding whitespace.
|
|
410
411
|
*
|
|
411
|
-
* Matches
|
|
412
|
+
* Matches several spelling variants for safety. Case-insensitive.
|
|
412
413
|
*/
|
|
413
414
|
function stripPlaceholderMarkers(text: string): string {
|
|
414
415
|
const re =
|
|
415
|
-
/\[\[?\s*(
|
|
416
|
+
/\[\[?\s*(?:ILLUSTRATION|SCHEME|IMAGE|IMG|DIAGRAM)\b[^\]]*?\]\]?/giu;
|
|
416
417
|
let out = text.replace(re, "");
|
|
417
418
|
// Clean up double blank lines + trailing whitespace the removal may
|
|
418
419
|
// have left behind.
|
|
@@ -3,15 +3,16 @@ import { reflex } from "@host/api";
|
|
|
3
3
|
/**
|
|
4
4
|
* "Explain this" feature: user highlights a snippet inside the module
|
|
5
5
|
* article and asks for a deeper explanation. The agent gets the
|
|
6
|
-
* surrounding paragraph as context so it understands what "
|
|
6
|
+
* surrounding paragraph as context so it understands what "this" means.
|
|
7
7
|
*
|
|
8
8
|
* Two modes:
|
|
9
9
|
* 1. Default — `question` omitted, agent gives a generic 2-5 paragraph
|
|
10
10
|
* breakdown of the selected fragment.
|
|
11
11
|
* 2. Custom — `question` supplied (book-style margin annotation), agent
|
|
12
12
|
* answers that specific question with the selection as the focal
|
|
13
|
-
* point. Lets the reader say things like "
|
|
14
|
-
* "
|
|
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.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
export interface ExplainSelectionArgs {
|
|
@@ -29,31 +30,31 @@ export default async function explainSelection(
|
|
|
29
30
|
): Promise<{ text: string }> {
|
|
30
31
|
const userQuestion = (args.question ?? "").trim();
|
|
31
32
|
const promptLines: string[] = [
|
|
32
|
-
|
|
33
|
+
`Course: "${args.topic}". Module: "${args.moduleTitle}".`,
|
|
33
34
|
];
|
|
34
35
|
|
|
35
36
|
if (userQuestion) {
|
|
36
37
|
promptLines.push(
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"Markdown
|
|
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.",
|
|
40
41
|
);
|
|
41
42
|
} else {
|
|
42
43
|
promptLines.push(
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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.",
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
promptLines.push(
|
|
50
51
|
"",
|
|
51
|
-
`##
|
|
52
|
+
`## Surrounding context\n${args.context.slice(0, 1500)}`,
|
|
52
53
|
"",
|
|
53
|
-
`##
|
|
54
|
+
`## User selection\n"${args.selection.slice(0, 800)}"`,
|
|
54
55
|
);
|
|
55
56
|
if (userQuestion) {
|
|
56
|
-
promptLines.push("", `##
|
|
57
|
+
promptLines.push("", `## User question\n${userQuestion.slice(0, 600)}`);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const r = await reflex.llm.complete({
|
|
@@ -38,20 +38,20 @@ export default async function generateOutline(
|
|
|
38
38
|
.map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`)
|
|
39
39
|
.join("\n\n");
|
|
40
40
|
const prompt = [
|
|
41
|
-
"
|
|
41
|
+
"Design a learning course on the topic \"${TOPIC}\" tailored to a user with the following answers:".replace(
|
|
42
42
|
"${TOPIC}",
|
|
43
43
|
args.topic,
|
|
44
44
|
),
|
|
45
45
|
"",
|
|
46
|
-
prior || "(
|
|
46
|
+
prior || "(no answers)",
|
|
47
47
|
"",
|
|
48
|
-
"
|
|
49
|
-
" • 5-9
|
|
50
|
-
" •
|
|
51
|
-
" • objective —
|
|
52
|
-
" • estMinutes —
|
|
53
|
-
" • id
|
|
54
|
-
"
|
|
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
55
|
` {"modules":[{"id":"intro","title":"…","objective":"…","estMinutes":30}, ...]}`,
|
|
56
56
|
].join("\n");
|
|
57
57
|
|
|
@@ -60,8 +60,8 @@ export default async function generateOutline(
|
|
|
60
60
|
invoke: (p) => reflex.agent.invoke({ prompt: p, timeoutMs: 4 * 60_000 }),
|
|
61
61
|
maxAttempts: 4,
|
|
62
62
|
shapeHint:
|
|
63
|
-
`{"modules":[{"id":"intro","title":"
|
|
64
|
-
|
|
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
65
|
validate: (parsed) => {
|
|
66
66
|
const v = parsed as { modules?: unknown };
|
|
67
67
|
const modules = sanitizeModules(v?.modules);
|
|
@@ -71,8 +71,8 @@ export default async function generateOutline(
|
|
|
71
71
|
|
|
72
72
|
if (!result.ok) {
|
|
73
73
|
throw new Error(
|
|
74
|
-
|
|
75
|
-
|
|
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
76
|
);
|
|
77
77
|
}
|
|
78
78
|
const modules = result.value;
|
|
@@ -82,11 +82,11 @@ export default async function generateOutline(
|
|
|
82
82
|
const body = [
|
|
83
83
|
`# ${args.topic}`,
|
|
84
84
|
"",
|
|
85
|
-
"##
|
|
85
|
+
"## Program",
|
|
86
86
|
"",
|
|
87
87
|
...modules.map(
|
|
88
88
|
(mod, i) =>
|
|
89
|
-
`${i + 1}. **${mod.title}** — ${mod.objective} (~${mod.estMinutes}
|
|
89
|
+
`${i + 1}. **${mod.title}** — ${mod.objective} (~${mod.estMinutes} min)`,
|
|
90
90
|
),
|
|
91
91
|
].join("\n");
|
|
92
92
|
|
|
@@ -28,16 +28,16 @@ export default async function generateQuiz(
|
|
|
28
28
|
): Promise<{ questions: QuizQuestion[] }> {
|
|
29
29
|
const trimmed = (args.article ?? "").slice(0, 6000);
|
|
30
30
|
const prompt = [
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
" •
|
|
35
|
-
" •
|
|
36
|
-
" •
|
|
37
|
-
"
|
|
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
38
|
` {"questions":[{"stem":"...","options":["a","b","c","d"],"correctIndex":0,"explanation":"..."}, ...]}`,
|
|
39
39
|
"",
|
|
40
|
-
`##
|
|
40
|
+
`## Module material\n${trimmed}`,
|
|
41
41
|
].join("\n");
|
|
42
42
|
|
|
43
43
|
const r = await reflex.llm.complete({ task: "quick", prompt });
|
|
@@ -35,21 +35,21 @@ export default async function generateTrainer(
|
|
|
35
35
|
): Promise<TrainerSpec> {
|
|
36
36
|
const userBrief =
|
|
37
37
|
(args.prompt ?? "").trim() ||
|
|
38
|
-
|
|
38
|
+
`Design a trainer that helps reinforce "${args.moduleObjective}".`;
|
|
39
39
|
const prompt = [
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
" •
|
|
44
|
-
" •
|
|
45
|
-
" •
|
|
46
|
-
" •
|
|
47
|
-
" •
|
|
48
|
-
" •
|
|
49
|
-
" •
|
|
50
|
-
"
|
|
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
51
|
"",
|
|
52
|
-
`##
|
|
52
|
+
`## Trainer idea\n${userBrief}`,
|
|
53
53
|
].join("\n");
|
|
54
54
|
|
|
55
55
|
// Trainer HTML is long — give the agent a generous 7 minutes; worker
|
|
@@ -58,13 +58,13 @@ export default async function generateTrainer(
|
|
|
58
58
|
const text = r.text ?? "";
|
|
59
59
|
const html = extractHtml(text);
|
|
60
60
|
if (!html) {
|
|
61
|
-
throw new Error("
|
|
61
|
+
throw new Error("Agent did not return valid HTML — please try again");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const trainerId = `${args.moduleId}-${Date.now().toString(36)}`;
|
|
65
65
|
const saved = await reflex.kb.add({
|
|
66
66
|
kind: "course-trainer",
|
|
67
|
-
title:
|
|
67
|
+
title: `Trainer · ${args.moduleTitle}`,
|
|
68
68
|
body: "```html\n" + html.slice(0, 30_000) + "\n```",
|
|
69
69
|
meta: {
|
|
70
70
|
courseId: args.courseId,
|
|
@@ -32,14 +32,14 @@ export default async function refreshCourseCard(): Promise<{ courses: number; av
|
|
|
32
32
|
await reflex.cards.update({
|
|
33
33
|
snapshot: {
|
|
34
34
|
kind: "kpi",
|
|
35
|
-
title: "🎓
|
|
35
|
+
title: "🎓 Learning",
|
|
36
36
|
data: {
|
|
37
37
|
items: [
|
|
38
|
-
{ label: "
|
|
38
|
+
{ label: "Active courses", value: String(courses) },
|
|
39
39
|
{
|
|
40
|
-
label: "
|
|
40
|
+
label: "Progress",
|
|
41
41
|
value: courses === 0 ? "—" : `${avg}%`,
|
|
42
|
-
hint: "
|
|
42
|
+
hint: "average across courses",
|
|
43
43
|
...(avg >= 60
|
|
44
44
|
? ({ delta: "up" } as const)
|
|
45
45
|
: avg < 30 && courses > 0
|
|
@@ -8,8 +8,8 @@ import { extractJson } from "./_json";
|
|
|
8
8
|
* `{done:true}` so the UI advances to outline generation.
|
|
9
9
|
*
|
|
10
10
|
* Keeping the wizard agent-driven means we don't hardcode "3 questions
|
|
11
|
-
* about level/focus/format" — for "
|
|
12
|
-
* the agent will ask different questions than for "
|
|
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
13
|
* Python". The UI just renders whatever comes back.
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ export interface TutorAskArgs {
|
|
|
22
22
|
topic: string;
|
|
23
23
|
history: TutorQA[];
|
|
24
24
|
/** Force-finish even if the agent wants more. UI uses this when the
|
|
25
|
-
* user clicks "
|
|
25
|
+
* user clicks "Enough questions — build the course already". */
|
|
26
26
|
forceFinish?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ export interface TutorAskResult {
|
|
|
32
32
|
question?: string;
|
|
33
33
|
/** Optional pre-baked choices for a multiple-choice ask. */
|
|
34
34
|
choices?: string[];
|
|
35
|
-
/** Hint to UI: short label like "
|
|
35
|
+
/** Hint to UI: short label like "level", "format", "goal". */
|
|
36
36
|
header?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -51,19 +51,19 @@ export default async function tutorAsk(
|
|
|
51
51
|
.join("\n\n");
|
|
52
52
|
|
|
53
53
|
const prompt = [
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
` {"done":false,"question":"...","header":"
|
|
62
|
-
"
|
|
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
63
|
` {"done":true}`,
|
|
64
64
|
"",
|
|
65
|
-
prior ? `##
|
|
66
|
-
`\
|
|
65
|
+
prior ? `## Previous answers\n${prior}` : "## This is the first question",
|
|
66
|
+
`\nQuestions asked so far: ${turns} (maximum ${MAX_TURNS}).`,
|
|
67
67
|
].join("\n");
|
|
68
68
|
|
|
69
69
|
const r = await reflex.llm.complete({ task: "quick", prompt });
|
|
@@ -74,7 +74,7 @@ export function ArticleView({
|
|
|
74
74
|
node.style.fontFamily = "inherit";
|
|
75
75
|
} catch (err) {
|
|
76
76
|
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
-
node.innerHTML = `<div class="text-xs text-red-600 not-italic">⚠
|
|
77
|
+
node.innerHTML = `<div class="text-xs text-red-600 not-italic">⚠ Failed to render diagram: ${escapeHtml(msg)}</div><pre class="mt-2 text-[11px] text-slate-700 whitespace-pre-wrap">${escapeHtml(src)}</pre>`;
|
|
78
78
|
node.classList.add("reflex-mermaid-done");
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -100,7 +100,7 @@ export function ArticleView({
|
|
|
100
100
|
<img
|
|
101
101
|
src={url}
|
|
102
102
|
alt={safeAlt}
|
|
103
|
-
title={safeAlt || "
|
|
103
|
+
title={safeAlt || "Open fullscreen"}
|
|
104
104
|
loading="lazy"
|
|
105
105
|
onClick={() => setZoom({ src: url, alt: safeAlt })}
|
|
106
106
|
className="my-5 rounded-lg border bg-white max-w-full h-auto cursor-zoom-in transition hover:opacity-90"
|
|
@@ -164,7 +164,7 @@ export function ArticleImageLightbox({
|
|
|
164
164
|
<div
|
|
165
165
|
onClick={onClose}
|
|
166
166
|
role="dialog"
|
|
167
|
-
aria-label={alt || "
|
|
167
|
+
aria-label={alt || "Image preview"}
|
|
168
168
|
style={{
|
|
169
169
|
position: "fixed",
|
|
170
170
|
inset: 0,
|
|
@@ -184,7 +184,7 @@ export function ArticleImageLightbox({
|
|
|
184
184
|
e.stopPropagation();
|
|
185
185
|
onClose();
|
|
186
186
|
}}
|
|
187
|
-
aria-label="
|
|
187
|
+
aria-label="Close"
|
|
188
188
|
style={{
|
|
189
189
|
position: "absolute",
|
|
190
190
|
top: 16,
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "learn-anything",
|
|
3
|
-
"name": "🎓
|
|
3
|
+
"name": "🎓 Learn Anything",
|
|
4
4
|
"version": "0.4.7",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Universal AI tutor: courses tailored to your level with content, quizzes, and trainers",
|
|
6
6
|
"author": "reflex-org",
|
|
7
7
|
"category": "study",
|
|
8
8
|
"ui": "ui.tsx",
|
|
9
9
|
"icon": "lucide:GraduationCap",
|
|
10
10
|
"card": {
|
|
11
11
|
"kind": "kpi",
|
|
12
|
-
"title": "🎓
|
|
12
|
+
"title": "🎓 Learning",
|
|
13
13
|
"data": {
|
|
14
14
|
"items": [
|
|
15
|
-
{ "label": "
|
|
16
|
-
{ "label": "
|
|
15
|
+
{ "label": "Active courses", "value": "0" },
|
|
16
|
+
{ "label": "Progress", "value": "—", "hint": "average across courses" }
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
},
|