@learnpack/learnpack 5.0.217 → 5.0.234

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.
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-CZrxF_55.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-CFK5bQP2.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-DmpsXknz.css">
15
15
  </head>
16
16
  <body>
@@ -28,6 +28,12 @@ export declare const doesAssetExists: (token: string, assetSlug: string) => Prom
28
28
  exists: boolean;
29
29
  academyId?: number;
30
30
  }>;
31
+ type TTechnology = {
32
+ slug: string;
33
+ lang: string;
34
+ };
35
+ export declare const fetchTechnologies: () => Promise<any[]>;
36
+ export declare const getCurrentTechnologies: () => TTechnology[];
31
37
  declare const _default: {
32
38
  login: (identification: string, password: string) => Promise<any>;
33
39
  publish: (config: any) => Promise<any>;
@@ -51,5 +57,6 @@ declare const _default: {
51
57
  getCategories: (token: string) => Promise<any>;
52
58
  updateRigoAssetID: (token: string, slug: string, asset_id: number) => Promise<any>;
53
59
  createRigoPackage: (token: string, slug: string, config: any) => Promise<any>;
60
+ getCurrentTechnologies: () => TTechnology[];
54
61
  };
55
62
  export default _default;
package/lib/utils/api.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.doesAssetExists = exports.createAsset = exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = exports.RIGOBOT_HOST = void 0;
3
+ exports.getCurrentTechnologies = exports.fetchTechnologies = exports.doesAssetExists = exports.createAsset = exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = exports.RIGOBOT_HOST = void 0;
4
4
  const console_1 = require("../utils/console");
5
5
  const storage = require("node-persist");
6
6
  const cli_ux_1 = require("cli-ux");
7
7
  const axios_1 = require("axios");
8
+ const dotenv = require("dotenv");
9
+ dotenv.config();
8
10
  const HOST = "https://breathecode.herokuapp.com";
9
11
  exports.RIGOBOT_HOST = "https://rigobot.herokuapp.com";
10
12
  // export const RIGOBOT_HOST = "https://rigobot-test-cca7d841c9d8.herokuapp.com"
@@ -442,6 +444,56 @@ const createRigoPackage = async (token, slug, config) => {
442
444
  throw error;
443
445
  }
444
446
  };
447
+ let technologiesCache = [];
448
+ const fetchTechnologies = async () => {
449
+ const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN;
450
+ const LANGS = ["en", "es", "us"];
451
+ if (!BREATHECODE_PERMANENT_TOKEN) {
452
+ throw new Error("BREATHECODE_PERMANENT_TOKEN is not defined in environment variables");
453
+ }
454
+ const headers = {
455
+ Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
456
+ };
457
+ const results = await Promise.all(LANGS.map(lang => axios_1.default
458
+ .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
459
+ .then(res => {
460
+ return res.data;
461
+ })
462
+ .then(data => data.map((item) => ({
463
+ slug: item.slug,
464
+ lang: lang,
465
+ })))));
466
+ const allItems = results.flat();
467
+ // Remove duplicates by slug+lang combination
468
+ const unique = [];
469
+ const seen = new Set();
470
+ for (const item of allItems) {
471
+ const key = `${item.slug}:${item.lang}`;
472
+ if (!seen.has(key)) {
473
+ seen.add(key);
474
+ unique.push(item);
475
+ }
476
+ }
477
+ return unique;
478
+ };
479
+ exports.fetchTechnologies = fetchTechnologies;
480
+ // Function to update the cache and schedule the next update
481
+ async function updateTechnologiesPeriodically() {
482
+ try {
483
+ technologiesCache = await (0, exports.fetchTechnologies)();
484
+ // Uncomment for debugging:
485
+ // console.log('Technologies list updated:', technologiesCache);
486
+ }
487
+ catch (error) {
488
+ console.error("Error updating technologies list:", error);
489
+ }
490
+ finally {
491
+ setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000);
492
+ }
493
+ }
494
+ updateTechnologiesPeriodically();
495
+ const getCurrentTechnologies = () => technologiesCache;
496
+ exports.getCurrentTechnologies = getCurrentTechnologies;
445
497
  exports.default = {
446
498
  login,
447
499
  publish,
@@ -459,4 +511,5 @@ exports.default = {
459
511
  getCategories,
460
512
  updateRigoAssetID,
461
513
  createRigoPackage,
514
+ getCurrentTechnologies: exports.getCurrentTechnologies,
462
515
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.217",
4
+ "version": "5.0.234",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -154,10 +154,10 @@
154
154
  ]
155
155
  },
156
156
  "scripts": {
157
- "copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents",
157
+ "copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose",
158
158
  "tsc": "tsc -b",
159
159
  "postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
160
- "prepack": "rm -rf lib && tsc -b && npm run copy-assets && oclif-dev manifest && oclif-dev readme",
160
+ "prepack": "rm -rf lib && tsc -b && npm run copy-assets ",
161
161
  "pre": "node ./test/precommit/index.ts",
162
162
  "test": "NODE_ENV=test nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
163
163
  "version": "oclif-dev readme && git add README.md",
@@ -25,8 +25,20 @@ const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
25
25
  export const handleAssetCreation = async (
26
26
  sessionPayload: { token: string; rigobotToken: string },
27
27
  learnJson: any,
28
+ selectedLang: string,
28
29
  learnpackDeployUrl: string
29
30
  ) => {
31
+ const categories: Record<string, number> = {
32
+ us: 91,
33
+ es: 92,
34
+ }
35
+
36
+ let category = categories[selectedLang]
37
+
38
+ if (!category) {
39
+ category = 91
40
+ }
41
+
30
42
  try {
31
43
  const user = await api.validateToken(sessionPayload.token)
32
44
 
@@ -39,13 +51,13 @@ export const handleAssetCreation = async (
39
51
  Console.info("Asset does not exist in this academy, creating it")
40
52
  const asset = await api.createAsset(sessionPayload.token, {
41
53
  slug: learnJson.slug,
42
- title: learnJson.title.us,
43
- lang: "us",
44
- description: learnJson.description.us,
54
+ title: learnJson.title[selectedLang],
55
+ lang: selectedLang,
56
+ description: learnJson.description[selectedLang],
45
57
  learnpack_deploy_url: learnpackDeployUrl,
46
- technologies: ["node", "bash"],
58
+ technologies: learnJson.technologies,
47
59
  url: learnpackDeployUrl,
48
- category: 51,
60
+ category: category,
49
61
  owner: user.id,
50
62
  author: user.id,
51
63
  preview: learnJson.preview,
@@ -63,8 +75,8 @@ export const handleAssetCreation = async (
63
75
  learnJson.slug,
64
76
  {
65
77
  learnpack_deploy_url: learnpackDeployUrl,
66
- title: learnJson.title.us,
67
- description: learnJson.description.us,
78
+ title: learnJson.title[selectedLang],
79
+ description: learnJson.description[selectedLang],
68
80
  }
69
81
  )
70
82
  await api.updateRigoAssetID(
@@ -423,7 +435,7 @@ class BuildCommand extends SessionCommand {
423
435
  fs.unlinkSync(zipFilePath)
424
436
  this.removeDirectory(buildDir)
425
437
 
426
- await handleAssetCreation(sessionPayload, learnJson, res.data.url)
438
+ await handleAssetCreation(sessionPayload, learnJson, "us", res.data.url)
427
439
  } catch (error) {
428
440
  if (axios.isAxiosError(error)) {
429
441
  if (error.response && error.response.status === 403) {
@@ -102,7 +102,7 @@ const uploadFileToBucket = async (
102
102
  path: string
103
103
  ) => {
104
104
  const fileRef = bucket.file(path)
105
- await fileRef.save(file)
105
+ await fileRef.save(Buffer.from(file, "utf8"))
106
106
  }
107
107
 
108
108
  const uploadImageToBucket = async (
@@ -231,6 +231,8 @@ async function startExerciseGeneration(
231
231
 
232
232
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
233
233
 
234
+ console.log("WEBHOOK URL", webhookUrl)
235
+
234
236
  await readmeCreator(
235
237
  rigoToken,
236
238
  {
@@ -1123,33 +1125,40 @@ export default class ServeCommand extends SessionCommand {
1123
1125
  }
1124
1126
  })
1125
1127
 
1126
- app.get("/test/:slug", (req, res) => {
1127
- emitToCourse(req.params.slug, "course-creation", {
1128
- lesson: "000-welcome-to-bird-domestication",
1129
- status: "generating",
1130
- log: "Hello",
1131
- })
1132
- emitToCourse(req.params.slug, "course-creation", {
1133
- lesson: "000-welcome-to-bird-domestication",
1134
- status: "generating",
1135
- log: "Hello",
1136
- })
1137
- emitToCourse(req.params.slug, "course-creation", {
1138
- lesson: "000-welcome-to-bird-domestication",
1139
- status: "generating",
1140
- log: "Hello broder",
1141
- })
1142
- emitToCourse(req.params.slug, "course-creation", {
1143
- lesson: "000-welcome-to-bird-domestication",
1144
- status: "done",
1145
- log: "Hello broder",
1146
- })
1147
- emitToCourse(req.params.slug, "course-creation", {
1148
- lesson: "other",
1149
- status: "generating",
1150
- log: "Hello",
1151
- })
1152
- res.send({ message: "Course creation started" })
1128
+ // app.get("/test/:slug", (req, res) => {
1129
+ // emitToCourse(req.params.slug, "course-creation", {
1130
+ // lesson: "000-welcome-to-bird-domestication",
1131
+ // status: "generating",
1132
+ // log: "Hello",
1133
+ // })
1134
+ // emitToCourse(req.params.slug, "course-creation", {
1135
+ // lesson: "000-welcome-to-bird-domestication",
1136
+ // status: "generating",
1137
+ // log: "Hello",
1138
+ // })
1139
+ // emitToCourse(req.params.slug, "course-creation", {
1140
+ // lesson: "000-welcome-to-bird-domestication",
1141
+ // status: "generating",
1142
+ // log: "Hello broder",
1143
+ // })
1144
+ // emitToCourse(req.params.slug, "course-creation", {
1145
+ // lesson: "000-welcome-to-bird-domestication",
1146
+ // status: "done",
1147
+ // log: "Hello broder",
1148
+ // })
1149
+ // emitToCourse(req.params.slug, "course-creation", {
1150
+ // lesson: "other",
1151
+ // status: "generating",
1152
+ // log: "Hello",
1153
+ // })
1154
+ // res.send({ message: "Course creation started" })
1155
+ // })
1156
+
1157
+ app.get("/technologies", async (req, res) => {
1158
+ console.log("GET /technologies")
1159
+
1160
+ const technologies = await api.getCurrentTechnologies()
1161
+ res.json(technologies)
1153
1162
  })
1154
1163
 
1155
1164
  app.get("/assets/:file", (req, res) => {
@@ -1280,6 +1289,41 @@ export default class ServeCommand extends SessionCommand {
1280
1289
  })
1281
1290
  })
1282
1291
 
1292
+ // app.post(
1293
+ // "/check-latex/:courseSlug/:exerciseSlug/:lang",
1294
+ // async (req, res) => {
1295
+ // const { courseSlug, exerciseSlug, lang } = req.params
1296
+
1297
+ // const rigoToken = req.header("x-rigo-token")
1298
+
1299
+ // if (!rigoToken) {
1300
+ // return res.status(400).json({ error: "Missing tokens" })
1301
+ // }
1302
+
1303
+ // const exercise = await bucket.file(
1304
+ // `courses/${courseSlug}/exercises/${exerciseSlug}/README.${lang}.md`
1305
+ // )
1306
+ // const [content] = await exercise.download()
1307
+ // const headers = {
1308
+ // Authorization: `Token ${rigoToken}`,
1309
+ // }
1310
+ // const response = await axios.get(
1311
+ // `${RIGOBOT_HOST}/v1/prompting/completion/60865/`,
1312
+ // {
1313
+ // headers,
1314
+ // }
1315
+ // )
1316
+
1317
+ // console.log(response.data.parsed.content, "RESPONSE from Rigobot")
1318
+
1319
+ // res.json({
1320
+ // message: "Exercise downloaded",
1321
+ // completion: response.data,
1322
+ // exercise: content.toString(),
1323
+ // })
1324
+ // }
1325
+ // )
1326
+
1283
1327
  app.get("/courses/:courseSlug/syllabus", async (req, res) => {
1284
1328
  try {
1285
1329
  console.log("GET /courses/:courseSlug/syllabus")
@@ -1524,11 +1568,11 @@ export default class ServeCommand extends SessionCommand {
1524
1568
  await handleAssetCreation(
1525
1569
  { token: bcToken, rigobotToken: rigoToken },
1526
1570
  fullConfig.config,
1571
+ selectedLang,
1527
1572
  rigoRes.data.url
1528
1573
  )
1529
1574
 
1530
1575
  rimraf.sync(tmpRoot)
1531
- console.log("RIGO RES", rigoRes.data)
1532
1576
 
1533
1577
  return res.json({ url: rigoRes.data.url })
1534
1578
  })
@@ -13,6 +13,7 @@ import {
13
13
  loginWithToken,
14
14
  parseLesson,
15
15
  fixTitleLength,
16
+ getTechnologies,
16
17
  } from "./utils/lib"
17
18
 
18
19
  import { Uploader } from "./components/Uploader"
@@ -42,11 +43,15 @@ function App() {
42
43
  resetFormState,
43
44
  cleanAll,
44
45
  setMessages,
46
+ technologies,
47
+ setTechnologies,
45
48
  } = useStore(
46
49
  useShallow((state) => ({
47
50
  formState: state.formState,
48
51
  setFormState: state.setFormState,
49
52
  setAuth: state.setAuth,
53
+ technologies: state.technologies,
54
+ setTechnologies: state.setTechnologies,
50
55
  push: state.push,
51
56
  history: state.history,
52
57
  cleanHistory: state.cleanHistory,
@@ -67,7 +72,8 @@ function App() {
67
72
 
68
73
  useEffect(() => {
69
74
  verifyToken()
70
- checkDescription()
75
+ checkQueryParams()
76
+ checkTechs()
71
77
  }, [])
72
78
 
73
79
  const verifyToken = async () => {
@@ -84,14 +90,28 @@ function App() {
84
90
  }
85
91
  }
86
92
 
87
- const checkDescription = () => {
88
- const { description, duration, plan, purpose, language } = checkParams([
93
+ const checkQueryParams = () => {
94
+ const {
95
+ description,
96
+ duration,
97
+ plan,
98
+ purpose,
99
+ language,
100
+ new: newParam,
101
+ } = checkParams([
89
102
  "description",
90
103
  "duration",
91
104
  "plan",
92
105
  "purpose",
93
106
  "language",
107
+ "new",
94
108
  ])
109
+
110
+ if (newParam && newParam.toLowerCase().trim() === "true") {
111
+ cleanAll()
112
+ }
113
+
114
+
95
115
  if (description) {
96
116
  console.log("description", description)
97
117
  setFormState({
@@ -134,8 +154,11 @@ function App() {
134
154
 
135
155
  const handleCreateTutorial = async () => {
136
156
  try {
137
- const isValid = await isValidRigoToken(auth.rigoToken)
138
- if (!isValid) {
157
+ let isAuthenticated = false
158
+ if (auth.publicToken) {
159
+ isAuthenticated = await isValidRigoToken(auth.publicToken)
160
+ }
161
+ if (!isAuthenticated && auth.rigoToken) {
139
162
  setAuth({
140
163
  ...auth,
141
164
  rigoToken: "",
@@ -145,14 +168,24 @@ function App() {
145
168
  })
146
169
  }
147
170
 
171
+ let techs = technologies.filter((t) => t.lang === formState.language)
172
+
173
+ if (techs.length === 0) {
174
+ techs = technologies.filter((t) => t.lang === "us")
175
+ }
176
+
148
177
  const res = await publicInteractiveCreation(
149
178
  {
150
- courseInfo: `${JSON.stringify(formState)}`,
179
+ courseInfo: `${JSON.stringify(
180
+ formState
181
+ )}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
182
+ .map((t) => t.slug)
183
+ .join(", ")}</techs>`,
151
184
  prevInteractions: "USER: " + formState.description,
152
185
  },
153
- auth.rigoToken && isValid ? auth.rigoToken : auth.publicToken,
186
+ auth.rigoToken && isAuthenticated ? auth.rigoToken : auth.publicToken,
154
187
  formState.purpose || "learnpack-lesson-writer",
155
- auth.rigoToken && isValid ? false : true
188
+ auth.rigoToken && isAuthenticated ? false : true
156
189
  )
157
190
  const lessons = res.parsed.listOfSteps.map((lesson: any) => {
158
191
  return parseLesson(lesson, [])
@@ -165,7 +198,10 @@ function App() {
165
198
  title: fixTitleLength(res.parsed.title),
166
199
  description: res.parsed.description,
167
200
  language: res.parsed.languageCode || formState.language || "en",
168
- technologies: res.parsed.technologies,
201
+ technologies:
202
+ res.parsed.technologies.length > 0
203
+ ? res.parsed.technologies
204
+ : ["education", "quizzes"],
169
205
  },
170
206
  })
171
207
 
@@ -206,6 +242,14 @@ function App() {
206
242
  }
207
243
  }
208
244
 
245
+ const checkTechs = async () => {
246
+ if (technologies.length === 0) {
247
+ const technologies = await getTechnologies()
248
+ console.log("TECHNOLOGIES", technologies)
249
+ setTechnologies(technologies)
250
+ }
251
+ }
252
+
209
253
  const buildSteps = () => {
210
254
  const steps = [
211
255
  {
@@ -34,19 +34,28 @@ const SyllabusEditor: React.FC = () => {
34
34
  const navigate = useNavigate()
35
35
  const { i18n } = useTranslation()
36
36
 
37
- const { history, auth, setAuth, push, cleanAll, messages, setMessages } =
38
- useStore(
39
- useShallow((state) => ({
40
- history: state.history,
41
- auth: state.auth,
42
- setAuth: state.setAuth,
43
- push: state.push,
44
- cleanAll: state.cleanAll,
45
- messages: state.messages,
46
- // formState: state.formState,
47
- setMessages: state.setMessages,
48
- }))
49
- )
37
+ const {
38
+ history,
39
+ auth,
40
+ setAuth,
41
+ push,
42
+ cleanAll,
43
+ messages,
44
+ setMessages,
45
+ technologies,
46
+ } = useStore(
47
+ useShallow((state) => ({
48
+ history: state.history,
49
+ auth: state.auth,
50
+ setAuth: state.setAuth,
51
+ push: state.push,
52
+ cleanAll: state.cleanAll,
53
+ messages: state.messages,
54
+ // formState: state.formState,
55
+ setMessages: state.setMessages,
56
+ technologies: state.technologies,
57
+ }))
58
+ )
50
59
 
51
60
  const [isGenerating, setIsGenerating] = useState(false)
52
61
  const [showLoginModal, setShowLoginModal] = useState(false)
@@ -120,9 +129,12 @@ const SyllabusEditor: React.FC = () => {
120
129
  { type: "user", content: prompt },
121
130
  { type: "assistant", content: "" },
122
131
  ])
132
+ let isAuthenticated = false
133
+ if (auth.rigoToken) {
134
+ isAuthenticated = await isValidRigoToken(auth.rigoToken)
135
+ }
123
136
 
124
- const isValid = await isValidRigoToken(auth.rigoToken)
125
- if (!isValid) {
137
+ if (!isAuthenticated && auth.rigoToken) {
126
138
  setAuth({
127
139
  ...auth,
128
140
  rigoToken: "",
@@ -134,19 +146,22 @@ const SyllabusEditor: React.FC = () => {
134
146
 
135
147
  const res = await publicInteractiveCreation(
136
148
  {
137
- courseInfo: JSON.stringify(syllabus.courseInfo),
149
+ courseInfo:
150
+ JSON.stringify(syllabus.courseInfo) +
151
+ `\nThe following technologies are available, choose up to 3 from the following list: <techs>${technologies
152
+ .filter((t) => t.lang === syllabus.courseInfo.language)
153
+ .map((t) => t.slug)
154
+ .join(", ")}</techs>`,
138
155
  prevInteractions:
139
156
  messages
140
157
  .map((message) => `${message.type}: ${message.content}`)
141
158
  .join("\n") + `\nUSER: ${prompt}`,
142
159
  },
143
- auth.rigoToken && isValid ? auth.rigoToken : auth.publicToken,
160
+ auth.rigoToken && isAuthenticated ? auth.rigoToken : auth.publicToken,
144
161
  syllabus?.courseInfo?.purpose || "learnpack-lesson-writer",
145
- auth.rigoToken && isValid ? false : true
162
+ auth.rigoToken && isAuthenticated ? false : true
146
163
  )
147
164
 
148
- console.log(res, "RES from rigobot")
149
-
150
165
  const lessons: Lesson[] = res.parsed.listOfSteps.map((step: any) =>
151
166
  parseLesson(step, syllabus.lessons)
152
167
  )
@@ -441,3 +441,8 @@ export const fixTitleLength = (title: string) => {
441
441
  fixed = fixed.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
442
442
  return fixed
443
443
  }
444
+
445
+ export const getTechnologies = async () => {
446
+ const response = await axios.get(`/technologies`)
447
+ return response.data
448
+ }
@@ -36,6 +36,11 @@ type Consumables = {
36
36
  [key: string]: number
37
37
  }
38
38
 
39
+ type TTechnology = {
40
+ slug: string
41
+ lang: string
42
+ }
43
+
39
44
  type Store = {
40
45
  auth: Auth
41
46
  formState: FormState
@@ -51,6 +56,8 @@ type Store = {
51
56
  setMessages: (messages: TMessage[]) => void
52
57
  cleanHistory: () => void
53
58
  history: Syllabus[]
59
+ technologies: TTechnology[]
60
+ setTechnologies: (technologies: TTechnology[]) => void
54
61
  undo: () => void
55
62
  push: (syllabus: Syllabus) => void
56
63
  cleanAll: () => void
@@ -93,6 +100,8 @@ const useStore = create<Store>()(
93
100
  ],
94
101
  },
95
102
  messages: [],
103
+ technologies: [],
104
+ setTechnologies: (technologies: TTechnology[]) => set({ technologies }),
96
105
  setMessages: (messages: TMessage[]) => set({ messages }),
97
106
  setFormState: (formState: Partial<FormState>) =>
98
107
  set((state) => ({ formState: { ...state.formState, ...formState } })),
@@ -145,6 +154,7 @@ const useStore = create<Store>()(
145
154
  uploadedFiles: [],
146
155
  messages: [],
147
156
  history: [],
157
+ technologies: [],
148
158
  formState: {
149
159
  description: "",
150
160
  duration: 0,