{"id":21433133,"url":"https://github.com/llegomark/betterreadings","last_synced_at":"2025-08-01T20:43:12.906Z","repository":{"id":100585481,"uuid":"600325778","full_name":"llegomark/betterreadings","owner":"llegomark","description":"Improve your child's reading skills with personalized reading passages. Our website offers a wide range of topics and reading levels, making reading both fun and educational. Start your child's reading journey today and give them the tools they need to succeed.","archived":false,"fork":false,"pushed_at":"2023-10-07T00:05:38.000Z","size":1785,"stargazers_count":11,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-25T16:26:18.411Z","etag":null,"topics":["ai","artificial-intelligence","javascript","machine-learning","machine-learning-algorithms","nextjs","nodejs","openai","react","reactjs","typescript"],"latest_commit_sha":null,"homepage":"https://chat.llego.dev/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/llegomark.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-02-11T06:08:09.000Z","updated_at":"2023-11-01T10:31:50.000Z","dependencies_parsed_at":"2025-07-14T13:30:52.602Z","dependency_job_id":"f60d613d-cc60-4d31-b376-7ade4335f460","html_url":"https://github.com/llegomark/betterreadings","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/llegomark/betterreadings","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/llegomark%2Fbetterreadings","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/llegomark%2Fbetterreadings/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/llegomark%2Fbetterreadings/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/llegomark%2Fbetterreadings/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/llegomark","download_url":"https://codeload.github.com/llegomark/betterreadings/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/llegomark%2Fbetterreadings/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268294157,"owners_count":24227629,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-01T02:00:08.611Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","artificial-intelligence","javascript","machine-learning","machine-learning-algorithms","nextjs","nodejs","openai","react","reactjs","typescript"],"created_at":"2024-11-22T23:25:13.361Z","updated_at":"2025-08-01T20:43:12.878Z","avatar_url":"https://github.com/llegomark.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Better Readings\n\nWelcome to Better Readings, the revolutionary website designed to help students improve their reading skills, comprehension, and confidence. Better Readings provides teachers and educators with a platform that delivers custom reading passages tailored to each student's grade level. Our high-quality, engaging content is created by experienced educators who understand the needs of students and the importance of personalized learning.\n\nBetter Readings recognizes that every student is unique, and therefore, we strive to deliver personalized content that caters to their individual needs. Our custom reading passages are designed to challenge and inspire students, without overwhelming them. This approach keeps students engaged and motivated throughout their learning experience.\n\nOur website is easy to navigate, with a user-friendly interface that makes it accessible for teachers who may not be familiar with technology. Teachers can generate reading passages that are appropriate for their students' level, track their students' progress, and adjust the content accordingly. This helps teachers to identify areas of strength and weakness and to adjust their teaching strategies to achieve the best possible outcomes.\n\nBetter Readings is committed to delivering a high-quality learning experience for students. Our content is developed with a focus on high-interest topics that students will find engaging and relevant. By doing so, we aim to instill a love of reading and encourage students to continue to read throughout their lives.\n\nIn conclusion, Better Readings is an effective and innovative tool for teachers and educators looking to help their students improve their reading skills, comprehension, and confidence. Our custom reading passages, personalized to each student's grade level, deliver high-quality content in an engaging and accessible format.\n\n## Generating Reading Passages using OpenAI API\n\nTo generate reading passages using OpenAI API, the website makes use of a few key files: Generate.ts, Reading.ts, and Middleware.ts.\n\n### Generate.ts\n\nThe Generate.ts file contains the code for generating reading passages using OpenAI API. The code is written in TypeScript.\n\nFirst, the file defines a list of OpenAI API keys. If additional API keys are needed, they can be added to the list. The code then checks if all API keys are available, and if any are missing, an error is thrown.\n\n```\nconst API_KEYS: string[] = [\n  process.env.OPENAI_API_KEY1 as string,\n  process.env.OPENAI_API_KEY2 as string,\n  process.env.OPENAI_API_KEY3 as string,\n  // add more API keys as necessary\n];\n\nif (!API_KEYS.every(Boolean)) {\n  throw new Error(\"Missing env var(s) from OpenAI\");\n}\n```\n\nNext, the interface for the request body is defined. The request body must contain a prompt string for generating the reading passage.\n\n```\ninterface RequestBody {\n  prompt: string;\n}\n```\n\nThe configuration object for the Next.js API route is defined next. It specifies that the code should run on the \"edge\" runtime.\n\n```\nexport const config = {\n  runtime: \"edge\",\n};\n```\n\nThe main function in Generate.ts is an async function that handles the Next.js API request. The prompt is obtained from the request body, and if it is not provided, an error response is returned.\n```\nconst handler = async (req: NextRequest): Promise\u003cResponse\u003e =\u003e {\n  const { prompt } = (await req.json()) as RequestBody;\n\n  if (!prompt) {\n    return new Response(\"No prompt in the request\", {\n      status: 400,\n      statusText: \"Bad Request\",\n    });\n  }\n  ```\n\nThe GPT3Tokenizer is used to count the number of tokens in the prompt, and the maximum number of tokens allowed for the prompt is set to 400. If the prompt exceeds the maximum number of tokens, an error response is returned.\n```\nconst tokenizer = new gpt3Tokenizer({ type: 'gpt3' });\nconst tokens = tokenizer.encode(prompt);\nconst numTokens = tokens.bpe.length;\nconst MAX_PROMPT_TOKENS = 400;\n\nif (numTokens \u003e MAX_PROMPT_TOKENS) {\n  return new Response(`The prompt has ${numTokens} tokens, which exceeds the maximum limit of ${MAX_PROMPT_TOKENS} tokens.`, {\n    status: 400,\n    statusText: \"Bad Request\",\n  });\n}\n```\nOne of the API keys is randomly selected, and the API parameters for the OpenAI text completion request are set. The function then calls the OpenAI API to stream the generated text. If there is an error in the process, an error response is returned.\n```\n  try {\n    // Call the OpenAI API to stream the generated text\n    const stream = await OpenAIStream(payload, apiKey);\n    // Return the response from the OpenAI stream as the response to the API request\n    return new Response(stream);\n  } catch (e) {\n    console.error(e);\n    // If there is any error in the process, return an error response\n    return new Response(\"Failed to fetch stream from OpenAI\", {\n      status: 500,\n      statusText: \"Internal Server Error\",\n    });\n  }\n```\n### Reading.ts\nThe Reading.ts file contains the function OpenAIStream that is called from Generate.ts to handle the streaming of data from the OpenAI API. The function takes in the payload object and an API key, and returns a readable stream that will continuously receive data from the OpenAI API until it returns the \"[DONE]\" message.\n\nThe function first creates a text encoder and decoder, and sets a counter to zero.\n```\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nlet counter = 0;\n```\nThe function then sends a POST request to the OpenAI API with the given payload and API key. If the response is not successful, an error is thrown.\n```\n    const res = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${apiKey}`,\n      },\n      method: \"POST\",\n      body: JSON.stringify(payload),\n    });\n\n    // Throw an error if the response is not successful\n    if (!res.ok) {\n      throw new Error(`Failed to fetch stream from OpenAI: ${res.statusText}`);\n    }\n```\nA ReadableStream object is then created that will continuously receive data from the response.\n```\nconst stream = new ReadableStream({\n  async start(controller) {\n    // Define a callback function to handle incoming data\n    function onParse(event: ParsedEvent | ReconnectInterval) {\n      if (event.type === \"event\") {\n        const data = event.data;\n\n        // Close the stream if the OpenAI API returns the \"[DONE]\" message\n        if (data === \"[DONE]\") {\n          controller.close();\n          return;\n        }\n\n        //... Continue the rest of the code\n      }\n    }\n\n    //... Continue the rest of the code\n  },\n\n  async cancel() {\n    //... Continue the rest of the code\n  },\n});\n\nreturn stream;\n```\nThe start() method of the ReadableStream is an async function that is called when the stream is started. It defines a callback function onParse to handle incoming data. If the data is the \"[DONE]\" message, the stream is closed. If the data contains generated text, the text is extracted and enqueued as encoded binary data in the stream.\n```\n            try {\n              const parsedData = JSON.parse(data) as OpenAIResponse;\n              if (parsedData.choices \u0026\u0026 parsedData.choices.length \u003e 0) {\n                const choice = parsedData.choices[0];\n                if (choice \u0026\u0026 choice.delta?.content) {\n                  text = choice.delta.content;\n                }\n              }\n            } catch (e) {\n              console.error(e);\n            }\n```\nThe cancel() method of the ReadableStream is an async function that is called when the stream is cancelled. It cancels the response if the stream is closed or an error occurs.\n```\nasync cancel() {\n  if (res.body \u0026\u0026 typeof res.body.cancel === \"function\") {\n    try {\n      await res.body.cancel();\n    } catch (e) {\n      console.error(e);\n    }\n  }\n},\n```\n### Middleware.ts\nThe Middleware.ts file contains the middleware function that enforces rate limiting for requests to the \"/api/generate\" path. The function is exported as the default export.\n\nThe middleware function takes in the request and event objects, which represent the incoming HTTP request and Next.js fetch event, respectively. It returns a response or undefined.\n```\nexport default async function middleware(\n  request: NextRequest,\n  event: NextFetchEvent\n): Promise\u003cResponse | undefined\u003e {\n  //... Continue the rest of the code\n}\n```\nThe function first gets the user's IP address from the request, defaulting to \"127.0.0.1\" if not present.\n```\nconst ip = request.ip ?? \"127.0.0.1\";\n```\nIt then uses the ratelimit library to get information about the user's request limit. If the user has not exceeded the request limit, a NextResponse.next() response is returned. Otherwise, the user is redirected to the \"/api/blocked\" endpoint.\n```\nconst ratelimitInfo: RatelimitInfo = await ratelimit.limit(`mw_${ip}`);\nevent.waitUntil(ratelimitInfo.pending);\n\nconst response = ratelimitInfo.success\n  ? NextResponse.next()\n  : NextResponse.redirect(new URL(\"/api/blocked\", request.url), request);\n```\nThe response headers are then set to indicate the rate limit and remaining requests.\n```\nresponse.headers.set(\"X-RateLimit-Limit\", ratelimitInfo.limit.toString());\nresponse.headers.set(\n  \"X-RateLimit-Remaining\",\n  ratelimitInfo.remaining.toString()\n);\nresponse.headers.set(\"X-RateLimit-Reset\", ratelimitInfo.reset.toString());\n```\nFinally, the function returns the response.\n```\nreturn response;\n```\nThe config object is also exported, specifying that this middleware should be applied to requests matching the \"/api/generate\" path.\n```\nexport const config = {\n  matcher: \"/api/generate\",\n};\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fllegomark%2Fbetterreadings","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fllegomark%2Fbetterreadings","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fllegomark%2Fbetterreadings/lists"}