@learnpack/learnpack 5.0.100 → 5.0.108

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,8 +10,8 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-mdqn3dGF.js"></script>
14
- <link rel="stylesheet" crossorigin href="/creator/assets/index-fdwyhdDD.css">
13
+ <script type="module" crossorigin src="/creator/assets/index-naiPvmWT.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/creator/assets/index-Cvdi97GX.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"5.0.100","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
1
+ {"version":"5.0.108","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
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.100",
4
+ "version": "5.0.108",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -28,6 +28,7 @@
28
28
  "@oclif/plugin-plugins": "^1.8.0",
29
29
  "@oclif/plugin-warn-if-update-available": "^1.7.0",
30
30
  "@types/archiver": "^6.0.2",
31
+ "@types/html-to-text": "^9.0.4",
31
32
  "archiver": "^7.0.1",
32
33
  "axios": "^1.7.7",
33
34
  "body-parser": "^1.19.0",
@@ -45,6 +46,7 @@
45
46
  "express": "^4.17.1",
46
47
  "form-data": "^4.0.2",
47
48
  "front-matter": "^4.0.2",
49
+ "html-to-text": "^9.0.5",
48
50
  "js-yaml": "^4.1.0",
49
51
  "markdown-it": "^14.1.0",
50
52
  "mkdirp": "^3.0.1",
@@ -62,7 +64,9 @@
62
64
  "text-readability": "^1.1.0",
63
65
  "tslib": "^1",
64
66
  "validator": "^13.1.1",
65
- "xxhashjs": "^0.2.2"
67
+ "xxhashjs": "^0.2.2",
68
+ "youtube-transcript": "^1.2.1",
69
+ "ytdl-core": "^4.11.5"
66
70
  },
67
71
  "devDependencies": {
68
72
  "@oclif/dev-cli": "^1.22.2",
@@ -1,10 +1,13 @@
1
1
  import { flags } from "@oclif/command"
2
+ // import * as ytdl from "ytdl-core"
3
+ import { YoutubeTranscript } from "youtube-transcript"
2
4
  import * as express from "express"
3
5
  import * as cors from "cors"
4
6
  import * as path from "path"
5
7
  import * as os from "os"
6
8
  import * as archiver from "archiver"
7
9
  import * as mkdirp from "mkdirp"
10
+ import { convert } from "html-to-text"
8
11
  import * as rimraf from "rimraf"
9
12
  import SessionCommand from "../utils/SessionCommand"
10
13
  import { Storage } from "@google-cloud/storage"
@@ -37,6 +40,12 @@ const fixPreviewUrl = (slug: string, previewUrl: string) => {
37
40
  return expectedUrl
38
41
  }
39
42
 
43
+ const getTitleFromHTML = (html: string) => {
44
+ const titleRegex = /<title>(.*?)<\/title>/
45
+ const titleMatch = html.match(titleRegex)
46
+ return titleMatch ? titleMatch[1] : null
47
+ }
48
+
40
49
  export default class ServeCommand extends SessionCommand {
41
50
  static description = "Runs a small server to build tutorials"
42
51
 
@@ -558,6 +567,54 @@ export default class ServeCommand extends SessionCommand {
558
567
  }
559
568
  })
560
569
 
570
+ const YT_REGEX =
571
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/
572
+
573
+ app.get("/actions/fetch/:link", async (req, res) => {
574
+ const { link } = req.params
575
+ try {
576
+ // 1) Decode the URL
577
+ const decoded = Buffer.from(link, "base64url").toString("utf-8")
578
+ const ytMatch = decoded.match(YT_REGEX)
579
+
580
+ if (ytMatch) {
581
+ const videoId = ytMatch[1]
582
+ // fetch metadata
583
+ const items = await YoutubeTranscript.fetchTranscript(videoId)
584
+ const transcript = items.map(i => i.text).join(" ")
585
+
586
+ const { data: meta } = await axios.get(
587
+ "https://www.youtube.com/oembed",
588
+ {
589
+ params: { url: decoded, format: "json" },
590
+ }
591
+ )
592
+
593
+ return res.json({
594
+ url: decoded,
595
+ title: meta.title,
596
+ author: meta.author_name,
597
+ thumbnail: meta.thumbnail_url,
598
+ transcript,
599
+ })
600
+ }
601
+
602
+ const response = await axios.get(decoded, { responseType: "text" })
603
+ const html = response.data as string
604
+ const title = getTitleFromHTML(html)
605
+ console.log("TITLE", title)
606
+
607
+ const text = convert(html)
608
+ return res.json({
609
+ url: decoded,
610
+ text,
611
+ title,
612
+ })
613
+ } catch (error: any) {
614
+ console.error("❌ /actions/fetch error:", error.message || error)
615
+ res.status(500).json({ error: error.message || "Failed to fetch link" })
616
+ }
617
+ })
561
618
  app.delete("/packages/:slug", async (req, res) => {
562
619
  console.log("DELETE /packages/:slug")
563
620
 
@@ -24,6 +24,7 @@
24
24
  "react-markdown": "^10.1.0",
25
25
  "react-router": "^7.5.0",
26
26
  "syllable": "^5.0.1",
27
+ "youtube-transcript": "^1.2.1",
27
28
  "zustand": "^5.0.3"
28
29
  },
29
30
  "devDependencies": {
@@ -6277,6 +6278,15 @@
6277
6278
  "url": "https://github.com/sponsors/sindresorhus"
6278
6279
  }
6279
6280
  },
6281
+ "node_modules/youtube-transcript": {
6282
+ "version": "1.2.1",
6283
+ "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.2.1.tgz",
6284
+ "integrity": "sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q==",
6285
+ "license": "MIT",
6286
+ "engines": {
6287
+ "node": ">=18.0.0"
6288
+ }
6289
+ },
6280
6290
  "node_modules/zustand": {
6281
6291
  "version": "5.0.3",
6282
6292
  "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
@@ -26,6 +26,7 @@
26
26
  "react-markdown": "^10.1.0",
27
27
  "react-router": "^7.5.0",
28
28
  "syllable": "^5.0.1",
29
+ "youtube-transcript": "^1.2.1",
29
30
  "zustand": "^5.0.3"
30
31
  },
31
32
  "devDependencies": {
@@ -9,6 +9,8 @@ import useStore from "./utils/store"
9
9
  import { interactiveCreation } from "./utils/rigo"
10
10
  import { checkParams, loginWithToken, parseLesson } from "./utils/lib"
11
11
  import FileUploader from "./components/FileUploader"
12
+ // import LinkUploader from "./components/LinkUploader"
13
+ import Source from "./components/Source"
12
14
 
13
15
  // const exampleContentIndex = `-Introduction to AI: Explain what is AI and its applications
14
16
  // -Introduction to Machine Learning: Explain What is machine learning and its aplications
@@ -116,21 +118,7 @@ function App() {
116
118
  />
117
119
  ),
118
120
  },
119
- // {
120
- // title:
121
- // "First you need to login with 4Geeks.com to use AI Generation tool for creators. ",
122
- // slug: "login",
123
- // isCompleted: false,
124
- // content: (
125
- // <Login
126
- // onFinish={() => {
127
- // setFormState({
128
- // currentStep: "duration",
129
- // })
130
- // }}
131
- // />
132
- // ),
133
- // },
121
+
134
122
  {
135
123
  title: "What is the estimated duration for this tutorial?",
136
124
  slug: "duration",
@@ -143,7 +131,7 @@ function App() {
143
131
  onClick={() => {
144
132
  setFormState({
145
133
  duration: 30,
146
- currentStep: "targetAudience",
134
+ currentStep: "hasContentIndex",
147
135
  })
148
136
  }}
149
137
  selected={formState.duration === 30}
@@ -154,7 +142,7 @@ function App() {
154
142
  onClick={() => {
155
143
  setFormState({
156
144
  duration: 60,
157
- currentStep: "targetAudience",
145
+ currentStep: "hasContentIndex",
158
146
  })
159
147
  }}
160
148
  selected={formState.duration === 60}
@@ -165,7 +153,7 @@ function App() {
165
153
  onClick={() => {
166
154
  setFormState({
167
155
  duration: 120,
168
- currentStep: "targetAudience",
156
+ currentStep: "hasContentIndex",
169
157
  })
170
158
  }}
171
159
  selected={formState.duration === 120}
@@ -173,25 +161,6 @@ function App() {
173
161
  </div>
174
162
  ),
175
163
  },
176
- {
177
- title: "What is the target audience for this tutorial?",
178
- slug: "targetAudience",
179
- isCompleted: false,
180
- content: (
181
- <div className="flex flex-row gap-4">
182
- <textarea
183
- placeholder="Describe the target audience for this tutorial"
184
- className="w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white"
185
- defaultValue={formState.targetAudience}
186
- onBlur={(e) => {
187
- setFormState({
188
- targetAudience: e.target.value,
189
- })
190
- }}
191
- />
192
- </div>
193
- ),
194
- },
195
164
  {
196
165
  title: "Do you have a content index for this tutorial?",
197
166
  slug: "hasContentIndex",
@@ -205,6 +174,7 @@ function App() {
205
174
  hasContentIndex: true,
206
175
  currentStep: "contentIndex",
207
176
  variables: [...formState.variables, "contentIndex"],
177
+ // variables: [...formState.variables.filter((v) => v !== "hasContentIndex"), "contentIndex"],
208
178
  })
209
179
  }}
210
180
  selected={false}
@@ -224,7 +194,7 @@ function App() {
224
194
  },
225
195
  {
226
196
  title:
227
- "Write or paste your content index below, each topic should be defined on a new line, here is an example:",
197
+ "Any materials to get this course started?",
228
198
  slug: "contentIndex",
229
199
  isCompleted: false,
230
200
  content: (
@@ -240,6 +210,18 @@ function App() {
240
210
  })
241
211
  }}
242
212
  />
213
+ <ul className="space-y-3">
214
+ {formState.sources?.map((it, i) => (
215
+ <Source key={i} source={it} />
216
+ ))}
217
+ </ul>
218
+ {/* <LinkUploader
219
+ onResult={(links) => {
220
+ setFormState({
221
+ sources: [...formState.sources, ...links],
222
+ })
223
+ }}
224
+ /> */}
243
225
  <FileUploader
244
226
  onResult={(files) => {
245
227
  // toast.success("File uploaded successfully")
@@ -277,16 +259,21 @@ function App() {
277
259
  }
278
260
  />
279
261
  ) : (
280
- <StepWizard
281
- formState={formState}
282
- steps={buildSteps()}
283
- setFormState={setFormState}
284
- onFinish={() => {
285
- setFormState({
286
- isCompleted: true,
287
- })
288
- }}
289
- />
262
+ <>
263
+ {/* <div className="flex flex-col gap-4">
264
+ <h1 className="text-2xl font-bold">{formState.sources?.length}</h1>
265
+ </div> */}
266
+ <StepWizard
267
+ formState={formState}
268
+ steps={buildSteps()}
269
+ setFormState={setFormState}
270
+ onFinish={() => {
271
+ setFormState({
272
+ isCompleted: true,
273
+ })
274
+ }}
275
+ />
276
+ </>
290
277
  )}
291
278
  </>
292
279
  )
@@ -281,4 +281,18 @@ export const SVGS = {
281
281
  />
282
282
  </svg>
283
283
  ),
284
+ redClose: (
285
+ <svg
286
+ width="10"
287
+ height="10"
288
+ viewBox="0 0 10 11"
289
+ fill="none"
290
+ xmlns="http://www.w3.org/2000/svg"
291
+ >
292
+ <path
293
+ d="M5.875 5.19922L9.8125 1.26172C10.0625 1.01172 10.0625 0.636719 9.8125 0.386719C9.5625 0.136719 9.1875 0.136719 8.9375 0.386719L5 4.32422L1.0625 0.386719C0.8125 0.136719 0.4375 0.136719 0.1875 0.386719C-0.0624999 0.636719 -0.0624999 1.01172 0.1875 1.26172L4.125 5.19922L0.1875 9.13672C0.0625001 9.26172 0 9.38672 0 9.57422C0 9.94922 0.25 10.1992 0.625 10.1992C0.8125 10.1992 0.9375 10.1367 1.0625 10.0117L5 6.07422L8.9375 10.0117C9.0625 10.1367 9.1875 10.1992 9.375 10.1992C9.5625 10.1992 9.6875 10.1367 9.8125 10.0117C10.0625 9.76172 10.0625 9.38672 9.8125 9.13672L5.875 5.19922Z"
294
+ fill="#EB5757"
295
+ />
296
+ </svg>
297
+ ),
284
298
  }
@@ -0,0 +1,109 @@
1
+ import React, { useState } from "react"
2
+
3
+ export interface ParsedLink {
4
+ url: string
5
+ title?: string
6
+ text?: string
7
+ transcript?: string
8
+ description?: string
9
+ author?: string
10
+ duration?: number
11
+ thumbnail?: string
12
+ }
13
+
14
+ interface LinkUploaderProps {
15
+ onResult: (links: ParsedLink[]) => void
16
+ apiBase?: string
17
+ }
18
+
19
+ const toBase64Url = (str: string) =>
20
+ btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
21
+
22
+ const LinkUploader: React.FC<LinkUploaderProps> = ({
23
+ onResult,
24
+ apiBase = "http://localhost:3000",
25
+ }) => {
26
+ const [url, setUrl] = useState("")
27
+ const [loading, setLoading] = useState(false)
28
+ const [error, setError] = useState<string | null>(null)
29
+
30
+ const handleAdd = async () => {
31
+ const raw = url.trim()
32
+ if (!raw) return setError("Please enter a URL.")
33
+ setError(null)
34
+ setLoading(true)
35
+
36
+ try {
37
+ const key = toBase64Url(raw)
38
+ const resp = await fetch(`${apiBase}/actions/fetch/${key}`)
39
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
40
+ const data = (await resp.json()) as ParsedLink
41
+
42
+ onResult([data]) // emit the parsed link immediately
43
+ setUrl("") // clear input
44
+ } catch (err: any) {
45
+ console.error(err)
46
+ setError(err.message || "Failed to fetch link.")
47
+ } finally {
48
+ setLoading(false)
49
+ }
50
+ }
51
+
52
+ return (
53
+ <div className="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg">
54
+ <div className="flex space-x-2">
55
+ <input
56
+ type="url"
57
+ value={url}
58
+ onChange={(e) => setUrl(e.target.value)}
59
+ onKeyDown={(e) => e.key === "Enter" && handleAdd()}
60
+ disabled={loading}
61
+ placeholder="Paste your link here…"
62
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 transition disabled:bg-gray-100"
63
+ />
64
+ <button
65
+ onClick={handleAdd}
66
+ disabled={loading}
67
+ className={`w-12 h-12 flex items-center justify-center rounded-lg text-white transition ${
68
+ loading
69
+ ? "bg-gray-400 cursor-not-allowed"
70
+ : "bg-learnpack hover:bg-opacity-90"
71
+ }`}
72
+ >
73
+ {loading ? (
74
+ <svg
75
+ className="animate-spin h-5 w-5 text-white"
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ fill="none"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ <circle
81
+ className="opacity-25"
82
+ cx="12"
83
+ cy="12"
84
+ r="10"
85
+ stroke="currentColor"
86
+ strokeWidth="4"
87
+ />
88
+ <path
89
+ className="opacity-75"
90
+ fill="currentColor"
91
+ d="M4 12a8 8 0 018-8v4l3.5-3.5L12 0v4a8 8 0 100 16v-4l-3.5 3.5L4 12z"
92
+ />
93
+ </svg>
94
+ ) : (
95
+ <span className="text-2xl leading-none">+</span>
96
+ )}
97
+ </button>
98
+ </div>
99
+
100
+ {error && (
101
+ <p className="mt-3 text-sm text-red-600 bg-red-50 p-2 rounded">
102
+ {error}
103
+ </p>
104
+ )}
105
+ </div>
106
+ )
107
+ }
108
+
109
+ export default LinkUploader
@@ -0,0 +1,68 @@
1
+ import React from "react"
2
+ import { ParsedLink } from "./LinkUploader"
3
+ import useStore from "../utils/store"
4
+ import { SVGS } from "../assets/svgs"
5
+
6
+ interface SourceProps {
7
+ source: ParsedLink
8
+ }
9
+
10
+ // const formatDuration = (sec: number) => {
11
+ // const m = Math.floor(sec / 60)
12
+ // const s = sec % 60
13
+ // return `${m}:${s.toString().padStart(2, "0")}`
14
+ // }
15
+
16
+ const Source: React.FC<SourceProps> = ({ source }) => {
17
+ const setFormState = useStore((state) => state.setFormState)
18
+ const formState = useStore((state) => state.formState)
19
+
20
+ const {
21
+ url,
22
+ title,
23
+ // author,
24
+ thumbnail,
25
+ // duration,
26
+ // description,
27
+ // transcript,
28
+ // text,
29
+ } = source
30
+
31
+
32
+
33
+ return (
34
+ <div className="relative max-w-xs bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow">
35
+ <div className="p-4 flex flex-col space-y-2">
36
+ <button
37
+ className="absolute top-2 right-2"
38
+ onClick={() => {
39
+ setFormState({
40
+ sources: formState.sources.filter((s) => s !== source),
41
+ })
42
+ }}
43
+ >
44
+ {SVGS.redClose}
45
+ </button>
46
+ <div className="flex items-center gap-2 justify-start">
47
+ {thumbnail && (
48
+ <img
49
+ src={thumbnail}
50
+ alt={title || url}
51
+ className="w-10 h-10 object-cover"
52
+ />
53
+ )}
54
+ {title && (
55
+ <span
56
+ className="text-sm font-semibold text-left text-gray-800 "
57
+ title={title}
58
+ >
59
+ {title}
60
+ </span>
61
+ )}
62
+ </div>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ export default Source
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from "react"
1
+ import { useEffect, useRef, useState } from "react"
2
2
  import useStore from "../../utils/store"
3
3
  import { TMessage } from "../Message"
4
4
  import FileUploader from "../FileUploader"
@@ -15,12 +15,26 @@ export const Sidebar = ({
15
15
  sendPrompt: (prompt: string) => void
16
16
  handleSubmit: () => void
17
17
  }) => {
18
+ const sidebarRef = useRef<HTMLDivElement>(null)
18
19
  const inputRef = useRef<HTMLTextAreaElement>(null)
19
20
  const uploadedFiles = useStore((state) => state.uploadedFiles)
20
21
  const setUploadedFiles = useStore((state) => state.setUploadedFiles)
21
22
 
22
23
  const [isOpen, setIsOpen] = useState(false)
23
24
 
25
+ useEffect(() => {
26
+ const handleClickOutside = (e: MouseEvent) => {
27
+ if (
28
+ isOpen &&
29
+ sidebarRef.current &&
30
+ !sidebarRef.current.contains(e.target as Node)
31
+ ) {
32
+ setIsOpen(false)
33
+ }
34
+ }
35
+ document.addEventListener("mousedown", handleClickOutside)
36
+ return () => document.removeEventListener("mousedown", handleClickOutside)
37
+ }, [isOpen])
24
38
  return (
25
39
  <>
26
40
  {!isOpen && (
@@ -33,6 +47,7 @@ export const Sidebar = ({
33
47
  )}
34
48
 
35
49
  <div
50
+ ref={sidebarRef}
36
51
  className={`fixed z-40 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
37
52
  isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
38
53
  }`}
@@ -81,16 +96,16 @@ export const Sidebar = ({
81
96
  onKeyUp={(e) => {
82
97
  if (e.key === "Enter" && !e.shiftKey) {
83
98
  e.preventDefault()
84
- sendPrompt(inputRef.current?.value || "")
99
+ const val = inputRef.current?.value.trim() || ""
100
+
101
+ if (val.toLowerCase() === "ok") {
102
+ handleSubmit()
103
+ } else {
104
+ sendPrompt(val)
105
+ }
106
+
85
107
  inputRef.current!.value = ""
86
108
  }
87
- if (
88
- e.key.toLowerCase() === "k" &&
89
- inputRef.current?.value.toLowerCase().trim() === "ok"
90
- ) {
91
- e.preventDefault()
92
- handleSubmit()
93
- }
94
109
  }}
95
110
  />
96
111
  <div className="absolute bottom-2 right-2 flex gap-1 items-center">
@@ -104,7 +119,17 @@ export const Sidebar = ({
104
119
 
105
120
  <button
106
121
  className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
107
- onClick={() => sendPrompt(inputRef.current?.value || "")}
122
+ onClick={() => {
123
+ if (inputRef.current?.value) {
124
+ const val = inputRef.current?.value.trim() || ""
125
+ if (val.toLowerCase() === "ok") {
126
+ handleSubmit()
127
+ } else {
128
+ sendPrompt(val)
129
+ }
130
+ inputRef.current!.value = ""
131
+ }
132
+ }}
108
133
  >
109
134
  {SVGS.send}
110
135
  </button>
@@ -44,6 +44,10 @@ h1 {
44
44
  background-color: var(--soft-blue);
45
45
  }
46
46
 
47
+ .bg-learnpack {
48
+ background-color: var(--learnpack-blue);
49
+ }
50
+
47
51
  .bg-gray-blue {
48
52
  background-color: #e7f1ff;
49
53
  }