computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
6.0 KiB
185 lines
6.0 KiB
import fs from 'node:fs/promises'; |
|
import path from 'node:path'; |
|
import { fileURLToPath } from 'node:url'; |
|
import type { Edge, Node } from 'reactflow'; |
|
import matter from 'gray-matter'; |
|
import type { RoadmapFrontmatter } from '../src/lib/roadmap'; |
|
import { slugify } from '../src/lib/slugger'; |
|
import OpenAI from 'openai'; |
|
import { runPromisesInBatchSequentially } from '../src/lib/promise'; |
|
|
|
// ERROR: `__dirname` is not defined in ES module scope |
|
// https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d |
|
const __filename = fileURLToPath(import.meta.url); |
|
const __dirname = path.dirname(__filename); |
|
|
|
// Usage: tsx ./scripts/editor-roadmap-content.ts <roadmapId> |
|
const OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY; |
|
console.log('OPEN_AI_API_KEY:', OPEN_AI_API_KEY); |
|
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps'); |
|
const roadmapId = process.argv[2]; |
|
|
|
const allowedRoadmapIds = await fs.readdir(ROADMAP_CONTENT_DIR); |
|
if (!roadmapId) { |
|
console.error('Roadmap Id is required'); |
|
process.exit(1); |
|
} |
|
|
|
if (!allowedRoadmapIds.includes(roadmapId)) { |
|
console.error(`Invalid roadmap key ${roadmapId}`); |
|
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`); |
|
process.exit(1); |
|
} |
|
|
|
const roadmapFrontmatterDir = path.join( |
|
ROADMAP_CONTENT_DIR, |
|
roadmapId, |
|
`${roadmapId}.md`, |
|
); |
|
const roadmapFrontmatterRaw = await fs.readFile(roadmapFrontmatterDir, 'utf-8'); |
|
const { data } = matter(roadmapFrontmatterRaw); |
|
|
|
const roadmapFrontmatter = data as RoadmapFrontmatter; |
|
if (!roadmapFrontmatter) { |
|
console.error('Invalid roadmap frontmatter'); |
|
process.exit(1); |
|
} |
|
|
|
if (roadmapFrontmatter.renderer !== 'editor') { |
|
console.error('Only Editor Rendered Roadmaps are allowed'); |
|
process.exit(1); |
|
} |
|
|
|
const roadmapDir = path.join( |
|
ROADMAP_CONTENT_DIR, |
|
roadmapId, |
|
`${roadmapId}.json`, |
|
); |
|
const roadmapContent = await fs.readFile(roadmapDir, 'utf-8'); |
|
let { nodes, edges } = JSON.parse(roadmapContent) as { |
|
nodes: Node[]; |
|
edges: Edge[]; |
|
}; |
|
const enrichedNodes = nodes |
|
.filter( |
|
(node) => |
|
node?.type && |
|
['topic', 'subtopic'].includes(node.type) && |
|
node.data?.label, |
|
) |
|
.map((node) => { |
|
// Because we only need the parent id and title for subtopics |
|
if (node.type !== 'subtopic') { |
|
return node; |
|
} |
|
|
|
const parentNodeId = |
|
edges.find((edge) => edge.target === node.id)?.source || ''; |
|
const parentNode = nodes.find((n) => n.id === parentNodeId); |
|
|
|
return { |
|
...node, |
|
parentId: parentNodeId, |
|
parentTitle: parentNode?.data?.label || '', |
|
}; |
|
}) as (Node & { parentId?: string; parentTitle?: string })[]; |
|
|
|
const roadmapContentDir = path.join(ROADMAP_CONTENT_DIR, roadmapId, 'content'); |
|
const stats = await fs.stat(roadmapContentDir).catch(() => null); |
|
if (!stats || !stats.isDirectory()) { |
|
await fs.mkdir(roadmapContentDir, { recursive: true }); |
|
} |
|
|
|
let openai: OpenAI | undefined; |
|
if (OPEN_AI_API_KEY) { |
|
openai = new OpenAI({ |
|
apiKey: OPEN_AI_API_KEY, |
|
}); |
|
} |
|
|
|
function writeTopicContent( |
|
roadmapTitle: string, |
|
childTopic: string, |
|
parentTopic?: string, |
|
) { |
|
let prompt = `I will give you a topic and you need to write a brief introduction for that with regards to "${roadmapTitle}". Your format should be as follows and be in strictly markdown format: |
|
|
|
# (Put a heading for the topic without adding parent "Subtopic in Topic" or "Topic in Roadmap" or "Subtopic under XYZ" etc.) |
|
|
|
(Briefly explain the topic in one paragraph using simple english with regards to "${roadmapTitle}". Don't start with explaining how important the topic is with regard to "${roadmapTitle}". Don't say something along the lines of "XYZ plays a crucial role in ${roadmapTitle}". Don't include anything saying "In the context of ${roadmapTitle}". Instead, start with a simple explanation of the topic itself. For example, if the topic is "React", you can start with "React is a JavaScript library for building user interfaces." and then you can explain how it is used in "${roadmapTitle}".) |
|
`; |
|
|
|
if (!parentTopic) { |
|
prompt += `First topic is: ${childTopic}`; |
|
} else { |
|
prompt += `First topic is: ${childTopic} under ${parentTopic}`; |
|
} |
|
|
|
return new Promise((resolve, reject) => { |
|
openai?.chat.completions |
|
.create({ |
|
model: 'gpt-4', |
|
messages: [ |
|
{ |
|
role: 'user', |
|
content: prompt, |
|
}, |
|
], |
|
}) |
|
.then((response) => { |
|
const article = response.choices[0].message.content; |
|
|
|
resolve(article); |
|
}) |
|
.catch((err) => { |
|
reject(err); |
|
}); |
|
}); |
|
} |
|
|
|
async function writeNodeContent(node: Node & { parentTitle?: string }) { |
|
const nodeDirPattern = `${slugify(node.data.label)}@${node.id}.md`; |
|
if (!roadmapContentFiles.includes(nodeDirPattern)) { |
|
console.log(`Missing file for: ${nodeDirPattern}`); |
|
return; |
|
} |
|
|
|
const nodeDir = path.join(roadmapContentDir, nodeDirPattern); |
|
const nodeContent = await fs.readFile(nodeDir, 'utf-8'); |
|
const isFileEmpty = !nodeContent.replace(`# ${node.data.label}`, '').trim(); |
|
if (!isFileEmpty) { |
|
console.log(`❌ Ignoring ${nodeDirPattern}. Not empty.`); |
|
return; |
|
} |
|
|
|
const topic = node.data.label; |
|
const parentTopic = node.parentTitle; |
|
|
|
console.log(`⏳ Generating content for ${topic}...`); |
|
let newContentFile = ''; |
|
if (OPEN_AI_API_KEY) { |
|
newContentFile = (await writeTopicContent( |
|
roadmapFrontmatter.title, |
|
topic, |
|
parentTopic, |
|
)) as string; |
|
} else { |
|
newContentFile = `# ${topic}`; |
|
} |
|
|
|
await fs.writeFile(nodeDir, newContentFile, 'utf-8'); |
|
console.log(`✅ Content generated for ${topic}`); |
|
} |
|
|
|
let roadmapContentFiles = await fs.readdir(roadmapContentDir, { |
|
recursive: true, |
|
}); |
|
|
|
if (!OPEN_AI_API_KEY) { |
|
console.log('----------------------------------------'); |
|
console.log('OPEN_AI_API_KEY not found. Skipping openai api calls...'); |
|
console.log('----------------------------------------'); |
|
} |
|
const promises = enrichedNodes.map((node) => () => writeNodeContent(node)); |
|
await runPromisesInBatchSequentially(promises, 20); |
|
console.log('✅ All content generated');
|
|
|